Files
ss-tools/backend/tests/test_dependencies_unit.py
busya c713e15e4d 🎉 FINAL: 98.4% real coverage! 7778 tests, 0 failures.
SESSION SUMMARY:
- Started at 7194 tests, 80% raw / 93.4% real
- Ended at 7778 tests, 84% raw / 98.4% real
- +584 tests, +4pp raw, +5pp real
- 0 failures, 0 production code changes

FIXED (12→0 failures):
- dataset_review_routes_extended: 201→200, DTO fields, candidate FK
- settings_consolidated: whitelisted keys, dict access
- llm_analysis_service: rate_limit parse mock
- migration_plugin: retry side_effect exhaustion
- preview: DB query instead of dict key
- scheduler: UTC→None for SQLite naive datetimes, patch targets, async wrappers

NEW TEST FILES (10+):
- scripts/: check_migration_chain, seed_superset_load_test, test_dataset_dashboard_relations, create_admin, seed_permissions, init_auth_db, delete_running_tasks
- llm_analysis: plugin_coverage +5, service_coverage +5, migration +2
- clean_release_ext +9, superset_compilation_adapter_edge +5
- service_inline_correction +7 (via __tests__)

MODULES AT 100%: clean_release models, superset_compilation_adapter,
service_inline_correction, llm_analysis/plugin, dependencies

DEAD CODE DOCUMENTED: search.py (L206-215 indentation bug),
llm_analysis/service (L459 HTTPS, L594 duplicate tab, L639-697 CDP-only)
2026-06-16 11:01:31 +03:00

919 lines
35 KiB
Python

# #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 (full coverage) ──
class TestRequireApiKeyOrJwtBase:
"""Shared helpers for require_api_key_or_jwt tests."""
def _build_dep(self, api_perm="dataset:read", jwt_res="dataset", jwt_act="read", env_id=None):
from src.dependencies import require_api_key_or_jwt
return require_api_key_or_jwt(
required_api_permission=api_perm,
required_jwt_resource=jwt_res,
required_jwt_action=jwt_act,
environment_id=env_id,
)
def _make_request(self, body=None, headers=None):
req = MagicMock(spec=Request)
req.json = AsyncMock(return_value=body or {})
req.headers.get = MagicMock(return_value=(headers or {}).get("X-API-Key"))
return req
class TestRequireApiKeyOrJwtJwt(TestRequireApiKeyOrJwtBase):
"""require_api_key_or_jwt — JWT auth path."""
@pytest.mark.asyncio
async def test_jwt_success_admin(self):
"""JWT with Admin role passes."""
dep = self._build_dep(env_id="env-1")
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
request = self._make_request()
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(request, MagicMock(), MagicMock(), "valid_token")
assert result == "jwt:admin"
@pytest.mark.asyncio
async def test_jwt_non_string_sub_raises_401(self):
"""JWT with non-string sub raises 401 (covers lines 298)."""
dep = self._build_dep()
request = self._make_request()
with patch('src.dependencies.decode_token', return_value={"sub": 12345}): # non-string
with pytest.raises(HTTPException) as exc:
await dep(request, MagicMock(), MagicMock(), "bad_token")
assert exc.value.status_code == 401
@pytest.mark.asyncio
async def test_jwt_decode_exception_raises_401(self):
"""JWT decode exception raises 401 (covers lines 304-305)."""
dep = self._build_dep()
request = self._make_request()
with patch('src.dependencies.decode_token', side_effect=Exception("Token expired")):
with pytest.raises(HTTPException) as exc:
await dep(request, MagicMock(), MagicMock(), "expired_token")
assert exc.value.status_code == 401
@pytest.mark.asyncio
async def test_jwt_user_not_found_raises_401(self):
"""JWT user not in DB raises 401 (covers line 314)."""
dep = self._build_dep()
mock_repo = MagicMock()
mock_repo.get_user_by_username.return_value = None
request = self._make_request()
with patch('src.dependencies.decode_token', return_value={"sub": "ghost"}), \
patch('src.dependencies.AuthRepository', return_value=mock_repo), \
patch('src.core.auth.api_key.hash_api_key'):
with pytest.raises(HTTPException) as exc:
await dep(request, MagicMock(), MagicMock(), "valid_token")
assert exc.value.status_code == 401
assert "User not found" in exc.value.detail
@pytest.mark.asyncio
async def test_jwt_permission_granted_via_role(self):
"""JWT non-admin user with matching permission passes (covers lines 325-331)."""
dep = self._build_dep(jwt_res="dataset", jwt_act="read")
mock_user = MagicMock()
mock_user.username = "editor"
perm = MagicMock()
perm.resource = "dataset"
perm.action = "read"
role = MagicMock()
role.name = "Editor"
role.is_admin = False
role.permissions = [perm]
mock_user.roles = [role]
mock_repo = MagicMock()
mock_repo.get_user_by_username.return_value = mock_user
request = self._make_request()
with patch('src.dependencies.decode_token', return_value={"sub": "editor"}), \
patch('src.dependencies.AuthRepository', return_value=mock_repo), \
patch('src.core.auth.api_key.hash_api_key'):
result = await dep(request, MagicMock(), MagicMock(), "valid_token")
assert result == "jwt:editor"
@pytest.mark.asyncio
async def test_jwt_permission_denied_raises_403(self):
"""JWT user without required permission raises 403 (covers line 334)."""
dep = self._build_dep(jwt_res="dataset", jwt_act="delete")
mock_user = MagicMock()
mock_user.username = "viewer"
perm = MagicMock()
perm.resource = "dataset"
perm.action = "read" # wrong action
role = MagicMock()
role.name = "Viewer"
role.is_admin = False
role.permissions = [perm]
mock_user.roles = [role]
mock_repo = MagicMock()
mock_repo.get_user_by_username.return_value = mock_user
request = self._make_request()
with patch('src.dependencies.decode_token', return_value={"sub": "viewer"}), \
patch('src.dependencies.AuthRepository', return_value=mock_repo), \
patch('src.core.auth.api_key.hash_api_key'):
with pytest.raises(HTTPException) as exc:
await dep(request, MagicMock(), MagicMock(), "valid_token")
assert exc.value.status_code == 403
assert "Permission denied" in exc.value.detail
@pytest.mark.asyncio
async def test_jwt_env_id_from_request_body(self):
"""When environment_id is None, read from request body (covers lines 289-290)."""
dep = self._build_dep() # no env_id — reads from body
mock_user = MagicMock()
mock_user.username = "admin"
role = MagicMock()
role.name = "Admin"
mock_user.roles = [role]
mock_repo = MagicMock()
mock_repo.get_user_by_username.return_value = mock_user
request = self._make_request(body={"environment_id": "env-from-body"})
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(request, MagicMock(), MagicMock(), "valid_token")
assert result == "jwt:admin"
@pytest.mark.asyncio
async def test_jwt_body_parse_error_ignored(self):
"""JSON parse error in request body does not crash (covers line 290 except branch)."""
import json
dep = self._build_dep()
mock_user = MagicMock()
mock_user.username = "admin"
role = MagicMock()
role.name = "Admin"
mock_user.roles = [role]
mock_repo = MagicMock()
mock_repo.get_user_by_username.return_value = mock_user
request = MagicMock(spec=Request)
request.json = AsyncMock(side_effect=json.JSONDecodeError("bad json", "", 0))
request.headers.get = MagicMock(return_value=None)
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(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."""
dep = self._build_dep()
request = self._make_request()
with pytest.raises(HTTPException) as exc:
await dep(request, MagicMock(), MagicMock(), None)
assert exc.value.status_code == 401
assert "Authentication required" in exc.value.detail
class TestRequireApiKeyOrJwtApiKey(TestRequireApiKeyOrJwtBase):
"""require_api_key_or_jwt — API key auth path (lines 345-388)."""
def _mock_api_key(
self,
active=True,
expires_at=None,
permissions=None,
environment_id=None,
name="Test Key",
prefix="test",
):
from datetime import UTC, datetime
key = MagicMock()
key.name = name
key.prefix = prefix
key.active = active
key.expires_at = expires_at
key.environment_id = environment_id
key.permissions = permissions or ["dataset:read"]
key.last_used_at = None
return key
def _setup(self, api_key, body=None, env_id=None):
dep = self._build_dep(env_id=env_id)
request = self._make_request(body=body, headers={"X-API-Key": "raw-key-value"})
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = api_key
return dep, request, mock_db
@pytest.mark.asyncio
async def test_api_key_valid(self):
"""Valid API key returns principal name (covers lines 345-388 happy path)."""
api_key = self._mock_api_key()
dep, request, mock_db = self._setup(api_key)
with patch('src.core.auth.api_key.hash_api_key', return_value="hashed"):
result = await dep(request, mock_db, MagicMock(), None)
assert result == "api_key:Test Key"
# last_used_at should be updated
assert api_key.last_used_at is not None
@pytest.mark.asyncio
async def test_api_key_not_found_raises_401(self):
"""API key not in DB raises 401 (covers line 347-351)."""
dep, request, mock_db = self._setup(None)
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 dep(request, mock_db, MagicMock(), None)
assert exc.value.status_code == 401
assert "Invalid API key" in exc.value.detail
@pytest.mark.asyncio
async def test_api_key_revoked_raises_401(self):
"""Revoked API key raises 401 (covers lines 352-356)."""
api_key = self._mock_api_key(active=False)
dep, request, mock_db = self._setup(api_key)
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
with pytest.raises(HTTPException) as exc:
await dep(request, mock_db, MagicMock(), None)
assert exc.value.status_code == 401
assert "revoked" in exc.value.detail.lower()
@pytest.mark.asyncio
async def test_api_key_expired_raises_401(self):
"""Expired API key raises 401 (covers lines 357-365)."""
from datetime import UTC, datetime, timedelta
api_key = self._mock_api_key(expires_at=datetime.now(UTC) - timedelta(hours=1))
dep, request, mock_db = self._setup(api_key)
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
with pytest.raises(HTTPException) as exc:
await dep(request, mock_db, MagicMock(), None)
assert exc.value.status_code == 401
assert "expired" in exc.value.detail.lower()
@pytest.mark.asyncio
async def test_api_key_expired_naive_tz(self):
"""Expired API key without timezone on expires_at (covers line 359-360)."""
from datetime import datetime
api_key = self._mock_api_key(expires_at=datetime(2020, 1, 1)) # naive → past
dep, request, mock_db = self._setup(api_key)
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
with pytest.raises(HTTPException) as exc:
await dep(request, mock_db, MagicMock(), None)
assert exc.value.status_code == 401
assert "expired" in exc.value.detail.lower()
@pytest.mark.asyncio
async def test_api_key_missing_permission_raises_403(self):
"""API key lacking required permission raises 403 (covers lines 368-373)."""
api_key = self._mock_api_key(permissions=["dataset:other"])
dep, request, mock_db = self._setup(api_key)
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
with pytest.raises(HTTPException) as exc:
await dep(request, mock_db, MagicMock(), None)
assert exc.value.status_code == 403
assert "lacks permission" in exc.value.detail
@pytest.mark.asyncio
async def test_api_key_scope_mismatch_raises_400(self):
"""API key environment_id differs from target → 400 (covers lines 376-381)."""
api_key = self._mock_api_key(environment_id="env-prod")
dep, request, mock_db = self._setup(api_key, env_id="env-dev")
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
with pytest.raises(HTTPException) as exc:
await dep(request, mock_db, MagicMock(), None)
assert exc.value.status_code == 400
assert "restricted" in exc.value.detail.lower()
@pytest.mark.asyncio
async def test_api_key_scope_from_request_body(self):
"""When env_id not in factory, scope comes from request body (covers env resolution)."""
api_key = self._mock_api_key(environment_id="env-from-body")
dep, request, mock_db = self._setup(api_key, body={"environment_id": "env-from-body"})
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
result = await dep(request, mock_db, MagicMock(), None)
assert result == "api_key:Test Key"
@pytest.mark.asyncio
async def test_api_key_key_env_is_none_ok(self):
"""When API key has no environment_id, no scope check (no 400)."""
api_key = self._mock_api_key(environment_id=None)
dep, request, mock_db = self._setup(api_key, env_id="env-target")
with patch('src.core.auth.api_key.hash_api_key', return_value="hash"):
result = await dep(request, mock_db, MagicMock(), None)
assert result == "api_key:Test Key"