api: make the OpenAPI spec usable as documentation
ci / lint (push) Failing after 56s
ci / types (push) Has been skipped
ci / unit (push) Has been skipped
ci / integration (push) Has been skipped
ci / security (push) Has been skipped
ci / dockerfile (push) Has been skipped
ci / chart (push) Has been skipped
ci / image (api) (push) Has been skipped
ci / image (reconciler) (push) Has been skipped
ci / image (worker) (push) Has been skipped
ci / bump (push) Has been skipped

FastAPI already served /docs, /redoc and /openapi.json, and the ingress already
passed them through — the mechanism was there, the content was not. The schema
alone cannot tell a caller the three things they most need to know, and the
route docstrings explain implementation reasoning to a maintainer rather than
usage to a consumer.

Added to the spec itself, so it travels with the API rather than living in a
README the caller does not have:

  - An app description covering bearer auth, that every write is 202 + poll,
    the instance lifecycle, and the uniform {"code", "message"} error body.
  - Tag descriptions for `instances` and `ops`.
  - Field descriptions and worked examples on CreateInstanceRequest,
    InstanceResponse and ErrorBody, so /docs shows a valid payload instead of
    leaving callers to infer one.

The bearer scheme was already exposed via HTTPBearer, which is what makes the
Authorize button in /docs work; there is now a test asserting it stays, along
with the description, the tags and the request example. Docs that are not
tested rot silently, and this is the artifact other teams integrate against.

README documents the three URLs and how to generate a client from the spec.
This commit is contained in:
Nguyen Minh Phuc
2026-07-21 05:33:43 +00:00
parent 72296ace84
commit bb8b14ef03
4 changed files with 183 additions and 12 deletions
+37
View File
@@ -634,3 +634,40 @@ async def test_auth_disabled_accepts_an_unauthenticated_request(settings: Settin
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
resp = await c.get("/v1/instances")
assert resp.status_code == 200
# --------------------------------------------------------------------------- openapi
async def test_openapi_documents_the_api_for_a_caller_without_this_repo(
client: httpx.AsyncClient,
) -> None:
"""/openapi.json is the contract other teams integrate against, so it gets a test.
Pins the parts a generated schema does not give you for free and that silently rot: the
prose description, the tag docs, and the bearer scheme that makes the Authorize button
in /docs work. Without the security scheme a caller cannot try a single authenticated
route from the UI.
"""
resp = await client.get("/openapi.json")
assert resp.status_code == 200
spec = resp.json()
assert spec["info"]["title"] == "svcforge"
# The description carries the two things the schema cannot express: writes are async,
# and authorisation is a WHERE clause that 404s rather than 403s.
description = spec["info"]["description"]
assert "202" in description and "404" in description
assert {t["name"] for t in spec["tags"]} == {"instances", "ops"}
assert "HTTPBearer" in spec["components"]["securitySchemes"]
assert "/v1/instances" in spec["paths"]
# An example payload, so a caller can see a valid body rather than infer one.
assert spec["components"]["schemas"]["CreateInstanceRequest"]["examples"]
async def test_swagger_and_redoc_are_served(client: httpx.AsyncClient) -> None:
"""The human-facing docs. Both are on by default; a `docs_url=None` would drop them."""
for path in ("/docs", "/redoc"):
resp = await client.get(path)
assert resp.status_code == 200, path