import * as cdk from "aws-cdk-lib"; import { Template } from "aws-cdk-lib/assertions"; import { OpenSweStack } from "../lib/open-swe-stack"; const ENV = { account: "328440206208", region: "us-east-1" }; /** * Several AWS APIs reject non-ASCII in fields that `cdk synth` happily emits and * `tsc` happily compiles — so a stray em-dash/arrow only blows up at DEPLOY time * (e.g. EC2 SecurityGroup GroupDescription: "Character sets beyond ASCII are not * supported"). This has bitten us twice (the AMI Description, then the instance-SG * description). This test fails the build at synth time instead. * * Scope: the EC2 fields with a documented ASCII/restricted-charset constraint — * SecurityGroup GroupDescription and ingress/egress rule descriptions. (CloudFormation * Output descriptions + Route53 comments accept UTF-8, so they are not asserted.) * * EC2 rule descriptions are stricter than ASCII: the allowed set is * `a-zA-Z0-9. _-:/()#,@[]+=&;{}!$*` — note it EXCLUDES `<` and `>`, which is why a * naive em-dash -> "->" replacement still fails at deploy. We assert that exact set. */ // Characters NOT in the EC2 description allowed set. const DISALLOWED = /[^a-zA-Z0-9. _:/()#,@[\]+=&;{}!$*-]/; function synthDev(): Record }> { const app = new cdk.App(); const dev = new OpenSweStack(app, "OpenSweDevStack", { stackName: "open-swe-dev", env: ENV, envName: "dev", }); return Template.fromStack(dev).toJSON().Resources; } describe("ASCII-only EC2 description fields", () => { const resources = synthDev(); it("SecurityGroup GroupDescription is ASCII", () => { for (const [id, r] of Object.entries(resources)) { if (r.Type !== "AWS::EC2::SecurityGroup") continue; const desc = (r.Properties?.GroupDescription as string) ?? ""; expect(DISALLOWED.test(desc) ? `${id}: ${desc}` : "ascii").toBe("ascii"); } }); it("SecurityGroup ingress/egress rule descriptions are ASCII", () => { for (const [id, r] of Object.entries(resources)) { const props = r.Properties ?? {}; const groups: Array<{ Description?: string }> = []; if (Array.isArray(props.SecurityGroupIngress)) groups.push(...props.SecurityGroupIngress); if (Array.isArray(props.SecurityGroupEgress)) groups.push(...props.SecurityGroupEgress); // Standalone AWS::EC2::SecurityGroupEgress / ...Ingress resources. if (r.Type === "AWS::EC2::SecurityGroupEgress" || r.Type === "AWS::EC2::SecurityGroupIngress") { groups.push(props as { Description?: string }); } for (const rule of groups) { const desc = rule.Description ?? ""; expect(DISALLOWED.test(desc) ? `${id}: ${desc}` : "ascii").toBe("ascii"); } } }); // EC2 caps base64-encoded user-data at 25600 bytes; CDK + tsc don't check it, so // an oversized boot script (e.g. an embedded deploy.sh) only fails at deploy. it("EC2 user-data fits the 25600-byte encoded limit", () => { for (const [id, r] of Object.entries(resources)) { if (r.Type !== "AWS::EC2::Instance") continue; const ud = (r.Properties?.UserData as { "Fn::Base64"?: string }) ?? {}; const script = typeof ud["Fn::Base64"] === "string" ? ud["Fn::Base64"] : ""; const encoded = Buffer.from(script, "utf8").toString("base64").length; expect(`${id}: ${encoded} bytes`).toBe(encoded < 25600 ? `${id}: ${encoded} bytes` : "OVER 25600"); } }); // CDK substitutes %%...%% tokens in user-data at synth. Any %%TOKEN%% left in the // rendered script means a token wasn't wired in app-service.ts (the @@...@@ tokens // are intentional — user-data seds those into the baked templates at boot). it("user-data has no unresolved %%CDK%% tokens", () => { for (const [id, r] of Object.entries(resources)) { if (r.Type !== "AWS::EC2::Instance") continue; const ud = (r.Properties?.UserData as { "Fn::Base64"?: string }) ?? {}; const script = typeof ud["Fn::Base64"] === "string" ? ud["Fn::Base64"] : ""; const leftover = script.match(/%%[A-Z0-9_]+%%/g) ?? []; expect(`${id}: ${leftover.join(",")}`).toBe(`${id}: `); } }); });