fauxqs 1.12.0 → 2.0.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.
Files changed (58) hide show
  1. package/README.md +1532 -1462
  2. package/dist/app.d.ts +5 -1
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +24 -2
  5. package/dist/app.js.map +1 -1
  6. package/dist/initConfig.d.ts +4 -1
  7. package/dist/initConfig.d.ts.map +1 -1
  8. package/dist/initConfig.js +15 -3
  9. package/dist/initConfig.js.map +1 -1
  10. package/dist/persistence.d.ts +53 -0
  11. package/dist/persistence.d.ts.map +1 -0
  12. package/dist/persistence.js +503 -0
  13. package/dist/persistence.js.map +1 -0
  14. package/dist/s3/actions/createBucket.d.ts.map +1 -1
  15. package/dist/s3/actions/createBucket.js +11 -1
  16. package/dist/s3/actions/createBucket.js.map +1 -1
  17. package/dist/s3/actions/renameObject.d.ts +9 -0
  18. package/dist/s3/actions/renameObject.d.ts.map +1 -0
  19. package/dist/s3/actions/renameObject.js +92 -0
  20. package/dist/s3/actions/renameObject.js.map +1 -0
  21. package/dist/s3/s3Router.d.ts.map +1 -1
  22. package/dist/s3/s3Router.js +5 -1
  23. package/dist/s3/s3Router.js.map +1 -1
  24. package/dist/s3/s3Store.d.ts +11 -2
  25. package/dist/s3/s3Store.d.ts.map +1 -1
  26. package/dist/s3/s3Store.js +76 -6
  27. package/dist/s3/s3Store.js.map +1 -1
  28. package/dist/server.js +7 -1
  29. package/dist/server.js.map +1 -1
  30. package/dist/sns/actions/confirmSubscription.d.ts.map +1 -1
  31. package/dist/sns/actions/confirmSubscription.js +1 -0
  32. package/dist/sns/actions/confirmSubscription.js.map +1 -1
  33. package/dist/sns/actions/setSubscriptionAttributes.d.ts.map +1 -1
  34. package/dist/sns/actions/setSubscriptionAttributes.js +1 -0
  35. package/dist/sns/actions/setSubscriptionAttributes.js.map +1 -1
  36. package/dist/sns/actions/setTopicAttributes.d.ts.map +1 -1
  37. package/dist/sns/actions/setTopicAttributes.js +1 -0
  38. package/dist/sns/actions/setTopicAttributes.js.map +1 -1
  39. package/dist/sns/actions/tagResource.d.ts.map +1 -1
  40. package/dist/sns/actions/tagResource.js +2 -0
  41. package/dist/sns/actions/tagResource.js.map +1 -1
  42. package/dist/sns/snsStore.d.ts +2 -0
  43. package/dist/sns/snsStore.d.ts.map +1 -1
  44. package/dist/sns/snsStore.js +8 -0
  45. package/dist/sns/snsStore.js.map +1 -1
  46. package/dist/spy.d.ts +1 -1
  47. package/dist/spy.d.ts.map +1 -1
  48. package/dist/sqs/actions/tagQueue.d.ts.map +1 -1
  49. package/dist/sqs/actions/tagQueue.js +1 -0
  50. package/dist/sqs/actions/tagQueue.js.map +1 -1
  51. package/dist/sqs/actions/untagQueue.d.ts.map +1 -1
  52. package/dist/sqs/actions/untagQueue.js +1 -0
  53. package/dist/sqs/actions/untagQueue.js.map +1 -1
  54. package/dist/sqs/sqsStore.d.ts +6 -0
  55. package/dist/sqs/sqsStore.d.ts.map +1 -1
  56. package/dist/sqs/sqsStore.js +40 -3
  57. package/dist/sqs/sqsStore.js.map +1 -1
  58. package/package.json +3 -2
package/README.md CHANGED
@@ -1,1462 +1,1532 @@
1
- <img src="logo-readme.jpg" alt="fauxqs" width="360" />
2
-
3
- [![npm version](https://img.shields.io/npm/v/fauxqs.svg)](https://www.npmjs.com/package/fauxqs)
4
-
5
- # fauxqs
6
-
7
- 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.
8
-
9
- All state is in-memory. No persistence, no external storage dependencies.
10
-
11
- ## Table of Contents
12
-
13
- - [Installation](#installation)
14
- - [Usage](#usage)
15
- - [Running the server](#running-the-server)
16
- - [Running in the background](#running-in-the-background)
17
- - [Running with Docker](#running-with-docker)
18
- - [Running in Docker Compose](#running-in-docker-compose)
19
- - [Container-to-container S3 virtual-hosted-style](#container-to-container-s3-virtual-hosted-style)
20
- - [Configuring AWS SDK clients](#configuring-aws-sdk-clients)
21
- - [Programmatic usage](#programmatic-usage)
22
- - [Relaxed rules](#relaxed-rules)
23
- - [Programmatic state setup](#programmatic-state-setup)
24
- - [Sending messages programmatically](#sending-messages-programmatically)
25
- - [Init config file](#init-config-file)
26
- - [Init config schema reference](#init-config-schema-reference)
27
- - [Message spy](#message-spy)
28
- - [Queue inspection](#queue-inspection)
29
- - [Configurable queue URL host](#configurable-queue-url-host)
30
- - [Region](#region)
31
- - [Supported API Actions](#supported-api-actions)
32
- - [SQS](#sqs)
33
- - [SNS](#sns)
34
- - [S3](#s3)
35
- - [STS](#sts)
36
- - [SQS Features](#sqs-features)
37
- - [SNS Features](#sns-features)
38
- - [S3 Features](#s3-features)
39
- - [S3 URL styles](#s3-url-styles)
40
- - [Using with AWS CLI](#using-with-aws-cli)
41
- - [Testing Strategies](#testing-strategies)
42
- - [Library mode for tests](#library-mode-for-tests)
43
- - [Docker mode for local development](#docker-mode-for-local-development)
44
- - [Recommended combination](#recommended-combination)
45
- - [Conventions](#conventions)
46
- - [Limitations](#limitations)
47
- - [Examples](#examples)
48
- - [Migrating from LocalStack](#migrating-from-localstack)
49
- - [SNS/SQS only](#snssqs-only)
50
- - [SNS/SQS/S3](#snssqss3)
51
- - [Going hybrid (recommended)](#going-hybrid-recommended)
52
- - [Benchmarks](#benchmarks)
53
- - [License](#license)
54
-
55
- ## Installation
56
-
57
- **Docker** (recommended for standalone usage) — [Docker Hub](https://hub.docker.com/r/kibertoad/fauxqs):
58
-
59
- ```bash
60
- docker run -p 4566:4566 kibertoad/fauxqs
61
- ```
62
-
63
- **npm** (for embedded library usage or CLI):
64
-
65
- ```bash
66
- npm install fauxqs
67
- ```
68
-
69
- ## Usage
70
-
71
- ### Running the server
72
-
73
- ```bash
74
- npx fauxqs
75
- ```
76
-
77
- The server starts on port `4566` and handles SQS, SNS, and S3 on a single endpoint.
78
-
79
- #### Environment variables
80
-
81
- | Variable | Description | Default |
82
- |----------|-------------|---------|
83
- | `FAUXQS_PORT` | Port to listen on | `4566` |
84
- | `FAUXQS_HOST` | Host for queue URLs (`sqs.<region>.<host>` format) | `localhost` |
85
- | `FAUXQS_DEFAULT_REGION` | Fallback region for ARNs and URLs | `us-east-1` |
86
- | `FAUXQS_LOGGER` | Enable request logging (`true`/`false`) | `true` |
87
- | `FAUXQS_INIT` | Path to a JSON init config file (see [Init config file](#init-config-file)) | (none) |
88
- | `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 |
89
- | `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` |
90
-
91
- ```bash
92
- FAUXQS_PORT=3000 FAUXQS_INIT=init.json npx fauxqs
93
- ```
94
-
95
- A health check is available at `GET /health`.
96
-
97
- ### Running in the background
98
-
99
- To keep fauxqs running while you work on your app or run tests repeatedly, start it as a background process:
100
-
101
- ```bash
102
- npx fauxqs &
103
- ```
104
-
105
- Or in a separate terminal:
106
-
107
- ```bash
108
- npx fauxqs
109
- ```
110
-
111
- All state accumulates in memory across requests, so queues, topics, and objects persist until the server is stopped.
112
-
113
- To stop the server:
114
-
115
- ```bash
116
- # If backgrounded in the same shell
117
- kill %1
118
-
119
- # Cross-platform, by port
120
- npx cross-port-killer 4566
121
- ```
122
-
123
- ### Running with Docker
124
-
125
- The official Docker image is available on Docker Hub:
126
-
127
- ```bash
128
- docker run -p 4566:4566 kibertoad/fauxqs
129
- ```
130
-
131
- With an init config file:
132
-
133
- ```bash
134
- docker run -p 4566:4566 \
135
- -v ./init.json:/app/init.json \
136
- -e FAUXQS_INIT=/app/init.json \
137
- kibertoad/fauxqs
138
- ```
139
-
140
- ### Running in Docker Compose
141
-
142
- Use the `kibertoad/fauxqs` image and mount a JSON init config to pre-create resources on startup:
143
-
144
- ```json
145
- // scripts/fauxqs/init.json
146
- {
147
- "queues": [
148
- {
149
- "name": "my-queue.fifo",
150
- "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
151
- },
152
- { "name": "my-dlq" }
153
- ],
154
- "topics": [{ "name": "my-events" }],
155
- "subscriptions": [{ "topic": "my-events", "queue": "my-dlq" }],
156
- "buckets": ["my-uploads"]
157
- }
158
- ```
159
-
160
- ```yaml
161
- # docker-compose.yml
162
- services:
163
- fauxqs:
164
- image: kibertoad/fauxqs:latest
165
- ports:
166
- - "4566:4566"
167
- environment:
168
- - FAUXQS_INIT=/app/init.json
169
- volumes:
170
- - ./scripts/fauxqs/init.json:/app/init.json
171
-
172
- app:
173
- # ...
174
- depends_on:
175
- fauxqs:
176
- condition: service_healthy
177
- ```
178
-
179
- 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.
180
-
181
- #### Container-to-container S3 virtual-hosted-style
182
-
183
- 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`.
184
-
185
- To use it, assign fauxqs a static IP and point other containers' DNS to it:
186
-
187
- ```yaml
188
- # docker-compose.yml
189
- services:
190
- fauxqs:
191
- image: kibertoad/fauxqs:latest
192
- networks:
193
- default:
194
- ipv4_address: 10.0.0.2
195
- ports:
196
- - "4566:4566"
197
- environment:
198
- - FAUXQS_INIT=/app/init.json
199
- - FAUXQS_HOST=fauxqs
200
- volumes:
201
- - ./scripts/fauxqs/init.json:/app/init.json
202
-
203
- app:
204
- dns: 10.0.0.2
205
- depends_on:
206
- fauxqs:
207
- condition: service_healthy
208
- environment:
209
- - AWS_ENDPOINT=http://s3.fauxqs:4566
210
-
211
- networks:
212
- default:
213
- ipam:
214
- config:
215
- - subnet: 10.0.0.0/24
216
- ```
217
-
218
- From the `app` container, `my-bucket.s3.fauxqs` resolves to `10.0.0.2` (the fauxqs container), so virtual-hosted-style S3 works:
219
-
220
- ```typescript
221
- const s3 = new S3Client({
222
- endpoint: "http://s3.fauxqs:4566",
223
- region: "us-east-1",
224
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
225
- // No forcePathStyle needed!
226
- });
227
- ```
228
-
229
- 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.
230
-
231
- ### Configuring AWS SDK clients
232
-
233
- Point your SDK clients at the local server:
234
-
235
- ```typescript
236
- import { SQSClient } from "@aws-sdk/client-sqs";
237
- import { SNSClient } from "@aws-sdk/client-sns";
238
- import { S3Client } from "@aws-sdk/client-s3";
239
-
240
- const sqsClient = new SQSClient({
241
- endpoint: "http://localhost:4566",
242
- region: "us-east-1",
243
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
244
- });
245
-
246
- const snsClient = new SNSClient({
247
- endpoint: "http://localhost:4566",
248
- region: "us-east-1",
249
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
250
- });
251
-
252
- // Using fauxqs.dev wildcard DNS — no helpers or forcePathStyle needed
253
- const s3Client = new S3Client({
254
- endpoint: "http://s3.localhost.fauxqs.dev:4566",
255
- region: "us-east-1",
256
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
257
- });
258
- ```
259
-
260
- Any credentials are accepted and never validated.
261
-
262
- > **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.
263
-
264
- ### Programmatic usage
265
-
266
- You can also embed fauxqs directly in your test suite:
267
-
268
- ```typescript
269
- import { startFauxqs } from "fauxqs";
270
-
271
- const server = await startFauxqs({ port: 4566, logger: false });
272
-
273
- console.log(server.address); // "http://127.0.0.1:4566"
274
- console.log(server.port); // 4566
275
-
276
- // point your SDK clients at server.address
277
-
278
- // clean up when done
279
- await server.stop();
280
- ```
281
-
282
- Pass `port: 0` to let the OS assign a random available port (useful in tests).
283
-
284
- #### Relaxed rules
285
-
286
- By default, fauxqs enforces AWS-strict validations. You can selectively relax some of these for convenience during development:
287
-
288
- ```typescript
289
- const server = await startFauxqs({
290
- port: 0,
291
- relaxedRules: {
292
- disableMinCopySourceSize: true,
293
- },
294
- });
295
- ```
296
-
297
- | Rule | Default | Description |
298
- |------|---------|-------------|
299
- | `disableMinCopySourceSize` | `false` | AWS requires the source object to be [larger than 5 MiB](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html) for byte-range `UploadPartCopy`. Set to `true` to allow byte-range copies from smaller sources. |
300
-
301
- #### Programmatic state setup
302
-
303
- The server object exposes methods for pre-creating resources without going through the SDK:
304
-
305
- ```typescript
306
- const server = await startFauxqs({ port: 0, logger: false });
307
-
308
- // Create individual resources — each returns metadata about the created resource
309
- const { queueUrl, queueArn, queueName } = server.createQueue("my-queue");
310
- const { queueUrl: dlqUrl } = server.createQueue("my-dlq", {
311
- attributes: { VisibilityTimeout: "60" },
312
- tags: { env: "test" },
313
- });
314
- const { topicArn } = server.createTopic("my-topic");
315
- server.subscribe({ topic: "my-topic", queue: "my-queue" });
316
- const { bucketName } = server.createBucket("my-bucket");
317
-
318
- // Create resources in a specific region
319
- server.createQueue("eu-queue", { region: "eu-west-1" });
320
- server.createTopic("eu-topic", { region: "eu-west-1" });
321
- server.subscribe({ topic: "eu-topic", queue: "eu-queue", region: "eu-west-1" });
322
-
323
- // Or create everything at once
324
- server.setup({
325
- queues: [
326
- { name: "orders" },
327
- { name: "notifications", attributes: { DelaySeconds: "5" } },
328
- { name: "eu-orders", region: "eu-west-1" },
329
- ],
330
- topics: [{ name: "events" }],
331
- subscriptions: [
332
- { topic: "events", queue: "orders" },
333
- { topic: "events", queue: "notifications" },
334
- ],
335
- buckets: ["uploads", "exports"],
336
- });
337
-
338
- // Delete individual resources (uses defaultRegion; pass { region } to override)
339
- server.deleteQueue("my-queue"); // no-op if queue doesn't exist
340
- server.deleteQueue("eu-queue", { region: "eu-west-1" }); // delete in specific region
341
- server.deleteTopic("my-topic"); // also removes associated subscriptions
342
- server.deleteTopic("eu-topic", { region: "eu-west-1" });
343
- server.emptyBucket("my-bucket"); // removes all objects, keeps the bucket
344
-
345
- // Clear all messages and S3 objects between tests (keeps queues, topics, subscriptions, buckets)
346
- server.reset();
347
-
348
- // Or nuke everything removes queues, topics, subscriptions, and buckets too
349
- server.purgeAll();
350
- ```
351
-
352
- #### Sending messages programmatically
353
-
354
- Send SQS messages and publish to SNS topics without instantiating SDK clients:
355
-
356
- ```typescript
357
- // SQS: enqueue a message directly
358
- const { messageId, md5OfBody } = server.sendMessage("my-queue", "hello world");
359
-
360
- // With message attributes
361
- server.sendMessage("my-queue", JSON.stringify({ orderId: "123" }), {
362
- messageAttributes: {
363
- eventType: { DataType: "String", StringValue: "order.created" },
364
- },
365
- });
366
-
367
- // With delay
368
- server.sendMessage("my-queue", "delayed message", { delaySeconds: 10 });
369
-
370
- // FIFO queue — returns sequenceNumber
371
- const { sequenceNumber } = server.sendMessage("my-queue.fifo", "fifo message", {
372
- messageGroupId: "group-1",
373
- messageDeduplicationId: "dedup-1",
374
- });
375
-
376
- // SNS: publish to a topic (fans out to all SQS subscriptions)
377
- const { messageId: snsMessageId } = server.publish("my-topic", "event payload");
378
-
379
- // With subject and message attributes
380
- server.publish("my-topic", JSON.stringify({ orderId: "456" }), {
381
- subject: "Order Update",
382
- messageAttributes: {
383
- eventType: { DataType: "String", StringValue: "order.updated" },
384
- },
385
- });
386
-
387
- // FIFO topic
388
- server.publish("my-topic.fifo", "fifo event", {
389
- messageGroupId: "group-1",
390
- messageDeduplicationId: "dedup-1",
391
- });
392
- ```
393
-
394
- `sendMessage` validates the message body (invalid characters, size limits), applies queue-level `DelaySeconds` defaults, handles FIFO deduplication, and emits spy events automatically. Returns `{ messageId, md5OfBody, md5OfMessageAttributes?, sequenceNumber? }`.
395
-
396
- `publish` validates message size, evaluates filter policies on each subscription, supports raw message delivery, and emits spy events. Returns `{ messageId }`.
397
-
398
- Both methods throw if the target queue or topic doesn't exist.
399
-
400
- #### Init config file
401
-
402
- 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.
403
-
404
- ```json
405
- {
406
- "queues": [
407
- { "name": "orders" },
408
- { "name": "orders-dlq" },
409
- { "name": "orders.fifo", "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" } }
410
- ],
411
- "topics": [
412
- { "name": "events" }
413
- ],
414
- "subscriptions": [
415
- { "topic": "events", "queue": "orders" }
416
- ],
417
- "buckets": ["uploads", "exports"]
418
- }
419
- ```
420
-
421
- Pass it via the `FAUXQS_INIT` environment variable or the `init` option:
422
-
423
- ```bash
424
- FAUXQS_INIT=init.json npx fauxqs
425
- ```
426
-
427
- ```typescript
428
- const server = await startFauxqs({ init: "init.json" });
429
- // or inline:
430
- const server = await startFauxqs({
431
- init: { queues: [{ name: "my-queue" }], buckets: ["my-bucket"] },
432
- });
433
- ```
434
-
435
- #### Init config schema reference
436
-
437
- All top-level fields are optional. Resources are created in dependency order: queues, topics, subscriptions, buckets.
438
-
439
- ##### `queues`
440
-
441
- Array of queue objects.
442
-
443
- | Field | Type | Required | Description |
444
- |-------|------|----------|-------------|
445
- | `name` | `string` | Yes | Queue name. Use `.fifo` suffix for FIFO queues. |
446
- | `region` | `string` | No | Override the default region for this queue. The queue's ARN and URL will use this region. |
447
- | `attributes` | `Record<string, string>` | No | Queue attributes (see table below). |
448
- | `tags` | `Record<string, string>` | No | Key-value tags for the queue. |
449
-
450
- Supported queue attributes:
451
-
452
- | Attribute | Default | Range / Values |
453
- |-----------|---------|----------------|
454
- | `VisibilityTimeout` | `"30"` | `0` – `43200` (seconds) |
455
- | `DelaySeconds` | `"0"` | `0` – `900` (seconds) |
456
- | `MaximumMessageSize` | `"1048576"` | `1024` – `1048576` (bytes) |
457
- | `MessageRetentionPeriod` | `"345600"` | `60` `1209600` (seconds) |
458
- | `ReceiveMessageWaitTimeSeconds` | `"0"` | `0` – `20` (seconds) |
459
- | `RedrivePolicy` | — | JSON string: `{"deadLetterTargetArn": "arn:...", "maxReceiveCount": "5"}` |
460
- | `Policy` | — | Queue policy JSON string (stored, not enforced) |
461
- | `KmsMasterKeyId` | — | KMS key ID (stored, no actual encryption) |
462
- | `KmsDataKeyReusePeriodSeconds` | — | KMS data key reuse period (stored, no actual encryption) |
463
- | `FifoQueue` | | `"true"` for FIFO queues (queue name must end with `.fifo`) |
464
- | `ContentBasedDeduplication` | — | `"true"` or `"false"` (FIFO queues only) |
465
-
466
- Example:
467
-
468
- ```json
469
- {
470
- "queues": [
471
- {
472
- "name": "orders",
473
- "attributes": { "VisibilityTimeout": "60", "DelaySeconds": "5" },
474
- "tags": { "env": "staging", "team": "platform" }
475
- },
476
- {
477
- "name": "orders-dlq"
478
- },
479
- {
480
- "name": "orders.fifo",
481
- "attributes": {
482
- "FifoQueue": "true",
483
- "ContentBasedDeduplication": "true"
484
- }
485
- },
486
- {
487
- "name": "retry-queue",
488
- "attributes": {
489
- "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-dlq\",\"maxReceiveCount\":\"3\"}"
490
- }
491
- },
492
- {
493
- "name": "eu-orders",
494
- "region": "eu-west-1"
495
- }
496
- ]
497
- }
498
- ```
499
-
500
- ##### `topics`
501
-
502
- Array of topic objects.
503
-
504
- | Field | Type | Required | Description |
505
- |-------|------|----------|-------------|
506
- | `name` | `string` | Yes | Topic name. Use `.fifo` suffix for FIFO topics. |
507
- | `region` | `string` | No | Override the default region for this topic. The topic's ARN will use this region. |
508
- | `attributes` | `Record<string, string>` | No | Topic attributes (e.g., `DisplayName`). |
509
- | `tags` | `Record<string, string>` | No | Key-value tags for the topic. |
510
-
511
- Example:
512
-
513
- ```json
514
- {
515
- "topics": [
516
- {
517
- "name": "events",
518
- "attributes": { "DisplayName": "Application Events" },
519
- "tags": { "env": "staging" }
520
- },
521
- {
522
- "name": "events.fifo",
523
- "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
524
- }
525
- ]
526
- }
527
- ```
528
-
529
- ##### `subscriptions`
530
-
531
- Array of subscription objects. Referenced topics and queues must be defined in the same config (or already exist on the server).
532
-
533
- | Field | Type | Required | Description |
534
- |-------|------|----------|-------------|
535
- | `topic` | `string` | Yes | Topic name (not ARN) to subscribe to. |
536
- | `queue` | `string` | Yes | Queue name (not ARN) to deliver messages to. |
537
- | `region` | `string` | No | Override the default region. The topic and queue ARNs will be resolved in this region. |
538
- | `attributes` | `Record<string, string>` | No | Subscription attributes (see table below). |
539
-
540
- Supported subscription attributes:
541
-
542
- | Attribute | Values | Description |
543
- |-----------|--------|-------------|
544
- | `RawMessageDelivery` | `"true"` / `"false"` | Deliver the raw message body instead of the SNS envelope JSON. |
545
- | `FilterPolicy` | JSON string | SNS filter policy for message filtering (e.g., `"{\"color\": [\"blue\"]}"`) |
546
- | `FilterPolicyScope` | `"MessageAttributes"` / `"MessageBody"` | Whether the filter policy applies to message attributes or body. Defaults to `MessageAttributes`. |
547
- | `RedrivePolicy` | JSON string | Subscription-level dead-letter queue config. |
548
- | `DeliveryPolicy` | JSON string | Delivery retry policy (stored, not enforced). |
549
- | `SubscriptionRoleArn` | ARN string | IAM role ARN for delivery (stored, not enforced). |
550
-
551
- Example:
552
-
553
- ```json
554
- {
555
- "subscriptions": [
556
- {
557
- "topic": "events",
558
- "queue": "orders",
559
- "attributes": {
560
- "RawMessageDelivery": "true",
561
- "FilterPolicy": "{\"eventType\": [\"order.created\", \"order.updated\"]}"
562
- }
563
- },
564
- {
565
- "topic": "events",
566
- "queue": "notifications"
567
- }
568
- ]
569
- }
570
- ```
571
-
572
- ##### `buckets`
573
-
574
- Array of bucket name strings.
575
-
576
- ```json
577
- {
578
- "buckets": ["uploads", "exports", "temp"]
579
- }
580
- ```
581
-
582
- #### Message spy
583
-
584
- `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`.
585
-
586
- Enable it with the `messageSpies` option:
587
-
588
- ```typescript
589
- const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
590
- ```
591
-
592
- The spy tracks events across all three services using a discriminated union on `service`:
593
-
594
- **SQS events** (`service: 'sqs'`):
595
- - **`published`** — message was enqueued (via SendMessage, SendMessageBatch, or SNS fan-out)
596
- - **`consumed`** — message was deleted (via DeleteMessage / DeleteMessageBatch)
597
- - **`dlq`** — message exceeded `maxReceiveCount` and was moved to a dead-letter queue
598
-
599
- **SNS events** (`service: 'sns'`):
600
- - **`published`** — message was published to a topic (before fan-out to SQS subscriptions)
601
-
602
- **S3 events** (`service: 's3'`):
603
- - **`uploaded`** — object was put (PutObject or CompleteMultipartUpload)
604
- - **`downloaded`** — object was retrieved (GetObject)
605
- - **`deleted`** — object was deleted (DeleteObject, only when key existed)
606
- - **`copied`** object was copied (CopyObject; also emits `uploaded` for the destination)
607
-
608
- ##### Awaiting messages
609
-
610
- ```typescript
611
- // Wait for a specific SQS message (resolves immediately if already in buffer)
612
- const msg = await server.spy.waitForMessage(
613
- (m) => m.service === "sqs" && m.body === "order.created" && m.queueName === "orders",
614
- "published",
615
- );
616
-
617
- // Wait by SQS message ID
618
- const msg = await server.spy.waitForMessageWithId(messageId, "consumed");
619
-
620
- // Partial object match (deep-equal on specified fields)
621
- const msg = await server.spy.waitForMessage({ service: "sqs", queueName: "orders", status: "published" });
622
-
623
- // Wait for an SNS publish event
624
- const msg = await server.spy.waitForMessage({ service: "sns", topicName: "my-topic", status: "published" });
625
-
626
- // Wait for an S3 upload event
627
- const msg = await server.spy.waitForMessage({ service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" });
628
- ```
629
-
630
- `waitForMessage` checks the buffer first (retroactive resolution). If no match is found, it returns a Promise that resolves when a matching message arrives.
631
-
632
- ##### Timeout
633
-
634
- 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:
635
-
636
- ```typescript
637
- // Reject after 2 seconds if no match
638
- const msg = await server.spy.waitForMessage(
639
- { service: "sqs", queueName: "orders" },
640
- "published",
641
- 2000,
642
- );
643
-
644
- // Also works with waitForMessageWithId
645
- const msg = await server.spy.waitForMessageWithId(messageId, "consumed", 5000);
646
- ```
647
-
648
- ##### Waiting for multiple messages
649
-
650
- `waitForMessages` collects `count` matching messages before resolving. It checks the buffer first, then awaits future arrivals:
651
-
652
- ```typescript
653
- // Wait for 3 messages on the orders queue
654
- const msgs = await server.spy.waitForMessages(
655
- { service: "sqs", queueName: "orders" },
656
- { count: 3, status: "published", timeout: 5000 },
657
- );
658
- // msgs.length === 3
659
- ```
660
-
661
- If the timeout expires before enough messages arrive, the promise rejects with a message showing how many were collected (e.g., `"collected 1/3"`).
662
-
663
- ##### Negative assertions
664
-
665
- `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:
666
-
667
- ```typescript
668
- // Assert no message was delivered to the wrong queue (waits 200ms by default)
669
- await server.spy.expectNoMessage({ service: "sqs", queueName: "wrong-queue" });
670
-
671
- // Custom window and status filter
672
- await server.spy.expectNoMessage(
673
- { service: "sqs", queueName: "orders" },
674
- { status: "dlq", within: 500 },
675
- );
676
- ```
677
-
678
- 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"`.
679
-
680
- ##### Synchronous check
681
-
682
- ```typescript
683
- const msg = server.spy.checkForMessage(
684
- (m) => m.service === "sqs" && m.queueName === "my-queue",
685
- "published",
686
- );
687
- // returns SpyMessage | undefined
688
- ```
689
-
690
- ##### Buffer management
691
-
692
- ```typescript
693
- // Get all tracked messages (oldest to newest)
694
- const all = server.spy.getAllMessages();
695
-
696
- // Clear buffer and reject pending waiters
697
- server.spy.clear();
698
- ```
699
-
700
- The buffer defaults to 100 messages (FIFO eviction). Configure with:
701
-
702
- ```typescript
703
- const server = await startFauxqs({
704
- messageSpies: { bufferSize: 500 },
705
- });
706
- ```
707
-
708
- ##### Types
709
-
710
- `server.spy` returns a `MessageSpyReader` — a read-only interface that exposes query and await methods but not internal mutation (e.g. recording new events):
711
-
712
- ```typescript
713
- interface MessageSpyReader {
714
- waitForMessage(filter: MessageSpyFilter, status?: string, timeout?: number): Promise<SpyMessage>;
715
- waitForMessageWithId(messageId: string, status?: string, timeout?: number): Promise<SpyMessage>;
716
- waitForMessages(filter: MessageSpyFilter, options: WaitForMessagesOptions): Promise<SpyMessage[]>;
717
- expectNoMessage(filter: MessageSpyFilter, options?: ExpectNoMessageOptions): Promise<void>;
718
- checkForMessage(filter: MessageSpyFilter, status?: string): SpyMessage | undefined;
719
- getAllMessages(): SpyMessage[];
720
- clear(): void;
721
- }
722
-
723
- interface WaitForMessagesOptions {
724
- count: number;
725
- status?: string;
726
- timeout?: number;
727
- }
728
-
729
- interface ExpectNoMessageOptions {
730
- status?: string;
731
- within?: number; // ms, defaults to 200
732
- }
733
- ```
734
-
735
- `SpyMessage` is a discriminated union:
736
-
737
- ```typescript
738
- interface SqsSpyMessage {
739
- service: "sqs";
740
- queueName: string;
741
- messageId: string;
742
- body: string;
743
- messageAttributes: Record<string, MessageAttributeValue>;
744
- status: "published" | "consumed" | "dlq";
745
- timestamp: number;
746
- }
747
-
748
- interface SnsSpyMessage {
749
- service: "sns";
750
- topicArn: string;
751
- topicName: string;
752
- messageId: string;
753
- body: string;
754
- messageAttributes: Record<string, MessageAttributeValue>;
755
- status: "published";
756
- timestamp: number;
757
- }
758
-
759
- interface S3SpyEvent {
760
- service: "s3";
761
- bucket: string;
762
- key: string;
763
- status: "uploaded" | "downloaded" | "deleted" | "copied";
764
- timestamp: number;
765
- }
766
-
767
- type SpyMessage = SqsSpyMessage | SnsSpyMessage | S3SpyEvent;
768
- ```
769
-
770
- ##### Spy disabled by default
771
-
772
- Accessing `server.spy` when `messageSpies` is not set throws an error. There is no overhead on the message flow when spies are disabled.
773
-
774
- #### Queue inspection
775
-
776
- Non-destructive inspection of SQS queue state — see all messages (ready, in-flight, and delayed) without consuming them or affecting visibility timeouts.
777
-
778
- ##### Programmatic API
779
-
780
- ```typescript
781
- const result = server.inspectQueue("my-queue");
782
- // result is undefined if queue doesn't exist
783
- if (result) {
784
- console.log(result.name); // "my-queue"
785
- console.log(result.url); // "http://sqs.us-east-1.localhost:4566/000000000000/my-queue"
786
- console.log(result.arn); // "arn:aws:sqs:us-east-1:000000000000:my-queue"
787
- console.log(result.attributes); // { VisibilityTimeout: "30", ... }
788
- console.log(result.messages.ready); // messages available for receive
789
- console.log(result.messages.delayed); // messages waiting for delay to expire
790
- console.log(result.messages.inflight); // received but not yet deleted
791
- // Each inflight entry includes: { message, receiptHandle, visibilityDeadline }
792
- }
793
- ```
794
-
795
- ##### HTTP endpoints
796
-
797
- ```bash
798
- # List all queues with summary counts
799
- curl http://localhost:4566/_fauxqs/queues
800
- # [{ "name": "my-queue", "approximateMessageCount": 5, "approximateInflightCount": 2, "approximateDelayedCount": 0, ... }]
801
-
802
- # Inspect a specific queue (full state)
803
- curl http://localhost:4566/_fauxqs/queues/my-queue
804
- # { "name": "my-queue", "messages": { "ready": [...], "delayed": [...], "inflight": [...] }, ... }
805
- ```
806
-
807
- Returns 404 for non-existent queues. Inspection never modifies queue state — messages remain exactly where they are.
808
-
809
- ### Configurable queue URL host
810
-
811
- 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`.
812
-
813
- To override the host (e.g., for a custom domain):
814
-
815
- ```typescript
816
- import { startFauxqs } from "fauxqs";
817
-
818
- const server = await startFauxqs({ port: 4566, host: "myhost.local" });
819
- // Queue URLs: http://sqs.us-east-1.myhost.local:4566/000000000000/myQueue
820
- ```
821
-
822
- This also works with `buildApp`:
823
-
824
- ```typescript
825
- import { buildApp } from "fauxqs";
826
-
827
- const app = buildApp({ host: "myhost.local" });
828
- ```
829
-
830
- 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.
831
-
832
- ### Region
833
-
834
- 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.
835
-
836
- 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.
837
-
838
- ```typescript
839
- const sqsEU = new SQSClient({ region: "eu-west-1", endpoint: "http://localhost:4566", ... });
840
- const sqsUS = new SQSClient({ region: "us-east-1", endpoint: "http://localhost:4566", ... });
841
-
842
- // These are two independent queues with different ARNs
843
- await sqsEU.send(new CreateQueueCommand({ QueueName: "orders" }));
844
- await sqsUS.send(new CreateQueueCommand({ QueueName: "orders" }));
845
- ```
846
-
847
- 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"`):
848
-
849
- ```typescript
850
- const server = await startFauxqs({ defaultRegion: "eu-west-1" });
851
- ```
852
-
853
- Resources created via init config or programmatic API use the `defaultRegion` unless overridden with an explicit `region` field:
854
-
855
- ```json
856
- {
857
- "queues": [
858
- { "name": "us-queue" },
859
- { "name": "eu-queue", "region": "eu-west-1" }
860
- ]
861
- }
862
- ```
863
-
864
- ## Supported API Actions
865
-
866
- ### SQS
867
-
868
- | Action | Supported |
869
- |--------|-----------|
870
- | CreateQueue | Yes |
871
- | DeleteQueue | Yes |
872
- | GetQueueUrl | Yes |
873
- | ListQueues | Yes |
874
- | GetQueueAttributes | Yes |
875
- | SetQueueAttributes | Yes |
876
- | PurgeQueue | Yes |
877
- | SendMessage | Yes |
878
- | SendMessageBatch | Yes |
879
- | ReceiveMessage | Yes |
880
- | DeleteMessage | Yes |
881
- | DeleteMessageBatch | Yes |
882
- | ChangeMessageVisibility | Yes |
883
- | ChangeMessageVisibilityBatch | Yes |
884
- | TagQueue | Yes |
885
- | UntagQueue | Yes |
886
- | ListQueueTags | Yes |
887
- | AddPermission | No |
888
- | RemovePermission | No |
889
- | ListDeadLetterSourceQueues | No |
890
- | StartMessageMoveTask | No |
891
- | CancelMessageMoveTask | No |
892
- | ListMessageMoveTasks | No |
893
-
894
- ### SNS
895
-
896
- | Action | Supported |
897
- |--------|-----------|
898
- | CreateTopic | Yes |
899
- | DeleteTopic | Yes |
900
- | ListTopics | Yes |
901
- | GetTopicAttributes | Yes |
902
- | SetTopicAttributes | Yes |
903
- | Subscribe | Yes |
904
- | Unsubscribe | Yes |
905
- | ConfirmSubscription | Yes |
906
- | ListSubscriptions | Yes |
907
- | ListSubscriptionsByTopic | Yes |
908
- | GetSubscriptionAttributes | Yes |
909
- | SetSubscriptionAttributes | Yes |
910
- | Publish | Yes |
911
- | PublishBatch | Yes |
912
- | TagResource | Yes |
913
- | UntagResource | Yes |
914
- | ListTagsForResource | Yes |
915
- | AddPermission | No |
916
- | RemovePermission | No |
917
- | GetDataProtectionPolicy | No |
918
- | PutDataProtectionPolicy | No |
919
-
920
- Platform application, SMS, and phone number actions are not supported.
921
-
922
- ### S3
923
-
924
- | Action | Supported |
925
- |--------|-----------|
926
- | CreateBucket | Yes |
927
- | HeadBucket | Yes |
928
- | DeleteBucket | Yes |
929
- | ListBuckets | Yes |
930
- | ListObjects | Yes |
931
- | ListObjectsV2 | Yes |
932
- | PutObject | Yes |
933
- | GetObject | Yes |
934
- | HeadObject | Yes |
935
- | DeleteObject | Yes |
936
- | DeleteObjects | Yes |
937
- | CopyObject | Yes |
938
- | CreateMultipartUpload | Yes |
939
- | UploadPart | Yes |
940
- | UploadPartCopy | Yes |
941
- | CompleteMultipartUpload | Yes |
942
- | AbortMultipartUpload | Yes |
943
- | GetObjectAttributes | Yes |
944
- | GetBucketLocation | No |
945
- | ListObjectVersions | No |
946
- | SelectObjectContent | No |
947
- | RestoreObject | No |
948
- | RenameObject | No |
949
- | ListMultipartUploads | No |
950
- | ListParts | No |
951
-
952
- Bucket configuration (CORS, lifecycle, encryption, replication, logging, website, notifications, policy), ACLs, versioning, tagging, object lock, and public access block actions are not supported.
953
-
954
- ### STS
955
-
956
- | Action | Supported |
957
- |--------|-----------|
958
- | GetCallerIdentity | Yes |
959
- | AssumeRole | No |
960
- | GetSessionToken | No |
961
- | GetFederationToken | No |
962
-
963
- 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.
964
-
965
- ## SQS Features
966
-
967
- - **Message attributes** with MD5 checksums matching the AWS algorithm
968
- - **Visibility timeout** messages become invisible after receive and reappear after timeout
969
- - **Delay queues** per-queue default delay and per-message delay overrides
970
- - **Long polling** `WaitTimeSeconds` on ReceiveMessage blocks until messages arrive or timeout
971
- - **Dead letter queues** — messages exceeding `maxReceiveCount` are moved to the configured DLQ
972
- - **Batch operations** SendMessageBatch, DeleteMessageBatch, ChangeMessageVisibilityBatch with entry ID validation (`InvalidBatchEntryId`) and total batch size validation (`BatchRequestTooLong`)
973
- - **Queue attribute range validation** — validates `VisibilityTimeout`, `DelaySeconds`, `ReceiveMessageWaitTimeSeconds`, `MaximumMessageSize`, and `MessageRetentionPeriod` on both CreateQueue and SetQueueAttributes
974
- - **Message size validation** — rejects messages exceeding 1 MiB (1,048,576 bytes)
975
- - **Unicode character validation** — rejects messages with characters outside the AWS-allowed set
976
- - **KMS attributes** `KmsMasterKeyId` and `KmsDataKeyReusePeriodSeconds` are accepted and stored (no actual encryption)
977
- - **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
978
- - **Queue tags**
979
-
980
- ## SNS Features
981
-
982
- - **SNS-to-SQS fan-out** publish to a topic and messages are delivered to all confirmed SQS subscriptions
983
- - **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
984
- - **Raw message delivery** — configurable per subscription
985
- - **Message size validation** rejects messages exceeding 256 KB (262,144 bytes)
986
- - **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
987
- - **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
988
- - **Subscription attribute validation** — `SetSubscriptionAttributes` validates attribute names and rejects unknown or read-only attributes
989
- - **Topic and subscription tags**
990
- - **FIFO topics** — `.fifo` suffix enforcement, `MessageGroupId` and `MessageDeduplicationId` passthrough to SQS subscriptions, content-based deduplication
991
- - **Batch publish**
992
-
993
- ## S3 Features
994
-
995
- - **Bucket management** — CreateBucket (idempotent), DeleteBucket (rejects non-empty), HeadBucket, ListBuckets, ListObjects (V1 and V2)
996
- - **Object operations** PutObject, GetObject, DeleteObject, HeadObject, CopyObject with ETag, Content-Type, and Last-Modified headers
997
- - **Multipart uploads** CreateMultipartUpload, UploadPart, UploadPartCopy, CompleteMultipartUpload, AbortMultipartUpload with correct multipart ETag calculation (`MD5-of-part-digests-partCount`), metadata preservation, and part overwrite support
998
- - **ListObjects V2** prefix filtering, delimiter-based virtual directories, MaxKeys, continuation tokens, StartAfter
999
- - **CopyObject** same-bucket and cross-bucket copy via `x-amz-copy-source` header, with metadata preservation
1000
- - **User metadata** `x-amz-meta-*` headers are stored and returned on GetObject and HeadObject
1001
- - **Bulk delete** — DeleteObjects for batch key deletion with proper XML entity handling
1002
- - **Keys with slashes** — full support for slash-delimited keys (e.g., `path/to/file.txt`)
1003
- - **Stream uploads** handles AWS chunked transfer encoding (`Content-Encoding: aws-chunked`) for stream bodies, including trailing header parsing for checksums
1004
- - **Checksums** — CRC32, SHA1, and SHA256 checksums are stored on upload (PutObject, UploadPart) and returned on download (GetObject, HeadObject with `x-amz-checksum-mode: ENABLED`). Multipart uploads compute composite checksums. GetObjectAttributes supports the `Checksum` attribute. CRC32C and CRC64NVME are silently ignored. Checksums are stored and returned as-is — no body validation is performed.
1005
- - **GetObjectAttributes** selective metadata retrieval via `x-amz-object-attributes` header: ETag, StorageClass, ObjectSize, ObjectParts (with pagination), and Checksum (including per-part checksums for multipart objects)
1006
- - **Path-style and virtual-hosted-style** — both S3 URL styles are supported (see below)
1007
-
1008
- ### S3 URL styles
1009
-
1010
- 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.
1011
-
1012
- #### Option 1: `fauxqs.dev` wildcard DNS (recommended for Docker image)
1013
-
1014
- 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.
1015
-
1016
- ```typescript
1017
- import { S3Client } from "@aws-sdk/client-s3";
1018
-
1019
- const s3 = new S3Client({
1020
- endpoint: "http://s3.localhost.fauxqs.dev:4566",
1021
- region: "us-east-1",
1022
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
1023
- });
1024
- ```
1025
-
1026
- You can also use raw HTTP requests:
1027
-
1028
- ```bash
1029
- # Upload
1030
- curl -X PUT --data-binary @file.txt http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
1031
-
1032
- # Download
1033
- curl http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
1034
- ```
1035
-
1036
- 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.
1037
-
1038
- 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`.
1039
-
1040
- #### Option 2: `interceptLocalhostDns()` (recommended for embedded library)
1041
-
1042
- Patches Node.js `dns.lookup` so that any hostname ending in `.localhost` resolves to `127.0.0.1`. No client changes needed.
1043
-
1044
- ```typescript
1045
- import { interceptLocalhostDns } from "fauxqs";
1046
-
1047
- const restore = interceptLocalhostDns();
1048
-
1049
- const s3 = new S3Client({
1050
- endpoint: "http://s3.localhost:4566",
1051
- region: "us-east-1",
1052
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
1053
- });
1054
-
1055
- // When done (e.g., in afterAll):
1056
- restore();
1057
- ```
1058
-
1059
- The suffix is configurable: `interceptLocalhostDns("myhost.test")` matches `*.myhost.test`.
1060
-
1061
- **Tradeoffs:** Affects all DNS lookups in the process. Best suited for test suites (`beforeAll` / `afterAll`).
1062
-
1063
- #### Option 3: `createLocalhostHandler()` (per-client)
1064
-
1065
- 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.
1066
-
1067
- ```typescript
1068
- import { S3Client } from "@aws-sdk/client-s3";
1069
- import { createLocalhostHandler } from "fauxqs";
1070
-
1071
- const s3 = new S3Client({
1072
- endpoint: "http://s3.localhost:4566",
1073
- region: "us-east-1",
1074
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
1075
- requestHandler: createLocalhostHandler(),
1076
- });
1077
- ```
1078
-
1079
- #### Option 4: `forcePathStyle` (simplest fallback)
1080
-
1081
- 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.
1082
-
1083
- ```typescript
1084
- const s3 = new S3Client({
1085
- endpoint: "http://localhost:4566",
1086
- forcePathStyle: true,
1087
- // ...
1088
- });
1089
- ```
1090
-
1091
-
1092
- ### Using with AWS CLI
1093
-
1094
- fauxqs is wire-compatible with the standard AWS CLI. Point it at the fauxqs endpoint:
1095
-
1096
- #### SQS
1097
-
1098
- ```bash
1099
- aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name my-queue
1100
- aws --endpoint-url http://localhost:4566 sqs create-queue \
1101
- --queue-name my-queue.fifo \
1102
- --attributes FifoQueue=true,ContentBasedDeduplication=true
1103
- aws --endpoint-url http://localhost:4566 sqs send-message \
1104
- --queue-url http://localhost:4566/000000000000/my-queue \
1105
- --message-body "hello"
1106
- ```
1107
-
1108
- #### SNS
1109
-
1110
- ```bash
1111
- aws --endpoint-url http://localhost:4566 sns create-topic --name my-topic
1112
- aws --endpoint-url http://localhost:4566 sns subscribe \
1113
- --topic-arn arn:aws:sns:us-east-1:000000000000:my-topic \
1114
- --protocol sqs \
1115
- --notification-endpoint arn:aws:sqs:us-east-1:000000000000:my-queue
1116
- ```
1117
-
1118
- #### S3
1119
-
1120
- ```bash
1121
- aws --endpoint-url http://localhost:4566 s3 mb s3://my-bucket
1122
- aws --endpoint-url http://localhost:4566 s3 cp file.txt s3://my-bucket/file.txt
1123
- ```
1124
-
1125
- If the AWS CLI uses virtual-hosted-style S3 URLs by default, configure path-style:
1126
-
1127
- ```bash
1128
- aws configure set default.s3.addressing_style path
1129
- ```
1130
-
1131
- ## Testing Strategies
1132
-
1133
- fauxqs supports two deployment modes that complement each other for a complete testing workflow:
1134
-
1135
- | Mode | Best for | Startup | Assertions |
1136
- |------|----------|---------|------------|
1137
- | **Library** (embedded) | Unit tests, integration tests, CI | Milliseconds, in-process | Full programmatic API: `sendMessage`, `publish`, `spy`, `inspectQueue`, `reset`, `purgeAll` |
1138
- | **Docker** (standalone) | Local development, acceptance tests, dev environments | Seconds, real HTTP | Init config, HTTP inspection endpoints |
1139
-
1140
- ### Library mode for tests
1141
-
1142
- 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:
1143
-
1144
- ```typescript
1145
- // test/setup.ts
1146
- import { startFauxqs, type FauxqsServer } from "fauxqs";
1147
-
1148
- export async function createTestServer(): Promise<FauxqsServer> {
1149
- const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
1150
-
1151
- // Pre-create resources via the programmatic API (no SDK roundtrips)
1152
- server.createQueue("my-queue");
1153
- server.createBucket("my-bucket");
1154
-
1155
- return server;
1156
- }
1157
- ```
1158
-
1159
- ```typescript
1160
- // test/app.test.ts
1161
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
1162
-
1163
- let server: FauxqsServer;
1164
-
1165
- beforeAll(async () => { server = await createTestServer(); });
1166
- afterAll(async () => { await server.stop(); });
1167
- beforeEach(() => { server.spy.clear(); });
1168
-
1169
- it("tracks uploads via the spy", async () => {
1170
- // ... trigger your app logic that uploads to S3 ...
1171
-
1172
- const event = await server.spy.waitForMessage(
1173
- { service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" },
1174
- undefined,
1175
- 2000, // timeout — prevents tests from hanging
1176
- );
1177
- expect(event.status).toBe("uploaded");
1178
- });
1179
- ```
1180
-
1181
- 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.
1182
-
1183
- ### Docker mode for local development
1184
-
1185
- Use `docker-compose.yml` with an init config to give your team a consistent local environment:
1186
-
1187
- ```yaml
1188
- # docker-compose.yml
1189
- services:
1190
- fauxqs:
1191
- image: kibertoad/fauxqs:latest
1192
- ports: ["4566:4566"]
1193
- environment:
1194
- - FAUXQS_INIT=/app/init.json
1195
- volumes:
1196
- - ./fauxqs-init.json:/app/init.json
1197
-
1198
- app:
1199
- build: .
1200
- depends_on:
1201
- fauxqs:
1202
- condition: service_healthy
1203
- environment:
1204
- - AWS_ENDPOINT=http://fauxqs:4566
1205
- ```
1206
-
1207
- Docker mode validates your real deployment topology — networking, DNS, container-to-container communication — and is language-agnostic (any AWS SDK can connect).
1208
-
1209
- ### Recommended combination
1210
-
1211
- 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.
1212
-
1213
- See the [`examples/recommended/`](examples/recommended/) directory for a complete working example with a Fastify app, library-mode vitest tests, and Docker compose configuration.
1214
-
1215
- ## Conventions
1216
-
1217
- - Account ID: `000000000000`
1218
- - 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.
1219
- - Queue URL format: `http://sqs.{region}.{host}:{port}/000000000000/{queueName}` (host defaults to `localhost`)
1220
- - Queue ARN format: `arn:aws:sqs:{region}:000000000000:{queueName}`
1221
- - Topic ARN format: `arn:aws:sns:{region}:000000000000:{topicName}`
1222
-
1223
- ## Limitations
1224
-
1225
- fauxqs is designed for development and testing. It does not support:
1226
-
1227
- - Non-SQS SNS delivery protocols (HTTP/S, Lambda, email, SMS)
1228
- - Persistence across restarts
1229
- - Authentication or authorization
1230
- - Cross-account operations
1231
-
1232
- ## Examples
1233
-
1234
- The [`examples/`](examples/) directory contains runnable TypeScript examples covering fauxqs-specific features beyond standard AWS SDK usage:
1235
-
1236
- | Example | Description |
1237
- |---------|-------------|
1238
- | [`alternatives/programmatic/programmatic-api.ts`](examples/alternatives/programmatic/programmatic-api.ts) | Server lifecycle, resource creation, SDK usage, `inspectQueue()`, `reset()`, `purgeAll()`, `setup()` |
1239
- | [`alternatives/programmatic/message-spy.ts`](examples/alternatives/programmatic/message-spy.ts) | `MessageSpyReader` — all spy methods, partial/predicate filters, discriminated union narrowing, DLQ tracking |
1240
- | [`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 |
1241
- | [`alternatives/programmatic/queue-inspection.ts`](examples/alternatives/programmatic/queue-inspection.ts) | Programmatic `inspectQueue()` and HTTP `/_fauxqs/queues` endpoints |
1242
- | [`alternatives/docker/standalone/`](examples/alternatives/docker/standalone/standalone-container.ts) | Connecting to a fauxqs Docker container from the host |
1243
- | [`alternatives/docker/container-to-container/`](examples/alternatives/docker/container-to-container/) | Container-to-container communication via docker-compose |
1244
- | [`recommended/`](examples/recommended/) | Dual-mode testing: library mode (vitest + spy) for CI, Docker for local dev |
1245
-
1246
- All examples are type-checked in CI to prevent staleness.
1247
-
1248
- ## Migrating from LocalStack
1249
-
1250
- If you're currently using LocalStack for local SNS, SQS, and/or S3 emulation, fauxqs is a drop-in replacement for those services. Both listen on port 4566 by default and accept the same AWS SDK calls, so the migration is straightforward.
1251
-
1252
- There are two approaches: a Docker swap (quickest) and a hybrid setup (recommended for the best integration test experience). Which one makes sense depends on whether you use S3.
1253
-
1254
- ### SNS/SQS only
1255
-
1256
- If your LocalStack usage is limited to SNS and SQS, the migration is a one-line Docker image swap. No SDK client changes are needed — the endpoint URL, port, and credentials stay the same.
1257
-
1258
- **Docker swap:**
1259
-
1260
- ```yaml
1261
- # Before (LocalStack)
1262
- services:
1263
- localstack:
1264
- image: localstack/localstack
1265
- ports: ["4566:4566"]
1266
- environment:
1267
- - SERVICES=sqs,sns
1268
-
1269
- # After (fauxqs)
1270
- services:
1271
- fauxqs:
1272
- image: kibertoad/fauxqs:latest
1273
- ports: ["4566:4566"]
1274
- ```
1275
-
1276
- **Region:** Both LocalStack and fauxqs auto-detect the region from the SDK client's `Authorization` header, so in most setups no configuration is needed. If you have older LocalStack configs that used the now-removed `DEFAULT_REGION` env var (deprecated in v0.12.7, removed in v2.0), the equivalent in fauxqs is `FAUXQS_DEFAULT_REGION` — it serves as a fallback when the region can't be resolved from request headers. Both default to `us-east-1`.
1277
-
1278
- The main difference is how resources are pre-created. LocalStack uses init hooks (`/etc/localstack/init/ready.d/` shell scripts with `awslocal` CLI calls), while fauxqs uses a declarative JSON config. `awslocal` defaults to `us-east-1` unless you pass `--region`, and fauxqs init config uses `defaultRegion` (`us-east-1`) unless you set an explicit `region` per resource — so both create resources in the same region by default:
1279
-
1280
- ```bash
1281
- # LocalStack init script (ready.d/init.sh)
1282
- # awslocal defaults to us-east-1; use --region to override
1283
- awslocal sqs create-queue --queue-name orders
1284
- awslocal sqs create-queue --queue-name orders-dlq
1285
- awslocal sns create-topic --name events
1286
- awslocal sns subscribe \
1287
- --topic-arn arn:aws:sns:us-east-1:000000000000:events \
1288
- --protocol sqs \
1289
- --notification-endpoint arn:aws:sqs:us-east-1:000000000000:orders
1290
- ```
1291
-
1292
- ```json
1293
- // fauxqs init.json — uses defaultRegion (us-east-1) unless "region" is set per resource
1294
- {
1295
- "queues": [{ "name": "orders" }, { "name": "orders-dlq" }],
1296
- "topics": [{ "name": "events" }],
1297
- "subscriptions": [{ "topic": "events", "queue": "orders" }]
1298
- }
1299
- ```
1300
-
1301
- ```yaml
1302
- # Before (LocalStack docker-compose.yml)
1303
- services:
1304
- localstack:
1305
- image: localstack/localstack
1306
- ports: ["4566:4566"]
1307
- environment:
1308
- - SERVICES=sqs,sns
1309
- volumes:
1310
- - ./ready.d:/etc/localstack/init/ready.d
1311
-
1312
- # After (fauxqs docker-compose.yml)
1313
- services:
1314
- fauxqs:
1315
- image: kibertoad/fauxqs:latest
1316
- ports: ["4566:4566"]
1317
- environment:
1318
- - FAUXQS_INIT=/app/init.json
1319
- volumes:
1320
- - ./init.json:/app/init.json
1321
- ```
1322
-
1323
- Update your docker-compose service name references (e.g., `http://localstack:4566` to `http://fauxqs:4566`) and you're done.
1324
-
1325
- ### SNS/SQS/S3
1326
-
1327
- When S3 is involved, the Docker swap is still straightforward — the only additional consideration is S3 URL style. If you were using `forcePathStyle: true` with LocalStack, it works identically with fauxqs. If you were using LocalStack's `localhost.localstack.cloud` wildcard DNS for virtual-hosted-style, switch to `localhost.fauxqs.dev`:
1328
-
1329
- ```typescript
1330
- // Before (LocalStack)
1331
- const s3 = new S3Client({
1332
- endpoint: "http://s3.localhost.localstack.cloud:4566",
1333
- // ...
1334
- });
1335
-
1336
- // After (fauxqs)
1337
- const s3 = new S3Client({
1338
- endpoint: "http://s3.localhost.fauxqs.dev:4566",
1339
- // ...
1340
- });
1341
- ```
1342
-
1343
- For container-to-container S3 in docker-compose, fauxqs includes a built-in dnsmasq that resolves `*.s3.fauxqs` to the container IP — see [Container-to-container S3 virtual-hosted-style](#container-to-container-s3-virtual-hosted-style).
1344
-
1345
- The init config for S3 is the same declarative JSON, with a `buckets` array:
1346
-
1347
- ```json
1348
- {
1349
- "queues": [{ "name": "orders" }],
1350
- "topics": [{ "name": "events" }],
1351
- "subscriptions": [{ "topic": "events", "queue": "orders" }],
1352
- "buckets": ["uploads", "exports"]
1353
- }
1354
- ```
1355
-
1356
- ### Going hybrid (recommended)
1357
-
1358
- The Docker swap gets you running quickly, but the real win comes from going hybrid: use fauxqs as an **embedded library** in your test suite and keep Docker for local development.
1359
-
1360
- With LocalStack, integration tests typically look like this:
1361
-
1362
- 1. Start LocalStack container (docker-compose or testcontainers) — takes seconds
1363
- 2. Create resources via `awslocal` or SDK calls more seconds
1364
- 3. Run your test logic
1365
- 4. Assert by polling SQS queues, checking S3 objects, etc.
1366
- 5. Clean up resources between tests — often fragile or skipped
1367
-
1368
- With fauxqs in library mode:
1369
-
1370
- 1. `startFauxqs({ port: 0 })` — starts in milliseconds, in-process
1371
- 2. `server.setup({ queues: [...], topics: [...] })` — instant, no network calls
1372
- 3. Run your test logic
1373
- 4. Assert with `server.spy.waitForMessage()` — no polling, no race conditions
1374
- 5. `server.reset()` between tests — clears messages, keeps resources
1375
-
1376
- **What you gain:**
1377
-
1378
- | Concern | LocalStack Docker | fauxqs library mode |
1379
- |---------|-------------------|---------------------|
1380
- | Test startup | Seconds (container boot + resource creation) | Milliseconds (in-process) |
1381
- | CI dependency | Docker required | npm only |
1382
- | Asserting message delivery | Poll SQS queue, hope timing is right | `spy.waitForMessage()` — resolves immediately or waits |
1383
- | Asserting message *not* delivered | `sleep()` + check | `spy.expectNoMessage()` — deterministic negative assertion |
1384
- | Filter policy testing | Receive from queue, check absence manually | `expectNoMessage()` on filtered-out queues |
1385
- | DLQ verification | Receive from DLQ queue via SDK | `spy.waitForMessage({ status: "dlq" })` + `inspectQueue()` |
1386
- | Queue state inspection | `GetQueueAttributes` (counts only) | `inspectQueue()` — see every message, grouped by state |
1387
- | State reset between tests | Restart container or re-create resources | `server.reset()` — instant, preserves resource definitions |
1388
- | Seeding test data | SDK calls through network stack | `server.sendMessage()` / `server.publish()` — direct, no network |
1389
- | S3 event tracking | Check bucket contents via SDK | `spy.waitForMessage({ service: "s3", status: "uploaded" })` |
1390
- | Parallel test files | Port conflicts or shared state | Each file gets its own server on port 0 |
1391
-
1392
- **Migration path:**
1393
-
1394
- 1. Install fauxqs as a dev dependency: `npm install -D fauxqs`
1395
- 2. Create a test helper:
1396
-
1397
- ```typescript
1398
- // test/setup.ts
1399
- import { startFauxqs, type FauxqsServer } from "fauxqs";
1400
-
1401
- export async function createTestServer(): Promise<FauxqsServer> {
1402
- const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
1403
-
1404
- server.setup({
1405
- queues: [{ name: "orders" }, { name: "orders-dlq" }],
1406
- topics: [{ name: "events" }],
1407
- subscriptions: [{ topic: "events", queue: "orders" }],
1408
- buckets: ["uploads"],
1409
- });
1410
-
1411
- return server;
1412
- }
1413
- ```
1414
-
1415
- 3. Replace your LocalStack container setup with the test helper:
1416
-
1417
- ```typescript
1418
- // Before: LocalStack via testcontainers or docker-compose
1419
- let endpoint: string;
1420
- beforeAll(async () => {
1421
- // start container, wait for health, create resources via SDK...
1422
- endpoint = "http://localhost:4566";
1423
- }, 30_000);
1424
-
1425
- // After: fauxqs library
1426
- let server: FauxqsServer;
1427
- beforeAll(async () => {
1428
- server = await createTestServer();
1429
- });
1430
- afterAll(async () => { await server.stop(); });
1431
- beforeEach(() => { server.reset(); });
1432
- ```
1433
-
1434
- 4. Replace polling-based assertions with spy-based ones:
1435
-
1436
- ```typescript
1437
- // Before: poll and hope
1438
- await sqsClient.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: "test" }));
1439
- const result = await sqsClient.send(new ReceiveMessageCommand({ QueueUrl: queueUrl, WaitTimeSeconds: 5 }));
1440
- expect(result.Messages?.[0]?.Body).toBe("test");
1441
-
1442
- // After: spy knows immediately
1443
- await sqsClient.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: "test" }));
1444
- const msg = await server.spy.waitForMessage(
1445
- { service: "sqs", queueName: "orders", status: "published" },
1446
- undefined,
1447
- 2000,
1448
- );
1449
- expect(msg.body).toBe("test");
1450
- ```
1451
-
1452
- 5. Keep your `docker-compose.yml` with the fauxqs image for local development `docker compose up` gives your team a running environment without Node.js installed.
1453
-
1454
- This hybrid setup gives you fast, deterministic tests in CI (no Docker required) and a realistic Docker environment for local development. See the [`examples/recommended/`](examples/recommended/) directory for a complete working example.
1455
-
1456
- ## Benchmarks
1457
-
1458
- 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.
1459
-
1460
- ## License
1461
-
1462
- MIT
1
+ <img src="logo-readme.jpg" alt="fauxqs" width="360" />
2
+
3
+ [![npm version](https://img.shields.io/npm/v/fauxqs.svg)](https://www.npmjs.com/package/fauxqs)
4
+
5
+ # fauxqs
6
+
7
+ 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.
8
+
9
+ All state is in-memory by default. Optional SQLite-based persistence is available via the `dataDir` option.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Installation](#installation)
14
+ - [Usage](#usage)
15
+ - [Running the server](#running-the-server)
16
+ - [Running in the background](#running-in-the-background)
17
+ - [Running with Docker](#running-with-docker)
18
+ - [Running in Docker Compose](#running-in-docker-compose)
19
+ - [Container-to-container S3 virtual-hosted-style](#container-to-container-s3-virtual-hosted-style)
20
+ - [Configuring AWS SDK clients](#configuring-aws-sdk-clients)
21
+ - [Programmatic usage](#programmatic-usage)
22
+ - [Relaxed rules](#relaxed-rules)
23
+ - [Programmatic state setup](#programmatic-state-setup)
24
+ - [Sending messages programmatically](#sending-messages-programmatically)
25
+ - [Init config file](#init-config-file)
26
+ - [Init config schema reference](#init-config-schema-reference)
27
+ - [Message spy](#message-spy)
28
+ - [Queue inspection](#queue-inspection)
29
+ - [Persistence](#persistence)
30
+ - [Configurable queue URL host](#configurable-queue-url-host)
31
+ - [Region](#region)
32
+ - [Supported API Actions](#supported-api-actions)
33
+ - [SQS](#sqs)
34
+ - [SNS](#sns)
35
+ - [S3](#s3)
36
+ - [STS](#sts)
37
+ - [SQS Features](#sqs-features)
38
+ - [SNS Features](#sns-features)
39
+ - [S3 Features](#s3-features)
40
+ - [S3 URL styles](#s3-url-styles)
41
+ - [Using with AWS CLI](#using-with-aws-cli)
42
+ - [Testing Strategies](#testing-strategies)
43
+ - [Library mode for tests](#library-mode-for-tests)
44
+ - [Docker mode for local development](#docker-mode-for-local-development)
45
+ - [Recommended combination](#recommended-combination)
46
+ - [Conventions](#conventions)
47
+ - [Limitations](#limitations)
48
+ - [Examples](#examples)
49
+ - [Migrating from LocalStack](#migrating-from-localstack)
50
+ - [SNS/SQS only](#snssqs-only)
51
+ - [SNS/SQS/S3](#snssqss3)
52
+ - [Going hybrid (recommended)](#going-hybrid-recommended)
53
+ - [Benchmarks](#benchmarks)
54
+ - [License](#license)
55
+
56
+ ## Installation
57
+
58
+ **Docker** (recommended for standalone usage) — [Docker Hub](https://hub.docker.com/r/kibertoad/fauxqs):
59
+
60
+ ```bash
61
+ docker run -p 4566:4566 kibertoad/fauxqs
62
+ ```
63
+
64
+ **npm** (for embedded library usage or CLI):
65
+
66
+ ```bash
67
+ npm install fauxqs
68
+ ```
69
+
70
+ > **Node.js compatibility:** fauxqs requires Node.js 22.5+ (for `node:sqlite`) when used as a library. If your project is on Node.js 20, you can either use `fauxqs@1.13.0` (last version with Node.js 20 support) or run the latest fauxqs as a [Docker container](#running-with-docker).
71
+
72
+ ## Usage
73
+
74
+ ### Running the server
75
+
76
+ ```bash
77
+ npx fauxqs
78
+ ```
79
+
80
+ The server starts on port `4566` and handles SQS, SNS, and S3 on a single endpoint.
81
+
82
+ #### Environment variables
83
+
84
+ | Variable | Description | Default |
85
+ |----------|-------------|---------|
86
+ | `FAUXQS_PORT` | Port to listen on | `4566` |
87
+ | `FAUXQS_HOST` | Host for queue URLs (`sqs.<region>.<host>` format) | `localhost` |
88
+ | `FAUXQS_DEFAULT_REGION` | Fallback region for ARNs and URLs | `us-east-1` |
89
+ | `FAUXQS_LOGGER` | Enable request logging (`true`/`false`) | `true` |
90
+ | `FAUXQS_INIT` | Path to a JSON init config file (see [Init config file](#init-config-file)) | (none) |
91
+ | `FAUXQS_DATA_DIR` | Directory for SQLite persistence (see [Persistence](#persistence)). Omit to keep all state in-memory. | (none) |
92
+ | `FAUXQS_PERSISTENCE` | Set to `true` to enable persistence when `FAUXQS_DATA_DIR` is set | `false` |
93
+ | `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 |
94
+ | `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` |
95
+
96
+ ```bash
97
+ FAUXQS_PORT=3000 FAUXQS_INIT=init.json npx fauxqs
98
+ ```
99
+
100
+ A health check is available at `GET /health`.
101
+
102
+ ### Running in the background
103
+
104
+ To keep fauxqs running while you work on your app or run tests repeatedly, start it as a background process:
105
+
106
+ ```bash
107
+ npx fauxqs &
108
+ ```
109
+
110
+ Or in a separate terminal:
111
+
112
+ ```bash
113
+ npx fauxqs
114
+ ```
115
+
116
+ All state accumulates in memory across requests, so queues, topics, and objects persist until the server is stopped.
117
+
118
+ To stop the server:
119
+
120
+ ```bash
121
+ # If backgrounded in the same shell
122
+ kill %1
123
+
124
+ # Cross-platform, by port
125
+ npx cross-port-killer 4566
126
+ ```
127
+
128
+ ### Running with Docker
129
+
130
+ The official Docker image is available on Docker Hub:
131
+
132
+ ```bash
133
+ docker run -p 4566:4566 kibertoad/fauxqs
134
+ ```
135
+
136
+ To persist state across container restarts, mount a volume at `/data` and set `FAUXQS_PERSISTENCE=true`:
137
+
138
+ ```bash
139
+ docker run -p 4566:4566 -v fauxqs-data:/data -e FAUXQS_PERSISTENCE=true kibertoad/fauxqs
140
+ ```
141
+
142
+ With an init config file:
143
+
144
+ ```bash
145
+ docker run -p 4566:4566 \
146
+ -v fauxqs-data:/data \
147
+ -v ./init.json:/app/init.json \
148
+ -e FAUXQS_INIT=/app/init.json \
149
+ kibertoad/fauxqs
150
+ ```
151
+
152
+ ### Running in Docker Compose
153
+
154
+ Use the `kibertoad/fauxqs` image and mount a JSON init config to pre-create resources on startup:
155
+
156
+ ```json
157
+ // scripts/fauxqs/init.json
158
+ {
159
+ "queues": [
160
+ {
161
+ "name": "my-queue.fifo",
162
+ "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
163
+ },
164
+ { "name": "my-dlq" }
165
+ ],
166
+ "topics": [{ "name": "my-events" }],
167
+ "subscriptions": [{ "topic": "my-events", "queue": "my-dlq" }],
168
+ "buckets": ["my-uploads"]
169
+ }
170
+ ```
171
+
172
+ ```yaml
173
+ # docker-compose.yml
174
+ services:
175
+ fauxqs:
176
+ image: kibertoad/fauxqs:latest
177
+ ports:
178
+ - "4566:4566"
179
+ environment:
180
+ - FAUXQS_INIT=/app/init.json
181
+ volumes:
182
+ - ./scripts/fauxqs/init.json:/app/init.json
183
+ - fauxqs-data:/data
184
+
185
+ app:
186
+ # ...
187
+ depends_on:
188
+ fauxqs:
189
+ condition: service_healthy
190
+
191
+ volumes:
192
+ fauxqs-data:
193
+ ```
194
+
195
+ 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. The `fauxqs-data` volume persists state across `docker compose down` / `up` cycles — queues, messages, objects, and all other state are restored on startup. Init config is idempotent, so re-applying it after a restart skips resources that already exist.
196
+
197
+ #### Container-to-container S3 virtual-hosted-style
198
+
199
+ 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`.
200
+
201
+ To use it, assign fauxqs a static IP and point other containers' DNS to it:
202
+
203
+ ```yaml
204
+ # docker-compose.yml
205
+ services:
206
+ fauxqs:
207
+ image: kibertoad/fauxqs:latest
208
+ networks:
209
+ default:
210
+ ipv4_address: 10.0.0.2
211
+ ports:
212
+ - "4566:4566"
213
+ environment:
214
+ - FAUXQS_INIT=/app/init.json
215
+ - FAUXQS_HOST=fauxqs
216
+ volumes:
217
+ - ./scripts/fauxqs/init.json:/app/init.json
218
+ - fauxqs-data:/data
219
+
220
+ app:
221
+ dns: 10.0.0.2
222
+ depends_on:
223
+ fauxqs:
224
+ condition: service_healthy
225
+ environment:
226
+ - AWS_ENDPOINT=http://s3.fauxqs:4566
227
+
228
+ volumes:
229
+ fauxqs-data:
230
+
231
+ networks:
232
+ default:
233
+ ipam:
234
+ config:
235
+ - subnet: 10.0.0.0/24
236
+ ```
237
+
238
+ From the `app` container, `my-bucket.s3.fauxqs` resolves to `10.0.0.2` (the fauxqs container), so virtual-hosted-style S3 works:
239
+
240
+ ```typescript
241
+ const s3 = new S3Client({
242
+ endpoint: "http://s3.fauxqs:4566",
243
+ region: "us-east-1",
244
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
245
+ // No forcePathStyle needed!
246
+ });
247
+ ```
248
+
249
+ 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.
250
+
251
+ ### Configuring AWS SDK clients
252
+
253
+ Point your SDK clients at the local server:
254
+
255
+ ```typescript
256
+ import { SQSClient } from "@aws-sdk/client-sqs";
257
+ import { SNSClient } from "@aws-sdk/client-sns";
258
+ import { S3Client } from "@aws-sdk/client-s3";
259
+
260
+ const sqsClient = new SQSClient({
261
+ endpoint: "http://localhost:4566",
262
+ region: "us-east-1",
263
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
264
+ });
265
+
266
+ const snsClient = new SNSClient({
267
+ endpoint: "http://localhost:4566",
268
+ region: "us-east-1",
269
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
270
+ });
271
+
272
+ // Using fauxqs.dev wildcard DNS — no helpers or forcePathStyle needed
273
+ const s3Client = new S3Client({
274
+ endpoint: "http://s3.localhost.fauxqs.dev:4566",
275
+ region: "us-east-1",
276
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
277
+ });
278
+ ```
279
+
280
+ Any credentials are accepted and never validated.
281
+
282
+ > **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.
283
+
284
+ ### Programmatic usage
285
+
286
+ You can also embed fauxqs directly in your test suite:
287
+
288
+ ```typescript
289
+ import { startFauxqs } from "fauxqs";
290
+
291
+ const server = await startFauxqs({ port: 4566, logger: false });
292
+
293
+ console.log(server.address); // "http://127.0.0.1:4566"
294
+ console.log(server.port); // 4566
295
+
296
+ // point your SDK clients at server.address
297
+
298
+ // clean up when done
299
+ await server.stop();
300
+ ```
301
+
302
+ Pass `port: 0` to let the OS assign a random available port (useful in tests).
303
+
304
+ #### Relaxed rules
305
+
306
+ By default, fauxqs enforces AWS-strict validations. You can selectively relax some of these for convenience during development:
307
+
308
+ ```typescript
309
+ const server = await startFauxqs({
310
+ port: 0,
311
+ relaxedRules: {
312
+ disableMinCopySourceSize: true,
313
+ },
314
+ });
315
+ ```
316
+
317
+ | Rule | Default | Description |
318
+ |------|---------|-------------|
319
+ | `disableMinCopySourceSize` | `false` | AWS requires the source object to be [larger than 5 MiB](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html) for byte-range `UploadPartCopy`. Set to `true` to allow byte-range copies from smaller sources. |
320
+
321
+ #### Programmatic state setup
322
+
323
+ The server object exposes methods for pre-creating resources without going through the SDK:
324
+
325
+ ```typescript
326
+ const server = await startFauxqs({ port: 0, logger: false });
327
+
328
+ // Create individual resources each returns metadata about the created resource
329
+ const { queueUrl, queueArn, queueName } = server.createQueue("my-queue");
330
+ const { queueUrl: dlqUrl } = server.createQueue("my-dlq", {
331
+ attributes: { VisibilityTimeout: "60" },
332
+ tags: { env: "test" },
333
+ });
334
+ const { topicArn } = server.createTopic("my-topic");
335
+ server.subscribe({ topic: "my-topic", queue: "my-queue" });
336
+ const { bucketName } = server.createBucket("my-bucket");
337
+
338
+ // Create resources in a specific region
339
+ server.createQueue("eu-queue", { region: "eu-west-1" });
340
+ server.createTopic("eu-topic", { region: "eu-west-1" });
341
+ server.subscribe({ topic: "eu-topic", queue: "eu-queue", region: "eu-west-1" });
342
+
343
+ // Or create everything at once
344
+ server.setup({
345
+ queues: [
346
+ { name: "orders" },
347
+ { name: "notifications", attributes: { DelaySeconds: "5" } },
348
+ { name: "eu-orders", region: "eu-west-1" },
349
+ ],
350
+ topics: [{ name: "events" }],
351
+ subscriptions: [
352
+ { topic: "events", queue: "orders" },
353
+ { topic: "events", queue: "notifications" },
354
+ ],
355
+ buckets: ["uploads", "exports"],
356
+ });
357
+
358
+ // Delete individual resources (uses defaultRegion; pass { region } to override)
359
+ server.deleteQueue("my-queue"); // no-op if queue doesn't exist
360
+ server.deleteQueue("eu-queue", { region: "eu-west-1" }); // delete in specific region
361
+ server.deleteTopic("my-topic"); // also removes associated subscriptions
362
+ server.deleteTopic("eu-topic", { region: "eu-west-1" });
363
+ server.emptyBucket("my-bucket"); // removes all objects, keeps the bucket
364
+
365
+ // Clear all messages and S3 objects between tests (keeps queues, topics, subscriptions, buckets)
366
+ server.reset();
367
+
368
+ // Or nuke everything — removes queues, topics, subscriptions, and buckets too
369
+ server.purgeAll();
370
+ ```
371
+
372
+ #### Sending messages programmatically
373
+
374
+ Send SQS messages and publish to SNS topics without instantiating SDK clients:
375
+
376
+ ```typescript
377
+ // SQS: enqueue a message directly
378
+ const { messageId, md5OfBody } = server.sendMessage("my-queue", "hello world");
379
+
380
+ // With message attributes
381
+ server.sendMessage("my-queue", JSON.stringify({ orderId: "123" }), {
382
+ messageAttributes: {
383
+ eventType: { DataType: "String", StringValue: "order.created" },
384
+ },
385
+ });
386
+
387
+ // With delay
388
+ server.sendMessage("my-queue", "delayed message", { delaySeconds: 10 });
389
+
390
+ // FIFO queue — returns sequenceNumber
391
+ const { sequenceNumber } = server.sendMessage("my-queue.fifo", "fifo message", {
392
+ messageGroupId: "group-1",
393
+ messageDeduplicationId: "dedup-1",
394
+ });
395
+
396
+ // SNS: publish to a topic (fans out to all SQS subscriptions)
397
+ const { messageId: snsMessageId } = server.publish("my-topic", "event payload");
398
+
399
+ // With subject and message attributes
400
+ server.publish("my-topic", JSON.stringify({ orderId: "456" }), {
401
+ subject: "Order Update",
402
+ messageAttributes: {
403
+ eventType: { DataType: "String", StringValue: "order.updated" },
404
+ },
405
+ });
406
+
407
+ // FIFO topic
408
+ server.publish("my-topic.fifo", "fifo event", {
409
+ messageGroupId: "group-1",
410
+ messageDeduplicationId: "dedup-1",
411
+ });
412
+ ```
413
+
414
+ `sendMessage` validates the message body (invalid characters, size limits), applies queue-level `DelaySeconds` defaults, handles FIFO deduplication, and emits spy events automatically. Returns `{ messageId, md5OfBody, md5OfMessageAttributes?, sequenceNumber? }`.
415
+
416
+ `publish` validates message size, evaluates filter policies on each subscription, supports raw message delivery, and emits spy events. Returns `{ messageId }`.
417
+
418
+ Both methods throw if the target queue or topic doesn't exist.
419
+
420
+ #### Init config file
421
+
422
+ 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.
423
+
424
+ ```json
425
+ {
426
+ "queues": [
427
+ { "name": "orders" },
428
+ { "name": "orders-dlq" },
429
+ { "name": "orders.fifo", "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" } }
430
+ ],
431
+ "topics": [
432
+ { "name": "events" }
433
+ ],
434
+ "subscriptions": [
435
+ { "topic": "events", "queue": "orders" }
436
+ ],
437
+ "buckets": ["uploads", "exports"]
438
+ }
439
+ ```
440
+
441
+ Pass it via the `FAUXQS_INIT` environment variable or the `init` option:
442
+
443
+ ```bash
444
+ FAUXQS_INIT=init.json npx fauxqs
445
+ ```
446
+
447
+ ```typescript
448
+ const server = await startFauxqs({ init: "init.json" });
449
+ // or inline:
450
+ const server = await startFauxqs({
451
+ init: { queues: [{ name: "my-queue" }], buckets: ["my-bucket"] },
452
+ });
453
+ ```
454
+
455
+ #### Init config schema reference
456
+
457
+ All top-level fields are optional. Resources are created in dependency order: queues, topics, subscriptions, buckets.
458
+
459
+ ##### `queues`
460
+
461
+ Array of queue objects.
462
+
463
+ | Field | Type | Required | Description |
464
+ |-------|------|----------|-------------|
465
+ | `name` | `string` | Yes | Queue name. Use `.fifo` suffix for FIFO queues. |
466
+ | `region` | `string` | No | Override the default region for this queue. The queue's ARN and URL will use this region. |
467
+ | `attributes` | `Record<string, string>` | No | Queue attributes (see table below). |
468
+ | `tags` | `Record<string, string>` | No | Key-value tags for the queue. |
469
+
470
+ Supported queue attributes:
471
+
472
+ | Attribute | Default | Range / Values |
473
+ |-----------|---------|----------------|
474
+ | `VisibilityTimeout` | `"30"` | `0` – `43200` (seconds) |
475
+ | `DelaySeconds` | `"0"` | `0` – `900` (seconds) |
476
+ | `MaximumMessageSize` | `"1048576"` | `1024` – `1048576` (bytes) |
477
+ | `MessageRetentionPeriod` | `"345600"` | `60` – `1209600` (seconds) |
478
+ | `ReceiveMessageWaitTimeSeconds` | `"0"` | `0` – `20` (seconds) |
479
+ | `RedrivePolicy` | — | JSON string: `{"deadLetterTargetArn": "arn:...", "maxReceiveCount": "5"}` |
480
+ | `Policy` | — | Queue policy JSON string (stored, not enforced) |
481
+ | `KmsMasterKeyId` | — | KMS key ID (stored, no actual encryption) |
482
+ | `KmsDataKeyReusePeriodSeconds` | — | KMS data key reuse period (stored, no actual encryption) |
483
+ | `FifoQueue` | — | `"true"` for FIFO queues (queue name must end with `.fifo`) |
484
+ | `ContentBasedDeduplication` | — | `"true"` or `"false"` (FIFO queues only) |
485
+
486
+ Example:
487
+
488
+ ```json
489
+ {
490
+ "queues": [
491
+ {
492
+ "name": "orders",
493
+ "attributes": { "VisibilityTimeout": "60", "DelaySeconds": "5" },
494
+ "tags": { "env": "staging", "team": "platform" }
495
+ },
496
+ {
497
+ "name": "orders-dlq"
498
+ },
499
+ {
500
+ "name": "orders.fifo",
501
+ "attributes": {
502
+ "FifoQueue": "true",
503
+ "ContentBasedDeduplication": "true"
504
+ }
505
+ },
506
+ {
507
+ "name": "retry-queue",
508
+ "attributes": {
509
+ "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:000000000000:orders-dlq\",\"maxReceiveCount\":\"3\"}"
510
+ }
511
+ },
512
+ {
513
+ "name": "eu-orders",
514
+ "region": "eu-west-1"
515
+ }
516
+ ]
517
+ }
518
+ ```
519
+
520
+ ##### `topics`
521
+
522
+ Array of topic objects.
523
+
524
+ | Field | Type | Required | Description |
525
+ |-------|------|----------|-------------|
526
+ | `name` | `string` | Yes | Topic name. Use `.fifo` suffix for FIFO topics. |
527
+ | `region` | `string` | No | Override the default region for this topic. The topic's ARN will use this region. |
528
+ | `attributes` | `Record<string, string>` | No | Topic attributes (e.g., `DisplayName`). |
529
+ | `tags` | `Record<string, string>` | No | Key-value tags for the topic. |
530
+
531
+ Example:
532
+
533
+ ```json
534
+ {
535
+ "topics": [
536
+ {
537
+ "name": "events",
538
+ "attributes": { "DisplayName": "Application Events" },
539
+ "tags": { "env": "staging" }
540
+ },
541
+ {
542
+ "name": "events.fifo",
543
+ "attributes": { "FifoQueue": "true", "ContentBasedDeduplication": "true" }
544
+ }
545
+ ]
546
+ }
547
+ ```
548
+
549
+ ##### `subscriptions`
550
+
551
+ Array of subscription objects. Referenced topics and queues must be defined in the same config (or already exist on the server).
552
+
553
+ | Field | Type | Required | Description |
554
+ |-------|------|----------|-------------|
555
+ | `topic` | `string` | Yes | Topic name (not ARN) to subscribe to. |
556
+ | `queue` | `string` | Yes | Queue name (not ARN) to deliver messages to. |
557
+ | `region` | `string` | No | Override the default region. The topic and queue ARNs will be resolved in this region. |
558
+ | `attributes` | `Record<string, string>` | No | Subscription attributes (see table below). |
559
+
560
+ Supported subscription attributes:
561
+
562
+ | Attribute | Values | Description |
563
+ |-----------|--------|-------------|
564
+ | `RawMessageDelivery` | `"true"` / `"false"` | Deliver the raw message body instead of the SNS envelope JSON. |
565
+ | `FilterPolicy` | JSON string | SNS filter policy for message filtering (e.g., `"{\"color\": [\"blue\"]}"`) |
566
+ | `FilterPolicyScope` | `"MessageAttributes"` / `"MessageBody"` | Whether the filter policy applies to message attributes or body. Defaults to `MessageAttributes`. |
567
+ | `RedrivePolicy` | JSON string | Subscription-level dead-letter queue config. |
568
+ | `DeliveryPolicy` | JSON string | Delivery retry policy (stored, not enforced). |
569
+ | `SubscriptionRoleArn` | ARN string | IAM role ARN for delivery (stored, not enforced). |
570
+
571
+ Example:
572
+
573
+ ```json
574
+ {
575
+ "subscriptions": [
576
+ {
577
+ "topic": "events",
578
+ "queue": "orders",
579
+ "attributes": {
580
+ "RawMessageDelivery": "true",
581
+ "FilterPolicy": "{\"eventType\": [\"order.created\", \"order.updated\"]}"
582
+ }
583
+ },
584
+ {
585
+ "topic": "events",
586
+ "queue": "notifications"
587
+ }
588
+ ]
589
+ }
590
+ ```
591
+
592
+ ##### `buckets`
593
+
594
+ Array of bucket name strings or objects. Use the object form to create directory buckets (S3 Express One Zone).
595
+
596
+ ```json
597
+ {
598
+ "buckets": [
599
+ "uploads",
600
+ "exports",
601
+ { "name": "my-directory-bucket", "type": "directory" }
602
+ ]
603
+ }
604
+ ```
605
+
606
+ | Field | Type | Required | Description |
607
+ |-------|------|----------|-------------|
608
+ | `name` | `string` | Yes | Bucket name. |
609
+ | `type` | `"general-purpose"` \| `"directory"` | No | Bucket type. Defaults to `"general-purpose"`. Directory buckets support `RenameObject`. |
610
+
611
+ #### Message spy
612
+
613
+ `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`.
614
+
615
+ Enable it with the `messageSpies` option:
616
+
617
+ ```typescript
618
+ const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
619
+ ```
620
+
621
+ The spy tracks events across all three services using a discriminated union on `service`:
622
+
623
+ **SQS events** (`service: 'sqs'`):
624
+ - **`published`** message was enqueued (via SendMessage, SendMessageBatch, or SNS fan-out)
625
+ - **`consumed`** — message was deleted (via DeleteMessage / DeleteMessageBatch)
626
+ - **`dlq`** message exceeded `maxReceiveCount` and was moved to a dead-letter queue
627
+
628
+ **SNS events** (`service: 'sns'`):
629
+ - **`published`** — message was published to a topic (before fan-out to SQS subscriptions)
630
+
631
+ **S3 events** (`service: 's3'`):
632
+ - **`uploaded`** — object was put (PutObject or CompleteMultipartUpload)
633
+ - **`downloaded`** — object was retrieved (GetObject)
634
+ - **`deleted`** object was deleted (DeleteObject, only when key existed)
635
+ - **`copied`** — object was copied (CopyObject; also emits `uploaded` for the destination)
636
+ - **`renamed`** — object was renamed (RenameObject, directory buckets only)
637
+
638
+ ##### Awaiting messages
639
+
640
+ ```typescript
641
+ // Wait for a specific SQS message (resolves immediately if already in buffer)
642
+ const msg = await server.spy.waitForMessage(
643
+ (m) => m.service === "sqs" && m.body === "order.created" && m.queueName === "orders",
644
+ "published",
645
+ );
646
+
647
+ // Wait by SQS message ID
648
+ const msg = await server.spy.waitForMessageWithId(messageId, "consumed");
649
+
650
+ // Partial object match (deep-equal on specified fields)
651
+ const msg = await server.spy.waitForMessage({ service: "sqs", queueName: "orders", status: "published" });
652
+
653
+ // Wait for an SNS publish event
654
+ const msg = await server.spy.waitForMessage({ service: "sns", topicName: "my-topic", status: "published" });
655
+
656
+ // Wait for an S3 upload event
657
+ const msg = await server.spy.waitForMessage({ service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" });
658
+ ```
659
+
660
+ `waitForMessage` checks the buffer first (retroactive resolution). If no match is found, it returns a Promise that resolves when a matching message arrives.
661
+
662
+ ##### Timeout
663
+
664
+ 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:
665
+
666
+ ```typescript
667
+ // Reject after 2 seconds if no match
668
+ const msg = await server.spy.waitForMessage(
669
+ { service: "sqs", queueName: "orders" },
670
+ "published",
671
+ 2000,
672
+ );
673
+
674
+ // Also works with waitForMessageWithId
675
+ const msg = await server.spy.waitForMessageWithId(messageId, "consumed", 5000);
676
+ ```
677
+
678
+ ##### Waiting for multiple messages
679
+
680
+ `waitForMessages` collects `count` matching messages before resolving. It checks the buffer first, then awaits future arrivals:
681
+
682
+ ```typescript
683
+ // Wait for 3 messages on the orders queue
684
+ const msgs = await server.spy.waitForMessages(
685
+ { service: "sqs", queueName: "orders" },
686
+ { count: 3, status: "published", timeout: 5000 },
687
+ );
688
+ // msgs.length === 3
689
+ ```
690
+
691
+ If the timeout expires before enough messages arrive, the promise rejects with a message showing how many were collected (e.g., `"collected 1/3"`).
692
+
693
+ ##### Negative assertions
694
+
695
+ `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:
696
+
697
+ ```typescript
698
+ // Assert no message was delivered to the wrong queue (waits 200ms by default)
699
+ await server.spy.expectNoMessage({ service: "sqs", queueName: "wrong-queue" });
700
+
701
+ // Custom window and status filter
702
+ await server.spy.expectNoMessage(
703
+ { service: "sqs", queueName: "orders" },
704
+ { status: "dlq", within: 500 },
705
+ );
706
+ ```
707
+
708
+ 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"`.
709
+
710
+ ##### Synchronous check
711
+
712
+ ```typescript
713
+ const msg = server.spy.checkForMessage(
714
+ (m) => m.service === "sqs" && m.queueName === "my-queue",
715
+ "published",
716
+ );
717
+ // returns SpyMessage | undefined
718
+ ```
719
+
720
+ ##### Buffer management
721
+
722
+ ```typescript
723
+ // Get all tracked messages (oldest to newest)
724
+ const all = server.spy.getAllMessages();
725
+
726
+ // Clear buffer and reject pending waiters
727
+ server.spy.clear();
728
+ ```
729
+
730
+ The buffer defaults to 100 messages (FIFO eviction). Configure with:
731
+
732
+ ```typescript
733
+ const server = await startFauxqs({
734
+ messageSpies: { bufferSize: 500 },
735
+ });
736
+ ```
737
+
738
+ ##### Types
739
+
740
+ `server.spy` returns a `MessageSpyReader` — a read-only interface that exposes query and await methods but not internal mutation (e.g. recording new events):
741
+
742
+ ```typescript
743
+ interface MessageSpyReader {
744
+ waitForMessage(filter: MessageSpyFilter, status?: string, timeout?: number): Promise<SpyMessage>;
745
+ waitForMessageWithId(messageId: string, status?: string, timeout?: number): Promise<SpyMessage>;
746
+ waitForMessages(filter: MessageSpyFilter, options: WaitForMessagesOptions): Promise<SpyMessage[]>;
747
+ expectNoMessage(filter: MessageSpyFilter, options?: ExpectNoMessageOptions): Promise<void>;
748
+ checkForMessage(filter: MessageSpyFilter, status?: string): SpyMessage | undefined;
749
+ getAllMessages(): SpyMessage[];
750
+ clear(): void;
751
+ }
752
+
753
+ interface WaitForMessagesOptions {
754
+ count: number;
755
+ status?: string;
756
+ timeout?: number;
757
+ }
758
+
759
+ interface ExpectNoMessageOptions {
760
+ status?: string;
761
+ within?: number; // ms, defaults to 200
762
+ }
763
+ ```
764
+
765
+ `SpyMessage` is a discriminated union:
766
+
767
+ ```typescript
768
+ interface SqsSpyMessage {
769
+ service: "sqs";
770
+ queueName: string;
771
+ messageId: string;
772
+ body: string;
773
+ messageAttributes: Record<string, MessageAttributeValue>;
774
+ status: "published" | "consumed" | "dlq";
775
+ timestamp: number;
776
+ }
777
+
778
+ interface SnsSpyMessage {
779
+ service: "sns";
780
+ topicArn: string;
781
+ topicName: string;
782
+ messageId: string;
783
+ body: string;
784
+ messageAttributes: Record<string, MessageAttributeValue>;
785
+ status: "published";
786
+ timestamp: number;
787
+ }
788
+
789
+ interface S3SpyEvent {
790
+ service: "s3";
791
+ bucket: string;
792
+ key: string;
793
+ status: "uploaded" | "downloaded" | "deleted" | "copied" | "renamed";
794
+ timestamp: number;
795
+ }
796
+
797
+ type SpyMessage = SqsSpyMessage | SnsSpyMessage | S3SpyEvent;
798
+ ```
799
+
800
+ ##### Spy disabled by default
801
+
802
+ Accessing `server.spy` when `messageSpies` is not set throws an error. There is no overhead on the message flow when spies are disabled.
803
+
804
+ #### Queue inspection
805
+
806
+ Non-destructive inspection of SQS queue state — see all messages (ready, in-flight, and delayed) without consuming them or affecting visibility timeouts.
807
+
808
+ ##### Programmatic API
809
+
810
+ ```typescript
811
+ const result = server.inspectQueue("my-queue");
812
+ // result is undefined if queue doesn't exist
813
+ if (result) {
814
+ console.log(result.name); // "my-queue"
815
+ console.log(result.url); // "http://sqs.us-east-1.localhost:4566/000000000000/my-queue"
816
+ console.log(result.arn); // "arn:aws:sqs:us-east-1:000000000000:my-queue"
817
+ console.log(result.attributes); // { VisibilityTimeout: "30", ... }
818
+ console.log(result.messages.ready); // messages available for receive
819
+ console.log(result.messages.delayed); // messages waiting for delay to expire
820
+ console.log(result.messages.inflight); // received but not yet deleted
821
+ // Each inflight entry includes: { message, receiptHandle, visibilityDeadline }
822
+ }
823
+ ```
824
+
825
+ ##### HTTP endpoints
826
+
827
+ ```bash
828
+ # List all queues with summary counts
829
+ curl http://localhost:4566/_fauxqs/queues
830
+ # [{ "name": "my-queue", "approximateMessageCount": 5, "approximateInflightCount": 2, "approximateDelayedCount": 0, ... }]
831
+
832
+ # Inspect a specific queue (full state)
833
+ curl http://localhost:4566/_fauxqs/queues/my-queue
834
+ # { "name": "my-queue", "messages": { "ready": [...], "delayed": [...], "inflight": [...] }, ... }
835
+ ```
836
+
837
+ Returns 404 for non-existent queues. Inspection never modifies queue state — messages remain exactly where they are.
838
+
839
+ ### Persistence
840
+
841
+ By default (when using `npx fauxqs` or the programmatic API without `dataDir`), all state is in-memory and lost when the server stops. To persist state across restarts, set a `dataDir` — fauxqs will store all queues, messages, topics, subscriptions, buckets, and objects in a SQLite database inside that directory.
842
+
843
+ **CLI:**
844
+
845
+ ```bash
846
+ FAUXQS_DATA_DIR=./data npx fauxqs
847
+ ```
848
+
849
+ **Docker:**
850
+
851
+ The Docker image has `FAUXQS_DATA_DIR=/data` preset. Mount a volume and set `FAUXQS_PERSISTENCE=true` to enable persistence:
852
+
853
+ ```bash
854
+ docker run -p 4566:4566 -v fauxqs-data:/data -e FAUXQS_PERSISTENCE=true kibertoad/fauxqs
855
+ ```
856
+
857
+ Without `FAUXQS_PERSISTENCE=true`, the server runs in-memory even if a volume is mounted. If no volume is mounted at `/data`, persistence is silently disabled regardless of the env var (no unnecessary writes to ephemeral container storage).
858
+
859
+ **Programmatic:**
860
+
861
+ ```typescript
862
+ const server = await startFauxqs({ port: 4566, dataDir: "./data" });
863
+ ```
864
+
865
+ All mutations are written through to SQLite immediately (no batching or delayed flush). On restart with the same `dataDir`, the server restores all state including:
866
+
867
+ - SQS queues with attributes, tags, and messages (ready, delayed, and inflight with their visibility deadlines)
868
+ - FIFO sequence counters (no duplicate sequence numbers after restart)
869
+ - SNS topics with attributes and tags, subscriptions with attributes (fan-out works immediately after restart)
870
+ - S3 buckets (including directory bucket type), objects with metadata, and in-progress multipart uploads
871
+
872
+ `reset()` and `purgeAll()` also write through to the database — `reset()` clears messages and objects, `purgeAll()` clears everything.
873
+
874
+ ### Configurable queue URL host
875
+
876
+ 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`.
877
+
878
+ To override the host (e.g., for a custom domain):
879
+
880
+ ```typescript
881
+ import { startFauxqs } from "fauxqs";
882
+
883
+ const server = await startFauxqs({ port: 4566, host: "myhost.local" });
884
+ // Queue URLs: http://sqs.us-east-1.myhost.local:4566/000000000000/myQueue
885
+ ```
886
+
887
+ This also works with `buildApp`:
888
+
889
+ ```typescript
890
+ import { buildApp } from "fauxqs";
891
+
892
+ const app = buildApp({ host: "myhost.local" });
893
+ ```
894
+
895
+ 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.
896
+
897
+ ### Region
898
+
899
+ 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.
900
+
901
+ 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.
902
+
903
+ ```typescript
904
+ const sqsEU = new SQSClient({ region: "eu-west-1", endpoint: "http://localhost:4566", ... });
905
+ const sqsUS = new SQSClient({ region: "us-east-1", endpoint: "http://localhost:4566", ... });
906
+
907
+ // These are two independent queues with different ARNs
908
+ await sqsEU.send(new CreateQueueCommand({ QueueName: "orders" }));
909
+ await sqsUS.send(new CreateQueueCommand({ QueueName: "orders" }));
910
+ ```
911
+
912
+ 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"`):
913
+
914
+ ```typescript
915
+ const server = await startFauxqs({ defaultRegion: "eu-west-1" });
916
+ ```
917
+
918
+ Resources created via init config or programmatic API use the `defaultRegion` unless overridden with an explicit `region` field:
919
+
920
+ ```json
921
+ {
922
+ "queues": [
923
+ { "name": "us-queue" },
924
+ { "name": "eu-queue", "region": "eu-west-1" }
925
+ ]
926
+ }
927
+ ```
928
+
929
+ ## Supported API Actions
930
+
931
+ ### SQS
932
+
933
+ | Action | Supported |
934
+ |--------|-----------|
935
+ | CreateQueue | Yes |
936
+ | DeleteQueue | Yes |
937
+ | GetQueueUrl | Yes |
938
+ | ListQueues | Yes |
939
+ | GetQueueAttributes | Yes |
940
+ | SetQueueAttributes | Yes |
941
+ | PurgeQueue | Yes |
942
+ | SendMessage | Yes |
943
+ | SendMessageBatch | Yes |
944
+ | ReceiveMessage | Yes |
945
+ | DeleteMessage | Yes |
946
+ | DeleteMessageBatch | Yes |
947
+ | ChangeMessageVisibility | Yes |
948
+ | ChangeMessageVisibilityBatch | Yes |
949
+ | TagQueue | Yes |
950
+ | UntagQueue | Yes |
951
+ | ListQueueTags | Yes |
952
+ | AddPermission | No |
953
+ | RemovePermission | No |
954
+ | ListDeadLetterSourceQueues | No |
955
+ | StartMessageMoveTask | No |
956
+ | CancelMessageMoveTask | No |
957
+ | ListMessageMoveTasks | No |
958
+
959
+ ### SNS
960
+
961
+ | Action | Supported |
962
+ |--------|-----------|
963
+ | CreateTopic | Yes |
964
+ | DeleteTopic | Yes |
965
+ | ListTopics | Yes |
966
+ | GetTopicAttributes | Yes |
967
+ | SetTopicAttributes | Yes |
968
+ | Subscribe | Yes |
969
+ | Unsubscribe | Yes |
970
+ | ConfirmSubscription | Yes |
971
+ | ListSubscriptions | Yes |
972
+ | ListSubscriptionsByTopic | Yes |
973
+ | GetSubscriptionAttributes | Yes |
974
+ | SetSubscriptionAttributes | Yes |
975
+ | Publish | Yes |
976
+ | PublishBatch | Yes |
977
+ | TagResource | Yes |
978
+ | UntagResource | Yes |
979
+ | ListTagsForResource | Yes |
980
+ | AddPermission | No |
981
+ | RemovePermission | No |
982
+ | GetDataProtectionPolicy | No |
983
+ | PutDataProtectionPolicy | No |
984
+
985
+ Platform application, SMS, and phone number actions are not supported.
986
+
987
+ ### S3
988
+
989
+ | Action | Supported |
990
+ |--------|-----------|
991
+ | CreateBucket | Yes |
992
+ | HeadBucket | Yes |
993
+ | DeleteBucket | Yes |
994
+ | ListBuckets | Yes |
995
+ | ListObjects | Yes |
996
+ | ListObjectsV2 | Yes |
997
+ | PutObject | Yes |
998
+ | GetObject | Yes |
999
+ | HeadObject | Yes |
1000
+ | DeleteObject | Yes |
1001
+ | DeleteObjects | Yes |
1002
+ | CopyObject | Yes |
1003
+ | CreateMultipartUpload | Yes |
1004
+ | UploadPart | Yes |
1005
+ | UploadPartCopy | Yes |
1006
+ | CompleteMultipartUpload | Yes |
1007
+ | AbortMultipartUpload | Yes |
1008
+ | GetObjectAttributes | Yes |
1009
+ | GetBucketLocation | No |
1010
+ | ListObjectVersions | No |
1011
+ | SelectObjectContent | No |
1012
+ | RestoreObject | No |
1013
+ | RenameObject | Yes |
1014
+ | ListMultipartUploads | No |
1015
+ | ListParts | No |
1016
+
1017
+ Bucket configuration (CORS, lifecycle, encryption, replication, logging, website, notifications, policy), ACLs, versioning, tagging, object lock, and public access block actions are not supported.
1018
+
1019
+ ### STS
1020
+
1021
+ | Action | Supported |
1022
+ |--------|-----------|
1023
+ | GetCallerIdentity | Yes |
1024
+ | AssumeRole | No |
1025
+ | GetSessionToken | No |
1026
+ | GetFederationToken | No |
1027
+
1028
+ 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.
1029
+
1030
+ ## SQS Features
1031
+
1032
+ - **Message attributes** with MD5 checksums matching the AWS algorithm
1033
+ - **Visibility timeout** — messages become invisible after receive and reappear after timeout
1034
+ - **Delay queues** — per-queue default delay and per-message delay overrides
1035
+ - **Long polling** — `WaitTimeSeconds` on ReceiveMessage blocks until messages arrive or timeout
1036
+ - **Dead letter queues** messages exceeding `maxReceiveCount` are moved to the configured DLQ
1037
+ - **Batch operations** — SendMessageBatch, DeleteMessageBatch, ChangeMessageVisibilityBatch with entry ID validation (`InvalidBatchEntryId`) and total batch size validation (`BatchRequestTooLong`)
1038
+ - **Queue attribute range validation**validates `VisibilityTimeout`, `DelaySeconds`, `ReceiveMessageWaitTimeSeconds`, `MaximumMessageSize`, and `MessageRetentionPeriod` on both CreateQueue and SetQueueAttributes
1039
+ - **Message size validation** — rejects messages exceeding 1 MiB (1,048,576 bytes)
1040
+ - **Unicode character validation** rejects messages with characters outside the AWS-allowed set
1041
+ - **KMS attributes** — `KmsMasterKeyId` and `KmsDataKeyReusePeriodSeconds` are accepted and stored (no actual encryption)
1042
+ - **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
1043
+ - **Queue tags**
1044
+
1045
+ ## SNS Features
1046
+
1047
+ - **SNS-to-SQS fan-out** — publish to a topic and messages are delivered to all confirmed SQS subscriptions
1048
+ - **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
1049
+ - **Raw message delivery** — configurable per subscription
1050
+ - **Message size validation** — rejects messages exceeding 256 KB (262,144 bytes)
1051
+ - **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
1052
+ - **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
1053
+ - **Subscription attribute validation** — `SetSubscriptionAttributes` validates attribute names and rejects unknown or read-only attributes
1054
+ - **Topic and subscription tags**
1055
+ - **FIFO topics** `.fifo` suffix enforcement, `MessageGroupId` and `MessageDeduplicationId` passthrough to SQS subscriptions, content-based deduplication
1056
+ - **Batch publish**
1057
+
1058
+ ## S3 Features
1059
+
1060
+ - **Bucket management** — CreateBucket (idempotent), DeleteBucket (rejects non-empty), HeadBucket, ListBuckets, ListObjects (V1 and V2)
1061
+ - **Object operations** PutObject, GetObject, DeleteObject, HeadObject, CopyObject with ETag, Content-Type, and Last-Modified headers
1062
+ - **Multipart uploads** — CreateMultipartUpload, UploadPart, UploadPartCopy, CompleteMultipartUpload, AbortMultipartUpload with correct multipart ETag calculation (`MD5-of-part-digests-partCount`), metadata preservation, and part overwrite support
1063
+ - **ListObjects V2** prefix filtering, delimiter-based virtual directories, MaxKeys, continuation tokens, StartAfter
1064
+ - **CopyObject** — same-bucket and cross-bucket copy via `x-amz-copy-source` header, with metadata preservation
1065
+ - **User metadata** `x-amz-meta-*` headers are stored and returned on GetObject and HeadObject
1066
+ - **Bulk delete** — DeleteObjects for batch key deletion with proper XML entity handling
1067
+ - **Keys with slashes** — full support for slash-delimited keys (e.g., `path/to/file.txt`)
1068
+ - **Stream uploads** handles AWS chunked transfer encoding (`Content-Encoding: aws-chunked`) for stream bodies, including trailing header parsing for checksums
1069
+ - **Checksums** CRC32, SHA1, and SHA256 checksums are stored on upload (PutObject, UploadPart) and returned on download (GetObject, HeadObject with `x-amz-checksum-mode: ENABLED`). Multipart uploads compute composite checksums. GetObjectAttributes supports the `Checksum` attribute. CRC32C and CRC64NVME are silently ignored. Checksums are stored and returned as-is — no body validation is performed.
1070
+ - **GetObjectAttributes** — selective metadata retrieval via `x-amz-object-attributes` header: ETag, StorageClass, ObjectSize, ObjectParts (with pagination), and Checksum (including per-part checksums for multipart objects)
1071
+ - **RenameObject** atomic rename within directory buckets (`PUT /:bucket/:key?renameObject`). Preserves all metadata, ETag, timestamps, and checksums. Rejects general-purpose buckets. Default no-overwrite (412 if destination exists unless `If-Match` is provided). Supports source and destination conditional headers.
1072
+ - **Directory buckets** — `CreateBucket` accepts `<Type>Directory</Type>` in the body. Programmatic API: `server.createBucket("name", { type: "directory" })`. Init config supports `{ name, type }` objects in the `buckets` array.
1073
+ - **Path-style and virtual-hosted-style** — both S3 URL styles are supported (see below)
1074
+
1075
+ ### S3 URL styles
1076
+
1077
+ 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.
1078
+
1079
+ #### Option 1: `fauxqs.dev` wildcard DNS (recommended for Docker image)
1080
+
1081
+ 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.
1082
+
1083
+ ```typescript
1084
+ import { S3Client } from "@aws-sdk/client-s3";
1085
+
1086
+ const s3 = new S3Client({
1087
+ endpoint: "http://s3.localhost.fauxqs.dev:4566",
1088
+ region: "us-east-1",
1089
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
1090
+ });
1091
+ ```
1092
+
1093
+ You can also use raw HTTP requests:
1094
+
1095
+ ```bash
1096
+ # Upload
1097
+ curl -X PUT --data-binary @file.txt http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
1098
+
1099
+ # Download
1100
+ curl http://my-bucket.s3.localhost.fauxqs.dev:4566/file.txt
1101
+ ```
1102
+
1103
+ 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.
1104
+
1105
+ 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`.
1106
+
1107
+ #### Option 2: `interceptLocalhostDns()` (recommended for embedded library)
1108
+
1109
+ Patches Node.js `dns.lookup` so that any hostname ending in `.localhost` resolves to `127.0.0.1`. No client changes needed.
1110
+
1111
+ ```typescript
1112
+ import { interceptLocalhostDns } from "fauxqs";
1113
+
1114
+ const restore = interceptLocalhostDns();
1115
+
1116
+ const s3 = new S3Client({
1117
+ endpoint: "http://s3.localhost:4566",
1118
+ region: "us-east-1",
1119
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
1120
+ });
1121
+
1122
+ // When done (e.g., in afterAll):
1123
+ restore();
1124
+ ```
1125
+
1126
+ The suffix is configurable: `interceptLocalhostDns("myhost.test")` matches `*.myhost.test`.
1127
+
1128
+ **Tradeoffs:** Affects all DNS lookups in the process. Best suited for test suites (`beforeAll` / `afterAll`).
1129
+
1130
+ #### Option 3: `createLocalhostHandler()` (per-client)
1131
+
1132
+ 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.
1133
+
1134
+ ```typescript
1135
+ import { S3Client } from "@aws-sdk/client-s3";
1136
+ import { createLocalhostHandler } from "fauxqs";
1137
+
1138
+ const s3 = new S3Client({
1139
+ endpoint: "http://s3.localhost:4566",
1140
+ region: "us-east-1",
1141
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
1142
+ requestHandler: createLocalhostHandler(),
1143
+ });
1144
+ ```
1145
+
1146
+ #### Option 4: `forcePathStyle` (simplest fallback)
1147
+
1148
+ 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.
1149
+
1150
+ ```typescript
1151
+ const s3 = new S3Client({
1152
+ endpoint: "http://localhost:4566",
1153
+ forcePathStyle: true,
1154
+ // ...
1155
+ });
1156
+ ```
1157
+
1158
+
1159
+ ### Using with AWS CLI
1160
+
1161
+ fauxqs is wire-compatible with the standard AWS CLI. Point it at the fauxqs endpoint:
1162
+
1163
+ #### SQS
1164
+
1165
+ ```bash
1166
+ aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name my-queue
1167
+ aws --endpoint-url http://localhost:4566 sqs create-queue \
1168
+ --queue-name my-queue.fifo \
1169
+ --attributes FifoQueue=true,ContentBasedDeduplication=true
1170
+ aws --endpoint-url http://localhost:4566 sqs send-message \
1171
+ --queue-url http://localhost:4566/000000000000/my-queue \
1172
+ --message-body "hello"
1173
+ ```
1174
+
1175
+ #### SNS
1176
+
1177
+ ```bash
1178
+ aws --endpoint-url http://localhost:4566 sns create-topic --name my-topic
1179
+ aws --endpoint-url http://localhost:4566 sns subscribe \
1180
+ --topic-arn arn:aws:sns:us-east-1:000000000000:my-topic \
1181
+ --protocol sqs \
1182
+ --notification-endpoint arn:aws:sqs:us-east-1:000000000000:my-queue
1183
+ ```
1184
+
1185
+ #### S3
1186
+
1187
+ ```bash
1188
+ aws --endpoint-url http://localhost:4566 s3 mb s3://my-bucket
1189
+ aws --endpoint-url http://localhost:4566 s3 cp file.txt s3://my-bucket/file.txt
1190
+ ```
1191
+
1192
+ If the AWS CLI uses virtual-hosted-style S3 URLs by default, configure path-style:
1193
+
1194
+ ```bash
1195
+ aws configure set default.s3.addressing_style path
1196
+ ```
1197
+
1198
+ ## Testing Strategies
1199
+
1200
+ fauxqs supports two deployment modes that complement each other for a complete testing workflow:
1201
+
1202
+ | Mode | Best for | Startup | Assertions |
1203
+ |------|----------|---------|------------|
1204
+ | **Library** (embedded) | Unit tests, integration tests, CI | Milliseconds, in-process | Full programmatic API: `sendMessage`, `publish`, `spy`, `inspectQueue`, `reset`, `purgeAll` |
1205
+ | **Docker** (standalone) | Local development, acceptance tests, dev environments | Seconds, real HTTP | Init config, HTTP inspection endpoints |
1206
+
1207
+ ### Library mode for tests
1208
+
1209
+ 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:
1210
+
1211
+ ```typescript
1212
+ // test/setup.ts
1213
+ import { startFauxqs, type FauxqsServer } from "fauxqs";
1214
+
1215
+ export async function createTestServer(): Promise<FauxqsServer> {
1216
+ const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
1217
+
1218
+ // Pre-create resources via the programmatic API (no SDK roundtrips)
1219
+ server.createQueue("my-queue");
1220
+ server.createBucket("my-bucket");
1221
+
1222
+ return server;
1223
+ }
1224
+ ```
1225
+
1226
+ ```typescript
1227
+ // test/app.test.ts
1228
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
1229
+
1230
+ let server: FauxqsServer;
1231
+
1232
+ beforeAll(async () => { server = await createTestServer(); });
1233
+ afterAll(async () => { await server.stop(); });
1234
+ beforeEach(() => { server.spy.clear(); });
1235
+
1236
+ it("tracks uploads via the spy", async () => {
1237
+ // ... trigger your app logic that uploads to S3 ...
1238
+
1239
+ const event = await server.spy.waitForMessage(
1240
+ { service: "s3", bucket: "my-bucket", key: "file.txt", status: "uploaded" },
1241
+ undefined,
1242
+ 2000, // timeout prevents tests from hanging
1243
+ );
1244
+ expect(event.status).toBe("uploaded");
1245
+ });
1246
+ ```
1247
+
1248
+ 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.
1249
+
1250
+ ### Docker mode for local development
1251
+
1252
+ Use `docker-compose.yml` with an init config to give your team a consistent local environment:
1253
+
1254
+ ```yaml
1255
+ # docker-compose.yml
1256
+ services:
1257
+ fauxqs:
1258
+ image: kibertoad/fauxqs:latest
1259
+ ports: ["4566:4566"]
1260
+ environment:
1261
+ - FAUXQS_INIT=/app/init.json
1262
+ volumes:
1263
+ - ./fauxqs-init.json:/app/init.json
1264
+ - fauxqs-data:/data
1265
+
1266
+ app:
1267
+ build: .
1268
+ depends_on:
1269
+ fauxqs:
1270
+ condition: service_healthy
1271
+ environment:
1272
+ - AWS_ENDPOINT=http://fauxqs:4566
1273
+
1274
+ volumes:
1275
+ fauxqs-data:
1276
+ ```
1277
+
1278
+ Docker mode validates your real deployment topology networking, DNS, container-to-container communication and is language-agnostic (any AWS SDK can connect). The `fauxqs-data` volume persists state across restarts queues, messages, and objects survive `docker compose down` / `up` cycles.
1279
+
1280
+ ### Recommended combination
1281
+
1282
+ 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.
1283
+
1284
+ See the [`examples/recommended/`](examples/recommended/) directory for a complete working example with a Fastify app, library-mode vitest tests, and Docker compose configuration.
1285
+
1286
+ ## Conventions
1287
+
1288
+ - Account ID: `000000000000`
1289
+ - 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.
1290
+ - Queue URL format: `http://sqs.{region}.{host}:{port}/000000000000/{queueName}` (host defaults to `localhost`)
1291
+ - Queue ARN format: `arn:aws:sqs:{region}:000000000000:{queueName}`
1292
+ - Topic ARN format: `arn:aws:sns:{region}:000000000000:{topicName}`
1293
+
1294
+ ## Limitations
1295
+
1296
+ fauxqs is designed for development and testing. It does not support:
1297
+
1298
+ - Non-SQS SNS delivery protocols (HTTP/S, Lambda, email, SMS)
1299
+ - Authentication or authorization
1300
+ - Cross-account operations
1301
+
1302
+ ## Examples
1303
+
1304
+ The [`examples/`](examples/) directory contains runnable TypeScript examples covering fauxqs-specific features beyond standard AWS SDK usage:
1305
+
1306
+ | Example | Description |
1307
+ |---------|-------------|
1308
+ | [`alternatives/programmatic/programmatic-api.ts`](examples/alternatives/programmatic/programmatic-api.ts) | Server lifecycle, resource creation, SDK usage, `inspectQueue()`, `reset()`, `purgeAll()`, `setup()` |
1309
+ | [`alternatives/programmatic/message-spy.ts`](examples/alternatives/programmatic/message-spy.ts) | `MessageSpyReader` — all spy methods, partial/predicate filters, discriminated union narrowing, DLQ tracking |
1310
+ | [`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 |
1311
+ | [`alternatives/programmatic/queue-inspection.ts`](examples/alternatives/programmatic/queue-inspection.ts) | Programmatic `inspectQueue()` and HTTP `/_fauxqs/queues` endpoints |
1312
+ | [`alternatives/docker/standalone/`](examples/alternatives/docker/standalone/standalone-container.ts) | Connecting to a fauxqs Docker container from the host |
1313
+ | [`alternatives/docker/container-to-container/`](examples/alternatives/docker/container-to-container/) | Container-to-container communication via docker-compose |
1314
+ | [`recommended/`](examples/recommended/) | Dual-mode testing: library mode (vitest + spy) for CI, Docker for local dev |
1315
+
1316
+ All examples are type-checked in CI to prevent staleness.
1317
+
1318
+ ## Migrating from LocalStack
1319
+
1320
+ If you're currently using LocalStack for local SNS, SQS, and/or S3 emulation, fauxqs is a drop-in replacement for those services. Both listen on port 4566 by default and accept the same AWS SDK calls, so the migration is straightforward.
1321
+
1322
+ There are two approaches: a Docker swap (quickest) and a hybrid setup (recommended for the best integration test experience). Which one makes sense depends on whether you use S3.
1323
+
1324
+ ### SNS/SQS only
1325
+
1326
+ If your LocalStack usage is limited to SNS and SQS, the migration is a one-line Docker image swap. No SDK client changes are needed — the endpoint URL, port, and credentials stay the same.
1327
+
1328
+ **Docker swap:**
1329
+
1330
+ ```yaml
1331
+ # Before (LocalStack)
1332
+ services:
1333
+ localstack:
1334
+ image: localstack/localstack
1335
+ ports: ["4566:4566"]
1336
+ environment:
1337
+ - SERVICES=sqs,sns
1338
+
1339
+ # After (fauxqs)
1340
+ services:
1341
+ fauxqs:
1342
+ image: kibertoad/fauxqs:latest
1343
+ ports: ["4566:4566"]
1344
+ ```
1345
+
1346
+ **Region:** Both LocalStack and fauxqs auto-detect the region from the SDK client's `Authorization` header, so in most setups no configuration is needed. If you have older LocalStack configs that used the now-removed `DEFAULT_REGION` env var (deprecated in v0.12.7, removed in v2.0), the equivalent in fauxqs is `FAUXQS_DEFAULT_REGION` — it serves as a fallback when the region can't be resolved from request headers. Both default to `us-east-1`.
1347
+
1348
+ The main difference is how resources are pre-created. LocalStack uses init hooks (`/etc/localstack/init/ready.d/` shell scripts with `awslocal` CLI calls), while fauxqs uses a declarative JSON config. `awslocal` defaults to `us-east-1` unless you pass `--region`, and fauxqs init config uses `defaultRegion` (`us-east-1`) unless you set an explicit `region` per resource — so both create resources in the same region by default:
1349
+
1350
+ ```bash
1351
+ # LocalStack init script (ready.d/init.sh)
1352
+ # awslocal defaults to us-east-1; use --region to override
1353
+ awslocal sqs create-queue --queue-name orders
1354
+ awslocal sqs create-queue --queue-name orders-dlq
1355
+ awslocal sns create-topic --name events
1356
+ awslocal sns subscribe \
1357
+ --topic-arn arn:aws:sns:us-east-1:000000000000:events \
1358
+ --protocol sqs \
1359
+ --notification-endpoint arn:aws:sqs:us-east-1:000000000000:orders
1360
+ ```
1361
+
1362
+ ```json
1363
+ // fauxqs init.json uses defaultRegion (us-east-1) unless "region" is set per resource
1364
+ {
1365
+ "queues": [{ "name": "orders" }, { "name": "orders-dlq" }],
1366
+ "topics": [{ "name": "events" }],
1367
+ "subscriptions": [{ "topic": "events", "queue": "orders" }]
1368
+ }
1369
+ ```
1370
+
1371
+ ```yaml
1372
+ # Before (LocalStack docker-compose.yml)
1373
+ services:
1374
+ localstack:
1375
+ image: localstack/localstack
1376
+ ports: ["4566:4566"]
1377
+ environment:
1378
+ - SERVICES=sqs,sns
1379
+ volumes:
1380
+ - ./ready.d:/etc/localstack/init/ready.d
1381
+
1382
+ # After (fauxqs docker-compose.yml)
1383
+ services:
1384
+ fauxqs:
1385
+ image: kibertoad/fauxqs:latest
1386
+ ports: ["4566:4566"]
1387
+ environment:
1388
+ - FAUXQS_INIT=/app/init.json
1389
+ volumes:
1390
+ - ./init.json:/app/init.json
1391
+ ```
1392
+
1393
+ Update your docker-compose service name references (e.g., `http://localstack:4566` to `http://fauxqs:4566`) and you're done.
1394
+
1395
+ ### SNS/SQS/S3
1396
+
1397
+ When S3 is involved, the Docker swap is still straightforward — the only additional consideration is S3 URL style. If you were using `forcePathStyle: true` with LocalStack, it works identically with fauxqs. If you were using LocalStack's `localhost.localstack.cloud` wildcard DNS for virtual-hosted-style, switch to `localhost.fauxqs.dev`:
1398
+
1399
+ ```typescript
1400
+ // Before (LocalStack)
1401
+ const s3 = new S3Client({
1402
+ endpoint: "http://s3.localhost.localstack.cloud:4566",
1403
+ // ...
1404
+ });
1405
+
1406
+ // After (fauxqs)
1407
+ const s3 = new S3Client({
1408
+ endpoint: "http://s3.localhost.fauxqs.dev:4566",
1409
+ // ...
1410
+ });
1411
+ ```
1412
+
1413
+ For container-to-container S3 in docker-compose, fauxqs includes a built-in dnsmasq that resolves `*.s3.fauxqs` to the container IP — see [Container-to-container S3 virtual-hosted-style](#container-to-container-s3-virtual-hosted-style).
1414
+
1415
+ The init config for S3 is the same declarative JSON, with a `buckets` array:
1416
+
1417
+ ```json
1418
+ {
1419
+ "queues": [{ "name": "orders" }],
1420
+ "topics": [{ "name": "events" }],
1421
+ "subscriptions": [{ "topic": "events", "queue": "orders" }],
1422
+ "buckets": ["uploads", "exports"]
1423
+ }
1424
+ ```
1425
+
1426
+ ### Going hybrid (recommended)
1427
+
1428
+ The Docker swap gets you running quickly, but the real win comes from going hybrid: use fauxqs as an **embedded library** in your test suite and keep Docker for local development.
1429
+
1430
+ With LocalStack, integration tests typically look like this:
1431
+
1432
+ 1. Start LocalStack container (docker-compose or testcontainers) — takes seconds
1433
+ 2. Create resources via `awslocal` or SDK calls — more seconds
1434
+ 3. Run your test logic
1435
+ 4. Assert by polling SQS queues, checking S3 objects, etc.
1436
+ 5. Clean up resources between tests — often fragile or skipped
1437
+
1438
+ With fauxqs in library mode:
1439
+
1440
+ 1. `startFauxqs({ port: 0 })` — starts in milliseconds, in-process
1441
+ 2. `server.setup({ queues: [...], topics: [...] })` — instant, no network calls
1442
+ 3. Run your test logic
1443
+ 4. Assert with `server.spy.waitForMessage()` no polling, no race conditions
1444
+ 5. `server.reset()` between tests — clears messages, keeps resources
1445
+
1446
+ **What you gain:**
1447
+
1448
+ | Concern | LocalStack Docker | fauxqs library mode |
1449
+ |---------|-------------------|---------------------|
1450
+ | Test startup | Seconds (container boot + resource creation) | Milliseconds (in-process) |
1451
+ | CI dependency | Docker required | npm only |
1452
+ | Asserting message delivery | Poll SQS queue, hope timing is right | `spy.waitForMessage()` resolves immediately or waits |
1453
+ | Asserting message *not* delivered | `sleep()` + check | `spy.expectNoMessage()` — deterministic negative assertion |
1454
+ | Filter policy testing | Receive from queue, check absence manually | `expectNoMessage()` on filtered-out queues |
1455
+ | DLQ verification | Receive from DLQ queue via SDK | `spy.waitForMessage({ status: "dlq" })` + `inspectQueue()` |
1456
+ | Queue state inspection | `GetQueueAttributes` (counts only) | `inspectQueue()` — see every message, grouped by state |
1457
+ | State reset between tests | Restart container or re-create resources | `server.reset()` — instant, preserves resource definitions |
1458
+ | Seeding test data | SDK calls through network stack | `server.sendMessage()` / `server.publish()` direct, no network |
1459
+ | S3 event tracking | Check bucket contents via SDK | `spy.waitForMessage({ service: "s3", status: "uploaded" })` |
1460
+ | Parallel test files | Port conflicts or shared state | Each file gets its own server on port 0 |
1461
+
1462
+ **Migration path:**
1463
+
1464
+ 1. Install fauxqs as a dev dependency: `npm install -D fauxqs`
1465
+ 2. Create a test helper:
1466
+
1467
+ ```typescript
1468
+ // test/setup.ts
1469
+ import { startFauxqs, type FauxqsServer } from "fauxqs";
1470
+
1471
+ export async function createTestServer(): Promise<FauxqsServer> {
1472
+ const server = await startFauxqs({ port: 0, logger: false, messageSpies: true });
1473
+
1474
+ server.setup({
1475
+ queues: [{ name: "orders" }, { name: "orders-dlq" }],
1476
+ topics: [{ name: "events" }],
1477
+ subscriptions: [{ topic: "events", queue: "orders" }],
1478
+ buckets: ["uploads"],
1479
+ });
1480
+
1481
+ return server;
1482
+ }
1483
+ ```
1484
+
1485
+ 3. Replace your LocalStack container setup with the test helper:
1486
+
1487
+ ```typescript
1488
+ // Before: LocalStack via testcontainers or docker-compose
1489
+ let endpoint: string;
1490
+ beforeAll(async () => {
1491
+ // start container, wait for health, create resources via SDK...
1492
+ endpoint = "http://localhost:4566";
1493
+ }, 30_000);
1494
+
1495
+ // After: fauxqs library
1496
+ let server: FauxqsServer;
1497
+ beforeAll(async () => {
1498
+ server = await createTestServer();
1499
+ });
1500
+ afterAll(async () => { await server.stop(); });
1501
+ beforeEach(() => { server.reset(); });
1502
+ ```
1503
+
1504
+ 4. Replace polling-based assertions with spy-based ones:
1505
+
1506
+ ```typescript
1507
+ // Before: poll and hope
1508
+ await sqsClient.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: "test" }));
1509
+ const result = await sqsClient.send(new ReceiveMessageCommand({ QueueUrl: queueUrl, WaitTimeSeconds: 5 }));
1510
+ expect(result.Messages?.[0]?.Body).toBe("test");
1511
+
1512
+ // After: spy knows immediately
1513
+ await sqsClient.send(new SendMessageCommand({ QueueUrl: queueUrl, MessageBody: "test" }));
1514
+ const msg = await server.spy.waitForMessage(
1515
+ { service: "sqs", queueName: "orders", status: "published" },
1516
+ undefined,
1517
+ 2000,
1518
+ );
1519
+ expect(msg.body).toBe("test");
1520
+ ```
1521
+
1522
+ 5. Keep your `docker-compose.yml` with the fauxqs image for local development — `docker compose up` gives your team a running environment without Node.js installed.
1523
+
1524
+ This hybrid setup gives you fast, deterministic tests in CI (no Docker required) and a realistic Docker environment for local development. See the [`examples/recommended/`](examples/recommended/) directory for a complete working example.
1525
+
1526
+ ## Benchmarks
1527
+
1528
+ 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.
1529
+
1530
+ ## License
1531
+
1532
+ MIT