Files
shopdb-flask/tests/test_openapi_examples.py
cproudlock 534f179ee6 Make main the lean product a site receives
main was 16 days and 259 commits behind the line the product is actually built
on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It
was neither current nor curated - just stale. This brings it forward and, at the
same time, makes it mean something: main is what a receiving site installs, and
nothing else.

WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides,
knowledgebase, machines, network, notifications, printers, slides, tools,
warranty - plus applications, which is manifest-less core and always ships. The
only manifest dependency inside that set is endpoints on computers, and both are
in it, so the closure is self-contained.

WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb
stay on the site line. So do the Windows installer tree, the VM rehearsal
helpers, the agent tooling config, one site's data loader, the internal audit
and findings records, and the planner spreadsheet. 1,135 tracked files against
1,438.

A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped:

deploy/site-profile-universal.json still named all seventeen plugins. It is the
file docs/DEPLOY.md tells a site to provision from, so a fresh install would
have failed at `flask plugin apply-profile` with PluginError - the first thing a
receiving site does, broken by the branch meant to serve them. It now names the
twelve that exist, and its comment says the two must agree.

agent/ was never refreshed. Whoever populated this branch copied shopdb/,
plugins/, frontend/ and tests/ from the site line and left the agent tree at its
September state: 22 C# and PowerShell sources missing, including AgentCredentials,
EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the
site line had deleted still present. The old tree is internally consistent, so it
compiles, and nothing in CI builds it - every gate was green over a server that
speaks enrollment and per-PC credentials to a bundled agent source that cannot.
Resynced whole.

DECISIONS THIS ENCODES, rather than defers:

- The plugin-authoring lab builds printedparts, whose manifest depends on
  employees, so it could not stay. The lab remains on the site line and main
  ships no build-along tutorial. That is a real loss for an adopting developer
  and is recorded here rather than discovered later.
- Recognition, Training and Recertification notifications are no longer seeded.
  Each splits one notification into a card per employee and renders a photo,
  which needs a plugin this branch does not bundle. Seeding them would offer an
  author a type that cannot resolve anybody. A site that installs employees gets
  them from that plugin's own seed.
- Windows/IIS is documented as the legacy path throughout, and the README leads
  with Docker, because that is what a receiving site gets.

The API description is regenerated for THIS branch - 347 paths and 483
operations, down from 415 and 573 - so the published contract describes what is
here rather than what the site line serves. docs/CONFIG.md lost the two
measuring-tool label keys the code no longer declares, which two lanes had
decided opposite ways.

Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run
rather than skip, and every index line count is derived from the file it names.
2026-09-19 11:24:40 -04:00

638 lines
27 KiB
Python

"""The two derived halves of the published spec: query parameters and examples.
docs/openapi.json is served at /api/docs on production AND published to the
wiki, and it is what the MCP server builds its tools from. Two things were
missing from every operation in it, and both are now derived rather than
written:
QUERY PARAMETERS. A Flask rule carries its path arguments and nothing else,
so 0 of 573 operations published a query parameter - including
a setup route where '?code=' is the only way to call it successfully.
scripts/gen_openapi.py now reads them out of each view's AST.
EXAMPLES. 0 of 573 carried a machine-readable example. They are harvested
from the test suite, which already sends real bodies and asserts real
responses.
Harvesting is where this module earns its place. A harvested body is whatever a
test happened to build, and tests are written with real-looking material -
passwords, tokens, badge numbers, hostnames, addresses. None of that matters in
a test file and all of it matters on a public page, so redaction runs before
anything reaches the spec. `redact` is tested here directly, and the generated
spec is swept for the same material, because a scrubber nobody checks is a
scrubber that stops working quietly.
The other property tested here is that NOTHING IS INVENTED. A query argument
whose name the generator cannot resolve to a literal is not published at all:
an argument that is not really there is worse than a missing one, because a
reader believes it, sends it and is silently ignored.
"""
import importlib.util
import json
import textwrap
from pathlib import Path
import pytest
import re
REPO = Path(__file__).resolve().parents[1]
SPEC = REPO / 'docs' / 'openapi.json'
def _generator():
"""scripts/gen_openapi.py, loaded by path.
It is a script, not a package module. Loading it runs its imports and
nothing else - it builds no app and reads no database until it is asked to.
"""
path = REPO / 'scripts' / 'gen_openapi.py'
spec = importlib.util.spec_from_file_location('gen_openapi_examples', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.fixture(scope='module')
def generator():
return _generator()
@pytest.fixture(scope='module')
def spec():
if not SPEC.is_file():
pytest.skip('no generated spec in this checkout')
return json.loads(SPEC.read_text())
def operations(spec):
for path, item in spec['paths'].items():
for verb, operation in item.items():
yield path, verb, operation
def examples(spec):
"""Every published example, as (where, value)."""
for path, verb, operation in operations(spec):
body = operation.get('requestBody') or {}
media = (body.get('content') or {}).get('application/json') or {}
if 'example' in media:
yield '%s %s request' % (verb.upper(), path), media['example']
success = ((operation.get('responses') or {}).get('200') or {})
media = (success.get('content') or {}).get('application/json') or {}
if 'example' in media:
yield '%s %s response' % (verb.upper(), path), media['example']
# ---------------------------------------------------------------------------
# redact
# ---------------------------------------------------------------------------
def test_a_credential_is_redacted_by_its_key(generator):
"""The key is the reliable signal, whatever the value happens to be today.
A fixture that leaves passwordhash empty must not teach the scrubber that
passwordhash is safe.
"""
scrubbed = generator.redact({
'password': 'hunter2',
'passwordhash': '',
'apikey': 'abc',
'refresh_token': 'abc',
'X-API-Key': 'abc',
'signature': 'abc',
})
assert set(scrubbed.values()) == {'<redacted:secret>'}
def test_a_person_and_a_machine_are_redacted_by_their_key(generator):
"""Not secrets, and not ours to publish either. ADR-015 for the machine
half: one plant's server name is meaningless to every other site reading
these pages."""
scrubbed = generator.redact({
'username': 'jsmith',
'badge': '204411',
'checkedoutby': 'J Smith',
'hostname': 'WJSF0123',
'fqdn': 'wjsf0123.example.net',
'ipaddress': '10.4.2.19',
'servicetag': 'H3K9QP2',
})
assert scrubbed['username'] == '<redacted:person>'
assert scrubbed['badge'] == '<redacted:person>'
assert scrubbed['checkedoutby'] == '<redacted:person>'
assert scrubbed['hostname'] == '<redacted:host>'
assert scrubbed['fqdn'] == '<redacted:host>'
assert scrubbed['ipaddress'] == '<redacted:host>'
assert scrubbed['servicetag'] == '<redacted:host>'
def test_a_credential_with_no_telling_key_is_redacted_by_its_value(generator):
"""The half the key rules cannot reach: material in a free-text field, or
in a list, where nothing names it."""
scrubbed = generator.redact({
'note': 'mail bob.smith@example.com when it lands',
'detail': 'bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaaa.bbbb',
'path': '\\\\fileserver\\deploy\\agent.msi',
'where': 'reported from 10.11.12.13',
'digest': 'd41d8cd98f00b204e9800998ecf8427e',
'bag': ['carol@example.com'],
})
assert scrubbed['note'] == '<redacted:email>'
assert scrubbed['detail'] == '<redacted:token>'
assert scrubbed['path'] == '<redacted:sharepath>'
assert scrubbed['where'] == '<redacted:address>'
assert scrubbed['digest'] == '<redacted:digest>'
assert scrubbed['bag'] == ['<redacted:email>']
def test_ordinary_material_survives(generator):
"""Over-redaction has a cost too: an example scrubbed to nothing shows a
reader nothing. What is safe stays."""
payload = {
'assetnumber': 'A-10024',
'model': 'Dell Latitude 5420',
'isactive': True,
'quantity': 12,
'status': 'In Service',
'url': 'http://localhost:5001/api/assets',
}
assert generator.redact(payload) == payload
def test_a_numeric_id_is_not_a_credential(generator):
"""'tokenid': 3 is the row number, not the token.
Redacting it cost every token route the one field a caller needs to address
the row. The exemption is numeric-only, so a string session id is still
scrubbed.
"""
scrubbed = generator.redact({'tokenid': 3, 'apikeyid': 7,
'sessionid': 'a-long-opaque-string-value'})
assert scrubbed['tokenid'] == 3
assert scrubbed['apikeyid'] == 7
assert scrubbed['sessionid'] == '<redacted:secret>'
def test_redaction_reaches_every_level(generator):
"""A token two objects down is as published as one at the top."""
scrubbed = generator.redact(
{'data': {'items': [{'inner': {'password': 'hunter2'}}]}})
assert scrubbed['data']['items'][0]['inner']['password'] == '<redacted:secret>'
def test_redaction_is_idempotent(generator):
"""A harvest runs again over a spec that already carries markers, and the
markers must not be re-scrubbed into something else."""
once = generator.redact({'password': 'hunter2', 'model': 'Dell 5420'})
assert generator.redact(once) == once
def test_a_long_list_is_trimmed_and_says_so(generator):
"""Three rows show the shape, which is the whole job of an example - but a
reader must not conclude the endpoint returns three."""
scrubbed = generator.redact({'data': list(range(50))})
assert scrubbed['data'][:3] == [0, 1, 2]
assert 'more' in scrubbed['data'][-1]
def test_a_cyclic_depth_is_bounded(generator):
"""Harvested bodies come from a running app, not from this file. A deeply
nested one must not take the generator down with it."""
deep = current = {}
for _ in range(50):
current['child'] = {}
current = current['child']
generator.redact(deep)
# ---------------------------------------------------------------------------
# The published spec, swept for the same material
# ---------------------------------------------------------------------------
# Patterns held HERE, deliberately not imported from the generator. A gate that
# checks the scrubber with the scrubber's own patterns can only ever assert that
# redact() ran: it was green while the spec published a live bootstrap token and
# a live kiosk setup code, because both hid in a URL whose key ('scripturl',
# 'url') names no secret and whose value the opaque rule excludes by design.
# These must keep catching that class even if someone edits the generator.
INDEPENDENT_URL_CREDENTIAL = re.compile(
r'(?i)[?&](t|token|code|key|apikey|api_key|secret|sig|signature|password|'
r'auth|access_token|refresh_token)=([^&\s"\'<]{8,})')
# Product token prefixes. If a new one is minted, add it here the same day.
INDEPENDENT_TOKEN_PREFIX = re.compile(r'\b(sdbb_|sdbe_|shopdb_pat_)[A-Za-z0-9_-]{8,}')
def _looks_high_entropy(text):
"""A long mixed-case, digit-bearing run with no word structure.
Deliberately crude: it is a backstop for a secret shape nobody has thought
of yet, not a classifier. Anything it flags is either a secret or an example
that would read better with a shorter value.
"""
for chunk in re.split(r'[\s"\'<>]+', text):
if len(chunk) < 28 or '.' in chunk or '/' in chunk:
continue
has = lambda pattern: re.search(pattern, chunk)
if has(r'[a-z]') and has(r'[A-Z]') and has(r'\d'):
return chunk
return None
def test_no_published_example_hides_a_credential_in_a_url(spec):
"""The class the scrubber's own patterns cannot see.
Two real credentials reached docs/openapi.json this way on 2026-09-18, in a
file served at /api/docs on production and copied to a published wiki.
"""
offenders = []
for where, example in examples(spec):
blob = json.dumps(example)
found = INDEPENDENT_URL_CREDENTIAL.search(blob)
if found and not found.group(2).startswith('<redacted:'):
offenders.append('%s: %s=%s' % (where, found.group(1), found.group(2)[:16]))
prefixed = INDEPENDENT_TOKEN_PREFIX.search(blob)
if prefixed:
offenders.append('%s: token prefix %s' % (where, prefixed.group(0)[:20]))
entropy = _looks_high_entropy(blob)
if entropy:
offenders.append('%s: high-entropy %r' % (where, entropy[:20]))
assert not offenders, (
'Published examples carry what looks like a credential:\n '
+ '\n '.join(sorted(set(offenders))))
def test_no_published_example_carries_a_secret_a_person_or_a_machine(spec, generator):
"""The gate on the whole harvest, from the side a reader sees.
Checked against the scrubber's own patterns rather than a second list, so
the gate and the scrubber cannot drift apart: this fails if redaction stops
being applied, if it is applied to a stale copy, or if a hand-edit puts
real material back.
"""
offenders = []
for where, example in examples(spec):
for key, value in _walk(example):
if not isinstance(value, str) or value.startswith('<redacted:'):
continue
name = (key or '').lower()
# Only strings reach here, and the numeric-id exemption in redact()
# is numeric-only, so no key is exempt on this side.
if name and generator.SECRET_KEY_PATTERN.search(name):
offenders.append('%s: key %r' % (where, key))
elif name and generator.PERSON_KEY_PATTERN.search(name):
offenders.append('%s: key %r' % (where, key))
elif name and generator.HOST_KEY_PATTERN.search(name):
offenders.append('%s: key %r' % (where, key))
elif generator._redacted_string(value):
offenders.append('%s: value under %r' % (where, key))
assert not offenders, (
'%d published example value(s) look like a secret, a person or a '
'machine. docs/openapi.json is served on production and published to '
'the wiki; regenerate it with scripts/gen_openapi.py:\n %s'
% (len(offenders), '\n '.join(sorted(set(offenders))[:15])))
def _walk(value, key=''):
"""(key, value) for every leaf in a harvested example."""
if isinstance(value, dict):
for itemkey, itemvalue in value.items():
for found in _walk(itemvalue, str(itemkey)):
yield found
elif isinstance(value, list):
for item in value:
for found in _walk(item, key):
yield found
else:
yield key, value
def test_a_published_example_is_bounded(spec, generator):
"""Everybody downloads this document and every generated client parses it.
One 400-row list response costs all of them."""
offenders = [where for where, example in examples(spec)
if len(json.dumps(example)) > generator.EXAMPLE_MAX_CHARS]
assert not offenders, '\n '.join(offenders[:15])
def test_a_response_example_never_contradicts_the_schema_beside_it(spec):
"""An example and a `$ref` that disagree are worse than no example at all.
Nineteen routes here do not return the standard envelope and every one was
documented as if it did. A harvested transcript is evidence about which,
so the generator drops the envelope `$ref` on any operation whose real
response is a different shape. This is the gate on that: an operation
published with the envelope `$ref` and an example that is not an envelope
is the exact contradiction the harvest exists to remove.
"""
offenders = []
for path, verb, operation in operations(spec):
media = (((operation.get('responses') or {}).get('200') or {})
.get('content') or {}).get('application/json') or {}
if 'example' not in media:
continue
claims = 'SuccessEnvelope' in json.dumps(media.get('schema') or {})
isenvelope = (isinstance(media['example'], dict)
and media['example'].get('status') == 'success')
if claims != isenvelope:
offenders.append('%s %s: schema says envelope=%s, example is=%s'
% (verb.upper(), path, claims, isenvelope))
assert not offenders, '\n '.join(offenders[:15])
# ---------------------------------------------------------------------------
# Derived query parameters: what is emitted, and what is deliberately not
# ---------------------------------------------------------------------------
def _derive(tmp_path, source):
"""Run the generator's query sweep over one written-out module."""
module = tmp_path / 'sample_views.py'
module.write_text(textwrap.dedent(source))
return _generator()._query_index(str(module))
def test_a_literal_query_read_is_emitted(tmp_path):
index = _derive(tmp_path, '''
def view():
search = request.args.get('search')
active = request.args.get('active', 'true')
days = request.args.get('days', 30, type=int)
tags = request.args.getlist('tag')
page = request.args['page']
''')
found = {item['name']: item for item in index['view']}
assert sorted(found) == ['active', 'days', 'page', 'search', 'tag']
assert found['search'] == {'name': 'search', 'type': 'string', 'required': False}
assert found['active']['default'] == 'true'
assert found['days'] == {'name': 'days', 'type': 'integer',
'required': False, 'default': 30}
assert found['tag']['type'] == 'array'
# request.args['x'] raises without the argument. It is the one read in the
# language that proves the caller must send it.
assert found['page']['required'] is True
def test_a_name_that_is_not_a_literal_is_not_published(tmp_path):
"""The honesty rule, and the reason this is safe to derive at all.
A key built at runtime cannot be resolved, and a parameter that is not
really there is worse than a missing one: a reader believes it, sends it,
and is silently ignored. A module-level constant DOES resolve, because it
is a literal with a name.
"""
index = _derive(tmp_path, '''
TOKEN_ARG = 'bootstraptoken'
def view(field):
built = request.args.get('filter_' + field)
indirect = request.args.get(field)
formatted = request.args.get(f'{field}id')
named = request.args.get(TOKEN_ARG)
''')
assert [item['name'] for item in index['view']] == ['bootstraptoken']
def test_the_repos_own_argument_helpers_are_read(tmp_path):
"""get_pagination_params and intarg/intargs read arguments on a handler's
behalf, so a sweep that only looks for request.args misses the paging
contract of every list route in the product."""
index = _derive(tmp_path, '''
def view():
page, perpage = get_pagination_params(request)
typeid = intarg('typeid')
since = intarg('days', 30)
rest = intargs('locationid', 'businessunitid')
''')
found = {item['name']: item for item in index['view']}
assert sorted(found) == ['businessunitid', 'days', 'locationid', 'page',
'per_page', 'perpage', 'typeid']
assert found['page'] == {'name': 'page', 'type': 'integer',
'required': False, 'default': 1}
assert found['days']['default'] == 30
assert found['typeid']['type'] == 'integer'
def test_a_view_that_reads_nothing_publishes_nothing(tmp_path):
index = _derive(tmp_path, '''
def view(asset_id):
return success_response({'assetid': asset_id})
''')
assert index['view'] == []
# ---------------------------------------------------------------------------
# The generator without any of this
# ---------------------------------------------------------------------------
def test_a_spec_generates_with_no_harvest_and_no_query_parameters(generator):
"""Both halves are OPTIONAL. A published checkout has no tests to harvest
from, and an inventory written before this existed has no `queryparams`. A
generator that needs either is a generator that cannot be run where it
matters."""
built = generator.build([{
'surface': 'core-assets',
'endpoints': [{
'method': 'GET|POST',
'path': '/api/assets',
'auth': 'permission:assets.view',
'params': 'not documented',
'purpose': 'List assets.',
'example': 'curl http://localhost:5001/api/assets',
}],
}])
operation = built['paths']['/api/assets']['get']
assert 'parameters' not in operation
media = built['paths']['/api/assets']['post']['requestBody']['content']
assert 'example' not in media['application/json']
success = operation['responses']['200']['content']['application/json']
assert 'example' not in success
assert success['schema'] == {'$ref': '#/components/schemas/SuccessEnvelope'}
def test_a_query_parameter_survives_into_the_spec(generator):
"""The other direction: an inventory entry that has them publishes them."""
built = generator.build([{
'surface': 'core-setup',
'endpoints': [{
'method': 'GET',
'path': '/api/setup/claim',
'auth': 'setup-code (single-use setup code in ?code=)',
'params': 'not documented',
'purpose': 'Trade a setup code for this browser session.',
'example': 'curl http://localhost:5001/api/setup/claim',
'queryparams': [{'name': 'code', 'type': 'string', 'required': False}],
'responseexample': {'status': 'success', 'data': None},
}],
}])
operation = built['paths']['/api/setup/claim']['get']
assert operation['parameters'] == [{
'name': 'code', 'in': 'query', 'required': False,
'schema': {'type': 'string'},
}]
success = operation['responses']['200']['content']['application/json']
assert success['example'] == {'status': 'success', 'data': None}
# A query filter that matches nothing is an empty list and a 200. Only a
# path parameter can be a record that is not there.
assert '404' not in operation['responses']
def test_a_per_request_value_is_pinned_but_keeps_its_shape(generator):
"""A transcript carries the instant it was taken and the id of the request
that produced it, and both differ on every run.
Published verbatim, re-harvesting rewrote 313 of 320 examples with no change
of meaning - an unreviewable diff over a committed, published file. An
example shows a reader the FORMAT of a field, not the moment one call
happened, so the format is kept exactly and the value is pinned.
"""
scrubbed = generator.redact({
'meta': {'timestamp': '2026-09-18T18:30:48.498252Z',
'requestid': '859aa86b'},
'createddate': '2026-09-18 08:15:00',
'expiresat': '2026-12-31T23:59:59+00:00',
'installedon': '2026-09-18',
})
# Microseconds, the space separator and the offset all survive: those are
# three different contracts and a reader is reading the example for which.
assert scrubbed['meta']['timestamp'] == '2026-01-01T00:00:00.000000Z'
assert scrubbed['meta']['requestid'] == '00000000'
assert scrubbed['createddate'] == '2026-01-01 00:00:00'
assert scrubbed['expiresat'] == '2026-01-01T00:00:00+00:00'
assert scrubbed['installedon'] == '2026-01-01'
def test_pinning_does_not_reach_ordinary_text(generator):
"""Only a value that IS a timestamp. A model number that happens to contain
digits is not one."""
payload = {'model': '5420-2026', 'note': 'due 2026-09-18 at the latest',
'version': '1.2.3'}
assert generator.redact(payload) == payload
def test_a_local_path_is_not_published(generator):
"""A path off the machine the harvest ran on says nothing to a reader and
is different on every run. A developer's home directory is also the exact
material tests/test_docs_publishable.py exists to keep off a public page."""
scrubbed = generator.redact({
'shareroot': '/tmp/pytest-of-someone/pytest-240/test_roundtrip0',
'checkout': '/home/someone/projects/shopdb-flask',
'relative': 'scripts/site_imports',
'route': '/api/assets',
})
assert scrubbed['shareroot'] == '<redacted:path>'
assert scrubbed['checkout'] == '<redacted:path>'
# A path that names no machine is the useful half, and it stays.
assert scrubbed['relative'] == 'scripts/site_imports'
assert scrubbed['route'] == '/api/assets'
def test_a_harvest_adds_and_never_clears(generator, tmp_path):
"""A targeted harvest has to be targeted.
Clearing on absence meant `--harvest tests/test_core` stripped the examples
off every route those tests do not touch, which is most of them - the
narrow command would have done the most damage. It also made the committed
file churn for nothing: run under xdist against a shared instance
directory, two full runs of the same tree reached 311 and 327 operations.
"""
inventory = tmp_path / 'api-inventory.json'
inventory.write_text(json.dumps([{
'surface': 'core-assets',
'endpoints': [
{'method': 'GET', 'path': '/api/assets',
'responseexample': {'status': 'success', 'data': ['kept']}},
{'method': 'POST', 'path': '/api/assets'},
],
}]))
original = generator.INVENTORY
generator.INVENTORY = str(inventory)
try:
generator.fold_examples({
('POST', '/api/assets'): {
'request': [{'assetnumber': 'A-1'}],
'response': [{'status': 'success', 'data': {'assetid': 1}}],
},
})
finally:
generator.INVENTORY = original
entries = json.loads(inventory.read_text())[0]['endpoints']
# Untouched by this harvest, and still carrying what it had.
assert entries[0]['responseexample'] == {'status': 'success', 'data': ['kept']}
assert entries[1]['requestexample'] == {'assetnumber': 'A-1'}
assert entries[1]['responseexample'] == {'status': 'success',
'data': {'assetid': 1}}
def test_picking_one_example_out_of_many_is_deterministic(generator):
"""The suite runs in parallel, so the order samples arrive in is not stable.
Largest-that-fits with a text tiebreak means two runs over the same samples
produce the same spec, which is what makes a regenerated docs/openapi.json
reviewable as a diff instead of a rewrite.
"""
samples = [
{'status': 'success', 'data': {'assetid': 1}},
{'status': 'success', 'data': {'assetid': 2, 'assetnumber': 'A-2'}},
{'status': 'success', 'data': None},
]
picked = generator._best_example(samples)
assert picked == generator._best_example(list(reversed(samples)))
assert picked['data']['assetnumber'] == 'A-2'
def test_an_example_that_cannot_be_made_to_fit_is_dropped(generator):
"""Silence beats a truncated JSON document: half an object is not an
example of anything, and it is not what the route returned."""
# A wide object, not a long string: strings and lists are already capped
# inside redact, so an object with hundreds of keys is the shape that can
# still come out over the limit.
huge = {'status': 'success',
'data': {'field%03d' % index: 'value' for index in range(500)}}
assert generator._best_example([huge]) is None
# And a smaller sample of the same operation still wins, rather than the
# whole operation losing its example to one oversized transcript.
small = {'status': 'success', 'data': {'assetid': 1}}
assert generator._best_example([huge, small]) == small
def test_a_delete_that_reads_a_body_says_so(generator):
"""DELETE /api/applications/{id} refuses without {"confirm": "OK"}.
The generator declared a body for POST, PUT and PATCH only, so that route
published a call that cannot succeed and a client built from the spec fails
on the day.
Evidence is the HANDLER reading a body (`readsbody`, derived from its AST),
not a harvested transcript. A transcript proves the test CLIENT sent
something, and the suite sends {} to routes that ignore it: on 2026-09-18
transcript-only evidence published "this route reads a JSON body" on DELETE
/api/endpoints/agentkey, whose handler never calls get_json. Both halves are
pinned below.
"""
entries = [
{'method': 'DELETE', 'path': '/api/applications/<int:app_id>',
'auth': 'permission:applications.delete', 'purpose': 'Delete it.',
'readsbody': True, 'requestexample': {'confirm': 'OK'}},
{'method': 'DELETE', 'path': '/api/assets/<int:assetid>',
'auth': 'permission:assets.delete', 'purpose': 'Delete it.'},
# A transcript with no handler evidence: the suite sent a body, the
# route ignores it, so the contract must stay silent.
{'method': 'DELETE', 'path': '/api/endpoints/agentkey',
'auth': 'permission:endpoints.manage', 'purpose': 'Revoke it.',
'requestexample': {}},
]
built = generator.build([{'surface': 'core-applications', 'endpoints': entries}])
withbody = built['paths']['/api/applications/{app_id}']['delete']
media = withbody['requestBody']['content']['application/json']
assert withbody['requestBody']['required'] is False
assert media['example'] == {'confirm': 'OK'}
# Transcript without handler evidence declares nothing.
ignored = built['paths']['/api/endpoints/agentkey']['delete']
assert 'requestBody' not in ignored
assert 'requestBody' not in built['paths']['/api/assets/{assetid}']['delete']