"""Shared test fixtures — SQLite in-memory DB + FastAPI TestClient. This avoids needing MySQL for unit/integration tests. All models are created fresh for every test function (function-scoped session). """ import sys, os # Ensure the backend app package is importable # Backend is at ../../../HarborForge.Backend/ relative to this file _backend_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "HarborForge.Backend")) sys.path.insert(0, _backend_path) import pytest from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker # --- Override engine BEFORE any app import touches the real DB --- from app.core.config import Base # Force-import ALL model modules so Base.metadata knows every table import app.models.models # noqa: F401 — User, Project, Comment, etc. import app.models.milestone # noqa: F401 import app.models.task # noqa: F401 import app.models.role_permission # noqa: F401 import app.models.activity # noqa: F401 import app.models.propose # noqa: F401 import app.models.essential # noqa: F401 import app.models.proposal # noqa: F401 try: import app.models.apikey # noqa: F401 except ImportError: pass try: import app.models.webhook # noqa: F401 except ImportError: pass try: import app.models.monitor # noqa: F401 except ImportError: pass TEST_DATABASE_URL = "sqlite://" # in-memory engine = create_engine( TEST_DATABASE_URL, connect_args={"check_same_thread": False}, # Use StaticPool so all sessions share the same in-memory connection poolclass=__import__("sqlalchemy.pool", fromlist=["StaticPool"]).StaticPool, ) # SQLite needs foreign keys enabled per-connection @event.listens_for(engine, "connect") def _set_sqlite_pragma(dbapi_conn, _): cursor = dbapi_conn.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(autouse=True) def setup_database(): """Create all tables before each test, drop after.""" Base.metadata.create_all(bind=engine) yield Base.metadata.drop_all(bind=engine) @pytest.fixture() def db(): """Yield a DB session for direct model manipulation.""" session = TestingSessionLocal() try: yield session finally: session.close() @pytest.fixture() def client(db): """FastAPI TestClient wired to the test DB + a default authenticated user.""" from fastapi.testclient import TestClient from app.main import app from app.core.config import get_db # Override DB dependency def _override_get_db(): try: yield db finally: pass # caller's `db` fixture handles close app.dependency_overrides[get_db] = _override_get_db yield TestClient(app) app.dependency_overrides.clear() # --------------------------------------------------------------------------- # Helper factories # --------------------------------------------------------------------------- @pytest.fixture() def make_user(db): """Factory to create a User row.""" from app.models.models import User _counter = [0] # Pre-compute a bcrypt hash for "password" to avoid passlib/bcrypt version issues _pwd_hash = "$2b$12$LJ3m4ys/Xz.l1PaOHHKN/uE7dQFnSm1AUBfEkL0C2dN9.3Oau4XG" def _make(username=None, is_admin=False): _counter[0] += 1 n = _counter[0] u = User( username=username or f"testuser{n}", email=f"test{n}@example.com", hashed_password=_pwd_hash, is_active=True, is_admin=is_admin, ) db.add(u) db.commit() db.refresh(u) return u return _make @pytest.fixture() def make_project(db): """Factory to create a Project row.""" from app.models.models import Project _counter = [0] def _make(owner_id, name=None, project_code=None): _counter[0] += 1 n = _counter[0] p = Project( name=name or f"TestProject{n}", project_code=project_code or f"TP{n}", owner_name="owner", owner_id=owner_id, ) db.add(p) db.commit() db.refresh(p) return p return _make @pytest.fixture() def make_milestone(db): """Factory to create a Milestone row.""" from app.models.milestone import Milestone, MilestoneStatus _counter = [0] def _make(project_id, created_by_id, status=MilestoneStatus.OPEN, **kw): _counter[0] += 1 n = _counter[0] ms = Milestone( title=kw.pop("title", f"Milestone {n}"), project_id=project_id, created_by_id=created_by_id, status=status, milestone_code=kw.pop("milestone_code", f"M{n:04d}"), **kw, ) db.add(ms) db.commit() db.refresh(ms) return ms return _make @pytest.fixture() def make_task(db): """Factory to create a Task row.""" from app.models.task import Task, TaskStatus _counter = [0] def _make(project_id, milestone_id, reporter_id, status=TaskStatus.PENDING, **kw): _counter[0] += 1 n = _counter[0] t = Task( title=kw.pop("title", f"Task {n}"), project_id=project_id, milestone_id=milestone_id, reporter_id=reporter_id, created_by_id=kw.pop("created_by_id", reporter_id), status=status, task_code=kw.pop("task_code", f"T{n:04d}"), task_type=kw.pop("task_type", "issue"), task_subtype=kw.pop("task_subtype", None), **kw, ) db.add(t) db.commit() db.refresh(t) return t return _make @pytest.fixture() def seed_roles_and_permissions(db): """Create the minimal role + permission setup needed by action endpoints. Returns (admin_role, mgr_role, dev_role). """ from app.models.role_permission import Role, Permission, RolePermission # --- roles --- admin_role = Role(name="admin", is_global=True) mgr_role = Role(name="mgr", is_global=False) dev_role = Role(name="dev", is_global=False) db.add_all([admin_role, mgr_role, dev_role]) db.commit() # --- permissions --- perm_names = [ ("milestone.freeze", "milestone"), ("milestone.start", "milestone"), ("milestone.close", "milestone"), ("task.close", "task"), ("task.reopen_closed", "task"), ("task.reopen_completed", "task"), ("propose.accept", "propose"), ("propose.reject", "propose"), ("propose.reopen", "propose"), # add broad perms for role checks ("project.read", "project"), ("project.write", "project"), ("milestone.read", "milestone"), ("milestone.write", "milestone"), ("milestone.create", "milestone"), ("task.read", "task"), ("task.write", "task"), ("task.create", "task"), ] perm_objs = {} for name, cat in perm_names: p = Permission(name=name, category=cat, description=name) db.add(p) db.flush() perm_objs[name] = p # admin gets all for p in perm_objs.values(): db.add(RolePermission(role_id=admin_role.id, permission_id=p.id)) # mgr gets milestone + propose + task management perms mgr_perms = [ "milestone.freeze", "milestone.start", "milestone.close", "task.close", "task.reopen_closed", "task.reopen_completed", "propose.accept", "propose.reject", "propose.reopen", "project.read", "project.write", "milestone.read", "milestone.write", "milestone.create", "task.read", "task.write", "task.create", ] for name in mgr_perms: db.add(RolePermission(role_id=mgr_role.id, permission_id=perm_objs[name].id)) # dev gets basic perms dev_perms = [ "project.read", "task.read", "task.write", "task.create", "milestone.read", "task.close", "task.reopen_closed", "task.reopen_completed", ] for name in dev_perms: db.add(RolePermission(role_id=dev_role.id, permission_id=perm_objs[name].id)) db.commit() db.refresh(admin_role) db.refresh(mgr_role) db.refresh(dev_role) return admin_role, mgr_role, dev_role @pytest.fixture() def make_member(db): """Factory to add a user as project member with a given role.""" from app.models.models import ProjectMember def _make(project_id, user_id, role_id): pm = ProjectMember(project_id=project_id, user_id=user_id, role_id=role_id) db.add(pm) db.commit() return pm return _make @pytest.fixture() def auth_header(): """Generate a JWT auth header for a given user.""" from app.api.deps import create_access_token def _make(user): token = create_access_token({"sub": str(user.id)}) return {"Authorization": f"Bearer {token}"} return _make