Phase 33 (story: .agent/user_stories/cache-busting.md). - app/core/caching.py: asset_version() — git short SHA (a commit is a deploy), stable content-hash fallback for non-git checkouts, "dev" for a missing static dir; computed once per process. CachingMiddleware — the five HTML pages revalidate (no-cache) with ?v=<token> asset refs rewritten in flight; /assets/* is public, max-age=31536000, immutable; everything else (all /api/*, the SSE chat stream in particular) passes through byte-identical. - tests/e2e/test_cache_busting.py: fresh-Chromium wire assertions — document no-cache, versioned CSS/JS request URLs sharing one token, immutable asset headers, /api/health baseline headers, SSE chat to done (mock LLM). - README 'Caching / deploys' section + story file. Also fixed two prod-image defects surfaced by this phase's podman smoke (the full app would not boot): - Containerfile: ship the scripts/ package — app/api/sync.py (phase 32) imports scripts.git_sync / scripts.import_docs at module level, so the container crashed on boot (ModuleNotFoundError: No module named 'scripts'). - compose.yaml: pass BOR_ADMIN_PASSWORD / BOR_SESSION_SECRET through to the app service (:- defaults keep 'podman compose up -d db' working; the app's own fail-loud gate still names missing admin auth). Smoke: podman compose --profile prod up -d on a fresh image + a fresh Chromium profile — /, /sources.html and /login.html all served Cache-Control: no-cache; all 8 asset requests versioned with one shared token (content-hash fallback inside the image — no .git there); /assets/* immutable for a year.
33 lines
1.3 KiB
Python
33 lines
1.3 KiB
Python
"""Unit tests: app factory edge cases (no live DB needed)."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.main as main_mod
|
|
|
|
|
|
def test_create_app_warns_and_serves_api_only_without_static_dir(
|
|
monkeypatch, tmp_path
|
|
) -> None:
|
|
"""If the frontend directory is missing, the API still boots (PLAN §7)."""
|
|
monkeypatch.setattr(
|
|
main_mod.settings, "static_dir", str(tmp_path / "definitely-missing")
|
|
)
|
|
app2 = main_mod.create_app()
|
|
client = TestClient(app2)
|
|
# /api still works…
|
|
assert client.get("/api/health").status_code == 200
|
|
# …but the static mount is absent (no index page).
|
|
assert client.get("/").status_code == 404
|
|
|
|
|
|
def test_create_app_wires_caching_middleware() -> None:
|
|
"""Phase 33: the app factory always attaches the cache-busting
|
|
middleware (by name) — even when the static dir is missing, so the
|
|
/api/* no-touch guarantee holds in every environment."""
|
|
app2 = main_mod.create_app()
|
|
# ``mw.cls`` is starlette's opaque ``_MiddlewareFactory`` protocol —
|
|
# reach for ``__name__`` the same way starlette's own __repr__ does.
|
|
middleware_names = [getattr(mw.cls, "__name__", "") for mw in app2.user_middleware]
|
|
assert "CachingMiddleware" in middleware_names
|