From 8b2582705dcc68ff363c3c5c2bb2e9b8e99eda14 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Wed, 19 Aug 2026 16:34:42 -0400 Subject: [PATCH] Aug 19 - Update test files --- tests/test_external_inspector.py | 77 +++++++++++++++++++++++++------- tests/test_open_items.py | 35 ++++++++++++++- 2 files changed, 95 insertions(+), 17 deletions(-) diff --git a/tests/test_external_inspector.py b/tests/test_external_inspector.py index 91be4e3..079d7b1 100644 --- a/tests/test_external_inspector.py +++ b/tests/test_external_inspector.py @@ -15,10 +15,13 @@ Runs on the in-memory SQLite app fixture (multi-tenancy inert). Covers: * facility / inspection / issue list routes stay scoped for the new role * the notification matrix exposes an External Inspector column whose defaults mirror the Inspector column - * creating an external inspector sends an invitation instead of setting a - password: password_set is False and a set-password token is minted + * an invited Customer Inspector is created with password_set False and a + set-password token, and cannot log in until they use it * creating any other role still requires a password - * the assign-contracts page accepts an external inspector (it 404'd before) + * creating a Customer Inspector goes through Customer Management, and + User Management no longer offers the role (phase51) + * the staff assign-contracts page redirects a customer-side account to + the page that owns it, and still serves our own inspectors The regression guard that matters most is test_external_inspector_scope_is_not_unrestricted: if a future edit reverts a @@ -224,23 +227,27 @@ def test_matrix_defaults_mirror_the_inspector_column(client): # ── Invitation flow ────────────────────────────────────────────────────────── def test_creating_external_inspector_invites_instead_of_setting_password(client): + """phase51 moved this account type to Customer Management. + + The invariant is unchanged and is what this test guards: a Customer + Inspector is INVITED, never given a password we chose. Only the door + changed — /customers/new instead of /auth/users/new — because both + customer-side roles are now owned by /customers. + """ from app.models.user import User env = _seed() _login(client, env['admin']) - resp = client.post('/auth/users/new', data={ - 'username': 'newxan', + resp = client.post('/customers/new', data={ 'full_name': 'New Xan', 'email': 'newxan@example.com', 'role': 'external_inspector', - 'password': '', - 'confirm_password': '', }, follow_redirects=True) assert resp.status_code == 200 - created = User.query.filter_by(username='newxan').first() - assert created is not None, 'external inspector was not created' + created = User.query.filter_by(email='newxan@example.com').first() + assert created is not None, 'customer inspector was not created' assert created.role == 'external_inspector' # Invited, not password-set: login is blocked until they use the link. assert created.password_set is False @@ -248,22 +255,42 @@ def test_creating_external_inspector_invites_instead_of_setting_password(client) assert created.set_password_token_expires is not None +def test_user_management_no_longer_creates_customer_side_accounts(client): + """The other half of the move: User Management must not mint one. + + UserForm stopped offering 'external_inspector', so a crafted POST hits + SelectField validation and nothing is created. Without this, a second + creation path could quietly reappear and skip the invitation flow. + """ + from app.models.user import User + + env = _seed() + _login(client, env['admin']) + + client.post('/auth/users/new', data={ + 'username': 'sneaky', + 'full_name': 'Sneaky Xan', + 'email': 'sneaky@example.com', + 'role': 'external_inspector', + 'password': '', + 'confirm_password': '', + }, follow_redirects=True) + assert User.query.filter_by(username='sneaky').first() is None + + def test_invited_external_inspector_cannot_log_in_until_setup(client): from app.models.user import User env = _seed() _login(client, env['admin']) - client.post('/auth/users/new', data={ - 'username': 'newxan', + client.post('/customers/new', data={ 'full_name': 'New Xan', 'email': 'newxan@example.com', 'role': 'external_inspector', - 'password': '', - 'confirm_password': '', }, follow_redirects=True) client.get('/auth/logout', follow_redirects=True) - created = User.query.filter_by(username='newxan').first() + created = User.query.filter_by(email='newxan@example.com').first() # The placeholder hash is random, so no password can work; assert the # account is in the blocked state rather than guessing a credential. assert created.password_set is False @@ -289,11 +316,31 @@ def test_creating_a_normal_role_still_requires_a_password(client): # ── Assign-contracts page ──────────────────────────────────────────────────── -def test_assign_contracts_page_accepts_external_inspector(client): +def test_assign_contracts_page_redirects_customer_side_to_customer_management(client): + """phase51: one editor per account, not two. + + The staff assign-contracts URL still exists for our own inspectors, but a + customer-side account is redirected to the page that now owns it. Editing + one through UserForm would fail anyway — 'external_inspector' is no longer + an offered role choice, so SelectField would reject the stored value. + """ env = _seed() _login(client, env['admin']) resp = client.get(f"/auth/users/{env['external'].id}/assign-contracts") + assert resp.status_code == 302 + assert f"/customers/{env['external'].id}" in resp.headers['Location'] + + # …and the page it redirects to is the real editor. + assert client.get(f"/customers/{env['external'].id}").status_code == 200 + + +def test_assign_contracts_page_still_accepts_our_own_inspector(client): + """The staff path must not have been broken by the redirect guard.""" + env = _seed() + _login(client, env['admin']) + + resp = client.get(f"/auth/users/{env['internal'].id}/assign-contracts") assert resp.status_code == 200 diff --git a/tests/test_open_items.py b/tests/test_open_items.py index 0ae92e6..ca2234f 100644 --- a/tests/test_open_items.py +++ b/tests/test_open_items.py @@ -343,8 +343,39 @@ def test_inspector_is_not_offered_schedule_creation(client): assert 'Create First Schedule' not in body -def test_customers_are_still_barred(client): +def test_customer_directors_may_reach_the_schedule_list(client): + """A Customer Director plans work at their OWN facilities (Aug 2026). + + They reach the list — scoped to their assignments, which for an + unassigned account like this one means an empty list, not a 403. + """ tmpl, fac = _seed() cust = _user('cara', 'customer') _login(client, cust) - assert client.get('/inspection-schedules/?tab=completed').status_code == 403 + assert client.get('/inspection-schedules/?tab=completed').status_code == 200 + + +def test_customers_still_cannot_start_a_scheduled_inspection(client): + """Planning is not performing — the boundary that replaced the old 403. + + A Customer Director may create and edit a schedule; Start belongs to the + assigned inspector, and a customer is never one. + """ + from app import db + from app.models.inspection_schedule import InspectionSchedule + from app.utils.time_utils import now_eastern + + tmpl, fac = _seed() + insp = _user('ivy', 'inspector') + sched = InspectionSchedule( + name='Weekly check', template_id=tmpl.id, facility_id=fac.id, + inspector_id=insp.id, frequency='weekly', mode='plan', active=True, + created_at=now_eastern(), next_run_at=now_eastern(), + ) + db.session.add(sched) + db.session.commit() + sid = sched.id + + cust = _user('cara', 'customer') + _login(client, cust) + assert client.get(f'/inspection-schedules/{sid}/start').status_code == 403