All gates green. Final report: **Phase 173 — final verification pass (all tasks already complete in `complete/`)** - Verified the full implementation is in the working tree: `docs_user`/`docs_token` settings + catalogue rows (task 01), `docs_repo` PUT 422 guard + `docs_token` write-only pin (task 02), `run_git(env)` + `push_document(user, token)` credential env (task 03), `settings_migrate.py` wired into `create_app()` (task 04), E2E suite (task 05). - **Completion criteria:** 1. GET `/api/admin/settings` lists `docs_user` (plain) + `docs_token` (secret, `value=null`), token never in any body — ✅ `test_docs_token_is_write_only_masked_end_to_end` + E2E sentinel-absent-from-DOM 2. PUT `docs_repo` with `://user:pass@` → 422 naming the fields; clean URL passes — ✅ integration + E2E UI-side 3. Token set → `GIT_CURL_USERNAME`/`GIT_CURL_PASSWORD`/`GIT_TERMINAL_PROMPT=0` on every git step; empty → `env=None` — ✅ mocked-git unit tests 4. Legacy env split once at startup, idempotent, one INFO line, `.env` untouched — ✅ `test_settings_migrate.py` (4 shapes) + `test_docs_push_migrate.py` 5. Full suite green, coverage >90%, lint clean — ✅ 6. E2E isolated: `test_docs_push_creds.py` 4/4; regression set (panel + layout + response_to_docs) 21/21 — ✅ 7. Commit + phase move — harness's job (per instructions, not mine). 8. README/AGENTS consistent — ✅ (below) - Tests/lint: `uv run pytest` exit 0 (4112 passed) · `uv run pytest --cov=app --cov-report=term-missing` TOTAL 99% · `uv run ruff check .` clean · `uv run pyright` 0 errors - Docs: README.md (incl. Docs-push config section, guard/migration paragraph, `.env.example` block) and AGENTS.md verified consistent in full; fixed two stale docstrings in `app/config.py` + `app/core/settings_catalog.py` (`is_set` → the API's actual `has_value` field). - No live-infrastructure changes (no live step in this phase). No next pending phase — `todo/` held only 173.
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""Unit tests: app factory edge cases (no live DB needed)."""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app.main as main_mod
|
|
from app.config import get_settings
|
|
from app.core import settings_overlay
|
|
|
|
|
|
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
|
|
|
|
|
|
def test_create_app_boots_with_the_switch_on(monkeypatch) -> None:
|
|
"""Phase 168 (task 02/03): the boot prime with the kill switch ON
|
|
— the ``settings overrides loaded count=…`` branch (the OFF branch
|
|
is exercised by every other suite's app import; the overlay's
|
|
engine is fail-soft, so a DB-down unit environment still boots).
|
|
``create_app`` also wires the chat runtime reset hook here (task
|
|
03) — the ``register_invalidator`` pin lives in
|
|
``test_settings_live_apply.py``."""
|
|
settings_overlay.reset_state_for_tests()
|
|
monkeypatch.setenv("BOR_SETTINGS_DB", "1")
|
|
app2 = main_mod.create_app()
|
|
assert app2 is not None
|
|
# restore: the switch reverts via monkeypatch; the cache clear
|
|
# ensures a boot-time row set never leaks into later suites'
|
|
# settings, and the hook slate is fresh for them too.
|
|
settings_overlay.reset_state_for_tests()
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def test_create_app_migration_failure_warns_and_boots(
|
|
monkeypatch, caplog
|
|
) -> None:
|
|
"""Phase 173 (task 04, C3): a migration failure logs a WARNING
|
|
and never blocks startup — the app still boots (with the
|
|
pre-phase behavior: the migration is a convenience, not a
|
|
dependency). The seam: the migration is monkeypatched to raise;
|
|
the real migration is a no-op in a DB-less unit environment
|
|
(the overlay's fail-soft read + the conftest-pinned empty
|
|
``BOR_DOCS_REPO``), so the boot path is otherwise unchanged."""
|
|
settings_overlay.reset_state_for_tests()
|
|
|
|
def _boom() -> bool:
|
|
raise RuntimeError("migration hiccup")
|
|
|
|
monkeypatch.setattr(main_mod, "migrate_legacy_docs_creds", _boom)
|
|
with caplog.at_level(logging.WARNING, logger="app"):
|
|
app2 = main_mod.create_app()
|
|
assert app2 is not None
|
|
warnings = [
|
|
record.getMessage() for record in caplog.records
|
|
if record.levelno == logging.WARNING
|
|
]
|
|
assert "settings migration failed (RuntimeError)" in warnings
|
|
# restore: the hook slate is fresh for the later suites, and the
|
|
# cache clear keeps a boot-time row set from leaking into their
|
|
# settings.
|
|
settings_overlay.reset_state_for_tests()
|
|
get_settings.cache_clear()
|