feat(P04): nova idp setup --check/--apply/--verify (REQ-340/341, C-2.1, backend+cli)
---ci--- project: acdl phase: 4 milestone: v1.28 status: execute persona: backend-engineer ---
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user