"""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 `` and inventory `` 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)))