"""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