diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..60e163f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==8.3.3 diff --git a/requirements.txt b/requirements.txt index d5b35b0..8d467b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ httpx==0.25.2 openpyxl==3.1.2 rapidfuzz==3.10.1 schedule==1.2.2 +argon2-cffi==23.1.0 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3e66e1f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,68 @@ +"""Test harness: a throwaway SQLite DB per test, get_db overridden, a TestClient +that does NOT run lifespan startup (so schedulers/SLMM polling stay off).""" +import os +import uuid +import pytest +from datetime import datetime +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from starlette.testclient import TestClient + +from backend.database import Base, get_db +import backend.models as models # noqa: F401 (ensure all tables are registered on Base) + + +@pytest.fixture() +def db_session(tmp_path): + db_file = tmp_path / "test.db" + engine = create_engine(f"sqlite:///{db_file}", connect_args={"check_same_thread": False}) + Base.metadata.create_all(bind=engine) + TestingSession = sessionmaker(autocommit=False, autoflush=False, bind=engine) + sess = TestingSession() + try: + yield sess + finally: + sess.close() + engine.dispose() + + +@pytest.fixture() +def client(db_session): + from backend.main import app # imported lazily so module side effects are contained + def _override(): + try: + yield db_session + finally: + pass + app.dependency_overrides[get_db] = _override + # No `with` → lifespan/startup events do not run (no scheduler/SLMM threads). + c = TestClient(app) + yield c + app.dependency_overrides.clear() + + +@pytest.fixture(autouse=True) +def _reset_portal_lockout(): + """Portal lockout state is a module-global dict; clear it between tests so + one test's failed attempts can't lock out another.""" + try: + import backend.portal_auth as _pa + if hasattr(_pa, "_failures"): + _pa._failures.clear() + except Exception: + pass + yield + + +def make_project(db_session, name=None, **kwargs): + """Insert and return a Project with a unique name.""" + p = models.Project( + id=str(uuid.uuid4()), + name=name or f"Proj {uuid.uuid4().hex[:8]}", + status="active", + created_at=datetime.utcnow(), + **kwargs, + ) + db_session.add(p) + db_session.commit() + return p