654 lines
23 KiB
Python
654 lines
23 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
|
|
|
|
_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=Exception("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.dependencies.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.dependencies.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.dependencies.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.dependencies.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.dependencies.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, 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() - timedelta(hours=1) # no tz
|
|
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_api_key
|
|
|
|
with patch('src.dependencies.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.dependencies.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.dependencies.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.dependencies.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.dependencies.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
|