secrets-engine/tests/test_multifield_readiness.py
tegwick 3a1bd4f1c8
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Harden secret provisioning and lifecycle controls
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a0217e-8c4c-7383-be6b-f50a6e485306
2026-08-23 12:05:58 +02:00

138 lines
4.2 KiB
Python

import copy
from pathlib import Path
from types import SimpleNamespace
import pytest
from secrets_engine import cli
from secrets_engine.catalog import validate_entry
from secrets_engine.config import Config
from secrets_engine.errors import PolicyGuardError
from secrets_engine.routing import route_lane
from secrets_engine.verify import VerifyResult
from tests.test_catalog import VALID
def _entry():
data = copy.deepcopy(VALID)
data["fields"] = ["api_token", "webhook_secret"]
return validate_entry(data)
class ReadinessClient:
def __init__(self, presence):
self.presence = presence
self.requested_fields = None
def is_reachable(self):
return True
def read_policy(self, _name):
return "path \"secret/data/test/team/thing\" {}"
def approle_exists(self, _name):
return True
def kv_fields_present(self, _mount, _path, fields):
self.requested_fields = list(fields)
return dict(self.presence)
def test_route_requires_every_declared_field_and_names_only_missing_fields(tmp_path):
client = ReadinessClient({"api_token": True, "webhook_secret": False})
result = route_lane(_entry(), hub_url="", repo_root=tmp_path, client=client)
assert client.requested_fields == ["api_token", "webhook_secret"]
assert result.value_present is False
assert result.ready is False
assert result.missing_fields == ["webhook_secret"]
assert result.missing == "provisioned secret fields: webhook_secret"
assert "--field webhook_secret" in result.next_command
def test_route_is_ready_only_when_every_declared_field_is_present(tmp_path):
client = ReadinessClient({"api_token": True, "webhook_secret": True})
result = route_lane(_entry(), hub_url="", repo_root=tmp_path, client=client)
assert result.value_present is True
assert result.missing_fields == []
assert result.ready is True
def _config(tmp_path: Path) -> Config:
return Config(
catalog_dir=tmp_path,
policy_dir=tmp_path,
evidence_dir=tmp_path / "evidence",
hub_url="",
bao_addr="http://127.0.0.1:8200",
topic_id="test-topic",
)
def test_verify_defaults_to_every_declared_field_and_one_path_denial(
tmp_path, monkeypatch
):
entry = _entry()
calls = []
records = []
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
monkeypatch.setattr(cli, "_require_lane_approval", lambda *_args: None)
monkeypatch.setattr(cli.OpenBaoClient, "resolve", lambda *_args, **_kwargs: object())
def fake_verify(_client, _entry, field, *, positive, negative):
calls.append((field, positive, negative))
check = "positive" if positive else "negative"
return [VerifyResult(check, True, {"field": field, "reason": "test"})]
monkeypatch.setattr(cli, "run_verification", fake_verify)
monkeypatch.setattr(
cli,
"_writer",
lambda _cfg: SimpleNamespace(record=lambda *args, **kwargs: records.append((args, kwargs))),
)
args = SimpleNamespace(
catalog_id=entry.id,
bootstrap_token_file=None,
field=None,
positive=False,
negative=False,
)
assert cli.cmd_verify(_config(tmp_path), args) == 0
assert calls == [
("api_token", True, False),
("webhook_secret", True, False),
("api_token", False, True),
]
assert len(records) == 3
def test_live_destroy_fails_before_approval_or_backend_until_action_contract(
tmp_path, monkeypatch
):
entry = _entry()
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
monkeypatch.setattr(
cli,
"_require_lane_approval",
lambda *_args: pytest.fail("coarse lane approval must not authorize destroy"),
)
monkeypatch.setattr(
cli.OpenBaoClient,
"resolve",
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
)
args = SimpleNamespace(
catalog_id=entry.id,
operation="destroy",
dry_run=False,
confirm_destroy=entry.id,
bootstrap_token_file=None,
)
with pytest.raises(PolicyGuardError, match="exact-action destruction approval"):
cli.cmd_lifecycle(_config(tmp_path), args)