Describe the API this branch actually serves
docs/openapi.json is served live at /api/docs on the production box and documented none of the routes added since 2026-09-11. It is generated from docs/api-inventory.json, which is not tracked on this branch, so the generator had no input - and tests/test_api_inventory_parity.py skipped itself for the same reason. No route here had ever been checked against the published contract. The inventory is now generated from this branch's own url_map rather than carried over from elsewhere, with each route's auth derived from the decorators and gates on its view rather than guessed. A route whose gate cannot be classified is marked for review, not quietly called public. gen_openapi.py can refresh it from the live app, so this does not rot again, and human-authored prose survives a refresh. The parity test now RUNS instead of skipping. The thing that mattered more than the missing pages: nine operations were published with "security: []" - the machine-readable claim that no credentials are needed - while their handlers require one. Two of them serve the bootstrap installer and the agent package. A generated client, a firewall plan or anything else reading the spec would have been told those are anonymous. Required auth is now expressed as required, and a token-or-network gate is expressed as optional auth with the network half named in the description, which is what it actually is. A test re-derives auth from the live app and fails when the committed inventory disagrees, because the parity check only ever compared route lists: a wrong auth string passed everything. test_openapi_spec.py asserted that optional auth was the majority of the surface, a premise the guest-role and permission model has made false. It now holds the property that matters - an optional-auth route is expressed as optional - rather than a count that was true once. gen_openapi.py also loads .env before building the app, like every other script that builds one, or a plain python run falls back to the hardcoded localhost URL.
This commit is contained in:
@@ -72,15 +72,83 @@ def test_write_verbs_can_carry_a_body(spec):
|
||||
'from this spec cannot send one:\n ' + '\n '.join(offenders[:15]))
|
||||
|
||||
|
||||
def documented_auth(operation):
|
||||
"""The auth string the generator wrote into this operation's description.
|
||||
|
||||
The spec is the only thing this module reads, and the description is where
|
||||
the human-readable auth lands ('**Auth:** jwt-optional (...)'). Comparing
|
||||
the published `security` against it is how a lie gets caught: the two come
|
||||
from the same inventory entry and must agree.
|
||||
"""
|
||||
match = re.search(r'\*\*Auth:\*\*\s*(.+)', operation.get('description') or '')
|
||||
return match.group(1).strip().lower() if match else ''
|
||||
|
||||
|
||||
def test_optional_auth_is_expressed_as_optional(spec):
|
||||
"""An endpoint that works logged out must not be published as requiring a
|
||||
token - that is a lie to every reader, and it is the majority case here."""
|
||||
optional = [operation for _, _, operation in operations(spec)
|
||||
if any(item == {} for item in operation.get('security') or [])]
|
||||
assert len(optional) > 50, (
|
||||
'The inventory documents over a hundred jwt-optional endpoints; the '
|
||||
'spec should express them with an empty security requirement '
|
||||
'alongside the scheme, not as bearer-required.')
|
||||
"""A route whose auth is optional is published as optional, either way round.
|
||||
|
||||
WHY THE COUNT IS GONE. This asserted `len(optional) > 50`, on the premise
|
||||
that optional auth was the majority case here - it said so in as many
|
||||
words. It is not any more. The guest role and the permission model moved
|
||||
the read surface to `permission:*`, which publishes as bearer-required and
|
||||
is correct, and the optional count fell to 14. The assertion went red
|
||||
without a single route being wrong.
|
||||
|
||||
A count was never the property anyone cares about. It goes red when the
|
||||
codebase legitimately changes shape, and it stays green while an individual
|
||||
optional route is published as bearer-required, which is the actual defect.
|
||||
Lowering the number to 14 would only buy time until the next legitimate
|
||||
move. So the premise is dropped and the per-route property is checked
|
||||
instead, whatever the total: an operation whose auth says optional carries
|
||||
BOTH the empty requirement and a scheme, and an operation that publishes
|
||||
the empty requirement says optional in its auth.
|
||||
"""
|
||||
offenders = []
|
||||
for path, verb, operation in operations(spec):
|
||||
auth = documented_auth(operation)
|
||||
security = operation.get('security') or []
|
||||
openly = any(item == {} for item in security)
|
||||
scheme = any(item for item in security)
|
||||
if 'optional' in auth:
|
||||
if not openly:
|
||||
offenders.append(
|
||||
'%s %s: auth says optional, published as required (%r)'
|
||||
% (verb.upper(), path, security))
|
||||
elif not scheme:
|
||||
offenders.append(
|
||||
'%s %s: auth says optional, published with no scheme at '
|
||||
'all, so a caller is never told what the credential is'
|
||||
% (verb.upper(), path))
|
||||
elif openly:
|
||||
offenders.append(
|
||||
'%s %s: published as optional, but its auth says %r'
|
||||
% (verb.upper(), path, auth))
|
||||
assert not offenders, (
|
||||
'Optional auth means "works without a credential, does more with one", '
|
||||
'and OpenAPI writes it as the empty requirement ALONGSIDE the '
|
||||
'scheme:\n ' + '\n '.join(offenders[:15]))
|
||||
|
||||
|
||||
def test_an_operation_needing_no_credential_says_so_in_its_auth(spec):
|
||||
"""`security: []` is a machine-readable claim that no credential is needed.
|
||||
|
||||
Nine operations published it while their handler required one, including
|
||||
the two that hand a bootstrapping PC its agent package - `security: []` and
|
||||
a 404 for anyone who does not present the single-use token. The generator
|
||||
reached that by substring: the auth string started with the word 'none', in
|
||||
a sentence that went on to name the token. This is the gate on that class
|
||||
of defect from the spec side, where a reader or a generated client sees it.
|
||||
"""
|
||||
offenders = [
|
||||
'%s %s: %r' % (verb.upper(), path, documented_auth(operation))
|
||||
for path, verb, operation in operations(spec)
|
||||
if not operation.get('security')
|
||||
and 'none' not in documented_auth(operation)
|
||||
and 'public' not in documented_auth(operation)
|
||||
]
|
||||
assert not offenders, (
|
||||
'These operations publish "no credentials needed" while their auth '
|
||||
'names a credential:\n ' + '\n '.join(offenders[:15]))
|
||||
|
||||
|
||||
def test_an_endpoint_that_needs_no_token_cannot_fail_with_401(spec):
|
||||
|
||||
Reference in New Issue
Block a user