settings: three tiers - wikitool.json (instance), .wikitool-local.json (checkout), secrets by reference incl. 1Password CLI; new command config set (#207)
Every settings field declares its tier; a value in the wrong file is an error naming
file, key and tier. A field naming a program or affecting a gate cannot be declared
instance. Secrets are references only: {"ref": "<name>"} via WIKITOOL_SECRET_<NAME> or
a user-only secrets file, {"ref": "op://..."} via op read (locked 1Password: exit 42).
The preflight records op as an optional tool. doctor gains one line per tier, docs
verify checks a committed wikitool.json for plaintext secrets, CI resolves a real
op:// value through 1password/load-secrets-action. No feature reads its settings
this way yet; without the two files every other command is unchanged.
Files changed:
- .gitea/workflows/ci.yml
- .gitignore
- AGENTS.md
- CHANGES.md
- INSTALL.md
- VERSION
- docs/explanation/README.md
- docs/explanation/configuration-tiers.md
- docs/how-to/README.md
- docs/how-to/use-1password-for-settings.md
- docs/reference/commands/README.md
- docs/reference/commands/configuration.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/types-instructions-and-docs.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/dev/doc-pull-through.md
- instructions/dev/testing-conventions.md
- instructions/preflight.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/config_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/config.py
- tools/chemenu/config_shapes.py
- tools/chemenu/prerequisites.py
- tools/chemenu/settings.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_settings.py
- tools/chemenu/toolpaths.py
- tools/preflight.ps1
- tools/preflight.sh
- tools/prerequisites.txt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
059a0a1840
commit
f6aaadc413
40 files changed
+2246
-31
No files matched your search
@@ -160,6 +160,10 @@ def hermetic_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
for name in (*_WIKITOOL_ENV, *_GIT_ENV):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
# A family rather than a name: `{"ref": "<name>"}` reads
|
||||
# `WIKITOOL_SECRET_<NAME>` for any name, so every one is cleared.
|
||||
for name in [n for n in os.environ if n.startswith(config.ENV_SECRET_PREFIX)]:
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
|
||||
# `.wikitool-tools.json` beside this checkout records where the developer's
|
||||
# git and rg live. A test must not depend on whether the preflight has run
|
||||
|
||||
@@ -202,6 +202,19 @@ def test_plan_ships_no_retired_root_docs(repo):
|
||||
assert "README.md" in plan
|
||||
|
||||
|
||||
def test_plan_ships_neither_settings_file(repo):
|
||||
"""`wikitool.json` belongs to the instance that wrote it - its id, its
|
||||
references - and `.wikitool-local.json` to one checkout. A distribution
|
||||
carries neither; the first `config set` in the new instance creates its own."""
|
||||
(repo / config.INSTANCE_SETTINGS_FILENAME).write_text(
|
||||
'{"schema": 1, "instance_id": "origin-only"}\n', encoding="utf-8")
|
||||
(repo / config.LOCAL_SETTINGS_FILENAME).write_text('{"schema": 1}\n', encoding="utf-8")
|
||||
plan = dist_cmd.build_plan()
|
||||
assert config.INSTANCE_SETTINGS_FILENAME not in plan
|
||||
assert config.LOCAL_SETTINGS_FILENAME not in plan
|
||||
assert "README.md" in plan
|
||||
|
||||
|
||||
def test_plan_never_includes_instructions_dev(repo):
|
||||
"""instructions/dev/ - flat dev-only instructions and the nested skill
|
||||
that switches a session into tool-development mode - is pruned
|
||||
|
||||
@@ -185,6 +185,40 @@ def test_complete_path_records_absolute_paths_and_sets_up_the_venv(machine, shel
|
||||
assert "Preflight passed" in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shell", SHELLS)
|
||||
def test_an_optional_op_is_recorded_when_found(machine, shell):
|
||||
machine.standard()
|
||||
machine.stub("op", "#!/bin/sh\necho '2.30.0'\n")
|
||||
result = machine.run(shell=shell)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert machine.recorded()["tools"]["op"] == str(machine.stubs / "op")
|
||||
assert any(line.split()[:3] == ["OK", "op", "2.30"] for line in result.stdout.splitlines())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shell", SHELLS)
|
||||
def test_an_absent_optional_op_is_no_problem_and_not_recorded(machine, shell):
|
||||
machine.standard()
|
||||
result = machine.run(shell=shell)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "op" not in machine.recorded()["tools"]
|
||||
assert any(line.split()[:2] == ["ABSENT", "op"] for line in result.stdout.splitlines())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shell", SHELLS)
|
||||
def test_set_names_an_optional_tool(machine, shell):
|
||||
machine.standard()
|
||||
elsewhere = machine.stub("op", "#!/bin/sh\necho '2.30.0'\n", where=machine.base / "elsewhere")
|
||||
result = machine.run("--set", f"op={elsewhere}", shell=shell)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert machine.recorded()["tools"]["op"] == str(elsewhere)
|
||||
|
||||
|
||||
def test_the_manifest_names_op_as_optional_only():
|
||||
manifest = prerequisites.load_manifest()
|
||||
assert [tool.name for tool in manifest.optional] == ["op"]
|
||||
assert "op" not in {tool.name for tool in manifest.tools}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shell", SHELLS)
|
||||
def test_second_run_changes_nothing(machine, shell):
|
||||
machine.standard()
|
||||
|
||||
@@ -95,6 +95,24 @@ def test_both_preflights_record_the_same_file(machine):
|
||||
assert machine.tools_file.read_bytes() == from_pwsh
|
||||
|
||||
|
||||
@pytest.mark.skipif(not SHELLS, reason="no POSIX shell on this machine")
|
||||
def test_both_preflights_record_an_optional_op_the_same_way(machine):
|
||||
machine.stub("op", "#!/bin/sh\necho '2.30.0'\n")
|
||||
assert _preflight(machine).returncode == 0
|
||||
from_pwsh = machine.tools_file.read_bytes()
|
||||
assert json.loads(from_pwsh)["tools"]["op"] == str(machine.stubs / "op")
|
||||
machine.tools_file.unlink()
|
||||
assert machine.run(shell=SHELLS[0]).returncode == 0
|
||||
assert machine.tools_file.read_bytes() == from_pwsh
|
||||
|
||||
|
||||
def test_an_absent_optional_op_is_no_problem(machine):
|
||||
result = _preflight(machine)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "op" not in machine.recorded()["tools"]
|
||||
assert any(line.split()[:2] == ["ABSENT", "op"] for line in result.stdout.splitlines())
|
||||
|
||||
|
||||
def test_help_prints_the_usage_and_exits_0(machine):
|
||||
result = _preflight(machine, "--help")
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
"""The tiered settings (`chemenu.settings`), `wikitool config set`, and what
|
||||
`doctor` and `docs verify` report about them.
|
||||
|
||||
No real section ships yet, so every test stands in `TRACKER`, a section with one
|
||||
field of each kind: an instance value, a private checkout value, a secret, a
|
||||
field that names a program, and a plain checkout value. 1Password is a fake `op`
|
||||
script written into the test's own directory - never a real account; the one
|
||||
exception is the CI smoke test at the bottom, which only runs where CI has
|
||||
loaded a real `op://` value into the environment.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import typer
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from chemenu import config, config_shapes, settings, toolpaths
|
||||
from chemenu.cli import app
|
||||
from chemenu.commands import _util, docs_verify, doctor
|
||||
from chemenu.config_shapes import CHECKOUT, INSTANCE, SECRET, Field, Shape
|
||||
from chemenu.errors import ValidationError
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
# Captured at import, before the autouse fixture clears every
|
||||
# `WIKITOOL_SECRET_*` for the test run - see the CI smoke test at the bottom.
|
||||
_LIVE_VALUE = os.environ.get("WIKITOOL_SECRET_CI_SMOKE", "")
|
||||
_LIVE_REQUIRED = os.environ.get("CHEMENU_LIVE_SECRET_REQUIRE", "") == "1"
|
||||
|
||||
needs_posix = pytest.mark.skipif(os.name == "nt", reason="the fake `op` is a POSIX shell script")
|
||||
|
||||
SECRET_VALUE = "s3cr3t-never-printed"
|
||||
PRIVATE_VALUE = "https://tracker.internal.example"
|
||||
|
||||
|
||||
def _positive(value: int):
|
||||
return None if value > 0 else "must be positive"
|
||||
|
||||
|
||||
TRACKER = Shape(
|
||||
key="tracker", title="Test tracker", purpose="A section with one field of each kind.",
|
||||
fields=(
|
||||
Field("stale_days", "positive integer", "Instance threshold.", tier=INSTANCE,
|
||||
json_type="integer", coerce=int, check=_positive, default=14),
|
||||
Field("url", "string", "Private infrastructure.", tier=CHECKOUT, private=True,
|
||||
json_type="string", required=True),
|
||||
Field("token", "string", "A secret.", tier=SECRET, json_type="string"),
|
||||
Field("program", "string", "Names a program, privately.", tier=CHECKOUT, private=True,
|
||||
names_command=True, json_type="string"),
|
||||
Field("label", "string", "Plain checkout value.", tier=CHECKOUT, json_type="string"),
|
||||
),
|
||||
examples=({},),
|
||||
unknown_keys=config_shapes.REJECT,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
tree = tmp_path / "instance"
|
||||
tree.mkdir()
|
||||
monkeypatch.setattr(config, "ROOT", tree)
|
||||
monkeypatch.setattr(config_shapes, "SETTINGS_SECTIONS", (TRACKER,))
|
||||
return tree
|
||||
|
||||
|
||||
def _write(root: Path, instance: dict | None = None, local: dict | None = None) -> None:
|
||||
if instance is not None:
|
||||
(root / config.INSTANCE_SETTINGS_FILENAME).write_text(json.dumps(instance), encoding="utf-8")
|
||||
if local is not None:
|
||||
(root / config.LOCAL_SETTINGS_FILENAME).write_text(json.dumps(local), encoding="utf-8")
|
||||
|
||||
|
||||
def _out(result) -> str:
|
||||
"""The command's output with Rich's line wrapping undone."""
|
||||
return " ".join(result.output.split())
|
||||
|
||||
|
||||
def _squashed(result) -> str:
|
||||
"""The output with every whitespace removed - for asserting a value is
|
||||
absent, where a value Rich wrapped mid-word must still be found."""
|
||||
return "".join(result.output.split())
|
||||
|
||||
|
||||
def _read(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _fake_op(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *, values: dict | None = None,
|
||||
locked: bool = False, hang: bool = False) -> Path:
|
||||
"""An `op` that answers `op read --no-newline <ref>` from `values`, says
|
||||
"not signed in" when `locked`, or never answers when `hang` - recorded in a
|
||||
`.wikitool-tools.json` of the test's own, the way the preflight records it."""
|
||||
lines = ["#!/bin/sh",
|
||||
'[ "$1" = read ] && [ "$2" = --no-newline ] || { echo "[ERROR] bad call: $*" >&2; exit 2; }']
|
||||
if hang:
|
||||
lines.append("sleep 10")
|
||||
if locked:
|
||||
lines += ['echo "[ERROR] 2026/10/10 12:00:00 You are not currently signed in." >&2', "exit 1"]
|
||||
lines.append('case "$3" in')
|
||||
for ref, value in (values or {}).items():
|
||||
lines.append(f" {shlex.quote(ref)}) printf '%s' {shlex.quote(value)}; exit 0 ;;")
|
||||
lines += ["esac",
|
||||
"""echo "[ERROR] 2026/10/10 12:00:00 could not read secret '$3': \\"item\\" isn't an item in the \\"Vault\\" vault" >&2""",
|
||||
"exit 1"]
|
||||
script = tmp_path / "bin" / "op"
|
||||
script.parent.mkdir(exist_ok=True)
|
||||
script.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
script.chmod(0o755)
|
||||
tools = tmp_path / toolpaths.FILE_NAME
|
||||
tools.write_text(json.dumps({"schema": 1, "complete": True, "tools": {"op": str(script)}}), encoding="utf-8")
|
||||
monkeypatch.setattr(toolpaths, "tools_file", lambda: tools)
|
||||
return script
|
||||
|
||||
|
||||
def _command_app(body) -> typer.Typer:
|
||||
"""A one-command app standing in for a future consumer of the settings,
|
||||
which reads them through the same two helpers."""
|
||||
test_app = typer.Typer()
|
||||
|
||||
@test_app.command()
|
||||
def run():
|
||||
body()
|
||||
|
||||
@test_app.callback()
|
||||
def _callback():
|
||||
pass
|
||||
|
||||
return test_app
|
||||
|
||||
|
||||
def _read_token():
|
||||
typer.echo(f"length {len(_util.setting_value(_util.load_settings().section('tracker'), 'token'))}")
|
||||
|
||||
|
||||
# --- declaring a field ----------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flag", ["names_command", "gate"])
|
||||
def test_a_field_that_names_a_program_or_a_gate_cannot_be_declared_instance(flag):
|
||||
with pytest.raises(ValueError, match="never tier 'instance'"):
|
||||
Field("x", "string", "x", tier=INSTANCE, **{flag: True})
|
||||
assert Field("x", "string", "x", tier=CHECKOUT, **{flag: True}).user_only
|
||||
|
||||
|
||||
def test_a_private_field_is_tier_checkout():
|
||||
with pytest.raises(ValueError, match="private field is tier 'checkout'"):
|
||||
Field("x", "string", "x", tier=INSTANCE, private=True)
|
||||
|
||||
|
||||
def test_the_settings_flags_need_a_tier():
|
||||
with pytest.raises(ValueError, match="need a tier"):
|
||||
Field("x", "string", "x", private=True)
|
||||
|
||||
|
||||
def test_an_unknown_tier_is_refused():
|
||||
with pytest.raises(ValueError, match="tier must be one of"):
|
||||
Field("x", "string", "x", tier="global")
|
||||
|
||||
|
||||
def test_every_shipped_settings_section_is_one():
|
||||
for shape in config_shapes.SETTINGS_SECTIONS:
|
||||
assert settings.section_problems(shape) == []
|
||||
# The four per-feature files predate the tiers and declare none.
|
||||
for shape in config_shapes.SHAPES:
|
||||
assert all(not spec.tier for spec in shape.fields)
|
||||
|
||||
|
||||
# --- reading: the right file ----------------------------------------------------
|
||||
|
||||
|
||||
def test_no_files_read_as_nothing_configured(root):
|
||||
found = settings.load()
|
||||
assert not found.instance_present and not found.local_present
|
||||
assert not found.section("tracker").configured
|
||||
assert found.section("tracker")["stale_days"] == 14
|
||||
|
||||
|
||||
def test_values_in_their_own_files_are_read(root):
|
||||
_write(root, {"schema": 1, "instance_id": "abc", "tracker": {"stale_days": 21}},
|
||||
{"tracker": {"url": PRIVATE_VALUE, "label": "mine"}})
|
||||
section = settings.load().section("tracker")
|
||||
assert section["stale_days"] == 21
|
||||
assert section["url"] == PRIVATE_VALUE
|
||||
assert section["label"] == "mine"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("instance,local,file_name,key,tier", [
|
||||
(None, {"tracker": {"stale_days": 7, "url": "x"}}, ".wikitool-local.json", "tracker.stale_days", "instance"),
|
||||
({"tracker": {"label": "mine"}}, {"tracker": {"url": "x"}}, "wikitool.json", "tracker.label", "checkout"),
|
||||
])
|
||||
def test_a_value_in_the_wrong_tier_fails_naming_file_key_and_tier(root, instance, local, file_name, key, tier):
|
||||
_write(root, instance, local)
|
||||
with pytest.raises(ValidationError) as caught:
|
||||
settings.load()
|
||||
assert f"{file_name}: {key} belongs to tier '{tier}'" in str(caught.value)
|
||||
|
||||
result = runner.invoke(_command_app(lambda: _util.load_settings()), ["run"])
|
||||
assert result.exit_code == 1
|
||||
assert f"{file_name}: {key} belongs to tier '{tier}'" in _out(result)
|
||||
|
||||
|
||||
def test_a_value_set_in_both_files_is_an_error_not_an_override(root):
|
||||
_write(root, {"tracker": {"url": {"ref": "tracker_url"}}}, {"tracker": {"url": "x"}})
|
||||
with pytest.raises(ValidationError, match="set in both"):
|
||||
settings.load()
|
||||
|
||||
|
||||
def test_a_program_field_is_never_in_the_committed_file_even_as_a_reference(root):
|
||||
_write(root, {"tracker": {"program": {"ref": "op://V/i/f"}}}, {"tracker": {"url": "x"}})
|
||||
with pytest.raises(ValidationError, match="not even as a reference"):
|
||||
settings.load()
|
||||
|
||||
|
||||
def test_a_reference_where_a_plain_value_belongs_is_refused(root):
|
||||
_write(root, {"tracker": {"stale_days": {"ref": "days"}}}, {"tracker": {"url": "x"}})
|
||||
with pytest.raises(ValidationError, match="takes a plain value, not a reference"):
|
||||
settings.load()
|
||||
|
||||
|
||||
def test_a_missing_required_field_is_reported_unless_a_reference_stands_in(root):
|
||||
_write(root, {"tracker": {"stale_days": 3}})
|
||||
with pytest.raises(ValidationError, match="tracker.url is missing"):
|
||||
settings.load()
|
||||
_write(root, {"tracker": {"stale_days": 3, "url": {"ref": "tracker_url"}}})
|
||||
assert settings.load().section("tracker").refs["url"] == settings.Ref("tracker_url")
|
||||
|
||||
|
||||
def test_an_unknown_section_is_reported_not_refused(root):
|
||||
_write(root, {"newer": {"x": 1}})
|
||||
assert settings.load().unknown == {"wikitool.json": ["newer"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ref", [{"ref": "Upper"}, {"ref": "op://only/two"}, {"ref": 3},
|
||||
{"ref": "x", "extra": 1}])
|
||||
def test_a_malformed_reference_is_refused(root, ref):
|
||||
_write(root, {"tracker": {"token": ref}}, {"tracker": {"url": "x"}})
|
||||
with pytest.raises(ValidationError):
|
||||
settings.load()
|
||||
|
||||
|
||||
# --- reading: no plain secret, no plain private value in the committed file -----
|
||||
|
||||
|
||||
@pytest.mark.parametrize("instance,local", [
|
||||
({"tracker": {"token": SECRET_VALUE}}, {"tracker": {"url": "x"}}),
|
||||
(None, {"tracker": {"url": "x", "token": SECRET_VALUE}}),
|
||||
({"tracker": {"url": SECRET_VALUE}}, None),
|
||||
])
|
||||
def test_a_plain_secret_or_committed_private_value_fails_without_showing_it(root, instance, local):
|
||||
_write(root, instance, local)
|
||||
with pytest.raises(ValidationError) as caught:
|
||||
settings.load()
|
||||
assert SECRET_VALUE not in str(caught.value)
|
||||
|
||||
result = runner.invoke(_command_app(lambda: _util.load_settings()), ["run"])
|
||||
assert result.exit_code == 1
|
||||
assert SECRET_VALUE not in _squashed(result)
|
||||
|
||||
checks = doctor.check_settings()
|
||||
assert any(c.status == "FAIL" for c in checks)
|
||||
assert SECRET_VALUE not in json.dumps([c.__dict__ for c in checks])
|
||||
|
||||
|
||||
def test_docs_verify_flags_a_plain_secret_in_the_committed_file_only(root):
|
||||
_write(root, {"tracker": {"token": SECRET_VALUE, "url": SECRET_VALUE}})
|
||||
problems = docs_verify.check_instance_settings()
|
||||
assert len(problems) == 2
|
||||
assert SECRET_VALUE not in "\n".join(problems)
|
||||
# The local file is never committed, and a required field may live there.
|
||||
_write(root, {"tracker": {"token": {"ref": "tracker_token"}}}, {"tracker": {"token": SECRET_VALUE}})
|
||||
assert docs_verify.check_instance_settings() == []
|
||||
|
||||
|
||||
def test_docs_verify_accepts_an_absent_committed_file(root):
|
||||
assert docs_verify.check_instance_settings() == []
|
||||
|
||||
|
||||
# --- resolving a named reference ------------------------------------------------
|
||||
|
||||
|
||||
def test_a_named_reference_resolves_from_its_variable(root, monkeypatch):
|
||||
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
|
||||
{"tracker": {"url": "x"}})
|
||||
monkeypatch.setenv("WIKITOOL_SECRET_TRACKER_TOKEN", SECRET_VALUE)
|
||||
assert settings.load().section("tracker")["token"] == SECRET_VALUE
|
||||
|
||||
|
||||
def test_without_the_variable_it_resolves_from_the_secrets_file_in_the_user_s_home(root, hermetic_environment):
|
||||
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
|
||||
{"tracker": {"url": "x"}})
|
||||
secrets_file = config.secrets_file("abc")
|
||||
assert secrets_file == hermetic_environment / ".config" / "chemenu" / "abc" / "secrets.json"
|
||||
secrets_file.parent.mkdir(parents=True)
|
||||
secrets_file.write_text(json.dumps({"tracker_token": SECRET_VALUE}), encoding="utf-8")
|
||||
secrets_file.chmod(0o600)
|
||||
assert settings.load().section("tracker")["token"] == SECRET_VALUE
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_a_secrets_file_others_can_read_is_refused(root, hermetic_environment):
|
||||
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
|
||||
{"tracker": {"url": "x"}})
|
||||
secrets_file = config.secrets_file("abc")
|
||||
secrets_file.parent.mkdir(parents=True)
|
||||
secrets_file.write_text(json.dumps({"tracker_token": SECRET_VALUE}), encoding="utf-8")
|
||||
secrets_file.chmod(0o644)
|
||||
with pytest.raises(ValidationError, match="chmod 600"):
|
||||
settings.load().section("tracker")["token"]
|
||||
|
||||
|
||||
def test_an_unresolvable_name_says_where_to_put_the_value(root):
|
||||
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
|
||||
{"tracker": {"url": "x"}})
|
||||
with pytest.raises(ValidationError, match="WIKITOOL_SECRET_TRACKER_TOKEN"):
|
||||
settings.load().section("tracker")["token"]
|
||||
result = runner.invoke(_command_app(_read_token), ["run"])
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
def test_a_reference_is_resolved_on_access_not_on_load(root):
|
||||
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
|
||||
{"tracker": {"url": "x"}})
|
||||
section = settings.load().section("tracker") # no error: nothing resolved yet
|
||||
assert section["url"] == "x"
|
||||
|
||||
|
||||
# --- resolving an op:// reference -----------------------------------------------
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_an_op_reference_resolves_through_op(root, tmp_path, monkeypatch):
|
||||
_fake_op(tmp_path, monkeypatch, values={"op://Vault/tracker/url": PRIVATE_VALUE,
|
||||
"op://Vault/tracker/token": SECRET_VALUE})
|
||||
_write(root, {"tracker": {"url": {"ref": "op://Vault/tracker/url"},
|
||||
"token": {"ref": "op://Vault/tracker/token"}}})
|
||||
section = settings.load().section("tracker")
|
||||
assert section["url"] == PRIVATE_VALUE
|
||||
assert section["token"] == SECRET_VALUE
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_a_locked_op_is_exit_42(root, tmp_path, monkeypatch):
|
||||
_fake_op(tmp_path, monkeypatch, locked=True)
|
||||
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
|
||||
with pytest.raises(settings.SecretSourceLocked):
|
||||
settings.load().section("tracker")["token"]
|
||||
result = runner.invoke(_command_app(_read_token), ["run"])
|
||||
assert result.exit_code == 42
|
||||
assert "NEEDS USER CLEARANCE" in _out(result)
|
||||
assert "op://Vault" not in _squashed(result)
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_an_op_that_does_not_answer_in_time_is_exit_42(root, tmp_path, monkeypatch):
|
||||
_fake_op(tmp_path, monkeypatch, hang=True)
|
||||
monkeypatch.setattr(settings, "OP_TIMEOUT_SECONDS", 1)
|
||||
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
|
||||
result = runner.invoke(_command_app(_read_token), ["run"])
|
||||
assert result.exit_code == 42
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_a_missing_item_is_exit_1_naming_the_reference(root, tmp_path, monkeypatch):
|
||||
_fake_op(tmp_path, monkeypatch, values={})
|
||||
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
|
||||
result = runner.invoke(_command_app(_read_token), ["run"])
|
||||
assert result.exit_code == 1
|
||||
assert "op://Vault/tracker/token" in _out(result)
|
||||
|
||||
|
||||
def test_a_missing_op_is_exit_1_pointing_at_the_preflight(root, tmp_path, monkeypatch):
|
||||
tools = tmp_path / toolpaths.FILE_NAME
|
||||
tools.write_text(json.dumps({"schema": 1, "complete": True, "tools": {}}), encoding="utf-8")
|
||||
monkeypatch.setattr(toolpaths, "tools_file", lambda: tools)
|
||||
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
|
||||
result = runner.invoke(_command_app(_read_token), ["run"])
|
||||
assert result.exit_code == 1
|
||||
assert "preflight" in _out(result)
|
||||
|
||||
|
||||
def test_without_a_tools_file_an_absent_op_is_exit_1_too(root, monkeypatch):
|
||||
monkeypatch.setenv("PATH", "")
|
||||
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
|
||||
result = runner.invoke(_command_app(_read_token), ["run"])
|
||||
assert result.exit_code == 1
|
||||
assert "1Password CLI" in _out(result)
|
||||
|
||||
|
||||
# --- doctor ---------------------------------------------------------------------
|
||||
|
||||
|
||||
def _by_name(checks) -> dict:
|
||||
return {c.name: c for c in checks}
|
||||
|
||||
|
||||
def test_doctor_reports_each_tier_absent_as_ok(root):
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert [c.status for c in checks.values()] == ["OK", "OK", "OK"]
|
||||
assert "absent" in checks["settings-instance"].detail
|
||||
assert "absent" in checks["settings-checkout"].detail
|
||||
assert checks["settings-secrets"].detail == "no references"
|
||||
|
||||
|
||||
def test_doctor_reports_a_malformed_file_as_fail(root):
|
||||
(root / config.INSTANCE_SETTINGS_FILENAME).write_text("{not json", encoding="utf-8")
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert checks["settings-instance"].status == "FAIL"
|
||||
assert checks["settings-checkout"].status == "OK"
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_doctor_reports_references_as_resolvable_or_not_never_their_value(root, tmp_path, monkeypatch):
|
||||
_fake_op(tmp_path, monkeypatch, values={"op://Vault/tracker/url": PRIVATE_VALUE})
|
||||
_write(root, {"instance_id": "abc", "tracker": {"url": {"ref": "op://Vault/tracker/url"},
|
||||
"token": {"ref": "tracker_token"}}})
|
||||
monkeypatch.setenv("WIKITOOL_SECRET_TRACKER_TOKEN", SECRET_VALUE)
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert checks["settings-secrets"].status == "OK"
|
||||
assert checks["settings-secrets"].detail == "2 of 2 reference(s) resolvable"
|
||||
assert "instance id abc" in checks["settings-instance"].detail
|
||||
|
||||
monkeypatch.delenv("WIKITOOL_SECRET_TRACKER_TOKEN")
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert checks["settings-secrets"].status == "FAIL"
|
||||
assert "1 of 2" in checks["settings-secrets"].detail
|
||||
dumped = json.dumps([c.__dict__ for c in checks.values()])
|
||||
assert SECRET_VALUE not in dumped and PRIVATE_VALUE not in dumped
|
||||
|
||||
|
||||
@needs_posix
|
||||
def test_doctor_warns_rather_than_fails_on_a_locked_op(root, tmp_path, monkeypatch):
|
||||
_fake_op(tmp_path, monkeypatch, locked=True)
|
||||
_write(root, {"tracker": {"url": {"ref": "op://Vault/tracker/url"}}})
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert checks["settings-secrets"].status == "WARN"
|
||||
assert "tracker.url" in checks["settings-secrets"].detail
|
||||
|
||||
|
||||
def test_doctor_warns_on_a_section_this_version_does_not_read(root):
|
||||
_write(root, {"newer": {}})
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert checks["settings-instance"].status == "WARN"
|
||||
|
||||
|
||||
# --- config set -----------------------------------------------------------------
|
||||
|
||||
|
||||
def test_config_set_writes_an_instance_value_and_creates_the_file(root):
|
||||
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "21"])
|
||||
assert result.exit_code == 0, result.output
|
||||
data = _read(root / config.INSTANCE_SETTINGS_FILENAME)
|
||||
assert data["tracker"] == {"stale_days": 21}
|
||||
assert data["schema"] == 1
|
||||
assert data["instance_id"]
|
||||
assert "created" in _out(result)
|
||||
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
|
||||
|
||||
instance_id = data["instance_id"]
|
||||
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "30"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert _read(root / config.INSTANCE_SETTINGS_FILENAME)["instance_id"] == instance_id
|
||||
|
||||
|
||||
def test_config_set_refuses_a_plain_secret_without_echoing_it(root):
|
||||
result = runner.invoke(app, ["config", "set", "tracker.token", SECRET_VALUE])
|
||||
assert result.exit_code == 1
|
||||
assert SECRET_VALUE not in _squashed(result)
|
||||
assert not (root / config.INSTANCE_SETTINGS_FILENAME).exists()
|
||||
|
||||
|
||||
def test_config_set_writes_a_secret_reference_into_the_committed_file(root):
|
||||
result = runner.invoke(app, ["config", "set", "tracker.token", "--ref", "tracker_token"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert _read(root / config.INSTANCE_SETTINGS_FILENAME)["tracker"]["token"] == {"ref": "tracker_token"}
|
||||
|
||||
|
||||
def test_config_set_puts_a_private_value_locally_and_a_reference_in_its_place(root):
|
||||
result = runner.invoke(app, ["config", "set", "tracker.url", PRIVATE_VALUE])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert _read(root / config.LOCAL_SETTINGS_FILENAME)["tracker"]["url"] == PRIVATE_VALUE
|
||||
assert PRIVATE_VALUE not in _squashed(result)
|
||||
assert "tracker" not in _read(root / config.INSTANCE_SETTINGS_FILENAME)
|
||||
|
||||
result = runner.invoke(app, ["config", "set", "tracker.url", "--ref", "op://Vault/tracker/url"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "removed from .wikitool-local.json" in _out(result)
|
||||
assert _read(root / config.INSTANCE_SETTINGS_FILENAME)["tracker"]["url"] == {"ref": "op://Vault/tracker/url"}
|
||||
assert "tracker" not in _read(root / config.LOCAL_SETTINGS_FILENAME)
|
||||
|
||||
|
||||
def test_config_set_leaves_a_program_field_to_the_user(root):
|
||||
result = runner.invoke(app, ["config", "set", "tracker.program", "/usr/bin/true"])
|
||||
assert result.exit_code == 42
|
||||
assert ".wikitool-local.json" in _out(result)
|
||||
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [
|
||||
["tracker.nope", "1"],
|
||||
["nope.field", "1"],
|
||||
["nodot", "1"],
|
||||
["tracker.stale_days", "--ref", "days"],
|
||||
["tracker.token", "--ref", "Not-A-Name"],
|
||||
["tracker.stale_days", "many"],
|
||||
["tracker.stale_days", "0"],
|
||||
["tracker.stale_days"],
|
||||
])
|
||||
def test_config_set_refuses_and_writes_nothing(root, args):
|
||||
result = runner.invoke(app, ["config", "set", *args])
|
||||
assert result.exit_code == 1, result.output
|
||||
assert not (root / config.INSTANCE_SETTINGS_FILENAME).exists()
|
||||
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
|
||||
|
||||
|
||||
def test_config_set_refuses_on_top_of_files_that_do_not_pass(root):
|
||||
_write(root, {"tracker": {"token": SECRET_VALUE}})
|
||||
before = (root / config.INSTANCE_SETTINGS_FILENAME).read_text(encoding="utf-8")
|
||||
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "3"])
|
||||
assert result.exit_code == 1
|
||||
assert SECRET_VALUE not in _squashed(result)
|
||||
assert (root / config.INSTANCE_SETTINGS_FILENAME).read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_config_set_with_no_section_registered_names_none(root, monkeypatch):
|
||||
monkeypatch.setattr(config_shapes, "SETTINGS_SECTIONS", ())
|
||||
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "3"])
|
||||
assert result.exit_code == 1
|
||||
assert "none is registered" in _out(result)
|
||||
|
||||
|
||||
# --- the two files in git -------------------------------------------------------
|
||||
|
||||
|
||||
def test_the_repository_ignores_the_local_file_and_tracks_the_committed_one():
|
||||
root = config._PACKAGE_ROOT
|
||||
probe = subprocess.run(
|
||||
["git", "-C", str(root), "check-ignore", "--no-index", "-q", config.LOCAL_SETTINGS_FILENAME],
|
||||
capture_output=True, check=False,
|
||||
)
|
||||
if probe.returncode not in (0, 1):
|
||||
pytest.skip("no git checkout to ask")
|
||||
assert probe.returncode == 0
|
||||
tracked = subprocess.run(
|
||||
["git", "-C", str(root), "check-ignore", "--no-index", "-q", config.INSTANCE_SETTINGS_FILENAME],
|
||||
capture_output=True, check=False,
|
||||
)
|
||||
assert tracked.returncode == 1
|
||||
|
||||
|
||||
# --- CI: a real op:// value -------------------------------------------------------
|
||||
|
||||
|
||||
def test_live_secret_reference_resolves_from_ci(root, monkeypatch):
|
||||
"""CI loads `op://CI-CD/...` through 1Password's own action into
|
||||
`WIKITOOL_SECRET_CI_SMOKE` (.gitea/workflows/ci.yml) and runs this test with
|
||||
`CHEMENU_LIVE_SECRET_REQUIRE=1`, which turns "no value" into a failure. The
|
||||
value is never printed - only that a reference resolved to something."""
|
||||
if not _LIVE_VALUE:
|
||||
if _LIVE_REQUIRED:
|
||||
pytest.fail("CHEMENU_LIVE_SECRET_REQUIRE=1 but WIKITOOL_SECRET_CI_SMOKE is empty - "
|
||||
"the 1Password step did not load it")
|
||||
pytest.skip("no WIKITOOL_SECRET_CI_SMOKE - set only in CI, by the 1Password step")
|
||||
monkeypatch.setenv("WIKITOOL_SECRET_CI_SMOKE", _LIVE_VALUE)
|
||||
_write(root, {"instance_id": "ci", "tracker": {"token": {"ref": "ci_smoke"}}}, {"tracker": {"url": "x"}})
|
||||
assert settings.load().section("tracker")["token"] == _LIVE_VALUE
|
||||
checks = _by_name(doctor.check_settings())
|
||||
assert checks["settings-secrets"].detail == "1 of 1 reference(s) resolvable"
|
||||
Reference in new issue
Block a user