@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
@@ -0,0 +1,800 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw030, checkMissingDependsOn } from "./waw030";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW030: Missing DependsOn for Known Patterns", () => {
10
+ test("check metadata", () => {
11
+ expect(waw030.id).toBe("WAW030");
12
+ expect(waw030.description).toContain("DependsOn");
13
+ });
14
+
15
+ // --- ECS Service + Listener pattern ---
16
+
17
+ test("ECS Service with LoadBalancers, no Listener DependsOn → warning", () => {
18
+ const ctx = makeCtx({
19
+ Resources: {
20
+ MyListener: {
21
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
22
+ Properties: {},
23
+ },
24
+ MyService: {
25
+ Type: "AWS::ECS::Service",
26
+ Properties: {
27
+ LoadBalancers: [{ TargetGroupArn: { Ref: "MyTG" } }],
28
+ },
29
+ },
30
+ },
31
+ });
32
+ const diags = checkMissingDependsOn(ctx);
33
+ expect(diags).toHaveLength(1);
34
+ expect(diags[0].checkId).toBe("WAW030");
35
+ expect(diags[0].severity).toBe("warning");
36
+ expect(diags[0].message).toContain("MyService");
37
+ expect(diags[0].message).toContain("Listener");
38
+ expect(diags[0].entity).toBe("MyService");
39
+ expect(diags[0].lexicon).toBe("aws");
40
+ });
41
+
42
+ test("ECS Service with LoadBalancers and Listener DependsOn → no diagnostic", () => {
43
+ const ctx = makeCtx({
44
+ Resources: {
45
+ MyListener: {
46
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
47
+ Properties: {},
48
+ },
49
+ MyService: {
50
+ Type: "AWS::ECS::Service",
51
+ DependsOn: "MyListener",
52
+ Properties: {
53
+ LoadBalancers: [{ TargetGroupArn: { Ref: "MyTG" } }],
54
+ },
55
+ },
56
+ },
57
+ });
58
+ const diags = checkMissingDependsOn(ctx);
59
+ expect(diags).toHaveLength(0);
60
+ });
61
+
62
+ test("ECS Service with LoadBalancers and Listener DependsOn (array) → no diagnostic", () => {
63
+ const ctx = makeCtx({
64
+ Resources: {
65
+ MyListener: {
66
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
67
+ Properties: {},
68
+ },
69
+ MyService: {
70
+ Type: "AWS::ECS::Service",
71
+ DependsOn: ["MyListener"],
72
+ Properties: {
73
+ LoadBalancers: [{ TargetGroupArn: { Ref: "MyTG" } }],
74
+ },
75
+ },
76
+ },
77
+ });
78
+ const diags = checkMissingDependsOn(ctx);
79
+ expect(diags).toHaveLength(0);
80
+ });
81
+
82
+ test("ECS Service without LoadBalancers → no diagnostic", () => {
83
+ const ctx = makeCtx({
84
+ Resources: {
85
+ MyListener: {
86
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
87
+ Properties: {},
88
+ },
89
+ MyService: {
90
+ Type: "AWS::ECS::Service",
91
+ Properties: { Cluster: "my-cluster" },
92
+ },
93
+ },
94
+ });
95
+ const diags = checkMissingDependsOn(ctx);
96
+ expect(diags).toHaveLength(0);
97
+ });
98
+
99
+ test("no ECS Service → no diagnostic", () => {
100
+ const ctx = makeCtx({
101
+ Resources: {
102
+ MyListener: {
103
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
104
+ Properties: {},
105
+ },
106
+ },
107
+ });
108
+ const diags = checkMissingDependsOn(ctx);
109
+ expect(diags).toHaveLength(0);
110
+ });
111
+
112
+ // --- EC2 Route + VPCGatewayAttachment pattern ---
113
+
114
+ test("Route with GatewayId, no VPCGatewayAttachment dependency → warning", () => {
115
+ const ctx = makeCtx({
116
+ Resources: {
117
+ MyAttachment: {
118
+ Type: "AWS::EC2::VPCGatewayAttachment",
119
+ Properties: {},
120
+ },
121
+ MyRoute: {
122
+ Type: "AWS::EC2::Route",
123
+ Properties: {
124
+ GatewayId: { Ref: "MyIGW" },
125
+ RouteTableId: { Ref: "MyRT" },
126
+ DestinationCidrBlock: "0.0.0.0/0",
127
+ },
128
+ },
129
+ },
130
+ });
131
+ const diags = checkMissingDependsOn(ctx);
132
+ expect(diags).toHaveLength(1);
133
+ expect(diags[0].checkId).toBe("WAW030");
134
+ expect(diags[0].severity).toBe("warning");
135
+ expect(diags[0].message).toContain("MyRoute");
136
+ expect(diags[0].message).toContain("VPCGatewayAttachment");
137
+ expect(diags[0].entity).toBe("MyRoute");
138
+ });
139
+
140
+ test("Route with GatewayId and VPCGatewayAttachment DependsOn → no diagnostic", () => {
141
+ const ctx = makeCtx({
142
+ Resources: {
143
+ MyAttachment: {
144
+ Type: "AWS::EC2::VPCGatewayAttachment",
145
+ Properties: {},
146
+ },
147
+ MyRoute: {
148
+ Type: "AWS::EC2::Route",
149
+ DependsOn: "MyAttachment",
150
+ Properties: {
151
+ GatewayId: { Ref: "MyIGW" },
152
+ RouteTableId: { Ref: "MyRT" },
153
+ DestinationCidrBlock: "0.0.0.0/0",
154
+ },
155
+ },
156
+ },
157
+ });
158
+ const diags = checkMissingDependsOn(ctx);
159
+ expect(diags).toHaveLength(0);
160
+ });
161
+
162
+ test("Route with GatewayId and property Ref to VPCGatewayAttachment → no diagnostic", () => {
163
+ const ctx = makeCtx({
164
+ Resources: {
165
+ MyAttachment: {
166
+ Type: "AWS::EC2::VPCGatewayAttachment",
167
+ Properties: {},
168
+ },
169
+ MyRoute: {
170
+ Type: "AWS::EC2::Route",
171
+ Properties: {
172
+ GatewayId: { Ref: "MyAttachment" },
173
+ RouteTableId: { Ref: "MyRT" },
174
+ DestinationCidrBlock: "0.0.0.0/0",
175
+ },
176
+ },
177
+ },
178
+ });
179
+ const diags = checkMissingDependsOn(ctx);
180
+ expect(diags).toHaveLength(0);
181
+ });
182
+
183
+ test("Route without GatewayId → no diagnostic", () => {
184
+ const ctx = makeCtx({
185
+ Resources: {
186
+ MyAttachment: {
187
+ Type: "AWS::EC2::VPCGatewayAttachment",
188
+ Properties: {},
189
+ },
190
+ MyRoute: {
191
+ Type: "AWS::EC2::Route",
192
+ Properties: {
193
+ NatGatewayId: { Ref: "MyNAT" },
194
+ RouteTableId: { Ref: "MyRT" },
195
+ DestinationCidrBlock: "0.0.0.0/0",
196
+ },
197
+ },
198
+ },
199
+ });
200
+ const diags = checkMissingDependsOn(ctx);
201
+ expect(diags).toHaveLength(0);
202
+ });
203
+
204
+ // --- Edge cases ---
205
+
206
+ test("ECS Service with empty LoadBalancers array → no diagnostic", () => {
207
+ const ctx = makeCtx({
208
+ Resources: {
209
+ MyListener: {
210
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
211
+ Properties: {},
212
+ },
213
+ MyService: {
214
+ Type: "AWS::ECS::Service",
215
+ Properties: { LoadBalancers: [] },
216
+ },
217
+ },
218
+ });
219
+ const diags = checkMissingDependsOn(ctx);
220
+ expect(diags).toHaveLength(0);
221
+ });
222
+
223
+ test("ECS Service with LoadBalancers but no Listener in template → no diagnostic", () => {
224
+ const ctx = makeCtx({
225
+ Resources: {
226
+ MyService: {
227
+ Type: "AWS::ECS::Service",
228
+ Properties: {
229
+ LoadBalancers: [{ TargetGroupArn: { Ref: "MyTG" } }],
230
+ },
231
+ },
232
+ },
233
+ });
234
+ const diags = checkMissingDependsOn(ctx);
235
+ expect(diags).toHaveLength(0);
236
+ });
237
+
238
+ test("Route with GatewayId but no VPCGatewayAttachment in template → no diagnostic", () => {
239
+ const ctx = makeCtx({
240
+ Resources: {
241
+ MyRoute: {
242
+ Type: "AWS::EC2::Route",
243
+ Properties: {
244
+ GatewayId: { Ref: "MyIGW" },
245
+ RouteTableId: { Ref: "MyRT" },
246
+ DestinationCidrBlock: "0.0.0.0/0",
247
+ },
248
+ },
249
+ },
250
+ });
251
+ const diags = checkMissingDependsOn(ctx);
252
+ expect(diags).toHaveLength(0);
253
+ });
254
+
255
+ test("Route with Fn::GetAtt (dot-delimited) referencing VPCGatewayAttachment → no diagnostic", () => {
256
+ const ctx = makeCtx({
257
+ Resources: {
258
+ MyAttachment: {
259
+ Type: "AWS::EC2::VPCGatewayAttachment",
260
+ Properties: {},
261
+ },
262
+ MyRoute: {
263
+ Type: "AWS::EC2::Route",
264
+ Properties: {
265
+ GatewayId: { "Fn::GetAtt": "MyAttachment.InternetGatewayId" },
266
+ RouteTableId: { Ref: "MyRT" },
267
+ DestinationCidrBlock: "0.0.0.0/0",
268
+ },
269
+ },
270
+ },
271
+ });
272
+ const diags = checkMissingDependsOn(ctx);
273
+ expect(diags).toHaveLength(0);
274
+ });
275
+
276
+ test("multiple ECS Services — only flags those missing DependsOn", () => {
277
+ const ctx = makeCtx({
278
+ Resources: {
279
+ Listener: {
280
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
281
+ Properties: {},
282
+ },
283
+ GoodService: {
284
+ Type: "AWS::ECS::Service",
285
+ DependsOn: "Listener",
286
+ Properties: {
287
+ LoadBalancers: [{ TargetGroupArn: { Ref: "TG" } }],
288
+ },
289
+ },
290
+ BadService: {
291
+ Type: "AWS::ECS::Service",
292
+ Properties: {
293
+ LoadBalancers: [{ TargetGroupArn: { Ref: "TG2" } }],
294
+ },
295
+ },
296
+ },
297
+ });
298
+ const diags = checkMissingDependsOn(ctx);
299
+ expect(diags).toHaveLength(1);
300
+ expect(diags[0].entity).toBe("BadService");
301
+ });
302
+
303
+ test("ECS Service DependsOn includes one of multiple listeners → no diagnostic", () => {
304
+ const ctx = makeCtx({
305
+ Resources: {
306
+ HttpListener: {
307
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
308
+ Properties: {},
309
+ },
310
+ HttpsListener: {
311
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
312
+ Properties: {},
313
+ },
314
+ MyService: {
315
+ Type: "AWS::ECS::Service",
316
+ DependsOn: "HttpsListener",
317
+ Properties: {
318
+ LoadBalancers: [{ TargetGroupArn: { Ref: "TG" } }],
319
+ },
320
+ },
321
+ },
322
+ });
323
+ const diags = checkMissingDependsOn(ctx);
324
+ expect(diags).toHaveLength(0);
325
+ });
326
+
327
+ test("empty Resources → no diagnostic", () => {
328
+ const ctx = makeCtx({ Resources: {} });
329
+ const diags = checkMissingDependsOn(ctx);
330
+ expect(diags).toHaveLength(0);
331
+ });
332
+
333
+ test("both patterns fire in same template", () => {
334
+ const ctx = makeCtx({
335
+ Resources: {
336
+ Listener: {
337
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
338
+ Properties: {},
339
+ },
340
+ Service: {
341
+ Type: "AWS::ECS::Service",
342
+ Properties: {
343
+ LoadBalancers: [{ TargetGroupArn: { Ref: "TG" } }],
344
+ },
345
+ },
346
+ Attachment: {
347
+ Type: "AWS::EC2::VPCGatewayAttachment",
348
+ Properties: {},
349
+ },
350
+ Route: {
351
+ Type: "AWS::EC2::Route",
352
+ Properties: {
353
+ GatewayId: { Ref: "MyIGW" },
354
+ DestinationCidrBlock: "0.0.0.0/0",
355
+ },
356
+ },
357
+ },
358
+ });
359
+ const diags = checkMissingDependsOn(ctx);
360
+ expect(diags).toHaveLength(2);
361
+ const entities = diags.map((d) => d.entity).sort();
362
+ expect(entities).toEqual(["Route", "Service"]);
363
+ });
364
+
365
+ // --- API Gateway Deployment + Method pattern ---
366
+
367
+ test("API Gateway Deployment + Method, no DependsOn → warning", () => {
368
+ const ctx = makeCtx({
369
+ Resources: {
370
+ MyMethod: {
371
+ Type: "AWS::ApiGateway::Method",
372
+ Properties: { RestApiId: { Ref: "MyApi" } },
373
+ },
374
+ MyDeployment: {
375
+ Type: "AWS::ApiGateway::Deployment",
376
+ Properties: { RestApiId: { Ref: "MyApi" } },
377
+ },
378
+ },
379
+ });
380
+ const diags = checkMissingDependsOn(ctx);
381
+ expect(diags).toHaveLength(1);
382
+ expect(diags[0].checkId).toBe("WAW030");
383
+ expect(diags[0].severity).toBe("warning");
384
+ expect(diags[0].message).toContain("MyDeployment");
385
+ expect(diags[0].message).toContain("Method");
386
+ expect(diags[0].entity).toBe("MyDeployment");
387
+ });
388
+
389
+ test("API Gateway Deployment + Method, with DependsOn → no diagnostic", () => {
390
+ const ctx = makeCtx({
391
+ Resources: {
392
+ MyMethod: {
393
+ Type: "AWS::ApiGateway::Method",
394
+ Properties: { RestApiId: { Ref: "MyApi" } },
395
+ },
396
+ MyDeployment: {
397
+ Type: "AWS::ApiGateway::Deployment",
398
+ DependsOn: "MyMethod",
399
+ Properties: { RestApiId: { Ref: "MyApi" } },
400
+ },
401
+ },
402
+ });
403
+ const diags = checkMissingDependsOn(ctx);
404
+ expect(diags).toHaveLength(0);
405
+ });
406
+
407
+ test("API Gateway Deployment + Method, with property ref to Method → no diagnostic", () => {
408
+ const ctx = makeCtx({
409
+ Resources: {
410
+ MyMethod: {
411
+ Type: "AWS::ApiGateway::Method",
412
+ Properties: { RestApiId: { Ref: "MyApi" } },
413
+ },
414
+ MyDeployment: {
415
+ Type: "AWS::ApiGateway::Deployment",
416
+ Properties: { RestApiId: { Ref: "MyApi" }, StageName: { Ref: "MyMethod" } },
417
+ },
418
+ },
419
+ });
420
+ const diags = checkMissingDependsOn(ctx);
421
+ expect(diags).toHaveLength(0);
422
+ });
423
+
424
+ test("API Gateway Deployment without any Methods in template → no diagnostic", () => {
425
+ const ctx = makeCtx({
426
+ Resources: {
427
+ MyDeployment: {
428
+ Type: "AWS::ApiGateway::Deployment",
429
+ Properties: { RestApiId: { Ref: "MyApi" } },
430
+ },
431
+ },
432
+ });
433
+ const diags = checkMissingDependsOn(ctx);
434
+ expect(diags).toHaveLength(0);
435
+ });
436
+
437
+ test("no API Gateway Deployment → no diagnostic", () => {
438
+ const ctx = makeCtx({
439
+ Resources: {
440
+ MyMethod: {
441
+ Type: "AWS::ApiGateway::Method",
442
+ Properties: { RestApiId: { Ref: "MyApi" } },
443
+ },
444
+ },
445
+ });
446
+ const diags = checkMissingDependsOn(ctx);
447
+ expect(diags).toHaveLength(0);
448
+ });
449
+
450
+ // --- API Gateway V2 Deployment + Route pattern ---
451
+
452
+ test("API Gateway V2 Deployment + Route, no DependsOn → warning", () => {
453
+ const ctx = makeCtx({
454
+ Resources: {
455
+ MyRoute: {
456
+ Type: "AWS::ApiGatewayV2::Route",
457
+ Properties: { ApiId: { Ref: "MyApi" } },
458
+ },
459
+ MyDeployment: {
460
+ Type: "AWS::ApiGatewayV2::Deployment",
461
+ Properties: { ApiId: { Ref: "MyApi" } },
462
+ },
463
+ },
464
+ });
465
+ const diags = checkMissingDependsOn(ctx);
466
+ expect(diags).toHaveLength(1);
467
+ expect(diags[0].checkId).toBe("WAW030");
468
+ expect(diags[0].severity).toBe("warning");
469
+ expect(diags[0].message).toContain("MyDeployment");
470
+ expect(diags[0].message).toContain("Route");
471
+ expect(diags[0].entity).toBe("MyDeployment");
472
+ });
473
+
474
+ test("API Gateway V2 Deployment + Route, with DependsOn → no diagnostic", () => {
475
+ const ctx = makeCtx({
476
+ Resources: {
477
+ MyRoute: {
478
+ Type: "AWS::ApiGatewayV2::Route",
479
+ Properties: { ApiId: { Ref: "MyApi" } },
480
+ },
481
+ MyDeployment: {
482
+ Type: "AWS::ApiGatewayV2::Deployment",
483
+ DependsOn: "MyRoute",
484
+ Properties: { ApiId: { Ref: "MyApi" } },
485
+ },
486
+ },
487
+ });
488
+ const diags = checkMissingDependsOn(ctx);
489
+ expect(diags).toHaveLength(0);
490
+ });
491
+
492
+ test("API Gateway V2 Deployment without Routes in template → no diagnostic", () => {
493
+ const ctx = makeCtx({
494
+ Resources: {
495
+ MyDeployment: {
496
+ Type: "AWS::ApiGatewayV2::Deployment",
497
+ Properties: { ApiId: { Ref: "MyApi" } },
498
+ },
499
+ },
500
+ });
501
+ const diags = checkMissingDependsOn(ctx);
502
+ expect(diags).toHaveLength(0);
503
+ });
504
+
505
+ // --- DynamoDB Table + ScalableTarget pattern ---
506
+
507
+ test("ScalableTarget (dynamodb) + Table, no DependsOn → warning", () => {
508
+ const ctx = makeCtx({
509
+ Resources: {
510
+ MyTable: {
511
+ Type: "AWS::DynamoDB::Table",
512
+ Properties: { TableName: "my-table" },
513
+ },
514
+ MyTarget: {
515
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
516
+ Properties: {
517
+ ServiceNamespace: "dynamodb",
518
+ ScalableDimension: "dynamodb:table:ReadCapacityUnits",
519
+ ResourceId: "table/my-table",
520
+ },
521
+ },
522
+ },
523
+ });
524
+ const diags = checkMissingDependsOn(ctx);
525
+ expect(diags).toHaveLength(1);
526
+ expect(diags[0].checkId).toBe("WAW030");
527
+ expect(diags[0].severity).toBe("warning");
528
+ expect(diags[0].message).toContain("MyTarget");
529
+ expect(diags[0].message).toContain("DynamoDB");
530
+ expect(diags[0].entity).toBe("MyTarget");
531
+ });
532
+
533
+ test("ScalableTarget (dynamodb) + Table, with DependsOn → no diagnostic", () => {
534
+ const ctx = makeCtx({
535
+ Resources: {
536
+ MyTable: {
537
+ Type: "AWS::DynamoDB::Table",
538
+ Properties: { TableName: "my-table" },
539
+ },
540
+ MyTarget: {
541
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
542
+ DependsOn: "MyTable",
543
+ Properties: {
544
+ ServiceNamespace: "dynamodb",
545
+ ScalableDimension: "dynamodb:table:ReadCapacityUnits",
546
+ ResourceId: "table/my-table",
547
+ },
548
+ },
549
+ },
550
+ });
551
+ const diags = checkMissingDependsOn(ctx);
552
+ expect(diags).toHaveLength(0);
553
+ });
554
+
555
+ test("ScalableTarget (dynamodb) + Table, with property ref to Table → no diagnostic", () => {
556
+ const ctx = makeCtx({
557
+ Resources: {
558
+ MyTable: {
559
+ Type: "AWS::DynamoDB::Table",
560
+ Properties: { TableName: "my-table" },
561
+ },
562
+ MyTarget: {
563
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
564
+ Properties: {
565
+ ServiceNamespace: "dynamodb",
566
+ ScalableDimension: "dynamodb:table:ReadCapacityUnits",
567
+ ResourceId: { "Fn::Sub": ["table/${TableName}", { TableName: { Ref: "MyTable" } }] },
568
+ },
569
+ },
570
+ },
571
+ });
572
+ const diags = checkMissingDependsOn(ctx);
573
+ expect(diags).toHaveLength(0);
574
+ });
575
+
576
+ test("ScalableTarget with non-dynamodb namespace → no diagnostic for dynamodb pattern", () => {
577
+ const ctx = makeCtx({
578
+ Resources: {
579
+ MyTable: {
580
+ Type: "AWS::DynamoDB::Table",
581
+ Properties: { TableName: "my-table" },
582
+ },
583
+ MyTarget: {
584
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
585
+ Properties: {
586
+ ServiceNamespace: "ecs",
587
+ ScalableDimension: "ecs:service:DesiredCount",
588
+ ResourceId: "service/my-cluster/my-service",
589
+ },
590
+ },
591
+ },
592
+ });
593
+ const diags = checkMissingDependsOn(ctx);
594
+ // Should not fire for dynamodb pattern (no ECS Service in template either)
595
+ expect(diags).toHaveLength(0);
596
+ });
597
+
598
+ test("ScalableTarget (dynamodb) but no Table in template → no diagnostic", () => {
599
+ const ctx = makeCtx({
600
+ Resources: {
601
+ MyTarget: {
602
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
603
+ Properties: {
604
+ ServiceNamespace: "dynamodb",
605
+ ScalableDimension: "dynamodb:table:ReadCapacityUnits",
606
+ ResourceId: "table/my-table",
607
+ },
608
+ },
609
+ },
610
+ });
611
+ const diags = checkMissingDependsOn(ctx);
612
+ expect(diags).toHaveLength(0);
613
+ });
614
+
615
+ // --- ECS Service + ScalableTarget pattern ---
616
+
617
+ test("ScalableTarget (ecs) + ECS Service, no DependsOn → warning", () => {
618
+ const ctx = makeCtx({
619
+ Resources: {
620
+ MyService: {
621
+ Type: "AWS::ECS::Service",
622
+ Properties: { Cluster: "my-cluster" },
623
+ },
624
+ MyTarget: {
625
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
626
+ Properties: {
627
+ ServiceNamespace: "ecs",
628
+ ScalableDimension: "ecs:service:DesiredCount",
629
+ ResourceId: "service/my-cluster/my-service",
630
+ },
631
+ },
632
+ },
633
+ });
634
+ const diags = checkMissingDependsOn(ctx);
635
+ expect(diags).toHaveLength(1);
636
+ expect(diags[0].checkId).toBe("WAW030");
637
+ expect(diags[0].severity).toBe("warning");
638
+ expect(diags[0].message).toContain("MyTarget");
639
+ expect(diags[0].message).toContain("ECS");
640
+ expect(diags[0].entity).toBe("MyTarget");
641
+ });
642
+
643
+ test("ScalableTarget (ecs) + ECS Service, with DependsOn → no diagnostic", () => {
644
+ const ctx = makeCtx({
645
+ Resources: {
646
+ MyService: {
647
+ Type: "AWS::ECS::Service",
648
+ Properties: { Cluster: "my-cluster" },
649
+ },
650
+ MyTarget: {
651
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
652
+ DependsOn: "MyService",
653
+ Properties: {
654
+ ServiceNamespace: "ecs",
655
+ ScalableDimension: "ecs:service:DesiredCount",
656
+ ResourceId: "service/my-cluster/my-service",
657
+ },
658
+ },
659
+ },
660
+ });
661
+ const diags = checkMissingDependsOn(ctx);
662
+ expect(diags).toHaveLength(0);
663
+ });
664
+
665
+ test("ScalableTarget (ecs) but no ECS Service in template → no diagnostic", () => {
666
+ const ctx = makeCtx({
667
+ Resources: {
668
+ MyTarget: {
669
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
670
+ Properties: {
671
+ ServiceNamespace: "ecs",
672
+ ScalableDimension: "ecs:service:DesiredCount",
673
+ ResourceId: "service/my-cluster/my-service",
674
+ },
675
+ },
676
+ },
677
+ });
678
+ const diags = checkMissingDependsOn(ctx);
679
+ expect(diags).toHaveLength(0);
680
+ });
681
+
682
+ test("ScalableTarget with non-ecs namespace → no diagnostic for ecs pattern", () => {
683
+ const ctx = makeCtx({
684
+ Resources: {
685
+ MyService: {
686
+ Type: "AWS::ECS::Service",
687
+ Properties: { Cluster: "my-cluster" },
688
+ },
689
+ MyTarget: {
690
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
691
+ Properties: {
692
+ ServiceNamespace: "dynamodb",
693
+ ScalableDimension: "dynamodb:table:ReadCapacityUnits",
694
+ ResourceId: "table/my-table",
695
+ },
696
+ },
697
+ },
698
+ });
699
+ const diags = checkMissingDependsOn(ctx);
700
+ // Should not fire for ECS pattern (no DynamoDB Table in template either)
701
+ expect(diags).toHaveLength(0);
702
+ });
703
+
704
+ // --- Cross-pattern edge cases ---
705
+
706
+ test("all patterns missing DependsOn → all fire", () => {
707
+ const ctx = makeCtx({
708
+ Resources: {
709
+ Listener: {
710
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
711
+ Properties: {},
712
+ },
713
+ EcsService: {
714
+ Type: "AWS::ECS::Service",
715
+ Properties: {
716
+ LoadBalancers: [{ TargetGroupArn: { Ref: "TG" } }],
717
+ },
718
+ },
719
+ Attachment: {
720
+ Type: "AWS::EC2::VPCGatewayAttachment",
721
+ Properties: {},
722
+ },
723
+ Route: {
724
+ Type: "AWS::EC2::Route",
725
+ Properties: {
726
+ GatewayId: { Ref: "MyIGW" },
727
+ DestinationCidrBlock: "0.0.0.0/0",
728
+ },
729
+ },
730
+ Method: {
731
+ Type: "AWS::ApiGateway::Method",
732
+ Properties: { RestApiId: { Ref: "Api" } },
733
+ },
734
+ ApiDeployment: {
735
+ Type: "AWS::ApiGateway::Deployment",
736
+ Properties: { RestApiId: { Ref: "Api" } },
737
+ },
738
+ V2Route: {
739
+ Type: "AWS::ApiGatewayV2::Route",
740
+ Properties: { ApiId: { Ref: "HttpApi" } },
741
+ },
742
+ V2Deployment: {
743
+ Type: "AWS::ApiGatewayV2::Deployment",
744
+ Properties: { ApiId: { Ref: "HttpApi" } },
745
+ },
746
+ MyTable: {
747
+ Type: "AWS::DynamoDB::Table",
748
+ Properties: { TableName: "t" },
749
+ },
750
+ DynamoTarget: {
751
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
752
+ Properties: {
753
+ ServiceNamespace: "dynamodb",
754
+ ResourceId: "table/t",
755
+ },
756
+ },
757
+ EcsTarget: {
758
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
759
+ Properties: {
760
+ ServiceNamespace: "ecs",
761
+ ResourceId: "service/c/s",
762
+ },
763
+ },
764
+ },
765
+ });
766
+ const diags = checkMissingDependsOn(ctx);
767
+ expect(diags).toHaveLength(6);
768
+ const entities = diags.map((d) => d.entity).sort();
769
+ expect(entities).toEqual([
770
+ "ApiDeployment",
771
+ "DynamoTarget",
772
+ "EcsService",
773
+ "EcsTarget",
774
+ "Route",
775
+ "V2Deployment",
776
+ ]);
777
+ });
778
+
779
+ test("ScalableTarget with ScalableDimension fallback (no ServiceNamespace) → detects namespace", () => {
780
+ const ctx = makeCtx({
781
+ Resources: {
782
+ MyTable: {
783
+ Type: "AWS::DynamoDB::Table",
784
+ Properties: { TableName: "my-table" },
785
+ },
786
+ MyTarget: {
787
+ Type: "AWS::ApplicationAutoScaling::ScalableTarget",
788
+ Properties: {
789
+ ScalableDimension: "dynamodb:table:ReadCapacityUnits",
790
+ ResourceId: "table/my-table",
791
+ },
792
+ },
793
+ },
794
+ });
795
+ const diags = checkMissingDependsOn(ctx);
796
+ expect(diags).toHaveLength(1);
797
+ expect(diags[0].entity).toBe("MyTarget");
798
+ expect(diags[0].message).toContain("DynamoDB");
799
+ });
800
+ });