Jul 10 - Add test suite
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user