File size: 6,686 Bytes
c682851
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""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}"
    )