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.
224 lines
10 KiB
Python
224 lines
10 KiB
Python
"""Unit: the admin settings router's pure helpers (phase 168, task 04)
|
|
— the env-native stringification (``_display``), the control-kind
|
|
derivation (``_field_type``), the ``_item`` builder's masked and
|
|
default branches, and the LOCKED A1 static pins: the module never
|
|
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, the phase-173 ``docs_repo`` guard on the PUT path)
|
|
is pinned in ``tests/integration/test_admin_settings_api.py``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
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
|
|
|
|
|
|
def test_display_bools_are_the_env_spelling() -> None:
|
|
"""bool → ``"true"``/``"false"`` — checked BEFORE int (``isinstance(True, int)``
|
|
is ``True`` — a bool field must never stringify through ``str()``'s
|
|
``"True"``)."""
|
|
assert _display("stream_thinking", True) == "true"
|
|
assert _display("stream_thinking", False) == "false"
|
|
# An int value for an int field is the plain str (no bool confusion).
|
|
assert _display("llm_retries", 3) == "3"
|
|
|
|
|
|
def test_display_list_field_is_json() -> None:
|
|
"""The one ``list[str]`` field (``suggestions``) stringifies as the
|
|
JSON array the env spelling would carry (K2: stored in the same
|
|
JSON form, pydantic parses it exactly like the env var)."""
|
|
assert set(LIST_FIELDS) == {"suggestions"}
|
|
assert _display("suggestions", ["a", "b"]) == '["a", "b"]'
|
|
assert _display("suggestions", []) == "[]"
|
|
|
|
|
|
def test_display_scalars_are_plain_strings() -> None:
|
|
"""Everything else is ``str()`` — floats, ints, strings, in the
|
|
env-native form (what ``app_settings`` stores, D1)."""
|
|
assert _display("relevance_threshold", 0.55) == "0.55"
|
|
assert _display("llm_timeout", 7.0) == "7.0"
|
|
assert _display("app_name", "Brain of Reese") == "Brain of Reese"
|
|
assert _display("import_extensions", "md,txt") == "md,txt"
|
|
|
|
|
|
def test_field_type_covers_all_kinds() -> None:
|
|
"""The panel control kind per annotation: ``bool`` before ``int``
|
|
(a bool annotation is never misread as int), the one list field is
|
|
``json``, the rest fall through to their annotation."""
|
|
assert _field_type("stream_thinking") == "bool"
|
|
assert _field_type("llm_retries") == "int"
|
|
assert _field_type("relevance_threshold") == "float"
|
|
assert _field_type("app_name") == "str"
|
|
assert _field_type("suggestions") == "json"
|
|
# Every field resolves to one of the five wire kinds.
|
|
kinds = {"int", "float", "bool", "str", "json"}
|
|
for key in Settings.model_fields:
|
|
assert _field_type(key) in kinds
|
|
|
|
|
|
def test_item_masks_secret_fields() -> None:
|
|
"""LOCKED A4: a secret field's item is write-only on the wire —
|
|
``masked=True``, ``value=None``, ``db_value=None``,
|
|
``default=""`` (the code default is never shown for a secret),
|
|
``has_value`` = is a non-empty value in effect at all (the test
|
|
env sets a non-empty admin password / session secret)."""
|
|
effective = Settings()
|
|
assert effective.admin_password # tests/conftest.py pins it non-empty
|
|
item = _item("admin_password", {}, effective)
|
|
assert item.masked is True
|
|
assert item.value is None
|
|
assert item.db_value is None
|
|
assert item.default == ""
|
|
assert item.has_value is True
|
|
assert item.secret is True
|
|
assert item.key == "admin_password"
|
|
assert item.env_var == "BOR_ADMIN_PASSWORD"
|
|
# A stored row for a secret field never exposes the text either.
|
|
# (The effective value is forced via an init kwarg — the operator's
|
|
# local `.env` may legitimately carry a key, so the test does not
|
|
# assume a specific effective value.)
|
|
item = _item("llm_api_key", {"llm_api_key": "sk-x"}, Settings(llm_api_key="sk-x"))
|
|
assert item.masked is True
|
|
assert item.value is None
|
|
assert item.db_value is None
|
|
assert item.has_value is True
|
|
assert item.has_override is True
|
|
item = _item("llm_api_key", {}, Settings(llm_api_key=""))
|
|
assert item.masked is True
|
|
assert item.value is None
|
|
assert item.db_value is None
|
|
assert item.has_value is False # no value in effect at all
|
|
|
|
|
|
def test_item_reports_env_default_and_db_sources(
|
|
monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""The ``source`` badge: ``db`` for an in-effect overridable row,
|
|
``env`` when the ``BOR_*`` var is in the process environment,
|
|
``default`` otherwise — and a non-overridable row is NEVER an
|
|
override (LOCKED A3: the overlay drops it, so a hand-inserted
|
|
``database_url`` row is not in effect)."""
|
|
import os
|
|
|
|
effective = Settings()
|
|
# "env": tests/conftest.py sets BOR_RELEVANCE_THRESHOLD.
|
|
assert os.environ.get("BOR_RELEVANCE_THRESHOLD") is not None
|
|
item = _item("relevance_threshold", {}, effective)
|
|
assert item.source == "env"
|
|
# The value is the EFFECTIVE float stringified ("0.30" → 0.3 → "0.3").
|
|
assert item.value == str(float(os.environ["BOR_RELEVANCE_THRESHOLD"]))
|
|
assert item.has_override is False
|
|
# "default": no BOR_DOCS_WORK_DIR in the test environment.
|
|
assert os.environ.get("BOR_DOCS_WORK_DIR") is None
|
|
item = _item("docs_work_dir", {}, effective)
|
|
assert item.source == "default"
|
|
assert item.value == effective.docs_work_dir
|
|
# "db": the row is in effect only while the overlay switch is ON
|
|
# (the conftest pin is 0 — flip it for the pin, it restores after).
|
|
monkeypatch.setenv("BOR_SETTINGS_DB", "1")
|
|
item = _item("relevance_threshold", {"relevance_threshold": "0.9"}, effective)
|
|
assert item.source == "db"
|
|
assert item.has_override is True
|
|
# Non-overridable: a stored row is not an override (LOCKED A3) —
|
|
# even with the switch on.
|
|
item = _item("database_url", {"database_url": "postgresql://x"}, effective)
|
|
assert item.has_override is False
|
|
assert item.masked is True
|
|
# The code default stringifies (non-secret fields).
|
|
item = _item("llm_retries", {}, effective)
|
|
assert item.default == "3"
|
|
assert item.value == "3"
|
|
assert item.db_value is None
|
|
|
|
|
|
def test_item_db_value_is_the_stored_string() -> None:
|
|
"""A stored non-secret override exposes ``db_value`` (the stored
|
|
string, D1) and ``has_override`` — even while the overlay switch
|
|
is OFF (the row is stored but not in effect: the badge is
|
|
env/default, the panel's ``overlay_enabled`` flag explains the
|
|
rest)."""
|
|
effective = Settings()
|
|
item = _item("llm_retries", {"llm_retries": "7"}, effective)
|
|
assert item.has_override is True
|
|
assert item.db_value == "7"
|
|
assert item.source == "default" # switch off (the conftest pin): not in effect
|
|
|
|
|
|
def test_module_never_writes_files_or_shells_out() -> None:
|
|
"""LOCKED A1 + the module's third invariant (the unit pin): the
|
|
router never opens a file (``.env`` is never written by any code
|
|
path), never shells out and never ``eval``s a value. (The docstring
|
|
legitimately mentions ``.env`` — the pins are on the CALLS, the
|
|
``test_settings_overlay`` house style.)"""
|
|
source = (
|
|
pathlib.Path(__file__).resolve().parents[2]
|
|
/ "app"
|
|
/ "api"
|
|
/ "admin_settings.py"
|
|
).read_text()
|
|
assert re.search(r"\bopen\s*\(", source) is None, (
|
|
"app/api/admin_settings.py must never open() a file (LOCKED A1: "
|
|
".env is never written)"
|
|
)
|
|
assert "subprocess" not in source
|
|
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", "")
|