"""Shared pytest fixtures. Tests run against in-memory SQLite — no docker, no Postgres install. The cross-dialect type variants in :mod:`app.models` keep the schema identical in behavior for everything PR 1 exercises (the JSONB column on ``gumroad_events`` isn't touched until PR 2). Auth: a fixed test token is wired into the settings cache before any app modules import, and the ``client`` fixture overrides the ``require_localhost`` guard since Starlette's TestClient connects from a synthetic ``testclient`` peer. """ from __future__ import annotations import os import sys from pathlib import Path # Set required env BEFORE importing anything from app.* so pydantic # Settings (lru_cache'd) picks up these values on first access. os.environ["DATATOOLS_ADMIN_TOKEN"] = "test-admin-token" os.environ["DATABASE_URL"] = "sqlite+pysqlite:///:memory:" # Make the desktop license module importable as `datatools_license`. # In the Docker image this happens via `COPY src/license /app/datatools_license`; # during local tests we simulate it by aliasing src.license. _REPO_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(_REPO_ROOT)) import src.license as _dt_license_module sys.modules.setdefault("datatools_license", _dt_license_module) for _sub in ("crypto", "schema", "features", "_dev_keypair"): sys.modules.setdefault( f"datatools_license.{_sub}", __import__(f"src.license.{_sub}", fromlist=[_sub]), ) import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool import app.db as app_db from app.db import Base from app.main import app @pytest.fixture(scope="session") def engine(): eng = create_engine( "sqlite+pysqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, future=True, ) # Enforce foreign keys on SQLite (off by default). @event.listens_for(eng, "connect") def _fk_on(dbapi_conn, _): dbapi_conn.execute("PRAGMA foreign_keys=ON") Base.metadata.create_all(eng) yield eng eng.dispose() @pytest.fixture(autouse=True) def _bind_app_engine(engine, monkeypatch): """Point the app's session factory at the test engine and wipe rows between tests so order-of-execution can't leak state.""" TestSession = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False) monkeypatch.setattr(app_db, "engine", engine) monkeypatch.setattr(app_db, "SessionLocal", TestSession) yield with engine.begin() as conn: for tbl in reversed(Base.metadata.sorted_tables): conn.execute(tbl.delete()) @pytest.fixture def db_session(engine): """Per-test session with rollback isolation.""" Session = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False) sess = Session() try: yield sess finally: # Clean rows between tests rather than transaction-rolling (the # mint code flushes mid-transaction and we want each test to # see a clean licenses table). sess.rollback() for tbl in reversed(Base.metadata.sorted_tables): sess.execute(tbl.delete()) sess.commit() sess.close() @pytest.fixture def client(): """Plain TestClient. Bearer-token check is live.""" with TestClient(app) as c: yield c @pytest.fixture def admin_headers() -> dict[str, str]: return {"Authorization": "Bearer test-admin-token"}