aws-sdk-vitest-mock 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -92
- package/index.cjs +18 -1
- package/index.d.ts +46 -1
- package/index.js +328 -221
- package/lib/matchers.d.ts +141 -0
- package/lib/mock-client.d.ts +436 -19
- package/lib/utils/paginator-helpers.d.ts +85 -0
- package/{matchers-C6AtmwWz.js → matchers-ClGOsQx8.js} +99 -0
- package/{matchers-Rq18z2C7.cjs → matchers-Dkkl4vtx.cjs} +1 -1
- package/package.json +11 -2
- package/vitest-setup.cjs +1 -1
- package/vitest-setup.js +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
A powerful, type-safe mocking library for AWS SDK v3 with Vitest
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/aws-sdk-vitest-mock">
|
|
13
|
+
<img src="https://img.shields.io/npm/v/aws-sdk-vitest-mock?color=cb3837&logo=npm" alt="npm version" />
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://github.com/sudokar/aws-sdk-vitest-mock/actions">
|
|
16
|
+
<img src="https://github.com/sudokar/aws-sdk-vitest-mock/actions/workflows/ci.yml/badge.svg" alt="CI Status" />
|
|
17
|
+
</a>
|
|
18
|
+
<img src="https://img.shields.io/badge/ESM%20Support-yes-4B32C3?logo=javascript" alt="ESM Support" />
|
|
19
|
+
<img src="https://img.shields.io/badge/Zero%20Dependencies-yes-brightgreen" alt="Zero Dependencies" />
|
|
20
|
+
<a href="https://eslint.org/">
|
|
21
|
+
<img src="https://img.shields.io/badge/code%20style-eslint-4B32C3?logo=eslint" alt="ESLint" />
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://prettier.io/">
|
|
24
|
+
<img src="https://img.shields.io/badge/code%20style-prettier-F7B93E?logo=prettier" alt="Prettier" />
|
|
25
|
+
</a>
|
|
26
|
+
<img src="https://img.shields.io/badge/Maintained-yes-brightgreen" alt="Maintained: Yes" />
|
|
27
|
+
</p>
|
|
28
|
+
|
|
11
29
|
---
|
|
12
30
|
|
|
13
31
|
## ✨ Features
|
|
@@ -43,6 +61,8 @@ pnpm add -D aws-sdk-vitest-mock
|
|
|
43
61
|
|
|
44
62
|
### Basic Usage
|
|
45
63
|
|
|
64
|
+
> **Note:** `mockClient()` mocks **all instances** of a client class. Use `mockClientInstance()` when you need to mock a specific instance.
|
|
65
|
+
|
|
46
66
|
```typescript
|
|
47
67
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
48
68
|
import { mockClient } from "aws-sdk-vitest-mock";
|
|
@@ -65,10 +85,10 @@ describe("DocumentService", () => {
|
|
|
65
85
|
let documentService: DocumentService;
|
|
66
86
|
|
|
67
87
|
beforeEach(() => {
|
|
68
|
-
// Mock
|
|
88
|
+
// Mock all instances of S3Client
|
|
69
89
|
s3Mock = mockClient(S3Client);
|
|
70
90
|
|
|
71
|
-
//
|
|
91
|
+
// Any S3Client instance created after this will be mocked
|
|
72
92
|
const s3Client = new S3Client({ region: "us-east-1" });
|
|
73
93
|
documentService = new DocumentService(s3Client);
|
|
74
94
|
});
|
|
@@ -93,6 +113,21 @@ describe("DocumentService", () => {
|
|
|
93
113
|
});
|
|
94
114
|
```
|
|
95
115
|
|
|
116
|
+
## 🎯 Key Concepts
|
|
117
|
+
|
|
118
|
+
Understanding these concepts will help you use the library effectively:
|
|
119
|
+
|
|
120
|
+
- **`mockClient(ClientClass)`** - Mocks **all instances** of a client class. Use this in most test scenarios where you control client creation.
|
|
121
|
+
- **`mockClientInstance(instance)`** - Mocks a **specific client instance**. Use when the client is created outside your test (e.g., in application bootstrap).
|
|
122
|
+
- **Command Matching** - Commands are matched by constructor. Optionally match by input properties (partial matching by default, strict matching available).
|
|
123
|
+
- **Sequential Responses** - Use `resolvesOnce()` / `rejectsOnce()` for one-time behaviors that fall back to permanent handlers set with `resolves()` / `rejects()`.
|
|
124
|
+
- **Chainable API** - All mock configuration methods return the stub, allowing method chaining for cleaner test setup.
|
|
125
|
+
- **Test Lifecycle**:
|
|
126
|
+
- **`reset()`** - Clears call history while preserving mock configurations. Use when you want to verify multiple test scenarios with the same mock setup.
|
|
127
|
+
- **`restore()`** - Completely removes mocking and restores original client behavior. Use in `afterEach()` to clean up between tests.
|
|
128
|
+
|
|
129
|
+
## 📖 Usage Guide
|
|
130
|
+
|
|
96
131
|
### Request Matching
|
|
97
132
|
|
|
98
133
|
```typescript
|
|
@@ -121,56 +156,99 @@ s3Mock
|
|
|
121
156
|
// All other calls return 'subsequent calls'
|
|
122
157
|
```
|
|
123
158
|
|
|
124
|
-
###
|
|
125
|
-
|
|
126
|
-
Load mock responses from files for easier test data management:
|
|
159
|
+
### Error Handling
|
|
127
160
|
|
|
128
161
|
```typescript
|
|
129
|
-
|
|
130
|
-
s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/s3-response.json");
|
|
162
|
+
s3Mock.on(GetObjectCommand).rejects(new Error("Not found"));
|
|
131
163
|
|
|
132
|
-
//
|
|
133
|
-
s3Mock
|
|
164
|
+
// Or with rejectsOnce
|
|
165
|
+
s3Mock
|
|
166
|
+
.on(GetObjectCommand)
|
|
167
|
+
.rejectsOnce(new Error("Temporary failure"))
|
|
168
|
+
.resolves({ Body: "success" });
|
|
169
|
+
```
|
|
134
170
|
|
|
135
|
-
|
|
136
|
-
|
|
171
|
+
### Custom Handlers
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
s3Mock.on(GetObjectCommand).callsFake(async (input, getClient) => {
|
|
175
|
+
const client = getClient();
|
|
176
|
+
console.log("Bucket:", input.Bucket);
|
|
177
|
+
return { Body: `Dynamic response for ${input.Key}` };
|
|
178
|
+
});
|
|
137
179
|
```
|
|
138
180
|
|
|
139
|
-
###
|
|
181
|
+
### Mocking Existing Instances
|
|
140
182
|
|
|
141
|
-
|
|
183
|
+
Use `mockClientInstance()` when you need to mock a client that's already been created:
|
|
142
184
|
|
|
143
185
|
```typescript
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}));
|
|
186
|
+
// Your application service that uses an injected S3 client
|
|
187
|
+
class FileUploadService {
|
|
188
|
+
constructor(private s3Client: S3Client) {}
|
|
148
189
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
190
|
+
async uploadFile(bucket: string, key: string, data: string) {
|
|
191
|
+
return await this.s3Client.send(
|
|
192
|
+
new PutObjectCommand({ Bucket: bucket, Key: key, Body: data }),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
test("should mock existing S3 client instance", async () => {
|
|
198
|
+
// Client is already created (e.g., in application bootstrap)
|
|
199
|
+
const s3Client = new S3Client({ region: "us-east-1" });
|
|
200
|
+
const service = new FileUploadService(s3Client);
|
|
201
|
+
|
|
202
|
+
// Mock the specific client instance
|
|
203
|
+
const mock = mockClientInstance(s3Client);
|
|
204
|
+
mock.on(PutObjectCommand).resolves({ ETag: "mock-etag" });
|
|
205
|
+
|
|
206
|
+
// Test your service
|
|
207
|
+
const result = await service.uploadFile("bucket", "key", "data");
|
|
208
|
+
|
|
209
|
+
expect(result.ETag).toBe("mock-etag");
|
|
210
|
+
expect(mock).toHaveReceivedCommand(PutObjectCommand);
|
|
153
211
|
});
|
|
212
|
+
```
|
|
154
213
|
|
|
155
|
-
|
|
156
|
-
// Second call with NextToken returns items 11-20
|
|
157
|
-
// Third call returns items 21-25 without NextToken
|
|
214
|
+
### Test Lifecycle Management
|
|
158
215
|
|
|
159
|
-
|
|
160
|
-
const objects = Array.from({ length: 15 }, (_, i) => ({
|
|
161
|
-
Key: `file-${i + 1}.txt`,
|
|
162
|
-
}));
|
|
216
|
+
Use `reset()` to clear call history between assertions while keeping mock configurations. Use `restore()` to completely clean up mocking:
|
|
163
217
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
218
|
+
```typescript
|
|
219
|
+
test("should handle multiple operations with same mock", async () => {
|
|
220
|
+
const s3Mock = mockClient(S3Client);
|
|
221
|
+
const client = new S3Client({});
|
|
222
|
+
|
|
223
|
+
// Configure mock once
|
|
224
|
+
s3Mock.on(GetObjectCommand).resolves({ Body: "file-content" });
|
|
225
|
+
|
|
226
|
+
// First operation
|
|
227
|
+
await client.send(
|
|
228
|
+
new GetObjectCommand({ Bucket: "bucket", Key: "file1.txt" }),
|
|
229
|
+
);
|
|
230
|
+
expect(s3Mock).toHaveReceivedCommandTimes(GetObjectCommand, 1);
|
|
231
|
+
|
|
232
|
+
// Reset clears call history but keeps mock configuration
|
|
233
|
+
s3Mock.reset();
|
|
234
|
+
expect(s3Mock).toHaveReceivedCommandTimes(GetObjectCommand, 0);
|
|
235
|
+
|
|
236
|
+
// Second operation - mock still works
|
|
237
|
+
await client.send(
|
|
238
|
+
new GetObjectCommand({ Bucket: "bucket", Key: "file2.txt" }),
|
|
239
|
+
);
|
|
240
|
+
expect(s3Mock).toHaveReceivedCommandTimes(GetObjectCommand, 1);
|
|
241
|
+
|
|
242
|
+
// Clean up completely
|
|
243
|
+
s3Mock.restore();
|
|
168
244
|
});
|
|
169
245
|
```
|
|
170
246
|
|
|
247
|
+
## 🔧 AWS Service Examples
|
|
248
|
+
|
|
171
249
|
### DynamoDB with Marshal/Unmarshal
|
|
172
250
|
|
|
173
|
-
Mock DynamoDB operations using
|
|
251
|
+
Mock DynamoDB operations using marshal/unmarshal utilities for type-safe data handling:
|
|
174
252
|
|
|
175
253
|
```typescript
|
|
176
254
|
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
@@ -265,7 +343,9 @@ describe("UserService with DynamoDB", () => {
|
|
|
265
343
|
});
|
|
266
344
|
```
|
|
267
345
|
|
|
268
|
-
|
|
346
|
+
## 🚀 Advanced Features
|
|
347
|
+
|
|
348
|
+
### Stream Mocking (S3)
|
|
269
349
|
|
|
270
350
|
Mock S3 operations that return streams with automatic environment detection:
|
|
271
351
|
|
|
@@ -283,21 +363,122 @@ s3Mock
|
|
|
283
363
|
.resolvesStream("Subsequent calls");
|
|
284
364
|
```
|
|
285
365
|
|
|
286
|
-
###
|
|
366
|
+
### Paginator Support
|
|
287
367
|
|
|
288
|
-
|
|
368
|
+
Mock AWS SDK v3 pagination with automatic token handling. **Tokens are the actual last item from each page** (works for both DynamoDB and S3).
|
|
369
|
+
|
|
370
|
+
#### DynamoDB Pagination
|
|
289
371
|
|
|
290
372
|
```typescript
|
|
291
|
-
|
|
292
|
-
|
|
373
|
+
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
|
|
374
|
+
import { DynamoDBClient, ScanCommand } from '@aws-sdk/client-dynamodb';
|
|
293
375
|
|
|
294
|
-
//
|
|
295
|
-
|
|
376
|
+
// Create marshalled items (as they would be stored in DynamoDB)
|
|
377
|
+
const users = [
|
|
378
|
+
{ id: "user-1", name: "Alice", email: "alice@example.com" },
|
|
379
|
+
{ id: "user-2", name: "Bob", email: "bob@example.com" },
|
|
380
|
+
{ id: "user-3", name: "Charlie", email: "charlie@example.com" },
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
const marshalledUsers = users.map(user => marshall(user));
|
|
384
|
+
|
|
385
|
+
// Configure pagination
|
|
386
|
+
dynamoMock.on(ScanCommand).resolvesPaginated(marshalledUsers, {
|
|
387
|
+
pageSize: 1,
|
|
388
|
+
itemsKey: "Items",
|
|
389
|
+
tokenKey: "LastEvaluatedKey", // DynamoDB response key
|
|
390
|
+
inputTokenKey: "ExclusiveStartKey" // DynamoDB request key
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Page 1: Get first user
|
|
394
|
+
const page1 = await client.send(new ScanCommand({ TableName: "Users" }));
|
|
395
|
+
expect(page1.Items).toHaveLength(1);
|
|
396
|
+
// LastEvaluatedKey is the marshalled last item (object, not string!)
|
|
397
|
+
expect(page1.LastEvaluatedKey).toEqual(marshall({ id: "user-1", name: "Alice", ... }));
|
|
398
|
+
|
|
399
|
+
// Unmarshall the items
|
|
400
|
+
const page1Users = page1.Items.map(item => unmarshall(item));
|
|
401
|
+
console.log(page1Users[0]); // { id: "user-1", name: "Alice", ... }
|
|
402
|
+
|
|
403
|
+
// Page 2: Use LastEvaluatedKey to get next page
|
|
404
|
+
const page2 = await client.send(
|
|
405
|
+
new ScanCommand({
|
|
406
|
+
TableName: "Users",
|
|
407
|
+
ExclusiveStartKey: page1.LastEvaluatedKey, // Pass the object directly
|
|
408
|
+
})
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// Page 3: Continue until LastEvaluatedKey is undefined
|
|
412
|
+
const page3 = await client.send(
|
|
413
|
+
new ScanCommand({
|
|
414
|
+
TableName: "Users",
|
|
415
|
+
ExclusiveStartKey: page2.LastEvaluatedKey,
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
expect(page3.LastEvaluatedKey).toBeUndefined(); // No more pages
|
|
296
419
|
```
|
|
297
420
|
|
|
421
|
+
#### S3 Pagination
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
425
|
+
|
|
426
|
+
const objects = Array.from({ length: 100 }, (_, i) => ({
|
|
427
|
+
Key: `file-${i + 1}.txt`,
|
|
428
|
+
Size: 1024,
|
|
429
|
+
LastModified: new Date(),
|
|
430
|
+
}));
|
|
431
|
+
|
|
432
|
+
s3Mock.on(ListObjectsV2Command).resolvesPaginated(objects, {
|
|
433
|
+
pageSize: 50,
|
|
434
|
+
itemsKey: "Contents",
|
|
435
|
+
tokenKey: "NextContinuationToken",
|
|
436
|
+
inputTokenKey: "ContinuationToken"
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// First page
|
|
440
|
+
const page1 = await client.send(
|
|
441
|
+
new ListObjectsV2Command({ Bucket: "my-bucket" })
|
|
442
|
+
);
|
|
443
|
+
expect(page1.Contents).toHaveLength(50);
|
|
444
|
+
// NextContinuationToken is the last object from page 1
|
|
445
|
+
expect(page1.NextContinuationToken).toEqual({ Key: "file-50.txt", ... });
|
|
446
|
+
|
|
447
|
+
// Second page
|
|
448
|
+
const page2 = await client.send(
|
|
449
|
+
new ListObjectsV2Command({
|
|
450
|
+
Bucket: "my-bucket",
|
|
451
|
+
ContinuationToken: page1.NextContinuationToken,
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
expect(page2.Contents).toHaveLength(50);
|
|
455
|
+
expect(page2.NextContinuationToken).toBeUndefined(); // No more pages
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Pagination Options:**
|
|
459
|
+
|
|
460
|
+
- `pageSize` - Number of items per page (default: 10)
|
|
461
|
+
- `itemsKey` - Property name for items array in response (default: "Items")
|
|
462
|
+
- `tokenKey` - Property name for pagination token in response (default: "NextToken")
|
|
463
|
+
- DynamoDB: use `"LastEvaluatedKey"`
|
|
464
|
+
- S3: use `"NextContinuationToken"`
|
|
465
|
+
- `inputTokenKey` - Property name for pagination token in request (defaults to same as tokenKey)
|
|
466
|
+
- DynamoDB: use `"ExclusiveStartKey"`
|
|
467
|
+
- S3: use `"ContinuationToken"`
|
|
468
|
+
|
|
469
|
+
**How It Works:**
|
|
470
|
+
|
|
471
|
+
The mock automatically uses the **last item from each page** as the pagination token. This means:
|
|
472
|
+
|
|
473
|
+
- ✅ For DynamoDB: `LastEvaluatedKey` is a proper object (can be unmarshalled)
|
|
474
|
+
- ✅ For S3: `NextContinuationToken` is the last object
|
|
475
|
+
- ✅ Tokens represent actual data, not opaque strings
|
|
476
|
+
- ✅ Works correctly with `unmarshall()` for DynamoDB
|
|
477
|
+
- Use this when AWS service uses different names for input/output tokens (e.g., DynamoDB's `ExclusiveStartKey` vs `LastEvaluatedKey`)
|
|
478
|
+
|
|
298
479
|
### AWS Error Simulation
|
|
299
480
|
|
|
300
|
-
Convenient methods for common AWS errors:
|
|
481
|
+
Convenient helper methods for common AWS errors:
|
|
301
482
|
|
|
302
483
|
```typescript
|
|
303
484
|
// S3 Errors
|
|
@@ -314,62 +495,33 @@ s3Mock.on(GetObjectCommand).rejectsWithThrottling();
|
|
|
314
495
|
s3Mock.on(GetObjectCommand).rejectsWithInternalServerError();
|
|
315
496
|
```
|
|
316
497
|
|
|
317
|
-
###
|
|
498
|
+
### Delay/Latency Simulation
|
|
499
|
+
|
|
500
|
+
Simulate network delays for testing timeouts and race conditions:
|
|
318
501
|
|
|
319
502
|
```typescript
|
|
320
|
-
|
|
503
|
+
// Resolve with delay
|
|
504
|
+
s3Mock.on(GetObjectCommand).resolvesWithDelay({ Body: "data" }, 1000);
|
|
321
505
|
|
|
322
|
-
//
|
|
323
|
-
s3Mock
|
|
324
|
-
.on(GetObjectCommand)
|
|
325
|
-
.rejectsOnce(new Error("Temporary failure"))
|
|
326
|
-
.resolves({ Body: "success" });
|
|
506
|
+
// Reject with delay
|
|
507
|
+
s3Mock.on(GetObjectCommand).rejectsWithDelay("Network timeout", 500);
|
|
327
508
|
```
|
|
328
509
|
|
|
329
|
-
###
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
s3Mock.on(GetObjectCommand).callsFake(async (input, getClient) => {
|
|
333
|
-
const client = getClient();
|
|
334
|
-
console.log("Bucket:", input.Bucket);
|
|
335
|
-
return { Body: `Dynamic response for ${input.Key}` };
|
|
336
|
-
});
|
|
337
|
-
```
|
|
510
|
+
### Fixture Loading
|
|
338
511
|
|
|
339
|
-
|
|
512
|
+
Load mock responses from files for easier test data management:
|
|
340
513
|
|
|
341
514
|
```typescript
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
constructor(private s3Client: S3Client) {}
|
|
345
|
-
|
|
346
|
-
async uploadFile(bucket: string, key: string, data: string) {
|
|
347
|
-
return await this.s3Client.send(
|
|
348
|
-
new PutObjectCommand({ Bucket: bucket, Key: key, Body: data }),
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
test("should mock existing S3 client instance", async () => {
|
|
354
|
-
// Create the client your application will use
|
|
355
|
-
const s3Client = new S3Client({ region: "us-east-1" });
|
|
356
|
-
const service = new FileUploadService(s3Client);
|
|
357
|
-
|
|
358
|
-
// Mock the existing client instance
|
|
359
|
-
const mock = mockClientInstance(s3Client);
|
|
360
|
-
mock.on(PutObjectCommand).resolves({ ETag: "mock-etag" });
|
|
361
|
-
|
|
362
|
-
// Test your service
|
|
363
|
-
const result = await service.uploadFile("bucket", "key", "data");
|
|
515
|
+
// Load JSON response from file (automatically parsed)
|
|
516
|
+
s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/s3-response.json");
|
|
364
517
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
});
|
|
518
|
+
// Load text response from file (returned as string)
|
|
519
|
+
s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/response.txt");
|
|
368
520
|
```
|
|
369
521
|
|
|
370
522
|
### Debug Mode
|
|
371
523
|
|
|
372
|
-
Enable debug logging to
|
|
524
|
+
Enable debug logging to see detailed information about mock configuration, lifecycle events, and command interactions:
|
|
373
525
|
|
|
374
526
|
```typescript
|
|
375
527
|
const s3Mock = mockClient(S3Client);
|
|
@@ -377,29 +529,62 @@ const s3Mock = mockClient(S3Client);
|
|
|
377
529
|
// Enable debug logging
|
|
378
530
|
s3Mock.enableDebug();
|
|
379
531
|
|
|
532
|
+
// Configuration logs appear immediately:
|
|
533
|
+
// [aws-sdk-vitest-mock](Debug) Configured resolves for GetObjectCommand
|
|
534
|
+
// {
|
|
535
|
+
// "matcher": {
|
|
536
|
+
// "Bucket": "test-bucket"
|
|
537
|
+
// },
|
|
538
|
+
// "strict": false
|
|
539
|
+
// }
|
|
380
540
|
s3Mock
|
|
381
541
|
.on(GetObjectCommand, { Bucket: "test-bucket" })
|
|
382
542
|
.resolves({ Body: "data" });
|
|
383
543
|
|
|
384
|
-
//
|
|
385
|
-
// [
|
|
386
|
-
//
|
|
387
|
-
//
|
|
544
|
+
// Interaction logs appear when commands are sent:
|
|
545
|
+
// [aws-sdk-vitest-mock](Debug) Received command: GetObjectCommand
|
|
546
|
+
// {
|
|
547
|
+
// "Bucket": "test-bucket",
|
|
548
|
+
// "Key": "file.txt"
|
|
549
|
+
// }
|
|
550
|
+
// [aws-sdk-vitest-mock](Debug) Found 1 mock(s) for GetObjectCommand
|
|
551
|
+
// [aws-sdk-vitest-mock](Debug) Using mock at index 0 for GetObjectCommand
|
|
388
552
|
await client.send(
|
|
389
553
|
new GetObjectCommand({ Bucket: "test-bucket", Key: "file.txt" }),
|
|
390
554
|
);
|
|
391
555
|
|
|
556
|
+
// Lifecycle logs:
|
|
557
|
+
// [aws-sdk-vitest-mock](Debug) Clearing call history (mocks preserved)
|
|
558
|
+
s3Mock.reset();
|
|
559
|
+
|
|
560
|
+
// [aws-sdk-vitest-mock](Debug) Restoring original client behavior and clearing all mocks
|
|
561
|
+
s3Mock.restore();
|
|
562
|
+
|
|
392
563
|
// Disable debug logging
|
|
393
564
|
s3Mock.disableDebug();
|
|
394
565
|
```
|
|
395
566
|
|
|
396
|
-
Debug mode
|
|
567
|
+
Debug mode provides comprehensive logging for:
|
|
568
|
+
|
|
569
|
+
**Mock Configuration:**
|
|
570
|
+
|
|
571
|
+
- Mock setup with `.on()`, `.resolves()`, `.rejects()`, `.callsFake()`, etc.
|
|
572
|
+
- Matcher details and strict mode settings
|
|
573
|
+
- Paginated response configuration
|
|
574
|
+
- File-based fixture loading
|
|
575
|
+
|
|
576
|
+
**Mock Interactions:**
|
|
397
577
|
|
|
398
578
|
- Incoming commands and their inputs
|
|
399
579
|
- Number of configured mocks for each command
|
|
400
580
|
- Mock matching results and reasons for failures
|
|
401
581
|
- One-time mock removal notifications
|
|
402
582
|
|
|
583
|
+
**Lifecycle Events:**
|
|
584
|
+
|
|
585
|
+
- Reset operations (clearing call history)
|
|
586
|
+
- Restore operations (removing all mocks)
|
|
587
|
+
|
|
403
588
|
## 🧪 Test Coverage
|
|
404
589
|
|
|
405
590
|
The library includes comprehensive test suites covering all features:
|
|
@@ -480,7 +665,7 @@ Mocks an existing AWS SDK client instance.
|
|
|
480
665
|
### `AwsClientStub` Methods
|
|
481
666
|
|
|
482
667
|
- `on(Command, matcher?, options?)` - Configure mock for a command
|
|
483
|
-
- `reset()` - Clear
|
|
668
|
+
- `reset()` - Clear call history while preserving mock configurations
|
|
484
669
|
- `restore()` - Restore original client behavior
|
|
485
670
|
- `calls()` - Get call history
|
|
486
671
|
- `enableDebug()` - Enable debug logging for troubleshooting
|
|
@@ -543,9 +728,11 @@ bun nx build
|
|
|
543
728
|
|
|
544
729
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the complete guide.
|
|
545
730
|
|
|
546
|
-
##
|
|
731
|
+
## Acknowledgements
|
|
732
|
+
|
|
733
|
+
This library is based on the core ideas and API patterns introduced by [aws-sdk-client-mock](https://github.com/m-radzikowski/aws-sdk-client-mock), which is no longer actively maintained.
|
|
547
734
|
|
|
548
|
-
|
|
735
|
+
It reimagines those concepts for Vitest, while extending them with additional features, improved ergonomics, and ongoing maintenance.
|
|
549
736
|
|
|
550
737
|
## 📝 License
|
|
551
738
|
|
package/index.cjs
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const v=require("vitest"),m=require("./matchers-Dkkl4vtx.cjs"),x=require("node:fs"),O=require("node:path"),F=require("node:stream");class f extends Error{constructor(t,o,i,n){super(t),this.name="AwsError",this.code=o,this.statusCode=i,this.retryable=n}}const L=e=>{const t=e?`The specified key does not exist. Key: ${e}`:"The specified key does not exist.";return new f(t,"NoSuchKey",404,!1)},D=e=>{const t=e?`The specified bucket does not exist. Bucket: ${e}`:"The specified bucket does not exist.";return new f(t,"NoSuchBucket",404,!1)},I=e=>{const t=e?`Access Denied for resource: ${e}`:"Access Denied";return new f(t,"AccessDenied",403,!1)},T=e=>{const t=e?`Requested resource not found: ${e}`:"Requested resource not found";return new f(t,"ResourceNotFoundException",400,!1)},K=()=>new f("The conditional request failed","ConditionalCheckFailedException",400,!1),M=()=>new f("Rate exceeded","Throttling",400,!0),A=()=>new f("We encountered an internal error. Please try again.","InternalServerError",500,!0);function B(e){try{return JSON.stringify(e,void 0,2)}catch{return typeof e=="object"&&e!==null?"[Complex Object]":typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"?String(e):"[Non-serializable value]"}}function j(){return{enabled:!1,log(e,t){if(this.enabled)if(t===void 0)console.log(`${m.colors.magenta("aws-sdk-vitest-mock(debug):")} ${e}`);else{const o=B(t);console.log(`${m.colors.magenta("aws-sdk-vitest-mock(debug):")} ${e}
|
|
2
|
+
${o}`)}}}}function S(e){e.enabled=!0}function w(e){e.enabled=!1}function q(e){const t=O.resolve(e),o=x.readFileSync(t,"utf8");return e.endsWith(".json")?JSON.parse(o):o}function J(e,t={}){const{pageSize:o=10,tokenKey:i="NextToken",itemsKey:n="Items"}=t;if(e.length===0)return[{[n]:[]}];const s=[];for(let r=0;r<e.length;r+=o){const c=e.slice(r,r+o),u=r+o<e.length,l={[n]:c};if(u){const g=l,a=c[c.length-1];g[i]=a}s.push(l)}return s}function _(){return typeof process<"u"&&process.versions?.node?"node":typeof process<"u"&&process.versions?.bun?"bun":"browser"}function z(e){const t=typeof e=="string"?Buffer.from(e,"utf8"):Buffer.from(e);let o=!1;return new F.Readable({read(){o||(this.push(t),this.push(null),o=!0)}})}function V(e){let t;return typeof e=="string"?t=new TextEncoder().encode(e):e instanceof Buffer?t=new Uint8Array(e):t=e,new ReadableStream({start(o){o.enqueue(t),o.close()}})}function k(e){const t=_();return t==="node"||t==="bun"?z(e):V(e)}function $(e,t){return Object.keys(t).every(o=>{const i=t[o],n=e[o];return i&&typeof i=="object"&&!Array.isArray(i)?typeof n!="object"||n===null?!1:$(n,i):n===i})}function C(e,t){if(e===t)return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return e===t;const o=Object.keys(e),i=Object.keys(t);return o.length!==i.length?!1:i.every(n=>{if(!Object.prototype.hasOwnProperty.call(e,n))return!1;const s=e,r=t,c=s[n],u=r[n];return typeof c=="object"&&c!==null&&typeof u=="object"&&u!==null?C(c,u):c===u})}function N(e){return async function(t){const o=()=>this;e.debugLogger.log(`Received command: ${t.constructor.name}`,t.input);const i=e.map.get(t.constructor);if(i){e.debugLogger.log(`Found ${i.length} mock(s) for ${t.constructor.name}`);const s=i.findIndex(r=>r.strict?r.matcher&&C(t.input,r.matcher):!r.matcher||$(t.input,r.matcher));if(s===-1){e.debugLogger.log(`No matching mock found for ${t.constructor.name}`,t.input);const r=i.map((u,l)=>{const g=u.matcher?JSON.stringify(u.matcher,void 0,2):"any input",a=u.strict?" (strict mode)":"";return` Mock #${l+1}: ${g}${a}`}).join(`
|
|
3
|
+
`),c=JSON.stringify(t.input,void 0,2);throw new Error(`No matching mock found for ${t.constructor.name}.
|
|
4
|
+
|
|
5
|
+
Found ${i.length} mock(s) but none matched the input.
|
|
6
|
+
|
|
7
|
+
Configured mocks:
|
|
8
|
+
${r}
|
|
9
|
+
|
|
10
|
+
Received input:
|
|
11
|
+
${c}
|
|
12
|
+
|
|
13
|
+
Tip: Enable debug mode with enableDebug() for detailed matching information.`)}else{const r=i[s];if(!r)throw new Error(`Mock at index ${s} not found`);return e.debugLogger.log(`Using mock at index ${s} for ${t.constructor.name}`),r.once&&(i.splice(s,1),e.debugLogger.log(`Removed one-time mock for ${t.constructor.name}`)),r.handler(t.input,o())}}else e.debugLogger.log(`No mocks configured for ${t.constructor.name}`);const n=JSON.stringify(t.input,void 0,2);throw new Error(`No mock configured for command: ${t.constructor.name}.
|
|
14
|
+
|
|
15
|
+
Received input:
|
|
16
|
+
${n}
|
|
17
|
+
|
|
18
|
+
Did you forget to call mockClient.on(${t.constructor.name})?`)}}function R(e,t,o,i={}){const n=(r,c,u)=>{const l={matcher:o,handler:r,once:c,strict:!!i.strict},g=e.map.get(t)??[];if(c){const a=g.findIndex(d=>!d.once);a===-1?g.push(l):g.splice(a,0,l),e.map.set(t,g),e.debugLogger.log(`Configured ${u}Once for ${t.name}`,o?{matcher:o,strict:!!i.strict}:void 0)}else{const a=g.filter(d=>d.once||JSON.stringify(d.matcher)!==JSON.stringify(o));a.push(l),e.map.set(t,a),e.debugLogger.log(`Configured ${u} for ${t.name}`,o?{matcher:o,strict:!!i.strict}:void 0)}},s={resolves(r){return n(()=>Promise.resolve(r),!1,"resolves"),s},rejects(r){return n(()=>{const c=typeof r=="string"?new Error(r):r;return Promise.reject(c)},!1,"rejects"),s},callsFake(r){return n(r,!1,"callsFake"),s},resolvesOnce(r){return n(()=>Promise.resolve(r),!0,"resolves"),s},rejectsOnce(r){return n(()=>{const c=typeof r=="string"?new Error(r):r;return Promise.reject(c)},!0,"rejects"),s},callsFakeOnce(r){return n(r,!0,"callsFake"),s},resolvesStream(r){return n(()=>Promise.resolve({Body:k(r)}),!1,"resolvesStream"),s},resolvesStreamOnce(r){return n(()=>Promise.resolve({Body:k(r)}),!0,"resolvesStream"),s},resolvesWithDelay(r,c){const u=l=>{setTimeout(()=>l(r),c)};return n(()=>new Promise(u),!1,"resolvesWithDelay"),s},rejectsWithDelay(r,c){const u=typeof r=="string"?new Error(r):r,l=(g,a)=>{setTimeout(()=>a(u),c)};return n(()=>new Promise(l),!1,"rejectsWithDelay"),s},rejectsWithNoSuchKey(r){return n(()=>Promise.reject(L(r)),!1,"rejectsWithNoSuchKey"),s},rejectsWithNoSuchBucket(r){return n(()=>Promise.reject(D(r)),!1,"rejectsWithNoSuchBucket"),s},rejectsWithAccessDenied(r){return n(()=>Promise.reject(I(r)),!1,"rejectsWithAccessDenied"),s},rejectsWithResourceNotFound(r){return n(()=>Promise.reject(T(r)),!1,"rejectsWithResourceNotFound"),s},rejectsWithConditionalCheckFailed(){return n(()=>Promise.reject(K()),!1,"rejectsWithConditionalCheckFailed"),s},rejectsWithThrottling(){return n(()=>Promise.reject(M()),!1,"rejectsWithThrottling"),s},rejectsWithInternalServerError(){return n(()=>Promise.reject(A()),!1,"rejectsWithInternalServerError"),s},resolvesPaginated(r,c={}){const u=J(r,c);let l=0;return e.debugLogger.log(`Configured resolvesPaginated for ${t.name}`,{pageSize:c.pageSize,itemsCount:r.length}),n(g=>{const a=c.tokenKey||"NextToken",d=c.inputTokenKey||a,h=g[d];if(h!=null){const E=c.itemsKey||"Items";let y=0;for(const W of u){const p=W[E];if(p&&p.length>0){const P=p[p.length-1];if(JSON.stringify(P)===JSON.stringify(h)){l=y+1;break}}y++}}else l=0;const b=u[l]||u[u.length-1]||u[0];if(!b)throw new Error("No paginated responses available");return l=Math.min(l+1,u.length-1),Promise.resolve(b)},!1,"resolvesPaginated"),s},resolvesFromFile(r){return e.debugLogger.log(`Configured resolvesFromFile for ${t.name}`,{filePath:r}),n(()=>{const c=q(r);return Promise.resolve(c)},!1,"resolvesFromFile"),s}};return s}const U=e=>{const t={map:new WeakMap,debugLogger:j()},o=e.prototype,i=v.vi.spyOn(o,"send").mockImplementation(N(t));return{client:void 0,on:(s,r,c)=>R(t,s,r,c),reset:()=>{t.debugLogger.log("Clearing call history (mocks preserved)"),i.mockClear()},restore:()=>{t.debugLogger.log("Restoring original client behavior and clearing all mocks"),i.mockRestore(),t.map=new WeakMap},calls:()=>i.mock.calls.map(s=>s[0]),__rawCalls:()=>i.mock.calls,enableDebug:()=>{S(t.debugLogger)},disableDebug:()=>{w(t.debugLogger)}}},G=e=>{const t={map:new WeakMap,debugLogger:j()},o=v.vi.spyOn(e,"send").mockImplementation(N(t));return{client:e,on:(n,s,r)=>R(t,n,s,r),reset:()=>{t.debugLogger.log("Clearing call history (mocks preserved) for client instance"),o.mockClear()},restore:()=>{t.debugLogger.log("Restoring original client behavior and clearing all mocks for client instance"),o.mockRestore(),t.map=new WeakMap},calls:()=>o.mock.calls.map(n=>n[0]),__rawCalls:()=>o.mock.calls,enableDebug:()=>{S(t.debugLogger)},disableDebug:()=>{w(t.debugLogger)}}};exports.matchers=m.matchers;exports.mockClient=U;exports.mockClientInstance=G;
|
package/index.d.ts
CHANGED
|
@@ -1,2 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* AWS SDK Vitest Mock - A powerful, type-safe mocking library for AWS SDK v3 with Vitest
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*
|
|
6
|
+
* @example Basic Setup
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { mockClient } from 'aws-sdk-vitest-mock';
|
|
9
|
+
* import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
|
10
|
+
*
|
|
11
|
+
* const s3Mock = mockClient(S3Client);
|
|
12
|
+
* s3Mock.on(GetObjectCommand).resolves({ Body: 'file contents' });
|
|
13
|
+
*
|
|
14
|
+
* const client = new S3Client({});
|
|
15
|
+
* const result = await client.send(new GetObjectCommand({ Bucket: 'test', Key: 'file.txt' }));
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @example Using Matchers
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { expect } from 'vitest';
|
|
21
|
+
* import { matchers } from 'aws-sdk-vitest-mock';
|
|
22
|
+
*
|
|
23
|
+
* expect.extend(matchers);
|
|
24
|
+
*
|
|
25
|
+
* expect(s3Mock).toHaveReceivedCommand(GetObjectCommand);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Core Functions for mocking AWS SDK clients
|
|
30
|
+
* @category Core Functions
|
|
31
|
+
*/
|
|
32
|
+
export { mockClient, mockClientInstance } from './lib/mock-client.js';
|
|
33
|
+
/**
|
|
34
|
+
* Command stub interface for configuring mock behaviors
|
|
35
|
+
* @category Command Stub
|
|
36
|
+
*/
|
|
37
|
+
export type { AwsCommandStub, AwsClientStub } from './lib/mock-client.js';
|
|
38
|
+
/**
|
|
39
|
+
* Custom Vitest matchers for AWS SDK assertions
|
|
40
|
+
* @category Matchers
|
|
41
|
+
*/
|
|
2
42
|
export { matchers } from './lib/matchers.js';
|
|
43
|
+
/**
|
|
44
|
+
* TypeScript types for matcher interfaces
|
|
45
|
+
* @category Matchers
|
|
46
|
+
*/
|
|
47
|
+
export type { AwsSdkMatchers } from './lib/matchers.js';
|