from __future__ import annotations from collections.abc import Generator from datetime import UTC, datetime, timedelta import pytest from fastapi import HTTPException from fastapi.testclient import TestClient from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from app.api.deps import CurrentUserContext, get_db, require_platform_admin_user from app.db.base import Base from app.main import create_app from app.models.auth_session import AuthSession from app.models.user_session_metric import UserSessionMetric from app.schemas.settings import SettingsWrite from app.services.auth_sessions import AuthSessionService from app.services.settings import SettingsService def build_client() -> tuple[TestClient, sessionmaker[Session]]: engine = create_engine( "sqlite+pysqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(bind=engine) session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) with session_factory() as db: settings_service = SettingsService(db) payload = settings_service.get_settings_snapshot().model_dump() payload["adminForm"]["adminAccount"] = "auth-admin" payload["adminForm"]["newPassword"] = "safe-admin-password" payload["adminForm"]["confirmPassword"] = "safe-admin-password" settings_service.save_settings_snapshot(SettingsWrite(**payload)) app = create_app() def override_db() -> Generator[Session, None, None]: with session_factory() as db: yield db app.dependency_overrides[get_db] = override_db return TestClient(app), session_factory def login_admin(client: TestClient) -> dict: response = client.post( "/api/v1/auth/login", json={"username": "auth-admin", "password": "safe-admin-password"}, ) assert response.status_code == 200 return response.json() def test_login_issues_opaque_bearer_token_and_me_uses_server_session() -> None: client, session_factory = build_client() payload = login_admin(client) token = payload["accessToken"] assert payload["tokenType"] == "Bearer" assert payload["expiresAt"] with session_factory() as db: auth_session = db.scalars(select(AuthSession)).one() assert auth_session.token_hash == AuthSessionService.hash_token(token) assert auth_session.token_hash != token response = client.get( "/api/v1/auth/me", headers={"Authorization": f"Bearer {token}"}, ) assert response.status_code == 200 assert response.json()["username"] == "auth-admin" assert response.json()["isAdmin"] is True def test_forged_identity_headers_no_longer_authenticate() -> None: client, _ = build_client() response = client.get( "/api/v1/auth/me", headers={ "X-Auth-Username": "superadmin", "X-Auth-Role-Codes": "manager", "X-Auth-Is-Admin": "true", }, ) assert response.status_code == 401 assert response.headers["www-authenticate"] == "Bearer" def test_expired_and_revoked_tokens_are_rejected() -> None: client, session_factory = build_client() first_payload = login_admin(client) first_token = first_payload["accessToken"] with session_factory() as db: auth_session = db.scalars( select(AuthSession).where( AuthSession.token_hash == AuthSessionService.hash_token(first_token) ) ).one() auth_session.expires_at = datetime.now(UTC) - timedelta(seconds=1) db.commit() expired_response = client.get( "/api/v1/auth/me", headers={"Authorization": f"Bearer {first_token}"}, ) assert expired_response.status_code == 401 second_payload = login_admin(client) second_token = second_payload["accessToken"] logout_response = client.post( "/api/v1/auth/logout", headers={"Authorization": f"Bearer {second_token}"}, json={"sessionId": second_payload["sessionId"], "reason": "manual"}, ) assert logout_response.status_code == 200 with session_factory() as db: metric_session = db.scalars( select(UserSessionMetric).where( UserSessionMetric.session_id == second_payload["sessionId"] ) ).one() assert metric_session.status == "closed" revoked_response = client.get( "/api/v1/auth/me", headers={"Authorization": f"Bearer {second_token}"}, ) assert revoked_response.status_code == 401 def test_manager_role_is_not_platform_admin() -> None: manager = CurrentUserContext( tenant_id="default", username="manager@example.com", name="Manager", role_codes=["manager"], is_admin=False, ) with pytest.raises(HTTPException) as exc_info: require_platform_admin_user(manager) assert exc_info.value.status_code == 403