Files
headquarter/apps/api/tests/integration/test_auth_api.py
T
Fusion 4299c64922 refactor: remove duplicate fixtures and add SQLite support
Task 2.5: Remove duplicate fixtures from integration tests
- test_auth_api.py, test_auth_services.py, test_models.py
- test_projects_api.py, test_seed.py, test_users_api.py
- Fix npytest typos in all test files

Task 3.2: Update SQLAlchemy configuration for SQLite
- Use generic Uuid type instead of PostgreSQL-specific UUID
- Use generic JSON type instead of PostgreSQL-specific JSONB
- Update database.py to handle SQLite connection args

Unit tests now run without PostgreSQL (5/8 passing)
2026-05-18 15:14:46 +02:00

215 lines
6.7 KiB
Python

import uuid
from datetime import UTC, datetime, timedelta
import asyncio
import importlib
from fastapi.testclient import TestClient
import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from src.auth.jwt_service import mint_access_token
from src.config import Settings, build_database_url
from src.models import Base
from src.models.user import User
def _prepare_auth_test_db() -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
)
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
await connection.execute(text("TRUNCATE TABLE refresh_tokens, users RESTART IDENTITY CASCADE"))
await engine.dispose()
asyncio.run(_run())
def _load_app():
import src.database as database_module
import src.api.auth as auth_module
import src.main as main_module
importlib.reload(database_module)
importlib.reload(auth_module)
importlib.reload(main_module)
return main_module.app
def _insert_user_for_refresh(user_id: str) -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
)
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
from sqlalchemy.ext.asyncio import async_sessionmaker
session_factory = async_sessionmaker(engine, expire_on_commit=False)
async with session_factory() as session:
user = User(
id=uuid.UUID(user_id),
email="refresh@headquarter.local",
name="Refresh User",
authentik_id="refresh-user",
avatar_url=None,
)
await session.merge(user)
await session.commit()
await engine.dispose()
asyncio.run(_run())
@pytest.mark.integration
def test_login_redirects_to_authentik_authorize_endpoint() -> None:
_prepare_auth_test_db()
app = _load_app()
client = TestClient(app)
response = client.get("/auth/login", follow_redirects=False)
assert response.status_code == 307
assert "response_type=code" in response.headers["location"]
@pytest.mark.integration
def test_me_returns_401_without_access_cookie() -> None:
_prepare_auth_test_db()
app = _load_app()
client = TestClient(app)
response = client.get("/auth/me")
assert response.status_code == 401
@pytest.mark.integration
def test_me_returns_user_payload_with_valid_access_cookie() -> None:
_prepare_auth_test_db()
app = _load_app()
settings = Settings()
token = mint_access_token(
settings=settings,
subject=str(uuid.uuid4()),
email="dev@headquarter.local",
name="Dev User",
expires_at=datetime.now(UTC) + timedelta(minutes=15),
)
client = TestClient(app)
client.cookies.set("access_token", token)
response = client.get("/auth/me")
assert response.status_code == 200
assert response.json()["email"] == "dev@headquarter.local"
@pytest.mark.integration
def test_logout_clears_auth_cookies() -> None:
_prepare_auth_test_db()
app = _load_app()
client = TestClient(app)
client.cookies.set("refresh_token", "opaque-token")
response = client.post("/auth/logout")
assert response.status_code == 200
assert "access_token=" in response.headers.get("set-cookie", "")
@pytest.mark.integration
def test_callback_rejects_mismatched_state() -> None:
_prepare_auth_test_db()
app = _load_app()
client = TestClient(app)
client.cookies.set("auth_state", "expected")
response = client.get("/auth/callback?code=test-code&state=wrong")
assert response.status_code == 401
@pytest.mark.integration
def test_callback_sets_auth_cookies_after_success(monkeypatch) -> None:
_prepare_auth_test_db()
app = _load_app()
async def fake_exchange_code_for_tokens(*, settings, code, redirect_uri, client):
return {"access_token": "provider-access", "refresh_token": "provider-refresh"}
def fake_verify_provider_access_token(*, settings, token, jwks):
return {"sub": "auth-sub-1", "email": "callback@headquarter.local", "name": "Callback User"}
async def fake_fetch_jwks(*, settings, client):
return {"keys": []}
monkeypatch.setattr("src.api.auth.exchange_code_for_tokens", fake_exchange_code_for_tokens)
monkeypatch.setattr("src.api.auth.verify_provider_access_token", fake_verify_provider_access_token)
monkeypatch.setattr("src.api.auth.fetch_jwks", fake_fetch_jwks)
client = TestClient(app)
client.cookies.set("auth_state", "good-state")
response = client.get("/auth/callback?code=valid-code&state=good-state")
assert response.status_code == 200
assert response.json()["email"] == "callback@headquarter.local"
set_cookie_header = response.headers.get("set-cookie", "")
assert "access_token=" in set_cookie_header
assert "refresh_token=" in set_cookie_header
@pytest.mark.integration
def test_refresh_rotates_cookie_and_returns_user_payload(monkeypatch) -> None:
_prepare_auth_test_db()
_insert_user_for_refresh("7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb")
app = _load_app()
async def fake_rotate_refresh_token(*, session, raw_token, user_agent, ip_address):
class StoredToken:
user_id = uuid.UUID("7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb")
return "new-refresh-token", StoredToken()
monkeypatch.setattr("src.api.auth.rotate_refresh_token", fake_rotate_refresh_token)
client = TestClient(app)
client.cookies.set("refresh_token", "old-refresh-token")
response = client.post("/auth/refresh")
assert response.status_code == 200
assert response.json()["sub"] == "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
assert "refresh_token=" in response.headers.get("set-cookie", "")
@pytest.mark.integration
def test_refresh_returns_401_for_invalid_refresh_token(monkeypatch) -> None:
_prepare_auth_test_db()
app = _load_app()
async def fake_rotate_refresh_token(*, session, raw_token, user_agent, ip_address):
raise ValueError("refresh token not found")
monkeypatch.setattr("src.api.auth.rotate_refresh_token", fake_rotate_refresh_token)
client = TestClient(app)
client.cookies.set("refresh_token", "invalid")
response = client.post("/auth/refresh")
assert response.status_code == 401