Files
HarborForge.Backend/tests/conftest.py
zhi 1ed7a85e11 BE-PR-011: Fix test infrastructure and add Proposal/Essential/Story restricted tests
- Patched conftest.py to monkey-patch app.core.config engine/SessionLocal
  with SQLite in-memory DB BEFORE importing the FastAPI app, preventing
  startup event from trying to connect to production MySQL
- All 29 tests pass: Essential CRUD (11), Proposal Accept (8),
  Story restricted (6), Legacy compat (4)
2026-03-30 16:17:00 +00:00

192 lines
6.3 KiB
Python

"""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}"}