From 8270bb470408da27f5b1634bcd7bb65aa4770f0e Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Tue, 4 Aug 2026 13:17:21 +0200 Subject: [PATCH] Replace nock with msw request handlers The mock no longer owns an msw server: a second setupServer() instance silently breaks the one owned by the test suite, so instead the handlers are exposed through getHandlers()/registerHandlers() and registered on the server the test suite already has. msw becomes a peer dependency. Table lifecycle actions now wait for the table to leave CREATING / UPDATING / DELETING before responding. dynalite schedules that transition in a timer after its response callback, which was hidden by nock's slower response path but not by msw's. --- .changeset/msw-request-handlers.md | 25 ++ README.md | 55 ++++- package.json | 6 +- pnpm-lock.yaml | 374 ++++++++++++++++++++++++++--- src/index.test.ts | 75 +++++- src/index.ts | 133 +++++++--- types.d.ts | 6 + 7 files changed, 602 insertions(+), 72 deletions(-) create mode 100644 .changeset/msw-request-handlers.md diff --git a/.changeset/msw-request-handlers.md b/.changeset/msw-request-handlers.md new file mode 100644 index 0000000..48fb481 --- /dev/null +++ b/.changeset/msw-request-handlers.md @@ -0,0 +1,25 @@ +--- +"@labdigital/mock-dynamodb": minor +--- + +Replace nock with msw request handlers (breaking) + +This mock no longer runs an interceptor of its own. Instead it exposes msw +request handlers that are registered on the msw server owned by your test +suite, which avoids two libraries fighting over request interception. `msw` is +a peer dependency now. + +```diff + const mddb = mockDynamoDB({ endpoint: "http://localhost:4000" }); ++const server = setupServer(...mddb.getHandlers()); ++server.listen(); + + mddb.reset(); +-mddb.stop(); ++server.close(); +``` + +`start()` and `stop()` throw an error pointing at the new setup, they are +removed in 1.0. Table lifecycle actions now wait for the table to reach its +settled state before responding, so a table can be written to immediately +after `createTable()` resolves. diff --git a/README.md b/README.md index 253e328..b325f42 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ # DynamoDB Mock This is a relatively simple wrapper around [dynalite](https://github.com/mhart/dynalite) to make it easier to use in a test -environment. +environment. Requests are intercepted with [msw](https://mswjs.io/), so no +actual server or port is used. [![npm](https://img.shields.io/npm/v/@labdigital/mock-dynamodb.svg)](https://www.npmjs.com/package/@labdigital/mock-dynamodb) ## Usage -```typescript +This package provides msw request handlers, the msw server itself is managed by +your own test setup. Pass the handlers to `setupServer()` so that they survive +`server.resetHandlers()`: +```typescript import { mockDynamoDB } from "@labdigital/mock-dynamodb"; +import { setupServer } from "msw/node"; +import { afterAll, afterEach, beforeAll } from "vitest"; + +export const mddb = mockDynamoDB({ endpoint: "http://localhost:4000" }); +export const server = setupServer(...mddb.getHandlers()); -const mddb = mockDynamoDB({ endpoint: "http://localhost:4000" }); const client = new DynamoDB({ endpoint: "http://localhost:4000", region: "local", @@ -20,7 +28,44 @@ const client = new DynamoDB({ }, }); -mddb.reset(); // Clear all data +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterEach(() => mddb.reset()); // Clear all data +afterAll(() => server.close()); +``` + +Every `accessKeyId` gets its own store, so tests using different credentials +don't see each other's data. `reset()` clears the data of all of them. + +If you need to bind to a server that is already running, use +`registerHandlers()` instead. Note that those handlers are removed again by +`server.resetHandlers()`, so they have to be registered per test: + +```typescript +beforeEach(() => mddb.registerHandlers(server)); +``` + +### Combining with other mocks +Use a single msw server for all of your mocks, running more than one server per +process is not supported by msw: + +```typescript +const server = setupServer(...ctMock.getHandlers(), ...mddb.getHandlers()); +``` + +## Migrating from 0.2.x +Version 0.3.0 replaced [nock](https://github.com/nock/nock) with msw, which +means this package no longer starts an interceptor of its own. `msw` is a peer +dependency now and has to be installed alongside it. -mddb.stop(); // Stop the server +```diff + const mddb = mockDynamoDB({ endpoint: "http://localhost:4000" }); ++const server = setupServer(...mddb.getHandlers()); ++server.listen(); + + mddb.reset(); +-mddb.stop(); ++server.close(); ``` + +`start()` and `stop()` throw an error pointing at the above, they are removed +in 1.0. diff --git a/package.json b/package.json index 18ed637..695469b 100644 --- a/package.json +++ b/package.json @@ -38,16 +38,18 @@ "test:ci": "pnpm vitest run --coverage", "tsc": "tsc" }, - "dependencies": { - "nock": "14.0.17" + "peerDependencies": { + "msw": "^2.0.0" }, "devDependencies": { "@aws-sdk/client-dynamodb": "3.1101.0", "@biomejs/biome": "2.5.6", "@changesets/cli": "2.31.1", "@types/express": "5.0.6", + "@types/node": "^26.1.2", "@vitest/coverage-v8": "4.1.10", "dynalite": "4.0.0", + "msw": "2.15.0", "tsdown": "0.22.14", "tsx": "4.23.5", "typescript": "7.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8cc93c..aa1c8a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,6 @@ settings: importers: .: - dependencies: - nock: - specifier: 14.0.17 - version: 14.0.17 devDependencies: '@aws-sdk/client-dynamodb': specifier: 3.1101.0 @@ -24,12 +20,18 @@ importers: '@types/express': specifier: 5.0.6 version: 5.0.6 + '@types/node': + specifier: ^26.1.2 + version: 26.1.2 '@vitest/coverage-v8': specifier: 4.1.10 version: 4.1.10(vitest@4.1.10) dynalite: specifier: 4.0.0 version: 4.0.0 + msw: + specifier: 2.15.0 + version: 2.15.0(@types/node@26.1.2)(typescript@7.0.2) tsdown: specifier: 0.22.14 version: 0.22.14(tsx@4.23.5)(typescript@7.0.2)(unrun@0.2.39) @@ -41,7 +43,7 @@ importers: version: 7.0.2 vitest: specifier: 4.1.10 - version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)) + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@26.1.2)(typescript@7.0.2))(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)) packages: @@ -579,6 +581,28 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -588,6 +612,19 @@ packages: '@types/node': optional: true + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -637,6 +674,9 @@ packages: '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + '@open-draft/logger@0.3.0': resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} @@ -1053,6 +1093,12 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -1358,6 +1404,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansis@4.3.1: resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} @@ -1423,9 +1473,28 @@ packages: resolution: {integrity: sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ==} engines: {node: '>=18'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1455,6 +1524,9 @@ packages: engines: {node: '>=20'} hasBin: true + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.1: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} @@ -1476,6 +1548,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1495,6 +1571,15 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1531,6 +1616,10 @@ packages: functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-tsconfig@5.0.0-beta.5: resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} engines: {node: '>=20.20.0'} @@ -1546,10 +1635,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} @@ -1583,6 +1679,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1632,9 +1732,6 @@ packages: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1704,6 +1801,20 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + msw@2.15.0: + resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + nanoid@3.3.17: resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1712,10 +1823,6 @@ packages: napi-macros@2.2.2: resolution: {integrity: sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==} - nock@14.0.17: - resolution: {integrity: sha512-EjRr1weMa4ALQX35AgZTEnP+weJJjlW1KGDiNM2IQC2069YDHas4f4B4UUYR+TTLyKWxJvOz2wObDKQs/LNreA==} - engines: {node: '>=18.20.0 <20 || >=20.12.1'} - node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -1767,6 +1874,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1798,10 +1908,6 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - propagate@2.0.1: - resolution: {integrity: sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==} - engines: {node: '>= 8'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -1815,6 +1921,10 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1822,6 +1932,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1871,6 +1984,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@3.1.2: + resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1903,12 +2019,20 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.2.0: resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1921,6 +2045,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -1940,10 +2068,21 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + + tldts@7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1990,6 +2129,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} @@ -2015,6 +2158,9 @@ packages: synckit: optional: true + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + verkit@0.3.1: resolution: {integrity: sha512-w2Eo8LSIIoW7qxNBzT7/17k+bh8plXo7G3dHjEIDqPlnluhzaxr9JX8F28VSYEtDvc1/a3WBDih6xNUZseebXg==} engines: {node: '>=18.12.0'} @@ -2110,9 +2256,25 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yuku-ast@0.8.3: resolution: {integrity: sha512-8x34yU5uhHUnJXzy2Qvjvec/vE9BzS0/2khVT1MsLmSLO/P8Q1Wp8IxHv+IhD+HMYETk6kherOSvP4JPWw2joQ==} @@ -2658,6 +2820,27 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.1.1(@types/node@26.1.2)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@26.1.2) + '@inquirer/type': 4.0.7(@types/node@26.1.2) + optionalDependencies: + '@types/node': 26.1.2 + + '@inquirer/core@11.2.1(@types/node@26.1.2)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@26.1.2) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 26.1.2 + '@inquirer/external-editor@1.0.3(@types/node@26.1.2)': dependencies: chardet: 2.2.0 @@ -2665,6 +2848,12 @@ snapshots: optionalDependencies: '@types/node': 26.1.2 + '@inquirer/figures@2.0.7': {} + + '@inquirer/type@4.0.7(@types/node@26.1.2)': + optionalDependencies: + '@types/node': 26.1.2 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2723,6 +2912,8 @@ snapshots: '@open-draft/deferred-promise@2.2.0': {} + '@open-draft/deferred-promise@3.0.0': {} + '@open-draft/logger@0.3.0': dependencies: is-node-process: 1.2.0 @@ -3002,6 +3193,12 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 26.1.2 + '@types/set-cookie-parser@2.4.10': + dependencies: + '@types/node': 26.1.2 + + '@types/statuses@2.0.6': {} + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -3074,7 +3271,7 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@26.1.2)(typescript@7.0.2))(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)) '@vitest/expect@4.1.10': dependencies: @@ -3085,12 +3282,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5))': + '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.1.2)(typescript@7.0.2))(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.15.0(@types/node@26.1.2)(typescript@7.0.2) vite: 7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5) '@vitest/pretty-format@4.1.10': @@ -3204,6 +3402,10 @@ snapshots: ansi-regex@5.0.1: {} + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansis@4.3.1: {} argparse@1.0.10: @@ -3262,8 +3464,24 @@ snapshots: napi-macros: 2.2.2 node-gyp-build: 4.8.4 + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3292,6 +3510,8 @@ snapshots: minimist: 1.2.8 once: 1.4.0 + emoji-regex@8.0.0: {} + empathic@2.0.1: {} enquirer@2.4.1: @@ -3359,6 +3579,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} + esprima@4.0.1: {} estree-walker@3.0.3: @@ -3377,6 +3599,16 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -3411,6 +3643,8 @@ snapshots: functional-red-black-tree@1.0.1: {} + get-caller-file@2.0.5: {} + get-tsconfig@5.0.0-beta.5: dependencies: resolve-pkg-maps: 1.0.0 @@ -3430,8 +3664,15 @@ snapshots: graceful-fs@4.2.11: {} + graphql@16.14.2: {} + has-flag@4.0.0: {} + headers-polyfill@5.0.1: + dependencies: + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.2 + hookable@6.1.1: {} html-escaper@2.0.2: {} @@ -3452,6 +3693,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3495,8 +3738,6 @@ snapshots: dependencies: argparse: 2.0.1 - json-stringify-safe@5.0.1: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -3563,16 +3804,37 @@ snapshots: mri@1.2.0: {} + msw@2.15.0(@types/node@26.1.2)(typescript@7.0.2): + dependencies: + '@inquirer/confirm': 6.1.1(@types/node@26.1.2) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.2 + type-fest: 5.8.0 + until-async: 3.0.2 + yargs: 17.7.3 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@types/node' + + mute-stream@3.0.0: {} + nanoid@3.3.17: {} napi-macros@2.2.2: {} - nock@14.0.17: - dependencies: - '@mswjs/interceptors': 0.41.9 - json-stringify-safe: 5.0.1 - propagate: 2.0.1 - node-gyp-build@4.8.4: {} obliterator@1.6.1: {} @@ -3611,6 +3873,8 @@ snapshots: path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -3631,8 +3895,6 @@ snapshots: prettier@2.8.8: {} - propagate@2.0.1: {} - quansync@0.2.11: {} quansync@1.0.0: {} @@ -3646,10 +3908,14 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + require-directory@2.1.1: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + rettime@0.11.11: {} + reusify@1.1.0: {} rolldown-plugin-dts@0.27.14(rolldown@1.2.2)(typescript@7.0.2): @@ -3748,6 +4014,8 @@ snapshots: semver@7.8.5: {} + set-cookie-parser@3.1.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3771,10 +4039,18 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@4.2.0: {} strict-event-emitter@0.5.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3785,6 +4061,8 @@ snapshots: dependencies: has-flag: 4.0.0 + tagged-tag@1.0.0: {} + term-size@2.2.1: {} tinybench@2.9.0: {} @@ -3798,10 +4076,20 @@ snapshots: tinyrainbow@3.1.1: {} + tldts-core@7.4.10: {} + + tldts@7.4.10: + dependencies: + tldts-core: 7.4.10 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.10 + tree-kill@1.2.2: {} tsdown@0.22.14(tsx@4.23.5)(typescript@7.0.2)(unrun@0.2.39): @@ -3839,6 +4127,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-fest@5.8.0: + dependencies: + tagged-tag: 1.0.0 + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -3876,6 +4168,8 @@ snapshots: rolldown: 1.0.0-rc.17 optional: true + until-async@3.0.2: {} + verkit@0.3.1: {} vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5): @@ -3892,10 +4186,10 @@ snapshots: jiti: 2.6.1 tsx: 4.23.5 - vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)): + vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@26.1.2)(typescript@7.0.2))(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.2)(typescript@7.0.2))(vite@7.3.2(@types/node@26.1.2)(jiti@2.6.1)(tsx@4.23.5)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -3929,8 +4223,28 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrappy@1.0.2: {} + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yuku-ast@0.8.3: dependencies: '@yuku-toolchain/types': 0.8.3 diff --git a/src/index.test.ts b/src/index.test.ts index 3d443dd..f4fcacf 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,8 +1,11 @@ import { DynamoDB } from "@aws-sdk/client-dynamodb"; -import { beforeEach, expect, test } from "vitest"; +import { setupServer } from "msw/node"; +import { afterAll, afterEach, beforeAll, expect, test } from "vitest"; import { mockDynamoDB } from "./index.js"; const mddb = mockDynamoDB({ endpoint: "http://localhost:4000" }); +const server = setupServer(...mddb.getHandlers()); + const client = new DynamoDB({ endpoint: "http://localhost:4000", region: "local", @@ -12,10 +15,20 @@ const client = new DynamoDB({ }, }); -beforeEach(() => { +beforeAll(() => { + server.listen({ onUnhandledRequest: "error" }); +}); + +afterEach(() => { + // Handlers passed to setupServer() survive resetHandlers() + server.resetHandlers(); mddb.reset(); }); +afterAll(() => { + server.close(); +}); + test("createTables", async () => { await client.createTable({ TableName: "Music", @@ -87,3 +100,61 @@ test("getItem not found", async () => { }), ).rejects.toThrow("Requested resource not found"); }); + +test("writes to a table right after creating it", async () => { + await client.createTable({ + TableName: "Music", + KeySchema: [{ AttributeName: "Artist", KeyType: "HASH" }], + AttributeDefinitions: [{ AttributeName: "Artist", AttributeType: "S" }], + BillingMode: "PAY_PER_REQUEST", + }); + + // No waitUntilTableExists() in between, the table has to be usable as soon + // as createTable() resolves + await client.putItem({ + TableName: "Music", + Item: { Artist: { S: "No One You Know" } }, + }); + + const item = await client.getItem({ + TableName: "Music", + Key: { Artist: { S: "No One You Know" } }, + }); + expect(item.Item).toEqual({ Artist: { S: "No One You Know" } }); +}); + +test("keeps a separate store per accessKeyId", async () => { + const otherClient = new DynamoDB({ + endpoint: "http://localhost:4000", + region: "local", + credentials: { accessKeyId: "fake-y", secretAccessKey: "fake" }, + }); + + await client.createTable({ + TableName: "Music", + KeySchema: [{ AttributeName: "Artist", KeyType: "HASH" }], + AttributeDefinitions: [{ AttributeName: "Artist", AttributeType: "S" }], + BillingMode: "PAY_PER_REQUEST", + }); + + expect((await client.listTables({})).TableNames).toEqual(["Music"]); + expect((await otherClient.listTables({})).TableNames).toEqual([]); +}); + +test("registerHandlers binds to an already running server", async () => { + const other = mockDynamoDB({ endpoint: "http://localhost:4001" }); + other.registerHandlers(server); + + const otherClient = new DynamoDB({ + endpoint: "http://localhost:4001", + region: "local", + credentials: { accessKeyId: "fake-x", secretAccessKey: "fake" }, + }); + + expect((await otherClient.listTables({})).TableNames).toEqual([]); +}); + +test("start and stop point at the msw setup", () => { + expect(() => mddb.start()).toThrow("setupServer(...mock.getHandlers())"); + expect(() => mddb.stop()).toThrow("close your own msw server"); +}); diff --git a/src/index.ts b/src/index.ts index 0d2115d..7829aac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import db, { type Store } from "dynalite/db"; import validations from "dynalite/validations"; -import nock, { type ReplyFnContext } from "nock"; +import { HttpResponse, http, type RequestHandler } from "msw"; +import type { SetupServer } from "msw/node"; import { type ActionType, actions } from "./actions.js"; import { parseAuthHeader } from "./auth.js"; import { actionValidations } from "./validations.js"; @@ -10,14 +11,49 @@ type Response = { body: any; }; +// dynalite responds to these before the table reaches its settled state: the +// CREATING/UPDATING/DELETING -> ACTIVE/deleted transition happens in a +// setTimeout that is scheduled after the response callback has already fired. +// Requests are dispatched in-process here, so the next one can arrive before +// that timer runs, making a table created a statement earlier look like it +// doesn't exist yet. Wait for the transition before responding. +const tableActions = new Set([ + "CreateTable", + "DeleteTable", + "UpdateTable", +]); + +const settleTable = async (store: Store, tableName: string) => { + // Bounded so a table that never settles fails as a test timeout on the + // caller's side rather than hanging here forever + for (let i = 0; i < 1000; i++) { + const status = await new Promise((resolve) => { + store.tableDb.get(tableName, (_err, table) => + resolve(table?.TableStatus), + ); + }); + + // Deleted tables resolve to undefined, which is settled as well + if ( + status !== "CREATING" && + status !== "UPDATING" && + status !== "DELETING" + ) { + return; + } + + await new Promise((resolve) => setImmediate(resolve)); + } +}; + const handler = async ( - req: ReplyFnContext["req"], + request: Request, body: string, store: Store, ): Promise => { - const target = (req.headers["x-amz-target"] || "").split("."); + const target = (request.headers.get("x-amz-target") || "").split("."); const action = target[1] as ActionType; - let data = JSON.parse(body as string); + let data = JSON.parse(body); const actionValidation = actionValidations[action]; try { @@ -48,10 +84,15 @@ const handler = async ( }); return await p - .then((data) => ({ - statusCode: 200, - body: data, - })) + .then(async (result) => { + if (tableActions.has(action) && data.TableName) { + await settleTable(store, data.TableName); + } + return { + statusCode: 200, + body: result, + }; + }) .catch((err) => { if (err.statusCode) { return { @@ -63,14 +104,14 @@ const handler = async ( }); }; -type Options = { +export type Options = { + /** Endpoint the DynamoDB client is configured with */ endpoint: string; }; export class MockDynamoDB { private stores: Record = {}; private options: Options; - private isStarted = false; constructor(options: Options) { this.options = options; @@ -87,21 +128,35 @@ export class MockDynamoDB { return this.stores[accessKeyId]; } - start() { - const self = this; - if (this.isStarted) { - return; - } - this.isStarted = true; - nock(this.options.endpoint) - .persist() - .post("/") - .reply(async function (_uri, body) { - const auth = parseAuthHeader(this.req.headers.authorization); - const store = self.getStore(auth.credentials.accessKeyId); - const data = await handler(this.req, body as string, store); - return [data.statusCode, data.body]; - }); + /** + * The msw request handlers for this mock. Pass these to setupServer() so + * they survive server.resetHandlers(): + * + * const server = setupServer(...mock.getHandlers()); + */ + getHandlers(): RequestHandler[] { + // DynamoDB sends every operation as a POST to the root path + const url = new URL("/", this.options.endpoint).toString(); + + return [ + http.post(url, async ({ request }) => { + const auth = parseAuthHeader( + request.headers.get("authorization") ?? "", + ); + const store = this.getStore(auth.credentials.accessKeyId); + const data = await handler(request, await request.text(), store); + return HttpResponse.json(data.body, { status: data.statusCode }); + }), + ]; + } + + /** + * Register the handlers on an already running msw server. Note that these + * are removed again by server.resetHandlers(), prefer passing + * getHandlers() to setupServer() instead. + */ + registerHandlers(server: SetupServer) { + server.use(...this.getHandlers()); } reset() { @@ -110,14 +165,26 @@ export class MockDynamoDB { } } - stop() { - nock.cleanAll(); - this.isStarted = false; + /** + * @deprecated This mock no longer runs its own msw server, since a second + * server breaks the one owned by your test suite. Register the handlers + * instead: setupServer(...mock.getHandlers()) + */ + start(): never { + throw new Error( + "MockDynamoDB.start() is removed, pass the handlers to your own msw " + + "server instead: setupServer(...mock.getHandlers())", + ); + } + + /** + * @deprecated Close your own msw server instead, see start(). + */ + stop(): never { + throw new Error( + "MockDynamoDB.stop() is removed, close your own msw server instead", + ); } } -export const mockDynamoDB = (options: Options) => { - const m = new MockDynamoDB(options); - m.start(); - return m; -}; +export const mockDynamoDB = (options: Options) => new MockDynamoDB(options); diff --git a/types.d.ts b/types.d.ts index f34b4ca..9b5567d 100644 --- a/types.d.ts +++ b/types.d.ts @@ -2,6 +2,12 @@ declare module "dynalite/db" { export type Store = { recreate(): void + tableDb: { + get( + key: string, + cb: (err: any, table?: { TableStatus?: string }) => void + ): void + } } export default { create(options: any): Store