Files
gatekeeper/test/conftest.py
T
ahtlon db87625384 Prevent accidental opening of the db on disk while running tests
This was anoying to fix (and even debug)
so I used the local llm for some code changes

Assisted-by: local-qwen3.8 <localllm@ahtlon.de>
2026-11-05 18:48:48 +01:00

217 lines
6.1 KiB
Python

import os
from datetime import datetime, timedelta
from time import tzset
import pytest
from alembic.config import Config
from alembic.script import ScriptDirectory
from fastapi.testclient import TestClient
from sqlalchemy import inspect
from sqlalchemy.pool import StaticPool
from sqlmodel import Session, SQLModel, create_engine, select
os.environ.setdefault("SECRET_KEY", "ff" * 16)
os.environ.setdefault("MOCK", "True")
os.environ.setdefault("SQLALCHEMY_DATABASE_URL", "sqlite://")
os.environ.setdefault("DISABLE_CARDS", "True")
import app.services.database as database_service
TEST_ADMIN_PASSWORD = "admin123"
engine = create_engine(
os.environ["SQLALCHEMY_DATABASE_URL"],
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
# inject custom engine
database_service.configure_engine(engine)
# import main after the custom engine is injected
from app.main import app # noqa: E402
from app.model.models import ( # noqa: E402
AccessAuthorizationDB,
Card,
GroupDB,
OneShotAccess,
UserDB,
)
from app.services.settings import settings # noqa: E402
def alembic_head() -> str:
"""Head revision of the migration scripts."""
alembic_cfg = Config(settings.alembic_config)
return ScriptDirectory.from_config(alembic_cfg).get_current_head()
def _seed_alembic_version() -> None:
"""Mark the test database as already migrated.
Without a version row create_db_and_tables() treats the database as new and
runs an alembic stamp, which builds its own engine from the configured URL
instead of using this shared in-memory engine.
"""
with engine.connect() as connection:
if "alembic_version" not in inspect(connection).get_table_names():
connection.exec_driver_sql(
"CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)"
)
connection.exec_driver_sql(
"INSERT OR IGNORE INTO alembic_version (version_num) VALUES (?)",
(_HEAD_REVISION,),
)
connection.commit()
def _seed_default_admin() -> None:
"""Seed the admin user the fixtures log in with.
TestClient runs lifespan, that calls create_first_user()
which blocks the admin_user fixture. Pre-creating the admin stops that
"""
from app.services.auth import get_password_hash
with Session(engine) as session:
if session.exec(select(UserDB).where(UserDB.name == "admin")).first() is None:
session.add(
UserDB(
name="admin",
passwordhash=get_password_hash(TEST_ADMIN_PASSWORD),
is_admin=True,
)
)
session.commit()
_HEAD_REVISION = alembic_head()
_seed_alembic_version()
@pytest.fixture
def system_tz(request):
"""Run a test with the host-local timezone forced to a POSIX TZ string."""
previous_tz = os.environ.get("TZ")
os.environ["TZ"] = request.param
tzset()
try:
yield request.param
finally:
if previous_tz is None:
os.environ.pop("TZ", None)
else:
os.environ["TZ"] = previous_tz
tzset()
@pytest.fixture(autouse=True)
def _installed_engine():
"""Undo configure_engine() leaks between tests."""
previous = database_service.get_engine()
yield
database_service.configure_engine(previous)
@pytest.fixture(scope="function")
def db_session():
"""Create a fresh database session for each test."""
SQLModel.metadata.create_all(engine)
_seed_alembic_version()
_seed_default_admin()
with Session(engine) as session:
yield session
SQLModel.metadata.drop_all(engine)
@pytest.fixture(scope="function")
def client(db_session):
"""Create a test client bound to the test engine.
No FastAPI dependency override is needed: get_session() resolves its engine
through get_engine(), which already points at the test engine.
"""
with TestClient(app) as test_client:
yield test_client
@pytest.fixture
def admin_user(db_session):
"""Get the admin user the seeded database already contains."""
admin = db_session.exec(select(UserDB).where(UserDB.name == "admin")).one()
db_session.refresh(admin)
return admin
@pytest.fixture
def regular_user(db_session):
"""Create a regular user for testing."""
from app.services.auth import get_password_hash
user = UserDB(
name="user", passwordhash=get_password_hash("user123"), is_admin=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
return user
@pytest.fixture
def auth_headers(client, admin_user):
"""Get authentication headers for admin user."""
response = client.post(
"/api/v1/token",
data={"username": admin_user.name, "password": TEST_ADMIN_PASSWORD},
)
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def user_auth_headers(client, regular_user):
"""Get authentication headers for regular user."""
response = client.post(
"/api/v1/token", data={"username": regular_user.name, "password": "user123"}
)
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
@pytest.fixture
def test_group(db_session):
"""Create a test group."""
group = GroupDB(name="Test Group")
db_session.add(group)
db_session.commit()
db_session.refresh(group)
return group
@pytest.fixture
def test_card(db_session, test_group):
"""Create a test card."""
card = Card(
key="test-key-123",
group_id=test_group.id,
enabled=True,
name="test_card",
card_serial="00:00:00:00:00:00:00",
)
db_session.add(card)
db_session.commit()
db_session.refresh(card)
return card
@pytest.fixture
def test_aa_os(db_session):
oneshot = OneShotAccess(uses=1, ends_at=datetime.now() + timedelta(days=1))
db_session.add(oneshot)
aa = AccessAuthorizationDB(name="Test oneshot AA", is_active=True, type="oneshot")
aa.oneshot = oneshot
db_session.add(aa)
db_session.commit()
db_session.refresh(aa)
return aa