from __future__ import annotations import pytest from httpx import AsyncClient from conftest import TestUserData, create_test_user pytestmark = [pytest.mark.asyncio] @pytest.mark.auth async def test_get_current_user_returns_own_info(viewer_client: AsyncClient, viewer_user: TestUserData) -> None: """Authenticated users should be able to fetch their own profile.""" response = await viewer_client.get("/api/users/me") assert response.status_code == 200, f"Current user endpoint should succeed, got {response.text}" assert response.json()["email"] == viewer_user.user.email, "Users/me should return the authenticated user" @pytest.mark.auth async def test_get_current_user_without_token_returns_401(client: AsyncClient) -> None: """Anonymous requests should be denied.""" response = await client.get("/api/users/me") assert response.status_code == 401, f"Unauthenticated request should return 401, got {response.text}" @pytest.mark.permissions async def test_list_users_as_admin_returns_all_users( admin_client: AsyncClient, admin_user: TestUserData, analyst_user: TestUserData, viewer_user: TestUserData, ) -> None: """Admins should be able to list all users.""" response = await admin_client.get("/api/users") assert response.status_code == 200, f"Admin list users should succeed, got {response.text}" emails = {item["email"] for item in response.json()} assert {admin_user.user.email, analyst_user.user.email, viewer_user.user.email}.issubset( emails ), "Admin user listing should include all seeded users" @pytest.mark.permissions @pytest.mark.parametrize("auth_headers", ["viewer_token", "analyst_token"]) async def test_list_users_as_non_admin_returns_403( request: pytest.FixtureRequest, client: AsyncClient, auth_headers: str, ) -> None: """Non-admin roles should not be allowed to list all users.""" headers = request.getfixturevalue(auth_headers) response = await client.get("/api/users", headers=headers) assert response.status_code == 403, f"{auth_headers} should not be allowed to list users, got {response.text}" assert response.json()["detail"] == "Forbidden", "Non-admin user listing should be forbidden" @pytest.mark.permissions async def test_update_user_role_as_admin_succeeds( admin_client: AsyncClient, viewer_user: TestUserData, ) -> None: """Admins should be able to change another user's role.""" response = await admin_client.put(f"/api/users/{viewer_user.user.id}/role", json={"role": "analyst"}) assert response.status_code == 200, f"Admin role update should succeed, got {response.text}" assert response.json()["role"] == "analyst", "Role update response should contain the new role" @pytest.mark.permissions async def test_update_user_role_as_non_admin_returns_403( viewer_client: AsyncClient, admin_user: TestUserData, ) -> None: """Non-admin users should not be able to change roles.""" response = await viewer_client.put(f"/api/users/{admin_user.user.id}/role", json={"role": "viewer"}) assert response.status_code == 403, f"Non-admin role change should return 403, got {response.text}" @pytest.mark.validation async def test_update_user_role_invalid_role_returns_422( admin_client: AsyncClient, viewer_user: TestUserData, ) -> None: """Invalid roles should fail schema validation.""" response = await admin_client.put(f"/api/users/{viewer_user.user.id}/role", json={"role": "superuser"}) assert response.status_code == 422, f"Invalid role should return 422, got {response.text}" assert response.json()["detail"] == "Validation error", "Invalid role should use normalized 422 payload" @pytest.mark.permissions async def test_update_user_status_as_admin_succeeds( admin_client: AsyncClient, viewer_user: TestUserData, ) -> None: """Admins should be able to deactivate another user.""" response = await admin_client.put(f"/api/users/{viewer_user.user.id}/status", json={"is_active": False}) assert response.status_code == 200, f"Admin status update should succeed, got {response.text}" assert response.json()["is_active"] is False, "User status should reflect the requested inactive state" @pytest.mark.permissions async def test_update_user_status_as_non_admin_returns_403( analyst_client: AsyncClient, viewer_user: TestUserData, ) -> None: """Non-admin users should not be able to change user status.""" response = await analyst_client.put(f"/api/users/{viewer_user.user.id}/status", json={"is_active": False}) assert response.status_code == 403, f"Non-admin status change should return 403, got {response.text}" @pytest.mark.business_rules async def test_deactivate_self_returns_400(admin_client: AsyncClient, admin_user: TestUserData) -> None: """Users should not be able to deactivate themselves.""" response = await admin_client.put(f"/api/users/{admin_user.user.id}/status", json={"is_active": False}) assert response.status_code == 400, f"Self-deactivation should return 400, got {response.text}" assert response.json()["detail"] == "User cannot deactivate themselves", "Business rule message should be explicit" @pytest.mark.permissions @pytest.mark.parametrize("path_suffix,payload", [("role", {"role": "viewer"}), ("status", {"is_active": False})]) async def test_update_nonexistent_user_returns_404( admin_client: AsyncClient, path_suffix: str, payload: dict[str, object], ) -> None: """Updating a missing user should return 404.""" response = await admin_client.put(f"/api/users/99999/{path_suffix}", json=payload) assert response.status_code == 404, f"Missing user update should return 404, got {response.text}" assert response.json()["detail"] == "User not found", "Missing user response should use the expected detail"