"""Pytest fixtures and configuration for backend tests. The conftest intentionally avoids importing ``app.main`` at module level because that triggers heavy side-effect imports (boto3, APScheduler, etc.) which are NOT needed for unit tests. The ``client`` fixture lazily imports the FastAPI app only when actually requested. """ import os # Set DATABASE_URL to SQLite *before* any app module is imported so that # the lazy engine in app.database never tries to connect to PostgreSQL. os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") import pytest from sqlalchemy import JSON, String, Text, create_engine, event from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from app.database import Base # ── Patch PostgreSQL-specific column types so SQLite can handle them ───── # Must run BEFORE importing models, because column type objects are # instantiated at class-definition time. from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB as PG_JSONB # Tell SQLAlchemy: when compiling for SQLite, render JSONB as plain JSON # and PostgreSQL UUID as CHAR(32). from sqlalchemy.dialects.sqlite.base import SQLiteTypeCompiler if not hasattr(SQLiteTypeCompiler, "visit_JSONB"): SQLiteTypeCompiler.visit_JSONB = lambda self, type_, **kw: "JSON" if not hasattr(SQLiteTypeCompiler, "visit_UUID"): SQLiteTypeCompiler.visit_UUID = lambda self, type_, **kw: "CHAR(32)" from app.auth import hash_password from app.models.user import User # ── Import all models so Base.metadata knows about every table ────────── import app.models # noqa: F401 — triggers model registration via __init__ # Use in-memory SQLite for tests SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, poolclass=StaticPool, ) # SQLite needs PRAGMA foreign_keys to enforce FK constraints @event.listens_for(engine, "connect") def _set_sqlite_pragma(dbapi_conn, connection_record): cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def override_get_db(): """Override the database dependency for testing.""" db = TestingSessionLocal() try: yield db finally: db.close() @pytest.fixture(scope="function") def db(): """Create a fresh database for each test.""" Base.metadata.create_all(bind=engine) db = TestingSessionLocal() yield db db.close() Base.metadata.drop_all(bind=engine) @pytest.fixture(scope="function") def client(db, monkeypatch): """Create a test client with database override. Imports ``app.main`` lazily to avoid pulling in boto3 / APScheduler when only the ``db`` fixture is needed. MinIO and the background scheduler are no-ops here so tests do not require Docker services for application startup. JWT blacklist uses an in-memory FakeRedis so tests do not require a real Redis instance. """ import fakeredis _fake_redis = fakeredis.FakeRedis(decode_responses=True) def _blacklist_conn(): return _fake_redis monkeypatch.setattr( "app.infrastructure.redis_client.get_redis_blacklist", _blacklist_conn, ) monkeypatch.setattr("app.main.ensure_bucket_exists", lambda: None) monkeypatch.setattr("app.main.start_scheduler", lambda: None) monkeypatch.setattr( "app.main.scheduler.shutdown", lambda wait=False: None, ) from app.main import app from app.database import get_db import app.database as _db_mod _db_mod._engine = engine _db_mod._SessionLocal = TestingSessionLocal app.dependency_overrides[get_db] = override_get_db Base.metadata.create_all(bind=engine) if hasattr(app.state, "limiter"): app.state.limiter.enabled = False from app.routers.auth import limiter as auth_limiter auth_limiter.enabled = False from fastapi.testclient import TestClient with TestClient(app) as test_client: yield test_client Base.metadata.drop_all(bind=engine) app.dependency_overrides.clear() @pytest.fixture(scope="function") def admin_user(db): """Create an admin user for testing.""" user = User( username="admin", email="admin@test.com", hashed_password=hash_password("admin123"), role="admin", is_active=True, must_change_password=False, ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture(scope="function") def red_tech_user(db): """Create a red_tech user for testing.""" user = User( username="redtech", email="redtech@test.com", hashed_password=hash_password("redtech123"), role="red_tech", is_active=True, must_change_password=False, ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture(scope="function") def blue_tech_user(db): """Create a blue_tech user for testing.""" user = User( username="bluetech", email="bluetech@test.com", hashed_password=hash_password("bluetech123"), role="blue_tech", is_active=True, must_change_password=False, ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture(scope="function") def red_lead_user(db): """Create a red_lead user for testing.""" user = User( username="redlead", email="redlead@test.com", hashed_password=hash_password("redlead123"), role="red_lead", is_active=True, must_change_password=False, ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture(scope="function") def blue_lead_user(db): """Create a blue_lead user for testing.""" user = User( username="bluelead", email="bluelead@test.com", hashed_password=hash_password("bluelead123"), role="blue_lead", is_active=True, must_change_password=False, ) db.add(user) db.commit() db.refresh(user) return user @pytest.fixture(scope="function") def admin_token(client, admin_user): """Get an auth token for the admin user.""" response = client.post( "/api/v1/auth/login", data={"username": "admin", "password": "admin123"}, ) return response.json()["access_token"] @pytest.fixture(scope="function") def red_tech_token(client, red_tech_user): """Get an auth token for the red_tech user.""" response = client.post( "/api/v1/auth/login", data={"username": "redtech", "password": "redtech123"}, ) return response.json()["access_token"] @pytest.fixture(scope="function") def auth_headers(admin_token): """Return authorization headers for admin user.""" return {"Authorization": f"Bearer {admin_token}"} @pytest.fixture(scope="function") def red_tech_headers(red_tech_token): """Return authorization headers for red_tech user.""" return {"Authorization": f"Bearer {red_tech_token}"} @pytest.fixture(scope="function") def blue_tech_token(client, blue_tech_user): """Get an auth token for the blue_tech user.""" response = client.post( "/api/v1/auth/login", data={"username": "bluetech", "password": "bluetech123"}, ) return response.json()["access_token"] @pytest.fixture(scope="function") def blue_tech_headers(blue_tech_token): """Return authorization headers for blue_tech user.""" return {"Authorization": f"Bearer {blue_tech_token}"} @pytest.fixture(scope="function") def red_lead_token(client, red_lead_user): """Get an auth token for the red_lead user.""" response = client.post( "/api/v1/auth/login", data={"username": "redlead", "password": "redlead123"}, ) return response.json()["access_token"] @pytest.fixture(scope="function") def red_lead_headers(red_lead_token): """Return authorization headers for red_lead user.""" return {"Authorization": f"Bearer {red_lead_token}"} @pytest.fixture(scope="function") def blue_lead_token(client, blue_lead_user): """Get an auth token for the blue_lead user.""" response = client.post( "/api/v1/auth/login", data={"username": "bluelead", "password": "bluelead123"}, ) return response.json()["access_token"] @pytest.fixture(scope="function") def blue_lead_headers(blue_lead_token): """Return authorization headers for blue_lead user.""" return {"Authorization": f"Bearer {blue_lead_token}"}