""" tests/test_followup_ownership.py --------------------------------- phase56 — a follow-up has exactly ONE owner, and every surface must agree. `Inspection.follow_up_owner` (the loaded-row property) and `Inspection.follow_up_owned_by()` (the SQL predicate) are two expressions of the same rule: the assignee when one is set, the inspection's own inspector otherwise. They are declared next to each other because they must not drift. What these guard ---------------- The predicate's two arms are mutually exclusive ON PURPOSE. Drop the `is_(None)` from the second arm and the original inspector keeps matching a follow-up that was handed to somebody else — two people turn up to do the same re-inspection, and nothing errors. The three query surfaces (mobile list filter, web dashboard card, iPad stats KPI) each used to express ownership by hand, and three of them tested AUTHORSHIP: an assignee saw the work in their list while both dashboards read 0. These tests pin the predicate itself; the surfaces now call it rather than re-deriving it. """ import pytest @pytest.fixture def client(app): with app.app_context(): from app import db db.drop_all() db.create_all() yield app.test_client() db.session.remove() def _user(username, role='inspector'): 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, password_set=True) u.set_password('pw-correct1') db.session.add(u) db.session.commit() return u def _inspection(inspector, assignee=None, follow_up=True): from app import db from app.models.facility import Facility from app.models.inspection import Inspection, InspectionTemplate fac = Facility.query.first() if fac is None: fac = Facility(name='Main Office', active=True) db.session.add(fac) db.session.commit() tmpl = InspectionTemplate.query.first() if tmpl is None: tmpl = InspectionTemplate(name='Restroom Check', active=True, form_schema=[]) db.session.add(tmpl) db.session.commit() insp = Inspection(template_id=tmpl.id, facility_id=fac.id, inspector_id=inspector.id, status='completed', follow_up_required=follow_up, follow_up_assigned_to=(assignee.id if assignee else None)) db.session.add(insp) db.session.commit() return insp def _owned_ids(user): from app.models.inspection import Inspection return sorted(i.id for i in Inspection.query .filter(Inspection.follow_up_owned_by(user.id)).all()) # ── the property ───────────────────────────────────────────────────────────── def test_owner_is_the_inspector_when_unassigned(client): ivy = _user('ivy') insp = _inspection(ivy) assert insp.follow_up_owner.id == ivy.id def test_owner_is_the_assignee_when_assigned(client): ivy, sam = _user('ivy'), _user('sam') insp = _inspection(ivy, assignee=sam) assert insp.follow_up_owner.id == sam.id # ── the SQL predicate ──────────────────────────────────────────────────────── def test_predicate_matches_unassigned_own_work(client): ivy = _user('ivy') insp = _inspection(ivy) assert _owned_ids(ivy) == [insp.id] def test_predicate_matches_work_handed_to_me(client): ivy, sam = _user('ivy'), _user('sam') insp = _inspection(ivy, assignee=sam) assert _owned_ids(sam) == [insp.id] def test_predicate_releases_the_original_inspector_once_assigned(client): """The `is_(None)` on the second arm. Without it Ivy still matches, and two people turn up to do the same re-inspection.""" ivy, sam = _user('ivy'), _user('sam') _inspection(ivy, assignee=sam) assert _owned_ids(ivy) == [] def test_every_row_has_exactly_one_owner(client): """The property and the predicate must partition the rows, not overlap.""" from app.models.inspection import Inspection ivy, sam, zoe = _user('ivy'), _user('sam'), _user('zoe') _inspection(ivy) # unassigned, Ivy's _inspection(ivy, assignee=sam) # Ivy's work, Sam's follow-up _inspection(sam, assignee=sam) # Sam's work, explicitly Sam's _inspection(zoe) # unassigned, Zoe's everyone = [ivy, sam, zoe] for insp in Inspection.query.all(): owners = [u.id for u in everyone if insp.id in _owned_ids(u)] assert owners == [insp.follow_up_owner.id], ( f'inspection {insp.id}: predicate says {owners}, ' f'property says {insp.follow_up_owner.id}')