From 529d4bff578958811a6438a4274400e9fce8d52f Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 12 Sep 2026 21:31:21 -0400 Subject: [PATCH] test: tie the install docs to the code they quote (3422 follow-up) 3422 was already fixed. Commit 86abaf0 applied option 1 in full and it is on main: .env.example carries the bootstrap section with the backup warning, README explains the refusal and why it is deliberate, docker-compose forwards the variable, and the milestone-362 smoke gate that FOUND the bug now sets it (build.yml:1159) and passes. The issue's premise - "CURATOR_BOOTSTRAP_NEW_KEY appears nowhere outside backend/" - is stale. What was left is the dependency that fix created. README.md and .env.example now both print the literal error text, the literal key path and the variable name, because a stranger greps for the string their terminal showed them. That is the right call and it means two user-facing files now depend on this module's wording with nothing connecting them - the install surface's characteristic defect, one rename away from a README that sends strangers to a path that does not exist. Three guards, all presence checks on both sides. An absence check against prose would pass for the wrong reason the moment a sentence were reworded (snippet 3352): - the raised message still contains the sentence README reproduces, the variable both docs say to set, and the restore-rather-than-mint alternative the whole refusal rests on; - both docs still name _CREDENTIAL_KEY_PATH and the variable, read from the code rather than retyped, so a rename fails here; - compose still forwards the variable - without that line the docs' "set it in .env" is silently inert and fails identically to not setting it. Option 2 (mint when the credential table is empty) is deliberately NOT done. The issue's own guidance is "(1) now, (2) if the friction proves annoying", and the friction has not been reported. Worth recording that its predicate checks out exactly: the Fernet key protects Credential.encrypted_blob and nothing else - no other Fernet user exists - so "no credential rows means nothing can be made undecryptable" is provable rather than probable. The cost is placement: create_app() is sync and constructs the key before any engine exists, so the check cannot live where the failure is. entrypoint.sh, which already runs alembic against the DB, is the natural seam. Only the web role is affected; the Celery roles build the key lazily inside tasks. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9 --- tests/test_credential_crypto.py | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/test_credential_crypto.py b/tests/test_credential_crypto.py index 6274bb5..2ca31b0 100644 --- a/tests/test_credential_crypto.py +++ b/tests/test_credential_crypto.py @@ -69,3 +69,69 @@ def test_missing_key_with_env_var_bootstraps(tmp_path, monkeypatch): key_path = tmp_path / "bootstrap.b64" CredentialCrypto(key_path) # no bootstrap_ok kwarg — relies on env assert key_path.exists() + + +# --- #3422: the install docs quote this module, so they must keep agreeing -- +# +# README.md and .env.example both print the literal failure a new installer +# hits, and the literal path and env var they must act on. That is the right +# call — a stranger greps for the string their terminal showed them — but it +# means those two files now DEPEND on this module's wording, with nothing +# connecting them. The characteristic defect of the install surface is exactly +# this: the documented behaviour and the code drift apart, and the code is the +# one that is right. +# +# Presence checks on both sides, deliberately: an absence check against prose +# would pass for the wrong reason the moment a sentence were reworded +# (snippet #3352). + +_REPO_ROOT = Path(__file__).resolve().parents[1] +_INSTALL_DOCS = ("README.md", ".env.example") + + +def test_the_bootstrap_refusal_still_reads_the_way_the_docs_quote_it(tmp_path, monkeypatch): + """The three things a reader is told to look for, in the raised message.""" + from backend.app.services.credential_crypto import ( + _BOOTSTRAP_ENV_VAR, + MissingCredentialKey, + ) + monkeypatch.delenv(_BOOTSTRAP_ENV_VAR, raising=False) + with pytest.raises(MissingCredentialKey) as exc: + CredentialCrypto(tmp_path / "absent.b64") + message = str(exc.value) + # The sentence README.md reproduces verbatim. + assert "Fernet key file not found at" in message + # The variable both docs tell the operator to set. + assert _BOOTSTRAP_ENV_VAR in message + # The alternative the docs lean on — that a restored instance restores the + # key rather than minting one. Losing this line loses the whole point. + assert "restore the key file" in message + + +def test_the_install_docs_name_the_real_key_path_and_variable(): + """Pins the two literals, read from the code rather than retyped here. + + Changing `_CREDENTIAL_KEY_PATH` or the env var name without updating the + docs fails this — which is the only thing standing between a rename and a + README that sends strangers to a path that does not exist. + """ + from backend.app import _CREDENTIAL_KEY_PATH + from backend.app.services.credential_crypto import _BOOTSTRAP_ENV_VAR + + for name in _INSTALL_DOCS: + text = (_REPO_ROOT / name).read_text() + assert str(_CREDENTIAL_KEY_PATH) in text, f"{name} does not name the key path" + assert _BOOTSTRAP_ENV_VAR in text, f"{name} does not name the bootstrap variable" + + +def test_compose_passes_the_bootstrap_variable_through(): + """The docs say "set it in .env"; that only works if compose forwards it. + + Without this line the instruction is silently inert — the operator sets the + variable, the container never sees it, and the failure is identical to not + having set it at all. + """ + from backend.app.services.credential_crypto import _BOOTSTRAP_ENV_VAR + + compose = (_REPO_ROOT / "docker-compose.yml").read_text() + assert f"{_BOOTSTRAP_ENV_VAR}: ${{{_BOOTSTRAP_ENV_VAR}" in compose