|
|
@@ -0,0 +1,625 @@
|
|
|
+"""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 = "adminpass123"):
|
|
|
+ """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 = "regularpass123"
|
|
|
+):
|
|
|
+ """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", "adminpass123")
|
|
|
+
|
|
|
+ @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", "pass123456")
|
|
|
+ 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."""
|
|
|
+ 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={
|
|
|
+ **SMTP_DATA,
|
|
|
+ "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", "adminpass123")
|
|
|
+
|
|
|
+ @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", "pass123456")
|
|
|
+ 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", "adminpass123")
|
|
|
+
|
|
|
+ @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": "knownpassword123"},
|
|
|
+ )
|
|
|
+
|
|
|
+ # Login with email
|
|
|
+ response = await async_client.post(
|
|
|
+ "/api/v1/auth/login",
|
|
|
+ json={"username": "emailuser@test.com", "password": "knownpassword123"},
|
|
|
+ )
|
|
|
+ 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": "casepassword123"},
|
|
|
+ )
|
|
|
+
|
|
|
+ response = await async_client.post(
|
|
|
+ "/api/v1/auth/login",
|
|
|
+ json={"username": "CASEUSER@TEST.COM", "password": "casepassword123"},
|
|
|
+ )
|
|
|
+ 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": "usernamepass123"},
|
|
|
+ )
|
|
|
+
|
|
|
+ # Login with username (not email)
|
|
|
+ response = await async_client.post(
|
|
|
+ "/api/v1/auth/login",
|
|
|
+ json={"username": "usernameuser", "password": "usernamepass123"},
|
|
|
+ )
|
|
|
+ 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", "adminpass123")
|
|
|
+
|
|
|
+ @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, old password stops working."""
|
|
|
+ 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": "originalpass123"},
|
|
|
+ )
|
|
|
+
|
|
|
+ # Verify login works with original password
|
|
|
+ login_resp = await async_client.post(
|
|
|
+ "/api/v1/auth/login",
|
|
|
+ json={"username": "resetme", "password": "originalpass123"},
|
|
|
+ )
|
|
|
+ assert login_resp.status_code == 200
|
|
|
+
|
|
|
+ # Trigger forgot password
|
|
|
+ with patch("backend.app.api.routes.auth.send_email"):
|
|
|
+ await async_client.post(
|
|
|
+ "/api/v1/auth/forgot-password",
|
|
|
+ json={"email": "resetme@test.com"},
|
|
|
+ )
|
|
|
+
|
|
|
+ # Old password should no longer work
|
|
|
+ login_resp = await async_client.post(
|
|
|
+ "/api/v1/auth/login",
|
|
|
+ json={"username": "resetme", "password": "originalpass123"},
|
|
|
+ )
|
|
|
+ assert login_resp.status_code == 401
|
|
|
+
|
|
|
+
|
|
|
+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", "adminpass123")
|
|
|
+
|
|
|
+ @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", "pass123456")
|
|
|
+
|
|
|
+ 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": "noemail123456", "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", "adminpass123")
|
|
|
+
|
|
|
+ @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"
|