From fa29e1b1206c18653a5f5897e4f9b25170a1c9d1 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 18:47:20 +0300 Subject: [PATCH] chore: migrate to msw@2 (#38) Co-authored-by: Weyert de Boer --- .eslintrc.js | 14 - CONTRIBUTING.md | 34 - package.json | 17 +- pnpm-lock.yaml | 1022 ++--------------- source-logo.png | Bin 13306 -> 0 bytes src/fromOpenApi/fromOpenApi.ts | 126 +- .../response/createResponseResolver.ts | 61 - .../response/transformers/bodyTransformer.ts | 62 - .../transformers/headersTransformer.ts | 34 - src/fromOpenApi/schema/evolve.ts | 1 + src/fromOpenApi/schema/types/string.ts | 3 +- src/fromOpenApi/utils/getServers.ts | 4 +- src/fromOpenApi/utils/openApiUtils.ts | 249 ++++ src/fromOpenApi/utils/toBase64.ts | 5 - src/fromTraffic/fromTraffic.ts | 212 +--- .../utils/__tests__/base64strings.test.ts | 21 + src/fromTraffic/utils/decodeBase64String.ts | 11 +- src/fromTraffic/utils/encodeBase64String.ts | 6 + src/fromTraffic/utils/fromByteArray.ts | 4 + src/fromTraffic/utils/harUtils.test.ts | 78 ++ src/fromTraffic/utils/harUtils.ts | 46 + test/oas/oas-json-schema.test.ts | 195 ++-- test/oas/oas-response-headers.test.ts | 41 +- test/oas/oas-response.test.ts | 62 +- test/oas/oas-servers.test.ts | 130 ++- test/oas/petstore.test.ts | 147 ++- test/support/createOpenApiSpec.ts | 2 +- test/support/inspectHandler.ts | 98 ++ test/support/withHandlers.ts | 7 +- test/traffic/fromTraffic.test.ts | 41 +- test/traffic/response-body.test.ts | 166 ++- test/traffic/response-cookies.test.ts | 22 + test/traffic/response-order.test.ts | 60 +- test/traffic/response-stream.test.ts | 31 +- test/traffic/utils/index.ts | 39 +- tsconfig.build.json | 2 +- tsconfig.test.json | 2 +- vitest.config.ts | 1 + vitest.d.ts | 14 + vitest.setup.ts | 88 ++ 40 files changed, 1416 insertions(+), 1742 deletions(-) delete mode 100644 .eslintrc.js delete mode 100644 CONTRIBUTING.md delete mode 100644 source-logo.png delete mode 100644 src/fromOpenApi/response/createResponseResolver.ts delete mode 100644 src/fromOpenApi/response/transformers/bodyTransformer.ts delete mode 100644 src/fromOpenApi/response/transformers/headersTransformer.ts create mode 100644 src/fromOpenApi/utils/openApiUtils.ts delete mode 100644 src/fromOpenApi/utils/toBase64.ts create mode 100644 src/fromTraffic/utils/__tests__/base64strings.test.ts create mode 100644 src/fromTraffic/utils/encodeBase64String.ts create mode 100644 src/fromTraffic/utils/fromByteArray.ts create mode 100644 src/fromTraffic/utils/harUtils.test.ts create mode 100644 src/fromTraffic/utils/harUtils.ts create mode 100644 test/support/inspectHandler.ts create mode 100644 test/traffic/response-cookies.test.ts create mode 100644 vitest.d.ts create mode 100644 vitest.setup.ts diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 2bcfa15..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - 'prettier', - ], - rules: { - '@typescript-eslint/no-var-requires': 'off', - }, -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e2c1a71..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,34 +0,0 @@ -## Generating HAR fixtures - -### Why not automate? - -There are no reliable automation tools to generate HAR files from traffic. The discrepancies introduces by those tools alter the identity of the generated HAR files, which means the same traffic results in a different HAR file when exported from the browser manually. - -To retain the most confidence, this library uses a manual approach to HAR fixtures. - -### 1. Create a usage scenario. - -Usage scenario consists of two parts: - -1. A temporary HTTP server responsible for responses. -1. A list of client-side requests issued by the automated browser. - -```sh -$ touch test/traffic/fixtures/request/.ts -``` - -> Reference the existing scenarios for more information. - -### 2. Export HAR file from the browser - -Run the usage example in the browser: - -```sh -$ pnpm har:fixture test/traffic/fixtures/request/.ts -``` - -Export the Network log as the `*.har` file using the browser's Dev Tools. - -### 3. Add and commit the HAR file - -Add the exported `*.har` file to `test/traffic/fixtures/archives` and commit the changes. diff --git a/package.json b/package.json index 94eb78e..b16b89c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "author": "Artem Zakharchenko ", "scripts": { "start": "pnpm build --watch", - "lint": "eslint ./{src,test}/**/*.ts", "har:fixture": "ts-node ./test/traffic/fixtures/requests/command.ts", "prepare": "pnpm simple-git-hooks init", "prebuild": "pnpm lint", @@ -29,8 +28,7 @@ }, "lint-staged": { "*.ts": [ - "prettier --write", - "eslint --fix" + "prettier --write" ] }, "simple-git-hooks": { @@ -42,7 +40,7 @@ }, "packageManager": "pnpm@8.15.6", "peerDependencies": { - "msw": "^1.2.1" + "msw": "^2.3.0" }, "devDependencies": { "@commitlint/cli": "^17.6.5", @@ -50,15 +48,10 @@ "@open-draft/test-server": "^0.5.1", "@types/compression": "^1.7.1", "@types/node": "18", - "@typescript-eslint/eslint-plugin": "^7.8.0", - "@typescript-eslint/parser": "^7.8.0", "compression": "^1.7.4", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3", "jsdom": "^22.1.0", "lint-staged": "^10.5.3", - "msw": "^1.2.1", + "msw": "^2.3.0", "playwright": "^1.34.3", "prettier": "^3.2.5", "rimraf": "^5.0.5", @@ -73,11 +66,9 @@ "@apidevtools/swagger-parser": "^10.0.2", "@types/faker": "^5.5.9", "@types/har-format": "^1.2.7", - "@types/set-cookie-parser": "^2.4.2", "faker": "5.x", "openapi-types": "^7.2.3", "outvariant": "^1.2.1", - "randexp": "^0.5.3", - "set-cookie-parser": "^2.6.0" + "randexp": "^0.5.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4673a57..c38be41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ dependencies: '@types/har-format': specifier: ^1.2.7 version: 1.2.15 - '@types/set-cookie-parser': - specifier: ^2.4.2 - version: 2.4.7 faker: specifier: 5.x version: 5.5.3 @@ -29,9 +26,6 @@ dependencies: randexp: specifier: ^0.5.3 version: 0.5.3 - set-cookie-parser: - specifier: ^2.6.0 - version: 2.6.0 devDependencies: '@commitlint/cli': @@ -49,24 +43,9 @@ devDependencies: '@types/node': specifier: '18' version: 18.19.33 - '@typescript-eslint/eslint-plugin': - specifier: ^7.8.0 - version: 7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': - specifier: ^7.8.0 - version: 7.8.0(eslint@8.57.0)(typescript@5.4.5) compression: specifier: ^1.7.4 version: 1.7.4 - eslint: - specifier: ^8.57.0 - version: 8.57.0 - eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@8.57.0) - eslint-plugin-prettier: - specifier: ^5.1.3 - version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) jsdom: specifier: ^22.1.0 version: 22.1.0 @@ -74,8 +53,8 @@ devDependencies: specifier: ^10.5.3 version: 10.5.4 msw: - specifier: ^1.2.1 - version: 1.3.3(typescript@5.4.5) + specifier: ^2.3.0 + version: 2.3.0(typescript@5.4.5) playwright: specifier: ^1.34.3 version: 1.44.0 @@ -161,6 +140,18 @@ packages: picocolors: 1.0.0 dev: true + /@bundled-es-modules/cookie@2.0.0: + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + dependencies: + cookie: 0.5.0 + dev: true + + /@bundled-es-modules/statuses@1.0.1: + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + dependencies: + statuses: 2.0.1 + dev: true + /@commitlint/cli@17.8.1: resolution: {integrity: sha512-ay+WbzQesE0Rv4EQKfNbSMiJJ12KdKTDzIt0tcK4k11FdsWmtwP0Kp1NWMOUswfIWo6Eb7p7Ln721Nx9FLNBjg==} engines: {node: '>=v14'} @@ -750,61 +741,41 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.57.0 - eslint-visitor-keys: 3.4.3 - dev: true - - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@inquirer/confirm@3.1.8: + resolution: {integrity: sha512-f3INZ+ca4dQdn+MQiq1yP/mOIR/Oc8BLRYuDh6ciToWd6z4W8yArfzjBCMQ0BPY8PcJKwZxGIt8Z6yNT32eSTw==} + engines: {node: '>=18'} dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@inquirer/core': 8.2.1 + '@inquirer/type': 1.3.2 dev: true - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} + /@inquirer/core@8.2.1: + resolution: {integrity: sha512-TIcuQMn2qrtyYe0j136UpHeYpk7AcR/trKeT/7YY0vRgcS9YSfJuQ2+PudPhSofLLsHNnRYAHScQCcVZrJkMqA==} + engines: {node: '>=18'} dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@inquirer/figures': 1.0.2 + '@inquirer/type': 1.3.2 + '@types/mute-stream': 0.0.4 + '@types/node': 20.12.12 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-spinners: 2.9.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 dev: true - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} + /@inquirer/figures@1.0.2: + resolution: {integrity: sha512-4F1MBwVr3c/m4bAUef6LgkvBfSjzwH+OfldgHqcuacWwSUetFebM2wi58WfG9uk1rR98U6GwLed4asLJbwdV5w==} + engines: {node: '>=18'} dev: true - /@humanwhocodes/object-schema@2.0.3: - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + /@inquirer/type@1.3.2: + resolution: {integrity: sha512-5Frickan9c89QbPkSu6I6y8p+9eR6hZkdPahGmNDsTFX8FHLPAozyzCZMKUeW8FyYwnlCKUjqIEqxY+UctARiw==} + engines: {node: '>=18'} dev: true /@isaacs/cliui@8.0.2: @@ -867,28 +838,21 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: false - /@mswjs/cookies@0.2.2: - resolution: {integrity: sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==} - engines: {node: '>=14'} - dependencies: - '@types/set-cookie-parser': 2.4.7 - set-cookie-parser: 2.6.0 + /@mswjs/cookies@1.1.0: + resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} + engines: {node: '>=18'} dev: true - /@mswjs/interceptors@0.17.10: - resolution: {integrity: sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==} - engines: {node: '>=14'} + /@mswjs/interceptors@0.29.1: + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} + engines: {node: '>=18'} dependencies: - '@open-draft/until': 1.0.3 - '@types/debug': 4.1.12 - '@xmldom/xmldom': 0.8.10 - debug: 4.3.4 - headers-polyfill: 3.2.5 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 outvariant: 1.4.2 - strict-event-emitter: 0.2.8 - web-encoding: 1.1.5 - transitivePeerDependencies: - - supports-color + strict-event-emitter: 0.5.1 dev: true /@nodelib/fs.scandir@2.1.5: @@ -912,6 +876,17 @@ packages: fastq: 1.17.1 dev: true + /@open-draft/deferred-promise@2.2.0: + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + dev: true + + /@open-draft/logger@0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.2 + dev: true + /@open-draft/test-server@0.5.1: resolution: {integrity: sha512-RYyjRVfddPMmVWePP49OQ/zmJL5D0kr+/LsLILYftC067LDPdZwUm75s9h+2+gqyVHU8qcWiWPXwYouDHUmVNw==} dependencies: @@ -928,8 +903,8 @@ packages: - utf-8-validate dev: true - /@open-draft/until@1.0.3: - resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} + /@open-draft/until@2.1.0: + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} dev: true /@pkgjs/parseargs@0.11.0: @@ -939,11 +914,6 @@ packages: dev: true optional: true - /@pkgr/core@0.1.1: - resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dev: true - /@rollup/rollup-android-arm-eabi@4.17.2: resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} cpu: [arm] @@ -1124,18 +1094,16 @@ packages: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: true + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: true + /@types/cors@2.8.17: resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} dependencies: '@types/node': 18.19.33 dev: true - /@types/debug@4.1.12: - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - dependencies: - '@types/ms': 0.7.34 - dev: true - /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true @@ -1170,14 +1138,6 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} dev: true - /@types/js-levenshtein@1.1.3: - resolution: {integrity: sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==} - dev: true - - /@types/json-schema@7.0.15: - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true - /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} dev: true @@ -1186,14 +1146,23 @@ packages: resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} dev: true - /@types/ms@0.7.34: - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + /@types/mute-stream@0.0.4: + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + dependencies: + '@types/node': 18.19.33 dev: true /@types/node@18.19.33: resolution: {integrity: sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==} dependencies: undici-types: 5.26.5 + dev: true + + /@types/node@20.12.12: + resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==} + dependencies: + undici-types: 5.26.5 + dev: true /@types/node@20.5.1: resolution: {integrity: sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==} @@ -1215,10 +1184,6 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true - /@types/semver@7.5.8: - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - dev: true - /@types/send@0.17.4: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: @@ -1234,145 +1199,12 @@ packages: '@types/send': 0.17.4 dev: true - /@types/set-cookie-parser@2.4.7: - resolution: {integrity: sha512-+ge/loa0oTozxip6zmhRIk8Z/boU51wl9Q6QdLZcokIGMzY5lFXYy/x7Htj2HTC6/KZP1hUbZ1ekx8DYXICvWg==} - dependencies: - '@types/node': 18.19.33 - - /@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0)(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/type-utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - eslint: 8.57.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - semver: 7.6.1 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - eslint: 8.57.0 - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/scope-manager@7.8.0: - resolution: {integrity: sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==} - engines: {node: ^18.18.0 || >=20.0.0} - dependencies: - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/visitor-keys': 7.8.0 - dev: true - - /@typescript-eslint/type-utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - '@typescript-eslint/utils': 7.8.0(eslint@8.57.0)(typescript@5.4.5) - debug: 4.3.4 - eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/types@7.8.0: - resolution: {integrity: sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==} - engines: {node: ^18.18.0 || >=20.0.0} - dev: true - - /@typescript-eslint/typescript-estree@7.8.0(typescript@5.4.5): - resolution: {integrity: sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/visitor-keys': 7.8.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.1 - ts-api-utils: 1.3.0(typescript@5.4.5) - typescript: 5.4.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@7.8.0(eslint@8.57.0)(typescript@5.4.5): - resolution: {integrity: sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.8.0 - '@typescript-eslint/types': 7.8.0 - '@typescript-eslint/typescript-estree': 7.8.0(typescript@5.4.5) - eslint: 8.57.0 - semver: 7.6.1 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/visitor-keys@7.8.0: - resolution: {integrity: sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==} - engines: {node: ^18.18.0 || >=20.0.0} - dependencies: - '@typescript-eslint/types': 7.8.0 - eslint-visitor-keys: 3.4.3 + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} dev: true - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + /@types/wrap-ansi@3.0.0: + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} dev: true /@vitest/expect@1.6.0: @@ -1414,17 +1246,6 @@ packages: pretty-format: 29.7.0 dev: true - /@xmldom/xmldom@0.8.10: - resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} - engines: {node: '>=10.0.0'} - dev: true - - /@zxing/text-encoding@0.9.0: - resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} - requiresBuild: true - dev: true - optional: true - /JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1446,14 +1267,6 @@ packages: negotiator: 0.6.3 dev: true - /acorn-jsx@5.3.2(acorn@8.11.3): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.11.3 - dev: true - /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} @@ -1493,15 +1306,6 @@ packages: ajv: 8.13.0 dev: false - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - /ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} dependencies: @@ -1613,21 +1417,10 @@ packages: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: true - /available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - dependencies: - possible-typed-array-names: 1.0.0 - dev: true - /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true - /base64id@2.0.0: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} @@ -1638,14 +1431,6 @@ packages: engines: {node: '>=8'} dev: true - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: true - /body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1666,13 +1451,6 @@ packages: - supports-color dev: true - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: @@ -1686,13 +1464,6 @@ packages: fill-range: 7.0.1 dev: true - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true - /bundle-require@4.1.0(esbuild@0.19.12): resolution: {integrity: sha512-FeArRFM+ziGkRViKRnSTbHZc35dgmR9yNog05Kn0+ItI59pOAISGvnnIwW1WgFZQW59IxD9QpJnUPkdIPfZuXg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1782,10 +1553,6 @@ packages: supports-color: 7.2.0 dev: true - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true - /check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: @@ -1832,9 +1599,9 @@ packages: string-width: 4.2.3 dev: true - /cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} + /cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} dev: true /cliui@8.0.1: @@ -1846,11 +1613,6 @@ packages: wrap-ansi: 7.0.0 dev: true - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: true - /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1922,10 +1684,6 @@ packages: - supports-color dev: true - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - /confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} dev: true @@ -1976,6 +1734,11 @@ packages: engines: {node: '>= 0.6'} dev: true + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + /cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -2116,16 +1879,6 @@ packages: type-detect: 4.0.8 dev: true - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dependencies: - clone: 1.0.4 - dev: true - /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2167,13 +1920,6 @@ packages: path-type: 4.0.0 dev: true - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - /domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -2353,156 +2099,23 @@ packages: engines: {node: '>=0.8.0'} dev: true - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-config-prettier@9.1.0(eslint@8.57.0): - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.57.0 - dev: true - - /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): - resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '*' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) - prettier: 3.2.5 - prettier-linter-helpers: 1.0.0 - synckit: 0.8.8 - dev: true - - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.1 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) - eslint-visitor-keys: 3.4.3 - dev: true - /esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true dev: false - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: '@types/estree': 1.0.5 dev: true - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} dev: true - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: true - /execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -2587,15 +2200,6 @@ packages: - supports-color dev: true - /external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - dev: true - /faker@5.5.3: resolution: {integrity: sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==} dev: false @@ -2603,10 +2207,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true - /fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -2618,34 +2218,12 @@ packages: micromatch: 4.0.5 dev: true - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - /fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} dependencies: reusify: 1.0.4 dev: true - /figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.2.0 - dev: true - /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -2684,25 +2262,6 @@ packages: path-exists: 4.0.0 dev: true - /flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.3.1 - keyv: 4.5.4 - rimraf: 3.0.2 - dev: true - - /flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - dev: true - - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - dependencies: - is-callable: 1.2.7 - dev: true - /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -2739,10 +2298,6 @@ packages: universalify: 2.0.1 dev: true - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - /fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2823,13 +2378,6 @@ packages: is-glob: 4.0.3 dev: true - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - /glob@10.3.12: resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2842,17 +2390,6 @@ packages: path-scurry: 1.10.2 dev: true - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - /global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} engines: {node: '>=4'} @@ -2860,13 +2397,6 @@ packages: ini: 1.3.8 dev: true - /globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -2889,10 +2419,6 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - /graphql@16.8.1: resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -2929,13 +2455,6 @@ packages: engines: {node: '>= 0.4'} dev: true - /has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - /hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2943,8 +2462,8 @@ packages: function-bind: 1.1.2 dev: true - /headers-polyfill@3.2.5: - resolution: {integrity: sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==} + /headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} dev: true /hosted-git-info@2.8.9: @@ -3026,10 +2545,6 @@ packages: safer-buffer: 2.1.2 dev: true - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true - /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -3043,23 +2558,11 @@ packages: resolve-from: 4.0.0 dev: true - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} dev: true - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true @@ -3068,40 +2571,11 @@ packages: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} dev: true - /inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - dev: true - /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} dev: true - /is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - dev: true - /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: true @@ -3113,11 +2587,6 @@ packages: binary-extensions: 2.3.0 dev: true - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - /is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: @@ -3134,13 +2603,6 @@ packages: engines: {node: '>=8'} dev: true - /is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.2 - dev: true - /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -3148,11 +2610,6 @@ packages: is-extglob: 2.1.1 dev: true - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true - /is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} dev: true @@ -3172,11 +2629,6 @@ packages: engines: {node: '>=8'} dev: true - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - /is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} @@ -3208,13 +2660,6 @@ packages: text-extensions: 1.9.0 dev: true - /is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - dependencies: - which-typed-array: 1.1.15 - dev: true - /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -3238,11 +2683,6 @@ packages: engines: {node: '>=10'} dev: true - /js-levenshtein@1.1.6: - resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} - engines: {node: '>=0.10.0'} - dev: true - /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -3304,25 +2744,13 @@ packages: - utf-8-validate dev: true - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true - /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -3336,25 +2764,11 @@ packages: engines: {'0': node >= 0.2.0} dev: true - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - dependencies: - json-buffer: 3.0.1 - dev: true - /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} dev: true - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - /lilconfig@3.1.1: resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} engines: {node: '>=14'} @@ -3619,12 +3033,6 @@ packages: engines: {node: '>=4'} dev: true - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - /minimatch@9.0.4: resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} engines: {node: '>=16 || 14 >=14.17'} @@ -3671,44 +3079,40 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true - /msw@1.3.3(typescript@5.4.5): - resolution: {integrity: sha512-CiPyRFiYJCXYyH/vwxT7m+sa4VZHuUH6cGwRBj0kaTjBGpsk4EnL47YzhoA859htVCF2vzqZuOsomIUlFqg9GQ==} - engines: {node: '>=14'} + /msw@2.3.0(typescript@5.4.5): + resolution: {integrity: sha512-cDr1q/QTMzaWhY8n9lpGhceY209k29UZtdTgJ3P8Bzne3TSMchX2EM/ldvn4ATLOktpCefCU2gcEgzHc31GTPw==} + engines: {node: '>=18'} hasBin: true requiresBuild: true peerDependencies: - typescript: '>= 4.4.x' + typescript: '>= 4.7.x' peerDependenciesMeta: typescript: optional: true dependencies: - '@mswjs/cookies': 0.2.2 - '@mswjs/interceptors': 0.17.10 - '@open-draft/until': 1.0.3 - '@types/cookie': 0.4.1 - '@types/js-levenshtein': 1.1.3 + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 3.1.8 + '@mswjs/cookies': 1.1.0 + '@mswjs/interceptors': 0.29.1 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 chalk: 4.1.2 - chokidar: 3.6.0 - cookie: 0.4.2 graphql: 16.8.1 - headers-polyfill: 3.2.5 - inquirer: 8.2.6 + headers-polyfill: 4.0.3 is-node-process: 1.2.0 - js-levenshtein: 1.1.6 - node-fetch: 2.7.0 outvariant: 1.4.2 path-to-regexp: 6.2.2 - strict-event-emitter: 0.4.6 - type-fest: 2.19.0 + strict-event-emitter: 0.5.1 + type-fest: 4.18.2 typescript: 5.4.5 yargs: 17.7.2 - transitivePeerDependencies: - - encoding - - supports-color dev: true - /mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + /mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true /mz@2.7.0: @@ -3725,27 +3129,11 @@ packages: hasBin: true dev: true - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} dev: true - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: true - /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -3833,38 +3221,6 @@ packages: resolution: {integrity: sha512-olbaNxz12R27+mTyJ/ZAFEfUruauHH27AkeQHDHRq5AF0LdNkK1SSV7EourXQDK+4aX7dv2HtyirAGK06WMAsA==} dev: false - /optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - dev: true - - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: true - - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true - /outvariant@1.4.2: resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} @@ -3948,11 +3304,6 @@ packages: engines: {node: '>=8'} dev: true - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -4040,11 +3391,6 @@ packages: semver-compare: 1.0.0 dev: true - /possible-typed-array-names@1.0.0: - resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} - engines: {node: '>= 0.4'} - dev: true - /postcss-load-config@4.0.2(ts-node@10.9.2): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -4071,18 +3417,6 @@ packages: source-map-js: 1.2.0 dev: true - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} - dependencies: - fast-diff: 1.3.0 - dev: true - /prettier@3.2.5: resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} engines: {node: '>=14'} @@ -4272,13 +3606,6 @@ packages: resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} dev: true - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - /rimraf@5.0.5: resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} engines: {node: '>=14'} @@ -4317,11 +3644,6 @@ packages: resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} dev: true - /run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - dev: true - /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -4409,9 +3731,6 @@ packages: - supports-color dev: true - /set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} - /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -4587,14 +3906,8 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true - /strict-event-emitter@0.2.8: - resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==} - dependencies: - events: 3.3.0 - dev: true - - /strict-event-emitter@0.4.6: - resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==} + /strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} dev: true /string-argv@0.3.1: @@ -4666,11 +3979,6 @@ packages: min-indent: 1.0.1 dev: true - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - /strip-literal@2.1.0: resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} dependencies: @@ -4714,23 +4022,11 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true - /synckit@0.8.8: - resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} - engines: {node: ^14.18.0 || >=16.0.0} - dependencies: - '@pkgr/core': 0.1.1 - tslib: 2.6.2 - dev: true - /text-extensions@1.9.0: resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} engines: {node: '>=0.10'} dev: true - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -4768,13 +4064,6 @@ packages: engines: {node: '>=14.0.0'} dev: true - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - dependencies: - os-tmpdir: 1.0.2 - dev: true - /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4797,10 +4086,6 @@ packages: url-parse: 1.5.10 dev: true - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: true - /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: @@ -4824,15 +4109,6 @@ packages: engines: {node: '>=8'} dev: true - /ts-api-utils@1.3.0(typescript@5.4.5): - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - dependencies: - typescript: 5.4.5 - dev: true - /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true @@ -4942,13 +4218,6 @@ packages: - ts-node dev: true - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -4959,11 +4228,6 @@ packages: engines: {node: '>=10'} dev: true - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -4979,9 +4243,9 @@ packages: engines: {node: '>=8'} dev: true - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} + /type-fest@4.18.2: + resolution: {integrity: sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==} + engines: {node: '>=16'} dev: true /type-is@1.6.18: @@ -5004,6 +4268,7 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true /universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} @@ -5036,16 +4301,6 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true - /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.13 - which-typed-array: 1.1.15 - dev: true - /utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} @@ -5188,24 +4443,6 @@ packages: xml-name-validator: 4.0.0 dev: true - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.4 - dev: true - - /web-encoding@1.1.5: - resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} - dependencies: - util: 0.12.5 - optionalDependencies: - '@zxing/text-encoding': 0.9.0 - dev: true - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: true - /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true @@ -5235,13 +4472,6 @@ packages: webidl-conversions: 7.0.0 dev: true - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: true - /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -5250,17 +4480,6 @@ packages: webidl-conversions: 4.0.2 dev: true - /which-typed-array@1.1.15: - resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.2 - dev: true - /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5278,11 +4497,6 @@ packages: stackback: 0.0.2 dev: true - /word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - dev: true - /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} diff --git a/source-logo.png b/source-logo.png deleted file mode 100644 index ea7d603eb1df70404ff72e66d6f4dbf43a84b16d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13306 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 { const specification = await SwaggerParser.dereference(document) + const requestHandlers: Array = [] + + if (typeof specification.paths === 'undefined') { + return [] + } + + const pathItems = Object.entries(specification.paths ?? {}) + for (const item of pathItems) { + const [url, handlers] = item + const pathItem = handlers as + | OpenAPIV2.PathItemObject + | OpenAPIV3.PathItemObject + + for (const key of Object.keys(pathItem)) { + const method = key as keyof OpenAPIV2.PathItemObject + + // Ignore unsupported HTTP methods. + if (!isSupportedHttpMethod(method)) { + continue + } + + const operation = pathItem[method] as OpenAPIV3.OperationObject + if (!operation) { + continue + } - const handlers = Object.entries(specification.paths).reduce< - Array - >( - ( - handlers, - [url, pathItem]: [ - string, - OpenAPIV2.PathItemObject | OpenAPIV3.PathItemObject, - ], - ) => { - for (const key of Object.keys(pathItem)) { - const method = key as unknown as keyof OpenAPIV2.PathItemObject + const serverUrls = getServers(specification) + + for (const baseUrl of serverUrls) { + const path = normalizeSwaggerUrl(url) + const requestUrl = isAbsoluteUrl(baseUrl) + ? new URL(path, baseUrl).href + : joinPaths(path, baseUrl) if ( - method !== 'get' && - method !== 'put' && - method !== 'post' && - method !== 'delete' && - method !== 'options' && - method !== 'head' && - method !== 'patch' + typeof operation.responses === 'undefined' || + operation.responses === null ) { - continue - } + const handler = new HttpHandler( + method, + requestUrl, + () => + new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }), + { + /** + * @fixme Support `once` the same as in HAR? + */ + }, + ) + + requestHandlers.push(handler) - const operation = pathItem[method] as OpenAPIV3.OperationObject - if (!operation) { continue } - const serverUrls = getServers(specification) - - for (const baseUrl of serverUrls) { - const path = normalizeSwaggerUrl(url) - const requestUrl = isAbsoluteUrl(baseUrl) - ? new URL(path, baseUrl).href - : joinPaths(path, baseUrl) + for (const responseStatus of Object.keys(operation.responses)) { + const content = operation.responses[responseStatus] + if (!content) { + continue + } - handlers.push( - rest[method](requestUrl, createResponseResolver(operation)), + const handler = new HttpHandler( + method, + requestUrl, + createResponseResolver(operation), + { + /** + * @fixme Support `once` the same as in HAR? + */ + }, ) + + requestHandlers.push(handler) } } + } + } - return handlers - }, - [], - ) + return requestHandlers +} - return handlers +function isSupportedHttpMethod(method: string): method is SupportedHttpMethods { + return supportedHttpMethods.includes(method) } diff --git a/src/fromOpenApi/response/createResponseResolver.ts b/src/fromOpenApi/response/createResponseResolver.ts deleted file mode 100644 index 74448df..0000000 --- a/src/fromOpenApi/response/createResponseResolver.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - MockedRequest, - ResponseResolver, - ResponseTransformer, - RestContext, -} from 'msw' -import { OpenAPIV3 } from 'openapi-types' -import { getBodyTransformers } from './transformers/bodyTransformer' -import { getHeadersTransformers } from './transformers/headersTransformer' - -export function createResponseResolver( - operation: OpenAPIV3.OperationObject, -): ResponseResolver { - return (req, res, ctx) => { - // Treat the opeartions that don't describe any - // responses as not implemented. - if ( - operation.responses == null || - Object.keys(operation.responses || {}).length === 0 - ) { - return res(ctx.status(501)) - } - - // Get the response object from the schema. - let responseObject: OpenAPIV3.ResponseObject - const explicitResponseStatus = req.url.searchParams.get('response') - - if (explicitResponseStatus) { - const explicitResponse = operation.responses[ - explicitResponseStatus - ] as OpenAPIV3.ResponseObject - - if (!explicitResponse) { - return res(ctx.status(501)) - } - - responseObject = explicitResponse - } else { - const fallbackResponse = - (operation.responses['200'] as OpenAPIV3.ResponseObject) || - (operation.responses.default as OpenAPIV3.ResponseObject) - - if (!fallbackResponse) { - return res() - } - - responseObject = fallbackResponse - } - - // Response status. - const responseStatus = explicitResponseStatus || '200' - - const transformers: ResponseTransformer[] = [ - ctx.status(Number(responseStatus)), - ...getHeadersTransformers(responseObject, req, ctx), - ...getBodyTransformers(responseObject, req, ctx), - ] - - return res(...transformers) - } -} diff --git a/src/fromOpenApi/response/transformers/bodyTransformer.ts b/src/fromOpenApi/response/transformers/bodyTransformer.ts deleted file mode 100644 index 595b1d5..0000000 --- a/src/fromOpenApi/response/transformers/bodyTransformer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MockedRequest, ResponseTransformer, RestContext } from 'msw' -import { OpenAPIV3 } from 'openapi-types' -import { evolveJsonSchema } from '../../schema/evolve' -import { toString } from '../../utils/toString' - -export function getBodyTransformers( - responseObject: OpenAPIV3.ResponseObject, - req: MockedRequest, - ctx: RestContext, -): ResponseTransformer[] { - if (!('content' in responseObject && responseObject.content != null)) { - return [] - } - - const requestHeaders = Object.fromEntries(req.headers.entries()) - const acceptedMimeTypes = ([] as string[]).concat(requestHeaders.accept) - const explicitContentType = acceptedMimeTypes[0] || '' - const explicitContentTypeRegexp = new RegExp( - explicitContentType.replace(/\/+/g, '\\/').replace(/\*/g, '.+?'), - ) - - const allContentTypes = Object.keys(responseObject.content) - - const contentType = - allContentTypes.find((contentType) => { - // Find the first declared response content type - // that matches the "Accept" request header. - // Keep in mind that values like "*/*" and "application/*" - // are completely valid. - return explicitContentTypeRegexp.test(contentType) - }) || allContentTypes[0] - - const mediaTypeObject = responseObject.content[contentType] - - const body = (() => { - if (mediaTypeObject.example) { - return mediaTypeObject.example - } - - if (mediaTypeObject.examples) { - const { value } = Object.values( - mediaTypeObject.examples, - )[0] as OpenAPIV3.ExampleObject - - return value - } - - if (mediaTypeObject.schema) { - return evolveJsonSchema(mediaTypeObject.schema as OpenAPIV3.SchemaObject) - } - })() - - const transformers: ResponseTransformer[] = [ - ctx.set('Content-Type', contentType), - ] - - if (body) { - transformers.push(ctx.body(toString(body))) - } - - return transformers -} diff --git a/src/fromOpenApi/response/transformers/headersTransformer.ts b/src/fromOpenApi/response/transformers/headersTransformer.ts deleted file mode 100644 index c70a213..0000000 --- a/src/fromOpenApi/response/transformers/headersTransformer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MockedRequest, ResponseTransformer, RestContext } from 'msw' -import { OpenAPIV3 } from 'openapi-types' -import { evolveJsonSchema } from '../../schema/evolve' -import { toString } from '../../utils/toString' - -export function getHeadersTransformers( - responseObject: OpenAPIV3.ResponseObject, - _req: MockedRequest, - ctx: RestContext, -): ResponseTransformer[] { - if (!responseObject.headers) { - return [] - } - - const transformers = Object.entries(responseObject.headers).reduce< - ResponseTransformer[] - >((transformers, [headerName, headerObject]) => { - const headerSchema = (headerObject as OpenAPIV3.HeaderObject) - .schema as OpenAPIV3.SchemaObject - - if (!headerSchema) { - return transformers - } - - const headerValue = evolveJsonSchema(headerSchema) - if (typeof headerValue === 'undefined') { - return transformers - } - - return transformers.concat(ctx.set(headerName, toString(headerValue))) - }, []) - - return transformers -} diff --git a/src/fromOpenApi/schema/evolve.ts b/src/fromOpenApi/schema/evolve.ts index fc7797b..090cee1 100644 --- a/src/fromOpenApi/schema/evolve.ts +++ b/src/fromOpenApi/schema/evolve.ts @@ -10,6 +10,7 @@ export function evolveJsonSchema( schema: OpenAPIV3.SchemaObject, ): string | number | boolean | unknown[] | Record | undefined { // Always use an explicit example first. + // A "schema" field may equal an example if it's a resolved reference. if (schema.example) { return schema.example } diff --git a/src/fromOpenApi/schema/types/string.ts b/src/fromOpenApi/schema/types/string.ts index 57292f6..9d04bbc 100644 --- a/src/fromOpenApi/schema/types/string.ts +++ b/src/fromOpenApi/schema/types/string.ts @@ -1,7 +1,6 @@ import { datatype, internet, finance } from 'faker' import { OpenAPIV3 } from 'openapi-types' import { randexp } from 'randexp' -import { toBase64 } from '../../utils/toBase64.js' import { toBinary } from '../../utils/toBinary.js' export function evolveString(schema: OpenAPIV3.SchemaObject): string { @@ -11,7 +10,7 @@ export function evolveString(schema: OpenAPIV3.SchemaObject): string { switch (schema.format?.toLowerCase()) { case 'byte': { - return toBase64(datatype.string()) + return btoa(datatype.string()) } case 'binary': { diff --git a/src/fromOpenApi/utils/getServers.ts b/src/fromOpenApi/utils/getServers.ts index 156b847..7271c30 100644 --- a/src/fromOpenApi/utils/getServers.ts +++ b/src/fromOpenApi/utils/getServers.ts @@ -1,10 +1,10 @@ -import { OpenAPIV2, OpenAPIV3 } from 'openapi-types' +import { OpenAPIV2, OpenAPIV3, OpenAPI } from 'openapi-types' /** * Returns the list of servers specified in the given OpenAPI document. */ export function getServers( - document: OpenAPIV2.Document | OpenAPIV3.Document, + document: OpenAPI.Document | OpenAPIV2.Document | OpenAPIV3.Document, ): Array { if ('basePath' in document && typeof document.basePath !== 'undefined') { return [document.basePath] diff --git a/src/fromOpenApi/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts new file mode 100644 index 0000000..ca5e955 --- /dev/null +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -0,0 +1,249 @@ +import { STATUS_CODES } from 'node:http' +import type { ResponseResolver } from 'msw' +import { OpenAPIV3 } from 'openapi-types' +import { evolveJsonSchema } from '../schema/evolve' +import { toString } from './toString' + +export function createResponseResolver( + operation: OpenAPIV3.OperationObject, +): ResponseResolver { + return ({ request }) => { + const { responses } = operation + + // Treat operations that describe no responses as not implemented. + if (responses == null) { + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) + } + if (Object.keys(responses).length === 0) { + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) + } + + let responseObject: OpenAPIV3.ResponseObject + + const url = new URL(request.url) + const explicitResponseStatus = url.searchParams.get('response') + + if (explicitResponseStatus) { + const responseByStatus = responses[ + explicitResponseStatus + ] as OpenAPIV3.ResponseObject + + if (!responseByStatus) { + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) + } + + responseObject = responseByStatus + } else { + const fallbackResponse = + (responses['200'] as OpenAPIV3.ResponseObject) || + (responses.default as OpenAPIV3.ResponseObject) + + if (!fallbackResponse) { + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) + } + + responseObject = fallbackResponse + } + + const status = Number(explicitResponseStatus || '200') + return new Response(toBody(request, responseObject), { + status, + statusText: STATUS_CODES[status], + headers: toHeaders(request, responseObject), + }) + } +} + +/** + * Get the Fetch API `Headers` from the OpenAPI response object. + */ +export function toHeaders( + request: Request, + responseObject: OpenAPIV3.ResponseObject, +): Headers | undefined { + const { content } = responseObject + if (!content) { + return undefined + } + + // See what "Content-Type" the request accepts. + const accept = request.headers.get('accept') || '' + const acceptedContentTypes = accept + .split(',') + .filter((item) => item.length !== 0) + + const responseContentTypes = Object.keys(content) + + // Lookup the first response content type that satisfies + // the expected request's "Accept" header. + let selectedContentType: string | undefined + if (acceptedContentTypes.length > 0) { + for (const acceptedContentType of acceptedContentTypes) { + const contentTypeRegExp = contentTypeToRegExp(acceptedContentType) + const matchingResponseContentType = responseContentTypes.find( + (responseContentType) => { + return contentTypeRegExp.test(responseContentType) + }, + ) + + if (matchingResponseContentType) { + selectedContentType = matchingResponseContentType + break + } + } + } else { + // If the request didn't specify any "Accept" header, + // use the first response content type from the spec. + selectedContentType = responseContentTypes[0] as string + } + + if (typeof responseObject.headers === 'undefined' && selectedContentType) { + const headers = new Headers() + headers.set('content-type', selectedContentType) + return headers + } + + const responseHeaders = responseObject.headers ?? {} + const headerNames = Object.keys(responseHeaders) + if (headerNames.length === 0) { + return undefined + } + + const headers = new Headers() + + for (const [headerName, headerObject] of Object.entries(responseHeaders)) { + const headerSchema = (headerObject as OpenAPIV3.HeaderObject).schema as + | OpenAPIV3.SchemaObject + | undefined + if (!headerSchema) { + continue + } + + const headerValue = evolveJsonSchema(headerSchema) + if (typeof headerValue === 'undefined') { + continue + } + + headers.append(headerName, toString(headerValue)) + } + + if (headers.get('content-type') === null && selectedContentType) { + headers.set('content-type', selectedContentType) + } + + return headers +} + +/** + * Get the Fetch API `BodyInit` from the OpenAPI response object. + */ +export function toBody( + request: Request, + responseObject: OpenAPIV3.ResponseObject, +): BodyInit { + const { content } = responseObject + if (!content) { + return null + } + + // See what "Content-Type" the request accepts. + const accept = request.headers.get('accept') || '' + const acceptedContentTypes = accept + .split(',') + .filter((item) => item.length !== 0) + + let mediaTypeObject: OpenAPIV3.MediaTypeObject | undefined + const responseContentTypes = Object.keys(content) + + // Lookup the first response content type that satisfies + // the expected request's "Accept" header. + let selectedContentType: string | undefined + if (acceptedContentTypes.length > 0) { + for (const acceptedContentType of acceptedContentTypes) { + const contentTypeRegExp = contentTypeToRegExp(acceptedContentType) + const matchingResponseContentType = responseContentTypes.find( + (responseContentType) => { + return contentTypeRegExp.test(responseContentType) + }, + ) + + if (matchingResponseContentType) { + selectedContentType = matchingResponseContentType + mediaTypeObject = content[selectedContentType] + break + } + } + } else { + // If the request didn't specify any "Accept" header, + // use the first response content type from the spec. + selectedContentType = responseContentTypes[0] as string + mediaTypeObject = content[selectedContentType] + } + + if (!mediaTypeObject) { + return null + } + + // If the response object has the body example, use it. + if (mediaTypeObject.example) { + if (typeof mediaTypeObject.example === 'object') { + return JSON.stringify(mediaTypeObject.example) + } + + return mediaTypeObject.example + } + + if (mediaTypeObject.examples) { + // Support exact response example specified in the + // "example" request URL search parameter. + const url = new URL(request.url) + const exampleName = url.searchParams.get('example') + + if (exampleName) { + const exampleByName = mediaTypeObject.examples[exampleName] as + | OpenAPIV3.ExampleObject + | undefined + return exampleByName + ? exampleByName.value + : `Cannot find example by name "${exampleName}"` + } + + // Otherwise, use the first example. + const firstExample = Object.values( + mediaTypeObject.examples, + )[0] as OpenAPIV3.ExampleObject + + if (typeof firstExample.value === 'object') { + return JSON.stringify(firstExample.value) + } + + return firstExample.value + } + + // If the response is a JSON Schema, evolve and use it. + if (mediaTypeObject.schema) { + const resolvedResponse = evolveJsonSchema( + mediaTypeObject.schema as OpenAPIV3.SchemaObject, + ) + + return JSON.stringify(resolvedResponse) + } + + return null +} + +function contentTypeToRegExp(contentType: string): RegExp { + return new RegExp(contentType.replace(/\/+/g, '\\/').replace(/\*/g, '.+?')) +} diff --git a/src/fromOpenApi/utils/toBase64.ts b/src/fromOpenApi/utils/toBase64.ts deleted file mode 100644 index 34885a9..0000000 --- a/src/fromOpenApi/utils/toBase64.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function toBase64(value: string): string { - return typeof Buffer === 'undefined' - ? btoa(value) - : Buffer.from(value).toString('base64') -} diff --git a/src/fromTraffic/fromTraffic.ts b/src/fromTraffic/fromTraffic.ts index 33cd411..c1c40f0 100644 --- a/src/fromTraffic/fromTraffic.ts +++ b/src/fromTraffic/fromTraffic.ts @@ -1,184 +1,78 @@ import { invariant } from 'outvariant' -import { Har, Entry, Request, Response } from 'har-format' -import { Cookie, parse } from 'set-cookie-parser' -import { - RestHandler, - rest, - context, - ResponseFunction, - ResponseTransformer, - ResponseComposition, - DefaultBodyType, - cleanUrl, -} from 'msw' -import { decodeBase64String } from './utils/decodeBase64String.js' +import type Har from 'har-format' +import { RequestHandler, HttpHandler, cleanUrl, delay } from 'msw' +import { toResponse } from './utils/harUtils' -export type MapEntryFn = (entry: Entry) => Entry | undefined - -type ResponseProducer = ( - response: ResponseComposition, - transformers: ResponseTransformer[], -) => ReturnType - -const defaultResponseProducer: ResponseProducer = (response, transformers) => { - return response(...transformers) -} - -/** - * Create a request handler from the given Network Archive entry. - */ -function toRequestHandler( - entry: Entry, - produceResponse: ResponseProducer = defaultResponseProducer, -): RestHandler { - const { request } = entry - const method = request.method.toLowerCase() as keyof typeof rest - const transformers = toResponseTransformers(entry) - - return rest[method](cleanUrl(request.url), (_, response) => { - return produceResponse(response, transformers) - }) -} - -/** - * Map a traffic entry to the array of response transformers. - */ -export function toResponseTransformers(entry: Entry): ResponseTransformer[] { - const { response, time } = entry - - const transformers: ResponseTransformer[] = [] - const responseHeaders = new Headers() - const responseCookies: Cookie[] = [] - - // Response status and status text. - transformers.push(context.status(response.status, response.statusText)) - - // Response headers. - for (const header of response.headers) { - const headerName = header.name.toLowerCase() - - // Skip response cookie headers because a mocked response cookies - // are not implemented through headers (security consideration). - // Store the list of cookie headers to apply them via `ctx.cookie` later. - if (['set-cookie', 'set-cookie2'].includes(headerName)) { - responseCookies.push(...parse(header.value)) - continue - } - - // Skip the "Content-Encoding" header to prevent "incorrect header check" errors. - // MSW must not attempt to compress the response body, even if it was originally - // compressed. All response bodies are sent uncompressed. - if (headerName === 'content-encoding') { - continue - } - - responseHeaders.set(header.name, header.value) - } - - transformers.push(context.set(Object.fromEntries(responseHeaders.entries()))) - - // Response cookies. - for (const cookie of responseCookies) { - const { name, value, ...options } = cookie - - transformers.push( - context.cookie(name, value, { - ...options, - sameSite: options.sameSite === '', - }), - ) - } - - // Response delay. - if (time) { - transformers.push(context.delay(time)) - } - - // Response body. - const responseBody = toResponseBody(response) - - if (responseBody) { - transformers.push(context.body(responseBody)) - } - - return transformers -} +export type MapEntryFunction = (entry: Har.Entry) => Har.Entry | undefined /** - * Extract a response body from the given HAR response entry. - * Decodes any base64-encoded text response bodies. - */ -export function toResponseBody( - response: Response, -): Uint8Array | string | undefined { - const { text, encoding, mimeType } = response.content - - if (!text) { - return - } - - if (encoding === 'base64' && mimeType.includes('text')) { - return decodeBase64String(text) - } - - return text -} - -/** - * Generate request handlers from the given HAR file. + * Generate request handlers from the given + * network archive (HAR) file. + * + * @example + * import har from './traffic.har' + * fromTraffic(har) */ export function fromTraffic( - har: Har, - mapEntry?: MapEntryFn, -): Array { + archive: Har.Har, + mapEntry?: MapEntryFunction, +): Array { invariant( - har, + archive, 'Failed to generate request handlers from traffic: expected an HAR object but got %s.', - typeof har, + typeof archive, ) invariant( - har.log.entries.length > 0, + archive.log.entries.length > 0, 'Failed to generate request handlers from traffic: given HAR object has no entries.', ) const requestIds = new Set() + const handlers: Array = [] - const handlers = har.log.entries.reduceRight( - (handlers, entry) => { - const resolvedEntry = mapEntry ? mapEntry(entry) : entry - - if (!resolvedEntry) { - return handlers - } + // Loop over the HAR entries from right to left. + for (let i = archive.log.entries.length - 1; i >= 0; i--) { + const rawEntry = archive.log.entries[i] + const entry = mapEntry ? mapEntry(rawEntry) : rawEntry - const requestId = createRequestId(resolvedEntry.request) - const isUniqueHandler = !requestIds.has(requestId) - - const handler = toRequestHandler(resolvedEntry, (res, transformers) => { - // Reducing the entries from right to left implies that the first - // entry we meet is, in fact, the last entry recorded. - // Always create a regular handler for the last entry. - // If there are any consecutive entries for the same URL, - // create a one-time handler instead to preserve response order. - const responseFn = isUniqueHandler ? res : res.once - return responseFn(...transformers) - }) + if (!entry) { + continue + } - // Prepend the handler to the list of handler because we're reducing - // from right to left, but the order of handlers must correspond - // to the chronological order of requests. - handlers.unshift(handler) - requestIds.add(requestId) + const { request } = entry + + const requestId = createRequestId(request) + const isUniqueHandler = !requestIds.has(requestId) + const method = request.method.toLowerCase() + const path = cleanUrl(request.url) + const response = toResponse(entry.response) + + const handler = new HttpHandler( + method, + path, + async () => { + if (entry.time) { + await delay(entry.time) + } + + return response + }, + { + once: !isUniqueHandler, + }, + ) - return handlers - }, - [], - ) + // Prepend the handler to the list of handler because we're reducing + // from right to left, but the order of handlers must correspond + // to the chronological order of requests. + handlers.unshift(handler) + requestIds.add(requestId) + } return handlers } -function createRequestId(request: Request): string { +function createRequestId(request: Har.Request): string { return `${request.method}+${request.url}` } diff --git a/src/fromTraffic/utils/__tests__/base64strings.test.ts b/src/fromTraffic/utils/__tests__/base64strings.test.ts new file mode 100644 index 0000000..7bdab4d --- /dev/null +++ b/src/fromTraffic/utils/__tests__/base64strings.test.ts @@ -0,0 +1,21 @@ +import { encodeBase64String } from '../encodeBase64String' +import { decodeBase64String } from '../decodeBase64String' +import { fromByteArray } from '../fromByteArray' + +describe('base64strings', () => { + test('should be able to decode base64 string', () => { + const base64String = 'aGVsbG8gd29ybGQ=' + const decodedString = decodeBase64String(base64String) + const asString = fromByteArray(decodedString) + expect(asString).toEqual('hello world') + }) + + test('should be able to encode string to base64', () => { + const input = 'hello world' + const base64String = 'aGVsbG8gd29ybGQ=' + const encodedString = encodeBase64String(input) + const asString = fromByteArray(encodedString) + expect(asString).toEqual(base64String) + }) + +}) diff --git a/src/fromTraffic/utils/decodeBase64String.ts b/src/fromTraffic/utils/decodeBase64String.ts index b28347f..58ea4ff 100644 --- a/src/fromTraffic/utils/decodeBase64String.ts +++ b/src/fromTraffic/utils/decodeBase64String.ts @@ -1,8 +1,7 @@ -import { Buffer } from 'buffer' +export function decodeBase64String(data: string): Uint8Array { + const binaryString = atob(data) + const encoder = new TextEncoder() + const bytes = encoder.encode(binaryString) + return bytes -export function decodeBase64String(text: string): Uint8Array { - return Uint8Array.from( - Buffer.from(text, 'base64').toString('binary'), - (char) => char.charCodeAt(0), - ) } diff --git a/src/fromTraffic/utils/encodeBase64String.ts b/src/fromTraffic/utils/encodeBase64String.ts new file mode 100644 index 0000000..3a4519f --- /dev/null +++ b/src/fromTraffic/utils/encodeBase64String.ts @@ -0,0 +1,6 @@ +export function encodeBase64String(data: string): Uint8Array { + const binaryString = btoa(data) + const encoder = new TextEncoder() + const bytes = encoder.encode(binaryString) + return bytes +} diff --git a/src/fromTraffic/utils/fromByteArray.ts b/src/fromTraffic/utils/fromByteArray.ts new file mode 100644 index 0000000..10307e4 --- /dev/null +++ b/src/fromTraffic/utils/fromByteArray.ts @@ -0,0 +1,4 @@ +export function fromByteArray(bytes: Uint8Array) { + const decoder = new TextDecoder().decode(bytes) + return decoder.toString() +} diff --git a/src/fromTraffic/utils/harUtils.test.ts b/src/fromTraffic/utils/harUtils.test.ts new file mode 100644 index 0000000..e539269 --- /dev/null +++ b/src/fromTraffic/utils/harUtils.test.ts @@ -0,0 +1,78 @@ +import { toHeaders, toResponse, toResponseBody } from './harUtils' + +describe(toHeaders, () => { + it('supports a single har headers', () => { + expect( + toHeaders([{ name: 'Content-Type', value: 'application/json' }]), + ).toEqual( + new Headers({ + 'Content-Type': 'application/json', + }), + ) + }) + + it('supports multiple har headers', () => { + expect( + toHeaders([ + { name: 'Content-Type', value: 'application/json' }, + { name: 'Authorization', value: 'Bearer 123' }, + ]), + ).toEqual( + new Headers({ + 'Content-Type': 'application/json', + Authorization: 'Bearer 123', + }), + ) + }) + + it('supports multi-value headers', () => { + expect( + toHeaders([ + { name: 'Set-Cookie', value: 'a=1' }, + { name: 'Set-Cookie', value: 'b=2' }, + ]), + ).toEqual( + new Headers([ + ['Set-Cookie', 'a=1'], + ['Set-Cookie', 'b=2'], + ]), + ) + }) +}) + +describe(toResponseBody, () => { + it('returns undefined given no response body', () => { + expect( + toResponseBody({ + size: 0, + mimeType: '', + }), + ).toBe(undefined) + }) + + it('returns the response body as text', () => { + expect( + toResponseBody({ + text: 'hello world', + size: 11, + mimeType: 'text/plain', + }), + ).toBe('hello world') + }) + + it('decodes the base64-encoded response body', () => { + const bodyBytes = Uint8Array.from('hello world', (c) => c.charCodeAt(0)) + const responseBody = toResponseBody({ + text: btoa('hello world'), + size: 11, + mimeType: 'text/plain', + encoding: 'base64', + }) + + expect(responseBody).toEqualBytes(bodyBytes) + }) + + it.todo('handles a compressed response body') +}) + +describe.todo(toResponse) diff --git a/src/fromTraffic/utils/harUtils.ts b/src/fromTraffic/utils/harUtils.ts new file mode 100644 index 0000000..8a1011d --- /dev/null +++ b/src/fromTraffic/utils/harUtils.ts @@ -0,0 +1,46 @@ +import type Har from 'har-format' +import { decodeBase64String } from './decodeBase64String' + +export function toHeaders(harHeaders: Array): Headers { + return new Headers( + harHeaders.map<[string, string]>((header) => { + /** + * @fixme This will preserve the "Content-Encoding" + * response header for compressed bodies. Make sure + * MSW also handles the compression, otherwise the + * "incorrect header check" error will be thrown. + */ + return [header.name, header.value] + }), + ) +} + +export function toResponseBody( + content: Har.Content, +): Uint8Array | string | undefined { + const { text, encoding, mimeType } = content + + if (!text) { + return + } + + if (encoding === 'base64' && mimeType.includes('text')) { + return decodeBase64String(text) + } + + /** + * @fixme Handle compressed response bodies. + */ + + return text +} + +export function toResponse(responseEntry: Har.Response): Response { + const body = toResponseBody(responseEntry.content) + const response = new Response(body, { + status: responseEntry.status, + statusText: responseEntry.statusText, + headers: toHeaders(responseEntry.headers), + }) + return response +} diff --git a/test/oas/oas-json-schema.test.ts b/test/oas/oas-json-schema.test.ts index 1ce1797..0f3127c 100644 --- a/test/oas/oas-json-schema.test.ts +++ b/test/oas/oas-json-schema.test.ts @@ -1,6 +1,10 @@ import { fromOpenApi } from '../../src/fromOpenApi/fromOpenApi' import { withHandlers } from '../support/withHandlers' import { createOpenApiSpec } from '../support/createOpenApiSpec' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' + +const ID_REGEXP = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ it('supports JSON Schema object', async () => { const handlers = await fromOpenApi( @@ -46,30 +50,25 @@ it('supports JSON Schema object', async () => { }, }), ) - const res = await withHandlers(handlers, () => { + + const response = await withHandlers(handlers, () => { return fetch('http://localhost/cart') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/json') - - const json: { - id: string - items: Array<{ id: string; price: number }> - } = await res.json() - - expect(Object.keys(json)).toEqual(['id', 'items']) - expect(json.id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ) - expect(json.items).toBeInstanceOf(Array) - - json.items.forEach((item) => { - expect(Object.keys(item)).toEqual(['id', 'price']) - expect(item.id).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ) - expect(typeof item.price).toEqual('number') + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/json') + expect(await response.json()).toEqual({ + id: expect.stringMatching(ID_REGEXP), + items: [ + expect.objectContaining({ + id: expect.stringMatching(ID_REGEXP), + price: expect.any(Number), + }), + expect.objectContaining({ + id: expect.stringMatching(ID_REGEXP), + price: expect.any(Number), + }), + ], }) }) @@ -86,9 +85,20 @@ it('normalizes path parameters', async () => { }, }), ) - - expect(handlers[0].info.header).toEqual('GET /pet/:petId') - expect(handlers[1].info.header).toEqual('GET /pet/:petId/:foodId') + expect(await inspectHandlers(handlers)).toEqual([ + expect.objectContaining({ + handler: { + method: 'GET', + path: 'http://localhost/pet/:petId', + }, + }), + expect.objectContaining({ + handler: { + method: 'GET', + path: 'http://localhost/pet/:petId/:foodId', + }, + }), + ]) }) it('treats operations without "responses" as not implemented (501)', async () => { @@ -104,21 +114,35 @@ it('treats operations without "responses" as not implemented (501)', async () => }, }), ) - - await withHandlers(handlers, () => - fetch('http://localhost/no-responses'), - ).then((res) => { - expect(res.status).toEqual(501) - }) - - await withHandlers(handlers, () => - fetch('http://localhost/empty-responses'), - ).then((res) => { - expect(res.status).toEqual(501) - }) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/no-responses', + }, + response: { + status: 501, + statusText: 'Not Implemented', + headers: [['content-type', 'text/plain;charset=UTF-8']], + body: 'Not Implemented', + }, + }, + { + handler: { + method: 'GET', + path: 'http://localhost/empty-responses', + }, + response: { + status: 501, + statusText: 'Not Implemented', + headers: [['content-type', 'text/plain;charset=UTF-8']], + body: 'Not Implemented', + }, + }, + ]) }) -it('responds with an empty 200 to a request without explicit 200 response', async () => { +it('treats responses without a 200 scenario as not implemented', async () => { const handlers = await fromOpenApi( createOpenApiSpec({ paths: { @@ -134,11 +158,11 @@ it('responds with an empty 200 to a request without explicit 200 response', asyn }), ) - const res = await withHandlers(handlers, () => - fetch('http://localhost/no-200'), - ) - expect(res.status).toEqual(200) - expect(await res.text()).toEqual('') + expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/no-200') + }), + ).toEqualResponse(new Response('Not Implemented', { status: 501 })) }) it('responds with 501 to a request for explicit non-existing response status', async () => { @@ -156,19 +180,17 @@ it('responds with 501 to a request for explicit non-existing response status', a }), ) - await withHandlers(handlers, () => - fetch('http://localhost/resource?response=200'), - ).then(async (res) => { - expect(res.status).toEqual(501) - expect(await res.text()).toEqual('') - }) + await expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/resource?response=200') + }), + ).toEqualResponse(new Response('Not Implemented', { status: 501 })) - await withHandlers(handlers, () => - fetch('http://localhost/resource?response=404'), - ).then(async (res) => { - expect(res.status).toEqual(501) - expect(await res.text()).toEqual('') - }) + await expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/resource?response=404') + }), + ).toEqualResponse(new Response('Not Implemented', { status: 501 })) }) it('respects the "Accept" request header', async () => { @@ -196,27 +218,56 @@ it('respects the "Accept" request header', async () => { ) // The "Accept" request header with a single value. - await withHandlers(handlers, () => { - return fetch('http://localhost/user', { + await expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/user', { + headers: { + Accept: 'application/xml', + }, + }) + }), + ).toEqualResponse( + new Response('xml-1', { + status: 200, headers: { - Accept: 'application/xml', + 'Content-Type': 'application/xml', }, - }) - }).then(async (res) => { - expect(res.status).toEqual(200) - expect(await res.text()).toEqual(`xml-1`) - }) + }), + ) - // The "Accept" request header with multiple values. - await withHandlers(handlers, () => { - return fetch('http://localhost/user', { + await expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/user', { + headers: { + Accept: 'application/json', + }, + }) + }), + ).toEqualResponse( + new Response(JSON.stringify({ id: 'user-1' }), { + status: 200, headers: { - Accept: 'application/json, application/xml', + 'Content-Type': 'application/json', }, - }) - }).then(async (res) => { - expect(res.status).toEqual(200) - // The first MimeType is used for the mocked data. - expect(await res.text()).toEqual(`{"id":"user-1"}`) - }) + }), + ) + + // Uses the response matching the first value of the "Accept" + // request header if multiple response mime types are accepted. + await expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/user', { + headers: { + Accept: 'application/json, application/xml', + }, + }) + }), + ).toEqualResponse( + new Response(JSON.stringify({ id: 'user-1' }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }), + ) }) diff --git a/test/oas/oas-response-headers.test.ts b/test/oas/oas-response-headers.test.ts index dbca29d..58a7903 100644 --- a/test/oas/oas-response-headers.test.ts +++ b/test/oas/oas-response-headers.test.ts @@ -1,6 +1,6 @@ import { fromOpenApi } from '../../src/fromOpenApi/fromOpenApi' import { createOpenApiSpec } from '../../test/support/createOpenApiSpec' -import { withHandlers } from '../support/withHandlers' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' it('supports response headers', async () => { const handlers = await fromOpenApi( @@ -38,20 +38,27 @@ it('supports response headers', async () => { }), ) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/user') - }) - - expect(res.status).toEqual(200) - const headers = new Headers(res.headers) - - expect(Object.fromEntries(headers.entries())).toEqual({ - 'content-type': 'text/plain', - 'x-powered-by': 'msw', - // Header values are always strings. - 'x-rate-limit-remaining': expect.any(String), - 'x-rate-limit-reset': expect.stringMatching( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+?Z$/, - ), - }) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/user', + }, + response: { + status: 200, + statusText: 'OK', + headers: [ + ['content-type', 'text/plain'], + ['x-rate-limit-remaining', expect.any(String)], + [ + 'x-rate-limit-reset', + expect.stringMatching( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+?Z$/, + ), + ], + ], + body: expect.any(String), + }, + }, + ]) }) diff --git a/test/oas/oas-response.test.ts b/test/oas/oas-response.test.ts index 0769b1d..9157524 100644 --- a/test/oas/oas-response.test.ts +++ b/test/oas/oas-response.test.ts @@ -1,36 +1,48 @@ import { fromOpenApi } from '../../src/fromOpenApi/fromOpenApi' -import { withHandlers } from '../support/withHandlers' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' it('supports explicit response example', async () => { const document = require('./fixtures/response-example.json') const handlers = await fromOpenApi(document) - - const res = await withHandlers(handlers, () => { - return fetch('https://example.com/user') - }) - - expect(res.status).toBe(200) - expect(res.headers.get('content-type')).toBe('application/json') - expect(await res.json()).toEqual({ - id: 'abc-123', - firstName: 'John', - lastName: 'Maverick', - }) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'https://example.com/user', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify({ + id: 'abc-123', + firstName: 'John', + lastName: 'Maverick', + }), + }, + }, + ]) }) it('supports a referenced response example', async () => { const document = require('./fixtures/response-ref') const handlers = await fromOpenApi(document) - - const res = await withHandlers(handlers, () => { - return fetch('https://example.com/user') - }) - - expect(res.status).toBe(200) - expect(res.headers.get('content-type')).toBe('application/json') - expect(await res.json()).toEqual({ - id: 'abc-123', - firstName: 'John', - lastName: 'Maverick', - }) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'https://example.com/user', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify({ + id: 'abc-123', + firstName: 'John', + lastName: 'Maverick', + }), + }, + }, + ]) }) diff --git a/test/oas/oas-servers.test.ts b/test/oas/oas-servers.test.ts index 7d86dec..0290b87 100644 --- a/test/oas/oas-servers.test.ts +++ b/test/oas/oas-servers.test.ts @@ -1,22 +1,18 @@ import { fromOpenApi } from '../../src/fromOpenApi/fromOpenApi' import { createOpenApiSpec } from '../support/createOpenApiSpec' -import { withHandlers } from '../support/withHandlers' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' -it('supports a single absolute server url', async () => { +it('supports absolute server url', async () => { const handlers = await fromOpenApi( createOpenApiSpec({ - servers: [ - { - url: 'https://example.com', - }, - ], + servers: [{ url: 'https://example.com' }], paths: { '/numbers': { get: { responses: { 200: { content: { - 'application/xml': { + 'application/json': { example: [1, 2, 3], }, }, @@ -27,26 +23,30 @@ it('supports a single absolute server url', async () => { }, }), ) - - const res = await withHandlers(handlers, () => { - return fetch('https://example.com/numbers') - }) - - expect(res.status).toEqual(200) - expect(await res.json()).toEqual([1, 2, 3]) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + // Must rebase request URLs against the "servers[0].url" + path: 'https://example.com/numbers', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([1, 2, 3]), + }, + }, + ]) }) -it('supports a single relative server url', async () => { +it('supports relative server url', async () => { const handlers = await fromOpenApi( createOpenApiSpec({ - servers: [ - { - url: '/v2', - }, - ], + servers: [{ url: '/v2' }], paths: { '/token': { - get: { + post: { responses: { 200: { content: { @@ -61,16 +61,23 @@ it('supports a single relative server url', async () => { }, }), ) - - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/v2/token') - }) - - expect(res.status).toEqual(200) - expect(await res.text()).toEqual('abc-123') + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'POST', + path: 'http://localhost/v2/token', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'plain/text']]), + body: 'abc-123', + }, + }, + ]) }) -it('supports multiple absolute server urls', async () => { +it('supports multiple server urls', async () => { const handlers = await fromOpenApi( createOpenApiSpec({ servers: [{ url: 'https://example.com' }, { url: 'https://v2.mswjs.io' }], @@ -80,7 +87,7 @@ it('supports multiple absolute server urls', async () => { responses: { 200: { content: { - 'application/xml': { + 'application/json': { example: [1, 2, 3], }, }, @@ -91,18 +98,32 @@ it('supports multiple absolute server urls', async () => { }, }), ) - - const responses = await withHandlers(handlers, () => { - return Promise.all([ - fetch('https://example.com/numbers'), - fetch('https://v2.mswjs.io/numbers'), - ]) - }) - - for (const res of responses) { - expect(res.status).toEqual(200) - expect(await res.json()).toEqual([1, 2, 3]) - } + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'https://example.com/numbers', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([1, 2, 3]), + }, + }, + { + handler: { + method: 'GET', + path: 'https://v2.mswjs.io/numbers', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify([1, 2, 3]), + }, + }, + ]) }) it('supports the "basePath" url', async () => { @@ -115,7 +136,7 @@ it('supports the "basePath" url', async () => { responses: { 200: { content: { - 'application/xml': { + 'application/json': { example: ['a', 'b', 'c'], }, }, @@ -126,11 +147,18 @@ it('supports the "basePath" url', async () => { }, }), ) - - const res = await withHandlers(handlers, () => { - return fetch('https://example.com/strings') - }) - - expect(res.status).toEqual(200) - expect(await res.json()).toEqual(['a', 'b', 'c']) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'https://example.com/strings', + }, + response: { + status: 200, + statusText: 'OK', + headers: expect.arrayContaining([['content-type', 'application/json']]), + body: JSON.stringify(['a', 'b', 'c']), + }, + }, + ]) }) diff --git a/test/oas/petstore.test.ts b/test/oas/petstore.test.ts index ebeaa98..acbf037 100644 --- a/test/oas/petstore.test.ts +++ b/test/oas/petstore.test.ts @@ -50,23 +50,23 @@ const entities = { } it('POST /pet', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet', { method: 'POST' }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual(entities.pet) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual(entities.pet) }) it('PUT /pet', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet', { method: 'PUT' }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual(entities.pet) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual(entities.pet) }) it('GET /pet/findByStatus', async () => { @@ -82,60 +82,56 @@ it('GET /pet/findByStatus', async () => { }) it('GET /pet/findByTags', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet/findByTags') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual( + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual( expect.arrayContaining([expect.objectContaining(entities.pet)]), ) }) it('GET /pet/{petId}', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet/abc-123') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual(entities.pet) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual(entities.pet) }) it('POST /pet/{petId}', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet/abc-123', { method: 'POST' }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual(null) - expect(await res.text()).toEqual('') + // Petstore does not describe a 200 scenario for this POST endpoint. + expect(response.status).toEqual(501) + expect(await response.text()).toEqual('Not Implemented') }) it('DELETE /pet/{petId}', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet/abc-123', { method: 'DELETE' }) }) - // The "DELETE /pet/{petId}" does not describe a 200 response. - // This implies that successful resource deletion is responded with - // an empty 200 OK response that needs no explicit specification. - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual(null) - expect(await res.text()).toEqual('') + expect(response.status).toEqual(501) + expect(await response.text()).toEqual('Not Implemented') }) it('POST http://localhost/v3/pet/:petId/uploadImage', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/pet/abc-123/uploadImage', { method: 'POST', }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/json') - expect(await res.json()).toEqual({ + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/json') + expect(await response.json()).toEqual({ code: expect.any(Number), message: expect.any(String), type: expect.any(String), @@ -146,14 +142,14 @@ it('POST http://localhost/v3/pet/:petId/uploadImage', async () => { * Inventory. */ it('GET /store/inventory', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/store/inventory') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/json') + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/json') - const json = await res.json() + const json = await response.json() const keys = Object.keys(json) // Empty response is also okay, additional properties are optional. @@ -169,98 +165,97 @@ it('GET /store/inventory', async () => { }) it('POST /store/order', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/store/order', { method: 'POST' }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/json') - expect(await res.json()).toEqual(entities.order) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/json') + expect(await response.json()).toEqual(entities.order) }) it('GET /store/order/{orderId}', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/store/order/abc-123') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual(entities.order) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual(entities.order) }) /** * User. */ it('POST /user', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/user', { method: 'POST' }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/json') - expect(await res.json()).toEqual(entities.user) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/json') + expect(await response.json()).toEqual(entities.user) }) it('POST /user/createWithList', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/user/createWithList', { method: 'POST', }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual(entities.user) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual(entities.user) }) it('GET /user/login', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/user/login') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.text()).toEqual(expect.any(String)) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.text()).toEqual(expect.any(String)) }) it('GET /user/logout', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/user/logout') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual(null) - expect(await res.text()).toEqual('') + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual(null) + expect(await response.text()).toEqual('') }) it('GET /user/:username', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/user/john-james') }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual('application/xml') - expect(await res.json()).toEqual(entities.user) + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual('application/xml') + expect(await response.json()).toEqual(entities.user) }) it('PUT /user/:username', async () => { - const res = await withHandlers(handlers, () => { + const response = await withHandlers(handlers, () => { return fetch('http://localhost/v3/user/john-james', { method: 'PUT' }) }) - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual(null) - expect(await res.text()).toEqual('') + expect(response.status).toEqual(200) + expect(response.headers.get('content-type')).toEqual(null) + expect(await response.text()).toEqual('') }) it('DELETE /user/:username', async () => { - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/v3/user/john-james', { - method: 'DELETE', - }) - }) - - expect(res.status).toEqual(200) - expect(res.headers.get('content-type')).toEqual(null) - expect(await res.text()).toEqual('') + // Does not describe the 200 response example. + await expect( + await withHandlers(handlers, () => { + return fetch('http://localhost/v3/user/john-james', { + method: 'DELETE', + }) + }), + ).toEqualResponse(new Response('Not Implemented', { status: 501 })) }) diff --git a/test/support/createOpenApiSpec.ts b/test/support/createOpenApiSpec.ts index 47a35b1..2b61427 100644 --- a/test/support/createOpenApiSpec.ts +++ b/test/support/createOpenApiSpec.ts @@ -4,7 +4,7 @@ export function createOpenApiSpec( document: Partial, ): OpenAPI.Document { return Object.assign( - {}, + {} as OpenAPI.Document, { openapi: '3.0.0', info: { diff --git a/test/support/inspectHandler.ts b/test/support/inspectHandler.ts new file mode 100644 index 0000000..7df80f2 --- /dev/null +++ b/test/support/inspectHandler.ts @@ -0,0 +1,98 @@ +import { RequestHandler, HttpHandler, GraphQLHandler } from 'msw' + +export interface InspectedHandler { + handler: SerializedHandler + response?: SerializedResponse +} + +type SerializedHandler = H extends HttpHandler + ? { + method: string + path: string + } + : H extends GraphQLHandler + ? { + kind: string + name: string + } + : never + +export interface SerializedResponse { + status: number + statusText?: string + headers: Array<[string, string]> + body?: string +} + +function isAbsoluteUrl(url: string) { + return (url.indexOf('://') > 0 || url.indexOf('//') === 0) +} + +async function inspectHandler( + handler: H, +): Promise> { + const requestId = Math.random().toString(16).slice(2) + + if (handler instanceof HttpHandler) { + const pathOfHandler = handler.info.path as string + + const locationOrigin = typeof location !== 'undefined' ? location.origin : 'http://localhost' + const fullQualifiedUrl = isAbsoluteUrl(pathOfHandler) ? pathOfHandler : `${locationOrigin}${pathOfHandler}` + + const result = await handler.run({ + request: new Request(fullQualifiedUrl, { + method: handler.info.method.toString(), + }), + requestId, + }) + + return { + handler: { + method: handler.info.method.toString().toUpperCase(), + path: fullQualifiedUrl, + }, + response: await serializeResponse(result?.response), + } + } + + if (handler instanceof GraphQLHandler) { + const result = await handler.run({ + request: new Request('', { + method: 'POST', + body: JSON.stringify({}), + }), + requestId, + }) + + return { + handler: { + kind: handler.info.operationType, + name: handler.info.operationName, + }, + response: await serializeResponse(result?.response), + } + } + + throw new Error( + `Failed to inspect handler "${handler.info.header}": unknown handler type`, + ) +} + +export async function inspectHandlers(handlers: Array) { + return await Promise.all(handlers.map(inspectHandler)) +} + +async function serializeResponse( + response?: Response, +): Promise { + if (!response) { + return + } + + return { + status: response.status, + statusText: response.statusText, + headers: Array.from(response.headers), + body: await response.text(), + } +} diff --git a/test/support/withHandlers.ts b/test/support/withHandlers.ts index 278701d..f6dca5d 100644 --- a/test/support/withHandlers.ts +++ b/test/support/withHandlers.ts @@ -1,8 +1,13 @@ import { RequestHandler } from 'msw' import { setupServer } from 'msw/node' +/** + * Creates an MSW `server` instance, populates it + * with the given `handlers`, runs the `callback`, + * and cleans up afterward. + */ export async function withHandlers( - handlers: RequestHandler[], + handlers: Array, callback: () => Promise, ): Promise { const server = setupServer(...handlers) diff --git a/test/traffic/fromTraffic.test.ts b/test/traffic/fromTraffic.test.ts index 6d580a1..4268fba 100644 --- a/test/traffic/fromTraffic.test.ts +++ b/test/traffic/fromTraffic.test.ts @@ -1,10 +1,11 @@ /** * @vitest-environment node */ -import { Har, Response } from 'har-format' -import { fromTraffic, toResponseBody } from '../../src/fromTraffic/fromTraffic' -import { decodeBase64String } from '../../src/fromTraffic/utils/decodeBase64String' +import Har from 'har-format' +import { fromTraffic } from '../../src/fromTraffic/fromTraffic' import { readArchive } from './utils' +import { toResponse } from '../../src/fromTraffic/utils/harUtils' +import { inspectHandlers } from '../support/inspectHandler' describe('fromTraffic', () => { it('throws an exception given no HAR object', () => { @@ -41,7 +42,7 @@ describe('fromTraffic', () => { }) }) - it('supports skipping an entry using the "mapEntry" function', () => { + it('supports skipping an entry using the "mapEntry" function', async () => { const handlers = fromTraffic( { log: { @@ -70,7 +71,7 @@ describe('fromTraffic', () => { }, ], }, - } as Har, + } as Har.Har, (entry) => { if (entry.request.url === 'https://api.stripe.com') { return entry @@ -79,7 +80,11 @@ describe('fromTraffic', () => { ) expect(handlers).toHaveLength(1) - expect(handlers[0].info.path).toEqual('https://api.stripe.com') + const [initialRequestHandler] = await inspectHandlers(handlers) + expect(initialRequestHandler.handler).toEqual({ + method: 'GET', + path: 'https://api.stripe.com', + }) }) }) @@ -87,7 +92,7 @@ describe('toResponseBody', () => { function createResponse( body?: string, options?: { encoding?: string }, - ): Response { + ): Har.Response { return { status: 200, statusText: 'OK', @@ -107,22 +112,22 @@ describe('toResponseBody', () => { } it('returns undefined given response with no body', () => { - expect(toResponseBody(createResponse(undefined))).toEqual(undefined) + expect(toResponse(createResponse(undefined)).body).toBeNull() }) - it('returns a decoded text body given a base64-encoded response body', () => { - const encoder = new TextEncoder() - const body = encoder.encode('hello world') - const expectedBody = decodeBase64String( - encoder.encode('hello world').toString(), + it('returns a decoded text body given a base64-encoded response body', async () => { + const exepctedBody = 'hello world' + const response = toResponse( + createResponse(btoa(exepctedBody), { encoding: 'base64' }), ) + const responseText = await response.text() - expect( - toResponseBody(createResponse(body.toString(), { encoding: 'base64' })), - ).toEqual(expectedBody) + expect(responseText).toEqual(exepctedBody) }) - it('returns a plain text response body as-is', () => { - expect(toResponseBody(createResponse('hello world'))).toEqual('hello world') + it('returns a plain text response body as-is', async () => { + const bodyResponse = toResponse(createResponse('hello world')) + const textOfResponse = await bodyResponse.text() + expect(textOfResponse).toEqual('hello world') }) }) diff --git a/test/traffic/response-body.test.ts b/test/traffic/response-body.test.ts index 11a221d..c6870d7 100644 --- a/test/traffic/response-body.test.ts +++ b/test/traffic/response-body.test.ts @@ -1,107 +1,95 @@ -/** - * @vitest-environment jsdom - */ import * as fs from 'fs' import * as path from 'path' import { fromTraffic } from '../../src/fromTraffic/fromTraffic' -import { withHandlers } from '../../test/support/withHandlers' -import { readArchive, headersAfterMsw, normalizeLocalhost } from './utils' +import { readArchive, normalizeLocalhost, _toHeaders } from './utils' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' -// Archives. -const responseText = readArchive( - 'test/traffic/fixtures/archives/response-text.har', -) -const responseJson = readArchive( - 'test/traffic/fixtures/archives/response-json.har', -) -const responseBinary = readArchive( - 'test/traffic/fixtures/archives/response-binary.har', -) -const responseCompressed = readArchive( - 'test/traffic/fixtures/archives/response-compressed.har', -) -const responseCookies = readArchive( - 'test/traffic/fixtures/archives/response-cookies.har', -) - -it('mocks a recorded text response', async () => { - const handlers = fromTraffic(responseText, normalizeLocalhost) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/text') - }) - - expect(res.status).toEqual(200) - expect(Object.fromEntries(res.headers.entries())).toEqual( - headersAfterMsw(responseText.log.entries[0].response.headers), - ) - expect(await res.text()).toEqual('hello world') +it('creates a request handler from a recorded text response', async () => { + const har = readArchive('test/traffic/fixtures/archives/response-text.har') + const handlers = fromTraffic(har, normalizeLocalhost) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/text', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: 'hello world', + }, + }, + ]) }) -it('mocks a recorded JSON response', async () => { - const handlers = fromTraffic(responseJson, normalizeLocalhost) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/json') - }) - - expect(res.status).toEqual(200) - expect(Object.fromEntries(res.headers.entries())).toEqual( - headersAfterMsw(responseJson.log.entries[0].response.headers), - ) - expect(await res.json()).toEqual({ - id: 'abc-123', - firstName: 'John', - }) +it('creates a request handler from a recorded json response', async () => { + const har = readArchive('test/traffic/fixtures/archives/response-json.har') + const handlers = fromTraffic(har, normalizeLocalhost) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/json', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: JSON.stringify({ + id: 'abc-123', + firstName: 'John', + }), + }, + }, + ]) }) -it('mocks a recorded binary (base64) response', async () => { - const handlers = fromTraffic(responseBinary, normalizeLocalhost) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/binary') - }) - const blob = await res.blob() - - expect(res.status).toEqual(200) - expect(Object.fromEntries(res.headers.entries())).toEqual( - headersAfterMsw(responseBinary.log.entries[0].response.headers), - ) - +it('creates a request handler from a recorded binary response', async () => { + const har = readArchive('test/traffic/fixtures/archives/response-binary.har') + const handlers = fromTraffic(har, normalizeLocalhost) const imageBinary = fs .readFileSync(path.resolve(__dirname, 'fixtures/image.jpg')) // Convert to "base64" because the browser's traffic encodes binaries // using "base64" encoding. This alters the length of the response blob. .toString('base64') - expect(blob.type).toEqual('image/jpg') - expect(blob.size).toEqual(imageBinary.length) - expect(await blob.text()).toEqual(imageBinary) -}) - -it('mocks a compressed recorded JSON response', async () => { - const handlers = fromTraffic(responseCompressed, normalizeLocalhost) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/json-compressed') - }) - - expect(res.status).toEqual(200) - expect(Object.fromEntries(res.headers.entries())).toEqual( - headersAfterMsw(responseCompressed.log.entries[0].response.headers), - ) - expect(await res.json()).toEqual({ - id: 'abc-123', - firstName: 'John', - }) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/binary', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: imageBinary, + }, + }, + ]) }) -it('propagates recoded response cookies to the mocked response', async () => { - const handlers = fromTraffic(responseCookies, normalizeLocalhost) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/cookies') - }) - - expect(res.status).toEqual(200) - expect(Object.fromEntries(res.headers.entries())).toEqual( - headersAfterMsw(responseCookies.log.entries[0].response.headers), +it('creates a request handler from a compressed recorded json response', async () => { + const har = readArchive( + 'test/traffic/fixtures/archives/response-compressed.har', ) - expect(document.cookie).toEqual('secret-token=abc-123') - expect(await res.text()).toEqual('yummy') + const handlers = fromTraffic(har, normalizeLocalhost) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/json-compressed', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: JSON.stringify({ + id: 'abc-123', + firstName: 'John', + }), + }, + }, + ]) }) diff --git a/test/traffic/response-cookies.test.ts b/test/traffic/response-cookies.test.ts new file mode 100644 index 0000000..9d3ffab --- /dev/null +++ b/test/traffic/response-cookies.test.ts @@ -0,0 +1,22 @@ +import { fromTraffic } from '../../src/fromTraffic/fromTraffic' +import { readArchive, normalizeLocalhost, _toHeaders } from './utils' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' + +it('preserves the "Set-Cookie" response header', async () => { + const har = readArchive('test/traffic/fixtures/archives/response-cookies.har') + const handlers = fromTraffic(har, normalizeLocalhost) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/cookies', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: 'yummy', + }, + }, + ]) +}) diff --git a/test/traffic/response-order.test.ts b/test/traffic/response-order.test.ts index a21aa94..35d793c 100644 --- a/test/traffic/response-order.test.ts +++ b/test/traffic/response-order.test.ts @@ -1,34 +1,36 @@ import { fromTraffic } from '../../src/fromTraffic/fromTraffic' -import { withHandlers } from '../../test/support/withHandlers' -import { normalizeLocalhost, readArchive } from './utils' - -const requestOrder = readArchive( - 'test/traffic/fixtures/archives/request-order.har', -) +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' +import { _toHeaders, normalizeLocalhost, readArchive } from './utils' it('respects the response sequence when repeatedly requesting the same endpoint', async () => { - const handlers = fromTraffic(requestOrder, normalizeLocalhost) - const [firstResponse, secondResponse, thirdResponse] = await withHandlers( - handlers, - async () => { - // Intentionally request the same endpoint. - return [ - await fetch('http://localhost/resource'), - await fetch('http://localhost/resource'), - await fetch('http://localhost/resource'), - ] + const har = readArchive('test/traffic/fixtures/archives/request-order.har') + const handlers = fromTraffic(har, normalizeLocalhost) + expect(await inspectHandlers(handlers)).toEqual([ + // The first request handler returns a unique response. + { + handler: { + method: 'GET', + path: 'http://localhost/resource', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: 'one', + }, }, - ) - - // The first request receives a unique response. - expect(firstResponse.status).toEqual(200) - expect(await firstResponse.text()).toEqual('one') - - // The second request receives a different response. - expect(secondResponse.status).toEqual(200) - expect(await secondResponse.text()).toEqual('two') - - // Any subsequent request receives the latest (second) response. - expect(thirdResponse.status).toEqual(200) - expect(await thirdResponse.text()).toEqual('two') + // Any subsequent request handlers produce the latest (second) response. + { + handler: { + method: 'GET', + path: 'http://localhost/resource', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[1].response.headers), + body: 'two', + }, + }, + ]) }) diff --git a/test/traffic/response-stream.test.ts b/test/traffic/response-stream.test.ts index 35ba2ef..462a8b7 100644 --- a/test/traffic/response-stream.test.ts +++ b/test/traffic/response-stream.test.ts @@ -1,17 +1,22 @@ import { fromTraffic } from '../../src/fromTraffic/fromTraffic' -import { withHandlers } from '../../test/support/withHandlers' -import { normalizeLocalhost, readArchive } from './utils' - -const responseStream = readArchive( - 'test/traffic/fixtures/archives/response-stream.har', -) +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' +import { normalizeLocalhost, readArchive, _toHeaders } from './utils' it('mocks a recorded response stream', async () => { - const handlers = fromTraffic(responseStream, normalizeLocalhost) - const res = await withHandlers(handlers, () => { - return fetch('http://localhost/stream') - }) - - expect(res.status).toEqual(200) - expect(await res.text()).toEqual('this is a chunked response') + const har = readArchive('test/traffic/fixtures/archives/response-stream.har') + const handlers = fromTraffic(har, normalizeLocalhost) + expect(await inspectHandlers(handlers)).toEqual([ + { + handler: { + method: 'GET', + path: 'http://localhost/stream', + }, + response: { + status: 200, + statusText: 'OK', + headers: _toHeaders(har.log.entries[0].response.headers), + body: 'this is a chunked response', + }, + }, + ]) }) diff --git a/test/traffic/utils/index.ts b/test/traffic/utils/index.ts index 58cd536..6068f3d 100644 --- a/test/traffic/utils/index.ts +++ b/test/traffic/utils/index.ts @@ -1,8 +1,8 @@ import * as fs from 'fs' -import { Har, Header } from 'har-format' -import { MapEntryFn } from '../../../src/fromTraffic/fromTraffic' +import * as Har from 'har-format' +import { MapEntryFunction } from '../../../src/fromTraffic/fromTraffic' -export function readArchive(archivePath: string): Har { +export function readArchive(archivePath: string): Har.Har { return JSON.parse(fs.readFileSync(archivePath, 'utf8')) } @@ -11,17 +11,34 @@ export function readArchive(archivePath: string): Har { * - Replaces `127.0.0.1` with `localhost`. * - Removes ports to prevent request mismatches. */ -export const normalizeLocalhost: MapEntryFn = (entry) => { - const { request } = entry - entry.request = { - ...request, - url: request.url.replace(/(127\.0\.0\.1)(:\d{4,})/, 'localhost'), +export const normalizeLocalhost: MapEntryFunction = (entry) => { + const url = new URL(entry.request.url) + // Disregard the original request host so we can always + // assert against "localhost" in tests. + url.host = 'localhost' + // Disregard the original port for the same reason. + url.port = '' + entry.request.url = url.href + return entry +} + +export function _toHeaders( + trafficHeaders: Array, +): Array<[string, string]> { + const headers: Array<[string, string]> = [] + + for (const header of trafficHeaders) { + headers.push([header.name.toLowerCase(), header.value]) } - return entry + // Sort the headers alphabetically so their order + // doesn't matter when asserting in test. + headers.sort() + + return headers } -function toHeaders(trafficHeaders: Header[]): Headers { +function toHeaders(trafficHeaders: Array): Headers { return trafficHeaders.reduce((headers, { name, value }) => { headers.set(name, value) return headers @@ -29,7 +46,7 @@ function toHeaders(trafficHeaders: Header[]): Headers { } export function headersAfterMsw( - trafficHeaders: Header[], + trafficHeaders: Array, ): Record { const headers = toHeaders(trafficHeaders) diff --git a/tsconfig.build.json b/tsconfig.build.json index 847d908..45c47b9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -9,5 +9,5 @@ "declaration": true }, "include": ["src"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "**/*.test.ts"] } diff --git a/tsconfig.test.json b/tsconfig.test.json index cd9d19a..8a4045b 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -10,5 +10,5 @@ "types": ["vitest/globals"], "lib": ["dom", "DOM.Iterable"] }, - "include": ["test"] + "include": ["./vitest.d.ts", "./vitest.setup.ts", "test", "src/**/*.test.ts"] } diff --git a/vitest.config.ts b/vitest.config.ts index abec1f4..c91b23a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, + setupFiles: ['./vitest.setup.ts'], environment: 'jsdom', environmentOptions: { jsdom: { diff --git a/vitest.d.ts b/vitest.d.ts new file mode 100644 index 0000000..be3c426 --- /dev/null +++ b/vitest.d.ts @@ -0,0 +1,14 @@ +import type { Assertion, AsymmetricMatchersContaining } from 'vitest' + +interface CustomMatchers { + /** + * Compare two `Uint8Array` arrays. + */ + toEqualBytes: (expected: Uint8Array) => R + toEqualResponse: (expected: Response) => Promise +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..a6cfe7e --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,88 @@ +import { invariant } from 'outvariant' + +expect.extend({ + toEqualBytes(actual: unknown, expected: unknown) { + invariant(isUint8Array(actual), 'Expected actual to be a Uint8Array') + invariant(isUint8Array(expected), 'Expected expected to be a Uint8Array') + + if (actual.length !== expected.length) { + return { + pass: false, + message: () => + `Expected Uint8Array of length (${expected.length}) but got (${actual.length})`, + actual: actual.length, + expected: expected.length, + } + } + + for (let i = 0; i < expected.length; i++) { + if (actual[i] !== expected[i]) { + return { + pass: false, + message: () => + `Expected Uint8Array to be equal but found a difference at index ${i}`, + actual: actual[i], + expected: expected[i], + } + } + } + + return { + pass: true, + message: () => '...', + } + }, + async toEqualResponse(actual: Response, expected: Record) { + invariant( + actual instanceof Response, + 'Expected actual to be an instance of Response', + ) + invariant( + expected instanceof Response, + 'Expected expected to be an instance of Response', + ) + + // Status code. + if (actual.status !== expected.status) { + return { + pass: false, + message: () => 'Response status codes are not equal', + actual: actual.status, + expected: expected.status, + } + } + + // Headers. + if ( + !this.equals(Array.from(actual.headers), Array.from(expected.headers)) + ) { + return { + pass: false, + message: () => 'Response headers are not equal', + actual: Array.from(actual.headers), + expected: Array.from(expected.headers), + } + } + + // Body. + const actualBody = await actual.text() + const expectedBody = await expected.text() + if (actualBody !== expectedBody) { + return { + pass: false, + message: () => 'Response bodies are not equal', + actual: actualBody, + expected: expectedBody, + } + } + + return { + pass: true, + message: () => 'Responses are equal', + } + }, +}) + +function isUint8Array(value: unknown): value is Uint8Array { + return value?.constructor.name === 'Uint8Array' +}