import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { test } from "node:test"; const require = createRequire(import.meta.url); const { App } = require("aws-cdk-lib"); const { Template } = require("aws-cdk-lib/assertions"); const { FrontendStack } = require("../lib/frontend-stack.js"); const account = "396287094661"; const region = "us-east-1"; const DEV_ROLE = "githubdeploy-shoc-frontend-new-dev"; const DEV_ORIGIN_ID = "shocfrontenddevDistributionOrigin10CCD0EE1"; const CONDITION = "ManageSiteInfrastructureCondition"; const RETAINED_TYPES = new Set([ "AWS::S3::Bucket", "AWS::S3::BucketPolicy", "AWS::CloudFront::Distribution", "AWS::CloudFront::Function", "AWS::CloudFront::OriginAccessControl", "AWS::Route53::RecordSet", "Custom::S3AutoDeleteObjects", ]); function devTemplate(retainForTerraformAdoption, overrides = {}) { const app = new App(); const stack = new FrontendStack(app, "shoc-frontend-dev", { envName: "dev", githubRepo: "Sea-Haven-Industries/shoc-frontend-new", deployBranch: "dev", domainNames: ["dev.seahaven.com"], certificateArn: `arn:aws:acm:${region}:${account}:certificate/2b78e74f-7b65-4b82-a413-7a498b102f00`, hostedZoneId: "Z07671212N75U4YLPWZR8", hostedZoneName: "dev.seahaven.com", retainForTerraformAdoption, env: { account, region }, ...overrides, }); return Template.fromStack(stack).toJSON(); } function entriesByType(template, type) { return Object.entries(template.Resources).filter(([, resource]) => resource.Type === type); } function isTransferred(logicalId, resource) { const isDeployRoleResource = (resource.Type === "AWS::IAM::Role" && resource.Properties.RoleName === DEV_ROLE) || (resource.Type === "AWS::IAM::Policy" && logicalId.startsWith("GithubDeployRole")); return RETAINED_TYPES.has(resource.Type) || isDeployRoleResource; } test("adoption mode emits the 13 transferred resources plus the auto-delete custom resource", () => { const template = devTemplate(true); assert.equal(entriesByType(template, "AWS::S3::Bucket").length, 1); assert.equal(entriesByType(template, "AWS::S3::BucketPolicy").length, 1); assert.equal(entriesByType(template, "AWS::CloudFront::Distribution").length, 1); assert.equal(entriesByType(template, "AWS::CloudFront::OriginAccessControl").length, 1); assert.equal(entriesByType(template, "AWS::CloudFront::Function").length, 1); assert.equal(entriesByType(template, "AWS::Route53::RecordSet").length, 2); assert.equal(entriesByType(template, "Custom::S3AutoDeleteObjects").length, 1); const transferred = Object.entries(template.Resources).filter(([id, resource]) => isTransferred(id, resource), ); // Bucket, bucket policy, distribution, OAC, function, A, AAAA, role, inline // policy = 9 CloudFormation resources (Terraform splits the bucket into 6 // addresses) plus the retained custom resource. assert.equal(transferred.length, 10); }); test("adoption mode preserves the live dev identifiers", () => { const template = devTemplate(true); const bucket = entriesByType(template, "AWS::S3::Bucket")[0][1]; assert.equal(bucket.Properties.BucketName, "seahaven-shoc-frontend-dev"); assert.equal(bucket.Properties.VersioningConfiguration.Status, "Enabled"); assert.ok( bucket.Properties.Tags.some( (tag) => tag.Key === "aws-cdk:auto-delete-objects" && tag.Value === "true", ), ); const distribution = entriesByType(template, "AWS::CloudFront::Distribution")[0][1]; assert.equal(distribution.Properties.DistributionConfig.Origins[0].Id, DEV_ORIGIN_ID); assert.equal( distribution.Properties.DistributionConfig.DefaultCacheBehavior.TargetOriginId, DEV_ORIGIN_ID, ); const [, deployRole] = entriesByType(template, "AWS::IAM::Role").find( ([, resource]) => resource.Properties.RoleName === DEV_ROLE, ); assert.equal( deployRole.Properties.PermissionsBoundary, `arn:aws:iam::${account}:policy/shoc-frontend-new-dev-deploy-boundary`, ); assert.ok( deployRole.Properties.Tags.some( (tag) => tag.Key === "HcpTerraformWorkspace" && tag.Value === "shoc-frontend-new-dev", ), ); const condition = deployRole.Properties.AssumeRolePolicyDocument.Statement[0].Condition; assert.equal( condition.StringEquals["token.actions.githubusercontent.com:sub"], "repo:Sea-Haven-Industries/shoc-frontend-new:ref:refs/heads/dev", ); assert.equal(condition.StringLike, undefined); // Legacy inline policy stays byte-compatible with the live document. const [, inlinePolicy] = entriesByType(template, "AWS::IAM::Policy").find(([id]) => id.startsWith("GithubDeployRole"), ); const sids = inlinePolicy.Properties.PolicyDocument.Statement.map((s) => s.Sid); assert.deepEqual(sids, [ "AssumeCdkBootstrapRoles", "DescribeStack", undefined, "InvalidateDistribution", ]); for (const output of [ "TerraformWorkspaceTag", "TerraformDeployBoundaryArn", "TerraformImportBucket", "TerraformImportBucketPolicy", "TerraformImportDistribution", "TerraformImportOriginAccessControl", "TerraformOriginAccessControlName", "TerraformOriginAccessControlDescription", "TerraformDistributionOriginId", "TerraformImportSpaRewriteFunction", "TerraformImportAliasA", "TerraformImportAliasAAAA", "TerraformImportDeployRole", "TerraformImportDeployRolePolicy", "TerraformDeployInlinePolicyName", "TerraformBucketAutoDeleteHelperRoleArn", "TerraformRetainedAutoDeleteCustomResource", ]) { assert.ok(template.Outputs[output], `missing output ${output}`); } assert.equal( template.Outputs.TerraformImportAliasA.Value, "Z07671212N75U4YLPWZR8_dev.seahaven.com_A", ); assert.equal(template.Outputs.TerraformDistributionOriginId.Value, DEV_ORIGIN_ID); }); test("adoption mode retains exactly the transferred resources", () => { const template = devTemplate(true); for (const [logicalId, resource] of Object.entries(template.Resources)) { if (isTransferred(logicalId, resource)) { assert.equal(resource.DeletionPolicy, "Retain", logicalId); assert.equal(resource.UpdateReplacePolicy, "Retain", logicalId); } else { assert.notEqual(resource.DeletionPolicy, "Retain", logicalId); assert.notEqual(resource.UpdateReplacePolicy, "Retain", logicalId); } } // The auto-delete provider Lambda, role, and log group stay unretained. for (const type of ["AWS::Lambda::Function", "AWS::Logs::LogGroup"]) { for (const [, resource] of entriesByType(template, type)) { assert.notEqual(resource.DeletionPolicy, "Retain"); } } const providerRoles = entriesByType(template, "AWS::IAM::Role").filter( ([, resource]) => resource.Properties.RoleName !== DEV_ROLE, ); assert.equal(providerRoles.length, 1); assert.notEqual(providerRoles[0][1].DeletionPolicy, "Retain"); }); test("adoption mode requires ManageSiteInfrastructure and gates transferred resources and outputs", () => { const template = devTemplate(true); const parameter = template.Parameters.ManageSiteInfrastructure; assert.ok(parameter); assert.equal(parameter.Type, "String"); assert.deepEqual(parameter.AllowedValues, ["true", "false"]); assert.equal(parameter.Default, undefined); assert.ok(template.Conditions[CONDITION]); for (const [logicalId, resource] of Object.entries(template.Resources)) { if (isTransferred(logicalId, resource)) { assert.equal(resource.Condition, CONDITION, logicalId); } else { assert.notEqual(resource.Condition, CONDITION, logicalId); } } for (const [outputName, output] of Object.entries(template.Outputs)) { assert.equal(output.Condition, CONDITION, outputName); } const [, autoDeleteHandler] = entriesByType(template, "AWS::Lambda::Function")[0]; assert.equal( autoDeleteHandler.Properties.Description, "Lambda function for auto-deleting objects in the site S3 bucket.", ); assert.equal(typeof autoDeleteHandler.Properties.Description, "string"); }); test("normal mode is unchanged: destructive cleanup, StringLike trust, no boundary, tag, or parameter", () => { const template = devTemplate(false); const bucket = entriesByType(template, "AWS::S3::Bucket")[0][1]; assert.equal(bucket.DeletionPolicy, "Delete"); assert.equal(bucket.UpdateReplacePolicy, "Delete"); const customResource = entriesByType(template, "Custom::S3AutoDeleteObjects")[0][1]; assert.notEqual(customResource.DeletionPolicy, "Retain"); const [, deployRole] = entriesByType(template, "AWS::IAM::Role").find( ([, resource]) => resource.Properties.RoleName === DEV_ROLE, ); assert.equal(deployRole.Properties.PermissionsBoundary, undefined); assert.ok(!deployRole.Properties.Tags?.some((tag) => tag.Key === "HcpTerraformWorkspace")); const condition = deployRole.Properties.AssumeRolePolicyDocument.Statement[0].Condition; assert.equal( condition.StringLike["token.actions.githubusercontent.com:sub"], "repo:Sea-Haven-Industries/shoc-frontend-new:ref:refs/heads/dev", ); assert.equal(template.Outputs.TerraformWorkspaceTag, undefined); assert.equal(template.Parameters?.ManageSiteInfrastructure, undefined); assert.equal(template.Conditions?.[CONDITION], undefined); for (const resource of Object.values(template.Resources)) { assert.equal(resource.Condition, undefined); } }); test("adoption mode refuses an environment without a verified origin ID", () => { assert.throws( () => devTemplate(true, { envName: "staging" }), /No verified Terraform adoption origin ID exists for staging/, ); });