Remediate the 2026-09-03 feature-test findings

This commit is contained in:
2026-09-03 17:14:53 -04:00
parent ddf41c3e65
commit ff9ae693a6
112 changed files with 5182 additions and 857 deletions

View File

@@ -488,3 +488,159 @@ def test_asset_presentation_route_token():
entries = MeasuringToolsPlugin().get_asset_presentation()
entry = next(e for e in entries if e['assettype'] == 'measuring_tool')
assert entry['route'] == '/measuringtools/by-asset/{assetid}'
# --- a date that is not a date -----------------------------------------------
#
# _parse_date used to swallow the ValueError and return None, so a typo in the
# calibration date answered 200 "Measuring tool updated", stored NULL, flipped
# the tool to 'unknown', and dropped it out of the calibration report and the
# map overlay. Clearing a date and mistyping one are different intents and now
# get different answers.
def test_a_mistyped_calibration_date_is_refused_not_stored_as_null(
client, auth_headers):
created = client.post('/api/measuringtools', headers=auth_headers, json={
'assetnumber': 'MT-DATE-1',
'name': 'Date guard caliper',
'statusid': _status_id(client),
'nextcalibrationdate': '2027-01-01',
})
assert created.status_code == 201, created.get_json()
toolid = created.get_json()['data']['measuringtool']['measuringtoolid']
refused = client.put(f'/api/measuringtools/{toolid}', headers=auth_headers,
json={'nextcalibrationdate': 'not-a-date'})
assert refused.status_code == 400, refused.get_json()
assert 'nextcalibrationdate' in \
refused.get_json()['data']['error']['message']
# The stored date is untouched - the whole point of refusing.
after = client.get(f'/api/measuringtools/{toolid}',
headers=auth_headers).get_json()['data']
assert after['measuringtool']['nextcalibrationdate'] == '2027-01-01'
assert after['measuringtool']['calibrationstatus'] != 'unknown'
def test_an_explicitly_empty_calibration_date_still_clears_it(client,
auth_headers):
"""Clearing is a real intent and must keep working, or the fix above just
trades one broken behaviour for another."""
created = client.post('/api/measuringtools', headers=auth_headers, json={
'assetnumber': 'MT-DATE-2',
'name': 'Clearable caliper',
'statusid': _status_id(client),
'nextcalibrationdate': '2027-01-01',
})
toolid = created.get_json()['data']['measuringtool']['measuringtoolid']
cleared = client.put(f'/api/measuringtools/{toolid}', headers=auth_headers,
json={'nextcalibrationdate': ''})
assert cleared.status_code == 200, cleared.get_json()
after = client.get(f'/api/measuringtools/{toolid}',
headers=auth_headers).get_json()['data']
assert after['measuringtool']['nextcalibrationdate'] is None
def test_a_mistyped_date_is_refused_at_create_too(client, auth_headers):
refused = client.post('/api/measuringtools', headers=auth_headers, json={
'assetnumber': 'MT-DATE-3',
'name': 'Never created',
'statusid': _status_id(client),
'lastcalibrationdate': '01/02/2026',
})
assert refused.status_code == 400, refused.get_json()
listed = client.get('/api/measuringtools?search=MT-DATE-3',
headers=auth_headers).get_json()['data']
assert listed == [], 'nothing may be created by a refused request'
# --- an audited field holding a date must not kill the request ---------------
#
# PUT /api/machines/<id> returned 500 and rolled the whole edit back for any
# machine with a maintenance date: the route builds
# {'old': getattr(row, field)} straight off the model, and json.dumps raises on
# a date. A rename that never touched the date failed too. The audit log
# refusing to record a change must not be what stops the change.
def test_an_edit_is_saved_even_when_an_audited_field_holds_a_date(
client, auth_headers):
created = client.post('/api/measuringtools', headers=auth_headers, json={
'assetnumber': 'MT-AUDIT-1',
'name': 'Before rename',
'statusid': _status_id(client),
'lastcalibrationdate': '2026-01-15',
'nextcalibrationdate': '2027-01-15',
})
assert created.status_code == 201, created.get_json()
toolid = created.get_json()['data']['measuringtool']['measuringtoolid']
# A rename that does not touch the dates. The date is only in the CHANGES
# dict because the interval moves with it.
renamed = client.put(f'/api/measuringtools/{toolid}', headers=auth_headers,
json={'name': 'After rename',
'nextcalibrationdate': '2028-01-15'})
assert renamed.status_code == 200, renamed.get_json()
after = client.get(f'/api/measuringtools/{toolid}',
headers=auth_headers).get_json()['data']
assert after['name'] == 'After rename'
assert after['measuringtool']['nextcalibrationdate'] == '2028-01-15'
# --- a derived filter must page over the MATCHES ----------------------------
#
# calibrationstatus was applied to the rows of the current page, so a request
# for 'overdue' returned whatever matched on that page while the total counted
# every tool. With one overdue tool in sixteen: page 1 empty, total 16, eight
# pages, and the row itself on page 3. The filter hid matching rows and
# miscounted the rest.
def test_a_calibration_filter_pages_over_the_matches_not_the_page(
client, auth_headers):
from datetime import date, timedelta
overdue = (date.today() - timedelta(days=5)).isoformat()
current = (date.today() + timedelta(days=400)).isoformat()
# One overdue tool, well past the first page of a small page size.
for index in range(6):
created = client.post('/api/measuringtools', headers=auth_headers, json={
'assetnumber': 'MT-PAGE-%02d' % index,
'name': 'Paged tool %d' % index,
'statusid': _status_id(client),
'nextcalibrationdate': overdue if index == 5 else current,
})
assert created.status_code == 201, created.get_json()
# Scoped with search as well, so earlier tests in this module cannot
# change the expected set - the point is paging over matches, not how many
# overdue tools the whole fixture happens to hold.
response = client.get(
'/api/measuringtools?calibrationstatus=overdue&search=MT-PAGE&perpage=2',
headers=auth_headers)
assert response.status_code == 200, response.get_json()
body = response.get_json()
rows = body['data']
pagination = body['meta']['pagination']
assert [row['assetnumber'] for row in rows] == ['MT-PAGE-05'], \
'the overdue tool must be on page 1 of a filtered list'
assert pagination['total'] == 1, \
'the total counts the MATCHES, not every tool'
assert pagination['totalpages'] == 1
def test_an_unfiltered_list_still_pages_normally(client, auth_headers):
for index in range(3):
client.post('/api/measuringtools', headers=auth_headers, json={
'assetnumber': 'MT-PLAIN-%02d' % index,
'name': 'Plain tool %d' % index,
'statusid': _status_id(client),
})
response = client.get('/api/measuringtools?perpage=2', headers=auth_headers)
assert response.status_code == 200
pagination = response.get_json()['meta']['pagination']
assert pagination['perpage'] == 2
assert pagination['total'] >= 3