"""nova idp setup tests (REQ-340, REQ-341, C-2.1). Tests: * ``generate_template()`` produces a valid CFN dict with the expected resource types (3 Lambdas, 4 DDB tables, KMS key, 3 URLs, 3 roles). * ``--check`` (mock AWS) → prints a prerequisite report. * ``--dry-run`` → resource summary. * ``--apply`` (mock cloudformation deploy) → prompts + deploys. """ from __future__ import annotations import importlib.util import json import os import sys from pathlib import Path from unittest import mock import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1") os.environ.setdefault("NOVA_LAMBDA_LOCAL_BYPASS", "1") 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 _CFN_PATH = Path(__file__).resolve().parent.parent / "core" / "lambda" / "nova_idp_cfn.py" _SETUP_PATH = Path(__file__).resolve().parent.parent / "core" / "lambda" / "nova_idp_setup.py" cfn = _load("nova_idp_cfn_test", _CFN_PATH) setup = _load("nova_idp_setup_test", _SETUP_PATH) # --------------------------------------------------------------------------- # generate_template # --------------------------------------------------------------------------- def test_generate_template_has_expected_resources(): t = cfn.generate_template() res = t["Resources"] types = [r["Type"] for r in res.values()] assert types.count("AWS::Lambda::Function") == 3 assert types.count("AWS::DynamoDB::Table") == 4 assert types.count("AWS::KMS::Key") == 1 assert types.count("AWS::KMS::Alias") == 1 assert types.count("AWS::Lambda::Url") == 3 assert types.count("AWS::IAM::Role") == 3 def test_generate_template_kms_key_spec(): t = cfn.generate_template() key = t["Resources"]["NovaOidcSigningKey"]["Properties"] assert key["KeySpec"] == "ECC_NIST_P256" assert key["KeyUsage"] == "SIGN_VERIFY" def test_generate_template_jwks_url_auth_none(): """JWKS function URL is AuthType NONE (public, REQ-338).""" t = cfn.generate_template() url = t["Resources"]["NovaIdpJwksUrl"]["Properties"] assert url["AuthType"] == "NONE" def test_generate_template_auth_url_iam(): t = cfn.generate_template() url = t["Resources"]["NovaIdpAuthUrl"]["Properties"] assert url["AuthType"] == "AWS_IAM" def test_generate_template_public_domain_adds_cloudfront(): t = cfn.generate_template(public_jwks_domain="jwks.example.com") types = [r["Type"] for r in t["Resources"].values()] assert "AWS::CloudFront::Distribution" in types assert "AWS::CertificateManager::Certificate" in types def test_resource_summary(): t = cfn.generate_template() s = cfn.resource_summary(t) assert s["AWS::Lambda::Function"] == 3 assert s["AWS::DynamoDB::Table"] == 4 # --------------------------------------------------------------------------- # --check # --------------------------------------------------------------------------- def test_check_prerequisites_returns_report(): with mock.patch("subprocess.check_output", side_effect=Exception("no creds")): report = setup.check_prerequisites() assert "aws_creds" in report assert report["aws_creds"] is False assert "missing" in report assert "iam_delta" in report assert "cloudformation:*" in report["iam_delta"] def test_check_prerequisites_with_creds(): fake = json.dumps({"Account": "123456789012", "UserId": "u", "Arn": "arn"}) with mock.patch("subprocess.check_output", return_value=fake): report = setup.check_prerequisites() assert report["aws_creds"] is True # --------------------------------------------------------------------------- # --dry-run # --------------------------------------------------------------------------- def test_dry_run_returns_summary(): r = setup.generate_and_deploy(dry_run=True) assert r["deployed"] is False assert "AWS::Lambda::Function" in r["summary"] assert r["summary"]["AWS::Lambda::Function"] == 3 # --------------------------------------------------------------------------- # --apply (mock cloudformation deploy) # --------------------------------------------------------------------------- def test_apply_aborts_without_approval(): r = setup.generate_and_deploy(approve_fn=lambda: False) assert r["deployed"] is False def test_apply_deploys_with_approval(): with mock.patch("subprocess.check_call", return_value=0): r = setup.generate_and_deploy(approve_fn=lambda: True) assert r["deployed"] is True def test_apply_deploy_failure_returns_not_deployed(): with mock.patch("subprocess.check_call", side_effect=RuntimeError("cfn error")): r = setup.generate_and_deploy(approve_fn=lambda: True) assert r["deployed"] is False # --------------------------------------------------------------------------- # --verify # --------------------------------------------------------------------------- def test_verify_roundtrip_passes(): r = setup.verify() assert r["passed"] is True # --------------------------------------------------------------------------- # CLI wrapper (nova/idp/setup.py) # --------------------------------------------------------------------------- def test_cli_setup_check(capsys): from nova.idp import setup as cli_setup args = mock.MagicMock() args.check = True; args.apply = False; args.verify = False; args.dry_run = False args.public_jwks_domain = None rc = cli_setup.run(args) assert rc == 0 out = capsys.readouterr().out assert "aws_creds" in out def test_cli_setup_dry_run(capsys): from nova.idp import setup as cli_setup args = mock.MagicMock() args.check = False; args.apply = False; args.verify = False; args.dry_run = True args.public_jwks_domain = None rc = cli_setup.run(args) assert rc == 0 out = capsys.readouterr().out assert "AWS::Lambda::Function" in out def test_cli_setup_verify(capsys): from nova.idp import setup as cli_setup args = mock.MagicMock() args.check = False; args.apply = False; args.verify = True; args.dry_run = False args.public_jwks_domain = None rc = cli_setup.run(args) assert rc == 0