@intentius/chant-lexicon-aws 0.0.6 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/integrity.json +25 -10
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +9444 -4597
  4. package/dist/rules/cf-refs.ts +99 -0
  5. package/dist/rules/ext001.ts +32 -25
  6. package/dist/rules/hardcoded-region.ts +1 -0
  7. package/dist/rules/iam-wildcard.ts +1 -0
  8. package/dist/rules/s3-encryption.ts +3 -3
  9. package/dist/rules/waw016.ts +86 -0
  10. package/dist/rules/waw017.ts +53 -0
  11. package/dist/rules/waw018.ts +71 -0
  12. package/dist/rules/waw019.ts +82 -0
  13. package/dist/rules/waw020.ts +64 -0
  14. package/dist/rules/waw021.ts +53 -0
  15. package/dist/rules/waw022.ts +43 -0
  16. package/dist/rules/waw023.ts +47 -0
  17. package/dist/rules/waw024.ts +54 -0
  18. package/dist/rules/waw025.ts +43 -0
  19. package/dist/rules/waw026.ts +46 -0
  20. package/dist/rules/waw027.ts +50 -0
  21. package/dist/rules/waw028.ts +47 -0
  22. package/dist/rules/waw029.ts +62 -0
  23. package/dist/rules/waw030.ts +246 -0
  24. package/dist/skills/chant-aws.md +430 -0
  25. package/dist/types/index.d.ts +58525 -58501
  26. package/package.json +2 -2
  27. package/src/actions/actions.test.ts +75 -0
  28. package/src/actions/dynamodb.ts +36 -0
  29. package/src/actions/ecr.ts +9 -0
  30. package/src/actions/ecs.ts +5 -0
  31. package/src/actions/iam.ts +3 -0
  32. package/src/actions/index.ts +9 -0
  33. package/src/actions/lambda.ts +11 -0
  34. package/src/actions/logs.ts +4 -0
  35. package/src/actions/s3.ts +34 -0
  36. package/src/actions/sns.ts +5 -0
  37. package/src/actions/sqs.ts +15 -0
  38. package/src/codegen/__snapshots__/snapshot.test.ts.snap +20 -20
  39. package/src/codegen/docs-links.test.ts +143 -0
  40. package/src/codegen/docs.ts +294 -124
  41. package/src/codegen/generate-lexicon.ts +8 -0
  42. package/src/codegen/generate-typescript.ts +25 -1
  43. package/src/codegen/generate.ts +1 -13
  44. package/src/codegen/package.ts +2 -0
  45. package/src/codegen/typecheck.test.ts +1 -1
  46. package/src/composites/composites.test.ts +442 -0
  47. package/src/composites/fargate-alb.ts +253 -0
  48. package/src/composites/index.ts +20 -0
  49. package/src/composites/lambda-api.ts +20 -0
  50. package/src/composites/lambda-dynamodb.ts +64 -0
  51. package/src/composites/lambda-eventbridge.ts +36 -0
  52. package/src/composites/lambda-function.ts +76 -0
  53. package/src/composites/lambda-s3.ts +72 -0
  54. package/src/composites/lambda-sns.ts +30 -0
  55. package/src/composites/lambda-sqs.ts +44 -0
  56. package/src/composites/scheduled-lambda.ts +37 -0
  57. package/src/composites/vpc-default.ts +148 -0
  58. package/src/default-tags.test.ts +38 -0
  59. package/src/default-tags.ts +77 -0
  60. package/src/generated/index.d.ts +58525 -58501
  61. package/src/generated/index.ts +1351 -1351
  62. package/src/generated/lexicon-aws.json +9444 -4597
  63. package/src/import/generator.test.ts +5 -5
  64. package/src/import/generator.ts +4 -4
  65. package/src/import/roundtrip-fixtures.test.ts +2 -1
  66. package/src/import/roundtrip.test.ts +5 -5
  67. package/src/index.ts +21 -0
  68. package/src/integration.test.ts +92 -21
  69. package/src/intrinsics.ts +24 -13
  70. package/src/lint/post-synth/cf-refs.ts +99 -0
  71. package/src/lint/post-synth/ext001.test.ts +214 -31
  72. package/src/lint/post-synth/ext001.ts +32 -25
  73. package/src/lint/post-synth/waw013.test.ts +120 -0
  74. package/src/lint/post-synth/waw014.test.ts +121 -0
  75. package/src/lint/post-synth/waw015.test.ts +147 -0
  76. package/src/lint/post-synth/waw016.test.ts +141 -0
  77. package/src/lint/post-synth/waw016.ts +86 -0
  78. package/src/lint/post-synth/waw017.test.ts +130 -0
  79. package/src/lint/post-synth/waw017.ts +53 -0
  80. package/src/lint/post-synth/waw018.test.ts +109 -0
  81. package/src/lint/post-synth/waw018.ts +71 -0
  82. package/src/lint/post-synth/waw019.test.ts +138 -0
  83. package/src/lint/post-synth/waw019.ts +82 -0
  84. package/src/lint/post-synth/waw020.test.ts +125 -0
  85. package/src/lint/post-synth/waw020.ts +64 -0
  86. package/src/lint/post-synth/waw021.test.ts +81 -0
  87. package/src/lint/post-synth/waw021.ts +53 -0
  88. package/src/lint/post-synth/waw022.test.ts +54 -0
  89. package/src/lint/post-synth/waw022.ts +43 -0
  90. package/src/lint/post-synth/waw023.test.ts +53 -0
  91. package/src/lint/post-synth/waw023.ts +47 -0
  92. package/src/lint/post-synth/waw024.test.ts +64 -0
  93. package/src/lint/post-synth/waw024.ts +54 -0
  94. package/src/lint/post-synth/waw025.test.ts +42 -0
  95. package/src/lint/post-synth/waw025.ts +43 -0
  96. package/src/lint/post-synth/waw026.test.ts +54 -0
  97. package/src/lint/post-synth/waw026.ts +46 -0
  98. package/src/lint/post-synth/waw027.test.ts +63 -0
  99. package/src/lint/post-synth/waw027.ts +50 -0
  100. package/src/lint/post-synth/waw028.test.ts +68 -0
  101. package/src/lint/post-synth/waw028.ts +47 -0
  102. package/src/lint/post-synth/waw029.test.ts +179 -0
  103. package/src/lint/post-synth/waw029.ts +62 -0
  104. package/src/lint/post-synth/waw030.test.ts +800 -0
  105. package/src/lint/post-synth/waw030.ts +246 -0
  106. package/src/lint/rules/hardcoded-region.ts +1 -0
  107. package/src/lint/rules/iam-wildcard.ts +1 -0
  108. package/src/lint/rules/rules.test.ts +8 -8
  109. package/src/lint/rules/s3-encryption.ts +3 -3
  110. package/src/lsp/completions.ts +2 -0
  111. package/src/lsp/hover.ts +17 -0
  112. package/src/nested-stack-integration.test.ts +100 -0
  113. package/src/nested-stack.ts +2 -2
  114. package/src/plugin.test.ts +13 -15
  115. package/src/plugin.ts +552 -114
  116. package/src/serializer.test.ts +370 -43
  117. package/src/serializer.ts +69 -17
  118. package/src/spec/fetch.ts +10 -0
  119. package/src/spec/parse.test.ts +141 -0
  120. package/src/spec/parse.ts +40 -0
  121. package/src/taggable.ts +44 -0
  122. package/src/testdata/nested-stacks/app.ts +26 -0
  123. package/src/testdata/nested-stacks/network/outputs.ts +17 -0
  124. package/src/testdata/nested-stacks/network/security.ts +17 -0
  125. package/src/testdata/nested-stacks/network/vpc.ts +54 -0
  126. package/dist/skills/aws-cloudformation.md +0 -41
  127. package/src/codegen/rollback.test.ts +0 -80
  128. package/src/codegen/rollback.ts +0 -20
@@ -126,7 +126,7 @@ export function generateTypeScriptDeclarations(
126
126
  lines.push("");
127
127
  lines.push("// --- Resource classes ---");
128
128
  for (const re of resources) {
129
- writeResourceClass(lines, re.tsName, re.properties, re.attributes, re.remap);
129
+ writeResourceClass(lines, re.tsName, re.properties, re.attributes, re.remap, "CFResourceAttributes");
130
130
  }
131
131
 
132
132
  // Section 2: Property classes
@@ -224,6 +224,30 @@ function staticTypeScript(): string {
224
224
  lines.push("");
225
225
  lines.push("// --- Type interfaces ---");
226
226
  lines.push("");
227
+ lines.push("export interface Declarable {");
228
+ lines.push(" readonly entityType: string;");
229
+ lines.push("}");
230
+ lines.push("");
231
+ lines.push("export interface CFResourceAttributes {");
232
+ lines.push(" DependsOn?: Declarable | Declarable[] | string | string[];");
233
+ lines.push(" Condition?: string;");
234
+ lines.push(' DeletionPolicy?: "Delete" | "Retain" | "RetainExceptOnCreate" | "Snapshot";');
235
+ lines.push(' UpdateReplacePolicy?: "Delete" | "Retain" | "Snapshot";');
236
+ lines.push(" UpdatePolicy?: {");
237
+ lines.push(" AutoScalingReplacingUpdate?: { WillReplace?: boolean };");
238
+ lines.push(" AutoScalingRollingUpdate?: {");
239
+ lines.push(" MaxBatchSize?: number;");
240
+ lines.push(" MinInstancesInService?: number;");
241
+ lines.push(" PauseTime?: string;");
242
+ lines.push(" WaitOnResourceSignals?: boolean;");
243
+ lines.push(" };");
244
+ lines.push(" };");
245
+ lines.push(" CreationPolicy?: {");
246
+ lines.push(" ResourceSignal?: { Count?: number; Timeout?: string };");
247
+ lines.push(" };");
248
+ lines.push(" Metadata?: Record<string, unknown>;");
249
+ lines.push("}");
250
+ lines.push("");
227
251
  lines.push("export interface PolicyDocument {");
228
252
  lines.push(' Version?: "2012-10-17" | "2008-10-17";');
229
253
  lines.push(" Id?: string;");
@@ -152,17 +152,6 @@ export function writeGeneratedFiles(result: GenerateResult, baseDir: string): vo
152
152
  "",
153
153
  ].join("\n"),
154
154
  },
155
- snapshot: (generatedDir) => {
156
- const { snapshotArtifacts, saveSnapshot } = require("./rollback");
157
- const lexiconPath = join(generatedDir, "lexicon-aws.json");
158
- if (existsSync(lexiconPath)) {
159
- const snapshot = snapshotArtifacts(generatedDir);
160
- if (Object.keys(snapshot.files).length > 0) {
161
- const snapshotsDir = join(baseDir, ".snapshots");
162
- saveSnapshot(snapshot, snapshotsDir);
163
- }
164
- }
165
- },
166
155
  });
167
156
  }
168
157
 
@@ -184,8 +173,7 @@ function generateRuntimeIndex(
184
173
  // Build attrs map
185
174
  const attrs: Record<string, string> = {};
186
175
  for (const a of r.resource.attributes) {
187
- const camelName = a.name.charAt(0).toLowerCase() + a.name.slice(1);
188
- attrs[camelName] = a.name;
176
+ attrs[a.name] = a.name;
189
177
  }
190
178
 
191
179
  resourceEntries.push({ tsName, resourceType: cfnType, attrs });
@@ -3,7 +3,9 @@
3
3
  * with AWS-specific manifest building and skill collection.
4
4
  */
5
5
 
6
+ import { createRequire } from "module";
6
7
  import { readFileSync } from "fs";
8
+ const require = createRequire(import.meta.url);
7
9
  import { join, dirname } from "path";
8
10
  import { fileURLToPath } from "url";
9
11
  import type { IntrinsicDef } from "@intentius/chant/lexicon";
@@ -6,7 +6,7 @@ describe("typecheckDTS", () => {
6
6
  const content = `
7
7
  export declare class Bucket {
8
8
  readonly type: string;
9
- readonly bucketName?: string;
9
+ readonly BucketName?: string;
10
10
  }
11
11
  `;
12
12
  const result = await typecheckDTS(content);
@@ -0,0 +1,442 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import { expandComposite, CompositeRegistry, isCompositeInstance } from "@intentius/chant";
3
+ import { AttrRef } from "@intentius/chant/attrref";
4
+ import { LambdaFunction, LambdaNode, LambdaPython, NodeLambda, PythonLambda } from "./lambda-function";
5
+ import { LambdaApi } from "./lambda-api";
6
+ import { LambdaScheduled, ScheduledLambda } from "./scheduled-lambda";
7
+ import { LambdaSqs } from "./lambda-sqs";
8
+ import { LambdaEventBridge } from "./lambda-eventbridge";
9
+ import { LambdaDynamoDB } from "./lambda-dynamodb";
10
+ import { LambdaS3 } from "./lambda-s3";
11
+ import { LambdaSns } from "./lambda-sns";
12
+ import { VpcDefault } from "./vpc-default";
13
+ import { FargateAlb } from "./fargate-alb";
14
+
15
+ const baseProps = {
16
+ name: "TestFunc",
17
+ Runtime: "nodejs20.x",
18
+ Handler: "index.handler",
19
+ Code: { ZipFile: "exports.handler = async () => ({statusCode:200})" },
20
+ };
21
+
22
+ describe("LambdaFunction", () => {
23
+ test("returns role and func members", () => {
24
+ const instance = LambdaFunction(baseProps);
25
+ expect(instance.role).toBeDefined();
26
+ expect(instance.func).toBeDefined();
27
+ expect(Object.keys(instance.members)).toEqual(["role", "func"]);
28
+ });
29
+
30
+ test("func.Role references role.Arn via AttrRef", () => {
31
+ const instance = LambdaFunction(baseProps);
32
+ const funcProps = (instance.func as any).props;
33
+ expect(funcProps.Role).toBeInstanceOf(AttrRef);
34
+ });
35
+
36
+ test("expandComposite produces correct logical names", () => {
37
+ const instance = LambdaFunction(baseProps);
38
+ const expanded = expandComposite("myLambda", instance);
39
+ expect(expanded.has("myLambdaRole")).toBe(true);
40
+ expect(expanded.has("myLambdaFunc")).toBe(true);
41
+ expect(expanded.size).toBe(2);
42
+ });
43
+
44
+ test("default timeout is 30", () => {
45
+ const instance = LambdaFunction(baseProps);
46
+ const funcProps = (instance.func as any).props;
47
+ expect(funcProps.Timeout).toBe(30);
48
+ });
49
+
50
+ test("VpcConfig auto-attaches VPCAccessExecutionRole", () => {
51
+ const instance = LambdaFunction({
52
+ ...baseProps,
53
+ VpcConfig: { SubnetIds: ["subnet-1"], SecurityGroupIds: ["sg-1"] },
54
+ });
55
+ const roleProps = (instance.role as any).props;
56
+ expect(roleProps.ManagedPolicyArns).toContain(
57
+ "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
58
+ );
59
+ });
60
+
61
+ test("without VpcConfig, no VPCAccessExecutionRole", () => {
62
+ const instance = LambdaFunction(baseProps);
63
+ const roleProps = (instance.role as any).props;
64
+ expect(roleProps.ManagedPolicyArns).not.toContain(
65
+ "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
66
+ );
67
+ });
68
+
69
+ test("additional ManagedPolicyArns are appended", () => {
70
+ const customArn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess";
71
+ const instance = LambdaFunction({
72
+ ...baseProps,
73
+ ManagedPolicyArns: [customArn],
74
+ });
75
+ const roleProps = (instance.role as any).props;
76
+ expect(roleProps.ManagedPolicyArns).toContain(customArn);
77
+ expect(roleProps.ManagedPolicyArns).toContain(
78
+ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
79
+ );
80
+ });
81
+ });
82
+
83
+ describe("LambdaNode / LambdaPython presets", () => {
84
+ test("LambdaNode defaults Runtime and Handler", () => {
85
+ const instance = LambdaNode({
86
+ name: "TestNode",
87
+ Code: { ZipFile: "exports.handler = async () => ({})" },
88
+ });
89
+ const funcProps = (instance.func as any).props;
90
+ expect(funcProps.Runtime).toBe("nodejs20.x");
91
+ expect(funcProps.Handler).toBe("index.handler");
92
+ });
93
+
94
+ test("LambdaPython defaults Runtime and Handler", () => {
95
+ const instance = LambdaPython({
96
+ name: "TestPython",
97
+ Code: { ZipFile: "def handler(event, context): return {}" },
98
+ });
99
+ const funcProps = (instance.func as any).props;
100
+ expect(funcProps.Runtime).toBe("python3.12");
101
+ expect(funcProps.Handler).toBe("handler.handler");
102
+ });
103
+
104
+ test("preset defaults can be overridden", () => {
105
+ const instance = LambdaNode({
106
+ name: "TestOverride",
107
+ Runtime: "nodejs18.x",
108
+ Code: { ZipFile: "" },
109
+ });
110
+ const funcProps = (instance.func as any).props;
111
+ expect(funcProps.Runtime).toBe("nodejs18.x");
112
+ });
113
+
114
+ test("deprecated aliases still work", () => {
115
+ expect(NodeLambda).toBe(LambdaNode);
116
+ expect(PythonLambda).toBe(LambdaPython);
117
+ });
118
+ });
119
+
120
+ describe("LambdaApi", () => {
121
+ test("returns role, func, and permission members", () => {
122
+ const instance = LambdaApi(baseProps);
123
+ expect(Object.keys(instance.members)).toEqual(["role", "func", "permission"]);
124
+ });
125
+
126
+ test("permission references func.Arn", () => {
127
+ const instance = LambdaApi(baseProps);
128
+ const permProps = (instance.permission as any).props;
129
+ expect(permProps.FunctionName).toBeInstanceOf(AttrRef);
130
+ expect(permProps.Principal).toBe("apigateway.amazonaws.com");
131
+ });
132
+
133
+ test("sourceArn is passed through", () => {
134
+ const instance = LambdaApi({
135
+ ...baseProps,
136
+ sourceArn: "arn:aws:execute-api:us-east-1:123:api/*",
137
+ });
138
+ const permProps = (instance.permission as any).props;
139
+ expect(permProps.SourceArn).toBe("arn:aws:execute-api:us-east-1:123:api/*");
140
+ });
141
+
142
+ test("expandComposite produces 3 entries", () => {
143
+ const expanded = expandComposite("api", LambdaApi(baseProps));
144
+ expect(expanded.size).toBe(3);
145
+ expect(expanded.has("apiRole")).toBe(true);
146
+ expect(expanded.has("apiFunc")).toBe(true);
147
+ expect(expanded.has("apiPermission")).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe("LambdaScheduled", () => {
152
+ const scheduledProps = { ...baseProps, schedule: "rate(5 minutes)" };
153
+
154
+ test("returns role, func, rule, and permission members", () => {
155
+ const instance = LambdaScheduled(scheduledProps);
156
+ expect(Object.keys(instance.members)).toEqual(["role", "func", "rule", "permission"]);
157
+ });
158
+
159
+ test("rule has ScheduleExpression and targets func", () => {
160
+ const instance = LambdaScheduled(scheduledProps);
161
+ const ruleProps = (instance.rule as any).props;
162
+ expect(ruleProps.ScheduleExpression).toBe("rate(5 minutes)");
163
+ expect(ruleProps.State).toBe("ENABLED");
164
+ expect(ruleProps.Targets).toHaveLength(1);
165
+ });
166
+
167
+ test("enabled: false sets State to DISABLED", () => {
168
+ const instance = LambdaScheduled({ ...scheduledProps, enabled: false });
169
+ const ruleProps = (instance.rule as any).props;
170
+ expect(ruleProps.State).toBe("DISABLED");
171
+ });
172
+
173
+ test("permission principal is events.amazonaws.com", () => {
174
+ const instance = LambdaScheduled(scheduledProps);
175
+ const permProps = (instance.permission as any).props;
176
+ expect(permProps.Principal).toBe("events.amazonaws.com");
177
+ });
178
+
179
+ test("expandComposite produces 4 entries", () => {
180
+ const expanded = expandComposite("cron", LambdaScheduled(scheduledProps));
181
+ expect(expanded.size).toBe(4);
182
+ expect(expanded.has("cronRole")).toBe(true);
183
+ expect(expanded.has("cronFunc")).toBe(true);
184
+ expect(expanded.has("cronRule")).toBe(true);
185
+ expect(expanded.has("cronPermission")).toBe(true);
186
+ });
187
+
188
+ test("deprecated ScheduledLambda alias still works", () => {
189
+ expect(ScheduledLambda).toBe(LambdaScheduled);
190
+ });
191
+ });
192
+
193
+ describe("LambdaSqs", () => {
194
+ test("returns queue, role, func members", () => {
195
+ const instance = LambdaSqs(baseProps);
196
+ expect(Object.keys(instance.members)).toEqual(["queue", "role", "func"]);
197
+ });
198
+
199
+ test("expandComposite produces 4 entries (queue + role + func + eventSourceMapping)", () => {
200
+ const expanded = expandComposite("worker", LambdaSqs(baseProps));
201
+ expect(expanded.has("workerQueue")).toBe(true);
202
+ expect(expanded.has("workerRole")).toBe(true);
203
+ expect(expanded.has("workerFunc")).toBe(true);
204
+ });
205
+ });
206
+
207
+ describe("LambdaEventBridge", () => {
208
+ test("returns rule, role, func, permission members", () => {
209
+ const instance = LambdaEventBridge({ ...baseProps, schedule: "rate(1 hour)" });
210
+ expect(Object.keys(instance.members)).toEqual(["rule", "role", "func", "permission"]);
211
+ });
212
+
213
+ test("supports eventPattern", () => {
214
+ const instance = LambdaEventBridge({
215
+ ...baseProps,
216
+ eventPattern: { source: ["aws.s3"] },
217
+ });
218
+ const ruleProps = (instance.rule as any).props;
219
+ expect(ruleProps.EventPattern).toEqual({ source: ["aws.s3"] });
220
+ });
221
+ });
222
+
223
+ describe("LambdaDynamoDB", () => {
224
+ test("returns table, role, func members", () => {
225
+ const instance = LambdaDynamoDB({ ...baseProps, partitionKey: "pk" });
226
+ expect(Object.keys(instance.members)).toEqual(["table", "role", "func"]);
227
+ });
228
+
229
+ test("creates sort key when specified", () => {
230
+ const instance = LambdaDynamoDB({ ...baseProps, partitionKey: "pk", sortKey: "sk" });
231
+ const tableProps = (instance.table as any).props;
232
+ expect(tableProps.AttributeDefinitions).toHaveLength(2);
233
+ expect(tableProps.KeySchema).toHaveLength(2);
234
+ });
235
+ });
236
+
237
+ describe("LambdaS3", () => {
238
+ test("returns bucket, role, func members", () => {
239
+ const instance = LambdaS3(baseProps);
240
+ expect(Object.keys(instance.members)).toEqual(["bucket", "role", "func"]);
241
+ });
242
+
243
+ test("bucket has encryption and public access block", () => {
244
+ const instance = LambdaS3(baseProps);
245
+ const bucketProps = (instance.bucket as any).props;
246
+ expect(bucketProps.BucketEncryption).toBeDefined();
247
+ expect(bucketProps.PublicAccessBlockConfiguration).toBeDefined();
248
+ });
249
+ });
250
+
251
+ describe("LambdaSns", () => {
252
+ test("returns topic, role, func, subscription, permission members", () => {
253
+ const instance = LambdaSns(baseProps);
254
+ expect(Object.keys(instance.members)).toEqual(["topic", "role", "func", "subscription", "permission"]);
255
+ });
256
+
257
+ test("subscription uses lambda protocol", () => {
258
+ const instance = LambdaSns(baseProps);
259
+ const subProps = (instance.subscription as any).props;
260
+ expect(subProps.Protocol).toBe("lambda");
261
+ });
262
+
263
+ test("permission principal is sns.amazonaws.com", () => {
264
+ const instance = LambdaSns(baseProps);
265
+ const permProps = (instance.permission as any).props;
266
+ expect(permProps.Principal).toBe("sns.amazonaws.com");
267
+ });
268
+ });
269
+
270
+ describe("VpcDefault", () => {
271
+ test("returns 17 members", () => {
272
+ const instance = VpcDefault({});
273
+ expect(Object.keys(instance.members)).toHaveLength(17);
274
+ });
275
+
276
+ test("has all expected member names", () => {
277
+ const instance = VpcDefault({});
278
+ const names = Object.keys(instance.members);
279
+ expect(names).toContain("vpc");
280
+ expect(names).toContain("igw");
281
+ expect(names).toContain("igwAttachment");
282
+ expect(names).toContain("publicSubnet1");
283
+ expect(names).toContain("publicSubnet2");
284
+ expect(names).toContain("privateSubnet1");
285
+ expect(names).toContain("privateSubnet2");
286
+ expect(names).toContain("publicRouteTable");
287
+ expect(names).toContain("publicRoute");
288
+ expect(names).toContain("publicRta1");
289
+ expect(names).toContain("publicRta2");
290
+ expect(names).toContain("privateRouteTable");
291
+ expect(names).toContain("privateRta1");
292
+ expect(names).toContain("privateRta2");
293
+ expect(names).toContain("natEip");
294
+ expect(names).toContain("natGateway");
295
+ expect(names).toContain("privateRoute");
296
+ });
297
+
298
+ test("VPC has DNS enabled", () => {
299
+ const instance = VpcDefault({});
300
+ const vpcProps = (instance.vpc as any).props;
301
+ expect(vpcProps.EnableDnsSupport).toBe(true);
302
+ expect(vpcProps.EnableDnsHostnames).toBe(true);
303
+ });
304
+
305
+ test("public subnets have MapPublicIpOnLaunch", () => {
306
+ const instance = VpcDefault({});
307
+ const pub1Props = (instance.publicSubnet1 as any).props;
308
+ const pub2Props = (instance.publicSubnet2 as any).props;
309
+ expect(pub1Props.MapPublicIpOnLaunch).toBe(true);
310
+ expect(pub2Props.MapPublicIpOnLaunch).toBe(true);
311
+ });
312
+
313
+ test("vpc.VpcId is wired to subnets", () => {
314
+ const instance = VpcDefault({});
315
+ const sub1Props = (instance.publicSubnet1 as any).props;
316
+ expect(sub1Props.VpcId).toBeInstanceOf(AttrRef);
317
+ });
318
+
319
+ test("NAT gateway is present", () => {
320
+ const instance = VpcDefault({});
321
+ expect(instance.natGateway).toBeDefined();
322
+ expect(instance.natEip).toBeDefined();
323
+ });
324
+
325
+ test("expandComposite produces 17 entries", () => {
326
+ const expanded = expandComposite("net", VpcDefault({}));
327
+ expect(expanded.size).toBe(17);
328
+ expect(expanded.has("netVpc")).toBe(true);
329
+ expect(expanded.has("netIgw")).toBe(true);
330
+ expect(expanded.has("netNatGateway")).toBe(true);
331
+ });
332
+
333
+ test("custom CIDR overrides defaults", () => {
334
+ const instance = VpcDefault({ cidr: "172.16.0.0/16" });
335
+ const vpcProps = (instance.vpc as any).props;
336
+ expect(vpcProps.CidrBlock).toBe("172.16.0.0/16");
337
+ });
338
+ });
339
+
340
+ describe("FargateAlb", () => {
341
+ const fargateProps = {
342
+ image: "nginx:latest",
343
+ vpcId: "vpc-123",
344
+ publicSubnetIds: ["subnet-pub1", "subnet-pub2"],
345
+ privateSubnetIds: ["subnet-priv1", "subnet-priv2"],
346
+ };
347
+
348
+ test("returns 11 members", () => {
349
+ const instance = FargateAlb(fargateProps);
350
+ expect(Object.keys(instance.members)).toHaveLength(11);
351
+ });
352
+
353
+ test("has all expected member names", () => {
354
+ const instance = FargateAlb(fargateProps);
355
+ const names = Object.keys(instance.members);
356
+ expect(names).toEqual([
357
+ "cluster", "executionRole", "taskRole", "logGroup", "taskDef",
358
+ "albSg", "taskSg", "alb", "targetGroup", "listener", "service",
359
+ ]);
360
+ });
361
+
362
+ test("expandComposite produces correct logical names", () => {
363
+ const expanded = expandComposite("web", FargateAlb(fargateProps));
364
+ expect(expanded.has("webCluster")).toBe(true);
365
+ expect(expanded.has("webExecutionRole")).toBe(true);
366
+ expect(expanded.has("webTaskRole")).toBe(true);
367
+ expect(expanded.has("webLogGroup")).toBe(true);
368
+ expect(expanded.has("webTaskDef")).toBe(true);
369
+ expect(expanded.has("webAlbSg")).toBe(true);
370
+ expect(expanded.has("webTaskSg")).toBe(true);
371
+ expect(expanded.has("webAlb")).toBe(true);
372
+ expect(expanded.has("webTargetGroup")).toBe(true);
373
+ expect(expanded.has("webListener")).toBe(true);
374
+ expect(expanded.has("webService")).toBe(true);
375
+ });
376
+
377
+ test("execution role has ECR and Logs policies", () => {
378
+ const instance = FargateAlb(fargateProps);
379
+ const roleProps = (instance.executionRole as any).props;
380
+ expect(roleProps.Policies).toHaveLength(1);
381
+ const policyDoc = (roleProps.Policies[0] as any).props.PolicyDocument;
382
+ expect(policyDoc.Statement).toHaveLength(2);
383
+ expect(policyDoc.Statement[0].Action).toContain("ecr:GetAuthorizationToken");
384
+ expect(policyDoc.Statement[1].Action).toContain("logs:CreateLogStream");
385
+ });
386
+
387
+ test("task role receives custom policies", () => {
388
+ const { Role_Policy } = require("../generated");
389
+ const customPolicy = new Role_Policy({
390
+ PolicyName: "Custom",
391
+ PolicyDocument: { Version: "2012-10-17", Statement: [] },
392
+ });
393
+ const instance = FargateAlb({ ...fargateProps, Policies: [customPolicy] });
394
+ const roleProps = (instance.taskRole as any).props;
395
+ expect(roleProps.Policies).toHaveLength(1);
396
+ });
397
+
398
+ test("task definition has awsvpc and FARGATE", () => {
399
+ const instance = FargateAlb(fargateProps);
400
+ const tdProps = (instance.taskDef as any).props;
401
+ expect(tdProps.NetworkMode).toBe("awsvpc");
402
+ expect(tdProps.RequiresCompatibilities).toEqual(["FARGATE"]);
403
+ });
404
+
405
+ test("ALB SG allows ingress on listener port", () => {
406
+ const instance = FargateAlb(fargateProps);
407
+ const sgProps = (instance.albSg as any).props;
408
+ expect(sgProps.SecurityGroupIngress).toHaveLength(1);
409
+ const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
410
+ expect(ingress.FromPort).toBe(80);
411
+ expect(ingress.CidrIp).toBe("0.0.0.0/0");
412
+ });
413
+
414
+ test("task SG references ALB SG GroupId", () => {
415
+ const instance = FargateAlb(fargateProps);
416
+ const sgProps = (instance.taskSg as any).props;
417
+ const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
418
+ expect(ingress.SourceSecurityGroupId).toBeInstanceOf(AttrRef);
419
+ });
420
+
421
+ test("default container port is 80", () => {
422
+ const instance = FargateAlb(fargateProps);
423
+ const tdProps = (instance.taskDef as any).props;
424
+ const containerDef = (tdProps.ContainerDefinitions[0] as any).props;
425
+ const portMapping = (containerDef.PortMappings[0] as any).props;
426
+ expect(portMapping.ContainerPort).toBe(80);
427
+ });
428
+
429
+ test("default desired count is 2", () => {
430
+ const instance = FargateAlb(fargateProps);
431
+ const svcProps = (instance.service as any).props;
432
+ expect(svcProps.DesiredCount).toBe(2);
433
+ });
434
+
435
+ test("custom listener port is applied", () => {
436
+ const instance = FargateAlb({ ...fargateProps, listenerPort: 8080 });
437
+ const sgProps = (instance.albSg as any).props;
438
+ const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
439
+ expect(ingress.FromPort).toBe(8080);
440
+ expect(ingress.ToPort).toBe(8080);
441
+ });
442
+ });