Aug 5 - Update code to follow up ST - MT14b
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
"""
|
||||
tests/test_schedule_acknowledge.py
|
||||
-----------------------------------
|
||||
Behaviour tests for phase50 — scheduled inspection receipt acknowledgement.
|
||||
|
||||
Runs on the in-memory SQLite app fixture (multi-tenancy inert). Covers:
|
||||
|
||||
* the assigned inspector can confirm receipt; the stamp and creator
|
||||
notification land, and a second confirm is an idempotent no-op
|
||||
* a manager cannot confirm on the inspector's behalf (assignee-only)
|
||||
* the login-free token route confirms without a session, and is safe to
|
||||
re-request (email client prefetch)
|
||||
* a token issued to a previous assignee stops working once the schedule is
|
||||
reassigned — the property that makes a stateless token safe
|
||||
* tampered, expired and dangling tokens each render their own status page
|
||||
rather than a bare error
|
||||
* reassignment resets the acknowledgement; an unrelated edit does NOT
|
||||
* acknowledgement is per ASSIGNMENT, not per occurrence — rolling the schedule
|
||||
forward must not re-open it
|
||||
* notify(extra_action=...) renders the confirm button in the email, and stops
|
||||
once confirmed
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Fresh schema + test client for each test (isolated in-memory DB)."""
|
||||
app.config['DIGEST_SECRET'] = 'test-digest'
|
||||
with app.app_context():
|
||||
from app import db
|
||||
from app.models import inspector_assignment # noqa: F401
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
yield app.test_client()
|
||||
db.session.remove()
|
||||
|
||||
|
||||
def _today():
|
||||
"""Today in the app's timezone, not the machine's."""
|
||||
from app.utils.time_utils import now_eastern
|
||||
return now_eastern().date()
|
||||
|
||||
|
||||
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 _seed():
|
||||
from app import db
|
||||
from app.models.facility import Facility
|
||||
from app.models.inspection import InspectionTemplate
|
||||
|
||||
tmpl = InspectionTemplate(name='Restroom Check', active=True,
|
||||
form_schema=[{'id': 'f1', 'type': 'rating_5',
|
||||
'label': 'Clean', 'row': 0, 'col': 0,
|
||||
'rowSpan': 1, 'colSpan': 1}])
|
||||
fac = Facility(name='Main Office', active=True)
|
||||
db.session.add_all([tmpl, fac])
|
||||
db.session.commit()
|
||||
return tmpl, fac
|
||||
|
||||
|
||||
def _schedule(tmpl, fac, inspector, creator, **kw):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
from app.utils.time_utils import now_eastern
|
||||
|
||||
kw.setdefault('mode', 'plan')
|
||||
kw.setdefault('frequency', 'weekly')
|
||||
kw.setdefault('active', True)
|
||||
s = InspectionSchedule(name='Weekly restrooms', template_id=tmpl.id,
|
||||
facility_id=fac.id, inspector_id=inspector.id,
|
||||
created_by=creator.id,
|
||||
next_run_at=now_eastern() + timedelta(days=3), **kw)
|
||||
db.session.add(s)
|
||||
db.session.commit()
|
||||
return s
|
||||
|
||||
|
||||
def _login(client, user):
|
||||
return client.post('/auth/login',
|
||||
data={'username': user.username, 'password': 'pw-correct1'},
|
||||
follow_redirects=True)
|
||||
|
||||
|
||||
# ── Logged-in acknowledgement ────────────────────────────────────────────────
|
||||
|
||||
def test_assigned_inspector_can_confirm(client):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
from app.models.notification import Notification
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
assert s.is_acknowledged is False
|
||||
|
||||
_login(client, insp)
|
||||
resp = client.post(f'/inspection-schedules/{s.id}/acknowledge')
|
||||
assert resp.status_code == 302
|
||||
|
||||
db.session.expire_all()
|
||||
s = db.session.get(InspectionSchedule, s.id)
|
||||
assert s.is_acknowledged is True
|
||||
assert s.acknowledged_at is not None
|
||||
|
||||
# The creator is told, since they are the one waiting on the confirmation.
|
||||
assert any('confirmed receipt' in n.title.lower()
|
||||
for n in Notification.query.filter_by(user_id=mgr.id).all())
|
||||
|
||||
|
||||
def test_confirming_twice_is_idempotent(client):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
from app.models.notification import Notification
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
_login(client, insp)
|
||||
client.post(f'/inspection-schedules/{s.id}/acknowledge')
|
||||
db.session.expire_all()
|
||||
first = db.session.get(InspectionSchedule, s.id).acknowledged_at
|
||||
|
||||
client.post(f'/inspection-schedules/{s.id}/acknowledge')
|
||||
db.session.expire_all()
|
||||
assert db.session.get(InspectionSchedule, s.id).acknowledged_at == first
|
||||
# And the creator is not pestered a second time.
|
||||
assert len([n for n in Notification.query.filter_by(user_id=mgr.id).all()
|
||||
if 'confirmed receipt' in n.title.lower()]) == 1
|
||||
|
||||
|
||||
def test_manager_cannot_confirm_on_the_inspectors_behalf(client):
|
||||
"""The record means "this person saw it", so only the assignee may set it."""
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
_login(client, mgr)
|
||||
assert client.post(f'/inspection-schedules/{s.id}/acknowledge').status_code == 403
|
||||
db.session.expire_all()
|
||||
assert db.session.get(InspectionSchedule, s.id).is_acknowledged is False
|
||||
|
||||
|
||||
def test_other_inspector_cannot_confirm(client):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
other = _user('otto', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
_login(client, other)
|
||||
assert client.post(f'/inspection-schedules/{s.id}/acknowledge').status_code == 403
|
||||
db.session.expire_all()
|
||||
assert db.session.get(InspectionSchedule, s.id).is_acknowledged is False
|
||||
|
||||
|
||||
# ── Login-free email token ───────────────────────────────────────────────────
|
||||
|
||||
def _token_for(app, schedule):
|
||||
from app.routes.inspection_schedules import _make_ack_token
|
||||
with app.test_request_context():
|
||||
return _make_ack_token(schedule)
|
||||
|
||||
|
||||
def test_email_token_confirms_without_a_session(client, app):
|
||||
"""Inspectors read this on a phone that is usually not logged in — a login
|
||||
wall is exactly what stops them confirming."""
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
token = _token_for(app, s)
|
||||
|
||||
resp = client.get(f'/inspection-schedules/confirm/{token}')
|
||||
assert resp.status_code == 200
|
||||
assert b'Receipt confirmed' in resp.data
|
||||
|
||||
db.session.expire_all()
|
||||
assert db.session.get(InspectionSchedule, s.id).is_acknowledged is True
|
||||
|
||||
|
||||
def test_email_token_reclick_is_harmless(client, app):
|
||||
"""A re-click, or an email client prefetching the link, must not error."""
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
token = _token_for(app, s)
|
||||
|
||||
client.get(f'/inspection-schedules/confirm/{token}')
|
||||
resp = client.get(f'/inspection-schedules/confirm/{token}')
|
||||
assert resp.status_code == 200
|
||||
assert b'Already confirmed' in resp.data
|
||||
|
||||
|
||||
def test_reassignment_invalidates_a_previously_emailed_token(client, app):
|
||||
"""This is what makes a stateless token safe: the old link dies at
|
||||
redemption, with nothing tracked anywhere."""
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
other = _user('otto', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
token = _token_for(app, s) # emailed to Ivy
|
||||
|
||||
s.inspector_id = other.id # reassigned to Otto
|
||||
db.session.commit()
|
||||
|
||||
resp = client.get(f'/inspection-schedules/confirm/{token}')
|
||||
assert resp.status_code == 409
|
||||
assert b'No longer assigned to you' in resp.data
|
||||
db.session.expire_all()
|
||||
assert db.session.get(InspectionSchedule, s.id).is_acknowledged is False
|
||||
|
||||
|
||||
def test_tampered_token_renders_the_invalid_page(client):
|
||||
resp = client.get('/inspection-schedules/confirm/not-a-real-token')
|
||||
assert resp.status_code == 400
|
||||
assert b"isn't valid" in resp.data
|
||||
|
||||
|
||||
def test_expired_token_renders_the_expired_page(client, app):
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
with app.test_request_context():
|
||||
from app.routes.inspection_schedules import _ACK_SALT
|
||||
ser = URLSafeTimedSerializer(app.config['SECRET_KEY'], salt=_ACK_SALT)
|
||||
# Sign it 31 days ago — past the 30-day _ACK_MAX_AGE.
|
||||
import itsdangerous.timed
|
||||
old = ser.dumps({'sid': s.id, 'iid': insp.id})
|
||||
|
||||
# Re-sign with a backdated timestamp by monkeypatching the serializer clock.
|
||||
import time as _time
|
||||
real = _time.time
|
||||
try:
|
||||
_time.time = lambda: real() - (60 * 60 * 24 * 31)
|
||||
with app.test_request_context():
|
||||
old = URLSafeTimedSerializer(
|
||||
app.config['SECRET_KEY'], salt=_ACK_SALT
|
||||
).dumps({'sid': s.id, 'iid': insp.id})
|
||||
finally:
|
||||
_time.time = real
|
||||
|
||||
resp = client.get(f'/inspection-schedules/confirm/{old}')
|
||||
assert resp.status_code == 400
|
||||
assert b'expired' in resp.data.lower()
|
||||
|
||||
|
||||
def test_token_for_a_deleted_schedule_renders_missing(client, app):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
token = _token_for(app, s)
|
||||
|
||||
db.session.delete(db.session.get(InspectionSchedule, s.id))
|
||||
db.session.commit()
|
||||
|
||||
resp = client.get(f'/inspection-schedules/confirm/{token}')
|
||||
assert resp.status_code == 404
|
||||
assert b'not found' in resp.data.lower()
|
||||
|
||||
|
||||
def test_inactive_schedule_renders_inactive(client, app):
|
||||
from app import db
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
token = _token_for(app, s)
|
||||
|
||||
s.active = False
|
||||
db.session.commit()
|
||||
|
||||
resp = client.get(f'/inspection-schedules/confirm/{token}')
|
||||
assert resp.status_code == 200
|
||||
assert b'no longer active' in resp.data.lower()
|
||||
|
||||
|
||||
# ── Reset semantics ──────────────────────────────────────────────────────────
|
||||
|
||||
def test_reassignment_resets_the_acknowledgement(client):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
other = _user('otto', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
_login(client, insp)
|
||||
client.post(f'/inspection-schedules/{s.id}/acknowledge')
|
||||
client.get('/auth/logout', follow_redirects=True)
|
||||
|
||||
_login(client, mgr)
|
||||
client.post(f'/inspection-schedules/{s.id}/edit', data={
|
||||
'name': 'Weekly restrooms', 'template_id': tmpl.id, 'facility_id': fac.id,
|
||||
'inspector_id': other.id, 'frequency': 'weekly', 'mode': 'plan',
|
||||
'weekdays': ['0'], 'active': 'on',
|
||||
}, follow_redirects=True)
|
||||
|
||||
db.session.expire_all()
|
||||
s = db.session.get(InspectionSchedule, s.id)
|
||||
assert s.inspector_id == other.id
|
||||
assert s.is_acknowledged is False
|
||||
|
||||
|
||||
def test_unrelated_edit_does_not_reset_the_acknowledgement(client):
|
||||
"""Only a real reassignment re-opens it — otherwise every rename would make
|
||||
the inspector confirm again."""
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
_login(client, insp)
|
||||
client.post(f'/inspection-schedules/{s.id}/acknowledge')
|
||||
client.get('/auth/logout', follow_redirects=True)
|
||||
db.session.expire_all()
|
||||
stamped = db.session.get(InspectionSchedule, s.id).acknowledged_at
|
||||
|
||||
_login(client, mgr)
|
||||
client.post(f'/inspection-schedules/{s.id}/edit', data={
|
||||
'name': 'Renamed round', 'template_id': tmpl.id, 'facility_id': fac.id,
|
||||
'inspector_id': insp.id, 'frequency': 'weekly', 'mode': 'plan',
|
||||
'weekdays': ['0'], 'active': 'on',
|
||||
}, follow_redirects=True)
|
||||
|
||||
db.session.expire_all()
|
||||
s = db.session.get(InspectionSchedule, s.id)
|
||||
assert s.name == 'Renamed round'
|
||||
assert s.acknowledged_at == stamped
|
||||
|
||||
|
||||
def test_acknowledgement_survives_rolling_the_schedule_forward(client):
|
||||
"""Per ASSIGNMENT, not per occurrence — an inspector who confirmed "this
|
||||
weekly round is mine" must not be asked again every week."""
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr, frequency='daily')
|
||||
|
||||
_login(client, insp)
|
||||
client.post(f'/inspection-schedules/{s.id}/acknowledge')
|
||||
db.session.expire_all()
|
||||
|
||||
s = db.session.get(InspectionSchedule, s.id)
|
||||
stamped = s.acknowledged_at
|
||||
s.fulfill()
|
||||
db.session.commit()
|
||||
|
||||
assert s.acknowledged_at == stamped
|
||||
assert s.is_acknowledged is True
|
||||
|
||||
|
||||
# ── notify(extra_action=...) ─────────────────────────────────────────────────
|
||||
|
||||
def test_confirm_action_is_offered_only_when_there_is_something_to_confirm(client, app):
|
||||
from app import db
|
||||
from app.routes.inspection_schedules import _confirm_action
|
||||
|
||||
tmpl, fac = _seed()
|
||||
insp = _user('ivy', 'inspector')
|
||||
mgr = _user('mona', 'admin')
|
||||
s = _schedule(tmpl, fac, insp, mgr)
|
||||
|
||||
with app.test_request_context():
|
||||
action = _confirm_action(s)
|
||||
assert action is not None
|
||||
assert action['label'] == 'Confirm receipt'
|
||||
assert '/inspection-schedules/confirm/' in action['url']
|
||||
|
||||
# Auto mode materialises its own inspection — nothing to receive.
|
||||
s.mode = 'auto'
|
||||
assert _confirm_action(s) is None
|
||||
s.mode = 'plan'
|
||||
|
||||
# Already confirmed — the button stops appearing in later reminders.
|
||||
s.acknowledged_at = datetime.now()
|
||||
assert _confirm_action(s) is None
|
||||
s.acknowledged_at = None
|
||||
|
||||
# Unassigned — nobody to confirm.
|
||||
s.inspector_id = None
|
||||
assert _confirm_action(s) is None
|
||||
|
||||
|
||||
def test_email_renders_the_extra_action_button(client, app):
|
||||
"""notify(extra_action=...) is new plumbing; assert it reaches the body."""
|
||||
from flask import render_template_string
|
||||
from app.utils.notifications import _EMAIL_HTML_SINGLE, _EMAIL_TEXT_SINGLE
|
||||
|
||||
action = {'label': 'Confirm receipt', 'url': 'https://lts.jqc.app/x/abc'}
|
||||
with app.test_request_context():
|
||||
html = render_template_string(_EMAIL_HTML_SINGLE, title='T', body='B',
|
||||
link='/inspection-schedules',
|
||||
base_url='https://lts.jqc.app',
|
||||
extra_action=action)
|
||||
text = render_template_string(_EMAIL_TEXT_SINGLE, title='T', body='B',
|
||||
link='/inspection-schedules',
|
||||
base_url='https://lts.jqc.app',
|
||||
extra_action=action)
|
||||
assert 'Confirm receipt' in html and action['url'] in html
|
||||
assert 'Confirm receipt' in text and action['url'] in text
|
||||
# The generic link still renders alongside it.
|
||||
assert 'View Details' in html
|
||||
|
||||
# And without extra_action nothing changes for every other caller.
|
||||
with app.test_request_context():
|
||||
plain = render_template_string(_EMAIL_HTML_SINGLE, title='T', body='B',
|
||||
link='/x', base_url='http://h',
|
||||
extra_action=None)
|
||||
assert 'Confirm receipt' not in plain
|
||||
assert 'View Details' in plain
|
||||
@@ -25,6 +25,18 @@ from datetime import date, datetime, timedelta
|
||||
import pytest
|
||||
|
||||
|
||||
def _today():
|
||||
"""Today in the app's timezone, not the machine's.
|
||||
|
||||
The routes validate due dates against now_eastern(). Using _today() here
|
||||
made these tests fail on a UTC host between 20:00 ET and midnight, when the
|
||||
two calendars disagree — "yesterday" by UTC is still today in Eastern, so a
|
||||
date the test expected to be rejected was legitimately accepted.
|
||||
"""
|
||||
from app.utils.time_utils import now_eastern
|
||||
return now_eastern().date()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""Fresh schema + test client for each test (isolated in-memory DB)."""
|
||||
@@ -81,11 +93,12 @@ def _post_follow_up(client, user, **body):
|
||||
# ── Creation ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_follow_up_creates_one_time_plan_schedule_from_the_parent(client):
|
||||
from app import db
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
user, tmpl, fac = _seed('mk', role='project_manager')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
due = date.today() + timedelta(days=7)
|
||||
due = _today() + timedelta(days=7)
|
||||
|
||||
resp = _post_follow_up(client, user,
|
||||
parent_inspection_id=parent.id,
|
||||
@@ -95,7 +108,7 @@ def test_follow_up_creates_one_time_plan_schedule_from_the_parent(client):
|
||||
data = resp.get_json()['data']
|
||||
assert data['created'] is True
|
||||
|
||||
s = InspectionSchedule.query.get(data['scheduled']['id'])
|
||||
s = db.session.get(InspectionSchedule, data['scheduled']['id'])
|
||||
assert s.parent_inspection_id == parent.id
|
||||
assert s.is_follow_up is True
|
||||
# Everything derived from the parent, nothing taken from the client.
|
||||
@@ -130,11 +143,12 @@ def test_follow_up_ignores_client_supplied_facility_and_template(client):
|
||||
|
||||
resp = _post_follow_up(client, user,
|
||||
parent_inspection_id=parent.id,
|
||||
due_date=(date.today() + timedelta(days=3)).isoformat(),
|
||||
due_date=(_today() + timedelta(days=3)).isoformat(),
|
||||
facility_id=other_fac.id,
|
||||
template_id=other_tmpl.id,
|
||||
frequency='daily', mode='auto')
|
||||
s = InspectionSchedule.query.get(resp.get_json()['data']['scheduled']['id'])
|
||||
s = db.session.get(InspectionSchedule,
|
||||
resp.get_json()['data']['scheduled']['id'])
|
||||
assert s.facility_id == fac.id
|
||||
assert s.template_id == tmpl.id
|
||||
assert s.frequency == 'once'
|
||||
@@ -146,8 +160,8 @@ def test_follow_up_is_idempotent_on_retry(client):
|
||||
|
||||
user, tmpl, fac = _seed('idem', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
first_due = date.today() + timedelta(days=5)
|
||||
second_due = date.today() + timedelta(days=9)
|
||||
first_due = _today() + timedelta(days=5)
|
||||
second_due = _today() + timedelta(days=9)
|
||||
|
||||
r1 = _post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=first_due.isoformat())
|
||||
@@ -171,14 +185,14 @@ def test_follow_up_reschedule_rearms_reminders(client):
|
||||
user, tmpl, fac = _seed('rearm', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
_post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=(date.today() + timedelta(days=2)).isoformat())
|
||||
due_date=(_today() + timedelta(days=2)).isoformat())
|
||||
|
||||
s = InspectionSchedule.query.filter_by(parent_inspection_id=parent.id).one()
|
||||
s.advance_notified = s.due_notified = s.overdue_notified = True
|
||||
db.session.commit()
|
||||
|
||||
_post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=(date.today() + timedelta(days=12)).isoformat())
|
||||
due_date=(_today() + timedelta(days=12)).isoformat())
|
||||
db.session.expire_all()
|
||||
s = InspectionSchedule.query.filter_by(parent_inspection_id=parent.id).one()
|
||||
assert s.advance_notified is False
|
||||
@@ -201,14 +215,14 @@ def test_follow_up_requires_a_completed_parent(client):
|
||||
db.session.commit()
|
||||
|
||||
r = _post_follow_up(client, user, parent_inspection_id=draft.id,
|
||||
due_date=(date.today() + timedelta(days=1)).isoformat())
|
||||
due_date=(_today() + timedelta(days=1)).isoformat())
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_follow_up_rejects_bad_input(client):
|
||||
user, tmpl, fac = _seed('bad', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
ok_due = (date.today() + timedelta(days=1)).isoformat()
|
||||
ok_due = (_today() + timedelta(days=1)).isoformat()
|
||||
|
||||
# Missing parent id.
|
||||
assert _post_follow_up(client, user, due_date=ok_due).status_code == 400
|
||||
@@ -222,7 +236,7 @@ def test_follow_up_rejects_bad_input(client):
|
||||
assert _post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date='next tuesday').status_code == 400
|
||||
# Past date.
|
||||
past = (date.today() - timedelta(days=1)).isoformat()
|
||||
past = (_today() - timedelta(days=1)).isoformat()
|
||||
assert _post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=past).status_code == 400
|
||||
|
||||
@@ -232,7 +246,7 @@ def test_follow_up_allows_today(client):
|
||||
user, tmpl, fac = _seed('today', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
r = _post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=date.today().isoformat())
|
||||
due_date=_today().isoformat())
|
||||
assert r.status_code == 201
|
||||
|
||||
|
||||
@@ -241,7 +255,7 @@ def test_follow_up_rejects_auditor(client):
|
||||
user, tmpl, fac = _seed('aud', role='auditor')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
r = _post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=(date.today() + timedelta(days=1)).isoformat())
|
||||
due_date=(_today() + timedelta(days=1)).isoformat())
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
@@ -259,7 +273,7 @@ def test_inspector_cannot_follow_up_someone_elses_inspection(client):
|
||||
db.session.commit()
|
||||
|
||||
r = _post_follow_up(client, other, parent_inspection_id=parent.id,
|
||||
due_date=(date.today() + timedelta(days=1)).isoformat())
|
||||
due_date=(_today() + timedelta(days=1)).isoformat())
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
@@ -268,7 +282,7 @@ def test_follow_up_requires_auth(client):
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
r = client.post('/api/v1/scheduled-inspections/follow-up',
|
||||
json={'parent_inspection_id': parent.id,
|
||||
'due_date': date.today().isoformat()})
|
||||
'due_date': _today().isoformat()})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
@@ -284,7 +298,7 @@ def test_web_start_inherits_the_parent_link(client):
|
||||
user, tmpl, fac = _seed('start', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
_post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=date.today().isoformat())
|
||||
due_date=_today().isoformat())
|
||||
sched = InspectionSchedule.query.filter_by(parent_inspection_id=parent.id).one()
|
||||
|
||||
client.post('/auth/login', data={'username': user.username,
|
||||
@@ -307,7 +321,7 @@ def test_web_start_resumes_instead_of_duplicating(client):
|
||||
user, tmpl, fac = _seed('resume', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
_post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=date.today().isoformat())
|
||||
due_date=_today().isoformat())
|
||||
sched = InspectionSchedule.query.filter_by(parent_inspection_id=parent.id).one()
|
||||
|
||||
client.post('/auth/login', data={'username': user.username,
|
||||
@@ -352,13 +366,14 @@ def test_cron_materialiser_inherits_the_parent_link(client):
|
||||
def test_api_create_infers_parent_from_the_schedule(client):
|
||||
"""An older iPad build submits the schedule id but no parent. Without the
|
||||
inference the parent would stay flagged forever."""
|
||||
from app import db
|
||||
from app.models.inspection import Inspection
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
user, tmpl, fac = _seed('infer', role='admin')
|
||||
parent = _completed_inspection(user, tmpl, fac)
|
||||
_post_follow_up(client, user, parent_inspection_id=parent.id,
|
||||
due_date=date.today().isoformat())
|
||||
due_date=_today().isoformat())
|
||||
sched = InspectionSchedule.query.filter_by(parent_inspection_id=parent.id).one()
|
||||
|
||||
resp = client.post('/api/v1/inspections', headers=_auth(user), json={
|
||||
@@ -369,13 +384,14 @@ def test_api_create_infers_parent_from_the_schedule(client):
|
||||
assert resp.status_code in (200, 201)
|
||||
|
||||
new_id = resp.get_json()['data']['inspection_id']
|
||||
run = Inspection.query.get(new_id)
|
||||
run = db.session.get(Inspection, new_id)
|
||||
assert run.parent_inspection_id == parent.id
|
||||
# And the whole point of the link: the parent's flag is cleared.
|
||||
assert parent.follow_up_required is False
|
||||
|
||||
|
||||
def test_api_explicit_parent_wins_over_the_schedule(client):
|
||||
from app import db
|
||||
from app.models.inspection import Inspection
|
||||
from app.models.inspection_schedule import InspectionSchedule
|
||||
|
||||
@@ -383,7 +399,7 @@ def test_api_explicit_parent_wins_over_the_schedule(client):
|
||||
parent_a = _completed_inspection(user, tmpl, fac)
|
||||
parent_b = _completed_inspection(user, tmpl, fac)
|
||||
_post_follow_up(client, user, parent_inspection_id=parent_a.id,
|
||||
due_date=date.today().isoformat())
|
||||
due_date=_today().isoformat())
|
||||
sched = InspectionSchedule.query.filter_by(parent_inspection_id=parent_a.id).one()
|
||||
|
||||
resp = client.post('/api/v1/inspections', headers=_auth(user), json={
|
||||
@@ -393,7 +409,7 @@ def test_api_explicit_parent_wins_over_the_schedule(client):
|
||||
'form_data': {'f1': 5},
|
||||
})
|
||||
new_id = resp.get_json()['data']['inspection_id']
|
||||
assert Inspection.query.get(new_id).parent_inspection_id == parent_b.id
|
||||
assert db.session.get(Inspection, new_id).parent_inspection_id == parent_b.id
|
||||
|
||||
|
||||
def test_ordinary_schedule_produces_no_parent_link(client):
|
||||
|
||||
Reference in New Issue
Block a user