2879abb355
Adds operator_gate Starlette HTTP middleware that gates every route except an explicit allow-list. Flag defaults OFF so all existing behaviour and tests are unchanged. wire_operator_auth helper in conftest lets tests monkeypatch the module-global SessionLocal and flag, keeping the gate's own DB session pointed at the test engine. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""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 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():
|
|
yield db_session
|
|
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.pop(get_db, None)
|
|
|
|
|
|
@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
|
|
|
|
|
|
def wire_operator_auth(monkeypatch, db_session, enabled=True):
|
|
"""Point the gate middleware's SessionLocal at the test engine and flip the
|
|
flag. The middleware opens its OWN session (it can't use the get_db override),
|
|
so it must read the same engine the test writes to."""
|
|
import backend.operator_auth as oa
|
|
from sqlalchemy.orm import sessionmaker
|
|
maker = sessionmaker(bind=db_session.get_bind(), autocommit=False, autoflush=False)
|
|
monkeypatch.setattr(oa, "SessionLocal", maker, raising=False)
|
|
monkeypatch.setattr(oa, "OPERATOR_AUTH_ENABLED", enabled, raising=False)
|
|
return oa
|