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 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 the S3 client
88
+ // Mock all instances of S3Client
69
89
  s3Mock = mockClient(S3Client);
70
90
 
71
- // Create service with real S3Client (which is now mocked)
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
- ### Fixture Loading
125
-
126
- Load mock responses from files for easier test data management:
159
+ ### Error Handling
127
160
 
128
161
  ```typescript
129
- // Load JSON response from file
130
- s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/s3-response.json");
162
+ s3Mock.on(GetObjectCommand).rejects(new Error("Not found"));
131
163
 
132
- // Load text response from file
133
- s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/response.txt");
164
+ // Or with rejectsOnce
165
+ s3Mock
166
+ .on(GetObjectCommand)
167
+ .rejectsOnce(new Error("Temporary failure"))
168
+ .resolves({ Body: "success" });
169
+ ```
134
170
 
135
- // JSON files are automatically parsed, text files returned as strings
136
- // File paths are resolved relative to current working directory
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
- ### Paginator Support
181
+ ### Mocking Existing Instances
140
182
 
141
- Mock AWS SDK v3 pagination with automatic token handling:
183
+ Use `mockClientInstance()` when you need to mock a client that's already been created:
142
184
 
143
185
  ```typescript
144
- // Mock DynamoDB scan with pagination
145
- const items = Array.from({ length: 25 }, (_, i) => ({
146
- id: { S: `item-${i + 1}` },
147
- }));
186
+ // Your application service that uses an injected S3 client
187
+ class FileUploadService {
188
+ constructor(private s3Client: S3Client) {}
148
189
 
149
- dynamoMock.on(ScanCommand).resolvesPaginated(items, {
150
- pageSize: 10,
151
- itemsKey: "Items",
152
- tokenKey: "NextToken",
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
- // First call returns items 1-10 with NextToken
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
- // Mock S3 list objects with pagination
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
- s3Mock.on(ListObjectsV2Command).resolvesPaginated(objects, {
165
- pageSize: 10,
166
- itemsKey: "Contents",
167
- tokenKey: "ContinuationToken",
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 AWS SDK's marshal/unmarshal utilities for type-safe data handling:
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
- ### Stream Mocking (S3 Helper)
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
- ### Delay/Latency Simulation
366
+ ### Paginator Support
287
367
 
288
- Simulate network delays for testing timeouts and race conditions:
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
- // Resolve with delay
292
- s3Mock.on(GetObjectCommand).resolvesWithDelay({ Body: "data" }, 1000);
373
+ import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
374
+ import { DynamoDBClient, ScanCommand } from '@aws-sdk/client-dynamodb';
293
375
 
294
- // Reject with delay
295
- s3Mock.on(GetObjectCommand).rejectsWithDelay("Network timeout", 500);
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
- ### Error Handling
498
+ ### Delay/Latency Simulation
499
+
500
+ Simulate network delays for testing timeouts and race conditions:
318
501
 
319
502
  ```typescript
320
- s3Mock.on(GetObjectCommand).rejects(new Error("Not found"));
503
+ // Resolve with delay
504
+ s3Mock.on(GetObjectCommand).resolvesWithDelay({ Body: "data" }, 1000);
321
505
 
322
- // Or with rejectsOnce
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
- ### Custom Handlers
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
- ### Mocking Existing Instances
512
+ Load mock responses from files for easier test data management:
340
513
 
341
514
  ```typescript
342
- // Your application service that uses an injected S3 client
343
- class FileUploadService {
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
- expect(result.ETag).toBe("mock-etag");
366
- expect(mock).toHaveReceivedCommand(PutObjectCommand);
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 troubleshoot mock configurations and see detailed information about command matching:
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
- // This will log:
385
- // [AWS Mock Debug] Received command: GetObjectCommand
386
- // [AWS Mock Debug] Found 1 mock(s) for GetObjectCommand
387
- // [AWS Mock Debug] Using mock at index 0 for GetObjectCommand
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 logs include:
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 all mocks and call history
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
- ## 🙏 Acknowledgements
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
- This library was inspired by [aws-sdk-client-mock](https://github.com/m-radzikowski/aws-sdk-client-mock). Adapted the core concepts and API design for Vitest while adding additional features and capabilities.
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 b=require("vitest"),P=require("node:fs"),R=require("node:path"),N=require("node:stream"),C=require("./matchers-Rq18z2C7.cjs");class d extends Error{constructor(e,o,c,n){super(e),this.name="AwsError",this.code=o,this.statusCode=c,this.retryable=n}}const W=t=>{const e=t?`The specified key does not exist. Key: ${t}`:"The specified key does not exist.";return new d(e,"NoSuchKey",404,!1)},M=t=>{const e=t?`The specified bucket does not exist. Bucket: ${t}`:"The specified bucket does not exist.";return new d(e,"NoSuchBucket",404,!1)},$=t=>{const e=t?`Access Denied for resource: ${t}`:"Access Denied";return new d(e,"AccessDenied",403,!1)},D=t=>{const e=t?`Requested resource not found: ${t}`:"Requested resource not found";return new d(e,"ResourceNotFoundException",400,!1)},I=()=>new d("The conditional request failed","ConditionalCheckFailedException",400,!1),O=()=>new d("Rate exceeded","Throttling",400,!0),T=()=>new d("We encountered an internal error. Please try again.","InternalServerError",500,!0);function k(){return{enabled:!1,log(t,e){this.enabled&&(e===void 0?console.log(`[aws-sdk-vitest-mock](Debug) ${t}`):console.log(`[aws-sdk-vitest-mock](Debug) ${t}`,e))}}}function y(t){t.enabled=!0}function v(t){t.enabled=!1}function F(t){const e=R.resolve(t),o=P.readFileSync(e,"utf8");return t.endsWith(".json")?JSON.parse(o):o}function L(t,e={}){const{pageSize:o=10,tokenKey:c="NextToken",itemsKey:n="Items"}=e;if(t.length===0)return[{[n]:[]}];const s=[];for(let r=0;r<t.length;r+=o){const u=t.slice(r,r+o),i=r+o<t.length,l={[n]:u};if(i){const a=l;a[c]=`token-${r+o}`}s.push(l)}return s}function A(){return typeof process<"u"&&process.versions?.node?"node":typeof process<"u"&&process.versions?.bun?"bun":"browser"}function K(t){const e=typeof t=="string"?Buffer.from(t,"utf8"):Buffer.from(t);let o=!1;return new N.Readable({read(){o||(this.push(e),this.push(null),o=!0)}})}function q(t){let e;return typeof t=="string"?e=new TextEncoder().encode(t):t instanceof Buffer?e=new Uint8Array(t):e=t,new ReadableStream({start(o){o.enqueue(e),o.close()}})}function h(t){const e=A();return e==="node"||e==="bun"?K(t):q(t)}function w(t,e){return Object.keys(e).every(o=>{const c=e[o],n=t[o];return c&&typeof c=="object"&&!Array.isArray(c)?typeof n!="object"||n===null?!1:w(n,c):n===c})}function j(t,e){if(t===e)return!0;if(typeof t!="object"||t===null||typeof e!="object"||e===null)return t===e;const o=Object.keys(t),c=Object.keys(e);return o.length!==c.length?!1:c.every(n=>{if(!Object.prototype.hasOwnProperty.call(t,n))return!1;const s=t,r=e,u=s[n],i=r[n];return typeof u=="object"&&u!==null&&typeof i=="object"&&i!==null?j(u,i):u===i})}function S(t){return async function(e){const o=()=>this;t.debugLogger.log(`Received command: ${e.constructor.name}`,e.input);const c=t.map.get(e.constructor);if(c){t.debugLogger.log(`Found ${c.length} mock(s) for ${e.constructor.name}`);const n=c.findIndex(s=>s.strict?s.matcher&&j(e.input,s.matcher):!s.matcher||w(e.input,s.matcher));if(n===-1)t.debugLogger.log(`No matching mock found for ${e.constructor.name}`,e.input);else{const s=c[n];if(!s)throw new Error(`Mock at index ${n} not found`);return t.debugLogger.log(`Using mock at index ${n} for ${e.constructor.name}`),s.once&&(c.splice(n,1),t.debugLogger.log(`Removed one-time mock for ${e.constructor.name}`)),s.handler(e.input,o())}}else t.debugLogger.log(`No mocks configured for ${e.constructor.name}`);throw new Error(`No mock configured for command: ${e.constructor.name}`)}}function x(t,e,o,c={}){const n=(r,u)=>{const i={matcher:o,handler:r,once:u,strict:!!c.strict},l=t.map.get(e)??[];if(u){const a=l.findIndex(f=>!f.once);a===-1?l.push(i):l.splice(a,0,i),t.map.set(e,l)}else{const a=l.filter(f=>f.once||JSON.stringify(f.matcher)!==JSON.stringify(o));a.push(i),t.map.set(e,a)}},s={resolves(r){return n(()=>Promise.resolve(r),!1),s},rejects(r){return n(()=>{const u=typeof r=="string"?new Error(r):r;return Promise.reject(u)},!1),s},callsFake(r){return n(r,!1),s},resolvesOnce(r){return n(()=>Promise.resolve(r),!0),s},rejectsOnce(r){return n(()=>{const u=typeof r=="string"?new Error(r):r;return Promise.reject(u)},!0),s},callsFakeOnce(r){return n(r,!0),s},resolvesStream(r){return n(()=>Promise.resolve({Body:h(r)}),!1),s},resolvesStreamOnce(r){return n(()=>Promise.resolve({Body:h(r)}),!0),s},resolvesWithDelay(r,u){const i=l=>{setTimeout(()=>l(r),u)};return n(()=>new Promise(i),!1),s},rejectsWithDelay(r,u){const i=typeof r=="string"?new Error(r):r,l=(a,f)=>{setTimeout(()=>f(i),u)};return n(()=>new Promise(l),!1),s},rejectsWithNoSuchKey(r){return n(()=>Promise.reject(W(r)),!1),s},rejectsWithNoSuchBucket(r){return n(()=>Promise.reject(M(r)),!1),s},rejectsWithAccessDenied(r){return n(()=>Promise.reject($(r)),!1),s},rejectsWithResourceNotFound(r){return n(()=>Promise.reject(D(r)),!1),s},rejectsWithConditionalCheckFailed(){return n(()=>Promise.reject(I()),!1),s},rejectsWithThrottling(){return n(()=>Promise.reject(O()),!1),s},rejectsWithInternalServerError(){return n(()=>Promise.reject(T()),!1),s},resolvesPaginated(r,u={}){const i=L(r,u);let l=0;return n(a=>{const f=u.tokenKey||"NextToken",p=a[f];if(p){const g=/token-(\d+)/.exec(p);if(g&&g[1]){const E=g[1];l=Math.floor(Number.parseInt(E,10)/(u.pageSize||10))}}else l=0;const m=i[l]||i[i.length-1]||i[0];if(!m)throw new Error("No paginated responses available");return l=Math.min(l+1,i.length-1),Promise.resolve(m)},!1),s},resolvesFromFile(r){return n(()=>{const u=F(r);return Promise.resolve(u)},!1),s}};return s}const B=t=>{const e={map:new WeakMap,debugLogger:k()},o=t.prototype,c=b.vi.spyOn(o,"send").mockImplementation(S(e));return{client:void 0,on:(s,r,u)=>x(e,s,r,u),reset:()=>{c.mockClear(),e.map=new WeakMap},restore:()=>{c.mockRestore(),e.map=new WeakMap},calls:()=>c.mock.calls.map(s=>s[0]),__rawCalls:()=>c.mock.calls,enableDebug:()=>{y(e.debugLogger)},disableDebug:()=>{v(e.debugLogger)}}},_=t=>{const e={map:new WeakMap,debugLogger:k()},o=b.vi.spyOn(t,"send").mockImplementation(S(e));return{client:t,on:(n,s,r)=>x(e,n,s,r),reset:()=>{o.mockClear(),e.map=new WeakMap},restore:()=>{o.mockRestore(),e.map=new WeakMap},calls:()=>o.mock.calls.map(n=>n[0]),__rawCalls:()=>o.mock.calls,enableDebug:()=>{y(e.debugLogger)},disableDebug:()=>{v(e.debugLogger)}}};exports.matchers=C.matchers;exports.mockClient=B;exports.mockClientInstance=_;
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
- export * from './lib/mock-client.js';
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';