Fix rotation review evidence and reconcile blocked S1 workplans
Some checks failed
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
CI Smoke / source-contract (push) Has been cancelled

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e3b9-b19e-7ba1-9eb4-4faea76af3ea
This commit is contained in:
codex 2026-09-27 18:47:55 +02:00
parent 41f25fe42d
commit 9886567b40
10 changed files with 255 additions and 18 deletions

View file

@ -1,11 +1,17 @@
from __future__ import annotations
import copy
import contextlib
import io
import json
import sys
import tempfile
import unittest
import uuid
from pathlib import Path
from unittest.mock import patch
import yaml
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
@ -13,6 +19,7 @@ sys.path.insert(0, str(ROOT / "scripts"))
from check_secret_paths import is_encrypted_content, is_protected_path # noqa: E402
from s1_receipt import ReceiptError, load_receipt, validate_receipt # noqa: E402
from sops_rotation import rotation_plan # noqa: E402
import sops_rotation # noqa: E402
class SecretAndReceiptContractTests(unittest.TestCase):
@ -74,6 +81,53 @@ class SecretAndReceiptContractTests(unittest.TestCase):
self.assertTrue(plan)
self.assertFalse(any(item["changed"] for item in plan))
def test_rotation_approval_binds_ciphertext(self) -> None:
plan = [{"path": "secrets/example.yaml", "sha256": "a" * 64,
"before_recipients": ["age1before"],
"after_recipients": ["age1after"], "changed": True}]
approval = {"approved": True, "approved_by": "reviewer",
"approved_at": "2026-09-27T00:00:00Z",
"changes": [{k: v for k, v in plan[0].items() if k != "changed"}]}
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "approval.yaml"
path.write_text(yaml.safe_dump(approval))
sops_rotation._load_approval(path, plan)
plan[0]["sha256"] = "b" * 64
with self.assertRaises(sops_rotation.RotationError):
sops_rotation._load_approval(path, plan)
def test_applied_rotation_keeps_reviewed_before_and_after(self) -> None:
before = [{"path": "secrets/example.yaml", "sha256": "a" * 64,
"before_recipients": ["age1before"],
"after_recipients": ["age1after"], "changed": True}]
after = [{"path": "secrets/example.yaml", "sha256": "b" * 64,
"before_recipients": ["age1after"],
"after_recipients": ["age1after"], "changed": False}]
with tempfile.TemporaryDirectory() as tmp:
receipt_path = Path(tmp) / "receipt.json"
with patch.object(sys, "argv", ["rotation", "--apply", "--approval-file",
"approval.yaml", "--receipt", str(receipt_path)]), \
patch.object(sops_rotation, "rotation_plan", side_effect=[before, after]), \
patch.object(sops_rotation, "_load_approval"), \
patch.object(sops_rotation, "_apply"), \
patch.object(sops_rotation, "_verify_decryption", return_value=True), \
contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(sops_rotation.main(), 0)
receipt = json.loads(receipt_path.read_text())
self.assertEqual(receipt["before_recipients"], ["age1before"])
self.assertEqual(receipt["after_recipients"], ["age1after"])
self.assertEqual(receipt["file_metadata"][0]["sha256"], "a" * 64)
self.assertEqual(receipt["file_metadata"][0]["after_sha256"], "b" * 64)
self.assertEqual(json.loads(output.getvalue())["changes"], 1)
def test_default_rotation_output_contains_reviewable_plan(self) -> None:
with patch.object(sys, "argv", ["rotation"]), \
contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(sops_rotation.main(), 0)
payload = json.loads(output.getvalue())
self.assertEqual(payload["plan"], rotation_plan(ROOT))
self.assertFalse(payload["decryption_verified"])
if __name__ == "__main__":
unittest.main()