| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714 |
- """Integration tests for Advanced Authentication API endpoints.
- Tests the full request/response cycle for SMTP configuration, advanced auth toggle,
- email-based login, forgot password, admin password reset, and user creation
- with advanced authentication enabled.
- """
- from unittest.mock import patch
- import pytest
- from httpx import AsyncClient
- # Shared SMTP settings data used across test classes
- SMTP_DATA = {
- "smtp_host": "smtp.test.com",
- "smtp_port": 587,
- "smtp_username": "test@test.com",
- "smtp_password": "testpass",
- "smtp_security": "starttls",
- "smtp_auth_enabled": True,
- "smtp_from_email": "noreply@test.com",
- }
- async def _setup_admin(async_client: AsyncClient, username: str = "admin", password: str = "AdminPass1!"):
- """Enable auth and create admin user, return admin token."""
- await async_client.post(
- "/api/v1/auth/setup",
- json={
- "auth_enabled": True,
- "admin_username": username,
- "admin_password": password,
- },
- )
- login = await async_client.post(
- "/api/v1/auth/login",
- json={"username": username, "password": password},
- )
- return login.json()["access_token"]
- async def _setup_smtp_and_advanced_auth(async_client: AsyncClient, token: str):
- """Configure SMTP and enable advanced auth. Must mock send_email externally."""
- headers = {"Authorization": f"Bearer {token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- async def _create_regular_user(
- async_client: AsyncClient, token: str, username: str = "regular", password: str = "Regularpass1!"
- ):
- """Create a regular (non-admin) user and return their token."""
- headers = {"Authorization": f"Bearer {token}"}
- await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": username, "password": password, "role": "user"},
- )
- login = await async_client.post(
- "/api/v1/auth/login",
- json={"username": username, "password": password},
- )
- return login.json()["access_token"]
- class TestSMTPConfigAPI:
- """Integration tests for SMTP configuration endpoints."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "smtpadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_smtp_settings(self, async_client: AsyncClient, admin_token: str):
- """POST /auth/smtp with valid settings returns 200."""
- response = await async_client.post(
- "/api/v1/auth/smtp",
- headers={"Authorization": f"Bearer {admin_token}"},
- json=SMTP_DATA,
- )
- assert response.status_code == 200
- assert "saved" in response.json()["message"].lower() or "success" in response.json()["message"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_get_smtp_settings_masks_password(self, async_client: AsyncClient, admin_token: str):
- """GET /auth/smtp returns settings with password masked (None)."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- # Save settings first
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- response = await async_client.get("/api/v1/auth/smtp", headers=headers)
- assert response.status_code == 200
- result = response.json()
- assert result["smtp_host"] == "smtp.test.com"
- assert result["smtp_password"] is None
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_smtp_settings_requires_admin(self, async_client: AsyncClient, admin_token: str):
- """Non-admin user gets 403 on SMTP endpoints."""
- user_token = await _create_regular_user(async_client, admin_token, "smtpregular", "Pass12345!")
- headers = {"Authorization": f"Bearer {user_token}"}
- response = await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- assert response.status_code == 403
- response = await async_client.get("/api/v1/auth/smtp", headers=headers)
- assert response.status_code == 403
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_save_smtp_settings_no_auth(self, async_client: AsyncClient, admin_token: str):
- """No token on SMTP save returns 401."""
- response = await async_client.post("/api/v1/auth/smtp", json=SMTP_DATA)
- assert response.status_code == 401
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_test_smtp_connection(self, async_client: AsyncClient, admin_token: str):
- """POST /auth/smtp/test with mocked send_email returns success."""
- await async_client.post(
- "/api/v1/auth/smtp",
- headers={"Authorization": f"Bearer {admin_token}"},
- json=SMTP_DATA,
- )
- with patch("backend.app.api.routes.auth.send_email"):
- response = await async_client.post(
- "/api/v1/auth/smtp/test",
- headers={"Authorization": f"Bearer {admin_token}"},
- json={
- "test_recipient": "recipient@test.com",
- },
- )
- assert response.status_code == 200
- assert response.json()["success"] is True
- class TestAdvancedAuthToggleAPI:
- """Integration tests for enabling/disabling advanced authentication."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "toggleadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_enable_advanced_auth(self, async_client: AsyncClient, admin_token: str):
- """Enable advanced auth after SMTP is configured returns 200."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- # Configure SMTP first
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- response = await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- assert response.status_code == 200
- assert response.json()["advanced_auth_enabled"] is True
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_enable_advanced_auth_without_smtp(self, async_client: AsyncClient, admin_token: str):
- """Enable advanced auth without SMTP configured returns 400."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- response = await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- assert response.status_code == 400
- assert "SMTP" in response.json()["detail"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_disable_advanced_auth(self, async_client: AsyncClient, admin_token: str):
- """Disable advanced auth returns 200."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- # Enable first
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- response = await async_client.post("/api/v1/auth/advanced-auth/disable", headers=headers)
- assert response.status_code == 200
- assert response.json()["advanced_auth_enabled"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_advanced_auth_status_public(self, async_client: AsyncClient, admin_token: str):
- """GET /auth/advanced-auth/status is accessible without token."""
- response = await async_client.get("/api/v1/auth/advanced-auth/status")
- assert response.status_code == 200
- result = response.json()
- assert "advanced_auth_enabled" in result
- assert "smtp_configured" in result
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_enable_requires_admin(self, async_client: AsyncClient, admin_token: str):
- """Non-admin user gets 403 on enable/disable."""
- user_token = await _create_regular_user(async_client, admin_token, "toggleregular", "Pass12345!")
- headers = {"Authorization": f"Bearer {user_token}"}
- response = await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- assert response.status_code == 403
- response = await async_client.post("/api/v1/auth/advanced-auth/disable", headers=headers)
- assert response.status_code == 403
- class TestEmailLoginAPI:
- """Integration tests for email-based login."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "emailadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_login_with_email(self, async_client: AsyncClient, admin_token: str):
- """Login with email address when advanced auth is enabled returns token."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- with patch("backend.app.api.routes.users.send_email"):
- # Configure SMTP + advanced auth
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- # Create user with email (password auto-generated, so we set one explicitly via update)
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "emailuser", "email": "emailuser@test.com", "role": "user"},
- )
- assert create_resp.status_code == 201
- user_id = create_resp.json()["id"]
- # Set a known password via admin update
- await async_client.patch(
- f"/api/v1/users/{user_id}",
- headers=headers,
- json={"password": "Knownpassword1!"},
- )
- # Login with email
- response = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "emailuser@test.com", "password": "Knownpassword1!"},
- )
- assert response.status_code == 200
- assert "access_token" in response.json()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_login_with_email_case_insensitive(self, async_client: AsyncClient, admin_token: str):
- """Login with uppercase email matches case-insensitively."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- with patch("backend.app.api.routes.users.send_email"):
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "caseuser", "email": "caseuser@test.com", "role": "user"},
- )
- user_id = create_resp.json()["id"]
- await async_client.patch(
- f"/api/v1/users/{user_id}",
- headers=headers,
- json={"password": "Casepassword1!"},
- )
- response = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "CASEUSER@TEST.COM", "password": "Casepassword1!"},
- )
- assert response.status_code == 200
- assert "access_token" in response.json()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_login_with_email_advanced_auth_disabled(self, async_client: AsyncClient, admin_token: str):
- """Email login fails when advanced auth is disabled."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- # Create user with email but no advanced auth
- await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "noemail", "password": "NoEmailPass1!", "email": "noemail@test.com", "role": "user"},
- )
- # Try to login with email — should fail since advanced auth is off
- response = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "noemail@test.com", "password": "NoEmailPass1!"},
- )
- assert response.status_code == 401
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_login_with_username_still_works(self, async_client: AsyncClient, admin_token: str):
- """Username-based login still works when advanced auth is enabled."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- with patch("backend.app.api.routes.users.send_email"):
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "usernameuser", "email": "usernameuser@test.com", "role": "user"},
- )
- user_id = create_resp.json()["id"]
- await async_client.patch(
- f"/api/v1/users/{user_id}",
- headers=headers,
- json={"password": "Usernamepass1!"},
- )
- # Login with username (not email)
- response = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "usernameuser", "password": "Usernamepass1!"},
- )
- assert response.status_code == 200
- assert "access_token" in response.json()
- class TestForgotPasswordAPI:
- """Integration tests for forgot-password flow."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "forgotadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_forgot_password_sends_email(self, async_client: AsyncClient, admin_token: str):
- """POST /auth/forgot-password with valid email sends reset email."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- with patch("backend.app.api.routes.users.send_email"):
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- # Create a user with email
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "forgotuser", "email": "forgot@test.com", "role": "user"},
- )
- assert create_resp.status_code == 201
- with patch("backend.app.api.routes.auth.send_email") as mock_send:
- response = await async_client.post(
- "/api/v1/auth/forgot-password",
- json={"email": "forgot@test.com"},
- )
- assert response.status_code == 200
- mock_send.assert_called_once()
- # Verify the email was sent to the right address
- assert mock_send.call_args[0][1] == "forgot@test.com"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_forgot_password_unknown_email(self, async_client: AsyncClient, admin_token: str):
- """Unknown email still returns 200 (anti-enumeration) but send_email not called."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- with patch("backend.app.api.routes.auth.send_email") as mock_send:
- response = await async_client.post(
- "/api/v1/auth/forgot-password",
- json={"email": "unknown@test.com"},
- )
- assert response.status_code == 200
- mock_send.assert_not_called()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_forgot_password_requires_advanced_auth(self, async_client: AsyncClient, admin_token: str):
- """Forgot password returns 400 when advanced auth is disabled."""
- response = await async_client.post(
- "/api/v1/auth/forgot-password",
- json={"email": "test@test.com"},
- )
- assert response.status_code == 400
- assert "not enabled" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_forgot_password_changes_password(self, async_client: AsyncClient, admin_token: str):
- """After forgot-password + confirm, old password stops working and new one works.
- H-6: The flow is now token-based: /forgot-password issues a reset link and
- /forgot-password/confirm consumes the token and sets the new password.
- """
- from unittest.mock import AsyncMock
- headers = {"Authorization": f"Bearer {admin_token}"}
- with patch("backend.app.api.routes.users.send_email"):
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "resetme", "email": "resetme@test.com", "role": "user"},
- )
- user_id = create_resp.json()["id"]
- await async_client.patch(
- f"/api/v1/users/{user_id}",
- headers=headers,
- json={"password": "Originalpass1!"},
- )
- # Verify login works with original password
- login_resp = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "resetme", "password": "Originalpass1!"},
- )
- assert login_resp.status_code == 200
- # Trigger forgot-password and capture the reset URL (contains the token)
- captured: dict[str, str] = {}
- async def _capture_link_email(db, username, reset_url):
- captured["reset_url"] = reset_url
- return ("subject", "body", "<body/>")
- with (
- patch(
- "backend.app.api.routes.auth.create_password_reset_link_email_from_template",
- side_effect=_capture_link_email,
- ),
- patch("backend.app.api.routes.auth.send_email"),
- ):
- resp = await async_client.post(
- "/api/v1/auth/forgot-password",
- json={"email": "resetme@test.com"},
- )
- assert resp.status_code == 200
- assert "reset_url" in captured, "Reset URL not captured — email function was not called"
- # Extract the token from the captured URL and confirm the reset
- reset_token = captured["reset_url"].split("reset_token=")[1]
- confirm_resp = await async_client.post(
- "/api/v1/auth/forgot-password/confirm",
- json={"token": reset_token, "new_password": "Newpass456!"},
- )
- assert confirm_resp.status_code == 200
- # Old password should no longer work
- login_resp = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "resetme", "password": "Originalpass1!"},
- )
- assert login_resp.status_code == 401
- # New password must work
- login_resp = await async_client.post(
- "/api/v1/auth/login",
- json={"username": "resetme", "password": "Newpass456!"},
- )
- assert login_resp.status_code == 200
- class TestAdminResetPasswordAPI:
- """Integration tests for admin password reset endpoint."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "resetadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reset_password_sends_email(self, async_client: AsyncClient, admin_token: str):
- """POST /auth/reset-password sends email to user."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- with patch("backend.app.api.routes.users.send_email"):
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "resetuser", "email": "resetuser@test.com", "role": "user"},
- )
- user_id = create_resp.json()["id"]
- with patch("backend.app.api.routes.auth.send_email") as mock_send:
- response = await async_client.post(
- "/api/v1/auth/reset-password",
- headers=headers,
- json={"user_id": user_id},
- )
- assert response.status_code == 200
- mock_send.assert_called_once()
- assert mock_send.call_args[0][1] == "resetuser@test.com"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reset_password_requires_admin(self, async_client: AsyncClient, admin_token: str):
- """Non-admin user gets 403 on reset-password."""
- # Create regular user before enabling advanced auth (no email required)
- user_token = await _create_regular_user(async_client, admin_token, "resetregular", "Pass12345!")
- with patch("backend.app.api.routes.users.send_email"):
- await _setup_smtp_and_advanced_auth(async_client, admin_token)
- response = await async_client.post(
- "/api/v1/auth/reset-password",
- headers={"Authorization": f"Bearer {user_token}"},
- json={"user_id": 1},
- )
- assert response.status_code == 403
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reset_password_requires_advanced_auth(self, async_client: AsyncClient, admin_token: str):
- """Reset password returns 400 when advanced auth is disabled."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- response = await async_client.post(
- "/api/v1/auth/reset-password",
- headers=headers,
- json={"user_id": 999},
- )
- assert response.status_code == 400
- assert "not enabled" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reset_password_user_not_found(self, async_client: AsyncClient, admin_token: str):
- """Reset password with invalid user_id returns 404."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- response = await async_client.post(
- "/api/v1/auth/reset-password",
- headers=headers,
- json={"user_id": 99999},
- )
- assert response.status_code == 404
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_reset_password_user_no_email(self, async_client: AsyncClient, admin_token: str):
- """Reset password for user without email returns 400."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- # Save SMTP and enable advanced auth
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- # Disable advanced auth temporarily to create a user without email
- await async_client.post("/api/v1/auth/advanced-auth/disable", headers=headers)
- create_resp = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "noemailuser", "password": "Noemail12345!", "role": "user"},
- )
- user_id = create_resp.json()["id"]
- # Re-enable advanced auth
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- response = await async_client.post(
- "/api/v1/auth/reset-password",
- headers=headers,
- json={"user_id": user_id},
- )
- assert response.status_code == 400
- assert "email" in response.json()["detail"].lower()
- class TestUserCreationAdvancedAuth:
- """Integration tests for user creation with advanced auth enabled."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "createadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_user_advanced_auth_requires_email(self, async_client: AsyncClient, admin_token: str):
- """Creating user without email when advanced auth is on returns 400."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- response = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "noemailcreate", "role": "user"},
- )
- assert response.status_code == 400
- assert "email" in response.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_user_advanced_auth_auto_password(self, async_client: AsyncClient, admin_token: str):
- """Creating user with email auto-generates password and sends welcome email."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- with patch("backend.app.api.routes.users.send_email") as mock_send:
- response = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "autopassuser", "email": "autopass@test.com", "role": "user"},
- )
- assert response.status_code == 201
- result = response.json()
- assert result["username"] == "autopassuser"
- assert result["email"] == "autopass@test.com"
- # Welcome email should have been sent
- mock_send.assert_called_once()
- assert mock_send.call_args[0][1] == "autopass@test.com"
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_user_duplicate_email(self, async_client: AsyncClient, admin_token: str):
- """Creating two users with the same email returns 400."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- with patch("backend.app.api.routes.users.send_email"):
- resp1 = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "dupemail1", "email": "dupe@test.com", "role": "user"},
- )
- assert resp1.status_code == 201
- resp2 = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "dupemail2", "email": "dupe@test.com", "role": "user"},
- )
- assert resp2.status_code == 400
- assert "email" in resp2.json()["detail"].lower()
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_create_user_response_includes_email(self, async_client: AsyncClient, admin_token: str):
- """Created user response includes email field."""
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- with patch("backend.app.api.routes.users.send_email"):
- response = await async_client.post(
- "/api/v1/users/",
- headers=headers,
- json={"username": "emailresp", "email": "emailresp@test.com", "role": "user"},
- )
- assert response.status_code == 201
- result = response.json()
- assert "email" in result
- assert result["email"] == "emailresp@test.com"
- # ===========================================================================
- # M-1: OIDC/LDAP users must not be able to use the password reset flow
- # ===========================================================================
- class TestAuthSourcePasswordResetBlocking:
- """Forgot-password must silently skip OIDC and LDAP users (M-1)."""
- @pytest.fixture
- async def admin_token(self, async_client: AsyncClient):
- return await _setup_admin(async_client, "authsrcadmin", "AdminPass1!")
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_forgot_password_silently_skips_oidc_user(
- self, async_client: AsyncClient, admin_token: str, db_session
- ):
- """forgot-password for an OIDC user returns 200 but does NOT send email."""
- from backend.app.core.auth import get_password_hash
- from backend.app.models.user import User
- headers = {"Authorization": f"Bearer {admin_token}"}
- await async_client.post("/api/v1/auth/smtp", headers=headers, json=SMTP_DATA)
- await async_client.post("/api/v1/auth/advanced-auth/enable", headers=headers)
- # Directly insert an OIDC-sourced user into the DB
- oidc_user = User(
- username="oidcpwreset",
- email="oidcpwreset@test.com",
- auth_source="oidc",
- password_hash=get_password_hash("irrelevant"),
- role="user",
- is_active=True,
- )
- db_session.add(oidc_user)
- await db_session.commit()
- with patch("backend.app.api.routes.auth.send_email") as mock_send:
- response = await async_client.post(
- "/api/v1/auth/forgot-password",
- json={"email": "oidcpwreset@test.com"},
- )
- # Anti-enumeration: still returns 200
- assert response.status_code == 200
- # But no email is sent for OIDC users
- mock_send.assert_not_called()
|