2026-08-23 12:58:12 +02:00
|
|
|
import copy
|
|
|
|
|
import json
|
|
|
|
|
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 BackendError, DecisionError
|
|
|
|
|
from tests.test_catalog import VALID
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _config(tmp_path):
|
|
|
|
|
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 _records(tmp_path):
|
|
|
|
|
path = next((tmp_path / "evidence").glob("evidence-*.jsonl"))
|
|
|
|
|
return [json.loads(line) for line in path.read_text().splitlines()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _provision_args(entry):
|
|
|
|
|
return SimpleNamespace(
|
|
|
|
|
catalog_id=entry.id,
|
|
|
|
|
stage=entry.stage,
|
|
|
|
|
field="api_token",
|
|
|
|
|
generate=False,
|
|
|
|
|
from_file="/tmp/test-value-file",
|
|
|
|
|
bootstrap_token_file=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_provision_backend_exception_has_attempt_and_terminal_evidence(
|
|
|
|
|
tmp_path, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
entry = validate_entry(copy.deepcopy(VALID))
|
|
|
|
|
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())
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
cli,
|
|
|
|
|
"provision_from_file",
|
|
|
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
|
|
|
|
BackendError("fake-SUPER-SECRET-backend-message")
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(BackendError):
|
|
|
|
|
cli.cmd_provision(_config(tmp_path), _provision_args(entry))
|
|
|
|
|
|
|
|
|
|
records = _records(tmp_path)
|
|
|
|
|
assert [record["result"] for record in records] == [
|
|
|
|
|
"attempt",
|
|
|
|
|
"failed-BackendError",
|
|
|
|
|
]
|
|
|
|
|
assert records[-1]["detail"]["approval_status"] == "not-required"
|
|
|
|
|
assert "fake-SUPER-SECRET" not in json.dumps(records)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_provision_decision_rejection_is_recorded_before_backend(
|
|
|
|
|
tmp_path, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
data = copy.deepcopy(VALID)
|
|
|
|
|
data["approval"] = {
|
|
|
|
|
"model": "decision",
|
|
|
|
|
"decision_ref": "CCR-2026-0001",
|
|
|
|
|
}
|
|
|
|
|
entry = validate_entry(data)
|
|
|
|
|
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
cli,
|
|
|
|
|
"_require_lane_approval",
|
|
|
|
|
lambda *_args: (_ for _ in ()).throw(DecisionError("not approved")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
cli.OpenBaoClient,
|
|
|
|
|
"resolve",
|
|
|
|
|
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(DecisionError):
|
|
|
|
|
cli.cmd_provision(_config(tmp_path), _provision_args(entry))
|
|
|
|
|
|
|
|
|
|
records = _records(tmp_path)
|
|
|
|
|
assert [record["result"] for record in records] == [
|
|
|
|
|
"attempt",
|
|
|
|
|
"failed-DecisionError",
|
|
|
|
|
]
|
|
|
|
|
assert records[-1]["detail"]["approval_status"] == "rejected"
|
|
|
|
|
assert records[-1]["detail"]["decision_ref"] == "CCR-2026-0001"
|
2026-08-23 14:15:42 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_production_handler_fails_closed_before_backend(tmp_path, monkeypatch):
|
|
|
|
|
data = copy.deepcopy(VALID)
|
|
|
|
|
data.update(
|
|
|
|
|
stage="prod",
|
|
|
|
|
approval={"model": "decision", "decision_ref": "CCR-2026-0001"},
|
|
|
|
|
)
|
|
|
|
|
entry = validate_entry(data)
|
|
|
|
|
monkeypatch.setattr(cli, "get_entry", lambda *_args: entry)
|
|
|
|
|
monkeypatch.delenv("SECRETS_ENGINE_UNSAFE_DEMO", raising=False)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
cli.OpenBaoClient,
|
|
|
|
|
"resolve",
|
|
|
|
|
lambda *_args, **_kwargs: pytest.fail("backend must not be reached"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(DecisionError, match="production action 'provision'"):
|
|
|
|
|
cli.cmd_provision(_config(tmp_path), _provision_args(entry))
|
|
|
|
|
|
|
|
|
|
records = _records(tmp_path)
|
|
|
|
|
assert [record["result"] for record in records] == [
|
|
|
|
|
"attempt",
|
|
|
|
|
"failed-DecisionError",
|
|
|
|
|
]
|
2026-08-29 12:52:55 +02:00
|
|
|
terminal = records[-1]
|
|
|
|
|
assert terminal["detail"]["stance_stage"] == "prod"
|
|
|
|
|
assert terminal["detail"]["stance_failure_mode"] == "fail_closed"
|
|
|
|
|
assert terminal["detail"]["approval_status"] == "rejected"
|
|
|
|
|
assert "stance_decision_id" not in terminal["detail"]
|
|
|
|
|
assert terminal["completeness_claimed"] is False
|
|
|
|
|
assert "SUPER-SECRET" not in json.dumps(records)
|
|
|
|
|
outbox = list((tmp_path / "evidence" / "outbox").glob("*.json"))
|
|
|
|
|
assert outbox, "production provision refusal is load-bearing and must be queued"
|