@kensio/yulin 1.20.15 → 1.20.16

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 (95) hide show
  1. package/README.md +13 -0
  2. package/dist/sdk/module/sim-sdk-module-client-interceptor.d.ts +5 -0
  3. package/dist/sdk/module/sim-sdk-module-client-interceptor.js +14 -0
  4. package/dist/sdk/module/sim-sdk-static-client-factory.d.ts +26 -0
  5. package/dist/sdk/module/sim-sdk-static-client-factory.js +59 -0
  6. package/dist/sdk/send-patch.d.ts +8 -0
  7. package/dist/sdk/send-patch.js +11 -0
  8. package/dist/service/aws/caller/sim-aws-caller-resolver.d.ts +28 -6
  9. package/dist/service/aws/caller/sim-aws-caller-resolver.js +18 -11
  10. package/dist/service/aws/caller/sim-aws-caller.d.ts +9 -0
  11. package/dist/service/aws/caller/sim-aws-resolved-caller.d.ts +19 -0
  12. package/dist/service/aws/caller/sim-aws-resolved-caller.js +26 -0
  13. package/dist/service/aws/factory/sim-aws-account-region-service-builder.d.ts +6 -0
  14. package/dist/service/aws/factory/sim-aws-account-region-service-builder.js +3 -0
  15. package/dist/service/aws/factory/sim-aws-account-service-cache.d.ts +6 -0
  16. package/dist/service/aws/factory/sim-aws-account-service-cache.js +5 -0
  17. package/dist/service/aws/factory/sim-aws-service-factory.d.ts +5 -0
  18. package/dist/service/aws/factory/sim-aws-service-factory.js +6 -0
  19. package/dist/service/aws/sim-aws-properties.d.ts +15 -0
  20. package/dist/service/aws/sim-aws.js +2 -1
  21. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.d.ts +30 -7
  22. package/dist/service/iam/authorize/context/sim-iam-auth-z-caller-context-builder.js +20 -11
  23. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.d.ts +2 -1
  24. package/dist/service/iam/authorize/context/sim-iam-auth-z-context-builder.js +5 -1
  25. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.d.ts +10 -0
  26. package/dist/service/iam/authorize/match/sim-iam-policy-principal-matcher.js +15 -3
  27. package/dist/service/iam/authorize/sim-iam-account-auth-z.d.ts +11 -4
  28. package/dist/service/iam/authorize/sim-iam-account-auth-z.js +5 -3
  29. package/dist/service/iam/authorize/sim-iam-authorizer.d.ts +2 -1
  30. package/dist/service/iam/authorize/sim-iam-authorizer.js +1 -0
  31. package/dist/service/iam/role/sim-iam-role-with-policy.factory.d.ts +9 -0
  32. package/dist/service/iam/role/sim-iam-role-with-policy.factory.js +4 -2
  33. package/dist/service/iam/sim-iam-account-parts.d.ts +8 -0
  34. package/dist/service/iam/sim-iam-account-parts.js +1 -0
  35. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.d.ts +9 -2
  36. package/dist/service/sts/auth-z/assume-role-auth-z-coordinator.js +2 -2
  37. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.d.ts +10 -4
  38. package/dist/service/sts/auth-z/assume-role-source-account-auth-z.js +8 -7
  39. package/dist/service/sts/auth-z/assume-role-target-auth-z.d.ts +2 -2
  40. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.d.ts +6 -2
  41. package/dist/service/sts/auth-z/assume-role-trust-policy-authorizer.js +14 -11
  42. package/dist/service/sts/command/assume-role/assume-role.handler.d.ts +5 -1
  43. package/dist/service/sts/command/assume-role/assume-role.handler.js +5 -2
  44. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.d.ts +5 -1
  45. package/dist/service/sts/command/get-caller-identity/get-caller-identity.handler.js +4 -1
  46. package/dist/service/sts/service-role/sim-service-role.js +5 -1
  47. package/dist/service/sts/sim-sts.d.ts +9 -1
  48. package/dist/service/sts/sim-sts.js +4 -0
  49. package/docs/README.md +54 -0
  50. package/docs/ai-skill/README.md +69 -0
  51. package/docs/cli/README.md +284 -0
  52. package/docs/factories/README.md +140 -0
  53. package/docs/lint/README.md +176 -0
  54. package/docs/non-aws-dependencies/README.md +261 -0
  55. package/docs/sdk/README.md +272 -0
  56. package/docs/serve/README.md +1126 -0
  57. package/docs/services/acm/README.md +823 -0
  58. package/docs/services/apigateway/README.md +1919 -0
  59. package/docs/services/apigatewayv2/README.md +2678 -0
  60. package/docs/services/athena/README.md +1044 -0
  61. package/docs/services/bedrock/README.md +350 -0
  62. package/docs/services/cloudformation/README.md +3440 -0
  63. package/docs/services/cloudfront/README.md +2874 -0
  64. package/docs/services/cloudwatch/README.md +420 -0
  65. package/docs/services/cognito/README.md +4923 -0
  66. package/docs/services/dynamodb/README.md +3784 -0
  67. package/docs/services/ecr/README.md +302 -0
  68. package/docs/services/ecs/README.md +2010 -0
  69. package/docs/services/elbv2/README.md +1897 -0
  70. package/docs/services/eventbridge/README.md +1110 -0
  71. package/docs/services/firehose/README.md +805 -0
  72. package/docs/services/glue/README.md +501 -0
  73. package/docs/services/iam/README.md +1283 -0
  74. package/docs/services/kinesis/README.md +454 -0
  75. package/docs/services/kms/README.md +659 -0
  76. package/docs/services/lambda/README.md +3645 -0
  77. package/docs/services/logs/README.md +868 -0
  78. package/docs/services/organizations/README.md +574 -0
  79. package/docs/services/personalize/README.md +835 -0
  80. package/docs/services/rekognition/README.md +1070 -0
  81. package/docs/services/route53/README.md +1783 -0
  82. package/docs/services/s3/README.md +3024 -0
  83. package/docs/services/scheduler/README.md +806 -0
  84. package/docs/services/secretsmanager/README.md +667 -0
  85. package/docs/services/ses/README.md +1011 -0
  86. package/docs/services/sns/README.md +1904 -0
  87. package/docs/services/sqs/README.md +1185 -0
  88. package/docs/services/ssm/README.md +1072 -0
  89. package/docs/services/stepfunctions/README.md +1332 -0
  90. package/docs/services/sts/README.md +245 -0
  91. package/docs/services/wafv2/README.md +1378 -0
  92. package/docs/terraform/README.md +248 -0
  93. package/docs/time/README.md +284 -0
  94. package/llms.txt +59 -0
  95. package/package.json +7 -4
@@ -0,0 +1,1185 @@
1
+ # Simulated SQS
2
+
3
+ Yulin includes a simulated Amazon SQS for tests and local development. Messages are held in memory,
4
+ hidden and released on the simulation's own clock, and every operation is authorized by simulated IAM.
5
+
6
+ Standard queues only. SQS-specific types are imported from the `@kensio/yulin/sqs` subpath.
7
+
8
+ ## Creating a queue and sending a message
9
+
10
+ ```typescript sim-sqs-send-and-receive
11
+ /**
12
+ * Sending a message to a simulated queue and receiving it.
13
+ */
14
+
15
+ import {
16
+ CreateQueueCommand,
17
+ DeleteMessageCommand,
18
+ ReceiveMessageCommand,
19
+ SendMessageCommand,
20
+ } from "@aws-sdk/client-sqs";
21
+
22
+ import { SimAws } from "@kensio/yulin";
23
+
24
+ const simAws = new SimAws();
25
+ const sqs = simAws.sqs();
26
+
27
+ const { QueueUrl } = await sqs.createQueue(
28
+ new CreateQueueCommand({ QueueName: "orders" }),
29
+ );
30
+
31
+ await sqs.sendMessage(
32
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }),
33
+ );
34
+
35
+ const received = await sqs.receiveMessage(
36
+ new ReceiveMessageCommand({ QueueUrl }),
37
+ );
38
+ const message = received.Messages?.[0];
39
+
40
+ console.log(message?.Body); // "order-1"
41
+
42
+ await sqs.deleteMessage(
43
+ new DeleteMessageCommand({
44
+ QueueUrl,
45
+ ReceiptHandle: message?.ReceiptHandle,
46
+ }),
47
+ );
48
+ ```
49
+
50
+ A queue URL is `https://sqs.<region>.amazonaws.com/<account-id>/<name>`, and the ARN is
51
+ `arn:aws:sqs:<region>:<account-id>:<name>`. An SQS ARN has no resource type in it. The queue name
52
+ follows the account id directly.
53
+
54
+ `CreateQueue` is idempotent, as it is on real AWS. A second request for the same name returns the
55
+ existing queue's URL when the attributes it names match, and fails with `QueueNameExists` when they
56
+ differ. A request naming no attributes always matches.
57
+
58
+ ## Visibility timeouts
59
+
60
+ A received message is hidden from other consumers for the queue's visibility timeout, 30 seconds by
61
+ default. The message records the instant it is hidden until. It becomes receivable again once
62
+ simulated time reaches that instant. Advancing the clock is all a test needs to watch an undeleted
63
+ message come back.
64
+
65
+ ```typescript sim-sqs-visibility-timeout
66
+ /**
67
+ * A message that was received but never deleted, coming back once its
68
+ * visibility timeout lapses.
69
+ */
70
+
71
+ import {
72
+ CreateQueueCommand,
73
+ ReceiveMessageCommand,
74
+ SendMessageCommand,
75
+ } from "@aws-sdk/client-sqs";
76
+
77
+ import { SimAws } from "@kensio/yulin";
78
+
79
+ const simAws = new SimAws();
80
+ const sqs = simAws.sqs();
81
+
82
+ const { QueueUrl } = await sqs.createQueue(
83
+ new CreateQueueCommand({
84
+ QueueName: "orders",
85
+ Attributes: { VisibilityTimeout: "30" },
86
+ }),
87
+ );
88
+
89
+ await sqs.sendMessage(
90
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }),
91
+ );
92
+
93
+ const first = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
94
+
95
+ console.log(first.Messages?.length); // 1
96
+
97
+ // The message is invisible to everyone else while the timeout runs.
98
+ const empty = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
99
+
100
+ console.log(empty.Messages); // undefined
101
+
102
+ await simAws.clock().advanceBy({ seconds: 31 });
103
+
104
+ const again = await sqs.receiveMessage(
105
+ new ReceiveMessageCommand({
106
+ QueueUrl,
107
+ MessageSystemAttributeNames: ["ApproximateReceiveCount"],
108
+ }),
109
+ );
110
+
111
+ console.log(again.Messages?.[0]?.Attributes?.["ApproximateReceiveCount"]); // "2"
112
+ ```
113
+
114
+ A receive request can override the timeout for the messages it takes with `VisibilityTimeout`, and a
115
+ consumer part way through a slow handler can ask for more time with `ChangeMessageVisibility`. The
116
+ new timeout runs from the moment of the change rather than from the receive, as it does on real AWS.
117
+ A timeout of zero gives the message straight back to the queue.
118
+
119
+ `ChangeMessageVisibility` on a message whose timeout has already lapsed fails with
120
+ `MessageNotInflight`. There is no timeout left to change, and real SQS answers the same way.
121
+
122
+ See [simulated time](https://yulinsim.dev/time/ "Simulated time docs") for what else the clock can do.
123
+
124
+ ## Receipt handles
125
+
126
+ Every receive issues a fresh receipt handle, and a delete has to use the handle from the most recent
127
+ receive of that message. A handle from an earlier receive is accepted and deletes nothing. Real SQS
128
+ accepts one too, and promises only that the message might not be deleted.
129
+
130
+ That is the failure a consumer slower than its visibility timeout hits. Its message went back on the
131
+ queue, someone else took it, and its own delete quietly does nothing.
132
+
133
+ ```typescript sim-sqs-stale-receipt-handle
134
+ /**
135
+ * A slow consumer deleting with a receipt handle another receive has
136
+ * superseded.
137
+ */
138
+
139
+ import {
140
+ CreateQueueCommand,
141
+ DeleteMessageCommand,
142
+ ReceiveMessageCommand,
143
+ SendMessageCommand,
144
+ } from "@aws-sdk/client-sqs";
145
+
146
+ import { SimAws } from "@kensio/yulin";
147
+
148
+ const simAws = new SimAws();
149
+ const sqs = simAws.sqs();
150
+
151
+ const { QueueUrl } = await sqs.createQueue(
152
+ new CreateQueueCommand({
153
+ QueueName: "orders",
154
+ Attributes: { VisibilityTimeout: "30" },
155
+ }),
156
+ );
157
+
158
+ await sqs.sendMessage(
159
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }),
160
+ );
161
+
162
+ const slow = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
163
+
164
+ // The slow consumer takes longer than the visibility timeout, and another
165
+ // consumer receives the message in the meantime.
166
+ await simAws.clock().advanceBy({ seconds: 31 });
167
+ await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
168
+
169
+ // The delete succeeds and deletes nothing.
170
+ await sqs.deleteMessage(
171
+ new DeleteMessageCommand({
172
+ QueueUrl,
173
+ ReceiptHandle: slow.Messages?.[0]?.ReceiptHandle,
174
+ }),
175
+ );
176
+
177
+ await simAws.clock().advanceBy({ seconds: 31 });
178
+
179
+ const still = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
180
+
181
+ console.log(still.Messages?.[0]?.Body); // "order-1"
182
+ ```
183
+
184
+ A handle whose visibility timeout has lapsed with nobody else having received the message since is
185
+ still the most recent one, so deleting with it works. A handle the queue never issued fails with
186
+ `ReceiptHandleIsInvalid`, and a repeated delete of a message already gone succeeds.
187
+
188
+ ## Dead-letter queues
189
+
190
+ A `RedrivePolicy` says where a message goes once a consumer has had enough attempts at it. Once a
191
+ message has been received `maxReceiveCount` times without being deleted, the next lapse of its
192
+ visibility timeout moves it to the queue named by `deadLetterTargetArn`. Advancing the clock drives
193
+ the move, as it drives the timeout itself.
194
+
195
+ ```typescript sim-sqs-dead-letter-queue
196
+ /**
197
+ * A message a consumer keeps failing on, ending up on the dead-letter queue.
198
+ */
199
+
200
+ import {
201
+ CreateQueueCommand,
202
+ GetQueueAttributesCommand,
203
+ ReceiveMessageCommand,
204
+ SendMessageCommand,
205
+ } from "@aws-sdk/client-sqs";
206
+
207
+ import { SimAws } from "@kensio/yulin";
208
+
209
+ const simAws = new SimAws();
210
+ const sqs = simAws.sqs();
211
+
212
+ // The dead-letter queue has to exist before anything can point at it.
213
+ const { QueueUrl: DeadLetterQueueUrl } = await sqs.createQueue(
214
+ new CreateQueueCommand({ QueueName: "orders-dlq" }),
215
+ );
216
+
217
+ const deadLetter = await sqs.getQueueAttributes(
218
+ new GetQueueAttributesCommand({
219
+ QueueUrl: DeadLetterQueueUrl,
220
+ AttributeNames: ["QueueArn"],
221
+ }),
222
+ );
223
+
224
+ const { QueueUrl } = await sqs.createQueue(
225
+ new CreateQueueCommand({
226
+ QueueName: "orders",
227
+ Attributes: {
228
+ VisibilityTimeout: "30",
229
+ RedrivePolicy: JSON.stringify({
230
+ deadLetterTargetArn: deadLetter.Attributes?.["QueueArn"],
231
+ maxReceiveCount: 3,
232
+ }),
233
+ },
234
+ }),
235
+ );
236
+
237
+ await sqs.sendMessage(
238
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }),
239
+ );
240
+
241
+ // A consumer takes the message and never gets as far as deleting it.
242
+ async function failToHandleMessage(): Promise<void> {
243
+ await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
244
+ await simAws.clock().advanceBy({ seconds: 31 });
245
+ }
246
+
247
+ await failToHandleMessage();
248
+ await failToHandleMessage();
249
+ await failToHandleMessage();
250
+
251
+ // The source queue has given up on it.
252
+ const empty = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
253
+
254
+ console.log(empty.Messages); // undefined
255
+
256
+ const dead = await sqs.receiveMessage(
257
+ new ReceiveMessageCommand({ QueueUrl: DeadLetterQueueUrl }),
258
+ );
259
+
260
+ console.log(dead.Messages?.[0]?.Body); // "order-1"
261
+ ```
262
+
263
+ The message keeps its `MessageId`, body and message attributes, and a test can identify it by any of
264
+ them. `ApproximateNumberOfMessages` on both queues reflects the move. A message deleted before its
265
+ attempts run out never gets there, and a message still inside its visibility timeout has not moved
266
+ yet, because the consumer holding it may still delete it.
267
+
268
+ `SentTimestamp` is unchanged by the move, as it is on a real standard queue. The dead-letter queue's
269
+ `MessageRetentionPeriod` therefore runs from when the message was first sent. That is why AWS
270
+ suggests giving a dead-letter queue a longer retention period than the queue feeding it.
271
+ `ApproximateReceiveCount` starts again from one, since a receive count counts receives from one queue
272
+ and the message has not been received from this one yet. A moved message also reports
273
+ `DeadLetterQueueSourceArn`, naming the queue it came from.
274
+
275
+ The policy is validated when it is set, whether by `CreateQueue` or `SetQueueAttributes`. It has to
276
+ be a JSON object with both a `deadLetterTargetArn` and a `maxReceiveCount` between 1 and 1000,
277
+ carried as a JSON number or as a string holding one. The `deadLetterTargetArn` has to name a queue
278
+ that already exists in the same account and region, as real SQS requires. A policy pointing at
279
+ nothing fails there and then, before a message has been lost to it. Anything else fails with
280
+ `InvalidParameterValue`.
281
+
282
+ `GetQueueAttributes` reports `RedrivePolicy` back as the string it was set with.
283
+
284
+ ## Delays
285
+
286
+ `DelaySeconds` hides a new message until it lapses, either set on the queue for every message or on
287
+ one message as it is sent.
288
+
289
+ ```typescript sim-sqs-delayed-message
290
+ /**
291
+ * A message that cannot be received until the delay it was sent with lapses.
292
+ */
293
+
294
+ import {
295
+ CreateQueueCommand,
296
+ ReceiveMessageCommand,
297
+ SendMessageCommand,
298
+ } from "@aws-sdk/client-sqs";
299
+
300
+ import { SimAws } from "@kensio/yulin";
301
+
302
+ const simAws = new SimAws();
303
+ const sqs = simAws.sqs();
304
+
305
+ const { QueueUrl } = await sqs.createQueue(
306
+ new CreateQueueCommand({ QueueName: "orders" }),
307
+ );
308
+
309
+ await sqs.sendMessage(
310
+ new SendMessageCommand({
311
+ QueueUrl,
312
+ MessageBody: "order-1",
313
+ DelaySeconds: 60,
314
+ }),
315
+ );
316
+
317
+ const early = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
318
+
319
+ console.log(early.Messages); // undefined
320
+
321
+ await simAws.clock().advanceBy({ seconds: 61 });
322
+
323
+ const late = await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
324
+
325
+ console.log(late.Messages?.[0]?.Body); // "order-1"
326
+ ```
327
+
328
+ `MessageRetentionPeriod` works on the same clock. A message on the queue for longer than the
329
+ retention period (four days by default) is gone.
330
+
331
+ ## Message attributes
332
+
333
+ Message attributes round-trip, and both digests are computed. `MD5OfMessageBody` is an MD5 of the
334
+ body, and `MD5OfMessageAttributes` uses the length-prefixed encoding real SQS digests attributes
335
+ with. A consumer checking either against its own digest is checking a real MD5.
336
+
337
+ A receive returns no attributes unless it names them, as on real AWS. `All` or `.*` selects every
338
+ attribute, a bare name selects one, and a name ending in `.*` selects a prefix.
339
+
340
+ ```typescript sim-sqs-message-attributes
341
+ /**
342
+ * Message attributes on a simulated queue, and asking for them back.
343
+ */
344
+
345
+ import {
346
+ CreateQueueCommand,
347
+ ReceiveMessageCommand,
348
+ SendMessageCommand,
349
+ } from "@aws-sdk/client-sqs";
350
+
351
+ import { SimAws } from "@kensio/yulin";
352
+
353
+ const simAws = new SimAws();
354
+ const sqs = simAws.sqs();
355
+
356
+ const { QueueUrl } = await sqs.createQueue(
357
+ new CreateQueueCommand({ QueueName: "orders" }),
358
+ );
359
+
360
+ const sent = await sqs.sendMessage(
361
+ new SendMessageCommand({
362
+ QueueUrl,
363
+ MessageBody: "order-1",
364
+ MessageAttributes: {
365
+ tenant: { DataType: "String", StringValue: "acme" },
366
+ attempt: { DataType: "Number", StringValue: "1" },
367
+ },
368
+ }),
369
+ );
370
+
371
+ const received = await sqs.receiveMessage(
372
+ new ReceiveMessageCommand({ QueueUrl, MessageAttributeNames: ["All"] }),
373
+ );
374
+ const message = received.Messages?.[0];
375
+
376
+ console.log(message?.MessageAttributes?.["tenant"]?.StringValue); // "acme"
377
+ console.log(message?.MD5OfMessageAttributes === sent.MD5OfMessageAttributes); // true
378
+ console.log(message?.MD5OfBody === sent.MD5OfMessageBody); // true
379
+ ```
380
+
381
+ The name and data type rules are the real ones. A data type is `String`, `Number` or `Binary`, and
382
+ each takes a custom label after a dot, so `Number.int` is a number as far as the rules go. A
383
+ reserved `AWS.` or `Amazon.` prefix on a name, a data type built on none of the three, or a value
384
+ that disagrees with its data type is refused. A test finds any of those without going near AWS.
385
+
386
+ The message system attributes are asked for separately, with `MessageSystemAttributeNames`, or with
387
+ the discontinued `AttributeNames` that means the same thing. `SentTimestamp`,
388
+ `ApproximateReceiveCount` and `ApproximateFirstReceiveTimestamp` are reported.
389
+
390
+ ## Queue attributes
391
+
392
+ `GetQueueAttributes` returns only the attributes a request names, as real SQS does, and `All` names
393
+ every attribute this simulation holds. The defaults are the AWS ones. `VisibilityTimeout` is 30,
394
+ `DelaySeconds` 0, `MessageRetentionPeriod` 345600, `MaximumMessageSize` 262144 and
395
+ `ReceiveMessageWaitTimeSeconds` 0.
396
+
397
+ ```typescript sim-sqs-queue-attributes
398
+ /**
399
+ * Reading the counts and settings of a simulated queue.
400
+ */
401
+
402
+ import {
403
+ CreateQueueCommand,
404
+ GetQueueAttributesCommand,
405
+ ReceiveMessageCommand,
406
+ SendMessageCommand,
407
+ SetQueueAttributesCommand,
408
+ } from "@aws-sdk/client-sqs";
409
+
410
+ import { SimAws } from "@kensio/yulin";
411
+
412
+ const simAws = new SimAws();
413
+ const sqs = simAws.sqs();
414
+
415
+ const { QueueUrl } = await sqs.createQueue(
416
+ new CreateQueueCommand({ QueueName: "orders" }),
417
+ );
418
+
419
+ await sqs.setQueueAttributes(
420
+ new SetQueueAttributesCommand({
421
+ QueueUrl,
422
+ Attributes: { VisibilityTimeout: "120" },
423
+ }),
424
+ );
425
+
426
+ await sqs.sendMessage(
427
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }),
428
+ );
429
+ await sqs.sendMessage(
430
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-2" }),
431
+ );
432
+ await sqs.receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
433
+
434
+ const read = await sqs.getQueueAttributes(
435
+ new GetQueueAttributesCommand({ QueueUrl, AttributeNames: ["All"] }),
436
+ );
437
+
438
+ console.log(read.Attributes?.["VisibilityTimeout"]); // "120"
439
+ console.log(read.Attributes?.["ApproximateNumberOfMessages"]); // "1"
440
+ console.log(read.Attributes?.["ApproximateNumberOfMessagesNotVisible"]); // "1"
441
+ console.log(read.Attributes?.["QueueArn"]); // "arn:aws:sqs:us-east-1:888888888888:orders"
442
+ ```
443
+
444
+ The other two settable attributes are JSON documents. `RedrivePolicy` is covered under
445
+ [dead-letter queues](#dead-letter-queues) above and `Policy` under
446
+ [queue policies](#queue-policies) below. Both are reported back as the string they were set with.
447
+
448
+ An attribute real SQS reports and this simulation does not model, `RedriveAllowPolicy` for one, is
449
+ left out of a response. Real SQS leaves out an attribute a queue has no value for in the same way.
450
+ Setting one is refused, because a queue that appeared to accept it would behave differently here than
451
+ on AWS.
452
+
453
+ `PurgeQueue` deletes everything on a queue, hidden messages included.
454
+
455
+ ## IAM permissions
456
+
457
+ Every operation is authorized against the queue's ARN, which carries the queue name with no resource
458
+ type in front of it. Two details of real SQS trip policies up:
459
+
460
+ - `ListQueues` has no resource type at all. A policy allowing it names `*`. A policy naming one
461
+ queue, or every queue in the Account and Region, grants no listing.
462
+ - The batch operations are authorized as their singular action. There is no `sqs:SendMessageBatch`,
463
+ `sqs:DeleteMessageBatch` or `sqs:ChangeMessageVisibilityBatch` action for a policy to name.
464
+
465
+ The queue's own policy is part of the decision too, covered under [queue policies](#queue-policies)
466
+ below. A caller with no permission is refused whether or not the queue exists, since a queue that is
467
+ not there has no policy to admit anyone with.
468
+
469
+ ```typescript sim-sqs-iam-policy
470
+ /**
471
+ * A Role allowed to consume from one simulated queue and nothing else.
472
+ */
473
+
474
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
475
+ import {
476
+ CreateQueueCommand,
477
+ ReceiveMessageCommand,
478
+ SendMessageCommand,
479
+ } from "@aws-sdk/client-sqs";
480
+
481
+ import { SimAws } from "@kensio/yulin";
482
+
483
+ const simAws = new SimAws();
484
+ const accountId = simAws.defaultAccountId;
485
+ const regionName = simAws.defaultRegionName;
486
+ const sqs = simAws.sqs();
487
+
488
+ const { QueueUrl } = await sqs.createQueue(
489
+ new CreateQueueCommand({ QueueName: "orders" }),
490
+ );
491
+
492
+ await sqs.sendMessage(
493
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }),
494
+ );
495
+
496
+ const role = await simAws.iam().createRole(
497
+ new CreateRoleCommand({
498
+ RoleName: "OrderConsumer",
499
+ AssumeRolePolicyDocument: JSON.stringify({
500
+ Version: "2012-10-17",
501
+ Statement: {
502
+ Effect: "Allow",
503
+ Principal: { AWS: `arn:aws:iam::${accountId}:root` },
504
+ Action: "sts:AssumeRole",
505
+ },
506
+ }),
507
+ }),
508
+ );
509
+
510
+ await simAws.iam().putRolePolicy(
511
+ new PutRolePolicyCommand({
512
+ RoleName: "OrderConsumer",
513
+ PolicyName: "ConsumeOrders",
514
+ PolicyDocument: JSON.stringify({
515
+ Version: "2012-10-17",
516
+ Statement: {
517
+ Effect: "Allow",
518
+ Action: ["sqs:ReceiveMessage", "sqs:DeleteMessage"],
519
+ // A queue ARN has no resource type: not "...:queue/orders".
520
+ Resource: `arn:aws:sqs:${regionName}:${accountId}:orders`,
521
+ },
522
+ }),
523
+ }),
524
+ );
525
+
526
+ const caller = { kind: "arn", arn: role.Role.Arn } as const;
527
+
528
+ const received = await sqs.receiveMessage(
529
+ new ReceiveMessageCommand({ QueueUrl }),
530
+ { caller },
531
+ );
532
+
533
+ console.log(received.Messages?.[0]?.Body); // "order-1"
534
+
535
+ try {
536
+ await sqs.sendMessage(
537
+ new SendMessageCommand({ QueueUrl, MessageBody: "order-2" }),
538
+ { caller },
539
+ );
540
+ } catch (error) {
541
+ console.log((error as Error).name); // "AccessDenied"
542
+ }
543
+ ```
544
+
545
+ ## Queue policies
546
+
547
+ A queue's `Policy` attribute is its resource policy, and simulated IAM evaluates it as one. It is
548
+ what admits a service principal such as `s3.amazonaws.com`, which owns no identity policies
549
+ anywhere. It is also half of what admits a principal from another account, which needs an identity
550
+ policy in its own account as well.
551
+
552
+ The policy is set with `CreateQueue` or `SetQueueAttributes` and read back with
553
+ `GetQueueAttributes`.
554
+
555
+ ```typescript sim-sqs-queue-policy
556
+ /**
557
+ * A queue policy admitting S3 to send to a queue, for one Bucket only.
558
+ */
559
+
560
+ import {
561
+ CreateQueueCommand,
562
+ SendMessageCommand,
563
+ SetQueueAttributesCommand,
564
+ } from "@aws-sdk/client-sqs";
565
+
566
+ import { SimAws } from "@kensio/yulin";
567
+
568
+ const simAws = new SimAws();
569
+ const sqs = simAws.sqs();
570
+ const queueArn = `arn:aws:sqs:${simAws.defaultRegionName}:${simAws.defaultAccountId}:orders`;
571
+
572
+ const { QueueUrl } = await sqs.createQueue(
573
+ new CreateQueueCommand({ QueueName: "orders" }),
574
+ );
575
+
576
+ await sqs.setQueueAttributes(
577
+ new SetQueueAttributesCommand({
578
+ QueueUrl,
579
+ Attributes: {
580
+ Policy: JSON.stringify({
581
+ Version: "2012-10-17",
582
+ Statement: [
583
+ {
584
+ Effect: "Allow",
585
+ Principal: { Service: "s3.amazonaws.com" },
586
+ Action: "sqs:SendMessage",
587
+ Resource: queueArn,
588
+ Condition: { ArnLike: { "aws:SourceArn": "arn:aws:s3:::uploads" } },
589
+ },
590
+ ],
591
+ }),
592
+ },
593
+ }),
594
+ );
595
+
596
+ // S3 has no identity policies anywhere, so the queue policy is the whole
597
+ // decision. What it is sending for goes in as aws:SourceArn.
598
+ const s3 = { kind: "service", service: "s3.amazonaws.com" } as const;
599
+
600
+ const sent = await sqs.sendMessage(
601
+ new SendMessageCommand({ QueueUrl, MessageBody: "uploads/order-1.json" }),
602
+ { caller: s3, sourceArn: "arn:aws:s3:::uploads" },
603
+ );
604
+
605
+ console.log(sent.MessageId !== undefined); // true
606
+
607
+ // A Bucket the condition does not cover is refused.
608
+ try {
609
+ await sqs.sendMessage(
610
+ new SendMessageCommand({ QueueUrl, MessageBody: "reports/order-1.json" }),
611
+ { caller: s3, sourceArn: "arn:aws:s3:::reports" },
612
+ );
613
+ } catch (error) {
614
+ console.log((error as Error).name); // "AccessDenied"
615
+ }
616
+ ```
617
+
618
+ `sourceArn` is what a request says it is being made on behalf of, and `sourceAccount` is the Account
619
+ owning that resource, supplied as `aws:SourceAccount`. A request that omits one leaves the key out
620
+ entirely, and a statement conditioned on it matches nothing.
621
+
622
+ A simulated S3 Bucket notifying a queue supplies both. The `ArnLike aws:SourceArn` condition CDK
623
+ writes and the `StringEquals aws:SourceAccount` guard AWS documents are each enough on their own.
624
+ See [Event notifications](https://yulinsim.dev/services/s3/#event-notifications) on the S3 page for the whole chain.
625
+
626
+ A caller from another account needs both sides to allow the request, as it does on real AWS. The
627
+ queue policy has to name the principal, and that principal's own account has to allow the action.
628
+ Either one on its own is a denial.
629
+
630
+ The policy is validated when it is set, by `CreateQueue` or `SetQueueAttributes`. A malformed
631
+ document fails there, before anything has been authorized against it. It has to be a JSON policy
632
+ document whose statements each carry an `Effect` of `Allow` or `Deny`, an `Action` or `NotAction`,
633
+ and a `Resource` or `NotResource`. Anything else fails with `InvalidAttributeValue`.
634
+
635
+ `GetQueueAttributes` reports `Policy` back as the string it was set with.
636
+
637
+ ## Batches
638
+
639
+ `SendMessageBatch` and `DeleteMessageBatch` take up to ten entries. An entry that fails on its own is
640
+ reported in `Failed` while the rest of the batch goes through, as real SQS reports it. An empty batch,
641
+ more than ten entries, a malformed entry id or two entries sharing an id fail the whole request.
642
+
643
+ ```typescript sim-sqs-send-message-batch
644
+ /**
645
+ * A batch send where one entry fails on its own.
646
+ */
647
+
648
+ import {
649
+ CreateQueueCommand,
650
+ SendMessageBatchCommand,
651
+ } from "@aws-sdk/client-sqs";
652
+
653
+ import { SimAws } from "@kensio/yulin";
654
+
655
+ const simAws = new SimAws();
656
+ const sqs = simAws.sqs();
657
+
658
+ const { QueueUrl } = await sqs.createQueue(
659
+ new CreateQueueCommand({
660
+ QueueName: "orders",
661
+ Attributes: { MaximumMessageSize: "1024" },
662
+ }),
663
+ );
664
+
665
+ const sent = await sqs.sendMessageBatch(
666
+ new SendMessageBatchCommand({
667
+ QueueUrl,
668
+ Entries: [
669
+ { Id: "one", MessageBody: "order-1" },
670
+ { Id: "two", MessageBody: "x".repeat(2048) },
671
+ ],
672
+ }),
673
+ );
674
+
675
+ console.log(sent.Successful?.map((entry) => entry.Id)); // ["one"]
676
+ console.log(sent.Failed?.[0]?.Code); // "InvalidParameterValue"
677
+ ```
678
+
679
+ ## Deleting a queue
680
+
681
+ `DeleteQueue` removes the queue and everything on it. Real SQS holds a deleted queue's name for 60
682
+ seconds, and so does this. Advancing simulated time past the hold frees the name. A stack redeployed
683
+ in the same test depends on that.
684
+
685
+ ```typescript sim-sqs-delete-queue
686
+ /**
687
+ * A deleted queue holding its name for a minute, as real SQS holds it.
688
+ */
689
+
690
+ import { CreateQueueCommand, DeleteQueueCommand } from "@aws-sdk/client-sqs";
691
+
692
+ import { SimAws } from "@kensio/yulin";
693
+ import { SimSqsQueueDeletedRecently } from "@kensio/yulin/sqs";
694
+
695
+ const simAws = new SimAws();
696
+ const sqs = simAws.sqs();
697
+
698
+ const { QueueUrl } = await sqs.createQueue(
699
+ new CreateQueueCommand({ QueueName: "orders" }),
700
+ );
701
+
702
+ await sqs.deleteQueue(new DeleteQueueCommand({ QueueUrl }));
703
+
704
+ try {
705
+ await sqs.createQueue(new CreateQueueCommand({ QueueName: "orders" }));
706
+ } catch (error) {
707
+ console.log(error instanceof SimSqsQueueDeletedRecently); // true
708
+ }
709
+
710
+ await simAws.clock().advanceBy({ seconds: 61 });
711
+
712
+ const recreated = await sqs.createQueue(
713
+ new CreateQueueCommand({ QueueName: "orders" }),
714
+ );
715
+
716
+ console.log(recreated.QueueUrl === QueueUrl); // true
717
+ ```
718
+
719
+ ## Scoping
720
+
721
+ Queues belong to an account and a region, as they do on real AWS. A queue name is unique within one
722
+ account and region and nowhere wider. The same name can name two different queues in two regions.
723
+
724
+ ```typescript sim-sqs-scoping
725
+ /**
726
+ * Simulated queues are scoped to an account and region.
727
+ */
728
+
729
+ import {
730
+ CreateQueueCommand,
731
+ GetQueueUrlCommand,
732
+ SendMessageCommand,
733
+ } from "@aws-sdk/client-sqs";
734
+
735
+ import { SimAws } from "@kensio/yulin";
736
+ import { SimSqsQueueDoesNotExist } from "@kensio/yulin/sqs";
737
+
738
+ const simAws = new SimAws();
739
+
740
+ const { QueueUrl } = await simAws
741
+ .account("222222222222")
742
+ .region("eu-west-2")
743
+ .sqs()
744
+ .createQueue(new CreateQueueCommand({ QueueName: "orders" }));
745
+
746
+ try {
747
+ await simAws
748
+ .account("222222222222")
749
+ .region("us-east-1")
750
+ .sqs()
751
+ .getQueueUrl(new GetQueueUrlCommand({ QueueName: "orders" }));
752
+ } catch (error) {
753
+ console.log(error instanceof SimSqsQueueDoesNotExist); // true
754
+ }
755
+
756
+ // A queue URL naming another Region reaches nothing either.
757
+ try {
758
+ await simAws
759
+ .account("222222222222")
760
+ .region("us-east-1")
761
+ .sqs()
762
+ .sendMessage(new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }));
763
+ } catch (error) {
764
+ console.log(error instanceof SimSqsQueueDoesNotExist); // true
765
+ }
766
+ ```
767
+
768
+ ## Inside a simulated Lambda handler
769
+
770
+ Function code requiring `@aws-sdk/client-sqs` is routed into the same simulated AWS environment, with
771
+ the function's execution role as the caller. A handler consuming a queue therefore has to be allowed
772
+ to, by that role's policy, the same as on real AWS.
773
+
774
+ ```typescript sim-sqs-lambda-consumer
775
+ /**
776
+ * A simulated Lambda handler consuming a message from a simulated queue.
777
+ */
778
+
779
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
780
+ import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";
781
+ import { CreateQueueCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
782
+
783
+ import { SimAws } from "@kensio/yulin";
784
+ import { makeLambdaCodeZip } from "@kensio/yulin/lambda";
785
+
786
+ const simAws = new SimAws();
787
+ const accountId = simAws.defaultAccountId;
788
+ const regionName = simAws.defaultRegionName;
789
+
790
+ const { QueueUrl } = await simAws
791
+ .sqs()
792
+ .createQueue(new CreateQueueCommand({ QueueName: "orders" }));
793
+
794
+ await simAws
795
+ .sqs()
796
+ .sendMessage(new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }));
797
+
798
+ const role = await simAws.iam().createRole(
799
+ new CreateRoleCommand({
800
+ RoleName: "OrderConsumerRole",
801
+ AssumeRolePolicyDocument: JSON.stringify({
802
+ Version: "2012-10-17",
803
+ Statement: {
804
+ Effect: "Allow",
805
+ Principal: { Service: "lambda.amazonaws.com" },
806
+ Action: "sts:AssumeRole",
807
+ },
808
+ }),
809
+ }),
810
+ );
811
+
812
+ await simAws.iam().putRolePolicy(
813
+ new PutRolePolicyCommand({
814
+ RoleName: "OrderConsumerRole",
815
+ PolicyName: "ConsumeOrders",
816
+ PolicyDocument: JSON.stringify({
817
+ Version: "2012-10-17",
818
+ Statement: {
819
+ Effect: "Allow",
820
+ Action: ["sqs:ReceiveMessage", "sqs:DeleteMessage"],
821
+ Resource: `arn:aws:sqs:${regionName}:${accountId}:orders`,
822
+ },
823
+ }),
824
+ }),
825
+ );
826
+
827
+ const handlerCode = [
828
+ 'const { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } = require("@aws-sdk/client-sqs");',
829
+ "exports.handler = async () => {",
830
+ " const client = new SQSClient({});",
831
+ " const received = await client.send(new ReceiveMessageCommand({",
832
+ " QueueUrl: process.env.QUEUE_URL,",
833
+ " }));",
834
+ " const message = received.Messages[0];",
835
+ " await client.send(new DeleteMessageCommand({",
836
+ " QueueUrl: process.env.QUEUE_URL,",
837
+ " ReceiptHandle: message.ReceiptHandle,",
838
+ " }));",
839
+ " return message.Body;",
840
+ "};",
841
+ ].join("\n");
842
+
843
+ await simAws.lambda().createFunction(
844
+ new CreateFunctionCommand({
845
+ FunctionName: "order-consumer",
846
+ Role: role.Role.Arn,
847
+ Handler: "index.handler",
848
+ Code: { ZipFile: makeLambdaCodeZip({ "index.js": handlerCode }) },
849
+ Environment: { Variables: { QUEUE_URL: QueueUrl! } },
850
+ }),
851
+ );
852
+
853
+ await simAws.backgroundTasksComplete();
854
+
855
+ const invoked = await simAws
856
+ .lambda()
857
+ .invoke(new InvokeCommand({ FunctionName: "order-consumer" }));
858
+
859
+ console.log(Buffer.from(invoked.Payload ?? []).toString("utf8")); // "\"order-1\""
860
+ ```
861
+
862
+ See [simulated Lambda](https://yulinsim.dev/services/lambda/ "Simulated Lambda docs") for how function code and execution roles
863
+ work. `SimSdk` interception works the same way. Intercepting `SQSClient` routes ordinary SDK code
864
+ into the simulation with nothing touching the network, covered under
865
+ [AWS SDK interception](https://yulinsim.dev/sdk/ "Simulated AWS SDK docs").
866
+
867
+ ## Triggering a Lambda from a queue
868
+
869
+ A Lambda event source mapping delivers messages from a queue to a function without anything calling
870
+ `ReceiveMessage` itself. Messages sent to the queue arrive at the handler as an SQS event, in
871
+ batches of up to `BatchSize`.
872
+
873
+ Delivery runs on the simulation's background scheduler. A test waits for it with
874
+ `simAws.backgroundTasksComplete()`.
875
+
876
+ ```typescript sim-sqs-lambda-event-source
877
+ /**
878
+ * A message sent to a queue reaching a Lambda through an event source mapping.
879
+ */
880
+
881
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
882
+ import {
883
+ CreateEventSourceMappingCommand,
884
+ CreateFunctionCommand,
885
+ } from "@aws-sdk/client-lambda";
886
+ import {
887
+ CreateQueueCommand,
888
+ ReceiveMessageCommand,
889
+ SendMessageCommand,
890
+ } from "@aws-sdk/client-sqs";
891
+
892
+ import { SimAws } from "@kensio/yulin";
893
+ import {
894
+ makeLambdaZipFileInput,
895
+ type SimLambdaSqsEvent,
896
+ } from "@kensio/yulin/lambda";
897
+
898
+ const simAws = new SimAws();
899
+ const queueArn = `arn:aws:sqs:${simAws.defaultRegionName}:${simAws.defaultAccountId}:orders`;
900
+
901
+ const { QueueUrl } = await simAws
902
+ .sqs()
903
+ .createQueue(new CreateQueueCommand({ QueueName: "orders" }));
904
+
905
+ const role = await simAws.iam().createRole(
906
+ new CreateRoleCommand({
907
+ RoleName: "OrderConsumerRole",
908
+ AssumeRolePolicyDocument: JSON.stringify({
909
+ Version: "2012-10-17",
910
+ Statement: {
911
+ Effect: "Allow",
912
+ Principal: { Service: "lambda.amazonaws.com" },
913
+ Action: "sts:AssumeRole",
914
+ },
915
+ }),
916
+ }),
917
+ );
918
+
919
+ // Lambda polls the queue as the execution role, so the role has to allow it.
920
+ await simAws.iam().putRolePolicy(
921
+ new PutRolePolicyCommand({
922
+ RoleName: "OrderConsumerRole",
923
+ PolicyName: "ConsumeOrders",
924
+ PolicyDocument: JSON.stringify({
925
+ Version: "2012-10-17",
926
+ Statement: {
927
+ Effect: "Allow",
928
+ Action: [
929
+ "sqs:ReceiveMessage",
930
+ "sqs:DeleteMessage",
931
+ "sqs:GetQueueAttributes",
932
+ ],
933
+ Resource: queueArn,
934
+ },
935
+ }),
936
+ }),
937
+ );
938
+
939
+ const consumed: string[] = [];
940
+
941
+ await simAws.lambda().createFunction(
942
+ new CreateFunctionCommand({
943
+ FunctionName: "order-consumer",
944
+ Role: role.Role.Arn,
945
+ Code: {
946
+ ZipFile: makeLambdaZipFileInput((event: SimLambdaSqsEvent) => {
947
+ for (const record of event.Records) {
948
+ consumed.push(record.body);
949
+ }
950
+ }),
951
+ },
952
+ }),
953
+ );
954
+
955
+ await simAws.lambda().createEventSourceMapping(
956
+ new CreateEventSourceMappingCommand({
957
+ EventSourceArn: queueArn,
958
+ FunctionName: "order-consumer",
959
+ }),
960
+ );
961
+
962
+ await simAws
963
+ .sqs()
964
+ .sendMessage(new SendMessageCommand({ QueueUrl, MessageBody: "order-1" }));
965
+
966
+ await simAws.backgroundTasksComplete();
967
+
968
+ console.log(consumed); // ["order-1"]
969
+
970
+ // The handler returned, so the message has been deleted from the queue.
971
+ const remaining = await simAws
972
+ .sqs()
973
+ .receiveMessage(new ReceiveMessageCommand({ QueueUrl }));
974
+
975
+ console.log(remaining.Messages); // undefined
976
+ ```
977
+
978
+ A handler that throws leaves the whole batch on the queue instead. The messages stay hidden until
979
+ their visibility timeout lapses, come back after that, and eventually move to the dead-letter queue
980
+ if the queue has a `RedrivePolicy`. That is the path any other failing consumer takes, and advancing
981
+ the clock is what drives it:
982
+
983
+ ```typescript
984
+ await simAws.clock().advanceBy({ seconds: 31 });
985
+ ```
986
+
987
+ See [simulated Lambda](https://yulinsim.dev/services/lambda/#triggering-a-function-from-an-sqs-queue "Simulated Lambda event
988
+ source mapping docs") for the event shape, partial batch failures, and the
989
+ `AWS::Lambda::EventSourceMapping` template resource.
990
+
991
+ ## Deploying a queue from CloudFormation
992
+
993
+ Simulated CloudFormation creates a queue from an `AWS::SQS::Queue` resource, in the stack's account
994
+ and region. The queue is created through `CreateQueue`. A template-created queue gets the same name
995
+ validation, the same attribute ranges and the same ARN and URL as one an SDK caller creates.
996
+
997
+ `Ref` on the resource gives the queue URL rather than its name or ARN, as it does on real AWS, and it
998
+ can be handed straight to `SendMessage`. `Fn::GetAtt … Arn`, `Fn::GetAtt … QueueName` and
999
+ `Fn::GetAtt … QueueUrl` give those.
1000
+
1001
+ ```typescript sim-sqs-cloudformation-queue
1002
+ /**
1003
+ * Deploying a queue from a CloudFormation template and sending to it.
1004
+ */
1005
+
1006
+ import { SendMessageCommand } from "@aws-sdk/client-sqs";
1007
+
1008
+ import { SimAws } from "@kensio/yulin";
1009
+
1010
+ const simAws = new SimAws();
1011
+
1012
+ const stack = await simAws.cloudFormation().deployTemplate({
1013
+ stackName: "orders-stack",
1014
+ template: {
1015
+ Resources: {
1016
+ OrdersQueue: {
1017
+ Type: "AWS::SQS::Queue",
1018
+ Properties: {
1019
+ QueueName: "orders",
1020
+ VisibilityTimeout: 120,
1021
+ MessageRetentionPeriod: 3600,
1022
+ },
1023
+ },
1024
+ },
1025
+ Outputs: {
1026
+ OrdersQueueUrl: {
1027
+ Value: { Ref: "OrdersQueue" },
1028
+ },
1029
+ OrdersQueueArn: {
1030
+ Value: { "Fn::GetAtt": ["OrdersQueue", "Arn"] },
1031
+ },
1032
+ },
1033
+ },
1034
+ });
1035
+
1036
+ await stack.waitForDeployComplete();
1037
+
1038
+ // Ref resolves to the queue URL, so it works as a SendMessage QueueUrl.
1039
+ const queueUrl = stack.output("OrdersQueueUrl");
1040
+
1041
+ await simAws
1042
+ .sqs()
1043
+ .sendMessage(
1044
+ new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: "order-1" }),
1045
+ );
1046
+
1047
+ console.log(stack.output("OrdersQueueArn"));
1048
+ // "arn:aws:sqs:us-east-1:888888888888:orders"
1049
+ ```
1050
+
1051
+ The properties applied to the queue are `VisibilityTimeout`, `DelaySeconds`,
1052
+ `MessageRetentionPeriod`, `MaximumMessageSize` and `ReceiveMessageWaitTimeSeconds`. Each is passed to
1053
+ `CreateQueue`, and a value outside the range real SQS accepts fails the resource.
1054
+
1055
+ A queue with no `QueueName` is named from the stack name and the logical ID. The queue above with its
1056
+ name left out would be `orders-stack-OrdersQueue`. Real CloudFormation adds random characters to
1057
+ that, which a template cannot predict either way. The generated name is trimmed to the 80 characters
1058
+ a queue name allows, ending in a hash of the untrimmed name so two long names that start the same
1059
+ stay apart.
1060
+
1061
+ `FifoQueue: true` fails the resource. Only standard queues are simulated, and a FIFO queue is named
1062
+ `<name>.fifo`, a name simulated SQS refuses to an SDK caller as well. There is no queue to create
1063
+ under the name the template gave it.
1064
+
1065
+ The properties this simulation has no behaviour for are a different case. The queue is created without
1066
+ them and each one is recorded in
1067
+ [`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without).
1068
+ A stack full of queues still deploys. Those properties are `RedrivePolicy`, `RedriveAllowPolicy`,
1069
+ `KmsMasterKeyId`, `KmsDataKeyReusePeriodSeconds`, `SqsManagedSseEnabled`,
1070
+ `ContentBasedDeduplication`, `DeduplicationScope`, `FifoThroughputLimit` and `Tags`. A property
1071
+ outside the `AWS::SQS::Queue` schema is recorded the same way.
1072
+
1073
+ `AWS::SQS::QueuePolicy` deploys the policy it names onto each queue in its `Queues` list, through
1074
+ `SetQueueAttributes`. A policy declared in a template is therefore validated and enforced exactly as
1075
+ one set through the SDK, and a document SQS would refuse fails the resource. `Queues` carries queue
1076
+ URLs, and `Ref` on an `AWS::SQS::Queue` gives one.
1077
+
1078
+ ```typescript
1079
+ {
1080
+ Type: "AWS::SQS::QueuePolicy",
1081
+ Properties: {
1082
+ Queues: [{ Ref: "OrdersQueue" }],
1083
+ PolicyDocument: {
1084
+ Version: "2012-10-17",
1085
+ Statement: [
1086
+ {
1087
+ Effect: "Allow",
1088
+ Principal: { Service: "s3.amazonaws.com" },
1089
+ Action: "sqs:SendMessage",
1090
+ Resource: { "Fn::GetAtt": ["OrdersQueue", "Arn"] },
1091
+ },
1092
+ ],
1093
+ },
1094
+ },
1095
+ }
1096
+ ```
1097
+
1098
+ CDK works without hand-editing. An `sqs.Queue` with `grantSendMessages(fn)` synthesises a template
1099
+ that deploys here, with the queue URL reaching the function through its environment and the grant
1100
+ policy naming the queue by the ARN `Fn::GetAtt` gives. A grant to a service principal synthesises an
1101
+ `AWS::SQS::QueuePolicy` alongside it, which deploys too.
1102
+
1103
+ ## Available functionality
1104
+
1105
+ Sim SQS currently supports:
1106
+
1107
+ - `CreateQueueCommand`, idempotent for a matching name and attributes, and `DeleteQueueCommand`
1108
+ - `GetQueueUrlCommand` and `ListQueuesCommand`, with a name prefix and paging
1109
+ - `GetQueueAttributesCommand`, `SetQueueAttributesCommand` and `PurgeQueueCommand`
1110
+ - `SendMessageCommand` and `SendMessageBatchCommand`, with per-message or per-queue delays
1111
+ - `ReceiveMessageCommand`, up to ten messages at a time, each under a fresh receipt handle
1112
+ - `DeleteMessageCommand`, `DeleteMessageBatchCommand` and `ChangeMessageVisibilityCommand`
1113
+ - Visibility timeouts, delays and message retention, all on the simulation's clock
1114
+ - `RedrivePolicy`, moving a message to its dead-letter queue once its receives run out
1115
+ - `MessageAttributes` round-tripping, with real `MD5OfMessageBody` and `MD5OfMessageAttributes` digests
1116
+ - The `SentTimestamp`, `ApproximateReceiveCount`, `ApproximateFirstReceiveTimestamp` and
1117
+ `DeadLetterQueueSourceArn` system attributes
1118
+ - Authorization of every operation by simulated IAM, against the real IAM action and queue ARN
1119
+ - The `Policy` attribute as the queue's resource policy, admitting another account's principal or a
1120
+ service principal, with `aws:SourceArn` and `aws:SourceAccount` conditions honoured
1121
+ - S3 event notifications sent to a queue, with the event document as the message body
1122
+ - Calls made from inside a simulated Lambda handler, authorized as the function's execution role
1123
+ - Lambda event source mappings, delivering messages to a simulated function and deleting the batches
1124
+ it handles
1125
+ - `AWS::SQS::Queue` and `AWS::SQS::QueuePolicy` in a CloudFormation or CDK template, with `Ref`
1126
+ giving the queue URL and `Fn::GetAtt` giving `Arn`, `QueueName` and `QueueUrl`
1127
+
1128
+ ## Limitations
1129
+
1130
+ Current documented limitations:
1131
+
1132
+ - Standard queues only. A queue name ending in `.fifo` is refused, as are `MessageGroupId`,
1133
+ `MessageDeduplicationId` and `ReceiveRequestAttemptId`.
1134
+ - Ordering and duplicates are stricter here than AWS promises. Messages come back oldest first, and a
1135
+ message is handed out to one consumer at a time. Real standard queues promise no ordering at all
1136
+ and guarantee at-least-once delivery, so a copy of a message can arrive twice there and messages
1137
+ can arrive out of order. Redelivery after a visibility timeout lapses is simulated, since that
1138
+ follows from the timeout. A duplicate arriving on its own is left out.
1139
+ - Long polling answers at once. `WaitTimeSeconds` and `ReceiveMessageWaitTimeSeconds` are accepted
1140
+ and validated, and a receive returns immediately. The whole simulation runs in the calling process,
1141
+ where a wait could only ever time out.
1142
+ - `DeleteQueue` and `PurgeQueue` take effect immediately, where real SQS may take up to 60 seconds
1143
+ over either. The 60-second hold on a deleted queue's name is simulated, so recreating a queue
1144
+ straight after deleting it fails with `QueueDeletedRecently` until the clock moves on.
1145
+ - Dead-letter queues are simulated for standard queues only, and only the `RedrivePolicy` half of
1146
+ them. Setting `RedriveAllowPolicy` through the SQS API is refused, and on an `AWS::SQS::Queue` it
1147
+ is recorded and the queue created without it. A dead-letter queue cannot restrict which queues may
1148
+ redrive to it either way. `ListDeadLetterSourceQueues` and the `StartMessageMoveTask` family for
1149
+ draining a dead-letter queue back to its source are absent. A test moves a redriven message back by
1150
+ sending it again.
1151
+ - `ApproximateReceiveCount` starting again from one on a dead-letter queue is this simulation's
1152
+ reading of SQS, and AWS documents no answer either way. A receive count counts receives from one
1153
+ queue, and the moved message has not been received from the dead-letter queue yet. `SentTimestamp`
1154
+ being unchanged by the move is documented AWS behaviour, and is simulated as such.
1155
+ - The `RedrivePolicy` property on `AWS::SQS::Queue` is left out, and an SDK call is what configures a
1156
+ dead-letter queue. The queue is created without the property and the omission is recorded in
1157
+ `stack.ignoredProperties`.
1158
+ - A queue policy is set through the `Policy` attribute only. `AddPermission` and `RemovePermission`,
1159
+ shorthands for writing one statement of it, are absent.
1160
+ - `GetQueueAttributes` reports the `Policy` string that was set. Real SQS re-serialises the document
1161
+ and adds an `Id` and a `Sid` to it, so what comes back there differs from what went in.
1162
+ - A request naming a `QueueOwnerAWSAccountId` other than the scope's own account is refused. A queue
1163
+ policy admits another account's principal to a queue here, and leaves that account's own queues
1164
+ unreachable from this one.
1165
+ - Encryption is left out. `KmsMasterKeyId`, `KmsDataKeyReusePeriodSeconds` and
1166
+ `SqsManagedSseEnabled` are recorded on an `AWS::SQS::Queue` and applied to nothing, and message
1167
+ bodies are held in process memory as they were sent. Anything sharing the process can read them.
1168
+ - Tags are left out. `TagQueue`, `UntagQueue` and `ListQueueTags` are absent, and `CreateQueue`
1169
+ refuses a `tags` parameter rather than dropping it.
1170
+ - `SenderId` is left out, because a simulated caller has no user or role id to report it as.
1171
+ `AWSTraceHeader` is left out too, and `MessageSystemAttributes` on a send are refused. Asking for
1172
+ any of them is accepted, as real SQS accepts a request for an attribute a message has no value for,
1173
+ and they are absent from the response.
1174
+ - SQS condition keys are left out, and a policy relying on one matches nothing. Ordinary condition
1175
+ operators on values sim IAM does supply work as usual.
1176
+ - `ChangeMessageVisibilityBatch` and the batch size limit (`BatchRequestTooLong`) are absent.
1177
+ - A Lambda event source mapping polls one batch at a time. Real Lambda runs several pollers at once
1178
+ and scales them with the queue, and what that concurrency does to ordering is invisible here. See
1179
+ [simulated Lambda](https://yulinsim.dev/services/lambda/#triggering-a-function-from-an-sqs-queue "Simulated Lambda event
1180
+ source mapping docs") for the rest of the mapping limitations.
1181
+ - `AWS::SQS::Queue` and `AWS::SQS::QueuePolicy` are the SQS resource types CloudFormation creates.
1182
+ Any other is reported as unsupported and skipped. A queue attribute outside the range real SQS
1183
+ accepts fails the resource, and a property it has no behaviour for is recorded and the queue
1184
+ created without it.
1185
+ - SQS is not served as an HTTP API by `serveSimAws`.