"""Tests for the TeamHUB trusted-identity adapter and enforcement middleware.""" from __future__ import annotations import importlib import os import pytest from fastapi.testclient import TestClient from app.integrations.identity import ( SCOPE_DOCUMENTS_ADMIN, SCOPE_DOCUMENTS_INGEST, SCOPE_DOCUMENTS_READ, parse_identity, ) def test_parse_returns_none_without_actor(): assert parse_identity({}) is None assert parse_identity({"X-TeamHub-Actor": " "}) is None def test_parse_reads_all_headers_and_splits_csv(): ident = parse_identity( { "X-TeamHub-Actor": "alice", "X-TeamHub-Actor-Id": "person-42", "X-TeamHub-App": "legacyhub", "X-TeamHub-Role": "admin", "X-TeamHub-Roles": "admin, qa", "X-TeamHub-Scopes": "documents:read, documents:approve", "X-TeamHub-Entitlements-Version": "v7", } ) assert ident is not None assert ident.actor == "alice" assert ident.actor_id == "person-42" assert ident.app == "legacyhub" assert ident.primary_role == "admin" assert ident.roles == ("admin", "qa") assert ident.scopes == ("documents:read", "documents:approve") assert ident.entitlements_version == "v7" def test_primary_role_is_included_in_roles(): ident = parse_identity({"X-TeamHub-Actor": "bob", "X-TeamHub-Role": "viewer"}) assert ident is not None assert "viewer" in ident.roles def test_resolved_scopes_combine_roles_and_explicit_scopes(): ident = parse_identity( {"X-TeamHub-Actor": "carol", "X-TeamHub-Roles": "ingestor", "X-TeamHub-Scopes": "extra:scope"} ) assert ident is not None resolved = ident.resolved_scopes() assert SCOPE_DOCUMENTS_READ in resolved assert SCOPE_DOCUMENTS_INGEST in resolved assert "extra:scope" in resolved assert SCOPE_DOCUMENTS_ADMIN not in resolved assert ident.has_scope(SCOPE_DOCUMENTS_INGEST) assert not ident.has_scope(SCOPE_DOCUMENTS_ADMIN) def test_admin_role_resolves_admin_scope(): ident = parse_identity({"X-TeamHub-Actor": "root", "X-TeamHub-Roles": "admin"}) assert ident is not None assert ident.has_scope(SCOPE_DOCUMENTS_ADMIN) # ---- enforcement middleware integration ---- @pytest.fixture def identity_required_app(monkeypatch): monkeypatch.setenv("AUTH_REQUIRE_IDENTITY", "true") monkeypatch.delenv("API_KEY", raising=False) import app.config as cfg import app.main as main_module cfg.get_settings.cache_clear() importlib.reload(cfg) importlib.reload(main_module) yield main_module.app for _k in ("API_KEY", "INGEST_API_KEY", "AUTH_REQUIRE_IDENTITY"): os.environ.pop(_k, None) cfg.get_settings.cache_clear() importlib.reload(cfg) importlib.reload(main_module) def _patch_health(monkeypatch): from app.api import routes_health from app.api.schemas import ComponentHealth for name in ("_check_postgres", "_check_minio", "_check_opensearch", "_check_qdrant", "_check_redis"): monkeypatch.setattr( routes_health, name, lambda n=name: ComponentHealth(name=n.removeprefix("_check_"), status="ok", detail={}), ) def test_health_open_without_identity(identity_required_app, monkeypatch): from app.config import settings _patch_health(monkeypatch) client = TestClient(identity_required_app) res = client.get(f"{settings.app_api_prefix}/health") assert res.status_code == 200 def test_protected_route_rejected_without_identity(identity_required_app, monkeypatch): from app.config import settings from app.indexing import hybrid_search monkeypatch.setattr(hybrid_search, "run_search", lambda req: pytest.fail("must not run")) client = TestClient(identity_required_app) res = client.post( f"{settings.app_api_prefix}/search", json={"query": "x", "limit": 1, "filters": {}, "search_mode": "hybrid"}, ) assert res.status_code == 401 assert "identity" in res.json()["detail"] def test_protected_route_allowed_with_identity(identity_required_app, monkeypatch): from app.api.schemas import SearchResponse from app.config import settings from app.indexing import hybrid_search monkeypatch.setattr( hybrid_search, "run_search", lambda req: SearchResponse( query=req.query, mode=req.search_mode, total_candidates=0, reranked=False, results=[] ), ) client = TestClient(identity_required_app) res = client.post( f"{settings.app_api_prefix}/search", headers={"X-TeamHub-Actor": "alice", "X-TeamHub-Roles": "qa"}, json={"query": "x", "limit": 1, "filters": {}, "search_mode": "hybrid"}, ) assert res.status_code == 200