test_advanced_auth_api.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. """Integration tests for Advanced Authentication API endpoints.
  2. Tests the full request/response cycle for SMTP configuration, advanced auth toggle,
  3. email-based login, forgot password, admin password reset, and user creation
  4. with advanced authentication enabled.
  5. """
  6. from unittest.mock import patch
  7. import pytest
  8. from httpx import AsyncClient
  9. # Shared SMTP settings data used across test classes
  10. SMTP_DATA = {
  11. "smtp_host": "smtp.test.com",
  12. "smtp_port": 587,
  13. "smtp_username": "test@test.com",
  14. "smtp_password": "testpass",
  15. "smtp_security": "starttls",
  16. "smtp_auth_enabled": True,
  17. "smtp_from_email": "noreply@test.com",
  18. }
  19. async def _setup_admin(async_client: AsyncClient, username: str = "admin", password: str = "adminpass123"):
  20. """Enable auth and create admin user, return admin token."""
  21. await async_client.post(
  22. "/api/v1/auth/setup",
  23. json={
  24. "auth_enabled": True,
  25. "admin_username": username,
  26. "admin_password": password,
  27. },
  28. )
  29. login = await async_client.post(
  30. "/api/v1/auth/login",
  31. json={"username": username, "password": password},
  32. )
  33. return login.json()["access_token"]
  34. async def _setup_smtp_and_advanced_auth(async_client: AsyncClient, token: str):
  35. """Configure SMTP and enable advanced auth. Must mock send_email externally."""
  36. headers = {"Authorization": f"Bearer {token}"}
  37. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  38. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  39. async def _create_regular_user(
  40. async_client: AsyncClient, token: str, username: str = "regular", password: str = "regularpass123"
  41. ):
  42. """Create a regular (non-admin) user and return their token."""
  43. headers = {"Authorization": f"Bearer {token}"}
  44. await async_client.post(
  45. "/api/v1/users/",
  46. headers=headers,
  47. json={"username": username, "password": password, "role": "user"},
  48. )
  49. login = await async_client.post(
  50. "/api/v1/auth/login",
  51. json={"username": username, "password": password},
  52. )
  53. return login.json()["access_token"]
  54. class TestSMTPConfigAPI:
  55. """Integration tests for SMTP configuration endpoints."""
  56. @pytest.fixture
  57. async def admin_token(self, async_client: AsyncClient):
  58. return await _setup_admin(async_client, "smtpadmin", "adminpass123")
  59. @pytest.mark.asyncio
  60. @pytest.mark.integration
  61. async def test_save_smtp_settings(self, async_client: AsyncClient, admin_token: str):
  62. """POST /auth/smtp with valid settings returns 200."""
  63. response = await async_client.post(
  64. "/api/v1/auth/smtp",
  65. headers={"Authorization": f"Bearer {admin_token}"},
  66. json=SMTP_DATA,
  67. )
  68. assert response.status_code == 200
  69. assert "saved" in response.json()["message"].lower() or "success" in response.json()["message"].lower()
  70. @pytest.mark.asyncio
  71. @pytest.mark.integration
  72. async def test_get_smtp_settings_masks_password(self, async_client: AsyncClient, admin_token: str):
  73. """GET /auth/smtp returns settings with password masked (None)."""
  74. headers = {"Authorization": f"Bearer {admin_token}"}
  75. # Save settings first
  76. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  77. response = await async_client.get("/api/v1/auth/smtp", headers=headers)
  78. assert response.status_code == 200
  79. result = response.json()
  80. assert result["smtp_host"] == "smtp.test.com"
  81. assert result["smtp_password"] is None
  82. @pytest.mark.asyncio
  83. @pytest.mark.integration
  84. async def test_smtp_settings_requires_admin(self, async_client: AsyncClient, admin_token: str):
  85. """Non-admin user gets 403 on SMTP endpoints."""
  86. user_token = await _create_regular_user(async_client, admin_token, "smtpregular", "pass123456")
  87. headers = {"Authorization": f"Bearer {user_token}"}
  88. response = await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  89. assert response.status_code == 403
  90. response = await async_client.get("/api/v1/auth/smtp", headers=headers)
  91. assert response.status_code == 403
  92. @pytest.mark.asyncio
  93. @pytest.mark.integration
  94. async def test_save_smtp_settings_no_auth(self, async_client: AsyncClient, admin_token: str):
  95. """No token on SMTP save returns 401."""
  96. response = await async_client.post("/api/v1/auth/smtp", json=SMTP_DATA)
  97. assert response.status_code == 401
  98. @pytest.mark.asyncio
  99. @pytest.mark.integration
  100. async def test_test_smtp_connection(self, async_client: AsyncClient, admin_token: str):
  101. """POST /auth/smtp/test with mocked send_email returns success."""
  102. with patch("backend.app.api.routes.auth.send_email"):
  103. response = await async_client.post(
  104. "/api/v1/auth/smtp/test",
  105. headers={"Authorization": f"Bearer {admin_token}"},
  106. json={
  107. **SMTP_DATA,
  108. "test_recipient": "recipient@test.com",
  109. },
  110. )
  111. assert response.status_code == 200
  112. assert response.json()["success"] is True
  113. class TestAdvancedAuthToggleAPI:
  114. """Integration tests for enabling/disabling advanced authentication."""
  115. @pytest.fixture
  116. async def admin_token(self, async_client: AsyncClient):
  117. return await _setup_admin(async_client, "toggleadmin", "adminpass123")
  118. @pytest.mark.asyncio
  119. @pytest.mark.integration
  120. async def test_enable_advanced_auth(self, async_client: AsyncClient, admin_token: str):
  121. """Enable advanced auth after SMTP is configured returns 200."""
  122. headers = {"Authorization": f"Bearer {admin_token}"}
  123. # Configure SMTP first
  124. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  125. response = await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  126. assert response.status_code == 200
  127. assert response.json()["advanced_auth_enabled"] is True
  128. @pytest.mark.asyncio
  129. @pytest.mark.integration
  130. async def test_enable_advanced_auth_without_smtp(self, async_client: AsyncClient, admin_token: str):
  131. """Enable advanced auth without SMTP configured returns 400."""
  132. headers = {"Authorization": f"Bearer {admin_token}"}
  133. response = await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  134. assert response.status_code == 400
  135. assert "SMTP" in response.json()["detail"]
  136. @pytest.mark.asyncio
  137. @pytest.mark.integration
  138. async def test_disable_advanced_auth(self, async_client: AsyncClient, admin_token: str):
  139. """Disable advanced auth returns 200."""
  140. headers = {"Authorization": f"Bearer {admin_token}"}
  141. # Enable first
  142. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  143. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  144. response = await async_client.post("/api/v1/auth/advanced-auth/disable", headers=headers)
  145. assert response.status_code == 200
  146. assert response.json()["advanced_auth_enabled"] is False
  147. @pytest.mark.asyncio
  148. @pytest.mark.integration
  149. async def test_advanced_auth_status_public(self, async_client: AsyncClient, admin_token: str):
  150. """GET /auth/advanced-auth/status is accessible without token."""
  151. response = await async_client.get("/api/v1/auth/advanced-auth/status")
  152. assert response.status_code == 200
  153. result = response.json()
  154. assert "advanced_auth_enabled" in result
  155. assert "smtp_configured" in result
  156. @pytest.mark.asyncio
  157. @pytest.mark.integration
  158. async def test_enable_requires_admin(self, async_client: AsyncClient, admin_token: str):
  159. """Non-admin user gets 403 on enable/disable."""
  160. user_token = await _create_regular_user(async_client, admin_token, "toggleregular", "pass123456")
  161. headers = {"Authorization": f"Bearer {user_token}"}
  162. response = await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  163. assert response.status_code == 403
  164. response = await async_client.post("/api/v1/auth/advanced-auth/disable", headers=headers)
  165. assert response.status_code == 403
  166. class TestEmailLoginAPI:
  167. """Integration tests for email-based login."""
  168. @pytest.fixture
  169. async def admin_token(self, async_client: AsyncClient):
  170. return await _setup_admin(async_client, "emailadmin", "adminpass123")
  171. @pytest.mark.asyncio
  172. @pytest.mark.integration
  173. async def test_login_with_email(self, async_client: AsyncClient, admin_token: str):
  174. """Login with email address when advanced auth is enabled returns token."""
  175. headers = {"Authorization": f"Bearer {admin_token}"}
  176. with patch("backend.app.api.routes.users.send_email"):
  177. # Configure SMTP + advanced auth
  178. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  179. # Create user with email (password auto-generated, so we set one explicitly via update)
  180. create_resp = await async_client.post(
  181. "/api/v1/users/",
  182. headers=headers,
  183. json={"username": "emailuser", "email": "emailuser@test.com", "role": "user"},
  184. )
  185. assert create_resp.status_code == 201
  186. user_id = create_resp.json()["id"]
  187. # Set a known password via admin update
  188. await async_client.patch(
  189. f"/api/v1/users/{user_id}",
  190. headers=headers,
  191. json={"password": "knownpassword123"},
  192. )
  193. # Login with email
  194. response = await async_client.post(
  195. "/api/v1/auth/login",
  196. json={"username": "emailuser@test.com", "password": "knownpassword123"},
  197. )
  198. assert response.status_code == 200
  199. assert "access_token" in response.json()
  200. @pytest.mark.asyncio
  201. @pytest.mark.integration
  202. async def test_login_with_email_case_insensitive(self, async_client: AsyncClient, admin_token: str):
  203. """Login with uppercase email matches case-insensitively."""
  204. headers = {"Authorization": f"Bearer {admin_token}"}
  205. with patch("backend.app.api.routes.users.send_email"):
  206. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  207. create_resp = await async_client.post(
  208. "/api/v1/users/",
  209. headers=headers,
  210. json={"username": "caseuser", "email": "caseuser@test.com", "role": "user"},
  211. )
  212. user_id = create_resp.json()["id"]
  213. await async_client.patch(
  214. f"/api/v1/users/{user_id}",
  215. headers=headers,
  216. json={"password": "casepassword123"},
  217. )
  218. response = await async_client.post(
  219. "/api/v1/auth/login",
  220. json={"username": "CASEUSER@TEST.COM", "password": "casepassword123"},
  221. )
  222. assert response.status_code == 200
  223. assert "access_token" in response.json()
  224. @pytest.mark.asyncio
  225. @pytest.mark.integration
  226. async def test_login_with_email_advanced_auth_disabled(self, async_client: AsyncClient, admin_token: str):
  227. """Email login fails when advanced auth is disabled."""
  228. headers = {"Authorization": f"Bearer {admin_token}"}
  229. # Create user with email but no advanced auth
  230. await async_client.post(
  231. "/api/v1/users/",
  232. headers=headers,
  233. json={"username": "noemail", "password": "noEmailPass1", "email": "noemail@test.com", "role": "user"},
  234. )
  235. # Try to login with email — should fail since advanced auth is off
  236. response = await async_client.post(
  237. "/api/v1/auth/login",
  238. json={"username": "noemail@test.com", "password": "noEmailPass1"},
  239. )
  240. assert response.status_code == 401
  241. @pytest.mark.asyncio
  242. @pytest.mark.integration
  243. async def test_login_with_username_still_works(self, async_client: AsyncClient, admin_token: str):
  244. """Username-based login still works when advanced auth is enabled."""
  245. headers = {"Authorization": f"Bearer {admin_token}"}
  246. with patch("backend.app.api.routes.users.send_email"):
  247. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  248. create_resp = await async_client.post(
  249. "/api/v1/users/",
  250. headers=headers,
  251. json={"username": "usernameuser", "email": "usernameuser@test.com", "role": "user"},
  252. )
  253. user_id = create_resp.json()["id"]
  254. await async_client.patch(
  255. f"/api/v1/users/{user_id}",
  256. headers=headers,
  257. json={"password": "usernamepass123"},
  258. )
  259. # Login with username (not email)
  260. response = await async_client.post(
  261. "/api/v1/auth/login",
  262. json={"username": "usernameuser", "password": "usernamepass123"},
  263. )
  264. assert response.status_code == 200
  265. assert "access_token" in response.json()
  266. class TestForgotPasswordAPI:
  267. """Integration tests for forgot-password flow."""
  268. @pytest.fixture
  269. async def admin_token(self, async_client: AsyncClient):
  270. return await _setup_admin(async_client, "forgotadmin", "adminpass123")
  271. @pytest.mark.asyncio
  272. @pytest.mark.integration
  273. async def test_forgot_password_sends_email(self, async_client: AsyncClient, admin_token: str):
  274. """POST /auth/forgot-password with valid email sends reset email."""
  275. headers = {"Authorization": f"Bearer {admin_token}"}
  276. with patch("backend.app.api.routes.users.send_email"):
  277. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  278. # Create a user with email
  279. create_resp = await async_client.post(
  280. "/api/v1/users/",
  281. headers=headers,
  282. json={"username": "forgotuser", "email": "forgot@test.com", "role": "user"},
  283. )
  284. assert create_resp.status_code == 201
  285. with patch("backend.app.api.routes.auth.send_email") as mock_send:
  286. response = await async_client.post(
  287. "/api/v1/auth/forgot-password",
  288. json={"email": "forgot@test.com"},
  289. )
  290. assert response.status_code == 200
  291. mock_send.assert_called_once()
  292. # Verify the email was sent to the right address
  293. assert mock_send.call_args[0][1] == "forgot@test.com"
  294. @pytest.mark.asyncio
  295. @pytest.mark.integration
  296. async def test_forgot_password_unknown_email(self, async_client: AsyncClient, admin_token: str):
  297. """Unknown email still returns 200 (anti-enumeration) but send_email not called."""
  298. headers = {"Authorization": f"Bearer {admin_token}"}
  299. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  300. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  301. with patch("backend.app.api.routes.auth.send_email") as mock_send:
  302. response = await async_client.post(
  303. "/api/v1/auth/forgot-password",
  304. json={"email": "unknown@test.com"},
  305. )
  306. assert response.status_code == 200
  307. mock_send.assert_not_called()
  308. @pytest.mark.asyncio
  309. @pytest.mark.integration
  310. async def test_forgot_password_requires_advanced_auth(self, async_client: AsyncClient, admin_token: str):
  311. """Forgot password returns 400 when advanced auth is disabled."""
  312. response = await async_client.post(
  313. "/api/v1/auth/forgot-password",
  314. json={"email": "test@test.com"},
  315. )
  316. assert response.status_code == 400
  317. assert "not enabled" in response.json()["detail"].lower()
  318. @pytest.mark.asyncio
  319. @pytest.mark.integration
  320. async def test_forgot_password_changes_password(self, async_client: AsyncClient, admin_token: str):
  321. """After forgot-password, old password stops working."""
  322. headers = {"Authorization": f"Bearer {admin_token}"}
  323. with patch("backend.app.api.routes.users.send_email"):
  324. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  325. create_resp = await async_client.post(
  326. "/api/v1/users/",
  327. headers=headers,
  328. json={"username": "resetme", "email": "resetme@test.com", "role": "user"},
  329. )
  330. user_id = create_resp.json()["id"]
  331. await async_client.patch(
  332. f"/api/v1/users/{user_id}",
  333. headers=headers,
  334. json={"password": "originalpass123"},
  335. )
  336. # Verify login works with original password
  337. login_resp = await async_client.post(
  338. "/api/v1/auth/login",
  339. json={"username": "resetme", "password": "originalpass123"},
  340. )
  341. assert login_resp.status_code == 200
  342. # Trigger forgot password
  343. with patch("backend.app.api.routes.auth.send_email"):
  344. await async_client.post(
  345. "/api/v1/auth/forgot-password",
  346. json={"email": "resetme@test.com"},
  347. )
  348. # Old password should no longer work
  349. login_resp = await async_client.post(
  350. "/api/v1/auth/login",
  351. json={"username": "resetme", "password": "originalpass123"},
  352. )
  353. assert login_resp.status_code == 401
  354. class TestAdminResetPasswordAPI:
  355. """Integration tests for admin password reset endpoint."""
  356. @pytest.fixture
  357. async def admin_token(self, async_client: AsyncClient):
  358. return await _setup_admin(async_client, "resetadmin", "adminpass123")
  359. @pytest.mark.asyncio
  360. @pytest.mark.integration
  361. async def test_reset_password_sends_email(self, async_client: AsyncClient, admin_token: str):
  362. """POST /auth/reset-password sends email to user."""
  363. headers = {"Authorization": f"Bearer {admin_token}"}
  364. with patch("backend.app.api.routes.users.send_email"):
  365. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  366. create_resp = await async_client.post(
  367. "/api/v1/users/",
  368. headers=headers,
  369. json={"username": "resetuser", "email": "resetuser@test.com", "role": "user"},
  370. )
  371. user_id = create_resp.json()["id"]
  372. with patch("backend.app.api.routes.auth.send_email") as mock_send:
  373. response = await async_client.post(
  374. "/api/v1/auth/reset-password",
  375. headers=headers,
  376. json={"user_id": user_id},
  377. )
  378. assert response.status_code == 200
  379. mock_send.assert_called_once()
  380. assert mock_send.call_args[0][1] == "resetuser@test.com"
  381. @pytest.mark.asyncio
  382. @pytest.mark.integration
  383. async def test_reset_password_requires_admin(self, async_client: AsyncClient, admin_token: str):
  384. """Non-admin user gets 403 on reset-password."""
  385. # Create regular user before enabling advanced auth (no email required)
  386. user_token = await _create_regular_user(async_client, admin_token, "resetregular", "pass123456")
  387. with patch("backend.app.api.routes.users.send_email"):
  388. await _setup_smtp_and_advanced_auth(async_client, admin_token)
  389. response = await async_client.post(
  390. "/api/v1/auth/reset-password",
  391. headers={"Authorization": f"Bearer {user_token}"},
  392. json={"user_id": 1},
  393. )
  394. assert response.status_code == 403
  395. @pytest.mark.asyncio
  396. @pytest.mark.integration
  397. async def test_reset_password_requires_advanced_auth(self, async_client: AsyncClient, admin_token: str):
  398. """Reset password returns 400 when advanced auth is disabled."""
  399. headers = {"Authorization": f"Bearer {admin_token}"}
  400. response = await async_client.post(
  401. "/api/v1/auth/reset-password",
  402. headers=headers,
  403. json={"user_id": 999},
  404. )
  405. assert response.status_code == 400
  406. assert "not enabled" in response.json()["detail"].lower()
  407. @pytest.mark.asyncio
  408. @pytest.mark.integration
  409. async def test_reset_password_user_not_found(self, async_client: AsyncClient, admin_token: str):
  410. """Reset password with invalid user_id returns 404."""
  411. headers = {"Authorization": f"Bearer {admin_token}"}
  412. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  413. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  414. response = await async_client.post(
  415. "/api/v1/auth/reset-password",
  416. headers=headers,
  417. json={"user_id": 99999},
  418. )
  419. assert response.status_code == 404
  420. @pytest.mark.asyncio
  421. @pytest.mark.integration
  422. async def test_reset_password_user_no_email(self, async_client: AsyncClient, admin_token: str):
  423. """Reset password for user without email returns 400."""
  424. headers = {"Authorization": f"Bearer {admin_token}"}
  425. # Save SMTP and enable advanced auth
  426. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  427. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  428. # Disable advanced auth temporarily to create a user without email
  429. await async_client.post("/api/v1/auth/advanced-auth/disable", headers=headers)
  430. create_resp = await async_client.post(
  431. "/api/v1/users/",
  432. headers=headers,
  433. json={"username": "noemailuser", "password": "noemail123456", "role": "user"},
  434. )
  435. user_id = create_resp.json()["id"]
  436. # Re-enable advanced auth
  437. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  438. response = await async_client.post(
  439. "/api/v1/auth/reset-password",
  440. headers=headers,
  441. json={"user_id": user_id},
  442. )
  443. assert response.status_code == 400
  444. assert "email" in response.json()["detail"].lower()
  445. class TestUserCreationAdvancedAuth:
  446. """Integration tests for user creation with advanced auth enabled."""
  447. @pytest.fixture
  448. async def admin_token(self, async_client: AsyncClient):
  449. return await _setup_admin(async_client, "createadmin", "adminpass123")
  450. @pytest.mark.asyncio
  451. @pytest.mark.integration
  452. async def test_create_user_advanced_auth_requires_email(self, async_client: AsyncClient, admin_token: str):
  453. """Creating user without email when advanced auth is on returns 400."""
  454. headers = {"Authorization": f"Bearer {admin_token}"}
  455. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  456. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  457. response = await async_client.post(
  458. "/api/v1/users/",
  459. headers=headers,
  460. json={"username": "noemailcreate", "role": "user"},
  461. )
  462. assert response.status_code == 400
  463. assert "email" in response.json()["detail"].lower()
  464. @pytest.mark.asyncio
  465. @pytest.mark.integration
  466. async def test_create_user_advanced_auth_auto_password(self, async_client: AsyncClient, admin_token: str):
  467. """Creating user with email auto-generates password and sends welcome email."""
  468. headers = {"Authorization": f"Bearer {admin_token}"}
  469. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  470. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  471. with patch("backend.app.api.routes.users.send_email") as mock_send:
  472. response = await async_client.post(
  473. "/api/v1/users/",
  474. headers=headers,
  475. json={"username": "autopassuser", "email": "autopass@test.com", "role": "user"},
  476. )
  477. assert response.status_code == 201
  478. result = response.json()
  479. assert result["username"] == "autopassuser"
  480. assert result["email"] == "autopass@test.com"
  481. # Welcome email should have been sent
  482. mock_send.assert_called_once()
  483. assert mock_send.call_args[0][1] == "autopass@test.com"
  484. @pytest.mark.asyncio
  485. @pytest.mark.integration
  486. async def test_create_user_duplicate_email(self, async_client: AsyncClient, admin_token: str):
  487. """Creating two users with the same email returns 400."""
  488. headers = {"Authorization": f"Bearer {admin_token}"}
  489. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  490. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  491. with patch("backend.app.api.routes.users.send_email"):
  492. resp1 = await async_client.post(
  493. "/api/v1/users/",
  494. headers=headers,
  495. json={"username": "dupemail1", "email": "dupe@test.com", "role": "user"},
  496. )
  497. assert resp1.status_code == 201
  498. resp2 = await async_client.post(
  499. "/api/v1/users/",
  500. headers=headers,
  501. json={"username": "dupemail2", "email": "dupe@test.com", "role": "user"},
  502. )
  503. assert resp2.status_code == 400
  504. assert "email" in resp2.json()["detail"].lower()
  505. @pytest.mark.asyncio
  506. @pytest.mark.integration
  507. async def test_create_user_response_includes_email(self, async_client: AsyncClient, admin_token: str):
  508. """Created user response includes email field."""
  509. headers = {"Authorization": f"Bearer {admin_token}"}
  510. await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
  511. await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
  512. with patch("backend.app.api.routes.users.send_email"):
  513. response = await async_client.post(
  514. "/api/v1/users/",
  515. headers=headers,
  516. json={"username": "emailresp", "email": "emailresp@test.com", "role": "user"},
  517. )
  518. assert response.status_code == 201
  519. result = response.json()
  520. assert "email" in result
  521. assert result["email"] == "emailresp@test.com"