Files
photoanalyzer/tests/integration/test_installation_manual.py
domverse 0d4f9d1c28
Some checks failed
Test / suites (push) Failing after 2m40s
Test / container (push) Failing after 5m10s
US09-02: Write the Installation and Operations Manual (#108)
2026-08-23 21:46:00 +02:00

179 lines
7.4 KiB
Python

"""US09-02: the installation manual, checked against the thing it describes.
Documentation rots quietly. A setting is renamed, a command grows a flag, an exit
code changes meaning — and the manual keeps confidently saying the old thing until
somebody follows it into a bad afternoon. So every fact in it that the code also
knows is compared with the code, in both directions where both directions are
meaningful: a setting the manual invents fails, and a setting the manual forgot
fails too.
"""
from __future__ import annotations
import re
import subprocess
import sys
from functools import lru_cache
from pathlib import Path
from photo_pipeline.config import ENV_PREFIX, LEGACY_ALIASES, Config
REPO = Path(__file__).resolve().parents[2]
MANUAL = REPO / "docs" / "installation.md"
MAIN = REPO / "photo_pipeline" / "__main__.py"
TEXT = MANUAL.read_text()
ENV_NAMES = re.compile(rf"{ENV_PREFIX}[A-Z0-9_]+")
# Read by docker-compose.yml, not by Config. They are configuration an installer sets,
# so the manual documents them; they are simply not application settings.
COMPOSITION_ONLY = {
f"{ENV_PREFIX}IMAGE",
f"{ENV_PREFIX}LIBRARY_HOST_PATH",
f"{ENV_PREFIX}PUBLISH_ADDRESS",
f"{ENV_PREFIX}UID",
f"{ENV_PREFIX}GID",
f"{ENV_PREFIX}ENV_FILE", # read before Config exists, so it is not a field
}
def documented_env_names() -> set[str]:
return set(ENV_NAMES.findall(TEXT))
@lru_cache
def cli_help(*args: str) -> str:
"""What the CLI says about itself, asked the way an operator asks."""
result = subprocess.run(
[sys.executable, "-m", "photo_pipeline", *args, "--help"],
cwd=REPO,
capture_output=True,
text=True,
timeout=120,
check=False,
)
assert result.returncode == 0, result.stderr
return result.stdout
@lru_cache
def cli_commands() -> frozenset[str]:
listed = re.search(r"\{([a-z0-9,\-]+)\}", cli_help())
assert listed, f"the CLI listed no subcommands:\n{cli_help()}"
return frozenset(listed.group(1).split(","))
def cli_flags(command: str) -> frozenset[str]:
return frozenset(re.findall(r"--[a-z][a-z-]+", cli_help(command)))
# ── settings ─────────────────────────────────────────────────────────────────
def test_every_setting_is_documented():
"""A setting nobody documents is a setting nobody configures deliberately."""
expected = {f"{ENV_PREFIX}{field.upper()}" for field in Config.model_fields}
missing = sorted(expected - documented_env_names())
assert missing == [], f"undocumented settings: {missing}"
def test_the_manual_invents_no_settings():
unknown = sorted(
name
for name in documented_env_names()
if name.removeprefix(ENV_PREFIX).lower() not in Config.model_fields
and name not in COMPOSITION_ONLY
)
assert unknown == [], f"documented but not a real setting: {unknown}"
def test_the_documented_defaults_are_the_real_defaults():
"""Spot-checked where a wrong default is actively dangerous: the bind address,
the trust boundary, and the gate that protects an irreplaceable library."""
assert Config.model_fields["host"].default == "127.0.0.1"
assert "`PHOTO_PIPELINE_HOST` | `127.0.0.1`" in TEXT
assert Config.model_fields["allowed_hosts"].default == ()
assert Config.model_fields["require_dry_run_approval"].default is False
assert "`PHOTO_PIPELINE_REQUIRE_DRY_RUN_APPROVAL` | `false`" in TEXT
def test_every_secret_is_marked_as_one():
for field in ("access_secret", "vision_api_key", "immich_api_key"):
name = f"{ENV_PREFIX}{field.upper()}"
row = next(line for line in TEXT.splitlines() if line.startswith(f"| `{name}`"))
assert "secret" in row.lower(), f"{name} is not marked as a secret"
def test_the_legacy_aliases_are_documented():
"""An operator with an existing photo_analyzer.env needs to know it still works."""
for alias in LEGACY_ALIASES:
assert alias in TEXT, f"legacy alias {alias} is undocumented"
# ── commands ─────────────────────────────────────────────────────────────────
# The manual is an installation and operations manual, not a command reference: these
# are the commands an installer actually runs, and every one of them must exist.
OPERATIONAL = ("migrate", "serve", "worker", "backup", "verify-backup", "restore", "diagnostics")
def test_every_command_the_manual_tells_you_to_run_exists():
missing = [name for name in OPERATIONAL if name not in cli_commands()]
assert missing == [], f"the manual names commands the CLI does not have: {missing}"
undocumented = [name for name in OPERATIONAL if not re.search(rf"\b{re.escape(name)}\b", TEXT)]
assert undocumented == [], f"operational commands the manual omits: {undocumented}"
def test_every_documented_flag_exists_on_the_command_it_is_shown_with():
for command, flag in (
("backup", "--reason"),
("backup", "--keep"),
("restore", "--into"),
("worker", "--allow-legacy"),
("serve", "--allow-legacy"),
):
assert flag in cli_flags(command), f"{command} has no {flag}"
assert flag in TEXT, f"{flag} is undocumented"
# ── exit codes ───────────────────────────────────────────────────────────────
def test_every_documented_exit_code_is_one_the_cli_can_return():
documented = {int(code) for code in re.findall(r"^\| `(\d)` \|", TEXT, re.MULTILINE)}
returned = {int(code) for code in re.findall(r"^\s+return (\d)$", MAIN.read_text(), re.MULTILINE)}
assert documented, "no exit codes are documented"
assert documented <= returned | {0}, f"documented but unreachable: {sorted(documented - returned)}"
def test_every_refusal_exit_code_is_documented():
"""0 is success and 1 is 'it said why'; every other code is a specific refusal an
operator will meet at startup, and meeting an undocumented one is the worst case."""
returned = {int(code) for code in re.findall(r"^\s+return (\d)$", MAIN.read_text(), re.MULTILINE)}
documented = {int(code) for code in re.findall(r"^\| `(\d)` \|", TEXT, re.MULTILINE)}
missing = sorted(code for code in returned if code not in documented and code != 0)
assert missing == [], f"undocumented exit codes: {missing}"
# ── the promises the manual makes ────────────────────────────────────────────
def test_the_manual_carries_no_credential_shaped_example():
"""The repository's own scanner refuses these in a commit; a manual is exactly
where a real-looking one gets copied from."""
suspicious = re.findall(
r"(?i)(api[_-]?key|secret|token|password)\s*[=:]\s*[\"']?([A-Za-z0-9_\-]{12,})",
TEXT,
)
assert suspicious == [], f"credential-shaped example: {suspicious}"
def test_the_manual_states_the_invariants_an_installer_must_not_break():
for invariant in ("_IGNORE/", "One writer", "EXIF is verified before upload"):
assert invariant in TEXT, f"the manual does not state: {invariant}"
def test_the_manual_is_reachable_from_the_index():
assert "installation.md" in (REPO / "docs" / "index.md").read_text()