mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-18 04:49:13 +00:00
web_interface/blueprints/api_v3.py held 111 routes, 56 helpers and 181 functions in one module -- 9% of the core by line count and three times the next largest file. It becomes a package of nine route modules grouped by path segment, plus __init__.py for the shared imports, constants, Blueprint and helpers. Every route module decorates the SAME api_v3 Blueprint object, so endpoint names stay api_v3.<function>, the URL map is unchanged and app.py is untouched. Verified: 111 routes before, 111 after, byte-identical rules, endpoints and methods, and every endpoint still on the one blueprint. plugins 3,867 config 1,178 starlark 692 system 619 fonts 452 misc 398 wifi 361 display 326 backup 212 __init__ 1,787 (imports, constants, Blueprint, 56 helpers) Two things the URL-map check could not catch, both found by running the suite: 1. PROJECT_ROOT = Path(__file__).parent.parent.parent. Moving the code one directory deeper made that resolve to web_interface/ instead of the project root. Nothing failed at import; it surfaced as ~110 tests failing with 404s and "installation script not found", because every path built from it was one level too shallow. Now parents[3], and test_api_v3_url_map.py asserts PROJECT_ROOT/run.py exists so the next move cannot repeat it. 2. Module-attribute patching. Tests do monkeypatch.setattr(api_v3_module, "_BACKUP_EXPORT_DIR", ...) and a route module that binds such a name by value never sees the patch. The shared code therefore stays in __init__.py rather than moving to a _common submodule -- it has to live on the module the tests patch -- and the eleven names tests patch are read back through the package (_pkg.X) instead of bound by value. Those eleven were found by AST-scanning every setattr in the test tree, not by guessing; "time" is among them, used to drive a fake clock through the second-resolution credential-backup filenames. Test changes are confined to what genuinely moved: patch targets that now name the owning route module, imports of helpers, and six tests that scan the api_v3 source as a file and now read the package directory. Full suite: 4,278 passed, 68 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
137 lines
5.3 KiB
Python
137 lines
5.3 KiB
Python
"""The /api/v3 URL map is a contract, and the split must not have moved it.
|
|
|
|
api_v3.py was one 10,469-line module and is now a package. Every route module
|
|
in it decorates the *same* Blueprint object, so this refactor was supposed to
|
|
be invisible from outside: same URLs, same endpoint names, same methods.
|
|
|
|
"Supposed to be" is the problem. A route function silently dropped during a
|
|
move -- a module that never gets imported, a decorator left behind -- costs
|
|
nothing at import time and fails only when someone hits the URL. So the map is
|
|
pinned here.
|
|
|
|
The snapshot is intentionally the *whole* map rather than a count. A count
|
|
passes when one route is deleted and another added, which is exactly the shape
|
|
a careless move produces.
|
|
|
|
If you are adding a route, this test is meant to fail: add the entry to
|
|
EXPECTED. If you are moving one between modules, it is meant to pass unchanged
|
|
-- endpoint names are `api_v3.<function>` regardless of which module the
|
|
function lives in, and that is the property that makes the package safe.
|
|
"""
|
|
import json
|
|
import os
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
from web_interface.blueprints.api_v3 import api_v3
|
|
|
|
SNAPSHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"fixtures", "api_v3_url_map.json")
|
|
|
|
|
|
def _current_map():
|
|
app = Flask(__name__)
|
|
app.register_blueprint(api_v3, url_prefix="/api/v3")
|
|
return sorted(
|
|
[r.rule, r.endpoint, sorted(r.methods)]
|
|
for r in app.url_map.iter_rules()
|
|
if r.endpoint != "static"
|
|
)
|
|
|
|
|
|
def test_the_url_map_matches_the_snapshot():
|
|
current = _current_map()
|
|
with open(SNAPSHOT, encoding="utf-8") as fh:
|
|
expected = json.load(fh)
|
|
|
|
cur = {(r, e) for r, e, _ in current}
|
|
exp = {(r, e) for r, e, _ in expected}
|
|
|
|
lost = sorted(exp - cur)
|
|
added = sorted(cur - exp)
|
|
assert not lost, (
|
|
f"{len(lost)} route(s) disappeared from /api/v3: {lost[:5]}. "
|
|
"A route lost in a module move costs nothing at import time and fails "
|
|
"only when someone hits the URL.")
|
|
assert not added, (
|
|
f"{len(added)} new route(s): {added[:5]}. If that is intended, "
|
|
f"regenerate {os.path.relpath(SNAPSHOT)}.")
|
|
|
|
# Methods too: a route that quietly loses POST is still a broken route.
|
|
cur_methods = {(r, e): m for r, e, m in current}
|
|
for rule, endpoint, methods in expected:
|
|
assert cur_methods[(rule, endpoint)] == methods, (
|
|
f"{rule} ({endpoint}) methods changed: "
|
|
f"{methods} -> {cur_methods[(rule, endpoint)]}")
|
|
|
|
|
|
def test_every_endpoint_is_on_the_one_blueprint():
|
|
"""The package must not fragment into several blueprints.
|
|
|
|
Splitting into per-domain *blueprints* would rename every endpoint from
|
|
`api_v3.foo` to `api_v3_plugins.foo` and break any url_for() that names
|
|
one. Keeping a single Blueprint object across the modules is what makes
|
|
the split a pure code move, so assert it rather than trusting it.
|
|
"""
|
|
for rule, endpoint, _ in _current_map():
|
|
assert endpoint.startswith("api_v3."), (
|
|
f"{rule} is registered as {endpoint}, not on the api_v3 blueprint")
|
|
|
|
|
|
def test_the_snapshot_is_not_empty():
|
|
"""Guards the failure mode this file exists to prevent.
|
|
|
|
An empty or truncated snapshot would make every assertion above pass
|
|
vacuously -- the same trap as a route map that imports no modules.
|
|
"""
|
|
with open(SNAPSHOT, encoding="utf-8") as fh:
|
|
expected = json.load(fh)
|
|
assert len(expected) > 100, (
|
|
f"snapshot has only {len(expected)} routes; it should have the whole "
|
|
"/api/v3 surface")
|
|
|
|
|
|
@pytest.mark.parametrize("module", [
|
|
"backup", "config", "display", "fonts", "misc",
|
|
"plugins", "starlark", "system", "wifi",
|
|
])
|
|
def test_every_route_module_contributes(module):
|
|
"""Each module must actually register something.
|
|
|
|
A module that fails to import, or that is left out of __init__, takes its
|
|
routes with it silently -- the package still imports and the app still
|
|
starts.
|
|
"""
|
|
import importlib
|
|
mod = importlib.import_module(f"web_interface.blueprints.api_v3.{module}")
|
|
routes = [n for n in dir(mod)
|
|
if callable(getattr(mod, n, None))
|
|
and getattr(getattr(mod, n), "__module__", "") == mod.__name__]
|
|
assert routes, f"{module}.py defines no view functions"
|
|
|
|
|
|
def test_project_root_points_at_the_project():
|
|
"""PROJECT_ROOT is derived from __file__, so moving the file breaks it.
|
|
|
|
The split moved this code from web_interface/blueprints/api_v3.py to
|
|
web_interface/blueprints/api_v3/_common.py -- one directory deeper -- and
|
|
`Path(__file__).parent.parent.parent` quietly began resolving to
|
|
web_interface/ instead of the project root. Nothing failed at import. It
|
|
surfaced as routes returning 404 and "installation script not found",
|
|
because every path built from it pointed one level too shallow.
|
|
|
|
A URL-map check cannot catch that: the routes were all registered, they
|
|
just could not find anything.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
from web_interface.blueprints.api_v3 import PROJECT_ROOT
|
|
|
|
# The project root is the directory holding run.py and web_interface/.
|
|
assert (PROJECT_ROOT / "run.py").is_file(), (
|
|
f"PROJECT_ROOT is {PROJECT_ROOT}, which has no run.py; it is not the "
|
|
"project root")
|
|
assert (PROJECT_ROOT / "web_interface").is_dir()
|
|
assert PROJECT_ROOT == Path(__file__).resolve().parents[1]
|