Aug 7 - Update: Enrollment intake form

This commit is contained in:
2026-08-07 16:52:49 -04:00
parent 513f708ee9
commit f3eb4badef
13 changed files with 2216 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
"""
tests/test_enrollment.py
------------------------
Behaviour tests for MT-17 — the enrollment intake form.
Runs on the in-memory SQLite app fixture (multi-tenancy inert). Covers:
* the public form renders without a login and carries no authenticated nav
* a submission is stored, is readable back, and the customer's own answers
are immutable afterwards (only the office block and status are writable)
* malformed submission ids are rejected before touching the filesystem
(path traversal)
* the admin views require a login
* TENANT ISOLATION: with multi-tenancy enabled, submissions are filed under
a per-tenant directory, one tenant's admin list never contains another's,
and an unresolved tenant raises rather than falling back to the shared root
The isolation tests are the reason this module exists. ST keeps every
submission in one flat directory, which in MT would put the names, emails and
phone numbers of one organisation's staff in front of another organisation's
admin. `test_unresolved_tenant_refuses_rather_than_sharing` pins the fail-closed
behaviour specifically, because a fallback to the root directory would be a
silent cross-tenant leak rather than a visible error.
"""
import os
import pytest
@pytest.fixture
def client(app, tmp_path):
"""Fresh schema + test client, with enrollment storage in a temp dir."""
with app.app_context():
from app import db
from app.models import inspector_assignment # noqa: F401
db.drop_all()
db.create_all()
app.config['ENROLLMENT_DIR'] = str(tmp_path / 'enrollments')
os.makedirs(app.config['ENROLLMENT_DIR'], exist_ok=True)
yield app.test_client()
db.session.remove()
def _user(username, role):
from app import db
from app.models.user import User
u = User(username=username, full_name=username.title(), role=role,
email=f'{username}@example.com', active=True)
u.set_password('pw-correct1')
db.session.add(u)
db.session.commit()
return u
def _login(client, user):
resp = client.post('/auth/login',
data={'username': user.username, 'password': 'pw-correct1'},
follow_redirects=True)
assert 'Login - ' not in resp.get_data(as_text=True), 'login failed'
return resp
def _record(app, project_name='Acme Tower'):
"""Build and save one submission through the real storage layer.
Mirrors the payload routes.submit() actually writes — the admin templates
read every one of these keys, so a minimal stub renders as UndefinedError
rather than exercising the page.
"""
from datetime import datetime
from app.enrollment import storage, schema
now = datetime.now()
rec = {
'project_name': project_name,
'request_by': 'Reception',
'requester_email': 'reception@example.com',
'date_requested': now.strftime('%Y-%m-%d'),
'notes': '',
'people': [],
'matrix': {},
'mobile_app': {},
'id': storage.new_id(now),
'submitted_at': now.isoformat(timespec='seconds'),
'office': {k: '' for k, _ in schema.OFFICE_FIELDS},
'status': 'new',
'meta': {'ip': '127.0.0.1', 'user_agent': 'pytest'},
}
storage.save(rec)
return rec
# ── Public form ──────────────────────────────────────────────────────────────
def test_public_form_renders_without_login(client):
resp = client.get('/enrollment')
assert resp.status_code == 200
body = resp.get_data(as_text=True)
assert 'Login - ' not in body
# Login-free page: it must not carry the authenticated portal nav.
assert 'jqc-sidebar' not in body
def test_admin_list_requires_login(client):
resp = client.get('/enrollment/admin')
assert resp.status_code == 302
assert '/auth/login' in resp.headers.get('Location', '')
def test_admin_list_renders_for_admin(client, app):
admin = _user('ada', 'admin')
_login(client, admin)
with app.test_request_context():
_record(app, 'Listed Site')
resp = client.get('/enrollment/admin')
assert resp.status_code == 200
assert 'Listed Site' in resp.get_data(as_text=True)
# ── Storage round-trip ───────────────────────────────────────────────────────
def test_submission_round_trips(client, app):
from app.enrollment import storage
with app.test_request_context():
rec = _record(app)
back = storage.load(rec['id'])
assert back is not None
assert back['project_name'] == 'Acme Tower'
def test_customer_answers_are_immutable_after_submission(client, app):
"""Only the office block and status are writable — the file must stay an
accurate record of what the customer actually asked for."""
from app.enrollment import storage
with app.test_request_context():
rec = _record(app, project_name='Original Name')
storage.update_office(rec['id'], {'assigned_to': 'ops'}, 'in_review')
back = storage.load(rec['id'])
assert back['project_name'] == 'Original Name' # untouched
assert back['office']['assigned_to'] == 'ops'
assert back['status'] == 'in_review'
@pytest.mark.parametrize('bad_id', [
'../../etc/passwd',
'..%2f..%2fetc',
'not-an-id',
'',
'20260101-120000-ZZZZZZZZ', # non-hex suffix
])
def test_malformed_ids_are_rejected(client, app, bad_id):
"""A crafted id must never be interpolated into a filesystem path."""
from app.enrollment import storage
with app.test_request_context():
assert storage.load(bad_id) is None
# ── Tenant isolation ─────────────────────────────────────────────────────────
class _FakeTenant:
def __init__(self, tid):
self.id = tid
def test_single_tenant_mode_uses_the_root_directory(client, app):
"""With MT off, behaviour matches ST exactly — no per-tenant subdirectory."""
from app.enrollment import storage
with app.test_request_context():
assert app.config.get('MULTI_TENANT_ENABLED') is False
assert storage.enrollment_dir() == app.config['ENROLLMENT_DIR']
def test_submissions_are_filed_per_tenant(client, app):
from flask import g
from app.enrollment import storage
root = app.config['ENROLLMENT_DIR']
app.config['MULTI_TENANT_ENABLED'] = True
try:
with app.test_request_context():
g.tenant = _FakeTenant(7)
assert storage.enrollment_dir() == os.path.join(root, 't7')
_record(app, 'Tenant Seven Site')
with app.test_request_context():
g.tenant = _FakeTenant(9)
assert storage.enrollment_dir() == os.path.join(root, 't9')
finally:
app.config['MULTI_TENANT_ENABLED'] = False
assert os.path.isdir(os.path.join(root, 't7'))
def test_one_tenant_never_sees_anothers_submissions(client, app):
"""The leak this phase exists to prevent."""
from flask import g
from app.enrollment import storage
app.config['MULTI_TENANT_ENABLED'] = True
try:
with app.test_request_context():
g.tenant = _FakeTenant(7)
_record(app, 'Seven Confidential')
with app.test_request_context():
g.tenant = _FakeTenant(9)
others = storage.load_all()
names = [r.get('project_name') for r in others]
assert 'Seven Confidential' not in names
assert others == []
finally:
app.config['MULTI_TENANT_ENABLED'] = False
def test_unresolved_tenant_refuses_rather_than_sharing(client, app):
"""Fail closed. Falling back to the shared root would be a silent
cross-tenant leak; an exception is loud and safe."""
from flask import g
from app.enrollment import storage
app.config['MULTI_TENANT_ENABLED'] = True
try:
with app.test_request_context():
g.tenant = None
with pytest.raises(storage.TenantUnresolved):
storage.enrollment_dir()
finally:
app.config['MULTI_TENANT_ENABLED'] = False
# ── Branding ─────────────────────────────────────────────────────────────────
def test_no_hardcoded_upstream_branding_reaches_a_tenant(client):
"""ST hardcodes its own company name and a personal address in this module.
Neither may ever be shown to another tenant's customers."""
import pathlib
root = pathlib.Path('app/enrollment')
for path in root.rglob('*'):
if path.is_file() and path.suffix in ('.py', '.html'):
text = path.read_text(encoding='utf-8')
assert 'L.T' not in text, f'{path} still carries upstream branding'
assert 'da.nguyen8744' not in text, f'{path} still carries a personal address'