aws-sdk-vitest-mock 0.4.0 → 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
@@ -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';
375
+
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
+ ];
322
382
 
323
- dynamoMock.on(ScanCommand).resolvesPaginated(items, {
324
- pageSize: 10,
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", ... }));
398
+
399
+ // Unmarshall the items
400
+ const page1Users = page1.Items.map(item => unmarshall(item));
401
+ console.log(page1Users[0]); // { id: "user-1", name: "Alice", ... }
333
402
 
334
- // Second call with ExclusiveStartKey returns items 11-20
335
- const result2 = await client.send(
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
+ ```
420
+
421
+ #### S3 Pagination
422
+
423
+ ```typescript
424
+ import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
351
425
 
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) => ({
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,62 @@ 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
+ 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:**
445
577
 
446
578
  - Incoming commands and their inputs
447
579
  - Number of configured mocks for each command
448
580
  - Mock matching results and reasons for failures
449
581
  - One-time mock removal notifications
450
582
 
583
+ **Lifecycle Events:**
584
+
585
+ - Reset operations (clearing call history)
586
+ - Restore operations (removing all mocks)
587
+
451
588
  ## 🧪 Test Coverage
452
589
 
453
590
  The library includes comprehensive test suites covering all features:
@@ -528,7 +665,7 @@ Mocks an existing AWS SDK client instance.
528
665
  ### `AwsClientStub` Methods
529
666
 
530
667
  - `on(Command, matcher?, options?)` - Configure mock for a command
531
- - `reset()` - Clear all mocks and call history
668
+ - `reset()` - Clear call history while preserving mock configurations
532
669
  - `restore()` - Restore original client behavior
533
670
  - `calls()` - Get call history
534
671
  - `enableDebug()` - Enable debug logging for troubleshooting
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 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';