This should fix that. test_settings.py written by qwen3.8 Assisted-by: local-qwen3.8 <localllm@ahtlon.de>
238 lines
6.6 KiB
Python
238 lines
6.6 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
|
|
|
|
# TEST_ENV must cover every Settings field
|
|
# test_settings.py fails with the missing name if it does not.
|
|
TEST_ENV = {
|
|
"SECRET_KEY": "ff" * 16,
|
|
"SQLALCHEMY_DATABASE_URL": "sqlite://",
|
|
"MQTT_HOST": "localhost",
|
|
"MQTT_PORT": "1883",
|
|
"MOCK": "True",
|
|
"ALEMBIC_CONFIG": "./alembic.ini",
|
|
"DISABLE_CARDS": "True",
|
|
"MIFARE_APP_MASTER_KEY": "0" * 16,
|
|
"MIFARE_ACL_READ_BASE_KEY": "1" * 16,
|
|
"MIFARE_ACL_WRITE_BASE_KEY": "2" * 16,
|
|
"DISABLE_CALDAV": "True",
|
|
"CALDAV_URL": "http://caldav.invalid/dav.php",
|
|
"CALDAV_USER": "test-user",
|
|
"CALDAV_PASSWORD": "test-password",
|
|
"CALDAV_CALENDARS": '["test-calendar"]',
|
|
"CALDAV_REFRESH_INTERVAL": "3600",
|
|
}
|
|
|
|
|
|
for _name in TEST_ENV:
|
|
os.environ.pop(_name, None)
|
|
os.environ.update(TEST_ENV)
|
|
|
|
import app.services.database as database_service # noqa: E402
|
|
|
|
TEST_ADMIN_PASSWORD = "admin123"
|
|
|
|
engine = create_engine(
|
|
TEST_ENV["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
|