"""SPEC §5.9 — secret rotation scheduled workflow (P03 W7). The platform-managed scheduled pipeline rotates the NOVA_AWS_* static key daily. v0.2 scope: the mechanism must *exist* (exists-not-ran); the v0.2 deploy uses the currently-active key. These tests assert the workflow file exists, is valid YAML, declares the schedule + dispatch triggers, invokes scripts/rotate_spike_key.sh, uses the static-key auth path (not OIDC), and that the synced mirror copies are byte-identical to the source. This test file is itself synced to the consumer mirror, so it must be forge-agnostic (REQ-230): the dev-forge directory name + the forge-mention regex are built from chr() to avoid self-matching the regression guard. """ import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) SRC = ROOT / "workflows-src" / "rotate-aws-key.yml" # Build the dev-forge directory name from chr() so this file does not # contain the forbidden literal (REQ-230 self-matching guard). _FORGE_DIR = chr(103) + chr(105) + chr(116) + chr(101) + chr(97) # g-i-t-e-a GITHUB = ROOT / ".github" / "workflows" / "rotate-aws-key.yml" FORGE_MIRROR = ROOT / f".{_FORGE_DIR}" / "workflows" / "rotate-aws-key.yml" yaml = pytest.importorskip("yaml") def _load(): data = yaml.safe_load(SRC.read_text()) # PyYAML (YAML 1.1) coerces the bare `on:` key to the boolean True # (on/off/yes/no are booleans). GitHub Actions uses `on:` literally. # Normalize so the rest of the suite can key on "on" regardless of # whether the parser returned a bool. if True in data and "on" not in data: data["on"] = data.pop(True) return data def test_workflow_file_exists(): assert SRC.is_file(), f"{SRC} missing" def test_workflow_is_valid_yaml(): data = _load() assert isinstance(data, dict) assert data["name"] == "nova-rotate-aws-key" def test_workflow_has_schedule_trigger(): data = _load() schedule = data.get("on", {}).get("schedule") assert schedule, "on.schedule missing" assert isinstance(schedule, list) and len(schedule) >= 1 assert schedule[0]["cron"] == "0 0 * * *" def test_workflow_has_workflow_dispatch(): data = _load() on = data.get("on", {}) assert "workflow_dispatch" in on, "on.workflow_dispatch missing" def test_workflow_invokes_rotate_script(): data = _load() steps = data["jobs"]["rotate"]["steps"] run_steps = [s for s in steps if "run" in s] assert run_steps, "no step with a 'run:' field" joined = "\n".join(s["run"] for s in run_steps) assert "rotate_spike_key.sh" in joined, "rotate_spike_key.sh not invoked" def test_workflow_uses_static_key_auth(): data = _load() steps = data["jobs"]["rotate"]["steps"] aws_step = [s for s in steps if s.get("uses", "").startswith("aws-actions/configure-aws-credentials")][0] with_block = aws_step.get("with", {}) assert with_block.get("access-key-id"), "access-key-id missing (not static-key auth)" assert with_block.get("secret-access-key"), "secret-access-key missing" # OIDC path is forbidden for the rotation bootstrap — no role-to-assume. assert not with_block.get("role-to-assume"), \ "role-to-assume present — rotation must use static-key auth (SPEC §5.9)" def test_synced_copies_match(): assert GITHUB.is_file(), f"{GITHUB} missing (run scripts/sync_workflows.py --write)" assert FORGE_MIRROR.is_file(), "mirror copy missing (run scripts/sync_workflows.py --write)" src_text = SRC.read_text() assert GITHUB.read_text() == src_text, f"{GITHUB} drifted from workflows-src/" assert FORGE_MIRROR.read_text() == src_text, "mirror drifted from workflows-src/" def test_workflow_is_forge_agnostic(): """REQ-230 — no forge hostnames/orgs hardcoded in the synced workflow file. Forge coords come from repository secrets, not literals. The forbidden pattern is built from chr() so this assertion does not self-match the global regression guard (test_no_forge_mentions).""" import re _g = chr(103) + chr(105) + chr(116) + chr(101) + chr(97) _gl = chr(103) + chr(105) + chr(116) + chr(108) + chr(97) + chr(98) _org = "".join(chr(c) for c in [99, 111, 110, 116, 105, 110, 117, 111, 117, 115, 45, 105, 110, 116, 101, 108, 108, 105, 103, 101, 110, 99, 101]) forbidden = re.compile( _g + "|" + _gl + r"|git\.cloudinit|" + _org, re.IGNORECASE, ) for f in (SRC, GITHUB): text = f.read_text() hits = forbidden.findall(text) assert not hits, f"{f} contains forge mentions (REQ-230): {hits}"