| """Unit tests for cdk_install.py (no live AWS or cdk deploy).""" |
|
|
| import argparse |
| import json |
| import os |
| import subprocess |
| import sys |
| from pathlib import Path |
|
|
| import pytest |
|
|
| CDK_DIR = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(CDK_DIR)) |
|
|
| import cdk_install as inst |
|
|
|
|
| def _demo_answers() -> inst.InstallAnswers: |
| return inst.InstallAnswers( |
| profile="demo", |
| aws_account_id="123456789012", |
| aws_region="eu-west-2", |
| cdk_prefix="Test-Redaction-", |
| cognito_domain_prefix="test-redaction", |
| vpc_mode="existing", |
| vpc_name="test-vpc", |
| public_subnet_mode="auto", |
| private_subnet_mode="auto", |
| ) |
|
|
|
|
| def _production_answers() -> inst.InstallAnswers: |
| a = _demo_answers() |
| a.profile = "production" |
| a.acm_cert_arn = "arn:aws:acm:eu-west-2:123:certificate/abc" |
| a.ssl_domain = "redaction.example.com" |
| a.cloudfront_geo = "GB" |
| return a |
|
|
|
|
| def _headless_answers() -> inst.InstallAnswers: |
| return inst.InstallAnswers( |
| profile="headless", |
| aws_account_id="123456789012", |
| aws_region="eu-west-2", |
| cdk_prefix="Headless-Redaction-", |
| cognito_domain_prefix="headless-redaction", |
| vpc_mode="existing", |
| vpc_name="test-vpc", |
| public_subnet_mode="auto", |
| private_subnet_mode="auto", |
| enable_s3_batch=True, |
| ) |
|
|
|
|
| def test_default_cognito_domain_prefix_strips_reserved_words_from_cdk_prefix(): |
| prefix = inst.default_cognito_domain_prefix_from_cdk_prefix( |
| "Lambeth-AWS-SharedServices-Dev-Redaction-" |
| ) |
| assert prefix == "lambeth-sharedservic" |
| assert "aws" not in prefix |
| assert "amazon" not in prefix |
| assert "cognito" not in prefix |
|
|
|
|
| def test_sanitize_cognito_domain_prefix_removes_reserved_substrings(): |
| assert ( |
| inst.sanitize_cognito_domain_prefix("lambeth-aws-sharedse") |
| == "lambeth-sharedse" |
| ) |
| assert inst.sanitize_cognito_domain_prefix("my-cognito-login") == "my-login" |
| assert ( |
| inst.sanitize_cognito_domain_prefix("amazon-redaction-dev") == "redaction-dev" |
| ) |
| assert inst.sanitize_cognito_domain_prefix("aws") == "redaction-app" |
|
|
|
|
| def test_validate_cognito_domain_prefix_rejects_reserved_words(): |
| assert inst.validate_cognito_domain_prefix("lambeth-aws-sharedse") is not None |
| assert inst.validate_cognito_domain_prefix("lambeth-sharedse-dev") is None |
|
|
|
|
| def test_build_env_values_demo(): |
| values = inst.build_env_values(_demo_answers()) |
| assert values["USE_ECS_EXPRESS_MODE"] == "True" |
| assert values["ECS_EXPRESS_USE_PUBLIC_SUBNETS"] == "True" |
| assert values["PRIVATE_SUBNETS_TO_USE"] == "" |
| assert values["USE_CLOUDFRONT"] == "True" |
| assert values["CLOUDFRONT_AUTH_MODE"] == "magic-link" |
| assert values["RESTRICT_ALB_INGRESS_TO_CLOUDFRONT"] == "True" |
| assert values["ENABLE_APPREGISTRY"] == "False" |
| assert values["RUN_USEAST_STACK"] == "False" |
| assert values["ENABLE_RESOURCE_DELETE_PROTECTION"] == "False" |
| assert values["VPC_NAME"] == "test-vpc" |
| assert values["CONTEXT_FILE"] == "precheck.context.json" |
| assert values["CDK_FOLDER"].endswith("/cdk/") |
|
|
|
|
| def test_build_env_values_production(): |
| values = inst.build_env_values(_production_answers()) |
| assert values["USE_ECS_EXPRESS_MODE"] == "False" |
| assert values["USE_CLOUDFRONT"] == "True" |
| assert values["RUN_USEAST_STACK"] == "False" |
| assert values["ENABLE_RESOURCE_DELETE_PROTECTION"] == "True" |
| assert values["ACM_SSL_CERTIFICATE_ARN"].startswith("arn:aws:acm:") |
| assert values["SSL_CERTIFICATE_DOMAIN"] == "redaction.example.com" |
| |
| |
| assert values["ENABLE_APPREGISTRY"] == "False" |
| assert "APPREGISTRY_STACK_NAME" not in values |
|
|
|
|
| def test_build_env_values_appregistry_opt_in_sets_stack_name(): |
| answers = _production_answers() |
| answers.custom_overrides["ENABLE_APPREGISTRY"] = "True" |
| values = inst.build_env_values(answers) |
| assert values["ENABLE_APPREGISTRY"] == "True" |
| assert values["APPREGISTRY_STACK_NAME"] == "Test-Redaction-AppRegistryStack" |
|
|
|
|
| def test_build_env_values_uses_custom_s3_bucket_names(): |
| answers = _demo_answers() |
| answers.s3_log_bucket_name = "063418083240-demo-redaction-s3-logs" |
| answers.s3_output_bucket_name = "063418083240-demo-redaction-s3-output" |
| answers.s3_malware_scan_bucket_name = "063418083240-demo-redaction-s3-malware-scan" |
| values = inst.build_env_values(answers) |
| assert values["S3_LOG_CONFIG_BUCKET_NAME"] == "063418083240-demo-redaction-s3-logs" |
| assert values["S3_OUTPUT_BUCKET_NAME"] == "063418083240-demo-redaction-s3-output" |
| assert ( |
| values["S3_MALWARE_SCAN_BUCKET_NAME"] |
| == "063418083240-demo-redaction-s3-malware-scan" |
| ) |
|
|
|
|
| def test_derive_s3_bucket_names_includes_malware_scan_bucket(): |
| names = inst.derive_s3_bucket_names("demo-redaction") |
| assert names["S3_MALWARE_SCAN_BUCKET_NAME"] == "demo-redactions3-malware-scan" |
|
|
|
|
| def test_normalize_s3_bucket_name_truncates_and_sanitizes(): |
| long_name = "A" * 80 + "/underscores" |
| normalized = inst.normalize_s3_bucket_name(long_name) |
| assert normalized == normalized.lower() |
| assert len(normalized) <= inst.S3_BUCKET_NAME_MAX_LEN |
| assert "_" not in normalized |
|
|
|
|
| def test_normalize_agentcore_runtime_url_strips_invocations_suffix(): |
| assert ( |
| inst.normalize_agentcore_runtime_url("https://runtime.example/invocations") |
| == "https://runtime.example" |
| ) |
| assert ( |
| inst.normalize_agentcore_runtime_url("https://runtime.example/invocations/") |
| == "https://runtime.example" |
| ) |
| assert ( |
| inst.normalize_agentcore_runtime_url("https://runtime.example/") |
| == "https://runtime.example" |
| ) |
|
|
|
|
| def test_suggest_available_s3_bucket_name_prefers_account_prefix(monkeypatch): |
| calls: list[str] = [] |
|
|
| def fake_resolve(name: str, **kwargs): |
| calls.append(name) |
| if name.startswith("123456789012-"): |
| return ("available", name) |
| return ("globally_taken", name) |
|
|
| monkeypatch.setattr("cdk_functions.resolve_s3_bucket_availability", fake_resolve) |
| suggested = inst.suggest_available_s3_bucket_name( |
| "demo-redaction-s3-logs", "123456789012" |
| ) |
| assert suggested == "123456789012-demo-redaction-s3-logs" |
| assert calls[0] == "demo-redaction-s3-logs" |
|
|
|
|
| def test_validate_globally_unique_env_values_flags_taken_bucket(monkeypatch): |
| monkeypatch.setattr( |
| "cdk_functions.resolve_s3_bucket_availability", |
| lambda name, **kwargs: ("globally_taken", name), |
| ) |
| values = inst.build_env_values(_demo_answers()) |
| errors = inst.validate_globally_unique_env_values(values) |
| assert any("taken globally" in err for err in errors) |
|
|
|
|
| def test_prompt_globally_unique_cognito_prefix_suggests_when_taken(monkeypatch): |
| monkeypatch.setattr( |
| "cdk_functions.resolve_cognito_domain_prefix_availability", |
| lambda prefix, **kwargs: "taken" if prefix == "demo-redaction" else "available", |
| ) |
| monkeypatch.setattr( |
| inst, |
| "suggest_available_cognito_domain_prefix", |
| lambda preferred, account_id, region, **kwargs: "063418083240-demo-redaction", |
| ) |
| monkeypatch.setattr(inst, "ask_yes_no", lambda *args, **kwargs: True) |
| result = inst._prompt_globally_unique_cognito_prefix( |
| "demo-redaction", |
| "063418083240", |
| "eu-west-2", |
| interactive=True, |
| assume_yes=False, |
| ) |
| assert result == "063418083240-demo-redaction" |
|
|
|
|
| def test_prompt_globally_unique_cognito_prefix_rejects_taken_cli_override(monkeypatch): |
| monkeypatch.setattr( |
| "cdk_functions.resolve_cognito_domain_prefix_availability", |
| lambda prefix, **kwargs: "taken", |
| ) |
| with pytest.raises(SystemExit): |
| inst._prompt_globally_unique_cognito_prefix( |
| "demo-redaction", |
| "063418083240", |
| "eu-west-2", |
| interactive=False, |
| assume_yes=False, |
| cli_override="demo-redaction", |
| ) |
|
|
|
|
| def test_subnet_cidr_prefix_len_for_express_demo(): |
| answers = _demo_answers() |
| assert inst.subnet_cidr_prefix_len_for_tier(answers, "public") == 27 |
| assert inst.subnet_cidr_prefix_len_for_tier(answers, "private") == 28 |
|
|
|
|
| def test_validate_public_subnet_cidr_for_express_rejects_small_blocks(): |
| assert inst.validate_public_subnet_cidr_for_express("10.0.0.0/28") is not None |
| assert inst.validate_public_subnet_cidr_for_express("10.0.0.0/27") is None |
| assert inst.validate_public_subnet_cidr_for_express("10.0.0.0/26") is None |
|
|
|
|
| def test_validate_env_values_rejects_express_public_subnets_too_small(): |
| values = inst.build_env_values(_demo_answers()) |
| values["PUBLIC_SUBNET_CIDR_BLOCKS"] = "['10.0.0.0/28', '10.0.0.16/28']" |
| errors = inst.validate_env_values(values) |
| assert any("too small for ECS Express" in err for err in errors) |
|
|
|
|
| def test_build_env_values_headless(): |
| values = inst.build_env_values(_headless_answers()) |
| assert values["USE_ECS_EXPRESS_MODE"] == "False" |
| assert values["USE_CLOUDFRONT"] == "False" |
| assert values["RUN_USEAST_STACK"] == "False" |
| assert values["ENABLE_HEADLESS_DEPLOYMENT"] == "True" |
| assert values["ENABLE_S3_BATCH_ECS_TRIGGER"] == "True" |
| assert values["COGNITO_AUTH"] == "False" |
| assert values["ECS_EXPRESS_USE_PUBLIC_SUBNETS"] == "True" |
| assert values["PRIVATE_SUBNETS_TO_USE"] == "" |
| assert values["S3_LOG_CONFIG_BUCKET_NAME"] == "headless-redaction-s3-logs" |
| assert values["S3_OUTPUT_BUCKET_NAME"] == "headless-redaction-s3-output" |
|
|
|
|
| def test_validate_env_values_rejects_bare_s3_bucket_names(): |
| values = inst.build_env_values(_headless_answers()) |
| values["S3_LOG_CONFIG_BUCKET_NAME"] = "s3-logs" |
| errors = inst.validate_env_values(values) |
| assert any("S3_LOG_CONFIG_BUCKET_NAME" in e for e in errors) |
|
|
|
|
| def test_headless_profile_uses_public_subnets_only(): |
| assert inst.answers_use_public_subnets_only(_headless_answers()) is True |
|
|
|
|
| def test_build_env_values_production_headless(): |
| answers = _production_answers() |
| answers.enable_headless = True |
| values = inst.build_env_values(answers) |
| assert values["USE_ECS_EXPRESS_MODE"] == "False" |
| assert values["USE_CLOUDFRONT"] == "False" |
| assert values["ENABLE_HEADLESS_DEPLOYMENT"] == "True" |
| assert values["ENABLE_RESOURCE_DELETE_PROTECTION"] == "True" |
| assert values.get("ECS_EXPRESS_USE_PUBLIC_SUBNETS") != "True" |
| assert "PRIVATE_SUBNET_CIDR_BLOCKS" in values |
|
|
|
|
| def test_validate_install_answers_rejects_demo_headless(): |
| answers = _demo_answers() |
| answers.enable_headless = True |
| errors = inst.validate_install_answers(answers) |
| assert any("Demonstration" in err for err in errors) |
|
|
|
|
| def test_validate_install_answers_rejects_custom_express_headless(): |
| answers = inst.InstallAnswers(profile="custom", enable_headless=True) |
| answers.custom_overrides["USE_ECS_EXPRESS_MODE"] = "True" |
| errors = inst.validate_install_answers(answers) |
| assert any("USE_ECS_EXPRESS_MODE=False" in err for err in errors) |
|
|
|
|
| def test_profile_allows_headless_add_on(): |
| assert inst.profile_allows_headless_add_on(_demo_answers()) is False |
| assert inst.profile_allows_headless_add_on(_production_answers()) is True |
| assert inst.profile_allows_headless_add_on(_headless_answers()) is True |
| custom = inst.InstallAnswers(profile="custom") |
| assert inst.profile_allows_headless_add_on(custom) is True |
| custom.custom_overrides["USE_ECS_EXPRESS_MODE"] = "True" |
| assert inst.profile_allows_headless_add_on(custom) is False |
|
|
|
|
| def test_validate_install_answers_skips_cognito_prefix_for_headless(): |
| values = inst.build_env_values(_headless_answers()) |
| values["COGNITO_USER_POOL_DOMAIN_PREFIX"] = "" |
| assert inst.validate_env_values(values) == [] |
|
|
|
|
| def test_validate_headless_rejects_pi(): |
| values = inst.build_env_values(_headless_answers()) |
| values["ENABLE_ECS_SERVICE_CONNECT"] = "True" |
| errors = inst.validate_env_values(values) |
| assert any("HEADLESS" in e for e in errors) |
|
|
|
|
| def test_validate_rejects_express_with_acm(): |
| values = inst.build_env_values(_demo_answers()) |
| values["ACM_SSL_CERTIFICATE_ARN"] = "arn:aws:acm:eu-west-2:123:certificate/x" |
| errors = inst.validate_env_values(values) |
| assert any("ACM_SSL_CERTIFICATE_ARN" in e for e in errors) |
|
|
|
|
| def test_build_env_values_mixed_subnet_tiers(): |
| answers = _production_answers() |
| answers.public_subnet_mode = "existing" |
| answers.private_subnet_mode = "create" |
| answers.public_subnet_names = ["existing-public-a", "existing-public-b"] |
| answers.public_subnet_cidrs = ["10.244.1.0/28", "10.244.2.0/28"] |
| answers.public_subnet_azs = ["eu-west-2a", "eu-west-2b"] |
| answers.private_subnet_names = ["Demo-Redaction-PrivateSubnet1"] |
| answers.private_subnet_cidrs = ["10.0.10.0/28"] |
| answers.private_subnet_azs = ["eu-west-2a"] |
| values = inst.build_env_values(answers) |
| assert ( |
| values["PUBLIC_SUBNETS_TO_USE"] == '["existing-public-a", "existing-public-b"]' |
| ) |
| assert values["PUBLIC_SUBNET_CIDR_BLOCKS"] == "['10.244.1.0/28', '10.244.2.0/28']" |
| assert values["PUBLIC_SUBNET_AVAILABILITY_ZONES"] == "['eu-west-2a', 'eu-west-2b']" |
| assert values["PRIVATE_SUBNETS_TO_USE"] == '["Demo-Redaction-PrivateSubnet1"]' |
| assert values["PRIVATE_SUBNET_CIDR_BLOCKS"] == "['10.0.10.0/28']" |
| assert values["PRIVATE_SUBNET_AVAILABILITY_ZONES"] == "['eu-west-2a']" |
|
|
|
|
| def test_enrich_existing_subnet_details_from_aws(monkeypatch): |
| answers = _demo_answers() |
| answers.vpc_name = "test-vpc" |
| answers.public_subnet_mode = "existing" |
| answers.public_subnet_names = ["pub-a", "pub-b"] |
| answers.private_subnet_mode = "create" |
|
|
| monkeypatch.setattr( |
| inst, |
| "list_vpcs", |
| lambda _region: [{"name": "test-vpc", "id": "vpc-123", "cidr": "10.0.0.0/16"}], |
| ) |
| monkeypatch.setattr( |
| inst, |
| "list_subnets_in_vpc", |
| lambda _vpc_id, _region: [ |
| {"name": "pub-a", "cidr": "10.244.1.0/28", "az": "eu-west-2a"}, |
| {"name": "pub-b", "cidr": "10.244.2.0/28", "az": "eu-west-2b"}, |
| ], |
| ) |
|
|
| errors = inst.enrich_existing_subnet_details_from_aws(answers) |
| assert errors == [] |
| assert answers.public_subnet_cidrs == ["10.244.1.0/28", "10.244.2.0/28"] |
| assert answers.public_subnet_azs == ["eu-west-2a", "eu-west-2b"] |
|
|
|
|
| def test_validate_subnet_answers_mixed_requires_public_names(): |
| answers = _demo_answers() |
| answers.public_subnet_mode = "existing" |
| answers.private_subnet_mode = "create" |
| answers.private_subnet_names = ["new-private"] |
| errors = inst.validate_subnet_answers(answers) |
| assert any("Public subnets" in err for err in errors) |
|
|
|
|
| def test_resolve_subnet_tier_modes_per_tier_override(): |
| args = argparse.Namespace( |
| subnet_mode="auto", |
| public_subnet_mode="existing", |
| private_subnet_mode="create", |
| ) |
| public, private = inst.resolve_subnet_tier_modes(args) |
| assert public == "existing" |
| assert private == "create" |
|
|
|
|
| def test_suggest_vpc_cidr_block_empty_region(): |
| assert inst.suggest_vpc_cidr_block([]) == "10.0.0.0/24" |
|
|
|
|
| def test_suggest_vpc_cidr_block_skips_overlaps(): |
| assert inst.suggest_vpc_cidr_block(["10.0.0.0/24"]) == "10.0.1.0/24" |
| assert inst.suggest_vpc_cidr_block(["10.0.0.0/16"]) == "10.1.0.0/24" |
|
|
|
|
| def test_validate_new_vpc_cidr_rejects_overlap_and_public_space(): |
| err = inst.validate_new_vpc_cidr("10.0.0.0/24", ["10.0.0.0/16"]) |
| assert err is not None |
| assert "overlaps" in err |
| err = inst.validate_new_vpc_cidr("8.8.8.0/24", []) |
| assert err is not None |
| assert "RFC1918" in err |
|
|
|
|
| def test_validate_new_vpc_cidr_accepts_available_block(): |
| assert inst.validate_new_vpc_cidr("10.2.0.0/24", ["10.0.0.0/16"]) is None |
|
|
|
|
| def test_prompt_new_vpc_cidr_validates_cli_override(monkeypatch): |
| answers = inst.InstallAnswers(aws_region="eu-west-2", new_vpc_cidr="10.0.0.0/24") |
| monkeypatch.setattr( |
| inst, "list_vpc_cidr_blocks_in_region", lambda _r: ["10.0.0.0/16"] |
| ) |
| with pytest.raises(SystemExit): |
| inst.prompt_new_vpc_cidr(answers, interactive=False) |
|
|
|
|
| def test_prompt_new_vpc_cidr_auto_select_noninteractive(monkeypatch): |
| answers = inst.InstallAnswers(aws_region="eu-west-2") |
| monkeypatch.setattr(inst, "list_vpc_cidr_blocks_in_region", lambda _r: []) |
| inst.prompt_new_vpc_cidr(answers, interactive=False) |
| assert answers.new_vpc_cidr == "10.0.0.0/24" |
|
|
|
|
| def test_suggest_subnet_cidr_blocks_lowest_available(): |
| blocks = inst.suggest_subnet_cidr_blocks("10.0.0.0/24", ["10.0.0.0/28"], 2) |
| assert blocks == ["10.0.0.16/28", "10.0.0.32/28"] |
|
|
|
|
| def test_suggest_subnet_cidr_blocks_respects_reserved(): |
| blocks = inst.suggest_subnet_cidr_blocks( |
| "10.0.0.0/24", |
| [], |
| 1, |
| reserved_cidrs=["10.0.0.0/28"], |
| ) |
| assert blocks == ["10.0.0.16/28"] |
|
|
|
|
| def test_vpc_cidr_blocks_from_describe_includes_associations(): |
| vpc = { |
| "CidrBlock": "10.0.0.0/16", |
| "CidrBlockAssociationSet": [{"CidrBlock": "10.1.0.0/16"}], |
| } |
| assert inst.vpc_cidr_blocks_from_describe(vpc) == [ |
| "10.0.0.0/16", |
| "10.1.0.0/16", |
| ] |
|
|
|
|
| def test_build_app_config_env_values_express_uses_in_app_cognito(): |
| values = inst.build_env_values(_demo_answers()) |
| updates = inst.build_app_config_env_values(values) |
| assert updates["COGNITO_AUTH"] == "True" |
| assert updates["RUN_AWS_FUNCTIONS"] == "True" |
| assert updates["DOCUMENT_REDACTION_BUCKET"].endswith("s3-logs") |
|
|
|
|
| def test_build_app_config_env_values_feature_defaults(): |
| values = inst.build_env_values(_demo_answers()) |
| updates = inst.build_app_config_env_values(values) |
| assert updates["RUN_ALL_EXAMPLES_THROUGH_AWS"] == "True" |
| assert updates["SHOW_EXAMPLES"] == "True" |
| assert updates["CUSTOM_VLM_BACKEND"] == "bedrock_vlm" |
| assert updates["INCLUDE_FACE_IDENTIFICATION_TEXTRACT_OPTION"] == "True" |
| assert updates["SHOW_SUMMARISATION"] == "True" |
| assert updates["SAVE_LOGS_TO_DYNAMODB"] == "True" |
| assert updates["SCAN_UPLOADS_FOR_MALWARE"] == "True" |
| assert updates["MALWARE_SCAN_S3_BUCKET"].endswith("s3-malware-scan") |
|
|
|
|
| def test_build_env_values_defaults_ecs_cpu_to_two_vcpu(): |
| values = inst.build_env_values(_demo_answers()) |
| assert values["ECS_TASK_CPU_SIZE"] == "2048" |
|
|
|
|
| def test_build_app_config_env_values_express_pi_disables_main_cognito(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| values = inst.build_env_values(answers) |
| updates = inst.build_app_config_env_values(values) |
| assert updates["COGNITO_AUTH"] == "False" |
|
|
|
|
| def test_build_app_config_env_values_headless(): |
| answers = _headless_answers() |
| values = inst.build_env_values(answers) |
| updates = inst.build_app_config_env_values(values) |
| assert updates["COGNITO_AUTH"] == "False" |
|
|
|
|
| def test_write_app_config_env_from_example(tmp_path, monkeypatch): |
| example = tmp_path / "app_config.env.example" |
| example.write_text( |
| "RUN_FASTAPI=True\nDOCUMENT_REDACTION_BUCKET=placeholder\n", |
| encoding="utf-8", |
| ) |
| target = tmp_path / "app_config.env" |
| monkeypatch.setattr(inst, "APP_CONFIG_ENV_EXAMPLE", example) |
| monkeypatch.setattr(inst, "APP_CONFIG_ENV_PATH", target) |
|
|
| answers = _demo_answers() |
| answers.write_app_config_env = True |
| values = inst.build_env_values(answers) |
| inst.write_app_config_env_file(answers, values) |
|
|
| written = inst.read_env_file(target) |
| assert written["RUN_FASTAPI"] == "True" |
| assert written["DOCUMENT_REDACTION_BUCKET"].endswith("s3-logs") |
| assert written["COGNITO_AUTH"] == "True" |
|
|
|
|
| def test_format_list_env(): |
| assert inst.format_list_env(["a", "b"]) == '["a", "b"]' |
| assert ( |
| inst.format_list_env(["10.0.0.0/28"], use_single_quotes=True) |
| == "['10.0.0.0/28']" |
| ) |
| assert inst.format_list_env([]) == "[]" |
|
|
|
|
| def test_write_env_file_backs_up(tmp_path): |
| env_path = tmp_path / "cdk_config.env" |
| env_path.write_text("OLD=1\n", encoding="utf-8") |
| inst.write_env_file(env_path, {"NEW": "2"}) |
| backups = list(tmp_path.glob("cdk_config.env.bak.*")) |
| assert len(backups) == 1 |
| assert "NEW=2" in env_path.read_text(encoding="utf-8") |
|
|
|
|
| def test_write_cdk_json_preserves_context(tmp_path, monkeypatch): |
| cdk_json = tmp_path / "cdk.json" |
| cdk_json.write_text( |
| json.dumps( |
| { |
| "app": "old-python app.py", |
| "context": {"@aws-cdk/custom:flag": True, "keep": 1}, |
| } |
| ), |
| encoding="utf-8", |
| ) |
| monkeypatch.setattr(inst, "CDK_JSON_PATH", cdk_json) |
| monkeypatch.setattr(inst, "CDK_JSON_EXAMPLE", tmp_path / "missing.example") |
|
|
| py = Path(sys.executable) |
| inst.write_cdk_json(py, force=True) |
|
|
| data = json.loads(cdk_json.read_text(encoding="utf-8")) |
| assert data["context"]["@aws-cdk/custom:flag"] is True |
| assert data["context"]["keep"] == 1 |
| assert data["app"].endswith("app.py") |
| assert "output" in data |
| backups = list(tmp_path.glob("cdk.json.bak.*")) |
| assert len(backups) == 1 |
|
|
|
|
| def test_format_cdk_app_command(): |
| cmd = inst.format_cdk_app_command(Path(sys.executable)) |
| assert cmd.endswith("app.py") |
| assert str(sys.executable).replace("/", "\\") in cmd or sys.executable in cmd |
|
|
|
|
| def test_venv_python_paths_includes_sys_executable(): |
| paths = inst._venv_python_paths() |
| assert Path(sys.executable).resolve() == paths[0].resolve() |
|
|
|
|
| def test_merge_preset_custom(): |
| merged = inst.merge_preset("demo", {"ECS_TASK_MEMORY_SIZE": "8192"}) |
| assert merged["USE_ECS_EXPRESS_MODE"] == "True" |
| assert merged["ECS_TASK_MEMORY_SIZE"] == "8192" |
|
|
|
|
| def test_build_env_values_pi_express(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| values = inst.build_env_values(answers) |
| assert values["ENABLE_PI_AGENT_EXPRESS_SERVICE"] == "True" |
| assert "AGENT_ALB_PATH_PREFIX" not in values |
| assert "AGENT_ALB_ROUTING" not in values |
| assert values["ECS_SERVICE_CONNECT_DISCOVERY_NAME"] == "redaction" |
| assert values["ECS_PI_EXPRESS_SC_PORT_NAME"] == "port-7862" |
| assert values["ECS_EXPRESS_SC_PORT_NAME"] == "port-7860" |
|
|
|
|
| def test_build_pi_agent_env_values_express_skips_root_path(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| env = inst.build_pi_agent_env_values(answers) |
| assert env["RUN_FASTAPI"] == "True" |
| assert "AGENT_ROOT_PATH" not in env |
|
|
|
|
| def test_build_env_values_pi_production_host(): |
| answers = _production_answers() |
| answers.enable_agentic_legacy = True |
| answers.enable_service_connect = True |
| answers.agentic_alb_routing = "host" |
| answers.agentic_alb_host_header = "agent.redaction.example.com" |
| values = inst.build_env_values(answers) |
| assert values["ENABLE_PI_AGENT_ECS_SERVICE"] == "True" |
| assert values["ENABLE_ECS_SERVICE_CONNECT"] == "True" |
| assert values["AGENT_ALB_HOST_HEADER"] == "agent.redaction.example.com" |
| assert values["AGENT_ALB_LISTENER_RULE_PRIORITY"] == "3" |
|
|
|
|
| def test_validate_pi_host_requires_header(): |
| answers = _production_answers() |
| answers.enable_agentic_legacy = True |
| answers.enable_service_connect = True |
| answers.agentic_alb_routing = "host" |
| answers.agentic_alb_host_header = "" |
| values = inst.build_env_values(answers) |
| errors = inst.validate_env_values(values) |
| assert any("AGENT_ALB_HOST_HEADER" in e for e in errors) |
|
|
|
|
| def test_validate_pi_express_skips_alb_routing(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| values = inst.build_env_values(answers) |
| assert inst.validate_env_values(values) == [] |
|
|
|
|
| def test_build_pi_agent_env_values(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = False |
| answers.enable_agentic_legacy = True |
| answers.agentic_alb_routing = "path" |
| answers.agentic_alb_path_prefix = "/pi" |
| values = inst.build_pi_agent_env_values(answers) |
| assert values["AGENT_DEPLOYMENT_PROFILE"] == "aws-ecs" |
| assert values["DOC_REDACTION_GRADIO_URL"] == "http://redaction:7860" |
| assert values["AGENT_ROOT_PATH"] == "/pi" |
|
|
|
|
| def test_apply_agentic_cli_flags_enable_agentic_demo(): |
| answers = inst.InstallAnswers(profile="demo") |
| args = argparse.Namespace( |
| enable_agentic=True, |
| enable_agentic_express=False, |
| enable_agentic_legacy=False, |
| agentic_alb_routing=None, |
| agentic_path_prefix="", |
| agentic_host_header="", |
| agentic_listener_priority="", |
| agentic_gradio_port="", |
| sc_discovery_name="", |
| pi_provider="", |
| skip_pi_agent_env=False, |
| agent_orchestrator=None, |
| enable_agentcore_runtime=False, |
| agentcore_runtime_url="", |
| agentcore_api_key="", |
| ) |
| inst.apply_agentic_cli_flags(args, answers) |
| assert answers.enable_agentic_express is True |
| assert answers.agent_orchestrator == "agentcore" |
| assert answers.enable_agentcore_runtime is True |
|
|
|
|
| def test_demo_default_orchestrator_is_agentcore(): |
| answers = inst.InstallAnswers(profile="demo", enable_agentic_express=True) |
| assert inst.default_agent_orchestrator_for_answers(answers) == "agentcore" |
| answers.profile = "production" |
| assert inst.default_agent_orchestrator_for_answers(answers) == "pi" |
|
|
|
|
| def test_merge_policy_file_locations_adds_invoke_policy(): |
| raw = inst.merge_policy_file_locations(agentcore_enabled=False) |
| assert inst.PI_AGENTCORE_INVOKE_POLICY_FILE not in raw |
| raw_agentcore = inst.merge_policy_file_locations(agentcore_enabled=True) |
| assert inst.PI_AGENTCORE_INVOKE_POLICY_FILE in raw_agentcore |
| assert "textract_policy.json" in raw_agentcore |
|
|
|
|
| def test_build_env_values_agentcore_includes_invoke_policy(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.enable_agentcore_runtime = True |
| answers.agentcore_runtime_url = "https://runtime.example" |
| values = inst.build_env_values(answers) |
| assert inst.PI_AGENTCORE_INVOKE_POLICY_FILE in values["POLICY_FILE_LOCATIONS"] |
|
|
|
|
| def test_validate_agentcore_allows_deferred_url(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.allow_empty_agentcore_url = True |
| assert inst.validate_install_answers(answers) == [] |
| values = inst.build_env_values(answers) |
| assert inst.validate_env_values(values, allow_empty_agentcore_url=True) == [] |
|
|
|
|
| def test_build_env_values_agentcore_orchestrator(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.enable_agentcore_runtime = True |
| answers.agentcore_runtime_url = "https://runtime.example" |
| values = inst.build_env_values(answers) |
| assert values["AGENT_ORCHESTRATOR"] == "agentcore" |
| assert values["ENABLE_AGENTCORE_RUNTIME"] == "True" |
| assert values["AGENTCORE_RUNTIME_URL"] == "https://runtime.example" |
|
|
|
|
| def test_build_pi_agent_env_values_agentcore(monkeypatch): |
| |
| |
| |
| monkeypatch.setattr(inst, "fetch_stack_output", lambda *_a, **_k: None) |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.agentcore_runtime_url = "https://runtime.example" |
| answers.agentcore_api_key = "secret-token" |
| values = inst.build_pi_agent_env_values(answers) |
| assert values["AGENT_ORCHESTRATOR"] == "agentcore" |
| assert values["AGENTCORE_RUNTIME_URL"] == "https://runtime.example" |
| assert values["AGENTCORE_API_KEY"] == "secret-token" |
| assert values["DOC_REDACTION_GRADIO_URL"] == "http://redaction:7860" |
|
|
|
|
| def test_normalize_agentcore_bedrock_model_defaults_to_sonnet(): |
| assert ( |
| inst.normalize_agentcore_bedrock_model("") |
| == inst.AGENTCORE_DEFAULT_BEDROCK_MODEL |
| ) |
| assert inst.normalize_agentcore_bedrock_model(" amazon.nova-pro-v1:0 ") == ( |
| "amazon.nova-pro-v1:0" |
| ) |
|
|
|
|
| def test_build_env_values_sets_agentcore_bedrock_model_default(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.enable_agentcore_runtime = True |
| answers.agentcore_runtime_url = "https://runtime.example" |
| values = inst.build_env_values(answers) |
| assert values["AGENTCORE_BEDROCK_MODEL"] == inst.AGENTCORE_DEFAULT_BEDROCK_MODEL |
|
|
|
|
| def test_build_env_values_honours_selected_bedrock_model(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.enable_agentcore_runtime = True |
| answers.agentcore_runtime_url = "https://runtime.example" |
| answers.agentcore_bedrock_model = "amazon.nova-pro-v1:0" |
| values = inst.build_env_values(answers) |
| assert values["AGENTCORE_BEDROCK_MODEL"] == "amazon.nova-pro-v1:0" |
|
|
|
|
| def test_build_pi_agent_env_values_agentcore_sets_bedrock_model(monkeypatch): |
| monkeypatch.setattr(inst, "fetch_stack_output", lambda *_a, **_k: None) |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| answers.agentcore_runtime_url = "https://runtime.example" |
| answers.agentcore_bedrock_model = "amazon.nova-pro-v1:0" |
| values = inst.build_pi_agent_env_values(answers) |
| assert values["AGENT_DEFAULT_PROVIDER"] == "amazon-bedrock" |
| assert values["AGENT_DEFAULT_MODEL"] == "amazon.nova-pro-v1:0" |
|
|
|
|
| def test_apply_agent_orchestrator_cli_flags_applies_model(): |
| answers = inst.InstallAnswers(profile="demo", enable_agentic_express=True) |
| args = argparse.Namespace( |
| agent_orchestrator="agentcore", |
| enable_agentcore_runtime=True, |
| agentcore_runtime_url="", |
| agentcore_api_key="", |
| agentcore_model="amazon.nova-pro-v1:0", |
| ) |
| inst.apply_agent_orchestrator_cli_flags(args, answers) |
| assert answers.agentcore_bedrock_model == "amazon.nova-pro-v1:0" |
|
|
|
|
| def test_report_bedrock_model_access_proceeds_on_unknown(monkeypatch): |
| monkeypatch.setattr( |
| inst, |
| "validate_bedrock_model_access", |
| lambda *_a, **_k: (None, "inconclusive"), |
| ) |
| assert ( |
| inst.report_bedrock_model_access( |
| "anthropic.claude-sonnet-4-6", |
| "eu-west-2", |
| interactive=False, |
| assume_yes=True, |
| ) |
| is True |
| ) |
|
|
|
|
| def test_resolve_doc_redaction_gradio_url_agentcore_uses_express_endpoint(monkeypatch): |
| answers = _demo_answers() |
| answers.agent_orchestrator = "agentcore" |
| monkeypatch.setattr( |
| inst, |
| "fetch_stack_output", |
| lambda *_a, **_k: "main.example.ecs.eu-west-2.on.aws", |
| ) |
| assert ( |
| inst.resolve_doc_redaction_gradio_url(answers) |
| == "https://main.example.ecs.eu-west-2.on.aws" |
| ) |
|
|
|
|
| def test_validate_agentcore_requires_runtime_url(): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| answers.agent_orchestrator = "agentcore" |
| errors = inst.validate_install_answers(answers) |
| assert any("AGENTCORE_RUNTIME_URL" in err for err in errors) |
|
|
| answers.agentcore_runtime_url = "https://runtime.example" |
| assert inst.validate_install_answers(answers) == [] |
|
|
| values = inst.build_env_values(answers) |
| assert inst.validate_env_values(values) == [] |
|
|
|
|
| def test_apply_agent_orchestrator_cli_flags(): |
| answers = inst.InstallAnswers(profile="demo", enable_agentic_express=True) |
| args = argparse.Namespace( |
| agent_orchestrator="langgraph", |
| enable_agentcore_runtime=False, |
| agentcore_runtime_url="", |
| agentcore_api_key="", |
| ) |
| inst.apply_agent_orchestrator_cli_flags(args, answers) |
| assert answers.agent_orchestrator == "langgraph" |
|
|
| args.enable_agentcore_runtime = True |
| args.agentcore_runtime_url = "https://runtime.example" |
| inst.apply_agent_orchestrator_cli_flags(args, answers) |
| assert answers.agent_orchestrator == "agentcore" |
| assert answers.enable_agentcore_runtime is True |
| assert answers.agentcore_runtime_url == "https://runtime.example" |
|
|
|
|
| def test_stacks_to_check_includes_appregistry_when_enabled(): |
| checks = inst.stacks_to_check( |
| "eu-west-2", |
| { |
| "ENABLE_APPREGISTRY": "True", |
| "APPREGISTRY_STACK_NAME": "Demo-Redaction-AppRegistryStack", |
| }, |
| ) |
| names = [name for name, _ in checks] |
| assert names == [ |
| "Demo-Redaction-AppRegistryStack", |
| inst.REGIONAL_STACK, |
| ] |
| assert checks[-1] == (inst.REGIONAL_STACK, "eu-west-2") |
|
|
|
|
| def test_stacks_to_check_without_appregistry(): |
| checks = inst.stacks_to_check("eu-west-2", {"ENABLE_APPREGISTRY": "False"}) |
| assert [name for name, _ in checks] == [ |
| inst.REGIONAL_STACK, |
| ] |
|
|
|
|
| def test_derived_appregistry_stack_name_from_cdk_prefix(): |
| name = inst.derived_appregistry_stack_name( |
| {"CDK_PREFIX": "Demo-Redaction-", "ENABLE_APPREGISTRY": "True"} |
| ) |
| assert name == "Demo-Redaction-AppRegistryStack" |
|
|
|
|
| def test_stacks_to_check_derives_appregistry_from_cdk_prefix(): |
| checks = inst.stacks_to_check( |
| "eu-west-2", |
| { |
| "CDK_PREFIX": "Demo-Redaction-", |
| "ENABLE_APPREGISTRY": "True", |
| }, |
| ) |
| assert "Demo-Redaction-AppRegistryStack" in [name for name, _ in checks] |
|
|
|
|
| def test_stacks_to_check_skips_cloudfront_when_disabled(): |
| checks = inst.stacks_to_check( |
| "eu-west-2", |
| { |
| "USE_CLOUDFRONT": "False", |
| "RUN_USEAST_STACK": "False", |
| "ENABLE_APPREGISTRY": "False", |
| }, |
| ) |
| assert checks == [(inst.REGIONAL_STACK, "eu-west-2")] |
|
|
|
|
| def test_discover_existing_doc_redaction_stacks_order(monkeypatch): |
| monkeypatch.setattr( |
| inst, "list_regional_appregistry_stack_names", lambda _region: [] |
| ) |
|
|
| def fake_describe(stack_name: str, region: str): |
| if stack_name == inst.REGIONAL_STACK and region == "eu-west-2": |
| return inst.ExistingStack( |
| name=stack_name, |
| region=region, |
| status="UPDATE_COMPLETE", |
| ) |
| return None |
|
|
| monkeypatch.setattr(inst, "describe_existing_stack", fake_describe) |
| found = inst.discover_existing_doc_redaction_stacks("eu-west-2") |
| assert [s.name for s in found] == [inst.REGIONAL_STACK] |
|
|
|
|
| def test_discover_existing_stacks_continues_after_access_denied(monkeypatch): |
| from botocore.exceptions import ClientError |
|
|
| monkeypatch.setattr( |
| inst, "list_regional_appregistry_stack_names", lambda _region: [] |
| ) |
|
|
| def fake_describe(stack_name: str, region: str): |
| if ( |
| stack_name == inst.CLOUDFRONT_STACK |
| and region == inst.CLOUDFRONT_STACK_REGION |
| ): |
| raise ClientError( |
| { |
| "Error": { |
| "Code": "AccessDenied", |
| "Message": "explicit deny in SCP", |
| } |
| }, |
| "DescribeStacks", |
| ) |
| if stack_name == inst.REGIONAL_STACK and region == "eu-west-2": |
| return inst.ExistingStack( |
| name=stack_name, |
| region=region, |
| status="UPDATE_COMPLETE", |
| ) |
| return None |
|
|
| monkeypatch.setattr(inst, "describe_existing_stack", fake_describe) |
| found = inst.discover_existing_doc_redaction_stacks("eu-west-2") |
| assert [s.name for s in found] == [inst.REGIONAL_STACK] |
|
|
|
|
| def test_discover_includes_orphan_appregistry_when_disabled_in_config(monkeypatch): |
| monkeypatch.setattr( |
| inst, |
| "list_regional_appregistry_stack_names", |
| lambda _region: ["Old-Redaction-AppRegistryStack"], |
| ) |
|
|
| def fake_describe(stack_name: str, region: str): |
| if stack_name == "Old-Redaction-AppRegistryStack": |
| return inst.ExistingStack( |
| name=stack_name, |
| region=region, |
| status="CREATE_COMPLETE", |
| ) |
| return None |
|
|
| monkeypatch.setattr(inst, "describe_existing_stack", fake_describe) |
| found = inst.discover_existing_doc_redaction_stacks( |
| "eu-west-2", |
| {"ENABLE_APPREGISTRY": "False"}, |
| ) |
| assert [s.name for s in found] == ["Old-Redaction-AppRegistryStack"] |
|
|
|
|
| def test_handle_existing_stacks_force_delete(monkeypatch): |
| stacks = [ |
| inst.ExistingStack( |
| name=inst.CLOUDFRONT_STACK, |
| region=inst.CLOUDFRONT_STACK_REGION, |
| status="CREATE_COMPLETE", |
| ) |
| ] |
| deleted: list = [] |
|
|
| monkeypatch.setattr( |
| inst, |
| "discover_existing_doc_redaction_stacks", |
| lambda *_a, **_k: stacks, |
| ) |
| monkeypatch.setattr( |
| inst, |
| "force_delete_cloudformation_stacks", |
| lambda s, **kwargs: deleted.extend(s), |
| ) |
|
|
| args = argparse.Namespace( |
| skip_stack_check=False, |
| config_only=False, |
| synth_only=False, |
| force_delete_stacks=True, |
| yes=True, |
| ) |
| inst.handle_existing_stacks_at_start(args, "eu-west-2") |
| assert deleted == stacks |
|
|
|
|
| def test_handle_existing_stacks_without_force_aborts_no_update(monkeypatch): |
| """Without --force-delete-stacks an existing stack aborts (never updates in place).""" |
| import pytest |
|
|
| monkeypatch.setattr( |
| inst, |
| "discover_existing_doc_redaction_stacks", |
| lambda *_a, **_k: [ |
| inst.ExistingStack( |
| name=inst.REGIONAL_STACK, |
| region="eu-west-2", |
| status="CREATE_COMPLETE", |
| ) |
| ], |
| ) |
| monkeypatch.setattr( |
| inst, |
| "force_delete_cloudformation_stacks", |
| lambda *_a, **_k: (_ for _ in ()).throw(AssertionError("should not delete")), |
| ) |
|
|
| args = argparse.Namespace( |
| skip_stack_check=False, |
| config_only=False, |
| synth_only=False, |
| force_delete_stacks=False, |
| yes=True, |
| ) |
| with pytest.raises(SystemExit): |
| inst.handle_existing_stacks_at_start(args, "eu-west-2") |
|
|
|
|
| def test_write_pi_agent_env_file_minimal(tmp_path, monkeypatch): |
| answers = _demo_answers() |
| answers.enable_agentic_express = True |
| target = tmp_path / "agent.env" |
| monkeypatch.setattr(inst, "PI_AGENT_ENV_PATH", target) |
| monkeypatch.setattr(inst, "PI_AGENT_ENV_EXAMPLE", tmp_path / "missing.example") |
| monkeypatch.setattr(inst, "PI_AGENT_ENV_LEGACY_PATH", tmp_path / "pi_agent.env") |
| inst.write_pi_agent_env_file(answers) |
| text = target.read_text(encoding="utf-8") |
| assert "AGENT_DEPLOYMENT_PROFILE=aws-ecs" in text |
| assert "DOC_REDACTION_GRADIO_URL=http://redaction:7860" in text |
|
|
|
|
| def test_run_cdk_command_invokes_cdk_cli_not_python(monkeypatch): |
| calls: list = [] |
|
|
| def fake_run(cmd, **kwargs): |
| calls.append((cmd, kwargs)) |
| return subprocess.CompletedProcess(cmd, 0) |
|
|
| monkeypatch.setattr(inst, "resolve_cdk_executable", lambda: r"C:\npm\cdk.cmd") |
| monkeypatch.setattr(inst.subprocess, "run", fake_run) |
| inst.run_cdk_command(["synth"], check=False) |
| cmd, kwargs = calls[0] |
| assert cmd == [r"C:\npm\cdk.cmd", "synth"] |
| assert "executable" not in kwargs |
|
|
|
|
| def test_build_cdk_subprocess_env_overrides_stale_defaults(tmp_path, monkeypatch): |
| env_path = tmp_path / "config" / "cdk_config.env" |
| env_path.parent.mkdir(parents=True) |
| env_path.write_text( |
| "VPC_NAME=my-vpc\nCDK_PREFIX=MyPrefix-\nAWS_REGION=eu-west-2\n", |
| encoding="utf-8", |
| ) |
| monkeypatch.setattr(inst, "ENV_PATH", env_path) |
| monkeypatch.setattr(inst, "CDK_DIR", tmp_path) |
| monkeypatch.setenv("VPC_NAME", "") |
| monkeypatch.setenv("CDK_PREFIX", "") |
|
|
| env = inst.build_cdk_subprocess_env() |
| assert env["VPC_NAME"] == "my-vpc" |
| assert env["CDK_PREFIX"] == "MyPrefix-" |
| assert env["CDK_CONFIG_PATH"] == str(env_path) |
|
|
|
|
| def test_apply_cdk_runtime_env_sets_paths(tmp_path, monkeypatch): |
| env_path = tmp_path / "cdk_config.env" |
| monkeypatch.setattr(inst, "ENV_PATH", env_path) |
| inst.apply_cdk_runtime_env( |
| {"CDK_FOLDER": "C:/example/cdk/", "AWS_REGION": "eu-west-2"} |
| ) |
| assert os.environ["CDK_CONFIG_PATH"] == str(env_path) |
| assert os.environ["CDK_FOLDER"] == "C:/example/cdk/" |
|
|
|
|
| def test_run_smoke_test_if_needed_skips_config_only(monkeypatch): |
| called = [] |
|
|
| def fake_smoke(_python_exe): |
| called.append(True) |
|
|
| monkeypatch.setattr(inst, "smoke_test_python_app", fake_smoke) |
| args = argparse.Namespace(config_only=True, skip_cdk_json=False) |
| inst.run_smoke_test_if_needed(Path(sys.executable), args) |
| assert called == [] |
|
|
|
|
| def test_main_writes_config_before_smoke_test(monkeypatch, tmp_path): |
| call_order: list[str] = [] |
| config_dir = tmp_path / "config" |
| config_dir.mkdir() |
| env_path = config_dir / "cdk_config.env" |
| cdk_json = tmp_path / "cdk.json" |
|
|
| monkeypatch.setattr(inst, "CONFIG_DIR", config_dir) |
| monkeypatch.setattr(inst, "ENV_PATH", env_path) |
| monkeypatch.setattr(inst, "CDK_JSON_PATH", cdk_json) |
| monkeypatch.setattr(inst, "CDK_DIR", tmp_path) |
| monkeypatch.setattr(inst, "check_cdk_cli", lambda: "2.0.0") |
| monkeypatch.setattr( |
| inst, |
| "handle_existing_stacks_at_start", |
| lambda *_a, **_k: None, |
| ) |
| monkeypatch.setattr( |
| inst, |
| "resolve_python_executable", |
| lambda **_kw: Path(sys.executable), |
| ) |
| monkeypatch.setattr(inst, "write_cdk_json", lambda *_a, **_k: cdk_json) |
|
|
| def fake_wizard(_args): |
| call_order.append("wizard") |
| return _demo_answers() |
|
|
| def fake_write_env_file(path, values): |
| call_order.append("write_config") |
| path.write_text("VPC_NAME=test-vpc\n", encoding="utf-8") |
| return values |
|
|
| def fake_smoke(_python_exe): |
| call_order.append("smoke") |
|
|
| monkeypatch.setattr(inst, "run_wizard", fake_wizard) |
| monkeypatch.setattr(inst, "write_env_file", fake_write_env_file) |
| monkeypatch.setattr(inst, "smoke_test_python_app", fake_smoke) |
| monkeypatch.setattr(inst, "print_summary", lambda *_a, **_k: None) |
| monkeypatch.setattr(inst, "ask_yes_no", lambda *_a, **_k: True) |
| monkeypatch.setattr(inst, "cdk_bootstrap_needed", lambda *_a, **_k: False) |
| monkeypatch.setattr(inst, "run_cdk_command", lambda *_a, **_k: None) |
|
|
| inst.main( |
| [ |
| "--yes", |
| "--profile", |
| "demo", |
| "--vpc-name", |
| "test-vpc", |
| "--synth-only", |
| ] |
| ) |
|
|
| assert call_order.index("wizard") < call_order.index("write_config") |
| assert call_order.index("write_config") < call_order.index("smoke") |
|
|
|
|
| def test_resolve_fixup_env_values_derives_service_from_prefix(): |
| values = {"CDK_PREFIX": "Demo-Redaction-", "USE_ECS_EXPRESS_MODE": "True"} |
| resolved = inst.resolve_fixup_env_values(values) |
| assert resolved["ECS_EXPRESS_SERVICE_NAME"] == "Demo-Redaction-ECSService" |
| assert resolved["CLUSTER_NAME"] == "Demo-Redaction-Cluster" |
| assert ( |
| resolved["ECS_PI_EXPRESS_SERVICE_NAME"] == "Demo-Redaction-AgentExpressService" |
| ) |
|
|
|
|
| def test_apply_post_deploy_fixup_express_syncs_cognito_secret_not_alb(monkeypatch): |
| import cdk_post_deploy as post |
|
|
| values = { |
| "USE_ECS_EXPRESS_MODE": "True", |
| "USE_CLOUDFRONT": "False", |
| "AWS_REGION": "eu-west-2", |
| "CDK_PREFIX": "Demo-Redaction-", |
| "ECS_EXPRESS_COGNITO_REDIRECT_BASE": "", |
| } |
| outputs = { |
| "ExpressServiceEndpoint": "https://express.example.com", |
| "CognitoPoolId": "pool-1", |
| "CognitoAppClientId": "client-1", |
| } |
| monkeypatch.setattr( |
| inst, |
| "fetch_stack_output", |
| lambda _stack, key, _region: outputs.get(key), |
| ) |
| monkeypatch.setattr(inst, "patch_env_file", lambda *_a, **_k: None) |
| monkeypatch.setattr(inst, "ask_yes_no", lambda *_a, **_k: True) |
| redeploy_calls: list = [] |
| monkeypatch.setattr( |
| inst, |
| "run_cdk_command", |
| lambda *args, **_k: redeploy_calls.append(args), |
| ) |
| monkeypatch.setattr( |
| post, |
| "cognito_alb_callbacks_need_update", |
| lambda *_a, **_k: True, |
| ) |
| monkeypatch.setattr( |
| post, |
| "apply_cognito_alb_callback_fixup", |
| lambda *_a, **_k: True, |
| ) |
| secret_fixup_calls: list = [] |
| monkeypatch.setattr( |
| post, |
| "apply_cognito_secret_fixup_from_stack", |
| lambda **_k: secret_fixup_calls.append(_k) or True, |
| ) |
|
|
| assert inst.apply_post_deploy_fixup(values, assume_yes=False) is True |
| assert redeploy_calls == [] |
| assert secret_fixup_calls |
| assert secret_fixup_calls[0]["main_service_name"] == "Demo-Redaction-ECSService" |
| assert secret_fixup_calls[0]["cluster_name"] == "Demo-Redaction-Cluster" |
|
|
|
|
| def test_jsii_import_failure_hint_detects_json_decode_error(): |
| hint = inst._jsii_import_failure_hint( |
| "json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)" |
| ) |
| assert "JSII Node.js helper" in hint |
| assert "node --version" in hint |
|
|
|
|
| def test_jsii_import_failure_hint_empty_for_other_errors(): |
| assert inst._jsii_import_failure_hint("ModuleNotFoundError: aws_cdk") == "" |
|
|