test_advanced_auth_api.py 26 KB

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