"""Tests for authentication controller endpoints.""" import uuid from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi.testclient import TestClient from src.main import app @pytest.fixture def client(): """Create a test client.""" return TestClient(app) # ============================================================================= # OpenAPI Spec Tests # ============================================================================= class TestAuthOpenAPISpec: """Test that auth endpoints are documented in OpenAPI spec.""" def test_auth_sync_in_openapi(self, client): """Auth sync endpoint should be in OpenAPI spec.""" response = client.get("/openapi.json") assert response.status_code == 200 spec = response.json() assert "/auth/sync" in spec["paths"] assert "post" in spec["paths"]["/auth/sync"] def test_auth_users_in_openapi(self, client): """Auth users endpoint should be in OpenAPI spec.""" response = client.get("/openapi.json") spec = response.json() assert "/auth/users" in spec["paths"] assert "get" in spec["paths"]["/auth/users"] def test_auth_groups_in_openapi(self, client): """Auth groups endpoint should be in OpenAPI spec.""" response = client.get("/openapi.json") spec = response.json() assert "/auth/groups" in spec["paths"] assert "get" in spec["paths"]["/auth/groups"] def test_auth_roles_in_openapi(self, client): """Auth roles endpoint should be in OpenAPI spec.""" response = client.get("/openapi.json") spec = response.json() assert "/auth/roles" in spec["paths"] assert "get" in spec["paths"]["/auth/roles"] def test_group_role_assignment_in_openapi(self, client): """Group role assignment endpoints should be in OpenAPI spec.""" response = client.get("/openapi.json") spec = response.json() path = "/auth/groups/{group_id}/roles/{role_id}" assert path in spec["paths"] assert "post" in spec["paths"][path] # Assign assert "delete" in spec["paths"][path] # Remove def test_sync_from_authentik_endpoints(self, client): """Sync from Authentik endpoints should be in OpenAPI spec.""" response = client.get("/openapi.json") spec = response.json() assert "/auth/users/sync-from-authentik" in spec["paths"] assert "/auth/groups/sync-from-authentik" in spec["paths"] # ============================================================================= # Controller Module Tests # ============================================================================= class TestAuthControllerModule: """Test auth controller module imports and configuration.""" def test_controller_imports(self): """Auth controller should be importable.""" from src.domains.auth.controller import AuthController, auth_controller assert AuthController is not None assert auth_controller is not None def test_controller_has_correct_prefix(self): """Auth controller should have /auth prefix.""" from src.domains.auth.controller import auth_controller assert auth_controller.prefix == "/auth" def test_controller_has_correct_tags(self): """Auth controller should have Authentication tag.""" from src.domains.auth.controller import auth_controller assert "Authentication" in auth_controller.tags # ============================================================================= # Sync Endpoint Tests # ============================================================================= class TestSyncEndpoint: """Test POST /auth/sync endpoint.""" def test_sync_requires_access_token(self, client): """Sync should require access_token in body.""" response = client.post("/auth/sync", json={}) assert response.status_code == 422 # Validation error def test_sync_with_invalid_token(self, client): """Sync should return 401 for invalid token.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.validate_token = AsyncMock( side_effect=ValueError("Invalid or expired token") ) MockService.return_value = mock_instance response = client.post( "/auth/sync", json={"access_token": "invalid_token"}, ) assert response.status_code == 401 # ============================================================================= # Users Endpoint Tests # ============================================================================= class TestUsersEndpoint: """Test GET /auth/users endpoint.""" def test_list_users_returns_list(self, client): """List users should return paginated response.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.list_users = AsyncMock(return_value=([], 0)) MockService.return_value = mock_instance response = client.get("/auth/users") assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data def test_list_users_with_search(self, client): """List users should accept search parameter.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.list_users = AsyncMock(return_value=([], 0)) MockService.return_value = mock_instance response = client.get("/auth/users?search=test") assert response.status_code == 200 def test_list_users_with_pagination(self, client): """List users should accept pagination parameters.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.list_users = AsyncMock(return_value=([], 0)) MockService.return_value = mock_instance response = client.get("/auth/users?offset=10&limit=20") assert response.status_code == 200 def test_list_users_limit_validation(self, client): """List users should reject limit > 100.""" response = client.get("/auth/users?limit=200") assert response.status_code == 422 # ============================================================================= # Groups Endpoint Tests # ============================================================================= class TestGroupsEndpoint: """Test GET /auth/groups endpoint.""" def test_list_groups_returns_list(self, client): """List groups should return paginated response.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.list_groups = AsyncMock(return_value=([], 0)) MockService.return_value = mock_instance response = client.get("/auth/groups") assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data def test_list_groups_with_search(self, client): """List groups should accept search parameter.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.list_groups = AsyncMock(return_value=([], 0)) MockService.return_value = mock_instance response = client.get("/auth/groups?search=admin") assert response.status_code == 200 # ============================================================================= # Roles Endpoint Tests # ============================================================================= class TestRolesEndpoint: """Test GET /auth/roles endpoint.""" def test_list_roles_returns_list(self, client): """List roles should return all roles.""" with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.list_roles = AsyncMock(return_value=[]) mock_instance.roles_to_schema = MagicMock(return_value=[]) MockService.return_value = mock_instance response = client.get("/auth/roles") assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data # ============================================================================= # Group-Role Assignment Endpoint Tests # ============================================================================= class TestGroupRoleAssignmentEndpoints: """Test group-role assignment and removal endpoints.""" def test_assign_role_to_group_success(self, client): """POST /auth/groups/{id}/roles/{id} should assign role.""" group_id = str(uuid.uuid4()) role_id = str(uuid.uuid4()) mock_group = MagicMock() mock_group.id = uuid.UUID(group_id) mock_group.name = "Test Group" mock_group.roles = [] with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.assign_role_to_group = AsyncMock(return_value=mock_group) MockService.return_value = mock_instance response = client.post(f"/auth/groups/{group_id}/roles/{role_id}") assert response.status_code == 200 data = response.json() assert data["group_id"] == group_id assert data["group_name"] == "Test Group" assert "roles" in data def test_assign_role_to_group_not_found(self, client): """POST should return 404 when group not found.""" group_id = str(uuid.uuid4()) role_id = str(uuid.uuid4()) with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.assign_role_to_group = AsyncMock( side_effect=ValueError("Group not found") ) MockService.return_value = mock_instance response = client.post(f"/auth/groups/{group_id}/roles/{role_id}") assert response.status_code == 404 def test_remove_role_from_group_success(self, client): """DELETE /auth/groups/{id}/roles/{id} should remove role.""" group_id = str(uuid.uuid4()) role_id = str(uuid.uuid4()) mock_group = MagicMock() mock_group.id = uuid.UUID(group_id) mock_group.name = "Test Group" mock_group.roles = [] with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.remove_role_from_group = AsyncMock(return_value=mock_group) MockService.return_value = mock_instance response = client.delete(f"/auth/groups/{group_id}/roles/{role_id}") assert response.status_code == 200 data = response.json() assert data["group_id"] == group_id def test_remove_role_from_group_not_found(self, client): """DELETE should return 404 when role not found.""" group_id = str(uuid.uuid4()) role_id = str(uuid.uuid4()) with patch("src.domains.auth.controller.AuthService") as MockService: mock_instance = MagicMock() mock_instance.remove_role_from_group = AsyncMock( side_effect=ValueError("Role not found") ) MockService.return_value = mock_instance response = client.delete(f"/auth/groups/{group_id}/roles/{role_id}") assert response.status_code == 404 def test_assign_role_invalid_uuid(self, client): """POST should return 422 for invalid UUID.""" response = client.post("/auth/groups/not-a-uuid/roles/also-not-uuid") assert response.status_code == 422 # ============================================================================= # Me Endpoint Tests # ============================================================================= class TestMeEndpoint: """Test GET /auth/me endpoint.""" def test_me_not_implemented(self, client): """Me endpoint should return 501 (not implemented yet).""" response = client.get("/auth/me") assert response.status_code == 501 # ============================================================================= # Schema Tests # ============================================================================= class TestAuthSchemas: """Test auth schema imports and structure.""" def test_all_schemas_importable(self): """All auth schemas should be importable.""" from src.domains.auth.schemas import ( AuthSyncRequest, AuthSyncResponse, RoleSchema, UserSchema, UserPreferencesSchema, TokenInfoSchema, UserListItemSchema, UsersListResponse, BulkSyncResultSchema, GroupListItemSchema, GroupsListResponse, RolesListResponse, GroupRoleAssignmentResponse, ) assert AuthSyncRequest is not None assert AuthSyncResponse is not None assert RoleSchema is not None assert UserSchema is not None assert UserPreferencesSchema is not None assert TokenInfoSchema is not None assert UserListItemSchema is not None assert UsersListResponse is not None assert BulkSyncResultSchema is not None assert GroupListItemSchema is not None assert GroupsListResponse is not None assert RolesListResponse is not None assert GroupRoleAssignmentResponse is not None def test_role_schema_includes_id(self): """RoleSchema should include id field.""" from src.domains.auth.schemas import RoleSchema schema = RoleSchema( id=uuid.uuid4(), name="test.general:admin", domain="test", category="general", action="admin", ) assert schema.id is not None def test_role_schema_includes_category(self): """RoleSchema should include category field.""" from src.domains.auth.schemas import RoleSchema schema = RoleSchema( id=uuid.uuid4(), name="test.specific:viewer", domain="test", category="specific", action="viewer", ) assert schema.category == "specific" def test_group_list_item_includes_roles(self): """GroupListItemSchema should include roles list.""" from src.domains.auth.schemas import GroupListItemSchema schema = GroupListItemSchema( id=uuid.uuid4(), authentik_id=uuid.uuid4(), name="Test Group", is_superuser=False, parent_name=None, member_count=5, synced_at=datetime.now(timezone.utc), roles=["admin.general:admin", "media.general:viewer"], ) assert len(schema.roles) == 2 # ============================================================================= # Model Tests # ============================================================================= class TestAuthModels: """Test auth model imports.""" def test_all_models_importable(self): """All auth models should be importable.""" from src.domains.auth.models import ( User, Role, UserRole, Group, UserPreferences, ApiKey, user_groups, group_roles, ) assert User is not None assert Role is not None assert UserRole is not None assert Group is not None assert UserPreferences is not None assert ApiKey is not None assert user_groups is not None assert group_roles is not None def test_role_model_has_category(self): """Role model should have category attribute.""" from src.domains.auth.models import Role role = Role( name="test.general:admin", domain="test", category="general", action="admin", ) assert role.category == "general" def test_group_has_roles_relationship(self): """Group model should have roles relationship.""" from src.domains.auth.models import Group assert hasattr(Group, "roles") # ============================================================================= # Permission System Tests (from oidc.py) # ============================================================================= class TestPermissionSystem: """Test permission checking functions.""" def test_permission_constants_defined(self): """Permission constants should be defined.""" from src.domains.auth.oidc import ( ACTION_HIERARCHY, VALID_DOMAINS, DEFAULT_CATEGORY, ) assert "viewer" in ACTION_HIERARCHY assert "user" in ACTION_HIERARCHY assert "editor" in ACTION_HIERARCHY assert "admin" in ACTION_HIERARCHY assert "control-room" in VALID_DOMAINS assert "media" in VALID_DOMAINS assert "admin" in VALID_DOMAINS assert DEFAULT_CATEGORY == "general" def test_action_hierarchy_ordering(self): """Action hierarchy should be ordered correctly.""" from src.domains.auth.oidc import ACTION_HIERARCHY assert ACTION_HIERARCHY["viewer"] < ACTION_HIERARCHY["user"] assert ACTION_HIERARCHY["user"] < ACTION_HIERARCHY["editor"] assert ACTION_HIERARCHY["editor"] < ACTION_HIERARCHY["admin"] def test_require_permission_importable(self): """require_permission should be importable.""" from src.domains.auth.oidc import require_permission, require_any_permission assert callable(require_permission) assert callable(require_any_permission) def test_require_permission_returns_dependency(self): """require_permission should return a callable dependency.""" from src.domains.auth.oidc import require_permission dependency = require_permission("control-room", "admin") assert callable(dependency) def test_require_any_permission_returns_dependency(self): """require_any_permission should return a callable dependency.""" from src.domains.auth.oidc import require_any_permission dependency = require_any_permission( ("control-room", "admin"), ("media", "editor"), ) assert callable(dependency)