aws-sdk-vitest-mock 0.4.0 → 0.6.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
@@ -104,6 +122,9 @@ Understanding these concepts will help you use the library effectively:
104
122
  - **Command Matching** - Commands are matched by constructor. Optionally match by input properties (partial matching by default, strict matching available).
105
123
  - **Sequential Responses** - Use `resolvesOnce()` / `rejectsOnce()` for one-time behaviors that fall back to permanent handlers set with `resolves()` / `rejects()`.
106
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.
107
128
 
108
129
  ## 📖 Usage Guide
109
130
 
@@ -190,6 +211,39 @@ test("should mock existing S3 client instance", async () => {
190
211
  });
191
212
  ```
192
213
 
214
+ ### Test Lifecycle Management
215
+
216
+ Use `reset()` to clear call history between assertions while keeping mock configurations. Use `restore()` to completely clean up mocking:
217
+
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();
244
+ });
245
+ ```
246
+
193
247
  ## 🔧 AWS Service Examples
194
248
 
195
249
  ### DynamoDB with Marshal/Unmarshal
@@ -311,55 +365,94 @@ s3Mock
311
365
 
312
366
  ### Paginator Support
313
367
 
314
- Mock AWS SDK v3 pagination with automatic token handling:
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
315
371
 
316
372
  ```typescript
317
- // Mock DynamoDB scan with pagination
318
- // DynamoDB uses different keys for input (ExclusiveStartKey) and output (LastEvaluatedKey)
319
- const items = Array.from({ length: 25 }, (_, i) => ({
320
- id: { S: `item-${i + 1}` },
321
- }));
373
+ import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
374
+ import { DynamoDBClient, ScanCommand } from '@aws-sdk/client-dynamodb';
322
375
 
323
- dynamoMock.on(ScanCommand).resolvesPaginated(items, {
324
- pageSize: 10,
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,
325
388
  itemsKey: "Items",
326
- tokenKey: "LastEvaluatedKey", // Token key in response
327
- inputTokenKey: "ExclusiveStartKey", // Token key in request (when different from response)
389
+ tokenKey: "LastEvaluatedKey", // DynamoDB response key
390
+ inputTokenKey: "ExclusiveStartKey" // DynamoDB request key
328
391
  });
329
392
 
330
- // First call returns items 1-10 with LastEvaluatedKey
331
- const result1 = await client.send(new ScanCommand({ TableName: "test" }));
332
- // result1.LastEvaluatedKey = "token-10"
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", ... }));
333
398
 
334
- // Second call with ExclusiveStartKey returns items 11-20
335
- const result2 = await client.send(
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(
336
405
  new ScanCommand({
337
- TableName: "test",
338
- ExclusiveStartKey: result1.LastEvaluatedKey,
339
- }),
406
+ TableName: "Users",
407
+ ExclusiveStartKey: page1.LastEvaluatedKey, // Pass the object directly
408
+ })
340
409
  );
341
- // result2.LastEvaluatedKey = "token-20"
342
410
 
343
- // Third call returns items 21-25 without LastEvaluatedKey
344
- const result3 = await client.send(
411
+ // Page 3: Continue until LastEvaluatedKey is undefined
412
+ const page3 = await client.send(
345
413
  new ScanCommand({
346
- TableName: "test",
347
- ExclusiveStartKey: result2.LastEvaluatedKey,
348
- }),
414
+ TableName: "Users",
415
+ ExclusiveStartKey: page2.LastEvaluatedKey,
416
+ })
349
417
  );
350
- // result3.LastEvaluatedKey = undefined (no more pages)
418
+ expect(page3.LastEvaluatedKey).toBeUndefined(); // No more pages
419
+ ```
351
420
 
352
- // Mock S3 list objects with pagination
353
- // S3 uses the same key for input and output, so inputTokenKey is optional
354
- const objects = Array.from({ length: 15 }, (_, i) => ({
421
+ #### S3 Pagination
422
+
423
+ ```typescript
424
+ import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
425
+
426
+ const objects = Array.from({ length: 100 }, (_, i) => ({
355
427
  Key: `file-${i + 1}.txt`,
428
+ Size: 1024,
429
+ LastModified: new Date(),
356
430
  }));
357
431
 
358
432
  s3Mock.on(ListObjectsV2Command).resolvesPaginated(objects, {
359
- pageSize: 10,
433
+ pageSize: 50,
360
434
  itemsKey: "Contents",
361
- tokenKey: "NextContinuationToken", // Used for both input and output
435
+ tokenKey: "NextContinuationToken",
436
+ inputTokenKey: "ContinuationToken"
362
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
363
456
  ```
364
457
 
365
458
  **Pagination Options:**
@@ -367,7 +460,20 @@ s3Mock.on(ListObjectsV2Command).resolvesPaginated(objects, {
367
460
  - `pageSize` - Number of items per page (default: 10)
368
461
  - `itemsKey` - Property name for items array in response (default: "Items")
369
462
  - `tokenKey` - Property name for pagination token in response (default: "NextToken")
370
- - `inputTokenKey` - Property name for pagination token in request (default: same as `tokenKey`)
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
371
477
  - Use this when AWS service uses different names for input/output tokens (e.g., DynamoDB's `ExclusiveStartKey` vs `LastEvaluatedKey`)
372
478
 
373
479
  ### AWS Error Simulation
@@ -415,9 +521,7 @@ s3Mock.on(GetObjectCommand).resolvesFromFile("./fixtures/response.txt");
415
521
 
416
522
  ### Debug Mode
417
523
 
418
- Enable debug logging to troubleshoot mock configurations when they're not matching as expected:
419
-
420
- 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:
421
525
 
422
526
  ```typescript
423
527
  const s3Mock = mockClient(S3Client);
@@ -425,29 +529,102 @@ const s3Mock = mockClient(S3Client);
425
529
  // Enable debug logging
426
530
  s3Mock.enableDebug();
427
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
+ // }
428
540
  s3Mock
429
541
  .on(GetObjectCommand, { Bucket: "test-bucket" })
430
542
  .resolves({ Body: "data" });
431
543
 
432
- // This will log:
433
- // [AWS Mock Debug] Received command: GetObjectCommand
434
- // [AWS Mock Debug] Found 1 mock(s) for GetObjectCommand
435
- // [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
436
552
  await client.send(
437
553
  new GetObjectCommand({ Bucket: "test-bucket", Key: "file.txt" }),
438
554
  );
439
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
+
440
563
  // Disable debug logging
441
564
  s3Mock.disableDebug();
442
565
  ```
443
566
 
444
- Debug mode logs include:
567
+ #### Global Debug Configuration
568
+
569
+ Enable debug logging for all mocks globally, with the ability to override at the individual mock level:
570
+
571
+ ```typescript
572
+ import { setGlobalDebug, mockClient } from "aws-sdk-vitest-mock";
573
+ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
574
+ import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
575
+
576
+ // Enable debug for all mocks
577
+ setGlobalDebug(true);
578
+
579
+ // All mocks will inherit the global debug setting
580
+ const s3Mock = mockClient(S3Client);
581
+ const dynamoMock = mockClient(DynamoDBClient);
582
+
583
+ // Both mocks will log debug information
584
+ s3Mock.on(GetObjectCommand).resolves({ Body: "data" });
585
+ dynamoMock.on(GetItemCommand).resolves({ Item: { id: { S: "1" } } });
586
+
587
+ // Override global setting for a specific mock
588
+ s3Mock.disableDebug(); // This mock won't log, but dynamoMock still will
589
+
590
+ // Disable global debug
591
+ setGlobalDebug(false);
592
+ ```
593
+
594
+ **Debug Priority (highest to lowest):**
595
+
596
+ 1. Individual mock's `enableDebug()` or `disableDebug()` call (explicit override)
597
+ 2. Global debug setting via `setGlobalDebug()`
598
+ 3. Default: disabled
599
+
600
+ **Key behaviors:**
601
+
602
+ - When global debug is enabled, all new and existing mocks will log unless explicitly disabled
603
+ - Individual mock settings always take priority over global settings
604
+ - `reset()` preserves individual debug settings
605
+ - Global debug can be changed at any time and affects all mocks without explicit settings
606
+
607
+ Debug mode provides comprehensive logging for:
608
+
609
+ **Mock Configuration:**
610
+
611
+ - Mock setup with `.on()`, `.resolves()`, `.rejects()`, `.callsFake()`, etc.
612
+ - Matcher details and strict mode settings
613
+ - Paginated response configuration
614
+ - File-based fixture loading
615
+
616
+ **Mock Interactions:**
445
617
 
446
618
  - Incoming commands and their inputs
447
619
  - Number of configured mocks for each command
448
620
  - Mock matching results and reasons for failures
449
621
  - One-time mock removal notifications
450
622
 
623
+ **Lifecycle Events:**
624
+
625
+ - Reset operations (clearing call history)
626
+ - Restore operations (removing all mocks)
627
+
451
628
  ## 🧪 Test Coverage
452
629
 
453
630
  The library includes comprehensive test suites covering all features:
@@ -513,6 +690,8 @@ test("should call DynamoDB", async () => {
513
690
 
514
691
  ## 📚 API Reference
515
692
 
693
+ > TypeScript documentation for this library can be found at [here](https://sudokar.github.io/aws-sdk-vitest-mock/)
694
+
516
695
  ### `mockClient<TClient>(ClientConstructor)`
517
696
 
518
697
  Creates a mock for an AWS SDK client constructor.
@@ -525,14 +704,18 @@ Mocks an existing AWS SDK client instance.
525
704
 
526
705
  **Returns:** `AwsClientStub<TClient>`
527
706
 
707
+ ### Global Debug Functions
708
+
709
+ - `setGlobalDebug(enabled: boolean)` - Enable or disable debug logging globally for all mocks
710
+
528
711
  ### `AwsClientStub` Methods
529
712
 
530
713
  - `on(Command, matcher?, options?)` - Configure mock for a command
531
- - `reset()` - Clear all mocks and call history
714
+ - `reset()` - Clear call history while preserving mock configurations
532
715
  - `restore()` - Restore original client behavior
533
716
  - `calls()` - Get call history
534
- - `enableDebug()` - Enable debug logging for troubleshooting
535
- - `disableDebug()` - Disable debug logging
717
+ - `enableDebug()` - Enable debug logging for troubleshooting (overrides global setting)
718
+ - `disableDebug()` - Disable debug logging (overrides global setting)
536
719
 
537
720
  ### `AwsCommandStub` Methods (Chainable)
538
721
 
package/index.cjs CHANGED
@@ -1 +1,18 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("vitest"),R=require("node:fs"),N=require("node:path"),C=require("node:stream"),W=require("./matchers-Rq18z2C7.cjs");class d extends Error{constructor(e,s,c,n){super(e),this.name="AwsError",this.code=s,this.statusCode=c,this.retryable=n}}const M=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)},T=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),F=()=>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 L(t){const e=N.resolve(t),s=R.readFileSync(e,"utf8");return t.endsWith(".json")?JSON.parse(s):s}function K(t,e={}){const{pageSize:s=10,tokenKey:c="NextToken",itemsKey:n="Items"}=e;if(t.length===0)return[{[n]:[]}];const o=[];for(let r=0;r<t.length;r+=s){const u=t.slice(r,r+s),i=r+s<t.length,l={[n]:u};if(i){const a=l;a[c]=`token-${r+s}`}o.push(l)}return o}function A(){return typeof process<"u"&&process.versions?.node?"node":typeof process<"u"&&process.versions?.bun?"bun":"browser"}function q(t){const e=typeof t=="string"?Buffer.from(t,"utf8"):Buffer.from(t);let s=!1;return new C.Readable({read(){s||(this.push(e),this.push(null),s=!0)}})}function B(t){let e;return typeof t=="string"?e=new TextEncoder().encode(t):t instanceof Buffer?e=new Uint8Array(t):e=t,new ReadableStream({start(s){s.enqueue(e),s.close()}})}function h(t){const e=A();return e==="node"||e==="bun"?q(t):B(t)}function w(t,e){return Object.keys(e).every(s=>{const c=e[s],n=t[s];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 s=Object.keys(t),c=Object.keys(e);return s.length!==c.length?!1:c.every(n=>{if(!Object.prototype.hasOwnProperty.call(t,n))return!1;const o=t,r=e,u=o[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 s=()=>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(o=>o.strict?o.matcher&&j(e.input,o.matcher):!o.matcher||w(e.input,o.matcher));if(n===-1)t.debugLogger.log(`No matching mock found for ${e.constructor.name}`,e.input);else{const o=c[n];if(!o)throw new Error(`Mock at index ${n} not found`);return t.debugLogger.log(`Using mock at index ${n} for ${e.constructor.name}`),o.once&&(c.splice(n,1),t.debugLogger.log(`Removed one-time mock for ${e.constructor.name}`)),o.handler(e.input,s())}}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,s,c={}){const n=(r,u)=>{const i={matcher:s,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(s));a.push(i),t.map.set(e,a)}},o={resolves(r){return n(()=>Promise.resolve(r),!1),o},rejects(r){return n(()=>{const u=typeof r=="string"?new Error(r):r;return Promise.reject(u)},!1),o},callsFake(r){return n(r,!1),o},resolvesOnce(r){return n(()=>Promise.resolve(r),!0),o},rejectsOnce(r){return n(()=>{const u=typeof r=="string"?new Error(r):r;return Promise.reject(u)},!0),o},callsFakeOnce(r){return n(r,!0),o},resolvesStream(r){return n(()=>Promise.resolve({Body:h(r)}),!1),o},resolvesStreamOnce(r){return n(()=>Promise.resolve({Body:h(r)}),!0),o},resolvesWithDelay(r,u){const i=l=>{setTimeout(()=>l(r),u)};return n(()=>new Promise(i),!1),o},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),o},rejectsWithNoSuchKey(r){return n(()=>Promise.reject(M(r)),!1),o},rejectsWithNoSuchBucket(r){return n(()=>Promise.reject(T(r)),!1),o},rejectsWithAccessDenied(r){return n(()=>Promise.reject($(r)),!1),o},rejectsWithResourceNotFound(r){return n(()=>Promise.reject(D(r)),!1),o},rejectsWithConditionalCheckFailed(){return n(()=>Promise.reject(I()),!1),o},rejectsWithThrottling(){return n(()=>Promise.reject(O()),!1),o},rejectsWithInternalServerError(){return n(()=>Promise.reject(F()),!1),o},resolvesPaginated(r,u={}){const i=K(r,u);let l=0;return n(a=>{const f=u.tokenKey||"NextToken",E=u.inputTokenKey||f,p=a[E];if(p){const g=/token-(\d+)/.exec(p);if(g&&g[1]){const P=g[1];l=Math.floor(Number.parseInt(P,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),o},resolvesFromFile(r){return n(()=>{const u=L(r);return Promise.resolve(u)},!1),o}};return o}const _=t=>{const e={map:new WeakMap,debugLogger:k()},s=t.prototype,c=b.vi.spyOn(s,"send").mockImplementation(S(e));return{client:void 0,on:(o,r,u)=>x(e,o,r,u),reset:()=>{c.mockClear(),e.map=new WeakMap},restore:()=>{c.mockRestore(),e.map=new WeakMap},calls:()=>c.mock.calls.map(o=>o[0]),__rawCalls:()=>c.mock.calls,enableDebug:()=>{y(e.debugLogger)},disableDebug:()=>{v(e.debugLogger)}}},V=t=>{const e={map:new WeakMap,debugLogger:k()},s=b.vi.spyOn(t,"send").mockImplementation(S(e));return{client:t,on:(n,o,r)=>x(e,n,o,r),reset:()=>{s.mockClear(),e.map=new WeakMap},restore:()=>{s.mockRestore(),e.map=new WeakMap},calls:()=>s.mock.calls.map(n=>n[0]),__rawCalls:()=>s.mock.calls,enableDebug:()=>{y(e.debugLogger)},disableDebug:()=>{v(e.debugLogger)}}};exports.matchers=W.matchers;exports.mockClient=_;exports.mockClientInstance=V;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const j=require("vitest"),D=require("./matchers-CNhdB_9q.cjs"),F=require("node:fs"),I=require("node:path"),M=require("node:stream");class d extends Error{constructor(t,o,i,n){super(t),this.name="AwsError",this.code=o,this.statusCode=i,this.retryable=n}}const T=e=>new d(e?`The specified key does not exist. Key: ${e}`:"The specified key does not exist.","NoSuchKey",404,!1),K=e=>new d(e?`The specified bucket does not exist. Bucket: ${e}`:"The specified bucket does not exist.","NoSuchBucket",404,!1),A=e=>new d(e?`Access Denied for resource: ${e}`:"Access Denied","AccessDenied",403,!1),B=e=>new d(e?`Requested resource not found: ${e}`:"Requested resource not found","ResourceNotFoundException",400,!1),q=()=>new d("The conditional request failed","ConditionalCheckFailedException",400,!1),J=()=>new d("Rate exceeded","Throttling",400,!0),_=()=>new d("We encountered an internal error. Please try again.","InternalServerError",500,!0),z=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]"}},v=(e,t)=>{const o=D.colors.magenta("aws-sdk-vitest-mock(debug):");if(t===void 0)console.log(`${o} ${e}`);else{const i=z(t);console.log(`${o} ${e}
2
+ ${i}`)}},L=(e=!1)=>({enabled:e,explicitlySet:!1,log(t,o){this.enabled&&v(t,o)},logDirect(t,o){v(t,o)}}),w=e=>{e.enabled=!0,e.explicitlySet=!0},$=e=>{e.enabled=!1,e.explicitlySet=!0},V=e=>{const t=I.resolve(e),o=F.readFileSync(t,"utf8");return e.endsWith(".json")?JSON.parse(o):o},G=(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),l=r+o<e.length,u={[n]:c};if(l){const a=u,g=c[c.length-1];a[i]=g}s.push(u)}return s},U=()=>typeof process<"u"&&process.versions?.node?"node":typeof process<"u"&&process.versions?.bun?"bun":"browser",H=e=>{const t=typeof e=="string"?Buffer.from(e,"utf8"):Buffer.from(e);let o=!1;return new M.Readable({read(){o||(this.push(t),this.push(null),o=!0)}})},Q=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()}})},S=e=>{const t=U();return t==="node"||t==="bun"?H(e):Q(e)};let N=!1;function X(e){N=e}function h(e){return e.explicitlySet?e.enabled:N}function C(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:C(n,i):n===i})}function E(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],l=r[n];return typeof c=="object"&&c!==null&&typeof l=="object"&&l!==null?E(c,l):c===l})}function Y(e,t,o){const i=t.map((s,r)=>{const c=s.matcher?JSON.stringify(s.matcher,void 0,2):"any input",l=s.strict?" (strict mode)":"";return` Mock #${r+1}: ${c}${l}`}).join(`
3
+ `),n=JSON.stringify(o,void 0,2);return new Error(`No matching mock found for ${e}.
4
+
5
+ Found ${t.length} mock(s) but none matched the input.
6
+
7
+ Configured mocks:
8
+ ${i}
9
+
10
+ Received input:
11
+ ${n}
12
+
13
+ Tip: Enable debug mode with enableDebug() for detailed matching information.`)}function Z(e,t){const o=JSON.stringify(t,void 0,2);return new Error(`No mock configured for command: ${e}.
14
+
15
+ Received input:
16
+ ${o}
17
+
18
+ Did you forget to call mockClient.on(${e})?`)}function ee(e,t){return e.findIndex(o=>o.strict?o.matcher&&E(t,o.matcher):!o.matcher||C(t,o.matcher))}function x(e){return async function(t){const o=()=>this,i=h(e.debugLogger),n=t.constructor.name;i&&e.debugLogger.logDirect(`Received command: ${n}`,t.input);const s=e.map.get(t.constructor);if(!s)throw i&&e.debugLogger.logDirect(`No mocks configured for ${n}`),Z(n,t.input);i&&e.debugLogger.logDirect(`Found ${s.length} mock(s) for ${n}`);const r=ee(s,t.input);if(r===-1)throw i&&e.debugLogger.logDirect(`No matching mock found for ${n}`,t.input),Y(n,s,t.input);const c=s[r];if(!c)throw new Error(`Mock at index ${r} not found`);return i&&e.debugLogger.logDirect(`Using mock at index ${r} for ${n}`),c.once&&(s.splice(r,1),i&&e.debugLogger.logDirect(`Removed one-time mock for ${n}`)),c.handler(t.input,o())}}function R(e,t,o,i={}){const n=(r,c,l)=>{const u={matcher:o,handler:r,once:c,strict:!!i.strict},a=e.map.get(t)??[],g=h(e.debugLogger);if(c){const f=a.findIndex(p=>!p.once);f===-1?a.push(u):a.splice(f,0,u),e.map.set(t,a),g&&e.debugLogger.logDirect(`Configured ${l}Once for ${t.name}`,o?{matcher:o,strict:!!i.strict}:void 0)}else{const f=a.filter(p=>p.once||JSON.stringify(p.matcher)!==JSON.stringify(o));f.push(u),e.map.set(t,f),g&&e.debugLogger.logDirect(`Configured ${l} 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:S(r)}),!1,"resolvesStream"),s},resolvesStreamOnce(r){return n(()=>Promise.resolve({Body:S(r)}),!0,"resolvesStream"),s},resolvesWithDelay(r,c){const l=u=>{setTimeout(()=>u(r),c)};return n(()=>new Promise(l),!1,"resolvesWithDelay"),s},rejectsWithDelay(r,c){const l=typeof r=="string"?new Error(r):r,u=(a,g)=>{setTimeout(()=>g(l),c)};return n(()=>new Promise(u),!1,"rejectsWithDelay"),s},rejectsWithNoSuchKey(r){return n(()=>Promise.reject(T(r)),!1,"rejectsWithNoSuchKey"),s},rejectsWithNoSuchBucket(r){return n(()=>Promise.reject(K(r)),!1,"rejectsWithNoSuchBucket"),s},rejectsWithAccessDenied(r){return n(()=>Promise.reject(A(r)),!1,"rejectsWithAccessDenied"),s},rejectsWithResourceNotFound(r){return n(()=>Promise.reject(B(r)),!1,"rejectsWithResourceNotFound"),s},rejectsWithConditionalCheckFailed(){return n(()=>Promise.reject(q()),!1,"rejectsWithConditionalCheckFailed"),s},rejectsWithThrottling(){return n(()=>Promise.reject(J()),!1,"rejectsWithThrottling"),s},rejectsWithInternalServerError(){return n(()=>Promise.reject(_()),!1,"rejectsWithInternalServerError"),s},resolvesPaginated(r,c={}){const l=G(r,c);let u=0;return e.debugLogger.log(`Configured resolvesPaginated for ${t.name}`,{pageSize:c.pageSize,itemsCount:r.length}),n(a=>{const g=c.tokenKey||"NextToken",f=c.inputTokenKey||g,m=a[f];if(m!=null){const W=c.itemsKey||"Items";let k=0;for(const P of l){const b=P[W];if(b&&b.length>0){const O=b[b.length-1];if(JSON.stringify(O)===JSON.stringify(m)){u=k+1;break}}k++}}else u=0;const y=l[u]||l[l.length-1]||l[0];if(!y)throw new Error("No paginated responses available");return u=Math.min(u+1,l.length-1),Promise.resolve(y)},!1,"resolvesPaginated"),s},resolvesFromFile(r){return e.debugLogger.log(`Configured resolvesFromFile for ${t.name}`,{filePath:r}),n(()=>{const c=V(r);return Promise.resolve(c)},!1,"resolvesFromFile"),s}};return s}const te=e=>{const t={map:new WeakMap,debugLogger:L()},o=e.prototype,i=j.vi.spyOn(o,"send").mockImplementation(x(t));return{client:void 0,on:(s,r,c)=>R(t,s,r,c),reset:()=>{h(t.debugLogger)&&t.debugLogger.logDirect("Clearing call history (mocks preserved)"),i.mockClear()},restore:()=>{h(t.debugLogger)&&t.debugLogger.logDirect("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:()=>{w(t.debugLogger)},disableDebug:()=>{$(t.debugLogger)}}},re=e=>{const t={map:new WeakMap,debugLogger:L()},o=j.vi.spyOn(e,"send").mockImplementation(x(t));return{client:e,on:(n,s,r)=>R(t,n,s,r),reset:()=>{h(t.debugLogger)&&t.debugLogger.logDirect("Clearing call history (mocks preserved) for client instance"),o.mockClear()},restore:()=>{h(t.debugLogger)&&t.debugLogger.logDirect("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:()=>{w(t.debugLogger)},disableDebug:()=>{$(t.debugLogger)}}};exports.matchers=D.matchers;exports.mockClient=te;exports.mockClientInstance=re;exports.setGlobalDebug=X;
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, setGlobalDebug, } 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';