932923ee99
Nova Slides Render / render (push) Failing after 22s
---ci--- project: acdl phase: 6 milestone: v1.29 status: complete ---/ci---
217 lines
9.1 KiB
Python
217 lines
9.1 KiB
Python
"""nova idp setup terraform-delegation tests (REQ-369, spec §7.5).
|
|
|
|
P3 Wave 2: verifies the ``nova idp setup --apply`` / ``--verify`` paths
|
|
delegate to ``terraform apply -auto-approve`` / ``terraform plan`` when
|
|
``terraform`` is on PATH, and fall back to the archived CFN path
|
|
(emitting a ``DeprecationWarning``) when terraform is absent.
|
|
|
|
Mirrors the importlib loading + ``mock.patch``/``monkeypatch`` style of
|
|
``tests/test_idp_setup.py`` (``lambda`` is a Python reserved word).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
import warnings
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
|
|
def _load(mod_name, rel_path):
|
|
spec = importlib.util.spec_from_file_location(mod_name, rel_path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
_SETUP_PATH = Path(__file__).resolve().parent.parent / "core" / "lambda" / "nova_idp_setup.py"
|
|
setup = _load("nova_idp_setup_tf_test", _SETUP_PATH)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# core/lambda/nova_idp_setup.py — terraform_apply / terraform_plan
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTerraformApply:
|
|
def test_apply_invokes_terraform_apply_auto_approve(self, monkeypatch):
|
|
"""terraform_apply shells out to ``terraform apply -auto-approve``."""
|
|
called = {}
|
|
|
|
def _fake_run(cmd, **kw):
|
|
called["cmd"] = list(cmd)
|
|
return mock.MagicMock(returncode=0)
|
|
|
|
monkeypatch.setattr(setup.subprocess, "run", _fake_run)
|
|
r = setup.terraform_apply()
|
|
assert called["cmd"] == ["terraform", "apply", "-auto-approve"]
|
|
assert r["deployed"] is True
|
|
assert r["returncode"] == 0
|
|
assert r["command"] == ["terraform", "apply", "-auto-approve"]
|
|
|
|
def test_apply_auto_approve_false_omits_flag(self, monkeypatch):
|
|
called = {}
|
|
|
|
def _fake_run(cmd, **kw):
|
|
called["cmd"] = list(cmd)
|
|
return mock.MagicMock(returncode=0)
|
|
|
|
monkeypatch.setattr(setup.subprocess, "run", _fake_run)
|
|
setup.terraform_apply(auto_approve=False)
|
|
assert called["cmd"] == ["terraform", "apply"]
|
|
|
|
def test_apply_nonzero_returncode_means_not_deployed(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
setup.subprocess, "run", lambda cmd, **kw: mock.MagicMock(returncode=1)
|
|
)
|
|
r = setup.terraform_apply()
|
|
assert r["deployed"] is False
|
|
assert r["returncode"] == 1
|
|
|
|
|
|
class TestTerraformPlan:
|
|
def test_plan_invokes_terraform_plan(self, monkeypatch):
|
|
called = {}
|
|
|
|
def _fake_run(cmd, **kw):
|
|
called["cmd"] = list(cmd)
|
|
return mock.MagicMock(returncode=0)
|
|
|
|
monkeypatch.setattr(setup.subprocess, "run", _fake_run)
|
|
r = setup.terraform_plan()
|
|
assert called["cmd"] == ["terraform", "plan"]
|
|
assert r["passed"] is True
|
|
assert r["command"] == ["terraform", "plan"]
|
|
|
|
def test_plan_nonzero_returncode_means_not_passed(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
setup.subprocess, "run", lambda cmd, **kw: mock.MagicMock(returncode=2)
|
|
)
|
|
r = setup.terraform_plan()
|
|
assert r["passed"] is False
|
|
assert r["returncode"] == 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# generate_and_deploy emits DeprecationWarning (CFN fallback path)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestCfnFallbackDeprecation:
|
|
def test_generate_and_deploy_warns_on_cfn_path(self):
|
|
"""The archived CFN deploy path raises DeprecationWarning (REQ-369)."""
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
with mock.patch("subprocess.check_call", return_value=0):
|
|
r = setup.generate_and_deploy(approve_fn=lambda: True)
|
|
assert r["deployed"] is True
|
|
dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]
|
|
assert len(dep) == 1, f"expected one DeprecationWarning, got {dep}"
|
|
assert "CFN path is archived" in str(dep[0].message)
|
|
assert "docs/archive/nova-idp-cfn-v1.28.md" in str(dep[0].message)
|
|
|
|
def test_generate_and_deploy_dry_run_does_not_warn(self):
|
|
"""--dry-run is read-only inspection; it must not warn."""
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
r = setup.generate_and_deploy(dry_run=True)
|
|
assert r["deployed"] is False
|
|
dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]
|
|
assert dep == [], f"dry-run must not emit DeprecationWarning, got {dep}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# nova/idp/setup.py CLI wrapper — terraform delegation vs CFN fallback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _cli_args(**kw):
|
|
"""Build a MagicMock mimicking the argparse Namespace for `nova idp setup`."""
|
|
a = mock.MagicMock()
|
|
a.check = kw.get("check", False)
|
|
a.apply = kw.get("apply", False)
|
|
a.verify = kw.get("verify", False)
|
|
a.dry_run = kw.get("dry_run", False)
|
|
a.public_jwks_domain = kw.get("public_jwks_domain", None)
|
|
return a
|
|
|
|
|
|
class TestCliApplyDelegation:
|
|
def test_apply_delegates_to_terraform_when_on_path(self, monkeypatch, capsys):
|
|
"""terraform on PATH → --apply runs `terraform apply -auto-approve`."""
|
|
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/terraform" if name == "terraform" else None)
|
|
called = {}
|
|
|
|
def _fake_run(cmd, **kw):
|
|
called["cmd"] = list(cmd)
|
|
return mock.MagicMock(returncode=0)
|
|
|
|
from nova.idp import setup as cli_setup
|
|
monkeypatch.setattr(cli_setup.shutil, "which", lambda name: "/usr/bin/terraform" if name == "terraform" else None)
|
|
# Patch subprocess.run inside the loaded core module (used by terraform_apply).
|
|
monkeypatch.setattr(setup.subprocess, "run", _fake_run)
|
|
rc = cli_setup.run(_cli_args(apply=True))
|
|
assert rc == 0
|
|
assert called["cmd"] == ["terraform", "apply", "-auto-approve"]
|
|
out = capsys.readouterr().out
|
|
assert "deployed" in out
|
|
|
|
def test_apply_falls_back_to_cfn_when_terraform_absent(self, monkeypatch, capsys):
|
|
"""terraform absent → --apply falls back to the CFN path + warns."""
|
|
monkeypatch.setattr("shutil.which", lambda name: None)
|
|
from nova.idp import setup as cli_setup
|
|
monkeypatch.setattr(cli_setup.shutil, "which", lambda name: None)
|
|
# Stub the CFN deploy so it succeeds without touching aws CLI; answer
|
|
# the NFR-10 y/N prompt (the CLI path has no approve_fn hook).
|
|
monkeypatch.setattr("subprocess.check_call", return_value=0)
|
|
monkeypatch.setattr("builtins.input", lambda *a, **kw: "y")
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
rc = cli_setup.run(_cli_args(apply=True))
|
|
assert rc == 0
|
|
dep = [w for w in caught if issubclass(w.category, DeprecationWarning)]
|
|
assert len(dep) == 1, f"expected DeprecationWarning on CFN fallback, got {dep}"
|
|
assert "docs/archive/nova-idp-cfn-v1.28.md" in str(dep[0].message)
|
|
out = capsys.readouterr().out
|
|
assert "AWS::Lambda::Function" in out # CFN resource summary printed
|
|
|
|
|
|
class TestCliVerifyDelegation:
|
|
def test_verify_delegates_to_terraform_plan_when_on_path(self, monkeypatch, capsys):
|
|
"""terraform on PATH → --verify runs `terraform plan`."""
|
|
from nova.idp import setup as cli_setup
|
|
monkeypatch.setattr(cli_setup.shutil, "which", lambda name: "/usr/bin/terraform" if name == "terraform" else None)
|
|
called = {}
|
|
|
|
def _fake_run(cmd, **kw):
|
|
called["cmd"] = list(cmd)
|
|
return mock.MagicMock(returncode=0)
|
|
|
|
monkeypatch.setattr(setup.subprocess, "run", _fake_run)
|
|
rc = cli_setup.run(_cli_args(verify=True))
|
|
assert rc == 0
|
|
assert called["cmd"] == ["terraform", "plan"]
|
|
out = capsys.readouterr().out
|
|
assert "passed" in out
|
|
|
|
def test_verify_falls_back_to_kms_roundtrip_when_terraform_absent(self, monkeypatch, capsys):
|
|
"""terraform absent → --verify falls back to the existing KMS round-trip."""
|
|
from nova.idp import setup as cli_setup
|
|
monkeypatch.setattr(cli_setup.shutil, "which", lambda name: None)
|
|
# The CLI loads core/lambda/nova_idp_setup.py into its own module
|
|
# instance; stub _load_setup so verify() is deterministic and does
|
|
# not require pyjwt/cryptography (the real round-trip is covered by
|
|
# tests/test_idp_setup.py).
|
|
fake_mod = mock.MagicMock()
|
|
fake_mod.verify.return_value = {"passed": True, "detail": "KMS round-trip OK"}
|
|
monkeypatch.setattr(cli_setup, "_load_setup", lambda: fake_mod)
|
|
rc = cli_setup.run(_cli_args(verify=True))
|
|
assert rc == 0
|
|
fake_mod.verify.assert_called_once()
|
|
out = capsys.readouterr().out
|
|
assert "passed" in out # KMS round-trip result printed |