"""Shared test fixtures — SQLite in-memory DB + FastAPI TestClient. Every test function gets a fresh database with baseline seed data: - Roles: admin, dev, viewer (+ permissions for propose.accept/reject/reopen) - An admin user (id=1) - A dev user (id=2) - A project (id=1) with both users as members - An open milestone (id=1) under the project """ import pytest from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker from fastapi.testclient import TestClient # --------------------------------------------------------------------------- # Patch the production engine/SessionLocal BEFORE importing app so that # startup events (Base.metadata.create_all, init_wizard, etc.) use the # in-memory SQLite database instead of trying to connect to MySQL. # --------------------------------------------------------------------------- SQLALCHEMY_DATABASE_URL = "sqlite:///file::memory:?cache=shared&uri=true" test_engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, ) # SQLite foreign-key enforcement @event.listens_for(test_engine, "connect") def _set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=test_engine) # Monkey-patch app.core.config so the entire app uses SQLite import app.core.config as _cfg _cfg.engine = test_engine _cfg.SessionLocal = TestingSessionLocal # Now it's safe to import app and friends from app.core.config import Base, get_db from app.main import app from app.models import models from app.models.role_permission import Role, Permission, RolePermission from app.models.milestone import Milestone, MilestoneStatus from app.api.deps import get_password_hash, create_access_token # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(autouse=True) def setup_database(): """Create all tables before each test, drop after.""" Base.metadata.create_all(bind=test_engine) yield Base.metadata.drop_all(bind=test_engine) @pytest.fixture() def db(): """Yield a DB session for direct model manipulation in tests.""" session = TestingSessionLocal() try: yield session finally: session.close() def _override_get_db(): session = TestingSessionLocal() try: yield session finally: session.close() @pytest.fixture() def client(): """FastAPI TestClient wired to the test DB.""" app.dependency_overrides[get_db] = _override_get_db with TestClient(app, raise_server_exceptions=False) as c: yield c app.dependency_overrides.clear() # --------------------------------------------------------------------------- # Seed helpers # --------------------------------------------------------------------------- def _seed_roles_and_permissions(db_session): """Create admin/dev/viewer roles and key permissions.""" admin_role = Role(id=1, name="admin", is_global=True) dev_role = Role(id=2, name="dev", is_global=False) viewer_role = Role(id=3, name="viewer", is_global=False) db_session.add_all([admin_role, dev_role, viewer_role]) db_session.flush() perms = [] for pname in ["propose.accept", "propose.reject", "propose.reopen", "task.create", "task.edit", "task.delete"]: p = Permission(name=pname, category="proposal") db_session.add(p) db_session.flush() perms.append(p) # Admin gets all permissions for p in perms: db_session.add(RolePermission(role_id=admin_role.id, permission_id=p.id)) # Dev gets propose.accept / reject / reopen and task perms for p in perms: db_session.add(RolePermission(role_id=dev_role.id, permission_id=p.id)) db_session.flush() return admin_role, dev_role, viewer_role def _seed_users(db_session, admin_role, dev_role): """Create admin + dev users.""" admin_user = models.User( id=1, username="admin", email="admin@test.com", hashed_password=get_password_hash("admin123"), is_admin=True, role_id=admin_role.id, ) dev_user = models.User( id=2, username="developer", email="dev@test.com", hashed_password=get_password_hash("dev123"), is_admin=False, role_id=dev_role.id, ) db_session.add_all([admin_user, dev_user]) db_session.flush() return admin_user, dev_user def _seed_project(db_session, admin_user, dev_user, dev_role): """Create a project with both users as members.""" project = models.Project( id=1, name="TestProject", project_code="TPRJ", owner_name=admin_user.username, owner_id=admin_user.id, ) db_session.add(project) db_session.flush() db_session.add(models.ProjectMember(project_id=project.id, user_id=admin_user.id, role_id=1)) db_session.add(models.ProjectMember(project_id=project.id, user_id=dev_user.id, role_id=dev_role.id)) db_session.flush() return project def _seed_milestone(db_session, project): """Create an open milestone.""" ms = Milestone( id=1, title="v1.0", milestone_code="TPRJ:M00001", status=MilestoneStatus.OPEN, project_id=project.id, created_by_id=1, ) db_session.add(ms) db_session.flush() return ms @pytest.fixture() def seed(db): """Seed the DB with roles, users, project, milestone. Returns a namespace dict.""" admin_role, dev_role, viewer_role = _seed_roles_and_permissions(db) admin_user, dev_user = _seed_users(db, admin_role, dev_role) project = _seed_project(db, admin_user, dev_user, dev_role) milestone = _seed_milestone(db, project) db.commit() admin_token = create_access_token({"sub": str(admin_user.id)}) dev_token = create_access_token({"sub": str(dev_user.id)}) return { "admin_user": admin_user, "dev_user": dev_user, "admin_role": admin_role, "dev_role": dev_role, "project": project, "milestone": milestone, "admin_token": admin_token, "dev_token": dev_token, } def auth_header(token: str) -> dict: """Return Authorization header dict.""" return {"Authorization": f"Bearer {token}"}