phase: 173_docs_push_creds_split
Build and Push Containers / build-and-push-app (push) Successful in 1m41s
Build and Push Containers / build-and-push-db (push) Successful in 11s

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.
This commit is contained in:
2026-10-10 16:47:39 -04:00
parent f27106a638
commit a86caec821
56 changed files with 3177 additions and 39 deletions
+52 -3
View File
@@ -6,8 +6,8 @@ opens a file (``.env`` is never written), never shells out and never
``eval``s a value.
The DB-driven behavior (the four routes, the candidate-set validation,
the auth matrix) is pinned in
``tests/integration/test_admin_settings_api.py``.
the auth matrix, the phase-173 ``docs_repo`` guard on the PUT path)
is pinned in ``tests/integration/test_admin_settings_api.py``.
"""
from __future__ import annotations
@@ -15,8 +15,14 @@ import pathlib
import re
import pytest
from fastapi import HTTPException
from app.api.admin_settings import _display, _field_type, _item
from app.api.admin_settings import (
_display,
_field_type,
_item,
_refuse_embedded_credentials,
)
from app.config import Settings
from app.core.settings_overlay import LIST_FIELDS
@@ -172,3 +178,46 @@ def test_module_never_writes_files_or_shells_out() -> None:
assert "os.system" not in source
assert re.search(r"\beval\s*\(", source) is None
assert "import shutil" not in source and "shutil." not in source
def test_refuse_embedded_credentials_fires_only_for_docs_repo() -> None:
"""Phase 173 (task 02, C4): the guard 422s for ``docs_repo`` with a
``://<user>:<pass>@`` segment — the detail NAMES the Docs user /
Docs token fields — and passes everything else: other keys (only
``docs_repo`` is guarded), clean ``https://`` URLs, local paths,
``ssh://`` remotes, and a ``user:``-looking subdomain (the
trailing ``@`` requirement keeps it from false-positiving).
An empty user OR an empty pass (``://:pass@`` / ``://u:@``) does
NOT match — the ``+`` quantifiers require both to be non-empty.
"""
# The 422: names the replacement fields.
with pytest.raises(HTTPException) as excinfo:
_refuse_embedded_credentials(
"docs_repo", "https://u:pass@gitea.example.com/o/r.git"
)
assert excinfo.value.status_code == 422
assert "Docs user" in excinfo.value.detail
assert "Docs token" in excinfo.value.detail
# Also a ``git://``-style URL with embedded credentials (the regex
# is scheme-agnostic: it keys on ``://<user>:<pass>@``).
with pytest.raises(HTTPException):
_refuse_embedded_credentials("docs_repo", "git://u:tok@host.example.com/r.git")
# Other keys: never guarded (``docs_user`` may hold any plain text).
_refuse_embedded_credentials("docs_user", "https://u:pass@host.example.com/r.git")
_refuse_embedded_credentials("llm_api_key", "://u:pass@")
# Clean shapes pass: https URL, local path, ssh remote.
_refuse_embedded_credentials("docs_repo", "https://gitea.example.com/o/r.git")
_refuse_embedded_credentials("docs_repo", "/srv/docs-repo")
_refuse_embedded_credentials("docs_repo", "ssh://git@gitea.example.com/o/r.git")
# A subdomain that looks like ``user:`` (no trailing ``@``).
_refuse_embedded_credentials("docs_repo", "https://sub.domain.example.com/r.git")
# Empty user OR empty pass: the ``+`` quantifiers require both
# non-empty — no match (a bare ``://`` is not the leak class).
_refuse_embedded_credentials("docs_repo", "https://:pass@host.example.com/r.git")
_refuse_embedded_credentials("docs_repo", "https://u:@host.example.com/r.git")
# A ``:`` after the ``@`` segment (a URL with no credentials at
# all) and an empty value.
_refuse_embedded_credentials("docs_repo", "https://host.example.com/r.git:3000")
_refuse_embedded_credentials("docs_repo", "")
+36
View File
@@ -665,6 +665,42 @@ def test_docs_branch_env_override(monkeypatch: pytest.MonkeyPatch) -> None:
assert s.docs_base_branch == "master"
def test_docs_creds_defaults_are_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 173 (task 01, C5): no credentials by default — the push is
anonymous/URL-credential-free, exactly as before this phase."""
monkeypatch.delenv("BOR_DOCS_USER", raising=False)
monkeypatch.delenv("BOR_DOCS_TOKEN", raising=False)
s = _settings()
assert s.docs_user == ""
assert s.docs_token == ""
def test_docs_creds_env_overrides(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 173 (task 01): ``BOR_DOCS_USER`` / ``BOR_DOCS_TOKEN``
resolve verbatim — plain ``str``, no coercion."""
monkeypatch.setenv("BOR_DOCS_USER", "alice")
monkeypatch.setenv("BOR_DOCS_TOKEN", "tok")
s = _settings()
assert s.docs_user == "alice"
assert s.docs_token == "tok"
def test_docs_creds_empty_env_stays_empty(monkeypatch: pytest.MonkeyPatch) -> None:
"""Phase 173 (task 01, C5): an empty (or whitespace) env value stays
``""`` / verbatim — no coercion, no normalization away from empty.
The anonymous state must stay expressible: the settings must NOT
strip the token, or an explicit empty would become a set credential.
(Whitespace is stored verbatim, like ``docs_repo``; the *push*
service decides the empty-vs-set semantics, not the settings.)"""
monkeypatch.setenv("BOR_DOCS_USER", "")
monkeypatch.setenv("BOR_DOCS_TOKEN", "")
s = _settings()
assert s.docs_user == ""
assert s.docs_token == ""
monkeypatch.setenv("BOR_DOCS_TOKEN", " ")
assert _settings().docs_token == " "
def test_docs_work_dir_env_override_is_raw_string(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("BOR_DOCS_WORK_DIR", "/data/bor/docs")
s = _settings()
+169
View File
@@ -9,19 +9,33 @@ ever involved.
The module skips (``pytest.skip``) when ``git --version`` fails — a
machine without git must not see hard failures.
Phase 173 (C2/C5) adds a mocked-``run_git`` section: the FULL 10-step
sequence is exercised against a fake git (no real repo) to pin the
credential transport — no token → every step gets ``env=None``
(byte-identical to the pre-phase call); a token → every step gets
``GIT_CURL_USERNAME`` / ``GIT_CURL_PASSWORD`` / ``GIT_TERMINAL_PROMPT=0``
merged over a controlled ``os.environ`` (``PATH`` survives the
subprocess env-replacement trap); a whitespace-only token = empty;
and a failing step still raises :class:`DocsPushError` with git's
stderr, credential-agnostic.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from typing import TypedDict
import pytest
import app.core.docs_push as docs_push_mod
from app.core.docs_push import DocsPushError, push_document
from scripts.git_sync import GitSyncError
BASE = "main"
BRANCH = "bor-docs"
REL = "docs/note.md"
CLEAN_REPO = "https://git.example.com/docs.git" # phase 173: the credential-free form
IDENTITY = ("-c", "commit.gpgsign=false", "-c", "user.name=Test", "-c", "user.email=t@example.com")
@@ -198,3 +212,158 @@ def test_unsafe_rel_path_is_refused_before_any_git(bare_repo: Path, tmp_path: Pa
)
# No checkout was even attempted.
assert not (tmp_path / "w").exists()
# --- phase 173: credentials on the wire (C2/C5, mocked git) ---------------
class _GitCall(TypedDict):
"""One recorded ``run_git`` call (argv / cwd / env)."""
argv: list[str]
cwd: Path
env: dict[str, str] | None
def _fake_run_git(
monkeypatch: pytest.MonkeyPatch, push_stderr: str = ""
) -> list[_GitCall]:
"""Monkeypatch ``app.core.docs_push.run_git``; record each call
(argv / cwd / env).
The FULL 10-step sequence runs: clone (the work dir is absent) →
the base fetch → the ``FETCH_HEAD`` capture → the branch fetch
(ALWAYS raises — the branch does not exist on the remote yet,
the swallowed path) → the branch probe → ``checkout -B`` → add →
commit → push (raises when *push_stderr* is set — the
un-swallowed failure, task 03 5d) → the ``HEAD`` capture. Returns
``(BRANCH, "b" * 40)`` on success.
"""
calls: list[_GitCall] = []
def fake_run(
argv: list[str], cwd: Path, env: dict[str, str] | None = None
) -> str:
calls.append({"argv": list(argv), "cwd": cwd, "env": env})
key = tuple(argv)
if key == ("git", "fetch", "--depth", "100", "origin", BRANCH):
raise GitSyncError(
f"git fetch --depth 100 origin {BRANCH} failed (exit 128): "
f"fatal: couldn't find remote ref {BRANCH}"
)
if push_stderr and key == ("git", "push", "origin", BRANCH):
raise GitSyncError(
f"git push origin {BRANCH} failed (exit 128): {push_stderr}"
)
if key[:2] == ("git", "rev-parse"):
return "a" * 40 if key[-1] == "FETCH_HEAD" else "b" * 40
return ""
monkeypatch.setattr(docs_push_mod, "run_git", fake_run)
return calls
def _args(work: Path) -> dict[str, str]:
return dict(
repo=CLEAN_REPO,
base_branch=BASE,
branch=BRANCH,
work_dir=str(work),
rel_path=REL,
content="# Note\n",
commit_message="docs: note",
)
def test_no_token_env_is_none_on_every_step(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Task 03 5a (C5 byte-identity pin): default args (no token) →
EVERY git step of the full sequence gets ``env=None`` — the
pre-phase ``subprocess.run`` shape, unchanged."""
work = tmp_path / "work" # absent — the clone step runs too
calls = _fake_run_git(monkeypatch)
branch, sha = push_document(**_args(work))
assert (branch, sha) == (BRANCH, "b" * 40)
assert len(calls) == 10 # clone, fetch×2, rev-parse×2, branch, checkout, add, commit, push
for call in calls:
assert call["env"] is None
# The CLEAN URL stays in the argv (the credential never appears
# because it is not passed at all).
assert calls[0]["argv"][-2] == CLEAN_REPO
def test_token_env_on_every_step_merged_over_environ(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Task 03 5b (C2): token set → EVERY git step of a successful push
gets the 3-var credential env MERGED OVER a controlled
``os.environ`` — ``PATH`` survives the subprocess
env-replacement trap, the credential never lands in the argv,
and the clean URL stays in it."""
monkeypatch.setenv("PATH", "/usr/bin")
monkeypatch.setenv("BOR_TEST_ENV", "sentinel")
work = tmp_path / "work"
calls = _fake_run_git(monkeypatch)
push_document(**_args(work), user="alice", token="s3cret")
assert len(calls) == 10
for call in calls:
env = call["env"]
assert isinstance(env, dict)
assert env["GIT_CURL_USERNAME"] == "alice"
assert env["GIT_CURL_PASSWORD"] == "s3cret"
assert env["GIT_TERMINAL_PROMPT"] == "0"
assert env["PATH"] == "/usr/bin" # the merge-over-os.environ pin
assert env["BOR_TEST_ENV"] == "sentinel" # the parent env survives
# C2: the credential travels ONLY in the environment.
assert "alice" not in call["argv"]
assert "s3cret" not in call["argv"]
# The clean URL is in the argv, untouched.
assert CLEAN_REPO in calls[0]["argv"]
def test_whitespace_only_token_is_treated_as_empty(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Task 03 5c (the ``.strip()`` rule): a whitespace-only token is
the no-credentials state → every step gets ``env=None``."""
calls = _fake_run_git(monkeypatch)
push_document(**_args(tmp_path / "work"), user="alice", token=" ")
assert len(calls) == 10
for call in calls:
assert call["env"] is None
def test_failing_step_raises_docs_push_error_with_and_without_credentials(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Task 03 5d: a failing step (the push) still raises
:class:`DocsPushError` with git's stderr — the error path is
credential-agnostic (with and without the credential env); the
failing step itself still carries the credential env when a token
is set."""
stderr = "fatal: rejected (simulated non-fast-forward)"
# without credentials (C5 — the pre-phase error path)
calls = _fake_run_git(monkeypatch, push_stderr=stderr)
with pytest.raises(DocsPushError) as excinfo:
push_document(**_args(tmp_path / "w1"))
msg = str(excinfo.value)
assert f"git push origin {BRANCH} failed (exit 128): {stderr}" in msg
assert calls[-1]["env"] is None # the failed push step — no env
# with credentials (the error carries the full stderr for logs;
# the API layer's sanitize_error stays the defense-in-depth)
calls = _fake_run_git(monkeypatch, push_stderr=stderr)
with pytest.raises(DocsPushError) as excinfo:
push_document(**_args(tmp_path / "w2"), user="alice", token="s3cret")
msg = str(excinfo.value)
assert f"git push origin {BRANCH} failed (exit 128): {stderr}" in msg
assert isinstance(calls[-1]["env"], dict)
assert calls[-1]["env"]["GIT_CURL_PASSWORD"] == "s3cret"
+30
View File
@@ -297,6 +297,36 @@ def test_run_git_captures_and_returns_stdout(
assert len(calls) == 1
def test_run_git_default_env_is_none_and_reaches_subprocess(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 173 (C5 byte-identity pin): the default ``env=None`` is
passed through verbatim to ``subprocess.run`` — the pre-phase call
shape, so every sync-path invocation is unchanged."""
calls = _fake_run(monkeypatch, stdout="ok")
assert git_sync.run_git(["git", "version"], cwd=tmp_path) == "ok"
assert len(calls) == 1
assert "env" in calls[0]
assert calls[0]["env"] is None
def test_run_git_explicit_env_reaches_subprocess_verbatim(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Phase 173 (C2): an explicit env dict reaches ``subprocess.run``
verbatim — ``run_git`` neither merges over nor mutates the parent
environment (the env-replacement trap is the caller's contract;
the docs-push sequence does its own ``os.environ`` merge)."""
calls = _fake_run(monkeypatch, stdout="ok")
env = {"A": "b"}
assert git_sync.run_git(["git", "version"], cwd=tmp_path, env=env) == "ok"
assert len(calls) == 1
assert calls[0]["env"] is env # the SAME object — passed through verbatim
assert calls[0]["env"] == {"A": "b"}
# --- phase 135: the branch pin (the argv matrix) ----------------------------
+33
View File
@@ -1,6 +1,8 @@
"""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
@@ -51,3 +53,34 @@ def test_create_app_boots_with_the_switch_on(monkeypatch) -> None:
# 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()
+36 -1
View File
@@ -54,12 +54,15 @@ def test_every_meta_carries_its_own_key() -> None:
def test_secrets_are_exactly_the_masked_set() -> None:
# LOCKED A4: write-only, masked — the 3 named secrets +
# database_url (read-only, but its URL carries credentials).
# database_url (read-only, but its URL carries credentials) +
# docs_token (phase 173, C1: the docs-push remote token reuses the
# write-only/masked mechanism, llm_api_key-style).
assert set(SECRETS) == {
"llm_api_key",
"admin_password",
"session_secret",
"database_url",
"docs_token",
}
@@ -108,6 +111,38 @@ def test_llm_thinking_level_is_a_plain_live_overridable_row() -> None:
assert "llm_thinking_level" not in NON_OVERRIDABLE
def test_docs_user_is_a_plain_live_overridable_row() -> None:
"""Phase 173 (task 01, C1/C5): ``docs_user`` sits in ``Docs push``
with the ``Docs user`` label and is plain — not ``secret`` (a
plain username), not ``restart_required`` (the push service reads
the setting per push via ``get_settings()`` — live), overridable
(the overlay is generic over ``str`` fields). An empty value is a
valid state (C5: the anonymous push)."""
meta = CATALOG["docs_user"]
assert meta.group == "Docs push"
assert meta.label == "Docs user"
assert meta.secret is False
assert "docs_user" not in SECRETS
assert "docs_user" not in RESTART_REQUIRED
assert "docs_user" not in NON_OVERRIDABLE
def test_docs_token_is_a_secret_live_overridable_row() -> None:
"""Phase 173 (task 01, C1): ``docs_token`` has the ``Docs token``
label and ``secret=True`` — write-only end-to-end via the EXISTING
derived mechanism (``SECRETS`` picks it up automatically; the API
reports ``is_set`` + ``value=null``, a blank PUT 422s, the panel
renders it masked, zero ``settings.js`` code). Live (the push
service reads it per push), overridable (generic ``str`` overlay)."""
meta = CATALOG["docs_token"]
assert meta.group == "Docs push"
assert meta.label == "Docs token"
assert meta.secret is True
assert "docs_token" in SECRETS
assert "docs_token" not in RESTART_REQUIRED
assert "docs_token" not in NON_OVERRIDABLE
def test_labels_nonempty_and_unique() -> None:
labels = [meta.label for meta in CATALOG.values()]
assert all(label.strip() for label in labels), "empty label in the catalogue"
+270
View File
@@ -0,0 +1,270 @@
"""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"}