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.
271 lines
10 KiB
Python
271 lines
10 KiB
Python
"""Phase 173 (task 04, C3): the one-time startup migration of a
|
|
legacy credentials-bearing ``docs_repo``.
|
|
|
|
Pure/unit level — no live DB: the ``app_settings`` read is a stubbed
|
|
overlay engine (the ``test_settings_overlay`` fake-engine idiom), the
|
|
write path is monkeypatched (``settings_migrate._write_rows`` — it
|
|
captures the rows AND mirrors them into the stubbed read store, so
|
|
``resolve_settings()`` resolves the DB rows the migration wrote — the
|
|
DB-shadowing pin), the env base comes from ``os.environ`` (``tests/
|
|
conftest.py`` pins ``BOR_DOCS_REPO=`` / ``BOR_SETTINGS_DB=0`` — the
|
|
fixture flips the switch on per test), and the overlay state is
|
|
reset by ``reset_state_for_tests()``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
|
|
from app.core import settings_migrate, settings_overlay
|
|
from app.core.settings_overlay import resolve_settings
|
|
|
|
#: The ONE INFO line the migration logs when it acts (AGENTS rule 10).
|
|
_MIGRATED_LINE = (
|
|
"settings: migrated embedded docs-push credentials out of "
|
|
"docs_repo into docs_user/docs_token"
|
|
)
|
|
|
|
|
|
class _FakeStore:
|
|
"""A controllable ``app_settings`` store: the read (a fake overlay
|
|
engine) and the write (the monkeypatched ``_write_rows``) share
|
|
one dict — a migration write is visible to ``resolve_settings()``
|
|
on the next resolution (the DB-shadowing pin)."""
|
|
|
|
def __init__(self, rows: dict[str, str] | None = None) -> None:
|
|
self.rows = dict(rows or {})
|
|
self.writes: list[dict[str, str]] = []
|
|
|
|
def install(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
store = self
|
|
|
|
class _Rows:
|
|
def __iter__(self) -> object:
|
|
return iter(sorted(store.rows.items()))
|
|
|
|
class _FakeConn:
|
|
def execute(self, _stmt: object) -> _Rows:
|
|
return _Rows()
|
|
|
|
def __enter__(self) -> _FakeConn:
|
|
return self
|
|
|
|
def __exit__(self, *exc: object) -> bool:
|
|
return False
|
|
|
|
class _FakeEngine:
|
|
def connect(self) -> _FakeConn: # type: ignore[no-untyped-def]
|
|
return _FakeConn()
|
|
|
|
monkeypatch.setattr(
|
|
settings_overlay, "_get_engine", lambda: _FakeEngine()
|
|
)
|
|
|
|
def _fake_write(rows: dict[str, str]) -> None:
|
|
store.writes.append(dict(rows))
|
|
store.rows.update(rows)
|
|
|
|
monkeypatch.setattr(settings_migrate, "_write_rows", _fake_write)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_state(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|
settings_overlay.reset_state_for_tests()
|
|
monkeypatch.setenv("BOR_SETTINGS_DB", "1")
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "")
|
|
monkeypatch.setenv("BOR_DOCS_USER", "")
|
|
monkeypatch.setenv("BOR_DOCS_TOKEN", "")
|
|
yield
|
|
settings_overlay.reset_state_for_tests()
|
|
|
|
|
|
def _migrated_lines(caplog: pytest.LogCaptureFixture) -> list[str]:
|
|
"""The ``app.settings`` records that carry the migration line."""
|
|
return [
|
|
record.getMessage()
|
|
for record in caplog.records
|
|
if _MIGRATED_LINE in record.getMessage()
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# (a) the no-op shapes — clean value, malformed segment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_clean_env_url_is_a_noop(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""(a) a clean env URL with no overrides: ``False``, no rows
|
|
written (the pre-phase behavior stands untouched)."""
|
|
store = _FakeStore()
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://host/r.git")
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
assert store.rows == {}
|
|
|
|
|
|
def test_clean_db_override_is_a_noop(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""(a, the DB-source variant) a clean URL arriving as a DB
|
|
override (the effective value, not the env): still a no-op."""
|
|
store = _FakeStore({"docs_repo": "https://host/r.git"})
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://elsewhere/r.git")
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
assert store.rows == {"docs_repo": "https://host/r.git"}
|
|
|
|
|
|
def test_malformed_empty_user_is_a_noop(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""(d) a malformed ``://:@…`` (empty user) is NOT the legacy
|
|
shape: ``False``, no rows, no log — the value stays as-is
|
|
(nothing breaks, nothing half-migrates)."""
|
|
store = _FakeStore()
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://:@host/r.git")
|
|
with caplog.at_level(logging.INFO, logger="app.settings"):
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
assert store.rows == {}
|
|
assert _migrated_lines(caplog) == []
|
|
|
|
|
|
def test_malformed_empty_pass_is_a_noop(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""(d, the other malformation) an empty pass (``://u:@…``):
|
|
the same no-op contract — no rows, no log."""
|
|
store = _FakeStore()
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://u:@host/r.git")
|
|
with caplog.at_level(logging.INFO, logger="app.settings"):
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
assert store.rows == {}
|
|
assert _migrated_lines(caplog) == []
|
|
|
|
|
|
def test_subdomain_is_not_a_false_positive(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""A ``://user:pass@``-shaped SUBDOMAIN (no trailing ``@`` in
|
|
the URL) must not match — the character classes + the trailing
|
|
``@`` requirement do that job (the same contract the PUT guard
|
|
pins)."""
|
|
store = _FakeStore()
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://sub.domain.example.com/r.git")
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# (b) the migration shape — split, DB-shadowing, the one INFO line
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_legacy_env_url_is_split_into_three_rows(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""(b) a legacy env URL with no overrides: ``True``, exactly the
|
|
three rows (clean repo / user / token), and
|
|
``resolve_settings()`` resolves the clean URL + the credentials
|
|
FROM THE DB rows (they shadow the env — the env value still
|
|
embeds the credentials, the effective value does not)."""
|
|
store = _FakeStore()
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://u:pass@host/r.git")
|
|
with caplog.at_level(logging.INFO, logger="app.settings"):
|
|
assert settings_migrate.migrate_legacy_docs_creds() is True
|
|
assert store.rows == {
|
|
"docs_repo": "https://host/r.git",
|
|
"docs_user": "u",
|
|
"docs_token": "pass",
|
|
}
|
|
assert len(store.writes) == 1 # one write call, one row set
|
|
# DB rows SHADOW the env: the effective value is the clean URL.
|
|
effective = resolve_settings()
|
|
assert effective.docs_repo == "https://host/r.git"
|
|
assert effective.docs_user == "u"
|
|
assert effective.docs_token == "pass"
|
|
# (e) the INFO line is emitted exactly once — and it carries no
|
|
# value (the log discipline, A4).
|
|
assert _migrated_lines(caplog) == [_MIGRATED_LINE]
|
|
for line in _migrated_lines(caplog):
|
|
assert "pass" not in line
|
|
|
|
|
|
def test_second_run_is_a_silent_noop(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""(b + idempotency) after a migration wrote the three rows, the
|
|
second run finds the ``docs_token`` row → ``False``, no rows
|
|
written, NO second INFO line (the one-line pin, e)."""
|
|
store = _FakeStore()
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://u:pass@host/r.git")
|
|
with caplog.at_level(logging.INFO, logger="app.settings"):
|
|
assert settings_migrate.migrate_legacy_docs_creds() is True
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert len(store.writes) == 1
|
|
assert _migrated_lines(caplog) == [_MIGRATED_LINE]
|
|
|
|
|
|
def test_legacy_value_from_a_db_override_is_split(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""(b, the DB-source variant) the legacy value arriving as a DB
|
|
``docs_repo`` override (the effective value): the same split —
|
|
the override row is rewritten clean, the credentials join the
|
|
new rows."""
|
|
store = _FakeStore({"docs_repo": "https://u:pass@host/r.git"})
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "") # env empty — DB is the source
|
|
assert settings_migrate.migrate_legacy_docs_creds() is True
|
|
assert store.rows == {
|
|
"docs_repo": "https://host/r.git",
|
|
"docs_user": "u",
|
|
"docs_token": "pass",
|
|
}
|
|
effective = resolve_settings()
|
|
assert effective.docs_repo == "https://host/r.git"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# (c) the clobber guard — an existing docs_token override stands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_existing_token_override_is_never_clobbered(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""(c) a legacy env URL while a ``docs_token`` override already
|
|
exists (owner-set, or a previous migration): ``False`` — the
|
|
pre-existing rows are untouched (no clobber)."""
|
|
store = _FakeStore({"docs_token": "owner-token"})
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://u:pass@host/r.git")
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
assert store.rows == {"docs_token": "owner-token"} # byte-identical
|
|
# the effective value resolves the owner's token, not the URL's.
|
|
assert resolve_settings().docs_token == "owner-token"
|
|
|
|
|
|
def test_existing_token_override_with_clean_repo_is_a_noop(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""(c, the clean variant) a token override with a clean repo
|
|
(env or DB): ``False`` — nothing to do, nothing touched."""
|
|
store = _FakeStore({"docs_token": "owner-token"})
|
|
store.install(monkeypatch)
|
|
monkeypatch.setenv("BOR_DOCS_REPO", "https://host/r.git")
|
|
assert settings_migrate.migrate_legacy_docs_creds() is False
|
|
assert store.writes == []
|
|
assert store.rows == {"docs_token": "owner-token"}
|