The inventory is hand-maintained, and should stay that way - its value is the prose, and what an endpoint is FOR cannot be derived from the code. An audit of all 372 documented operations found zero phantom routes and zero wrong parameter names, so the maintenance is in good order. What hand-maintenance cannot do is notice a route somebody added. Twenty-two were undocumented: the entire backups plugin surface, every one of the dashboard card endpoints added with contract 0.19.0, the GE-Enforce publish preflight, the employee SSO resolver, the protocol update verbs, and the four /api/docs routes - so the spec did not describe how to fetch the spec. Coverage is now a test. It walks the live url_map and fails when a served route has no entry, which means adding an endpoint includes describing it, in the same commit, while the author still knows what it is for. The reverse direction is checked too: a documented route that no longer exists sends a reader to a 404. Writing that test found one more thing. The inventory writes multi-verb routes as "PUT|PATCH", and neither the parity check nor the generator split on the pipe - so those operations were absent from the published spec entirely, with nothing reporting it. The spec now carries all 394 operations the code serves, which is the first time the two numbers have matched. The generator's own docstring claimed the inventory could be regenerated. It cannot; nothing generates it. That sentence is why nobody noticed it was falling behind.
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""Every route the app serves is described in docs/api-inventory.json.
|
|
|
|
The inventory is hand-maintained, and deliberately so: its value is the prose -
|
|
what each endpoint is FOR, what it accepts, a worked example - and none of that
|
|
can be derived from the code. An audit of all 372 documented operations found
|
|
zero phantom routes and zero wrong parameter names, so the maintenance itself is
|
|
in good order.
|
|
|
|
What it cannot do is notice a route somebody added. Twenty-two were serving
|
|
traffic undocumented, including an entire plugin's surface and every one of the
|
|
dashboard card endpoints - each of them added after its surface was written.
|
|
|
|
So the prose stays hand-written and the COVERAGE is enforced here. Adding an
|
|
endpoint now means describing it, in the same commit, which is when the author
|
|
still knows what it is for.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from shopdb import create_app
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
INVENTORY = REPO / 'docs' / 'api-inventory.json'
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not INVENTORY.is_file(),
|
|
reason='docs/ is excluded from publication; nothing to check here')
|
|
|
|
# Flask's own machinery, not this product's API.
|
|
EXEMPT_ENDPOINTS = {'static'}
|
|
|
|
# Documented as a family rather than per-file: these serve the frontend bundle
|
|
# and its assets, not JSON.
|
|
EXEMPT_RULE_PREFIXES = ('/static', '/assets')
|
|
|
|
|
|
def normalise(rule):
|
|
"""Flask `<int:id>` and inventory `<id>` describe the same path.
|
|
|
|
Converters are an implementation detail of the route, and the inventory was
|
|
written without them. Comparing on the converter would report a difference
|
|
that is not one.
|
|
"""
|
|
return re.sub(r'<(?:[^:>]+:)?([^>]+)>', r'<\1>', rule)
|
|
|
|
|
|
def served_operations():
|
|
app = create_app('testing')
|
|
served = set()
|
|
for rule in app.url_map.iter_rules():
|
|
if rule.endpoint in EXEMPT_ENDPOINTS:
|
|
continue
|
|
if any(rule.rule.startswith(prefix) for prefix in EXEMPT_RULE_PREFIXES):
|
|
continue
|
|
if not rule.rule.startswith('/api'):
|
|
continue
|
|
for method in sorted((rule.methods or set()) - {'HEAD', 'OPTIONS'}):
|
|
served.add((method, normalise(rule.rule)))
|
|
return served
|
|
|
|
|
|
def documented_operations():
|
|
documented = set()
|
|
for surface in json.loads(INVENTORY.read_text()):
|
|
for entry in surface.get('endpoints', []):
|
|
path = normalise(entry.get('path') or '')
|
|
# The inventory writes multi-verb routes as 'PUT|PATCH'. Splitting
|
|
# on comma and slash alone left those undocumented in this check
|
|
# AND dropped from the generated spec entirely.
|
|
for method in re.split(r'[/,|]', entry.get('method') or 'GET'):
|
|
method = method.strip().upper()
|
|
if method:
|
|
documented.add((method, path))
|
|
return documented
|
|
|
|
|
|
def test_every_served_route_is_documented():
|
|
missing = sorted(served_operations() - documented_operations())
|
|
assert not missing, (
|
|
'%d route(s) serve traffic with no entry in docs/api-inventory.json. '
|
|
'Describe them there (purpose, auth, params, example) and regenerate '
|
|
'the spec with scripts/gen_openapi.py:\n %s'
|
|
% (len(missing), '\n '.join('%s %s' % row for row in missing)))
|
|
|
|
|
|
def test_nothing_documented_has_been_removed():
|
|
"""A documented route that no longer exists sends a reader to a 404."""
|
|
stale = sorted(documented_operations() - served_operations())
|
|
assert not stale, (
|
|
'%d documented route(s) are no longer served:\n %s'
|
|
% (len(stale), '\n '.join('%s %s' % row for row in stale)))
|