# #region Test.AppDependencies [C:3] [TYPE Module] [SEMANTICS test,dependencies,fastapi,di,auth] # @BRIEF Unit tests for src/dependencies.py — DI functions, auth dependencies, feature flags. # @RELATION BINDS_TO -> [AppDependencies] # @TEST_EDGE: config_manager_lazy_init -> get_config_manager initializes on first call # @TEST_EDGE: plugin_loader_lazy_init -> get_plugin_loader initializes on first call # @TEST_EDGE: feature_disabled -> require_feature raises 404 # @TEST_EDGE: api_key_expired -> get_api_key_principal raises 401 # @TEST_EDGE: missing_auth -> require_api_key_or_jwt raises 401 when no auth provided import sys from pathlib import Path from unittest.mock import MagicMock, patch, AsyncMock import pytest from fastapi import HTTPException, Request, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError _src = str(Path(__file__).resolve().parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) # ── get_config_manager ── class TestGetConfigManager: """DI: get_config_manager() lazy init + reuse.""" def test_get_config_manager_initializes(self): """Lazy init: creates ConfigManager on first call.""" from src import dependencies dependencies.config_manager = None with patch('src.dependencies.init_db'), \ patch('src.dependencies.ConfigManager') as MockCM: instance = MagicMock() MockCM.return_value = instance result = dependencies.get_config_manager() assert result is instance MockCM.assert_called_once() def test_get_config_manager_reuses(self): """Reuse: returns existing instance on subsequent calls.""" from src import dependencies mock_cm = MagicMock() dependencies.config_manager = mock_cm result = dependencies.get_config_manager() assert result is mock_cm def test_get_config_manager_after_init(self): """Clearing global causes re-init.""" from src import dependencies dependencies.config_manager = None with patch('src.dependencies.init_db'), \ patch('src.dependencies.ConfigManager') as MockCM: instance = MagicMock() MockCM.return_value = instance result = dependencies.get_config_manager() assert result is instance # ── require_feature ── class TestRequireFeature: """DI: require_feature factory.""" def test_feature_enabled(self): """Enabled feature passes.""" from src.dependencies import require_feature mock_cm = MagicMock() mock_cm.get_config.return_value.settings.features.test_feature = True checker = require_feature("test_feature") # Should not raise checker(mock_cm) def test_feature_disabled_raises_404(self): """Disabled feature raises HTTPException 404.""" from src.dependencies import require_feature mock_cm = MagicMock() mock_cm.get_config.return_value.settings.features.test_feature = False checker = require_feature("test_feature") with pytest.raises(HTTPException) as exc: checker(mock_cm) assert exc.value.status_code == status.HTTP_404_NOT_FOUND assert "disabled" in exc.value.detail # ── get_plugin_loader ── class TestGetPluginLoader: """DI: get_plugin_loader() lazy init.""" def test_get_plugin_loader_initializes(self): """Lazy init: creates PluginLoader on first call.""" from src import dependencies dependencies.plugin_loader = None with patch('src.dependencies.PluginLoader') as MockPL, \ patch('src.dependencies.logger'): instance = MagicMock() MockPL.return_value = instance instance.get_all_plugin_configs.return_value = [] result = dependencies.get_plugin_loader() assert result is instance def test_get_plugin_loader_reuses(self): """Reuse: returns existing instance.""" from src import dependencies mock_pl = MagicMock() dependencies.plugin_loader = mock_pl result = dependencies.get_plugin_loader() assert result is mock_pl # ── get_task_manager ── class TestGetTaskManager: """DI: get_task_manager() lazy init.""" def test_get_task_manager_initializes(self): """Lazy init: creates TaskManager on first call.""" from src import dependencies dependencies.task_manager = None with patch('src.dependencies.get_plugin_loader'), \ patch('src.dependencies.TaskManager') as MockTM, \ patch('src.dependencies.logger'): instance = MagicMock() MockTM.return_value = instance result = dependencies.get_task_manager() assert result is instance def test_get_task_manager_reuses(self): """Reuse: returns existing instance.""" from src import dependencies mock_tm = MagicMock() dependencies.task_manager = mock_tm result = dependencies.get_task_manager() assert result is mock_tm # ── get_scheduler_service ── class TestGetSchedulerService: """DI: get_scheduler_service() lazy init.""" def test_get_scheduler_service_initializes(self): """Lazy init: creates SchedulerService on first call.""" from src import dependencies dependencies.scheduler_service = None with patch('src.dependencies.get_task_manager'), \ patch('src.dependencies.get_config_manager'), \ patch('src.dependencies.SchedulerService') as MockSS, \ patch('src.dependencies.logger'): instance = MagicMock() MockSS.return_value = instance result = dependencies.get_scheduler_service() assert result is instance def test_get_scheduler_service_reuses(self): """Reuse: returns existing instance.""" from src import dependencies mock_ss = MagicMock() dependencies.scheduler_service = mock_ss result = dependencies.get_scheduler_service() assert result is mock_ss # ── get_resource_service ── class TestGetResourceService: """DI: get_resource_service() lazy init.""" def test_get_resource_service_initializes(self): """Lazy init: creates ResourceService on first call.""" from src import dependencies dependencies.resource_service = None with patch('src.dependencies.ResourceService') as MockRS, \ patch('src.dependencies.logger'): instance = MagicMock() MockRS.return_value = instance result = dependencies.get_resource_service() assert result is instance def test_get_resource_service_reuses(self): """Reuse: returns existing instance.""" from src import dependencies mock_rs = MagicMock() dependencies.resource_service = mock_rs result = dependencies.get_resource_service() assert result is mock_rs # ── get_mapping_service ── class TestGetMappingService: """DI: get_mapping_service() creates new instance each time.""" def test_get_mapping_service(self): """Returns new MappingService with config_manager.""" from src.dependencies import get_mapping_service mock_cm = MagicMock() with patch('src.dependencies.get_config_manager', return_value=mock_cm), \ patch('src.dependencies.MappingService') as MockMS: instance = MagicMock() MockMS.return_value = instance result = get_mapping_service() assert result is instance MockMS.assert_called_once_with(mock_cm) # ── get_clean_release_repository ── class TestGetCleanReleaseRepository: """DI: get_clean_release_repository() returns shared singleton.""" def test_get_clean_release_repository(self): """Returns the module-level singleton.""" from src.dependencies import get_clean_release_repository result = get_clean_release_repository() assert result is not None def test_singleton_identity(self): """Multiple calls return same instance.""" from src.dependencies import get_clean_release_repository r1 = get_clean_release_repository() r2 = get_clean_release_repository() assert r1 is r2 # ── get_clean_release_facade ── class TestGetCleanReleaseFacade: """DI: get_clean_release_facade() builds facade with fresh DB session.""" def test_get_clean_release_facade(self): """Creates CleanReleaseFacade with all repos.""" from src.dependencies import get_clean_release_facade mock_db = MagicMock() mock_cm = MagicMock() with patch('src.dependencies.get_config_manager', return_value=mock_cm), \ patch('src.dependencies.CleanReleaseFacade') as MockFacade: instance = MagicMock() MockFacade.return_value = instance result = get_clean_release_facade(mock_db) assert result is instance # Verify all repos passed to facade call_kwargs = MockFacade.call_args[1] assert "candidate_repo" in call_kwargs assert "artifact_repo" in call_kwargs assert "manifest_repo" in call_kwargs assert "audit_repo" in call_kwargs # ── get_current_user ── class TestGetCurrentUser: """DI: get_current_user extracts user from JWT.""" def test_get_current_user_success(self): """Valid JWT returns User.""" from src.dependencies import get_current_user mock_user = MagicMock() mock_user.username = "testuser" mock_repo = MagicMock() mock_repo.get_user_by_username.return_value = mock_user with patch('src.dependencies.decode_token', return_value={"sub": "testuser"}), \ patch('src.dependencies.is_token_blacklisted', return_value=False), \ patch('src.dependencies.AuthRepository', return_value=mock_repo): result = get_current_user("valid_token", MagicMock()) assert result is mock_user def test_get_current_user_invalid_token(self): """Invalid JWT raises 401.""" from src.dependencies import get_current_user with patch('src.dependencies.decode_token', side_effect=JWTError("bad token")): with pytest.raises(HTTPException) as exc: get_current_user("bad_token", MagicMock()) assert exc.value.status_code == 401 def test_get_current_user_no_sub(self): """Token without sub raises 401.""" from src.dependencies import get_current_user with patch('src.dependencies.decode_token', return_value={"sub": None}): with pytest.raises(HTTPException) as exc: get_current_user("token", MagicMock()) assert exc.value.status_code == 401 def test_get_current_user_blacklisted(self): """Blacklisted token raises 401.""" from src.dependencies import get_current_user with patch('src.dependencies.decode_token', return_value={"sub": "user"}), \ patch('src.dependencies.is_token_blacklisted', return_value=True): with pytest.raises(HTTPException) as exc: get_current_user("revoked", MagicMock()) assert exc.value.status_code == 401 def test_get_current_user_not_found(self): """User not in DB raises 401.""" from src.dependencies import get_current_user mock_repo = MagicMock() mock_repo.get_user_by_username.return_value = None with patch('src.dependencies.decode_token', return_value={"sub": "nobody"}), \ patch('src.dependencies.is_token_blacklisted', return_value=False), \ patch('src.dependencies.AuthRepository', return_value=mock_repo): with pytest.raises(HTTPException) as exc: get_current_user("token", MagicMock()) assert exc.value.status_code == 401 # ── has_permission ── class TestHasPermission: """DI: has_permission factory.""" def _make_user(self, is_admin=False, permissions=None): """Build a mock User with roles and permissions.""" from src.models.auth import User, Role, Permission user = MagicMock(spec=User) if is_admin: role = MagicMock(spec=Role) role.name = "Admin" role.is_admin = True role.permissions = [] user.roles = [role] else: perms = [] for res, act in (permissions or []): p = MagicMock(spec=Permission) p.resource = res p.action = act perms.append(p) role = MagicMock(spec=Role) role.name = "User" role.is_admin = False role.permissions = perms user.roles = [role] return user def test_has_permission_admin(self): """Admin user has all permissions.""" from src.dependencies import has_permission checker = has_permission("dataset:session", "MANAGE") user = self._make_user(is_admin=True) result = checker(user) assert result is user def test_has_permission_granted(self): """User with matching permission passes.""" from src.dependencies import has_permission checker = has_permission("dataset:session", "READ") user = self._make_user(permissions=[("dataset:session", "READ")]) result = checker(user) assert result is user def test_has_permission_denied(self): """User without permission raises 403.""" from src.dependencies import has_permission checker = has_permission("dataset:session", "DELETE") user = self._make_user(permissions=[("dataset:session", "READ")]) with patch('src.core.auth.logger.log_security_event'): with pytest.raises(HTTPException) as exc: checker(user) assert exc.value.status_code == 403 # ── get_api_key_principal ── class TestGetApiKeyPrincipal: """DI: get_api_key_principal extracts principal from API key header.""" @pytest.mark.asyncio async def test_no_header_returns_none(self): """No X-API-Key header returns None.""" from src.dependencies import get_api_key_principal request = MagicMock(spec=Request) request.headers.get.return_value = None result = await get_api_key_principal(request, MagicMock()) assert result is None @pytest.mark.asyncio async def test_valid_key(self): """Valid API key returns APIKeyPrincipal.""" from src.dependencies import get_api_key_principal request = MagicMock(spec=Request) request.headers.get.return_value = "valid-key-123" mock_api_key = MagicMock() mock_api_key.id = "key-1" mock_api_key.name = "Test Key" mock_api_key.prefix = "prefix" mock_api_key.active = True mock_api_key.expires_at = None mock_api_key.environment_id = "env-1" mock_api_key.permissions = ["dataset:read"] mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key with patch('src.core.auth.api_key.hash_api_key', return_value="hashed_key"): result = await get_api_key_principal(request, mock_db) assert result is not None assert result.name == "Test Key" assert result.api_key_id == "key-1" assert result.environment_id == "env-1" assert "dataset:read" in result.permissions @pytest.mark.asyncio async def test_invalid_key_raises(self): """Invalid API key raises 401.""" from src.dependencies import get_api_key_principal request = MagicMock(spec=Request) request.headers.get.return_value = "bad-key" mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None with patch('src.core.auth.api_key.hash_api_key', return_value="bad_hash"): with pytest.raises(HTTPException) as exc: await get_api_key_principal(request, mock_db) assert exc.value.status_code == 401 @pytest.mark.asyncio async def test_revoked_key_raises(self): """Revoked API key raises 401.""" from src.dependencies import get_api_key_principal request = MagicMock(spec=Request) request.headers.get.return_value = "revoked-key" mock_api_key = MagicMock() mock_api_key.active = False mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key with patch('src.core.auth.api_key.hash_api_key', return_value="hash"): with pytest.raises(HTTPException) as exc: await get_api_key_principal(request, mock_db) assert exc.value.status_code == 401 assert "revoked" in exc.value.detail.lower() @pytest.mark.asyncio async def test_expired_key_raises(self): """Expired API key raises 401.""" from datetime import UTC, datetime, timedelta from src.dependencies import get_api_key_principal request = MagicMock(spec=Request) request.headers.get.return_value = "expired-key" mock_api_key = MagicMock() mock_api_key.active = True mock_api_key.expires_at = datetime.now(UTC) - timedelta(hours=1) mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key with patch('src.core.auth.api_key.hash_api_key', return_value="hash"): with pytest.raises(HTTPException) as exc: await get_api_key_principal(request, mock_db) assert exc.value.status_code == 401 assert "expired" in exc.value.detail.lower() @pytest.mark.asyncio async def test_expired_key_no_tz_raises(self): """Expired API key without timezone raises 401.""" from datetime import datetime from src.dependencies import get_api_key_principal request = MagicMock(spec=Request) request.headers.get.return_value = "expired-key" mock_api_key = MagicMock() mock_api_key.active = True mock_api_key.expires_at = datetime(2020, 1, 1, 0, 0, 0) # naive, clearly past mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key with patch('src.core.auth.api_key.hash_api_key', return_value="hash"): with pytest.raises(HTTPException) as exc: await get_api_key_principal(request, mock_db) assert exc.value.status_code == 401 # ── check_api_key_environment_scope ── class TestCheckApiKeyEnvironmentScope: """check_api_key_environment_scope enforces key scope.""" @pytest.mark.asyncio async def test_no_key_header_returns(self): """No API key header → no check.""" from src.dependencies import check_api_key_environment_scope request = MagicMock(spec=Request) request.headers.get.return_value = None # Should not raise await check_api_key_environment_scope(request, MagicMock(), "env-1") @pytest.mark.asyncio async def test_key_scope_matches(self): """Key scope matches target → pass.""" from src.dependencies import check_api_key_environment_scope request = MagicMock(spec=Request) request.headers.get.return_value = "valid-key" mock_api_key = MagicMock() mock_api_key.environment_id = "env-1" mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key with patch('src.core.auth.api_key.hash_api_key', return_value="hash"): await check_api_key_environment_scope(request, mock_db, "env-1") @pytest.mark.asyncio async def test_key_scope_mismatch_raises(self): """Key scope differs from target → 400.""" from src.dependencies import check_api_key_environment_scope request = MagicMock(spec=Request) request.headers.get.return_value = "wrong-key" mock_api_key = MagicMock() mock_api_key.environment_id = "env-1" mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key with patch('src.core.auth.api_key.hash_api_key', return_value="hash"): with pytest.raises(HTTPException) as exc: await check_api_key_environment_scope(request, mock_db, "env-2") assert exc.value.status_code == 400 assert "restricted" in exc.value.detail.lower() @pytest.mark.asyncio async def test_key_not_found_returns(self): """Key not in DB → pass (auth handled elsewhere).""" from src.dependencies import check_api_key_environment_scope request = MagicMock(spec=Request) request.headers.get.return_value = "unknown-key" mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None with patch('src.core.auth.api_key.hash_api_key', return_value="hash"): await check_api_key_environment_scope(request, mock_db, "env-1") # ── require_api_key_or_jwt (partial — JWT path) ── class TestRequireApiKeyOrJwt: """require_api_key_or_jwt factory — JWT path testing.""" @pytest.mark.asyncio async def test_jwt_success_admin(self): """JWT with Admin role passes.""" from src.dependencies import require_api_key_or_jwt mock_user = MagicMock() mock_user.username = "admin" role = MagicMock() role.name = "Admin" role.permissions = [] mock_user.roles = [role] mock_repo = MagicMock() mock_repo.get_user_by_username.return_value = mock_user # Build the dependency dep = require_api_key_or_jwt( required_api_permission="dataset:read", required_jwt_resource="dataset", required_jwt_action="read", environment_id="env-1", ) mock_request = MagicMock(spec=Request) mock_request.json = AsyncMock(return_value={}) with patch('src.dependencies.decode_token', return_value={"sub": "admin"}), \ patch('src.dependencies.AuthRepository', return_value=mock_repo), \ patch('src.core.auth.api_key.hash_api_key'): result = await dep(mock_request, MagicMock(), MagicMock(), "valid_token") assert result == "jwt:admin" @pytest.mark.asyncio async def test_no_auth_raises_401(self): """Neither JWT nor API key → 401.""" from src.dependencies import require_api_key_or_jwt dep = require_api_key_or_jwt( required_api_permission="test", required_jwt_resource="test", required_jwt_action="test", ) mock_request = MagicMock(spec=Request) mock_request.headers.get.return_value = None mock_request.json = AsyncMock(return_value={}) with pytest.raises(HTTPException) as exc: await dep(mock_request, MagicMock(), MagicMock(), None) assert exc.value.status_code == 401 assert "Authentication required" in exc.value.detail