""" Pytest fixtures for the JQC test suite. IMPORTANT: config.py evaluates `_require_env('SECRET_KEY')` / `DATABASE_URL` at import time, so the required environment variables must be set *before* anything imports `app` or `config`. pytest imports this conftest before collecting any test module, so setting them at module top here is sufficient — the TestingConfig then overrides the DB URI with in-memory SQLite regardless. """ import os os.environ.setdefault('SECRET_KEY', 'test-secret-key') os.environ.setdefault('DATABASE_URL', 'sqlite://') # overridden by TestingConfig os.environ.setdefault('DIGEST_SECRET', 'test-digest-secret') from datetime import timedelta import pytest from app import create_app, db as _db from app.utils.time_utils import now_eastern # Import model modules so their tables are registered on the metadata before # create_all() runs (they are also imported transitively via blueprints). from app.models.user import User from app.models.project import Project from app.models.facility import Facility, Area from app.models.inspection import InspectionTemplate, Inspection from app.models.issue import Issue # ── Core fixtures ───────────────────────────────────────────────────────────── @pytest.fixture(scope='session') def _app(): """Build the Flask app exactly once for the whole session. create_app() cannot be called more than once per process: register_api() attaches sub-blueprints to the module-level `api_bp` singleton, and Flask forbids re-registering a blueprint. So the app object is session-scoped. """ return create_app('testing') @pytest.fixture def app(_app): """A fresh app context + empty in-memory schema for each test. A NEW app context is pushed per test (not once per session): Flask-Login caches the current user in `g`, which is bound to the app context, so a shared context would leak an authenticated user from one test into the next. """ with _app.app_context(): _db.create_all() try: yield _app finally: _db.session.remove() _db.drop_all() @pytest.fixture def db(app): """The active SQLAlchemy session, bound to the per-test app context.""" return _db @pytest.fixture def client(app): """A Flask test client for the per-test app.""" return app.test_client() @pytest.fixture def login(client): """Return a helper that logs the given user in via the real login route. Usage: login(user, password='pw') CSRF is disabled in TestingConfig, so a plain form POST authenticates. """ def _login(user, password='pw'): # Don't follow the redirect: the session cookie is already set on the # 302 response, and this avoids coupling the helper to whatever the # post-login landing page renders. return client.post( '/auth/login', data={'username': user.username, 'password': password}, follow_redirects=False, ) return _login # ── Model factory helpers ───────────────────────────────────────────────────── # Small builders that commit and return a persisted row. Kept deliberately thin; # tests pass only the attributes they care about. _counter = {'n': 0} def _uniq(): _counter['n'] += 1 return _counter['n'] @pytest.fixture def make_user(db): def _make(role='inspector', password='pw', username=None, **kw): n = _uniq() user = User( username=username or f'{role}{n}', email=kw.pop('email', f'{role}{n}@example.com'), full_name=kw.pop('full_name', None), role=role, active=kw.pop('active', True), ) user.set_password(password) for k, v in kw.items(): setattr(user, k, v) db.session.add(user) db.session.commit() return user return _make @pytest.fixture def make_facility(db): def _make(active=True, project=None, with_token=True, **kw): n = _uniq() fac = Facility( name=kw.pop('name', f'Facility {n}'), address=kw.pop('address', f'{n} Test St'), active=active, project_id=project.id if project else None, ) if with_token: fac.ensure_public_token() for k, v in kw.items(): setattr(fac, k, v) db.session.add(fac) db.session.commit() return fac return _make @pytest.fixture def make_area(db): def _make(facility, with_token=True, **kw): n = _uniq() area = Area( facility_id=facility.id, name=kw.pop('name', f'Area {n}'), area_type=kw.pop('area_type', 'restroom'), ) if with_token: area.ensure_public_token() for k, v in kw.items(): setattr(area, k, v) db.session.add(area) db.session.commit() return area return _make @pytest.fixture def make_template(db): def _make(**kw): n = _uniq() tpl = InspectionTemplate( name=kw.pop('name', f'Template {n}'), active=kw.pop('active', True), ) for k, v in kw.items(): setattr(tpl, k, v) db.session.add(tpl) db.session.commit() return tpl return _make @pytest.fixture def make_inspection(db): def _make(facility, inspector, template, area=None, status='completed', overall_score=90.0, days_ago=5, **kw): insp = Inspection( template_id=template.id, facility_id=facility.id, area_id=area.id if area else None, inspector_id=inspector.id, inspection_date=now_eastern() - timedelta(days=days_ago), overall_score=overall_score, status=status, ) for k, v in kw.items(): setattr(insp, k, v) db.session.add(insp) db.session.commit() return insp return _make @pytest.fixture def make_issue(db): def _make(facility=None, area=None, status='open', severity='medium', description='Test issue', **kw): issue = Issue( facility_id=facility.id if facility else None, area_id=area.id if area else None, status=status, severity=severity, description=description, reported_at=kw.pop('reported_at', now_eastern()), ) for k, v in kw.items(): setattr(issue, k, v) db.session.add(issue) db.session.commit() return issue return _make