fauxqs 1.7.0 → 1.8.0

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.
package/README.md CHANGED
@@ -1,1071 +1,1163 @@
1
- # fauxqs
2
-
3
- Local SNS/SQS/S3 emulator for development and testing. Point your `@aws-sdk/client-sqs`, `@aws-sdk/client-sns`, and `@aws-sdk/client-s3` clients at fauxqs instead of real AWS.
4
-
5
- All state is in-memory. No persistence, no external storage dependencies.
6
-
7
- ## Table of Contents
8
-
9
- - [Installation](#installation)
10
- - [Usage](#usage)
11
- - [Running the server](#running-the-server)
12
- - [Running in the background](#running-in-the-background)
13
- - [Running with Docker](#running-with-docker)
14
- - [Running in Docker Compose](#running-in-docker-compose)
15
- - [Container-to-container S3 virtual-hosted-style](#container-to-container-s3-virtual-hosted-style)
16
- - [Configuring AWS SDK clients](#configuring-aws-sdk-clients)
17
- - [Programmatic usage](#programmatic-usage)
18
- - [Programmatic state setup](#programmatic-state-setup)
19
- - [Init config file](#init-config-file)
20
- - [Init config schema reference](#init-config-schema-reference)
21
- - [Message spy](#message-spy)
22
- - [Queue inspection](#queue-inspection)
23
- - [Configurable queue URL host](#configurable-queue-url-host)
24
- - [Region](#region)
25
- - [Supported API Actions](#supported-api-actions)
26
- - [SQS](#sqs)
27
- - [SNS](#sns)
28
- - [S3](#s3)
29
- - [STS](#sts)
30
- - [SQS Features](#sqs-features)
31
- - [SNS Features](#sns-features)
32
- - [S3 Features](#s3-features)
33
- - [S3 URL styles](#s3-url-styles)
34
- - [Using with AWS CLI](#using-with-aws-cli)
35
- - [Conventions](#conventions)
36
- - [Limitations](#limitations)
37
- - [Examples](#examples)
38
- - [Benchmarks](#benchmarks)
39
- - [License](#license)
40
-
41
- ## Installation
42
-
43
- **Docker** (recommended for standalone usage) — [Docker Hub](https://hub.docker.com/r/kibertoad/fauxqs):
44
-
45
- ```bash
46
- docker run -p 4566:4566 kibertoad/fauxqs
47
- ```
48
-
49
- **npm** (for embedded library usage or CLI):
50
-
51
- ```bash
52
- npm install fauxqs
53
- ```
54
-
55
- ## Usage
56
-
57
- ### Running the server
58
-
59
- ```bash
60
- npx fauxqs
61
- ```
62
-
63
- The server starts on port `4566` and handles SQS, SNS, and S3 on a single endpoint.
64
-
65
- #### Environment variables
66
-
67
- | Variable | Description | Default |
68
- |----------|-------------|---------|
69
- | `FAUXQS_PORT` | Port to listen on | `4566` |
70
- | `FAUXQS_HOST` | Host for queue URLs (`sqs.<region>.<host>` format) | `localhost` |
71
- | `FAUXQS_DEFAULT_REGION` | Fallback region for ARNs and URLs | `us-east-1` |
72
- | `FAUXQS_LOGGER` | Enable request logging (`true`/`false`) | `true` |
73
- | `FAUXQS_INIT` | Path to a JSON init config file (see [Init config file](#init-config-file)) | (none) |
74
- | `FAUXQS_DNS_NAME` | Domain that dnsmasq resolves (including all subdomains) to the container IP. Only needed when the container hostname doesn't match the docker-compose service name — e.g., when using `container_name` or running with plain `docker run`. In docker-compose the hostname is set to the service name automatically, so this is rarely needed. (Docker only) | container hostname |
75
- | `FAUXQS_DNS_UPSTREAM` | Where dnsmasq forwards non-fauxqs DNS queries (e.g., `registry.npmjs.org`). Change this if you're in a corporate network with an internal DNS server, or if you prefer a different public resolver like `1.1.1.1`. (Docker only) | `8.8.8.8` |
76
-
77
- ```bash
78
- FAUXQS_PORT=3000 FAUXQS_INIT=init.json npx fauxqs
79
- ```
80
-
81
- A health check is available at `GET /health`.
82
-
83
- ### Running in the background
84
-
85
- To keep fauxqs running while you work on your app or run tests repeatedly, start it as a background process:
86
-
87
- ```bash
88
- npx fauxqs &
89
- ```
90
-
91
- Or in a separate terminal:
92
-
93
- ```bash
94
- npx fauxqs
95
- ```
96
-
97
- All state accumulates in memory across requests, so queues, topics, and objects persist until the server is stopped.
98
-
99
- To stop the server:
100
-
101
- ```bash
102
- # If backgrounded in the same shell
103
- kill %1
104
-
105
- # Cross-platform, by port
106
- npx cross-port-killer 4566
107
- ```
108
-
109
- ### Running with Docker
110
-
111
- The official Docker image is available on Docker Hub:
112
-
113
- ```bash
114
- docker run -p 4566:4566 kibertoad/fauxqs
115
- ```
116
-
117
- With an init config file:
118
-
119
- ```bash
120
- docker run -p 4566:4566 \
121
- -v ./init.json:/app/init.json \
122
- -e FAUXQS_INIT=/app/init.json \
123
- kibertoad/fauxqs
124
- ```
125
-
126
- ### Running in Docker Compose
127
-
128
- Use the `kibertoad/fauxqs` image and mount a JSON init config to pre-create resources on startup:
129
-
130
- ```json
131
- // scripts/fauxqs/init.json
132
- {
133
- "queues": [
134
- {
135
- "name": "my-queue.fifo",
136
- "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
137
- },
138
- { "name": "my-dlq" }
139
- ],
140
- "topics": [{ "name": "my-events" }],
141
- "subscriptions": [{ "topic": "my-events", "queue": "my-dlq" }],
142
- "buckets": ["my-uploads"]
143
- }
144
- ```
145
-
146
- ```yaml
147
- # docker-compose.yml
148
- services:
149
- fauxqs:
150
- image: kibertoad/fauxqs:latest
151
- ports:
152
- - "4566:4566"
153
- environment:
154
- - FAUXQS_INIT=/app/init.json
155
- volumes:
156
- - ./scripts/fauxqs/init.json:/app/init.json
157
-
158
- app:
159
- # ...
160
- depends_on:
161
- fauxqs:
162
- condition: service_healthy
163
- ```
164
-
165
- The image has a built-in `HEALTHCHECK`, so `service_healthy` works without extra configuration in your compose file. Other containers reference fauxqs using the Docker service name (`http://fauxqs:4566`). The init config file creates all queues, topics, subscriptions, and buckets before the healthcheck passes, so dependent services start only after resources are ready.
166
-
167
- #### Container-to-container S3 virtual-hosted-style
168
-
169
- The Docker image includes a built-in DNS server ([dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html)) that resolves the container hostname and all its subdomains (e.g., `fauxqs`, `s3.fauxqs`, `my-bucket.s3.fauxqs`) to the container's own IP. This enables virtual-hosted-style S3 from other containers without `forcePathStyle`.
170
-
171
- To use it, assign fauxqs a static IP and point other containers' DNS to it:
172
-
173
- ```yaml
174
- # docker-compose.yml
175
- services:
176
- fauxqs:
177
- image: kibertoad/fauxqs:latest
178
- networks:
179
- default:
180
- ipv4_address: 10.0.0.2
181
- ports:
182
- - "4566:4566"
183
- environment:
184
- - FAUXQS_INIT=/app/init.json
185
- - FAUXQS_HOST=fauxqs
186
- volumes:
187
- - ./scripts/fauxqs/init.json:/app/init.json
188
-
189
- app:
190
- dns: 10.0.0.2
191
- depends_on:
192
- fauxqs:
193
- condition: service_healthy
194
- environment:
195
- - AWS_ENDPOINT=http://s3.fauxqs:4566
196
-
197
- networks:
198
- default:
199
- ipam:
200
- config:
201
- - subnet: 10.0.0.0/24
202
- ```
203
-
204
- From the `app` container, `my-bucket.s3.fauxqs` resolves to `10.0.0.2` (the fauxqs container), so virtual-hosted-style S3 works:
205
-
206
- ```typescript
207
- const s3 = new S3Client({
208
- endpoint: "http://s3.fauxqs:4566",
209
- region: "us-east-1",
210
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
211
- // No forcePathStyle needed!
212
- });
213
- ```
214
-
215
- The DNS server is configured automatically using the container hostname (which docker-compose sets to the service name), so in most setups no extra configuration is needed. See the [environment variables table](#environment-variables) for `FAUXQS_DNS_NAME` and `FAUXQS_DNS_UPSTREAM` if you need to override the defaults.
216
-
217
- ### Configuring AWS SDK clients
218
-
219
- Point your SDK clients at the local server:
220
-
221
- ```typescript
222
- import { SQSClient } from "@aws-sdk/client-sqs";
223
- import { SNSClient } from "@aws-sdk/client-sns";
224
- import { S3Client } from "@aws-sdk/client-s3";
225
-
226
- const sqsClient = new SQSClient({
227
- endpoint: "http://localhost:4566",
228
- region: "us-east-1",
229
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
230
- });
231
-
232
- const snsClient = new SNSClient({
233
- endpoint: "http://localhost:4566",
234
- region: "us-east-1",
235
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
236
- });
237
-
238
- // Using fauxqs.dev wildcard DNS — no helpers or forcePathStyle needed
239
- const s3Client = new S3Client({
240
- endpoint: "http://s3.localhost.fauxqs.dev:4566",
241
- region: "us-east-1",
242
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
243
- });
244
- ```
245
-
246
- Any credentials are accepted and never validated.
247
-
248
- > **Note:** The `fauxqs.dev` wildcard DNS (`*.localhost.fauxqs.dev` → `127.0.0.1`) replicates the approach [pioneered by LocalStack](https://hashnode.localstack.cloud/efficient-localstack-s3-endpoint-configuration) with `localhost.localstack.cloud`. A public DNS entry resolves all subdomains to localhost, so virtual-hosted-style S3 requests work without `/etc/hosts` changes, custom request handlers, or `forcePathStyle`. See [S3 URL styles](#s3-url-styles) for alternative approaches.
249
-
250
- ### Programmatic usage
251
-
252
- You can also embed fauxqs directly in your test suite:
253
-
254
- ```typescript
255
- import { startFauxqs } from "fauxqs";
256
-
257
- const server = await startFauxqs({ port: 4566, logger: false });
258
-
259
- console.log(server.address); // "http://127.0.0.1:4566"
260
- console.log(server.port); // 4566
261
-
262
- // point your SDK clients at server.address
263
-
264
- // clean up when done
265
- await server.stop();
266
- ```
267
-
268
- Pass `port: 0` to let the OS assign a random available port (useful in tests).
269
-
270
- #### Programmatic state setup
271
-
272
- The server object exposes methods for pre-creating resources without going through the SDK:
273
-
274
- ```typescript
275
- const server = await startFauxqs({ port: 0, logger: false });
276
-
277
- // Create individual resources
278
- server.createQueue("my-queue");
279
- server.createQueue("my-dlq", {
280
- attributes: { VisibilityTimeout: "60" },
281
- tags: { env: "test" },
282
- });
283
- server.createTopic("my-topic");
284
- server.subscribe({ topic: "my-topic", queue: "my-queue" });
285
- server.createBucket("my-bucket");
286
-
287
- // Create resources in a specific region
288
- server.createQueue("eu-queue", { region: "eu-west-1" });
289
- server.createTopic("eu-topic", { region: "eu-west-1" });
290
- server.subscribe({ topic: "eu-topic", queue: "eu-queue", region: "eu-west-1" });
291
-
292
- // Or create everything at once
293
- server.setup({
294
- queues: [
295
- { name: "orders" },
296
- { name: "notifications", attributes: { DelaySeconds: "5" } },
297
- { name: "eu-orders", region: "eu-west-1" },
298
- ],
299
- topics: [{ name: "events" }],
300
- subscriptions: [
301
- { topic: "events", queue: "orders" },
302
- { topic: "events", queue: "notifications" },
303
- ],
304
- buckets: ["uploads", "exports"],
305
- });
306
-
307
- // Reset all state between tests
308
- server.purgeAll();
309
- ```
310
-
311
- #### Init config file
312
-
313
- Create a JSON file to pre-create resources on startup. The file is validated on load — malformed configs produce a clear error instead of silent failures.
314
-
315
- ```json
316
- {
317
- "queues": [
318
- { "name": "orders" },
319
- { "name": "orders-dlq" },
320
- { "name": "orders.fifo", "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" } }
321
- ],
322
- "topics": [
323
- { "name": "events" }
324
- ],
325
- "subscriptions": [
326
- { "topic": "events", "queue": "orders" }
327
- ],
328
- "buckets": ["uploads", "exports"]
329
- }
330
- ```
331
-
332
- Pass it via the `FAUXQS_INIT` environment variable or the `init` option:
333
-
334
- ```bash
335
- FAUXQS_INIT=init.json npx fauxqs
336
- ```
337
-
338
- ```typescript
339
- const server = await startFauxqs({ init: "init.json" });
340
- // or inline:
341
- const server = await startFauxqs({
342
- init: { queues: [{ name: "my-queue" }], buckets: ["my-bucket"] },
343
- });
344
- ```
345
-
346
- #### Init config schema reference
347
-
348
- All top-level fields are optional. Resources are created in dependency order: queues, topics, subscriptions, buckets.
349
-
350
- ##### `queues`
351
-
352
- Array of queue objects.
353
-
354
- | Field | Type | Required | Description |
355
- |-------|------|----------|-------------|
356
- | `name` | `string` | Yes | Queue name. Use `.fifo` suffix for FIFO queues. |
357
- | `region` | `string` | No | Override the default region for this queue. The queue's ARN and URL will use this region. |
358
- | `attributes` | `Record<string, string>` | No | Queue attributes (see table below). |
359
- | `tags` | `Record<string, string>` | No | Key-value tags for the queue. |
360
-
361
- Supported queue attributes:
362
-
363
- | Attribute | Default | Range / Values |
364
- |-----------|---------|----------------|
365
- | `VisibilityTimeout` | `"30"` | `0` `43200` (seconds) |
366
- | `DelaySeconds` | `"0"` | `0` `900` (seconds) |
367
- | `MaximumMessageSize` | `"1048576"` | `1024` – `1048576` (bytes) |
368
- | `MessageRetentionPeriod` | `"345600"` | `60` – `1209600` (seconds) |
369
- | `ReceiveMessageWaitTimeSeconds` | `"0"` | `0` – `20` (seconds) |
370
- | `RedrivePolicy` | | JSON string: `{"deadLetterTargetArn": "arn:...", "maxReceiveCount": "5"}` |
371
- | `Policy` | — | Queue policy JSON string (stored, not enforced) |
372
- | `KmsMasterKeyId` | | KMS key ID (stored, no actual encryption) |
373
- | `KmsDataKeyReusePeriodSeconds` | | KMS data key reuse period (stored, no actual encryption) |
374
- | `FifoQueue` | | `"true"` for FIFO queues (queue name must end with `.fifo`) |
375
- | `ContentBasedDeduplication` | | `"true"` or `"false"` (FIFO queues only) |
376
-
377
- Example:
378
-
379
- ```json
380
- {
381
- "queues": [
382
- {
383
- "name": "orders",
384
- "attributes": { "VisibilityTimeout": "60", "DelaySeconds": "5" },
385
- "tags": { "env": "staging", "team": "platform" }
386
- },
387
- {
388
- "name": "orders-dlq"
389
- },
390
- {
391
- "name": "orders.fifo",
392
- "attributes": {
393
- "FifoQueue": "true",
394
- "ContentBasedDeduplication": "true"
395
- }
396
- },
397
- {
398
- "name": "retry-queue",
399
- "attributes": {
400
- "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-dlq\",\"maxReceiveCount\":\"3\"}"
401
- }
402
- },
403
- {
404
- "name": "eu-orders",
405
- "region": "eu-west-1"
406
- }
407
- ]
408
- }
409
- ```
410
-
411
- ##### `topics`
412
-
413
- Array of topic objects.
414
-
415
- | Field | Type | Required | Description |
416
- |-------|------|----------|-------------|
417
- | `name` | `string` | Yes | Topic name. Use `.fifo` suffix for FIFO topics. |
418
- | `region` | `string` | No | Override the default region for this topic. The topic's ARN will use this region. |
419
- | `attributes` | `Record<string, string>` | No | Topic attributes (e.g., `DisplayName`). |
420
- | `tags` | `Record<string, string>` | No | Key-value tags for the topic. |
421
-
422
- Example:
423
-
424
- ```json
425
- {
426
- "topics": [
427
- {
428
- "name": "events",
429
- "attributes": { "DisplayName": "Application Events" },
430
- "tags": { "env": "staging" }
431
- },
432
- {
433
- "name": "events.fifo",
434
- "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
435
- }
436
- ]
437
- }
438
- ```
439
-
440
- ##### `subscriptions`
441
-
442
- Array of subscription objects. Referenced topics and queues must be defined in the same config (or already exist on the server).
443
-
444
- | Field | Type | Required | Description |
445
- |-------|------|----------|-------------|
446
- | `topic` | `string` | Yes | Topic name (not ARN) to subscribe to. |
447
- | `queue` | `string` | Yes | Queue name (not ARN) to deliver messages to. |
448
- | `region` | `string` | No | Override the default region. The topic and queue ARNs will be resolved in this region. |
449
- | `attributes` | `Record<string, string>` | No | Subscription attributes (see table below). |
450
-
451
- Supported subscription attributes:
452
-
453
- | Attribute | Values | Description |
454
- |-----------|--------|-------------|
455
- | `RawMessageDelivery` | `"true"` / `"false"` | Deliver the raw message body instead of the SNS envelope JSON. |
456
- | `FilterPolicy` | JSON string | SNS filter policy for message filtering (e.g., `"{\"color\": [\"blue\"]}"`) |
457
- | `FilterPolicyScope` | `"MessageAttributes"` / `"MessageBody"` | Whether the filter policy applies to message attributes or body. Defaults to `MessageAttributes`. |
458
- | `RedrivePolicy` | JSON string | Subscription-level dead-letter queue config. |
459
- | `DeliveryPolicy` | JSON string | Delivery retry policy (stored, not enforced). |
460
- | `SubscriptionRoleArn` | ARN string | IAM role ARN for delivery (stored, not enforced). |
461
-
462
- Example:
463
-
464
- ```json
465
- {
466
- "subscriptions": [
467
- {
468
- "topic": "events",
469
- "queue": "orders",
470
- "attributes": {
471
- "RawMessageDelivery": "true",
472
- "FilterPolicy": "{\"eventType\": [\"order.created\", \"order.updated\"]}"
473
- }
474
- },
475
- {
476
- "topic": "events",
477
- "queue": "notifications"
478
- }
479
- ]
480
- }
481
- ```
482
-
483
- ##### `buckets`
484
-
485
- Array of bucket name strings.
486
-
487
- ```json
488
- {
489
- "buckets": ["uploads", "exports", "temp"]
490
- }
491
- ```
492
-
493
- #### Message spy
494
-
495
- `MessageSpyReader` lets you await specific events flowing through SQS, SNS, and S3 in your tests — without polling queues yourself. Inspired by `HandlerSpy` from `message-queue-toolkit`.
496
-
497
- Enable it with the `messageSpies` option:
498
-
499
- ```typescript
500
- const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
501
- ```
502
-
503
- The spy tracks events across all three services using a discriminated union on `service`:
504
-
505
- **SQS events** (`service: 'sqs'`):
506
- - **`published`** — message was enqueued (via SendMessage, SendMessageBatch, or SNS fan-out)
507
- - **`consumed`** message was deleted (via DeleteMessage / DeleteMessageBatch)
508
- - **`dlq`** — message exceeded `maxReceiveCount` and was moved to a dead-letter queue
509
-
510
- **SNS events** (`service: 'sns'`):
511
- - **`published`** — message was published to a topic (before fan-out to SQS subscriptions)
512
-
513
- **S3 events** (`service: 's3'`):
514
- - **`uploaded`** — object was put (PutObject or CompleteMultipartUpload)
515
- - **`downloaded`** — object was retrieved (GetObject)
516
- - **`deleted`** — object was deleted (DeleteObject, only when key existed)
517
- - **`copied`** — object was copied (CopyObject; also emits `uploaded` for the destination)
518
-
519
- ##### Awaiting messages
520
-
521
- ```typescript
522
- // Wait for a specific SQS message (resolves immediately if already in buffer)
523
- const msg = await server.spy.waitForMessage(
524
- (m) => m.service === "sqs" && m.body === "order.created" && m.queueName === "orders",
525
- "published",
526
- );
527
-
528
- // Wait by SQS message ID
529
- const msg = await server.spy.waitForMessageWithId(messageId, "consumed");
530
-
531
- // Partial object match (deep-equal on specified fields)
532
- const msg = await server.spy.waitForMessage({ service: "sqs", queueName: "orders", status: "published" });
533
-
534
- // Wait for an SNS publish event
535
- const msg = await server.spy.waitForMessage({ service: "sns", topicName: "my-topic", status: "published" });
536
-
537
- // Wait for an S3 upload event
538
- const msg = await server.spy.waitForMessage({ service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" });
539
- ```
540
-
541
- `waitForMessage` checks the buffer first (retroactive resolution). If no match is found, it returns a Promise that resolves when a matching message arrives.
542
-
543
- ##### Timeout
544
-
545
- All `waitForMessage` and `waitForMessageWithId` calls accept an optional `timeout` parameter (ms) as the third argument. If no matching message arrives in time, the promise rejects with a timeout error — preventing tests from hanging indefinitely:
546
-
547
- ```typescript
548
- // Reject after 2 seconds if no match
549
- const msg = await server.spy.waitForMessage(
550
- { service: "sqs", queueName: "orders" },
551
- "published",
552
- 2000,
553
- );
554
-
555
- // Also works with waitForMessageWithId
556
- const msg = await server.spy.waitForMessageWithId(messageId, "consumed", 5000);
557
- ```
558
-
559
- ##### Waiting for multiple messages
560
-
561
- `waitForMessages` collects `count` matching messages before resolving. It checks the buffer first, then awaits future arrivals:
562
-
563
- ```typescript
564
- // Wait for 3 messages on the orders queue
565
- const msgs = await server.spy.waitForMessages(
566
- { service: "sqs", queueName: "orders" },
567
- { count: 3, status: "published", timeout: 5000 },
568
- );
569
- // msgs.length === 3
570
- ```
571
-
572
- If the timeout expires before enough messages arrive, the promise rejects with a message showing how many were collected (e.g., `"collected 1/3"`).
573
-
574
- ##### Negative assertions
575
-
576
- `expectNoMessage` asserts that no matching message appears within a time window. Useful for verifying that filter policies dropped a message or that a side effect did not occur:
577
-
578
- ```typescript
579
- // Assert no message was delivered to the wrong queue (waits 200ms by default)
580
- await server.spy.expectNoMessage({ service: "sqs", queueName: "wrong-queue" });
581
-
582
- // Custom window and status filter
583
- await server.spy.expectNoMessage(
584
- { service: "sqs", queueName: "orders" },
585
- { status: "dlq", within: 500 },
586
- );
587
- ```
588
-
589
- If a matching message is already in the buffer, `expectNoMessage` rejects immediately. If one arrives during the wait, it rejects with `"matching message arrived during wait"`.
590
-
591
- ##### Synchronous check
592
-
593
- ```typescript
594
- const msg = server.spy.checkForMessage(
595
- (m) => m.service === "sqs" && m.queueName === "my-queue",
596
- "published",
597
- );
598
- // returns SpyMessage | undefined
599
- ```
600
-
601
- ##### Buffer management
602
-
603
- ```typescript
604
- // Get all tracked messages (oldest to newest)
605
- const all = server.spy.getAllMessages();
606
-
607
- // Clear buffer and reject pending waiters
608
- server.spy.clear();
609
- ```
610
-
611
- The buffer defaults to 100 messages (FIFO eviction). Configure with:
612
-
613
- ```typescript
614
- const server = await startFauxqs({
615
- messageSpies: { bufferSize: 500 },
616
- });
617
- ```
618
-
619
- ##### Types
620
-
621
- `server.spy` returns a `MessageSpyReader` — a read-only interface that exposes query and await methods but not internal mutation (e.g. recording new events):
622
-
623
- ```typescript
624
- interface MessageSpyReader {
625
- waitForMessage(filter: MessageSpyFilter, status?: string, timeout?: number): Promise<SpyMessage>;
626
- waitForMessageWithId(messageId: string, status?: string, timeout?: number): Promise<SpyMessage>;
627
- waitForMessages(filter: MessageSpyFilter, options: WaitForMessagesOptions): Promise<SpyMessage[]>;
628
- expectNoMessage(filter: MessageSpyFilter, options?: ExpectNoMessageOptions): Promise<void>;
629
- checkForMessage(filter: MessageSpyFilter, status?: string): SpyMessage | undefined;
630
- getAllMessages(): SpyMessage[];
631
- clear(): void;
632
- }
633
-
634
- interface WaitForMessagesOptions {
635
- count: number;
636
- status?: string;
637
- timeout?: number;
638
- }
639
-
640
- interface ExpectNoMessageOptions {
641
- status?: string;
642
- within?: number; // ms, defaults to 200
643
- }
644
- ```
645
-
646
- `SpyMessage` is a discriminated union:
647
-
648
- ```typescript
649
- interface SqsSpyMessage {
650
- service: "sqs";
651
- queueName: string;
652
- messageId: string;
653
- body: string;
654
- messageAttributes: Record<string, MessageAttributeValue>;
655
- status: "published" | "consumed" | "dlq";
656
- timestamp: number;
657
- }
658
-
659
- interface SnsSpyMessage {
660
- service: "sns";
661
- topicArn: string;
662
- topicName: string;
663
- messageId: string;
664
- body: string;
665
- messageAttributes: Record<string, MessageAttributeValue>;
666
- status: "published";
667
- timestamp: number;
668
- }
669
-
670
- interface S3SpyEvent {
671
- service: "s3";
672
- bucket: string;
673
- key: string;
674
- status: "uploaded" | "downloaded" | "deleted" | "copied";
675
- timestamp: number;
676
- }
677
-
678
- type SpyMessage = SqsSpyMessage | SnsSpyMessage | S3SpyEvent;
679
- ```
680
-
681
- ##### Spy disabled by default
682
-
683
- Accessing `server.spy` when `messageSpies` is not set throws an error. There is no overhead on the message flow when spies are disabled.
684
-
685
- #### Queue inspection
686
-
687
- Non-destructive inspection of SQS queue state — see all messages (ready, in-flight, and delayed) without consuming them or affecting visibility timeouts.
688
-
689
- ##### Programmatic API
690
-
691
- ```typescript
692
- const result = server.inspectQueue("my-queue");
693
- // result is undefined if queue doesn't exist
694
- if (result) {
695
- console.log(result.name); // "my-queue"
696
- console.log(result.url); // "http://sqs.us-east-1.localhost:4566/000000000000/my-queue"
697
- console.log(result.arn); // "arn:aws:sqs:us-east-1:000000000000:my-queue"
698
- console.log(result.attributes); // { VisibilityTimeout: "30", ... }
699
- console.log(result.messages.ready); // messages available for receive
700
- console.log(result.messages.delayed); // messages waiting for delay to expire
701
- console.log(result.messages.inflight); // received but not yet deleted
702
- // Each inflight entry includes: { message, receiptHandle, visibilityDeadline }
703
- }
704
- ```
705
-
706
- ##### HTTP endpoints
707
-
708
- ```bash
709
- # List all queues with summary counts
710
- curl http://localhost:4566/_fauxqs/queues
711
- # [{ "name": "my-queue", "approximateMessageCount": 5, "approximateInflightCount": 2, "approximateDelayedCount": 0, ... }]
712
-
713
- # Inspect a specific queue (full state)
714
- curl http://localhost:4566/_fauxqs/queues/my-queue
715
- # { "name": "my-queue", "messages": { "ready": [...], "delayed": [...], "inflight": [...] }, ... }
716
- ```
717
-
718
- Returns 404 for non-existent queues. Inspection never modifies queue state messages remain exactly where they are.
719
-
720
- ### Configurable queue URL host
721
-
722
- Queue URLs use the AWS-style `sqs.<region>.<host>` format. The `host` defaults to `localhost`, producing URLs like `http://sqs.us-east-1.localhost:4566/000000000000/myQueue`.
723
-
724
- To override the host (e.g., for a custom domain):
725
-
726
- ```typescript
727
- import { startFauxqs } from "fauxqs";
728
-
729
- const server = await startFauxqs({ port: 4566, host: "myhost.local" });
730
- // Queue URLs: http://sqs.us-east-1.myhost.local:4566/000000000000/myQueue
731
- ```
732
-
733
- This also works with `buildApp`:
734
-
735
- ```typescript
736
- import { buildApp } from "fauxqs";
737
-
738
- const app = buildApp({ host: "myhost.local" });
739
- ```
740
-
741
- The configured host ensures queue URLs are consistent across all creation paths (init config, programmatic API, and SDK requests), regardless of the request's `Host` header.
742
-
743
- ### Region
744
-
745
- Region is part of an entity's identity — a queue named `my-queue` in `us-east-1` is a completely different entity from `my-queue` in `eu-west-1`, just like in real AWS.
746
-
747
- The region used in ARNs and queue URLs is automatically detected from the SDK client's `Authorization` header (AWS SigV4 credential scope). If your SDK client is configured with `region: "eu-west-1"`, all entities created or looked up through that client will use `eu-west-1` in their ARNs and URLs.
748
-
749
- ```typescript
750
- const sqsEU = new SQSClient({ region: "eu-west-1", endpoint: "http://localhost:4566", ... });
751
- const sqsUS = new SQSClient({ region: "us-east-1", endpoint: "http://localhost:4566", ... });
752
-
753
- // These are two independent queues with different ARNs
754
- await sqsEU.send(new CreateQueueCommand({ QueueName: "orders" }));
755
- await sqsUS.send(new CreateQueueCommand({ QueueName: "orders" }));
756
- ```
757
-
758
- If the region cannot be resolved from request headers (e.g., requests without AWS SigV4 signing), the `defaultRegion` option is used as a fallback (defaults to `"us-east-1"`):
759
-
760
- ```typescript
761
- const server = await startFauxqs({ defaultRegion: "eu-west-1" });
762
- ```
763
-
764
- Resources created via init config or programmatic API use the `defaultRegion` unless overridden with an explicit `region` field:
765
-
766
- ```json
767
- {
768
- "queues": [
769
- { "name": "us-queue" },
770
- { "name": "eu-queue", "region": "eu-west-1" }
771
- ]
772
- }
773
- ```
774
-
775
- ## Supported API Actions
776
-
777
- ### SQS
778
-
779
- | Action | Supported |
780
- |--------|-----------|
781
- | CreateQueue | Yes |
782
- | DeleteQueue | Yes |
783
- | GetQueueUrl | Yes |
784
- | ListQueues | Yes |
785
- | GetQueueAttributes | Yes |
786
- | SetQueueAttributes | Yes |
787
- | PurgeQueue | Yes |
788
- | SendMessage | Yes |
789
- | SendMessageBatch | Yes |
790
- | ReceiveMessage | Yes |
791
- | DeleteMessage | Yes |
792
- | DeleteMessageBatch | Yes |
793
- | ChangeMessageVisibility | Yes |
794
- | ChangeMessageVisibilityBatch | Yes |
795
- | TagQueue | Yes |
796
- | UntagQueue | Yes |
797
- | ListQueueTags | Yes |
798
- | AddPermission | No |
799
- | RemovePermission | No |
800
- | ListDeadLetterSourceQueues | No |
801
- | StartMessageMoveTask | No |
802
- | CancelMessageMoveTask | No |
803
- | ListMessageMoveTasks | No |
804
-
805
- ### SNS
806
-
807
- | Action | Supported |
808
- |--------|-----------|
809
- | CreateTopic | Yes |
810
- | DeleteTopic | Yes |
811
- | ListTopics | Yes |
812
- | GetTopicAttributes | Yes |
813
- | SetTopicAttributes | Yes |
814
- | Subscribe | Yes |
815
- | Unsubscribe | Yes |
816
- | ConfirmSubscription | Yes |
817
- | ListSubscriptions | Yes |
818
- | ListSubscriptionsByTopic | Yes |
819
- | GetSubscriptionAttributes | Yes |
820
- | SetSubscriptionAttributes | Yes |
821
- | Publish | Yes |
822
- | PublishBatch | Yes |
823
- | TagResource | Yes |
824
- | UntagResource | Yes |
825
- | ListTagsForResource | Yes |
826
- | AddPermission | No |
827
- | RemovePermission | No |
828
- | GetDataProtectionPolicy | No |
829
- | PutDataProtectionPolicy | No |
830
-
831
- Platform application, SMS, and phone number actions are not supported.
832
-
833
- ### S3
834
-
835
- | Action | Supported |
836
- |--------|-----------|
837
- | CreateBucket | Yes |
838
- | HeadBucket | Yes |
839
- | ListObjects | Yes |
840
- | ListObjectsV2 | Yes |
841
- | CopyObject | Yes |
842
- | PutObject | Yes |
843
- | GetObject | Yes |
844
- | DeleteObject | Yes |
845
- | HeadObject | Yes |
846
- | DeleteObjects | Yes |
847
- | DeleteBucket | Yes |
848
- | ListBuckets | Yes |
849
- | CreateMultipartUpload | Yes |
850
- | UploadPart | Yes |
851
- | CompleteMultipartUpload | Yes |
852
- | AbortMultipartUpload | Yes |
853
- | ListObjectVersions | No |
854
- | GetBucketLocation | No |
855
-
856
- Bucket configuration (CORS, lifecycle, encryption, replication, etc.), ACLs, versioning, tagging, and other management actions are not supported.
857
-
858
- ### STS
859
-
860
- | Action | Supported |
861
- |--------|-----------|
862
- | GetCallerIdentity | Yes |
863
- | AssumeRole | No |
864
- | GetSessionToken | No |
865
- | GetFederationToken | No |
866
-
867
- Returns a mock identity with account `000000000000` and ARN `arn:aws:iam::000000000000:root`. This allows tools like Terraform and the AWS CLI that call `sts:GetCallerIdentity` on startup to work without errors. Other STS actions are not supported.
868
-
869
- ## SQS Features
870
-
871
- - **Message attributes** with MD5 checksums matching the AWS algorithm
872
- - **Visibility timeout** messages become invisible after receive and reappear after timeout
873
- - **Delay queues** — per-queue default delay and per-message delay overrides
874
- - **Long polling** `WaitTimeSeconds` on ReceiveMessage blocks until messages arrive or timeout
875
- - **Dead letter queues** — messages exceeding `maxReceiveCount` are moved to the configured DLQ
876
- - **Batch operations** — SendMessageBatch, DeleteMessageBatch, ChangeMessageVisibilityBatch with entry ID validation (`InvalidBatchEntryId`) and total batch size validation (`BatchRequestTooLong`)
877
- - **Queue attribute range validation** — validates `VisibilityTimeout`, `DelaySeconds`, `ReceiveMessageWaitTimeSeconds`, `MaximumMessageSize`, and `MessageRetentionPeriod` on both CreateQueue and SetQueueAttributes
878
- - **Message size validation** rejects messages exceeding 1 MiB (1,048,576 bytes)
879
- - **Unicode character validation** — rejects messages with characters outside the AWS-allowed set
880
- - **KMS attributes** — `KmsMasterKeyId` and `KmsDataKeyReusePeriodSeconds` are accepted and stored (no actual encryption)
881
- - **FIFO queues** — `.fifo` suffix enforcement, `MessageGroupId` ordering, per-group locking (one inflight message per group), `MessageDeduplicationId`, content-based deduplication, sequence numbers, and FIFO-aware DLQ support
882
- - **Queue tags**
883
-
884
- ## SNS Features
885
-
886
- - **SNS-to-SQS fan-out** — publish to a topic and messages are delivered to all confirmed SQS subscriptions
887
- - **Filter policies** — both `MessageAttributes` and `MessageBody` scope, supporting exact match, prefix, suffix, anything-but (including anything-but with suffix), numeric ranges, exists, null conditions, and `$or` top-level grouping. MessageBody scope supports nested key matching
888
- - **Raw message delivery** — configurable per subscription
889
- - **Message size validation** — rejects messages exceeding 256 KB (262,144 bytes)
890
- - **Topic idempotency with conflict detection** — `CreateTopic` returns the existing topic when called with the same name, attributes, and tags, but throws when attributes or tags differ
891
- - **Subscription idempotency with conflict detection** — `Subscribe` returns the existing subscription when the same (topic, protocol, endpoint) combination is used with matching attributes, but throws when attributes differ
892
- - **Subscription attribute validation** — `SetSubscriptionAttributes` validates attribute names and rejects unknown or read-only attributes
893
- - **Topic and subscription tags**
894
- - **FIFO topics** — `.fifo` suffix enforcement, `MessageGroupId` and `MessageDeduplicationId` passthrough to SQS subscriptions, content-based deduplication
895
- - **Batch publish**
896
-
897
- ## S3 Features
898
-
899
- - **Bucket management** — CreateBucket (idempotent), DeleteBucket (rejects non-empty), HeadBucket, ListBuckets, ListObjects (V1 and V2)
900
- - **Object operations** — PutObject, GetObject, DeleteObject, HeadObject, CopyObject with ETag, Content-Type, and Last-Modified headers
901
- - **Multipart uploads** — CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload with correct multipart ETag calculation (`MD5-of-part-digests-partCount`), metadata preservation, and part overwrite support
902
- - **ListObjects V2** — prefix filtering, delimiter-based virtual directories, MaxKeys, continuation tokens, StartAfter
903
- - **CopyObject** — same-bucket and cross-bucket copy via `x-amz-copy-source` header, with metadata preservation
904
- - **User metadata** — `x-amz-meta-*` headers are stored and returned on GetObject and HeadObject
905
- - **Bulk delete** — DeleteObjects for batch key deletion with proper XML entity handling
906
- - **Keys with slashes** — full support for slash-delimited keys (e.g., `path/to/file.txt`)
907
- - **Stream uploads** — handles AWS chunked transfer encoding (`Content-Encoding: aws-chunked`) for stream bodies
908
- - **Path-style and virtual-hosted-style** — both S3 URL styles are supported (see below)
909
-
910
- ### S3 URL styles
911
-
912
- The AWS SDK sends S3 requests using virtual-hosted-style URLs by default (e.g., `my-bucket.s3.localhost:4566`). This requires `*.localhost` to resolve to `127.0.0.1`. fauxqs supports several approaches.
913
-
914
- #### Option 1: `fauxqs.dev` wildcard DNS (recommended for Docker image)
915
-
916
- Works out of the box when running the [official Docker image](#running-with-docker) — nothing to configure. The `fauxqs.dev` domain provides wildcard DNS — `*.localhost.fauxqs.dev` resolves to `127.0.0.1` via a public DNS entry. Just use `s3.localhost.fauxqs.dev` as your endpoint. This replicates the approach [pioneered by LocalStack](https://docs.localstack.cloud/aws/services/s3/) with `localhost.localstack.cloud`: a public DNS record maps all subdomains to localhost, so virtual-hosted-style requests work without `/etc/hosts` changes, custom request handlers, or `forcePathStyle`. Works from any language, `fetch()`, or CLI tool.
917
-
918
- ```typescript
919
- import { S3Client } from "@aws-sdk/client-s3";
920
-
921
- const s3 = new S3Client({
922
- endpoint: "http://s3.localhost.fauxqs.dev:4566",
923
- region: "us-east-1",
924
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
925
- });
926
- ```
927
-
928
- You can also use raw HTTP requests:
929
-
930
- ```bash
931
- # Upload
932
- curl -X PUT --data-binary @file.txt http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
933
-
934
- # Download
935
- curl http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
936
- ```
937
-
938
- This is the recommended approach for host-to-Docker setups. If you are using fauxqs as an [embedded library](#programmatic-usage) in Node.js tests, prefer Option 2 (`interceptLocalhostDns`) instead — it patches DNS globally so all clients work without modification, and requires no external DNS.
939
-
940
- For **container-to-container** S3 virtual-hosted-style in docker-compose, use the [built-in DNS server](#container-to-container-s3-virtual-hosted-style) instead — it resolves `*.s3.fauxqs` to the fauxqs container IP so other containers can use virtual-hosted-style S3 without `forcePathStyle`.
941
-
942
- #### Option 2: `interceptLocalhostDns()` (recommended for embedded library)
943
-
944
- Patches Node.js `dns.lookup` so that any hostname ending in `.localhost` resolves to `127.0.0.1`. No client changes needed.
945
-
946
- ```typescript
947
- import { interceptLocalhostDns } from "fauxqs";
948
-
949
- const restore = interceptLocalhostDns();
950
-
951
- const s3 = new S3Client({
952
- endpoint: "http://s3.localhost:4566",
953
- region: "us-east-1",
954
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
955
- });
956
-
957
- // When done (e.g., in afterAll):
958
- restore();
959
- ```
960
-
961
- The suffix is configurable: `interceptLocalhostDns("myhost.test")` matches `*.myhost.test`.
962
-
963
- **Tradeoffs:** Affects all DNS lookups in the process. Best suited for test suites (`beforeAll` / `afterAll`).
964
-
965
- #### Option 3: `createLocalhostHandler()` (per-client)
966
-
967
- Creates an HTTP request handler that resolves all hostnames to `127.0.0.1`. Scoped to a single client instance — no side effects, no external DNS dependency.
968
-
969
- ```typescript
970
- import { S3Client } from "@aws-sdk/client-s3";
971
- import { createLocalhostHandler } from "fauxqs";
972
-
973
- const s3 = new S3Client({
974
- endpoint: "http://s3.localhost:4566",
975
- region: "us-east-1",
976
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
977
- requestHandler: createLocalhostHandler(),
978
- });
979
- ```
980
-
981
- #### Option 4: `forcePathStyle` (simplest fallback)
982
-
983
- Forces the SDK to use path-style URLs (`http://localhost:4566/my-bucket/key`) instead of virtual-hosted-style. No DNS or handler changes needed, but affects how the SDK resolves S3 URLs at runtime.
984
-
985
- ```typescript
986
- const s3 = new S3Client({
987
- endpoint: "http://localhost:4566",
988
- forcePathStyle: true,
989
- // ...
990
- });
991
- ```
992
-
993
-
994
- ### Using with AWS CLI
995
-
996
- fauxqs is wire-compatible with the standard AWS CLI. Point it at the fauxqs endpoint:
997
-
998
- #### SQS
999
-
1000
- ```bash
1001
- aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name my-queue
1002
- aws --endpoint-url http://localhost:4566 sqs create-queue \
1003
- --queue-name my-queue.fifo \
1004
- --attributes FifoQueue=true,ContentBasedDeduplication=true
1005
- aws --endpoint-url http://localhost:4566 sqs send-message \
1006
- --queue-url http://localhost:4566/000000000000/my-queue \
1007
- --message-body "hello"
1008
- ```
1009
-
1010
- #### SNS
1011
-
1012
- ```bash
1013
- aws --endpoint-url http://localhost:4566 sns create-topic --name my-topic
1014
- aws --endpoint-url http://localhost:4566 sns subscribe \
1015
- --topic-arn arn:aws:sns:us-east-1:000000000000:my-topic \
1016
- --protocol sqs \
1017
- --notification-endpoint arn:aws:sqs:us-east-1:000000000000:my-queue
1018
- ```
1019
-
1020
- #### S3
1021
-
1022
- ```bash
1023
- aws --endpoint-url http://localhost:4566 s3 mb s3://my-bucket
1024
- aws --endpoint-url http://localhost:4566 s3 cp file.txt s3://my-bucket/file.txt
1025
- ```
1026
-
1027
- If the AWS CLI uses virtual-hosted-style S3 URLs by default, configure path-style:
1028
-
1029
- ```bash
1030
- aws configure set default.s3.addressing_style path
1031
- ```
1032
-
1033
- ## Conventions
1034
-
1035
- - Account ID: `000000000000`
1036
- - Region: auto-detected from SDK `Authorization` header; falls back to `defaultRegion` (defaults to `us-east-1`). Region is part of entity identity — same-name entities in different regions are independent.
1037
- - Queue URL format: `http://sqs.{region}.{host}:{port}/000000000000/{queueName}` (host defaults to `localhost`)
1038
- - Queue ARN format: `arn:aws:sqs:{region}:000000000000:{queueName}`
1039
- - Topic ARN format: `arn:aws:sns:{region}:000000000000:{topicName}`
1040
-
1041
- ## Limitations
1042
-
1043
- fauxqs is designed for development and testing. It does not support:
1044
-
1045
- - Non-SQS SNS delivery protocols (HTTP/S, Lambda, email, SMS)
1046
- - Persistence across restarts
1047
- - Authentication or authorization
1048
- - Cross-account operations
1049
-
1050
- ## Examples
1051
-
1052
- The [`examples/`](examples/) directory contains runnable TypeScript examples covering fauxqs-specific features beyond standard AWS SDK usage:
1053
-
1054
- | Example | Description |
1055
- |---------|-------------|
1056
- | [`programmatic/programmatic-api.ts`](examples/programmatic/programmatic-api.ts) | Server lifecycle, resource creation, SDK usage, `inspectQueue()`, `purgeAll()`, `setup()` |
1057
- | [`programmatic/message-spy.ts`](examples/programmatic/message-spy.ts) | `MessageSpyReader` — all spy methods, partial/predicate filters, discriminated union narrowing, DLQ tracking |
1058
- | [`programmatic/init-config.ts`](examples/programmatic/init-config.ts) | File-based and inline init config, DLQ chains, `setup()` idempotency, purge + re-apply pattern |
1059
- | [`programmatic/queue-inspection.ts`](examples/programmatic/queue-inspection.ts) | Programmatic `inspectQueue()` and HTTP `/_fauxqs/queues` endpoints |
1060
- | [`docker/standalone-container.ts`](examples/docker/standalone-container.ts) | Connecting to a fauxqs Docker container from the host |
1061
- | [`docker/container-to-container.ts`](examples/docker/container-to-container.ts) | Container-to-container communication via docker-compose |
1062
-
1063
- All examples are type-checked in CI to prevent staleness.
1064
-
1065
- ## Benchmarks
1066
-
1067
- SQS throughput benchmarks are available in the [`benchmarks/`](benchmarks/) directory, comparing fauxqs across different deployment modes (in-process library, official Docker image, lightweight Docker container) and against LocalStack. See [`benchmarks/BENCHMARKING.md`](benchmarks/BENCHMARKING.md) for setup descriptions, instructions, and how to interpret results.
1068
-
1069
- ## License
1070
-
1071
- MIT
1
+ # fauxqs
2
+
3
+ Local SNS/SQS/S3 emulator for development and testing. Point your `@aws-sdk/client-sqs`, `@aws-sdk/client-sns`, and `@aws-sdk/client-s3` clients at fauxqs instead of real AWS.
4
+
5
+ All state is in-memory. No persistence, no external storage dependencies.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Usage](#usage)
11
+ - [Running the server](#running-the-server)
12
+ - [Running in the background](#running-in-the-background)
13
+ - [Running with Docker](#running-with-docker)
14
+ - [Running in Docker Compose](#running-in-docker-compose)
15
+ - [Container-to-container S3 virtual-hosted-style](#container-to-container-s3-virtual-hosted-style)
16
+ - [Configuring AWS SDK clients](#configuring-aws-sdk-clients)
17
+ - [Programmatic usage](#programmatic-usage)
18
+ - [Programmatic state setup](#programmatic-state-setup)
19
+ - [Init config file](#init-config-file)
20
+ - [Init config schema reference](#init-config-schema-reference)
21
+ - [Message spy](#message-spy)
22
+ - [Queue inspection](#queue-inspection)
23
+ - [Configurable queue URL host](#configurable-queue-url-host)
24
+ - [Region](#region)
25
+ - [Supported API Actions](#supported-api-actions)
26
+ - [SQS](#sqs)
27
+ - [SNS](#sns)
28
+ - [S3](#s3)
29
+ - [STS](#sts)
30
+ - [SQS Features](#sqs-features)
31
+ - [SNS Features](#sns-features)
32
+ - [S3 Features](#s3-features)
33
+ - [S3 URL styles](#s3-url-styles)
34
+ - [Using with AWS CLI](#using-with-aws-cli)
35
+ - [Testing Strategies](#testing-strategies)
36
+ - [Library mode for tests](#library-mode-for-tests)
37
+ - [Docker mode for local development](#docker-mode-for-local-development)
38
+ - [Recommended combination](#recommended-combination)
39
+ - [Conventions](#conventions)
40
+ - [Limitations](#limitations)
41
+ - [Examples](#examples)
42
+ - [Benchmarks](#benchmarks)
43
+ - [License](#license)
44
+
45
+ ## Installation
46
+
47
+ **Docker** (recommended for standalone usage) — [Docker Hub](https://hub.docker.com/r/kibertoad/fauxqs):
48
+
49
+ ```bash
50
+ docker run -p 4566:4566 kibertoad/fauxqs
51
+ ```
52
+
53
+ **npm** (for embedded library usage or CLI):
54
+
55
+ ```bash
56
+ npm install fauxqs
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ ### Running the server
62
+
63
+ ```bash
64
+ npx fauxqs
65
+ ```
66
+
67
+ The server starts on port `4566` and handles SQS, SNS, and S3 on a single endpoint.
68
+
69
+ #### Environment variables
70
+
71
+ | Variable | Description | Default |
72
+ |----------|-------------|---------|
73
+ | `FAUXQS_PORT` | Port to listen on | `4566` |
74
+ | `FAUXQS_HOST` | Host for queue URLs (`sqs.<region>.<host>` format) | `localhost` |
75
+ | `FAUXQS_DEFAULT_REGION` | Fallback region for ARNs and URLs | `us-east-1` |
76
+ | `FAUXQS_LOGGER` | Enable request logging (`true`/`false`) | `true` |
77
+ | `FAUXQS_INIT` | Path to a JSON init config file (see [Init config file](#init-config-file)) | (none) |
78
+ | `FAUXQS_DNS_NAME` | Domain that dnsmasq resolves (including all subdomains) to the container IP. Only needed when the container hostname doesn't match the docker-compose service name — e.g., when using `container_name` or running with plain `docker run`. In docker-compose the hostname is set to the service name automatically, so this is rarely needed. (Docker only) | container hostname |
79
+ | `FAUXQS_DNS_UPSTREAM` | Where dnsmasq forwards non-fauxqs DNS queries (e.g., `registry.npmjs.org`). Change this if you're in a corporate network with an internal DNS server, or if you prefer a different public resolver like `1.1.1.1`. (Docker only) | `8.8.8.8` |
80
+
81
+ ```bash
82
+ FAUXQS_PORT=3000 FAUXQS_INIT=init.json npx fauxqs
83
+ ```
84
+
85
+ A health check is available at `GET /health`.
86
+
87
+ ### Running in the background
88
+
89
+ To keep fauxqs running while you work on your app or run tests repeatedly, start it as a background process:
90
+
91
+ ```bash
92
+ npx fauxqs &
93
+ ```
94
+
95
+ Or in a separate terminal:
96
+
97
+ ```bash
98
+ npx fauxqs
99
+ ```
100
+
101
+ All state accumulates in memory across requests, so queues, topics, and objects persist until the server is stopped.
102
+
103
+ To stop the server:
104
+
105
+ ```bash
106
+ # If backgrounded in the same shell
107
+ kill %1
108
+
109
+ # Cross-platform, by port
110
+ npx cross-port-killer 4566
111
+ ```
112
+
113
+ ### Running with Docker
114
+
115
+ The official Docker image is available on Docker Hub:
116
+
117
+ ```bash
118
+ docker run -p 4566:4566 kibertoad/fauxqs
119
+ ```
120
+
121
+ With an init config file:
122
+
123
+ ```bash
124
+ docker run -p 4566:4566 \
125
+ -v ./init.json:/app/init.json \
126
+ -e FAUXQS_INIT=/app/init.json \
127
+ kibertoad/fauxqs
128
+ ```
129
+
130
+ ### Running in Docker Compose
131
+
132
+ Use the `kibertoad/fauxqs` image and mount a JSON init config to pre-create resources on startup:
133
+
134
+ ```json
135
+ // scripts/fauxqs/init.json
136
+ {
137
+ "queues": [
138
+ {
139
+ "name": "my-queue.fifo",
140
+ "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
141
+ },
142
+ { "name": "my-dlq" }
143
+ ],
144
+ "topics": [{ "name": "my-events" }],
145
+ "subscriptions": [{ "topic": "my-events", "queue": "my-dlq" }],
146
+ "buckets": ["my-uploads"]
147
+ }
148
+ ```
149
+
150
+ ```yaml
151
+ # docker-compose.yml
152
+ services:
153
+ fauxqs:
154
+ image: kibertoad/fauxqs:latest
155
+ ports:
156
+ - "4566:4566"
157
+ environment:
158
+ - FAUXQS_INIT=/app/init.json
159
+ volumes:
160
+ - ./scripts/fauxqs/init.json:/app/init.json
161
+
162
+ app:
163
+ # ...
164
+ depends_on:
165
+ fauxqs:
166
+ condition: service_healthy
167
+ ```
168
+
169
+ The image has a built-in `HEALTHCHECK`, so `service_healthy` works without extra configuration in your compose file. Other containers reference fauxqs using the Docker service name (`http://fauxqs:4566`). The init config file creates all queues, topics, subscriptions, and buckets before the healthcheck passes, so dependent services start only after resources are ready.
170
+
171
+ #### Container-to-container S3 virtual-hosted-style
172
+
173
+ The Docker image includes a built-in DNS server ([dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html)) that resolves the container hostname and all its subdomains (e.g., `fauxqs`, `s3.fauxqs`, `my-bucket.s3.fauxqs`) to the container's own IP. This enables virtual-hosted-style S3 from other containers without `forcePathStyle`.
174
+
175
+ To use it, assign fauxqs a static IP and point other containers' DNS to it:
176
+
177
+ ```yaml
178
+ # docker-compose.yml
179
+ services:
180
+ fauxqs:
181
+ image: kibertoad/fauxqs:latest
182
+ networks:
183
+ default:
184
+ ipv4_address: 10.0.0.2
185
+ ports:
186
+ - "4566:4566"
187
+ environment:
188
+ - FAUXQS_INIT=/app/init.json
189
+ - FAUXQS_HOST=fauxqs
190
+ volumes:
191
+ - ./scripts/fauxqs/init.json:/app/init.json
192
+
193
+ app:
194
+ dns: 10.0.0.2
195
+ depends_on:
196
+ fauxqs:
197
+ condition: service_healthy
198
+ environment:
199
+ - AWS_ENDPOINT=http://s3.fauxqs:4566
200
+
201
+ networks:
202
+ default:
203
+ ipam:
204
+ config:
205
+ - subnet: 10.0.0.0/24
206
+ ```
207
+
208
+ From the `app` container, `my-bucket.s3.fauxqs` resolves to `10.0.0.2` (the fauxqs container), so virtual-hosted-style S3 works:
209
+
210
+ ```typescript
211
+ const s3 = new S3Client({
212
+ endpoint: "http://s3.fauxqs:4566",
213
+ region: "us-east-1",
214
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
215
+ // No forcePathStyle needed!
216
+ });
217
+ ```
218
+
219
+ The DNS server is configured automatically using the container hostname (which docker-compose sets to the service name), so in most setups no extra configuration is needed. See the [environment variables table](#environment-variables) for `FAUXQS_DNS_NAME` and `FAUXQS_DNS_UPSTREAM` if you need to override the defaults.
220
+
221
+ ### Configuring AWS SDK clients
222
+
223
+ Point your SDK clients at the local server:
224
+
225
+ ```typescript
226
+ import { SQSClient } from "@aws-sdk/client-sqs";
227
+ import { SNSClient } from "@aws-sdk/client-sns";
228
+ import { S3Client } from "@aws-sdk/client-s3";
229
+
230
+ const sqsClient = new SQSClient({
231
+ endpoint: "http://localhost:4566",
232
+ region: "us-east-1",
233
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
234
+ });
235
+
236
+ const snsClient = new SNSClient({
237
+ endpoint: "http://localhost:4566",
238
+ region: "us-east-1",
239
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
240
+ });
241
+
242
+ // Using fauxqs.dev wildcard DNS no helpers or forcePathStyle needed
243
+ const s3Client = new S3Client({
244
+ endpoint: "http://s3.localhost.fauxqs.dev:4566",
245
+ region: "us-east-1",
246
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
247
+ });
248
+ ```
249
+
250
+ Any credentials are accepted and never validated.
251
+
252
+ > **Note:** The `fauxqs.dev` wildcard DNS (`*.localhost.fauxqs.dev` `127.0.0.1`) replicates the approach [pioneered by LocalStack](https://hashnode.localstack.cloud/efficient-localstack-s3-endpoint-configuration) with `localhost.localstack.cloud`. A public DNS entry resolves all subdomains to localhost, so virtual-hosted-style S3 requests work without `/etc/hosts` changes, custom request handlers, or `forcePathStyle`. See [S3 URL styles](#s3-url-styles) for alternative approaches.
253
+
254
+ ### Programmatic usage
255
+
256
+ You can also embed fauxqs directly in your test suite:
257
+
258
+ ```typescript
259
+ import { startFauxqs } from "fauxqs";
260
+
261
+ const server = await startFauxqs({ port: 4566, logger: false });
262
+
263
+ console.log(server.address); // "http://127.0.0.1:4566"
264
+ console.log(server.port); // 4566
265
+
266
+ // point your SDK clients at server.address
267
+
268
+ // clean up when done
269
+ await server.stop();
270
+ ```
271
+
272
+ Pass `port: 0` to let the OS assign a random available port (useful in tests).
273
+
274
+ #### Programmatic state setup
275
+
276
+ The server object exposes methods for pre-creating resources without going through the SDK:
277
+
278
+ ```typescript
279
+ const server = await startFauxqs({ port: 0, logger: false });
280
+
281
+ // Create individual resources
282
+ server.createQueue("my-queue");
283
+ server.createQueue("my-dlq", {
284
+ attributes: { VisibilityTimeout: "60" },
285
+ tags: { env: "test" },
286
+ });
287
+ server.createTopic("my-topic");
288
+ server.subscribe({ topic: "my-topic", queue: "my-queue" });
289
+ server.createBucket("my-bucket");
290
+
291
+ // Create resources in a specific region
292
+ server.createQueue("eu-queue", { region: "eu-west-1" });
293
+ server.createTopic("eu-topic", { region: "eu-west-1" });
294
+ server.subscribe({ topic: "eu-topic", queue: "eu-queue", region: "eu-west-1" });
295
+
296
+ // Or create everything at once
297
+ server.setup({
298
+ queues: [
299
+ { name: "orders" },
300
+ { name: "notifications", attributes: { DelaySeconds: "5" } },
301
+ { name: "eu-orders", region: "eu-west-1" },
302
+ ],
303
+ topics: [{ name: "events" }],
304
+ subscriptions: [
305
+ { topic: "events", queue: "orders" },
306
+ { topic: "events", queue: "notifications" },
307
+ ],
308
+ buckets: ["uploads", "exports"],
309
+ });
310
+
311
+ // Clear all messages and S3 objects between tests (keeps queues, topics, subscriptions, buckets)
312
+ server.reset();
313
+
314
+ // Or nuke everything — removes queues, topics, subscriptions, and buckets too
315
+ server.purgeAll();
316
+ ```
317
+
318
+ #### Init config file
319
+
320
+ Create a JSON file to pre-create resources on startup. The file is validated on load malformed configs produce a clear error instead of silent failures.
321
+
322
+ ```json
323
+ {
324
+ "queues": [
325
+ { "name": "orders" },
326
+ { "name": "orders-dlq" },
327
+ { "name": "orders.fifo", "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" } }
328
+ ],
329
+ "topics": [
330
+ { "name": "events" }
331
+ ],
332
+ "subscriptions": [
333
+ { "topic": "events", "queue": "orders" }
334
+ ],
335
+ "buckets": ["uploads", "exports"]
336
+ }
337
+ ```
338
+
339
+ Pass it via the `FAUXQS_INIT` environment variable or the `init` option:
340
+
341
+ ```bash
342
+ FAUXQS_INIT=init.json npx fauxqs
343
+ ```
344
+
345
+ ```typescript
346
+ const server = await startFauxqs({ init: "init.json" });
347
+ // or inline:
348
+ const server = await startFauxqs({
349
+ init: { queues: [{ name: "my-queue" }], buckets: ["my-bucket"] },
350
+ });
351
+ ```
352
+
353
+ #### Init config schema reference
354
+
355
+ All top-level fields are optional. Resources are created in dependency order: queues, topics, subscriptions, buckets.
356
+
357
+ ##### `queues`
358
+
359
+ Array of queue objects.
360
+
361
+ | Field | Type | Required | Description |
362
+ |-------|------|----------|-------------|
363
+ | `name` | `string` | Yes | Queue name. Use `.fifo` suffix for FIFO queues. |
364
+ | `region` | `string` | No | Override the default region for this queue. The queue's ARN and URL will use this region. |
365
+ | `attributes` | `Record<string, string>` | No | Queue attributes (see table below). |
366
+ | `tags` | `Record<string, string>` | No | Key-value tags for the queue. |
367
+
368
+ Supported queue attributes:
369
+
370
+ | Attribute | Default | Range / Values |
371
+ |-----------|---------|----------------|
372
+ | `VisibilityTimeout` | `"30"` | `0` `43200` (seconds) |
373
+ | `DelaySeconds` | `"0"` | `0` `900` (seconds) |
374
+ | `MaximumMessageSize` | `"1048576"` | `1024` `1048576` (bytes) |
375
+ | `MessageRetentionPeriod` | `"345600"` | `60` `1209600` (seconds) |
376
+ | `ReceiveMessageWaitTimeSeconds` | `"0"` | `0` – `20` (seconds) |
377
+ | `RedrivePolicy` | — | JSON string: `{"deadLetterTargetArn": "arn:...", "maxReceiveCount": "5"}` |
378
+ | `Policy` | — | Queue policy JSON string (stored, not enforced) |
379
+ | `KmsMasterKeyId` | — | KMS key ID (stored, no actual encryption) |
380
+ | `KmsDataKeyReusePeriodSeconds` | — | KMS data key reuse period (stored, no actual encryption) |
381
+ | `FifoQueue` | — | `"true"` for FIFO queues (queue name must end with `.fifo`) |
382
+ | `ContentBasedDeduplication` | — | `"true"` or `"false"` (FIFO queues only) |
383
+
384
+ Example:
385
+
386
+ ```json
387
+ {
388
+ "queues": [
389
+ {
390
+ "name": "orders",
391
+ "attributes": { "VisibilityTimeout": "60", "DelaySeconds": "5" },
392
+ "tags": { "env": "staging", "team": "platform" }
393
+ },
394
+ {
395
+ "name": "orders-dlq"
396
+ },
397
+ {
398
+ "name": "orders.fifo",
399
+ "attributes": {
400
+ "FifoQueue": "true",
401
+ "ContentBasedDeduplication": "true"
402
+ }
403
+ },
404
+ {
405
+ "name": "retry-queue",
406
+ "attributes": {
407
+ "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-dlq\",\"maxReceiveCount\":\"3\"}"
408
+ }
409
+ },
410
+ {
411
+ "name": "eu-orders",
412
+ "region": "eu-west-1"
413
+ }
414
+ ]
415
+ }
416
+ ```
417
+
418
+ ##### `topics`
419
+
420
+ Array of topic objects.
421
+
422
+ | Field | Type | Required | Description |
423
+ |-------|------|----------|-------------|
424
+ | `name` | `string` | Yes | Topic name. Use `.fifo` suffix for FIFO topics. |
425
+ | `region` | `string` | No | Override the default region for this topic. The topic's ARN will use this region. |
426
+ | `attributes` | `Record<string, string>` | No | Topic attributes (e.g., `DisplayName`). |
427
+ | `tags` | `Record<string, string>` | No | Key-value tags for the topic. |
428
+
429
+ Example:
430
+
431
+ ```json
432
+ {
433
+ "topics": [
434
+ {
435
+ "name": "events",
436
+ "attributes": { "DisplayName": "Application Events" },
437
+ "tags": { "env": "staging" }
438
+ },
439
+ {
440
+ "name": "events.fifo",
441
+ "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
442
+ }
443
+ ]
444
+ }
445
+ ```
446
+
447
+ ##### `subscriptions`
448
+
449
+ Array of subscription objects. Referenced topics and queues must be defined in the same config (or already exist on the server).
450
+
451
+ | Field | Type | Required | Description |
452
+ |-------|------|----------|-------------|
453
+ | `topic` | `string` | Yes | Topic name (not ARN) to subscribe to. |
454
+ | `queue` | `string` | Yes | Queue name (not ARN) to deliver messages to. |
455
+ | `region` | `string` | No | Override the default region. The topic and queue ARNs will be resolved in this region. |
456
+ | `attributes` | `Record<string, string>` | No | Subscription attributes (see table below). |
457
+
458
+ Supported subscription attributes:
459
+
460
+ | Attribute | Values | Description |
461
+ |-----------|--------|-------------|
462
+ | `RawMessageDelivery` | `"true"` / `"false"` | Deliver the raw message body instead of the SNS envelope JSON. |
463
+ | `FilterPolicy` | JSON string | SNS filter policy for message filtering (e.g., `"{\"color\": [\"blue\"]}"`) |
464
+ | `FilterPolicyScope` | `"MessageAttributes"` / `"MessageBody"` | Whether the filter policy applies to message attributes or body. Defaults to `MessageAttributes`. |
465
+ | `RedrivePolicy` | JSON string | Subscription-level dead-letter queue config. |
466
+ | `DeliveryPolicy` | JSON string | Delivery retry policy (stored, not enforced). |
467
+ | `SubscriptionRoleArn` | ARN string | IAM role ARN for delivery (stored, not enforced). |
468
+
469
+ Example:
470
+
471
+ ```json
472
+ {
473
+ "subscriptions": [
474
+ {
475
+ "topic": "events",
476
+ "queue": "orders",
477
+ "attributes": {
478
+ "RawMessageDelivery": "true",
479
+ "FilterPolicy": "{\"eventType\": [\"order.created\", \"order.updated\"]}"
480
+ }
481
+ },
482
+ {
483
+ "topic": "events",
484
+ "queue": "notifications"
485
+ }
486
+ ]
487
+ }
488
+ ```
489
+
490
+ ##### `buckets`
491
+
492
+ Array of bucket name strings.
493
+
494
+ ```json
495
+ {
496
+ "buckets": ["uploads", "exports", "temp"]
497
+ }
498
+ ```
499
+
500
+ #### Message spy
501
+
502
+ `MessageSpyReader` lets you await specific events flowing through SQS, SNS, and S3 in your tests — without polling queues yourself. Inspired by `HandlerSpy` from `message-queue-toolkit`.
503
+
504
+ Enable it with the `messageSpies` option:
505
+
506
+ ```typescript
507
+ const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
508
+ ```
509
+
510
+ The spy tracks events across all three services using a discriminated union on `service`:
511
+
512
+ **SQS events** (`service: 'sqs'`):
513
+ - **`published`** — message was enqueued (via SendMessage, SendMessageBatch, or SNS fan-out)
514
+ - **`consumed`** — message was deleted (via DeleteMessage / DeleteMessageBatch)
515
+ - **`dlq`** — message exceeded `maxReceiveCount` and was moved to a dead-letter queue
516
+
517
+ **SNS events** (`service: 'sns'`):
518
+ - **`published`** — message was published to a topic (before fan-out to SQS subscriptions)
519
+
520
+ **S3 events** (`service: 's3'`):
521
+ - **`uploaded`** — object was put (PutObject or CompleteMultipartUpload)
522
+ - **`downloaded`** object was retrieved (GetObject)
523
+ - **`deleted`** object was deleted (DeleteObject, only when key existed)
524
+ - **`copied`** object was copied (CopyObject; also emits `uploaded` for the destination)
525
+
526
+ ##### Awaiting messages
527
+
528
+ ```typescript
529
+ // Wait for a specific SQS message (resolves immediately if already in buffer)
530
+ const msg = await server.spy.waitForMessage(
531
+ (m) => m.service === "sqs" && m.body === "order.created" && m.queueName === "orders",
532
+ "published",
533
+ );
534
+
535
+ // Wait by SQS message ID
536
+ const msg = await server.spy.waitForMessageWithId(messageId, "consumed");
537
+
538
+ // Partial object match (deep-equal on specified fields)
539
+ const msg = await server.spy.waitForMessage({ service: "sqs", queueName: "orders", status: "published" });
540
+
541
+ // Wait for an SNS publish event
542
+ const msg = await server.spy.waitForMessage({ service: "sns", topicName: "my-topic", status: "published" });
543
+
544
+ // Wait for an S3 upload event
545
+ const msg = await server.spy.waitForMessage({ service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" });
546
+ ```
547
+
548
+ `waitForMessage` checks the buffer first (retroactive resolution). If no match is found, it returns a Promise that resolves when a matching message arrives.
549
+
550
+ ##### Timeout
551
+
552
+ All `waitForMessage` and `waitForMessageWithId` calls accept an optional `timeout` parameter (ms) as the third argument. If no matching message arrives in time, the promise rejects with a timeout error — preventing tests from hanging indefinitely:
553
+
554
+ ```typescript
555
+ // Reject after 2 seconds if no match
556
+ const msg = await server.spy.waitForMessage(
557
+ { service: "sqs", queueName: "orders" },
558
+ "published",
559
+ 2000,
560
+ );
561
+
562
+ // Also works with waitForMessageWithId
563
+ const msg = await server.spy.waitForMessageWithId(messageId, "consumed", 5000);
564
+ ```
565
+
566
+ ##### Waiting for multiple messages
567
+
568
+ `waitForMessages` collects `count` matching messages before resolving. It checks the buffer first, then awaits future arrivals:
569
+
570
+ ```typescript
571
+ // Wait for 3 messages on the orders queue
572
+ const msgs = await server.spy.waitForMessages(
573
+ { service: "sqs", queueName: "orders" },
574
+ { count: 3, status: "published", timeout: 5000 },
575
+ );
576
+ // msgs.length === 3
577
+ ```
578
+
579
+ If the timeout expires before enough messages arrive, the promise rejects with a message showing how many were collected (e.g., `"collected 1/3"`).
580
+
581
+ ##### Negative assertions
582
+
583
+ `expectNoMessage` asserts that no matching message appears within a time window. Useful for verifying that filter policies dropped a message or that a side effect did not occur:
584
+
585
+ ```typescript
586
+ // Assert no message was delivered to the wrong queue (waits 200ms by default)
587
+ await server.spy.expectNoMessage({ service: "sqs", queueName: "wrong-queue" });
588
+
589
+ // Custom window and status filter
590
+ await server.spy.expectNoMessage(
591
+ { service: "sqs", queueName: "orders" },
592
+ { status: "dlq", within: 500 },
593
+ );
594
+ ```
595
+
596
+ If a matching message is already in the buffer, `expectNoMessage` rejects immediately. If one arrives during the wait, it rejects with `"matching message arrived during wait"`.
597
+
598
+ ##### Synchronous check
599
+
600
+ ```typescript
601
+ const msg = server.spy.checkForMessage(
602
+ (m) => m.service === "sqs" && m.queueName === "my-queue",
603
+ "published",
604
+ );
605
+ // returns SpyMessage | undefined
606
+ ```
607
+
608
+ ##### Buffer management
609
+
610
+ ```typescript
611
+ // Get all tracked messages (oldest to newest)
612
+ const all = server.spy.getAllMessages();
613
+
614
+ // Clear buffer and reject pending waiters
615
+ server.spy.clear();
616
+ ```
617
+
618
+ The buffer defaults to 100 messages (FIFO eviction). Configure with:
619
+
620
+ ```typescript
621
+ const server = await startFauxqs({
622
+ messageSpies: { bufferSize: 500 },
623
+ });
624
+ ```
625
+
626
+ ##### Types
627
+
628
+ `server.spy` returns a `MessageSpyReader` — a read-only interface that exposes query and await methods but not internal mutation (e.g. recording new events):
629
+
630
+ ```typescript
631
+ interface MessageSpyReader {
632
+ waitForMessage(filter: MessageSpyFilter, status?: string, timeout?: number): Promise<SpyMessage>;
633
+ waitForMessageWithId(messageId: string, status?: string, timeout?: number): Promise<SpyMessage>;
634
+ waitForMessages(filter: MessageSpyFilter, options: WaitForMessagesOptions): Promise<SpyMessage[]>;
635
+ expectNoMessage(filter: MessageSpyFilter, options?: ExpectNoMessageOptions): Promise<void>;
636
+ checkForMessage(filter: MessageSpyFilter, status?: string): SpyMessage | undefined;
637
+ getAllMessages(): SpyMessage[];
638
+ clear(): void;
639
+ }
640
+
641
+ interface WaitForMessagesOptions {
642
+ count: number;
643
+ status?: string;
644
+ timeout?: number;
645
+ }
646
+
647
+ interface ExpectNoMessageOptions {
648
+ status?: string;
649
+ within?: number; // ms, defaults to 200
650
+ }
651
+ ```
652
+
653
+ `SpyMessage` is a discriminated union:
654
+
655
+ ```typescript
656
+ interface SqsSpyMessage {
657
+ service: "sqs";
658
+ queueName: string;
659
+ messageId: string;
660
+ body: string;
661
+ messageAttributes: Record<string, MessageAttributeValue>;
662
+ status: "published" | "consumed" | "dlq";
663
+ timestamp: number;
664
+ }
665
+
666
+ interface SnsSpyMessage {
667
+ service: "sns";
668
+ topicArn: string;
669
+ topicName: string;
670
+ messageId: string;
671
+ body: string;
672
+ messageAttributes: Record<string, MessageAttributeValue>;
673
+ status: "published";
674
+ timestamp: number;
675
+ }
676
+
677
+ interface S3SpyEvent {
678
+ service: "s3";
679
+ bucket: string;
680
+ key: string;
681
+ status: "uploaded" | "downloaded" | "deleted" | "copied";
682
+ timestamp: number;
683
+ }
684
+
685
+ type SpyMessage = SqsSpyMessage | SnsSpyMessage | S3SpyEvent;
686
+ ```
687
+
688
+ ##### Spy disabled by default
689
+
690
+ Accessing `server.spy` when `messageSpies` is not set throws an error. There is no overhead on the message flow when spies are disabled.
691
+
692
+ #### Queue inspection
693
+
694
+ Non-destructive inspection of SQS queue state — see all messages (ready, in-flight, and delayed) without consuming them or affecting visibility timeouts.
695
+
696
+ ##### Programmatic API
697
+
698
+ ```typescript
699
+ const result = server.inspectQueue("my-queue");
700
+ // result is undefined if queue doesn't exist
701
+ if (result) {
702
+ console.log(result.name); // "my-queue"
703
+ console.log(result.url); // "http://sqs.us-east-1.localhost:4566/000000000000/my-queue"
704
+ console.log(result.arn); // "arn:aws:sqs:us-east-1:000000000000:my-queue"
705
+ console.log(result.attributes); // { VisibilityTimeout: "30", ... }
706
+ console.log(result.messages.ready); // messages available for receive
707
+ console.log(result.messages.delayed); // messages waiting for delay to expire
708
+ console.log(result.messages.inflight); // received but not yet deleted
709
+ // Each inflight entry includes: { message, receiptHandle, visibilityDeadline }
710
+ }
711
+ ```
712
+
713
+ ##### HTTP endpoints
714
+
715
+ ```bash
716
+ # List all queues with summary counts
717
+ curl http://localhost:4566/_fauxqs/queues
718
+ # [{ "name": "my-queue", "approximateMessageCount": 5, "approximateInflightCount": 2, "approximateDelayedCount": 0, ... }]
719
+
720
+ # Inspect a specific queue (full state)
721
+ curl http://localhost:4566/_fauxqs/queues/my-queue
722
+ # { "name": "my-queue", "messages": { "ready": [...], "delayed": [...], "inflight": [...] }, ... }
723
+ ```
724
+
725
+ Returns 404 for non-existent queues. Inspection never modifies queue state — messages remain exactly where they are.
726
+
727
+ ### Configurable queue URL host
728
+
729
+ Queue URLs use the AWS-style `sqs.<region>.<host>` format. The `host` defaults to `localhost`, producing URLs like `http://sqs.us-east-1.localhost:4566/000000000000/myQueue`.
730
+
731
+ To override the host (e.g., for a custom domain):
732
+
733
+ ```typescript
734
+ import { startFauxqs } from "fauxqs";
735
+
736
+ const server = await startFauxqs({ port: 4566, host: "myhost.local" });
737
+ // Queue URLs: http://sqs.us-east-1.myhost.local:4566/000000000000/myQueue
738
+ ```
739
+
740
+ This also works with `buildApp`:
741
+
742
+ ```typescript
743
+ import { buildApp } from "fauxqs";
744
+
745
+ const app = buildApp({ host: "myhost.local" });
746
+ ```
747
+
748
+ The configured host ensures queue URLs are consistent across all creation paths (init config, programmatic API, and SDK requests), regardless of the request's `Host` header.
749
+
750
+ ### Region
751
+
752
+ Region is part of an entity's identity — a queue named `my-queue` in `us-east-1` is a completely different entity from `my-queue` in `eu-west-1`, just like in real AWS.
753
+
754
+ The region used in ARNs and queue URLs is automatically detected from the SDK client's `Authorization` header (AWS SigV4 credential scope). If your SDK client is configured with `region: "eu-west-1"`, all entities created or looked up through that client will use `eu-west-1` in their ARNs and URLs.
755
+
756
+ ```typescript
757
+ const sqsEU = new SQSClient({ region: "eu-west-1", endpoint: "http://localhost:4566", ... });
758
+ const sqsUS = new SQSClient({ region: "us-east-1", endpoint: "http://localhost:4566", ... });
759
+
760
+ // These are two independent queues with different ARNs
761
+ await sqsEU.send(new CreateQueueCommand({ QueueName: "orders" }));
762
+ await sqsUS.send(new CreateQueueCommand({ QueueName: "orders" }));
763
+ ```
764
+
765
+ If the region cannot be resolved from request headers (e.g., requests without AWS SigV4 signing), the `defaultRegion` option is used as a fallback (defaults to `"us-east-1"`):
766
+
767
+ ```typescript
768
+ const server = await startFauxqs({ defaultRegion: "eu-west-1" });
769
+ ```
770
+
771
+ Resources created via init config or programmatic API use the `defaultRegion` unless overridden with an explicit `region` field:
772
+
773
+ ```json
774
+ {
775
+ "queues": [
776
+ { "name": "us-queue" },
777
+ { "name": "eu-queue", "region": "eu-west-1" }
778
+ ]
779
+ }
780
+ ```
781
+
782
+ ## Supported API Actions
783
+
784
+ ### SQS
785
+
786
+ | Action | Supported |
787
+ |--------|-----------|
788
+ | CreateQueue | Yes |
789
+ | DeleteQueue | Yes |
790
+ | GetQueueUrl | Yes |
791
+ | ListQueues | Yes |
792
+ | GetQueueAttributes | Yes |
793
+ | SetQueueAttributes | Yes |
794
+ | PurgeQueue | Yes |
795
+ | SendMessage | Yes |
796
+ | SendMessageBatch | Yes |
797
+ | ReceiveMessage | Yes |
798
+ | DeleteMessage | Yes |
799
+ | DeleteMessageBatch | Yes |
800
+ | ChangeMessageVisibility | Yes |
801
+ | ChangeMessageVisibilityBatch | Yes |
802
+ | TagQueue | Yes |
803
+ | UntagQueue | Yes |
804
+ | ListQueueTags | Yes |
805
+ | AddPermission | No |
806
+ | RemovePermission | No |
807
+ | ListDeadLetterSourceQueues | No |
808
+ | StartMessageMoveTask | No |
809
+ | CancelMessageMoveTask | No |
810
+ | ListMessageMoveTasks | No |
811
+
812
+ ### SNS
813
+
814
+ | Action | Supported |
815
+ |--------|-----------|
816
+ | CreateTopic | Yes |
817
+ | DeleteTopic | Yes |
818
+ | ListTopics | Yes |
819
+ | GetTopicAttributes | Yes |
820
+ | SetTopicAttributes | Yes |
821
+ | Subscribe | Yes |
822
+ | Unsubscribe | Yes |
823
+ | ConfirmSubscription | Yes |
824
+ | ListSubscriptions | Yes |
825
+ | ListSubscriptionsByTopic | Yes |
826
+ | GetSubscriptionAttributes | Yes |
827
+ | SetSubscriptionAttributes | Yes |
828
+ | Publish | Yes |
829
+ | PublishBatch | Yes |
830
+ | TagResource | Yes |
831
+ | UntagResource | Yes |
832
+ | ListTagsForResource | Yes |
833
+ | AddPermission | No |
834
+ | RemovePermission | No |
835
+ | GetDataProtectionPolicy | No |
836
+ | PutDataProtectionPolicy | No |
837
+
838
+ Platform application, SMS, and phone number actions are not supported.
839
+
840
+ ### S3
841
+
842
+ | Action | Supported |
843
+ |--------|-----------|
844
+ | CreateBucket | Yes |
845
+ | HeadBucket | Yes |
846
+ | ListObjects | Yes |
847
+ | ListObjectsV2 | Yes |
848
+ | CopyObject | Yes |
849
+ | PutObject | Yes |
850
+ | GetObject | Yes |
851
+ | DeleteObject | Yes |
852
+ | HeadObject | Yes |
853
+ | DeleteObjects | Yes |
854
+ | DeleteBucket | Yes |
855
+ | ListBuckets | Yes |
856
+ | CreateMultipartUpload | Yes |
857
+ | UploadPart | Yes |
858
+ | CompleteMultipartUpload | Yes |
859
+ | AbortMultipartUpload | Yes |
860
+ | ListObjectVersions | No |
861
+ | GetBucketLocation | No |
862
+
863
+ Bucket configuration (CORS, lifecycle, encryption, replication, etc.), ACLs, versioning, tagging, and other management actions are not supported.
864
+
865
+ ### STS
866
+
867
+ | Action | Supported |
868
+ |--------|-----------|
869
+ | GetCallerIdentity | Yes |
870
+ | AssumeRole | No |
871
+ | GetSessionToken | No |
872
+ | GetFederationToken | No |
873
+
874
+ Returns a mock identity with account `000000000000` and ARN `arn:aws:iam::000000000000:root`. This allows tools like Terraform and the AWS CLI that call `sts:GetCallerIdentity` on startup to work without errors. Other STS actions are not supported.
875
+
876
+ ## SQS Features
877
+
878
+ - **Message attributes** with MD5 checksums matching the AWS algorithm
879
+ - **Visibility timeout** — messages become invisible after receive and reappear after timeout
880
+ - **Delay queues** — per-queue default delay and per-message delay overrides
881
+ - **Long polling** — `WaitTimeSeconds` on ReceiveMessage blocks until messages arrive or timeout
882
+ - **Dead letter queues** — messages exceeding `maxReceiveCount` are moved to the configured DLQ
883
+ - **Batch operations** — SendMessageBatch, DeleteMessageBatch, ChangeMessageVisibilityBatch with entry ID validation (`InvalidBatchEntryId`) and total batch size validation (`BatchRequestTooLong`)
884
+ - **Queue attribute range validation** — validates `VisibilityTimeout`, `DelaySeconds`, `ReceiveMessageWaitTimeSeconds`, `MaximumMessageSize`, and `MessageRetentionPeriod` on both CreateQueue and SetQueueAttributes
885
+ - **Message size validation** — rejects messages exceeding 1 MiB (1,048,576 bytes)
886
+ - **Unicode character validation** — rejects messages with characters outside the AWS-allowed set
887
+ - **KMS attributes** — `KmsMasterKeyId` and `KmsDataKeyReusePeriodSeconds` are accepted and stored (no actual encryption)
888
+ - **FIFO queues** — `.fifo` suffix enforcement, `MessageGroupId` ordering, per-group locking (one inflight message per group), `MessageDeduplicationId`, content-based deduplication, sequence numbers, and FIFO-aware DLQ support
889
+ - **Queue tags**
890
+
891
+ ## SNS Features
892
+
893
+ - **SNS-to-SQS fan-out** — publish to a topic and messages are delivered to all confirmed SQS subscriptions
894
+ - **Filter policies** — both `MessageAttributes` and `MessageBody` scope, supporting exact match, prefix, suffix, anything-but (including anything-but with suffix), numeric ranges, exists, null conditions, and `$or` top-level grouping. MessageBody scope supports nested key matching
895
+ - **Raw message delivery** — configurable per subscription
896
+ - **Message size validation** — rejects messages exceeding 256 KB (262,144 bytes)
897
+ - **Topic idempotency with conflict detection** — `CreateTopic` returns the existing topic when called with the same name, attributes, and tags, but throws when attributes or tags differ
898
+ - **Subscription idempotency with conflict detection** — `Subscribe` returns the existing subscription when the same (topic, protocol, endpoint) combination is used with matching attributes, but throws when attributes differ
899
+ - **Subscription attribute validation** — `SetSubscriptionAttributes` validates attribute names and rejects unknown or read-only attributes
900
+ - **Topic and subscription tags**
901
+ - **FIFO topics** — `.fifo` suffix enforcement, `MessageGroupId` and `MessageDeduplicationId` passthrough to SQS subscriptions, content-based deduplication
902
+ - **Batch publish**
903
+
904
+ ## S3 Features
905
+
906
+ - **Bucket management** — CreateBucket (idempotent), DeleteBucket (rejects non-empty), HeadBucket, ListBuckets, ListObjects (V1 and V2)
907
+ - **Object operations** — PutObject, GetObject, DeleteObject, HeadObject, CopyObject with ETag, Content-Type, and Last-Modified headers
908
+ - **Multipart uploads** — CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload with correct multipart ETag calculation (`MD5-of-part-digests-partCount`), metadata preservation, and part overwrite support
909
+ - **ListObjects V2** — prefix filtering, delimiter-based virtual directories, MaxKeys, continuation tokens, StartAfter
910
+ - **CopyObject** same-bucket and cross-bucket copy via `x-amz-copy-source` header, with metadata preservation
911
+ - **User metadata** — `x-amz-meta-*` headers are stored and returned on GetObject and HeadObject
912
+ - **Bulk delete** DeleteObjects for batch key deletion with proper XML entity handling
913
+ - **Keys with slashes** — full support for slash-delimited keys (e.g., `path/to/file.txt`)
914
+ - **Stream uploads** handles AWS chunked transfer encoding (`Content-Encoding: aws-chunked`) for stream bodies
915
+ - **Path-style and virtual-hosted-style** — both S3 URL styles are supported (see below)
916
+
917
+ ### S3 URL styles
918
+
919
+ The AWS SDK sends S3 requests using virtual-hosted-style URLs by default (e.g., `my-bucket.s3.localhost:4566`). This requires `*.localhost` to resolve to `127.0.0.1`. fauxqs supports several approaches.
920
+
921
+ #### Option 1: `fauxqs.dev` wildcard DNS (recommended for Docker image)
922
+
923
+ Works out of the box when running the [official Docker image](#running-with-docker) — nothing to configure. The `fauxqs.dev` domain provides wildcard DNS — `*.localhost.fauxqs.dev` resolves to `127.0.0.1` via a public DNS entry. Just use `s3.localhost.fauxqs.dev` as your endpoint. This replicates the approach [pioneered by LocalStack](https://docs.localstack.cloud/aws/services/s3/) with `localhost.localstack.cloud`: a public DNS record maps all subdomains to localhost, so virtual-hosted-style requests work without `/etc/hosts` changes, custom request handlers, or `forcePathStyle`. Works from any language, `fetch()`, or CLI tool.
924
+
925
+ ```typescript
926
+ import { S3Client } from "@aws-sdk/client-s3";
927
+
928
+ const s3 = new S3Client({
929
+ endpoint: "http://s3.localhost.fauxqs.dev:4566",
930
+ region: "us-east-1",
931
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
932
+ });
933
+ ```
934
+
935
+ You can also use raw HTTP requests:
936
+
937
+ ```bash
938
+ # Upload
939
+ curl -X PUT --data-binary @file.txt http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
940
+
941
+ # Download
942
+ curl http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
943
+ ```
944
+
945
+ This is the recommended approach for host-to-Docker setups. If you are using fauxqs as an [embedded library](#programmatic-usage) in Node.js tests, prefer Option 2 (`interceptLocalhostDns`) instead — it patches DNS globally so all clients work without modification, and requires no external DNS.
946
+
947
+ For **container-to-container** S3 virtual-hosted-style in docker-compose, use the [built-in DNS server](#container-to-container-s3-virtual-hosted-style) instead — it resolves `*.s3.fauxqs` to the fauxqs container IP so other containers can use virtual-hosted-style S3 without `forcePathStyle`.
948
+
949
+ #### Option 2: `interceptLocalhostDns()` (recommended for embedded library)
950
+
951
+ Patches Node.js `dns.lookup` so that any hostname ending in `.localhost` resolves to `127.0.0.1`. No client changes needed.
952
+
953
+ ```typescript
954
+ import { interceptLocalhostDns } from "fauxqs";
955
+
956
+ const restore = interceptLocalhostDns();
957
+
958
+ const s3 = new S3Client({
959
+ endpoint: "http://s3.localhost:4566",
960
+ region: "us-east-1",
961
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
962
+ });
963
+
964
+ // When done (e.g., in afterAll):
965
+ restore();
966
+ ```
967
+
968
+ The suffix is configurable: `interceptLocalhostDns("myhost.test")` matches `*.myhost.test`.
969
+
970
+ **Tradeoffs:** Affects all DNS lookups in the process. Best suited for test suites (`beforeAll` / `afterAll`).
971
+
972
+ #### Option 3: `createLocalhostHandler()` (per-client)
973
+
974
+ Creates an HTTP request handler that resolves all hostnames to `127.0.0.1`. Scoped to a single client instance — no side effects, no external DNS dependency.
975
+
976
+ ```typescript
977
+ import { S3Client } from "@aws-sdk/client-s3";
978
+ import { createLocalhostHandler } from "fauxqs";
979
+
980
+ const s3 = new S3Client({
981
+ endpoint: "http://s3.localhost:4566",
982
+ region: "us-east-1",
983
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
984
+ requestHandler: createLocalhostHandler(),
985
+ });
986
+ ```
987
+
988
+ #### Option 4: `forcePathStyle` (simplest fallback)
989
+
990
+ Forces the SDK to use path-style URLs (`http://localhost:4566/my-bucket/key`) instead of virtual-hosted-style. No DNS or handler changes needed, but affects how the SDK resolves S3 URLs at runtime.
991
+
992
+ ```typescript
993
+ const s3 = new S3Client({
994
+ endpoint: "http://localhost:4566",
995
+ forcePathStyle: true,
996
+ // ...
997
+ });
998
+ ```
999
+
1000
+
1001
+ ### Using with AWS CLI
1002
+
1003
+ fauxqs is wire-compatible with the standard AWS CLI. Point it at the fauxqs endpoint:
1004
+
1005
+ #### SQS
1006
+
1007
+ ```bash
1008
+ aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name my-queue
1009
+ aws --endpoint-url http://localhost:4566 sqs create-queue \
1010
+ --queue-name my-queue.fifo \
1011
+ --attributes FifoQueue=true,ContentBasedDeduplication=true
1012
+ aws --endpoint-url http://localhost:4566 sqs send-message \
1013
+ --queue-url http://localhost:4566/000000000000/my-queue \
1014
+ --message-body "hello"
1015
+ ```
1016
+
1017
+ #### SNS
1018
+
1019
+ ```bash
1020
+ aws --endpoint-url http://localhost:4566 sns create-topic --name my-topic
1021
+ aws --endpoint-url http://localhost:4566 sns subscribe \
1022
+ --topic-arn arn:aws:sns:us-east-1:000000000000:my-topic \
1023
+ --protocol sqs \
1024
+ --notification-endpoint arn:aws:sqs:us-east-1:000000000000:my-queue
1025
+ ```
1026
+
1027
+ #### S3
1028
+
1029
+ ```bash
1030
+ aws --endpoint-url http://localhost:4566 s3 mb s3://my-bucket
1031
+ aws --endpoint-url http://localhost:4566 s3 cp file.txt s3://my-bucket/file.txt
1032
+ ```
1033
+
1034
+ If the AWS CLI uses virtual-hosted-style S3 URLs by default, configure path-style:
1035
+
1036
+ ```bash
1037
+ aws configure set default.s3.addressing_style path
1038
+ ```
1039
+
1040
+ ## Testing Strategies
1041
+
1042
+ fauxqs supports two deployment modes that complement each other for a complete testing workflow:
1043
+
1044
+ | Mode | Best for | Startup | Assertions |
1045
+ |------|----------|---------|------------|
1046
+ | **Library** (embedded) | Unit tests, integration tests, CI | Milliseconds, in-process | Full programmatic API: `spy`, `inspectQueue`, `reset`, `purgeAll` |
1047
+ | **Docker** (standalone) | Local development, acceptance tests, dev environments | Seconds, real HTTP | Init config, HTTP inspection endpoints |
1048
+
1049
+ ### Library mode for tests
1050
+
1051
+ Embed fauxqs directly in your test suite. Each test file gets its own server instance on a random port — no Docker dependency, no shared state, no port conflicts:
1052
+
1053
+ ```typescript
1054
+ // test/setup.ts
1055
+ import { startFauxqs, type FauxqsServer } from "fauxqs";
1056
+
1057
+ export async function createTestServer(): Promise<FauxqsServer> {
1058
+ const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
1059
+
1060
+ // Pre-create resources via the programmatic API (no SDK roundtrips)
1061
+ server.createQueue("my-queue");
1062
+ server.createBucket("my-bucket");
1063
+
1064
+ return server;
1065
+ }
1066
+ ```
1067
+
1068
+ ```typescript
1069
+ // test/app.test.ts
1070
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
1071
+
1072
+ let server: FauxqsServer;
1073
+
1074
+ beforeAll(async () => { server = await createTestServer(); });
1075
+ afterAll(async () => { await server.stop(); });
1076
+ beforeEach(() => { server.spy.clear(); });
1077
+
1078
+ it("tracks uploads via the spy", async () => {
1079
+ // ... trigger your app logic that uploads to S3 ...
1080
+
1081
+ const event = await server.spy.waitForMessage(
1082
+ { service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" },
1083
+ undefined,
1084
+ 2000, // timeout — prevents tests from hanging
1085
+ );
1086
+ expect(event.status).toBe("uploaded");
1087
+ });
1088
+ ```
1089
+
1090
+ Library mode gives you deterministic assertions via the [message spy](#message-spy), non-destructive state inspection via [`inspectQueue()`](#queue-inspection), and instant state reset with `reset()` / `purgeAll()` — none of which are available from outside the process.
1091
+
1092
+ ### Docker mode for local development
1093
+
1094
+ Use `docker-compose.yml` with an init config to give your team a consistent local environment:
1095
+
1096
+ ```yaml
1097
+ # docker-compose.yml
1098
+ services:
1099
+ fauxqs:
1100
+ image: kibertoad/fauxqs:latest
1101
+ ports: ["4566:4566"]
1102
+ environment:
1103
+ - FAUXQS_INIT=/app/init.json
1104
+ volumes:
1105
+ - ./fauxqs-init.json:/app/init.json
1106
+
1107
+ app:
1108
+ build: .
1109
+ depends_on:
1110
+ fauxqs:
1111
+ condition: service_healthy
1112
+ environment:
1113
+ - AWS_ENDPOINT=http://fauxqs:4566
1114
+ ```
1115
+
1116
+ Docker mode validates your real deployment topology — networking, DNS, container-to-container communication — and is language-agnostic (any AWS SDK can connect).
1117
+
1118
+ ### Recommended combination
1119
+
1120
+ Use both modes together. Library mode runs in CI on every commit (fast, no Docker required). Docker mode runs locally via `docker compose up` and optionally in a separate CI stage for acceptance testing.
1121
+
1122
+ See the [`examples/recommended/`](examples/recommended/) directory for a complete working example with a Fastify app, library-mode vitest tests, and Docker compose configuration.
1123
+
1124
+ ## Conventions
1125
+
1126
+ - Account ID: `000000000000`
1127
+ - Region: auto-detected from SDK `Authorization` header; falls back to `defaultRegion` (defaults to `us-east-1`). Region is part of entity identity — same-name entities in different regions are independent.
1128
+ - Queue URL format: `http://sqs.{region}.{host}:{port}/000000000000/{queueName}` (host defaults to `localhost`)
1129
+ - Queue ARN format: `arn:aws:sqs:{region}:000000000000:{queueName}`
1130
+ - Topic ARN format: `arn:aws:sns:{region}:000000000000:{topicName}`
1131
+
1132
+ ## Limitations
1133
+
1134
+ fauxqs is designed for development and testing. It does not support:
1135
+
1136
+ - Non-SQS SNS delivery protocols (HTTP/S, Lambda, email, SMS)
1137
+ - Persistence across restarts
1138
+ - Authentication or authorization
1139
+ - Cross-account operations
1140
+
1141
+ ## Examples
1142
+
1143
+ The [`examples/`](examples/) directory contains runnable TypeScript examples covering fauxqs-specific features beyond standard AWS SDK usage:
1144
+
1145
+ | Example | Description |
1146
+ |---------|-------------|
1147
+ | [`alternatives/programmatic/programmatic-api.ts`](examples/alternatives/programmatic/programmatic-api.ts) | Server lifecycle, resource creation, SDK usage, `inspectQueue()`, `reset()`, `purgeAll()`, `setup()` |
1148
+ | [`alternatives/programmatic/message-spy.ts`](examples/alternatives/programmatic/message-spy.ts) | `MessageSpyReader` — all spy methods, partial/predicate filters, discriminated union narrowing, DLQ tracking |
1149
+ | [`alternatives/programmatic/init-config.ts`](examples/alternatives/programmatic/init-config.ts) | File-based and inline init config, DLQ chains, `setup()` idempotency, purge + re-apply pattern |
1150
+ | [`alternatives/programmatic/queue-inspection.ts`](examples/alternatives/programmatic/queue-inspection.ts) | Programmatic `inspectQueue()` and HTTP `/_fauxqs/queues` endpoints |
1151
+ | [`alternatives/docker/standalone/`](examples/alternatives/docker/standalone/standalone-container.ts) | Connecting to a fauxqs Docker container from the host |
1152
+ | [`alternatives/docker/container-to-container/`](examples/alternatives/docker/container-to-container/) | Container-to-container communication via docker-compose |
1153
+ | [`recommended/`](examples/recommended/) | Dual-mode testing: library mode (vitest + spy) for CI, Docker for local dev |
1154
+
1155
+ All examples are type-checked in CI to prevent staleness.
1156
+
1157
+ ## Benchmarks
1158
+
1159
+ SQS throughput benchmarks are available in the [`benchmarks/`](benchmarks/) directory, comparing fauxqs across different deployment modes (in-process library, official Docker image, lightweight Docker container) and against LocalStack. See [`benchmarks/BENCHMARKING.md`](benchmarks/BENCHMARKING.md) for setup descriptions, instructions, and how to interpret results.
1160
+
1161
+ ## License
1162
+
1163
+ MIT