"""Documentation-drift guards for the README API table and `.env.example`. The Definition of Done requires that a new endpoint reaches the README and a new setting reaches `.env.example`. Nothing enforced that, so both drifted: the README advertised eight endpoints while the application served twenty, and five configurable settings were absent from the template a new contributor copies. Prose cannot be type-checked, but these two facts can. Both checks read the committed documents as text and compare them against the application's own introspection — the OpenAPI schema and `Settings.model_fields` — so they run without a database and fail on the commit that introduces the drift rather than during a later audit. """ from __future__ import annotations import os import re from pathlib import Path from typing import Any _ROOT = Path(__file__).resolve().parents[2] _README = _ROOT / "README.md" _ENV_EXAMPLE = _ROOT / ".env.example" # Settings that configure nothing a deployment operator should set. `app_name`, # `version`, and `api_v1_prefix` identify the build itself: overriding them from # the environment changes what the API calls itself without changing behaviour, # so they are deliberately absent from the template. _INTERNAL_SETTINGS = frozenset({"APP_NAME", "VERSION", "API_V1_PREFIX"}) def _openapi_paths() -> set[tuple[str, str]]: """Return every `(METHOD, path)` pair the application actually serves. Read from the OpenAPI schema rather than `app.routes`, because Starlette nests routes added via `include_router` inside wrapper objects that carry no `methods` attribute of their own. """ # `Settings` has required fields with no defaults; the session fixture in # conftest populates them, and these guard direct single-file invocation. os.environ.setdefault( "DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5433/talentlens_test" ) os.environ.setdefault("JWT_SECRET", "test-secret-not-a-real-key") os.environ.setdefault("STORAGE_BACKEND", "memory") os.environ.setdefault("ENVIRONMENT", "test") from app.main import create_app spec: dict[str, Any] = create_app().openapi() return { (method.upper(), path) for path, operations in spec["paths"].items() for method in operations } def _readme_documented_routes() -> set[tuple[str, str]]: """Return every `(METHOD, path)` pair the README's API table lists. Matches Markdown table rows of the form `| `GET` | `/api/v1/x` | ... |`, tolerating the backticks and surrounding whitespace the table uses. """ row = re.compile( r"^\|\s*`(GET|POST|PUT|PATCH|DELETE)`\s*\|\s*`([^`]+)`\s*\|", re.MULTILINE, ) return { (match.group(1).upper(), match.group(2).strip()) for match in row.finditer(_README.read_text(encoding="utf-8")) } def _env_example_keys() -> set[str]: """Return every environment variable name assigned in `.env.example`.""" assignment = re.compile(r"^\s*([A-Z][A-Z0-9_]*)\s*=", re.MULTILINE) return set(assignment.findall(_ENV_EXAMPLE.read_text(encoding="utf-8"))) def _settings_keys() -> set[str]: """Return the environment variable name of every `Settings` field.""" from app.config import Settings return {name.upper() for name in Settings.model_fields} def test_the_readme_api_table_is_parsed_at_all() -> None: """Anti-vacuous guard: an unparsed table makes the coverage check trivially pass.""" documented = _readme_documented_routes() assert documented, ( f"parsed zero endpoints out of {_README.name} — the API table format changed " "and the drift checks below are no longer verifying anything" ) def test_the_env_example_is_parsed_at_all() -> None: """Anti-vacuous guard: an unparsed template makes the coverage check trivially pass.""" assert _env_example_keys(), f"parsed zero assignments out of {_ENV_EXAMPLE.name}" def test_readme_documents_every_registered_route() -> None: """An endpoint absent from the README is an endpoint no reviewer knows to exercise. The README is the only API map a reader gets before running the service, so an undocumented route is functionally invisible — including the rubric and search surfaces, which is exactly the drift this caught. """ served = _openapi_paths() documented = _readme_documented_routes() assert served, "the OpenAPI schema exposed no paths — the app failed to wire its routers" undocumented = sorted(f"{method} {path}" for method, path in served - documented) assert not undocumented, ( f"routes served by the application but missing from the {_README.name} API table: " f"{undocumented} — add them to the table before merging" ) def test_the_readme_api_table_lists_no_route_that_does_not_exist() -> None: """A table entry for a removed route sends a reader to a guaranteed 404. The inverse of the check above: documentation that overstates the surface is as misleading as documentation that understates it. """ served = _openapi_paths() documented = _readme_documented_routes() stale = sorted(f"{method} {path}" for method, path in documented - served) assert not stale, ( f"the {_README.name} API table lists routes the application does not serve: {stale}" ) def test_env_example_documents_every_configurable_setting() -> None: """A setting absent from the template is a setting nobody knows they can set. `.env.example` is what a new contributor copies to `.env`. A field missing from it is discoverable only by reading `config.py`, which defeats the point of shipping a template. """ configurable = _settings_keys() - _INTERNAL_SETTINGS documented = _env_example_keys() assert configurable, "Settings declared no fields — config.py failed to import" missing = sorted(configurable - documented) assert not missing, ( f"settings configurable via the environment but absent from {_ENV_EXAMPLE.name}: " f"{missing} — document them before merging" ) def test_env_example_declares_no_variable_the_application_ignores() -> None: """A template key matching no setting is a silent no-op the operator trusts. Someone sets it, the value is read by nothing, and the misconfiguration surfaces as behaviour that contradicts the file they configured. """ documented = _env_example_keys() known = _settings_keys() unknown = sorted(documented - known) assert not unknown, ( f"{_ENV_EXAMPLE.name} declares variables no Settings field reads: {unknown}" )