Jul 10 - Add test suite

This commit is contained in:
2026-07-10 15:49:55 -04:00
parent 512a80837a
commit 7a9814119f
10 changed files with 732 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
"""
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