Answer API errors in the envelope, say when supplies are unread, badge each chain
Three more findings from the 2026-09-03 run.
Werkzeug answered 405, 415 and a malformed JSON body itself, in HTML. A client
written against the documented {status,data,meta} contract got a page of markup
with no error code, which is indistinguishable from the server having fallen
over. The 405 had a second edge: on a route the caller cannot otherwise see, a
method-not-allowed CONFIRMS the path exists, so a gated kiosk route meant to
read as absent announced itself by refusing the wrong verb. All three now
answer in the envelope, and only under /api/ - everything else is the SPA and
keeps the framework's behaviour, deep links included.
The Toner Report rendered "0 Printers Checked / 0 Low / 0 Critical" and "No
printers match the selected filter" while the API it had just called said
supply monitoring was unreachable. Zeros read as a clean bill of health for a
fleet nobody has looked at. The page now carries the reason the API gave and
suppresses the counts, because a number nobody measured is not a result. Same
defect shape as the supplies card fixed on 2026-08-29; `unavailable` was
already in the payload and the page ignored it.
Backup revisions badged "current" per (asset, kind). NTLARS settings describe a
MACHINE but are read off the controlling PC, so a machine with two reporting
PCs has two independent chains of the same kind - and only the higher revision
id was badged, so the second PC's newest revision showed as history and lost
its last-verified stamp. The machine looked like one source re-reporting rather
than two. sourcehostname is the chain; a revision without one forms its own,
which keeps the old behaviour for everything that has only ever had one source.
Nine tests. The envelope ones verified by planting: remove the three handlers
and three fail.
Scope: product
This commit is contained in:
66
tests/test_core/test_error_envelope.py
Normal file
66
tests/test_core/test_error_envelope.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""Every /api/ error answers in the documented envelope.
|
||||
|
||||
docs/API-REFERENCE.md says an error is
|
||||
{"status":"error","data":{"error":{"code","message"}},"meta":{...}}. Three
|
||||
statuses came straight from Werkzeug as HTML instead:
|
||||
|
||||
405 a wrong method - and on a route the caller cannot otherwise see, the
|
||||
405 also CONFIRMED the path exists, so a gated kiosk route meant to
|
||||
read as absent announced itself by refusing the wrong verb
|
||||
415 a body sent without Content-Type: application/json
|
||||
400 a body that is not parseable JSON
|
||||
|
||||
A client written against the contract gets a page of markup with no error code,
|
||||
which is indistinguishable from the server having fallen over.
|
||||
|
||||
Outside /api/ the framework's own behaviour is left alone: that is the SPA, and
|
||||
it has its own catch-all.
|
||||
"""
|
||||
|
||||
|
||||
def _is_envelope(response):
|
||||
body = response.get_json(silent=True)
|
||||
if not body:
|
||||
return False
|
||||
return (body.get('status') == 'error'
|
||||
and 'error' in body.get('data', {})
|
||||
and 'code' in body['data']['error']
|
||||
and 'message' in body['data']['error']
|
||||
and 'meta' in body)
|
||||
|
||||
|
||||
def test_a_wrong_method_answers_in_the_envelope(client):
|
||||
response = client.delete('/api/assets/types')
|
||||
assert response.status_code == 405
|
||||
assert response.content_type.startswith('application/json'), \
|
||||
response.content_type
|
||||
assert _is_envelope(response), response.get_data(as_text=True)[:200]
|
||||
|
||||
|
||||
def test_a_body_with_the_wrong_content_type_answers_in_the_envelope(client):
|
||||
response = client.post('/api/auth/login', data='not json',
|
||||
content_type='text/plain')
|
||||
assert response.status_code == 415
|
||||
assert _is_envelope(response), response.get_data(as_text=True)[:200]
|
||||
|
||||
|
||||
def test_a_malformed_json_body_answers_in_the_envelope(client):
|
||||
response = client.post('/api/auth/login', data='{"username": ',
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
assert _is_envelope(response), response.get_data(as_text=True)[:200]
|
||||
|
||||
|
||||
def test_a_missing_api_route_still_answers_in_the_envelope(client):
|
||||
response = client.get('/api/nothing-here-at-all')
|
||||
assert response.status_code == 404
|
||||
assert _is_envelope(response)
|
||||
|
||||
|
||||
def test_the_spa_catch_all_is_untouched(client):
|
||||
"""A browser asking for an app route must still get the app, not JSON.
|
||||
Enveloping everything would have broken deep links."""
|
||||
response = client.get('/pcs/1')
|
||||
assert response.status_code in (200, 404)
|
||||
if response.status_code == 200:
|
||||
assert not response.content_type.startswith('application/json')
|
||||
Reference in New Issue
Block a user