From fea871cba982d594c2dcd985bff7dc7c7ac356ae Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 20 May 2024 20:01:29 +0200 Subject: [PATCH 01/23] chore: migrate to msw@2 --- package.json | 8 +- pnpm-lock.yaml | 463 +++++------------- src/fromOpenApi/fromOpenApi.ts | 98 ++-- .../response/createResponseResolver.ts | 61 --- .../response/transformers/bodyTransformer.ts | 62 --- .../transformers/headersTransformer.ts | 34 -- src/fromOpenApi/utils/openApiUtils.ts | 172 +++++++ src/fromTraffic/fromTraffic.ts | 212 ++------ src/fromTraffic/utils/decodeBase64String.ts | 2 - src/fromTraffic/utils/harUtils.ts | 46 ++ test/support/withHandlers.ts | 2 +- test/traffic/fromTraffic.test.ts | 17 +- 12 files changed, 468 insertions(+), 709 deletions(-) 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 create mode 100644 src/fromTraffic/utils/harUtils.ts diff --git a/package.json b/package.json index 94eb78e..322aa7c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ }, "packageManager": "pnpm@8.15.6", "peerDependencies": { - "msw": "^1.2.1" + "msw": "^2.3.0" }, "devDependencies": { "@commitlint/cli": "^17.6.5", @@ -58,7 +58,7 @@ "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 +73,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..ca7e0df 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': @@ -74,8 +68,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 +155,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'} @@ -807,6 +813,43 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} dev: true + /@inquirer/confirm@3.1.8: + resolution: {integrity: sha512-f3INZ+ca4dQdn+MQiq1yP/mOIR/Oc8BLRYuDh6ciToWd6z4W8yArfzjBCMQ0BPY8PcJKwZxGIt8Z6yNT32eSTw==} + engines: {node: '>=18'} + dependencies: + '@inquirer/core': 8.2.1 + '@inquirer/type': 1.3.2 + dev: true + + /@inquirer/core@8.2.1: + resolution: {integrity: sha512-TIcuQMn2qrtyYe0j136UpHeYpk7AcR/trKeT/7YY0vRgcS9YSfJuQ2+PudPhSofLLsHNnRYAHScQCcVZrJkMqA==} + engines: {node: '>=18'} + dependencies: + '@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 + + /@inquirer/figures@1.0.2: + resolution: {integrity: sha512-4F1MBwVr3c/m4bAUef6LgkvBfSjzwH+OfldgHqcuacWwSUetFebM2wi58WfG9uk1rR98U6GwLed4asLJbwdV5w==} + engines: {node: '>=18'} + dev: true + + /@inquirer/type@1.3.2: + resolution: {integrity: sha512-5Frickan9c89QbPkSu6I6y8p+9eR6hZkdPahGmNDsTFX8FHLPAozyzCZMKUeW8FyYwnlCKUjqIEqxY+UctARiw==} + engines: {node: '>=18'} + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -867,28 +910,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 +948,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 +975,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: @@ -1124,18 +1171,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,10 +1215,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 @@ -1186,14 +1227,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==} @@ -1234,10 +1284,13 @@ 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 + /@types/statuses@2.0.5: + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + dev: true + + /@types/wrap-ansi@3.0.0: + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + dev: true /@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==} @@ -1414,17 +1467,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 @@ -1613,21 +1655,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 +1669,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} @@ -1686,13 +1709,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 +1798,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 +1844,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 +1858,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: @@ -1976,6 +1983,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'} @@ -2120,12 +2132,6 @@ packages: 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'} @@ -2498,11 +2504,6 @@ packages: 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 +2588,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 @@ -2632,13 +2624,6 @@ packages: 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} @@ -2697,12 +2682,6 @@ packages: 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'} @@ -2929,13 +2908,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 +2915,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 +2998,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'} @@ -3068,40 +3036,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 +3052,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 +3068,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 +3075,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 @@ -3208,13 +3130,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 +3153,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 @@ -3671,44 +3581,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: @@ -3734,18 +3640,6 @@ packages: 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: @@ -3845,26 +3739,6 @@ packages: 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==} @@ -4040,11 +3914,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'} @@ -4317,11 +4186,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 +4273,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 +4448,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: @@ -4768,13 +4623,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 +4645,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: @@ -4979,9 +4823,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 +4848,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 +4881,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 +5023,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 +5052,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 +5060,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'} diff --git a/src/fromOpenApi/fromOpenApi.ts b/src/fromOpenApi/fromOpenApi.ts index df6edea..1009b20 100644 --- a/src/fromOpenApi/fromOpenApi.ts +++ b/src/fromOpenApi/fromOpenApi.ts @@ -1,67 +1,75 @@ -import { RequestHandler, rest } from 'msw' -import { OpenAPIV3, OpenAPIV2 } from 'openapi-types' +import { RequestHandler, HttpHandler, http } from 'msw' +import type { OpenAPIV3, OpenAPIV2 } from 'openapi-types' import SwaggerParser from '@apidevtools/swagger-parser' -import { createResponseResolver } from './response/createResponseResolver.js' import { normalizeSwaggerUrl } from './utils/normalizeSwaggerUrl.js' import { getServers } from './utils/getServers.js' import { isAbsoluteUrl, joinPaths } from './utils/url.js' +import { createResponseResolver } from './utils/openApiUtils.js' + +type SupportedHttpMethods = keyof typeof http +const supportedHttpMethods = Object.keys( + http, +) as unknown as SupportedHttpMethods /** * Generates request handlers from the given OpenAPI V2/V3 document. + * + * @example + * import specification from './api.oas.json' + * await fromOpenApi(specification) */ export async function fromOpenApi( document: string | OpenAPIV3.Document | OpenAPIV2.Document, ): Promise> { const specification = await SwaggerParser.dereference(document) + const handlers: Array = [] - 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 - - if ( - method !== 'get' && - method !== 'put' && - method !== 'post' && - method !== 'delete' && - method !== 'options' && - method !== 'head' && - method !== 'patch' - ) { - continue - } + for (const url in Object.entries(specification.paths)) { + const pathItem = specification.paths[url] as + | OpenAPIV2.PathItemObject + | OpenAPIV3.PathItemObject - const operation = pathItem[method] as OpenAPIV3.OperationObject - if (!operation) { - continue - } + for (const key of Object.keys(pathItem)) { + const method = key as keyof OpenAPIV2.PathItemObject - const serverUrls = getServers(specification) + // Ignore unsupported HTTP methods. + if (!isSupportedHttpMethod(method)) { + continue + } - for (const baseUrl of serverUrls) { - const path = normalizeSwaggerUrl(url) - const requestUrl = isAbsoluteUrl(baseUrl) - ? new URL(path, baseUrl).href - : joinPaths(path, baseUrl) + const operation = pathItem[method] as OpenAPIV3.OperationObject - handlers.push( - rest[method](requestUrl, createResponseResolver(operation)), - ) - } + if (!operation) { + continue } - return handlers - }, - [], - ) + 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) + + const handler = new HttpHandler( + method, + requestUrl, + createResponseResolver(operation), + { + /** + * @fixme Support `once` the same as in HAR? + */ + }, + ) + + handlers.push(handler) + } + } + } 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/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts new file mode 100644 index 0000000..a1ed657 --- /dev/null +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -0,0 +1,172 @@ +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 }) + } + if (Object.keys(responses).length === 0) { + return new Response('Not implemented', { status: 501 }) + } + + 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 }) + } + + responseObject = responseByStatus + } else { + const fallbackResponse = + (responses['200'] as OpenAPIV3.ResponseObject) || + (responses.default as OpenAPIV3.ResponseObject) + + if (!fallbackResponse) { + return new Response('Not implemented', { status: 501 }) + } + + responseObject = fallbackResponse + } + + return new Response(toBody(request, responseObject), { + status: Number(explicitResponseStatus || '200'), + headers: toHeaders(responseObject), + }) + } +} + +/** + * Get the Fetch API `Headers` from the OpenAPI response object. + */ +export function toHeaders( + responseObject: OpenAPIV3.ResponseObject, +): Headers | undefined { + if (!responseObject.headers) { + return undefined + } + + const headers = new Headers() + + for (const [headerName, headerObject] of Object.entries( + responseObject.headers, + )) { + 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)) + } + + 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(',') + + let mediaTypeObject: OpenAPIV3.MediaTypeObject | undefined + const responseContentTypes = Object.keys(content) + + // Lookup the first response content type that satisfies + // the expected request's "Accept" header. + if (acceptedContentTypes.length > 0) { + for (const acceptedContentType of acceptedContentTypes) { + const contentTypeRegExp = contentTypeToRegExp(acceptedContentType) + const matchingResponseContentType = responseContentTypes.find( + (responseContentType) => { + return contentTypeRegExp.test(responseContentType) + }, + ) + + if (matchingResponseContentType) { + mediaTypeObject = content[matchingResponseContentType] + break + } + } + } else { + // If the request didn't specify any "Accept" header, + // use the first response content type from the spec. + mediaTypeObject = content[responseContentTypes[0]] + } + + if (!mediaTypeObject) { + return null + } + + // If the response object has the body example, use it. + if (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 + + return firstExample.value + } + + // If the response is a JSON Schema, evolve and use it. + if (mediaTypeObject.schema) { + return evolveJsonSchema(mediaTypeObject.schema as OpenAPIV3.SchemaObject) + } + + return null +} + +function contentTypeToRegExp(contentType: string): RegExp { + return new RegExp(contentType.replace(/\/+/g, '\\/').replace(/\*/g, '.+?')) +} 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/decodeBase64String.ts b/src/fromTraffic/utils/decodeBase64String.ts index b28347f..63e0229 100644 --- a/src/fromTraffic/utils/decodeBase64String.ts +++ b/src/fromTraffic/utils/decodeBase64String.ts @@ -1,5 +1,3 @@ -import { Buffer } from 'buffer' - export function decodeBase64String(text: string): Uint8Array { return Uint8Array.from( Buffer.from(text, 'base64').toString('binary'), 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/support/withHandlers.ts b/test/support/withHandlers.ts index 278701d..ef605df 100644 --- a/test/support/withHandlers.ts +++ b/test/support/withHandlers.ts @@ -2,7 +2,7 @@ import { RequestHandler } from 'msw' import { setupServer } from 'msw/node' 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..8537d49 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 Har from 'har-format' +import { fromTraffic } from '../../src/fromTraffic/fromTraffic' import { decodeBase64String } from '../../src/fromTraffic/utils/decodeBase64String' import { readArchive } from './utils' +import { toResponse } from '../../src/fromTraffic/utils/harUtils' describe('fromTraffic', () => { it('throws an exception given no HAR object', () => { @@ -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,7 @@ describe('fromTraffic', () => { ) expect(handlers).toHaveLength(1) - expect(handlers[0].info.path).toEqual('https://api.stripe.com') + expect(handlers[0].info.header).toEqual('GET https://api.stripe.com') }) }) @@ -87,7 +88,7 @@ describe('toResponseBody', () => { function createResponse( body?: string, options?: { encoding?: string }, - ): Response { + ): Har.Response { return { status: 200, statusText: 'OK', @@ -107,7 +108,7 @@ describe('toResponseBody', () => { } it('returns undefined given response with no body', () => { - expect(toResponseBody(createResponse(undefined))).toEqual(undefined) + expect(toResponse(createResponse(undefined))).toEqual(undefined) }) it('returns a decoded text body given a base64-encoded response body', () => { @@ -118,11 +119,11 @@ describe('toResponseBody', () => { ) expect( - toResponseBody(createResponse(body.toString(), { encoding: 'base64' })), + toResponse(createResponse(body.toString(), { encoding: 'base64' })), ).toEqual(expectedBody) }) it('returns a plain text response body as-is', () => { - expect(toResponseBody(createResponse('hello world'))).toEqual('hello world') + expect(toResponse(createResponse('hello world'))).toEqual('hello world') }) }) From 5cde20555f72070158eb892a124d5850b0a38bef Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 25 May 2024 10:42:43 +0300 Subject: [PATCH 02/23] chore: add tests, simplify base64 handling --- src/fromOpenApi/schema/types/string.ts | 3 +- src/fromOpenApi/utils/toBase64.ts | 5 -- src/fromTraffic/utils/decodeBase64String.ts | 12 ++-- src/fromTraffic/utils/harUtils.test.ts | 77 +++++++++++++++++++++ tsconfig.build.json | 2 +- tsconfig.test.json | 2 +- 6 files changed, 87 insertions(+), 14 deletions(-) delete mode 100644 src/fromOpenApi/utils/toBase64.ts create mode 100644 src/fromTraffic/utils/harUtils.test.ts 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/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/utils/decodeBase64String.ts b/src/fromTraffic/utils/decodeBase64String.ts index 63e0229..7325caa 100644 --- a/src/fromTraffic/utils/decodeBase64String.ts +++ b/src/fromTraffic/utils/decodeBase64String.ts @@ -1,6 +1,8 @@ -export function decodeBase64String(text: string): Uint8Array { - return Uint8Array.from( - Buffer.from(text, 'base64').toString('binary'), - (char) => char.charCodeAt(0), - ) +export function decodeBase64String(data: string): Uint8Array { + const binaryString = atob(data) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return bytes } diff --git a/src/fromTraffic/utils/harUtils.test.ts b/src/fromTraffic/utils/harUtils.test.ts new file mode 100644 index 0000000..379ad02 --- /dev/null +++ b/src/fromTraffic/utils/harUtils.test.ts @@ -0,0 +1,77 @@ +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', () => { + expect( + toResponseBody({ + text: btoa('hello world'), + size: 11, + mimeType: 'text/plain', + encoding: 'base64', + }), + ).toEqual(Uint8Array.from('hello world', (c) => c.charCodeAt(0))) + }) + + it.todo('handles a compressed response body') +}) + +describe.todo(toResponse) 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..260e48c 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -10,5 +10,5 @@ "types": ["vitest/globals"], "lib": ["dom", "DOM.Iterable"] }, - "include": ["test"] + "include": ["test", "src/**/*.test.ts"] } From b7d1ef467d0b179cfdb2f0146e89753c68519ce0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 25 May 2024 10:49:27 +0300 Subject: [PATCH 03/23] chore: remove old logo --- source-logo.png | Bin 13306 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 source-logo.png 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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 Date: Sat, 25 May 2024 10:49:39 +0300 Subject: [PATCH 04/23] chore(withHandlers): describe the function --- test/support/withHandlers.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/support/withHandlers.ts b/test/support/withHandlers.ts index ef605df..f6dca5d 100644 --- a/test/support/withHandlers.ts +++ b/test/support/withHandlers.ts @@ -1,6 +1,11 @@ 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: Array, callback: () => Promise, From 162f6c0a049c811dfed4a46c40e7306777c03368 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 25 May 2024 20:17:20 +0200 Subject: [PATCH 05/23] test: rewrite "response-body" tests for fromTraffic --- test/support/inspectHandler.ts | 89 ++++++++++++++ test/traffic/response-body.test.ts | 166 ++++++++++++-------------- test/traffic/response-cookies.test.ts | 22 ++++ test/traffic/utils/index.ts | 39 ++++-- 4 files changed, 216 insertions(+), 100 deletions(-) create mode 100644 test/support/inspectHandler.ts create mode 100644 test/traffic/response-cookies.test.ts diff --git a/test/support/inspectHandler.ts b/test/support/inspectHandler.ts new file mode 100644 index 0000000..8c7e487 --- /dev/null +++ b/test/support/inspectHandler.ts @@ -0,0 +1,89 @@ +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 + +interface SerializedResponse { + status: number + statusText?: string + headers: Array<[string, string]> + body?: string +} + +async function inspectHandler( + handler: H, +): Promise> { + const requestId = Math.random().toString(16).slice(2) + + if (handler instanceof HttpHandler) { + const result = await handler.run({ + request: new Request(handler.info.path, { + method: handler.info.method, + }), + requestId, + }) + + return { + handler: { + method: handler.info.method.toString().toUpperCase(), + path: handler.info.path, + }, + 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/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/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) From 83977b54dc266c56eba0b429e2954357252a193d Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Sun, 26 May 2024 02:59:29 +0100 Subject: [PATCH 06/23] test: improve OpenSchema tests and implementation --- src/fromOpenApi/fromOpenApi.ts | 73 ++++++++++++++++------ src/fromOpenApi/utils/getServers.ts | 4 +- src/fromOpenApi/utils/openApiUtils.ts | 87 +++++++++++++++++++++++---- test/oas/oas-json-schema.test.ts | 10 +-- test/oas/oas-response-headers.test.ts | 1 - test/oas/oas-servers.test.ts | 10 +-- test/support/createOpenApiSpec.ts | 2 +- 7 files changed, 144 insertions(+), 43 deletions(-) diff --git a/src/fromOpenApi/fromOpenApi.ts b/src/fromOpenApi/fromOpenApi.ts index 1009b20..6290aec 100644 --- a/src/fromOpenApi/fromOpenApi.ts +++ b/src/fromOpenApi/fromOpenApi.ts @@ -1,10 +1,10 @@ import { RequestHandler, HttpHandler, http } from 'msw' -import type { OpenAPIV3, OpenAPIV2 } from 'openapi-types' +import type { OpenAPIV3, OpenAPIV2, OpenAPI } from 'openapi-types' import SwaggerParser from '@apidevtools/swagger-parser' import { normalizeSwaggerUrl } from './utils/normalizeSwaggerUrl.js' import { getServers } from './utils/getServers.js' import { isAbsoluteUrl, joinPaths } from './utils/url.js' -import { createResponseResolver } from './utils/openApiUtils.js' +import { createResponseResolver, createResponseResolverFromContent } from './utils/openApiUtils.js' type SupportedHttpMethods = keyof typeof http const supportedHttpMethods = Object.keys( @@ -19,13 +19,19 @@ const supportedHttpMethods = Object.keys( * await fromOpenApi(specification) */ export async function fromOpenApi( - document: string | OpenAPIV3.Document | OpenAPIV2.Document, + document: string | OpenAPI.Document | OpenAPIV3.Document | OpenAPIV2.Document, ): Promise> { const specification = await SwaggerParser.dereference(document) - const handlers: Array = [] + const requestHandlers: Array = [] - for (const url in Object.entries(specification.paths)) { - const pathItem = specification.paths[url] as + 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 @@ -38,7 +44,6 @@ export async function fromOpenApi( } const operation = pathItem[method] as OpenAPIV3.OperationObject - if (!operation) { continue } @@ -51,23 +56,51 @@ export async function fromOpenApi( ? new URL(path, baseUrl).href : joinPaths(path, baseUrl) - const handler = new HttpHandler( - method, - requestUrl, - createResponseResolver(operation), - { - /** - * @fixme Support `once` the same as in HAR? - */ - }, - ) - - handlers.push(handler) + if ( + typeof operation.responses === 'undefined' || + operation.responses === null + ) { + const handler = new HttpHandler( + method, + requestUrl, + () => new Response('Not implemented', { status: 501 }), + { + /** + * @fixme Support `once` the same as in HAR? + */ + }, + ) + + requestHandlers.push(handler) + + continue + } + + for (const responseStatus of Object.keys(operation.responses)) { + const content = operation.responses[responseStatus] + if (!content) { + continue + } + + const handler = new HttpHandler( + method, + requestUrl, + createResponseResolver(operation), + { + /** + * @fixme Support `once` the same as in HAR? + */ + }, + ) + + requestHandlers.push(handler) + } + } } } - return handlers + return requestHandlers } function isSupportedHttpMethod(method: string): method is SupportedHttpMethods { 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 index a1ed657..4691667 100644 --- a/src/fromOpenApi/utils/openApiUtils.ts +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -38,6 +38,7 @@ export function createResponseResolver( (responses.default as OpenAPIV3.ResponseObject) if (!fallbackResponse) { + console.log(`fallbackResponse missing`) return new Response('Not implemented', { status: 501 }) } @@ -46,7 +47,7 @@ export function createResponseResolver( return new Response(toBody(request, responseObject), { status: Number(explicitResponseStatus || '200'), - headers: toHeaders(responseObject), + headers: toHeaders(request, responseObject), }) } } @@ -55,21 +56,65 @@ export function createResponseResolver( * Get the Fetch API `Headers` from the OpenAPI response object. */ export function toHeaders( + request: Request, responseObject: OpenAPIV3.ResponseObject, ): Headers | undefined { - if (!responseObject.headers) { + console.log(`toHeaders() request:`, request) + const { content } = responseObject + if (!content) { + console.log(`content missing`) + 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( - responseObject.headers, - )) { + for (const [headerName, headerObject] of Object.entries(responseHeaders)) { const headerSchema = (headerObject as OpenAPIV3.HeaderObject).schema as | OpenAPIV3.SchemaObject | undefined - if (!headerSchema) { continue } @@ -82,6 +127,10 @@ export function toHeaders( headers.append(headerName, toString(headerValue)) } + if (headers.get('content-type') === null && selectedContentType) { + headers.set('content-type', selectedContentType) + } + return headers } @@ -93,20 +142,22 @@ export function toBody( 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(',') + 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) @@ -117,14 +168,16 @@ export function toBody( ) if (matchingResponseContentType) { - mediaTypeObject = content[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. - mediaTypeObject = content[responseContentTypes[0]] + selectedContentType = responseContentTypes[0] as string + mediaTypeObject = content[selectedContentType] } if (!mediaTypeObject) { @@ -133,6 +186,10 @@ export function toBody( // 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 } @@ -156,12 +213,20 @@ export function toBody( 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) { - return evolveJsonSchema(mediaTypeObject.schema as OpenAPIV3.SchemaObject) + const resolvedResponse = evolveJsonSchema( + mediaTypeObject.schema as OpenAPIV3.SchemaObject, + ) + + return JSON.stringify(resolvedResponse, null, 2) } return null diff --git a/test/oas/oas-json-schema.test.ts b/test/oas/oas-json-schema.test.ts index 1ce1797..a635c67 100644 --- a/test/oas/oas-json-schema.test.ts +++ b/test/oas/oas-json-schema.test.ts @@ -49,6 +49,7 @@ it('supports JSON Schema object', async () => { const res = await withHandlers(handlers, () => { return fetch('http://localhost/cart') }) + console.log(`response:`, res) expect(res.status).toEqual(200) expect(res.headers.get('content-type')).toEqual('application/json') @@ -87,8 +88,8 @@ 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(handlers[0].info.header).toEqual('get /pet/:petId') + expect(handlers[1].info.header).toEqual('get /pet/:petId/:foodId') }) it('treats operations without "responses" as not implemented (501)', async () => { @@ -159,15 +160,16 @@ 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) => { + console.log(res) expect(res.status).toEqual(501) - expect(await res.text()).toEqual('') + expect(await res.text()).toEqual('Not implemented') }) await withHandlers(handlers, () => fetch('http://localhost/resource?response=404'), ).then(async (res) => { expect(res.status).toEqual(501) - expect(await res.text()).toEqual('') + expect(await res.text()).toEqual('Not implemented') }) }) diff --git a/test/oas/oas-response-headers.test.ts b/test/oas/oas-response-headers.test.ts index dbca29d..1468526 100644 --- a/test/oas/oas-response-headers.test.ts +++ b/test/oas/oas-response-headers.test.ts @@ -47,7 +47,6 @@ it('supports response headers', async () => { 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( diff --git a/test/oas/oas-servers.test.ts b/test/oas/oas-servers.test.ts index 7d86dec..252ebc5 100644 --- a/test/oas/oas-servers.test.ts +++ b/test/oas/oas-servers.test.ts @@ -16,7 +16,7 @@ it('supports a single absolute server url', async () => { responses: { 200: { content: { - 'application/xml': { + 'application/json': { example: [1, 2, 3], }, }, @@ -33,7 +33,9 @@ it('supports a single absolute server url', async () => { }) expect(res.status).toEqual(200) - expect(await res.json()).toEqual([1, 2, 3]) + const responseText = await res.text() + const responseJson = JSON.parse(responseText) + expect(responseJson).toEqual([1, 2, 3]) }) it('supports a single relative server url', async () => { @@ -80,7 +82,7 @@ it('supports multiple absolute server urls', async () => { responses: { 200: { content: { - 'application/xml': { + 'application/json': { example: [1, 2, 3], }, }, @@ -115,7 +117,7 @@ it('supports the "basePath" url', async () => { responses: { 200: { content: { - 'application/xml': { + 'application/json': { example: ['a', 'b', 'c'], }, }, 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: { From 97d1f807455a3b8d7f67d4f2486508c8be811f4c Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Sun, 26 May 2024 14:23:04 +0100 Subject: [PATCH 07/23] test: update the unit tests for `fromTraffic` --- .../utils/__tests__/base64strings.test.ts | 21 ++++++++++++++++++ src/fromTraffic/utils/decodeBase64String.ts | 7 +++--- src/fromTraffic/utils/encodeBase64String.ts | 6 +++++ src/fromTraffic/utils/fromByteArray.ts | 4 ++++ src/fromTraffic/utils/harUtils.test.ts | 22 +++++++++++-------- src/fromTraffic/utils/isEqualBytes.ts | 13 +++++++++++ test/traffic/fromTraffic.test.ts | 8 ++++--- 7 files changed, 65 insertions(+), 16 deletions(-) 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/isEqualBytes.ts 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 7325caa..58ea4ff 100644 --- a/src/fromTraffic/utils/decodeBase64String.ts +++ b/src/fromTraffic/utils/decodeBase64String.ts @@ -1,8 +1,7 @@ export function decodeBase64String(data: string): Uint8Array { const binaryString = atob(data) - const bytes = new Uint8Array(binaryString.length) - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i) - } + const encoder = new TextEncoder() + const bytes = encoder.encode(binaryString) return bytes + } 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 index 379ad02..83e9c8b 100644 --- a/src/fromTraffic/utils/harUtils.test.ts +++ b/src/fromTraffic/utils/harUtils.test.ts @@ -1,3 +1,5 @@ +import { encodeBase64String } from './encodeBase64String' +import { isEqualBytes } from './isEqualBytes' import { toHeaders, toResponse, toResponseBody } from './harUtils' describe(toHeaders, () => { @@ -61,15 +63,17 @@ describe(toResponseBody, () => { }) it('decodes the base64-encoded response body', () => { - expect( - toResponseBody({ - text: btoa('hello world'), - size: 11, - mimeType: 'text/plain', - encoding: 'base64', - }), - ).toEqual(Uint8Array.from('hello world', (c) => c.charCodeAt(0))) - }) + 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).toBeDefined() + expect(isEqualBytes(bodyBytes, responseBody as Uint8Array)).toBe(true) + }) it.todo('handles a compressed response body') }) diff --git a/src/fromTraffic/utils/isEqualBytes.ts b/src/fromTraffic/utils/isEqualBytes.ts new file mode 100644 index 0000000..60035ec --- /dev/null +++ b/src/fromTraffic/utils/isEqualBytes.ts @@ -0,0 +1,13 @@ +export function isEqualBytes(bytes1: Uint8Array, bytes2: Uint8Array): boolean { + if (bytes1.length !== bytes2.length) { + return false + } + + for (let i = 0; i < bytes1.length; i++) { + if (bytes1[i] !== bytes2[i]) { + return false + } + } + + return true +} diff --git a/test/traffic/fromTraffic.test.ts b/test/traffic/fromTraffic.test.ts index 8537d49..532cd5d 100644 --- a/test/traffic/fromTraffic.test.ts +++ b/test/traffic/fromTraffic.test.ts @@ -80,7 +80,7 @@ describe('fromTraffic', () => { ) expect(handlers).toHaveLength(1) - expect(handlers[0].info.header).toEqual('GET https://api.stripe.com') + expect(handlers[0].info.header).toEqual('get https://api.stripe.com') }) }) @@ -123,7 +123,9 @@ describe('toResponseBody', () => { ).toEqual(expectedBody) }) - it('returns a plain text response body as-is', () => { - expect(toResponse(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') }) }) From abb022b4ddb6d01329aab04dd5fe1b5e84c4e20c Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Sun, 26 May 2024 14:35:22 +0100 Subject: [PATCH 08/23] test: update the fromTraffic unit tests --- test/traffic/fromTraffic.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/traffic/fromTraffic.test.ts b/test/traffic/fromTraffic.test.ts index 532cd5d..a071078 100644 --- a/test/traffic/fromTraffic.test.ts +++ b/test/traffic/fromTraffic.test.ts @@ -6,6 +6,7 @@ import { fromTraffic } from '../../src/fromTraffic/fromTraffic' import { decodeBase64String } from '../../src/fromTraffic/utils/decodeBase64String' 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', () => { @@ -42,7 +43,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: { @@ -80,7 +81,11 @@ describe('fromTraffic', () => { ) expect(handlers).toHaveLength(1) - expect(handlers[0].info.header).toEqual('get https://api.stripe.com') + const [initialRequestHandler] = await inspectHandlers(handlers) + expect(initialRequestHandler.handler).toEqual({ + method: 'GET', + path: 'https://api.stripe.com', + }) }) }) From 9171e9819dc3914dd00ac0787a5ba8d0c5707d57 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 26 May 2024 17:02:09 +0200 Subject: [PATCH 09/23] chore(withHandlers): add deprecation message --- src/fromOpenApi/fromOpenApi.ts | 3 +-- test/support/withHandlers.ts | 3 +++ vitest.d.ts | 13 +++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 vitest.d.ts diff --git a/src/fromOpenApi/fromOpenApi.ts b/src/fromOpenApi/fromOpenApi.ts index 6290aec..71f4172 100644 --- a/src/fromOpenApi/fromOpenApi.ts +++ b/src/fromOpenApi/fromOpenApi.ts @@ -4,7 +4,7 @@ import SwaggerParser from '@apidevtools/swagger-parser' import { normalizeSwaggerUrl } from './utils/normalizeSwaggerUrl.js' import { getServers } from './utils/getServers.js' import { isAbsoluteUrl, joinPaths } from './utils/url.js' -import { createResponseResolver, createResponseResolverFromContent } from './utils/openApiUtils.js' +import { createResponseResolver } from './utils/openApiUtils.js' type SupportedHttpMethods = keyof typeof http const supportedHttpMethods = Object.keys( @@ -95,7 +95,6 @@ export async function fromOpenApi( requestHandlers.push(handler) } - } } } diff --git a/test/support/withHandlers.ts b/test/support/withHandlers.ts index f6dca5d..b58c31e 100644 --- a/test/support/withHandlers.ts +++ b/test/support/withHandlers.ts @@ -5,6 +5,9 @@ import { setupServer } from 'msw/node' * Creates an MSW `server` instance, populates it * with the given `handlers`, runs the `callback`, * and cleans up afterward. + * + * @deprecated Remove this once all tests are migrated + * NOT to use this. Use `inspectHandlers` instead. */ export async function withHandlers( handlers: Array, diff --git a/vitest.d.ts b/vitest.d.ts new file mode 100644 index 0000000..eeb6bba --- /dev/null +++ b/vitest.d.ts @@ -0,0 +1,13 @@ +import type { Assertion, AsymmetricMatchersContaining } from 'vitest' + +interface CustomMatchers { + /** + * Compare two `Uint8Array` arrays. + */ + toEqualBytes: (expected: Uint8Array) => R +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} From 25255acc50b82729ffe0d214b1f439bf378b7790 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 26 May 2024 17:24:44 +0200 Subject: [PATCH 10/23] chore: remove eslint --- .eslintrc.js | 14 -- package.json | 9 +- pnpm-lock.yaml | 585 ------------------------------------------------- 3 files changed, 1 insertion(+), 607 deletions(-) delete mode 100644 .eslintrc.js 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/package.json b/package.json index 322aa7c..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": { @@ -50,12 +48,7 @@ "@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": "^2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca7e0df..c38be41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,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 @@ -756,63 +741,6 @@ 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} - 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} - dev: true - - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@2.0.3: - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - dev: true - /@inquirer/confirm@3.1.8: resolution: {integrity: sha512-f3INZ+ca4dQdn+MQiq1yP/mOIR/Oc8BLRYuDh6ciToWd6z4W8yArfzjBCMQ0BPY8PcJKwZxGIt8Z6yNT32eSTw==} engines: {node: '>=18'} @@ -986,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] @@ -1215,10 +1138,6 @@ packages: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} 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 @@ -1265,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: @@ -1292,142 +1207,6 @@ packages: resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} dev: true - /@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 - dev: true - - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true - /@vitest/expect@1.6.0: resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} dependencies: @@ -1488,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'} @@ -1535,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: @@ -1689,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: @@ -1929,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 @@ -2128,10 +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 - /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -2173,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'} @@ -2359,146 +2099,18 @@ 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'} @@ -2595,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'} @@ -2610,27 +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 - /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'} @@ -2669,19 +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 - /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -2718,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} @@ -2802,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'} @@ -2821,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'} @@ -2839,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'} @@ -2868,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} @@ -3011,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 @@ -3094,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'} @@ -3214,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: @@ -3246,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'} @@ -3529,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'} @@ -3631,10 +3129,6 @@ 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'} @@ -3727,18 +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 - /outvariant@1.4.2: resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} @@ -3822,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'} @@ -3940,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'} @@ -4141,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'} @@ -4521,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: @@ -4569,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'} @@ -4668,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 @@ -4786,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'} @@ -4803,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'} @@ -5077,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'} From 8992ce21cd73bbd95b0f9d634a79d910c0f6e15c Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 26 May 2024 17:27:39 +0200 Subject: [PATCH 11/23] chore: remove contributing --- CONTRIBUTING.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 CONTRIBUTING.md 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. From 261b39f5ec945067760bc587b39ed4683fc6d3f0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 26 May 2024 17:28:51 +0200 Subject: [PATCH 12/23] chore(openApiUtils): remove console.log --- src/fromOpenApi/utils/openApiUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fromOpenApi/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts index 4691667..a5be29b 100644 --- a/src/fromOpenApi/utils/openApiUtils.ts +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -59,7 +59,6 @@ export function toHeaders( request: Request, responseObject: OpenAPIV3.ResponseObject, ): Headers | undefined { - console.log(`toHeaders() request:`, request) const { content } = responseObject if (!content) { console.log(`content missing`) From 3a69b7f421753f8a81d2d0e434b003e44caf4965 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 26 May 2024 17:32:54 +0200 Subject: [PATCH 13/23] test: fix "response-order" test --- test/traffic/response-order.test.ts | 51 ++++++++++++++++------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/test/traffic/response-order.test.ts b/test/traffic/response-order.test.ts index a21aa94..576dd2a 100644 --- a/test/traffic/response-order.test.ts +++ b/test/traffic/response-order.test.ts @@ -1,5 +1,5 @@ import { fromTraffic } from '../../src/fromTraffic/fromTraffic' -import { withHandlers } from '../../test/support/withHandlers' +import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' import { normalizeLocalhost, readArchive } from './utils' const requestOrder = readArchive( @@ -8,27 +8,32 @@ const requestOrder = readArchive( 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'), - ] + 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: expect.any(Array), + 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: expect.any(Array), + body: 'two', + }, + }, + ]) }) From 85efdff08b2b3f918427e4f495e06e7efd193f8a Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Sun, 26 May 2024 18:08:23 +0100 Subject: [PATCH 14/23] chore: remove unnecessary logging --- src/fromOpenApi/utils/openApiUtils.ts | 2 -- test/oas/oas-json-schema.test.ts | 2 -- test/traffic/fromTraffic.test.ts | 18 +++++++++--------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/fromOpenApi/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts index a5be29b..ed761d9 100644 --- a/src/fromOpenApi/utils/openApiUtils.ts +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -38,7 +38,6 @@ export function createResponseResolver( (responses.default as OpenAPIV3.ResponseObject) if (!fallbackResponse) { - console.log(`fallbackResponse missing`) return new Response('Not implemented', { status: 501 }) } @@ -61,7 +60,6 @@ export function toHeaders( ): Headers | undefined { const { content } = responseObject if (!content) { - console.log(`content missing`) return undefined } diff --git a/test/oas/oas-json-schema.test.ts b/test/oas/oas-json-schema.test.ts index a635c67..a07d221 100644 --- a/test/oas/oas-json-schema.test.ts +++ b/test/oas/oas-json-schema.test.ts @@ -49,7 +49,6 @@ it('supports JSON Schema object', async () => { const res = await withHandlers(handlers, () => { return fetch('http://localhost/cart') }) - console.log(`response:`, res) expect(res.status).toEqual(200) expect(res.headers.get('content-type')).toEqual('application/json') @@ -160,7 +159,6 @@ 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) => { - console.log(res) expect(res.status).toEqual(501) expect(await res.text()).toEqual('Not implemented') }) diff --git a/test/traffic/fromTraffic.test.ts b/test/traffic/fromTraffic.test.ts index a071078..c664f34 100644 --- a/test/traffic/fromTraffic.test.ts +++ b/test/traffic/fromTraffic.test.ts @@ -7,6 +7,8 @@ import { decodeBase64String } from '../../src/fromTraffic/utils/decodeBase64Stri import { readArchive } from './utils' import { toResponse } from '../../src/fromTraffic/utils/harUtils' import { inspectHandlers } from '../support/inspectHandler' +import { encodeBase64String } from '../../src/fromTraffic/utils/encodeBase64String' +import { isEqualBytes } from '../../src/fromTraffic/utils/isEqualBytes' describe('fromTraffic', () => { it('throws an exception given no HAR object', () => { @@ -113,19 +115,17 @@ describe('toResponseBody', () => { } it('returns undefined given response with no body', () => { - expect(toResponse(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( - toResponse(createResponse(body.toString(), { encoding: 'base64' })), - ).toEqual(expectedBody) + responseText + ).toEqual(exepctedBody) }) it('returns a plain text response body as-is', async () => { From d6dbbdc09819d076061190afac2cdcdedd4a9514 Mon Sep 17 00:00:00 2001 From: Weyert de Boer Date: Sun, 26 May 2024 22:23:05 +0100 Subject: [PATCH 15/23] fix: inspectHandler to support relative paths --- test/support/inspectHandler.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/support/inspectHandler.ts b/test/support/inspectHandler.ts index 8c7e487..7df80f2 100644 --- a/test/support/inspectHandler.ts +++ b/test/support/inspectHandler.ts @@ -17,22 +17,31 @@ type SerializedHandler = H extends HttpHandler } : never -interface SerializedResponse { +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(handler.info.path, { - method: handler.info.method, + request: new Request(fullQualifiedUrl, { + method: handler.info.method.toString(), }), requestId, }) @@ -40,7 +49,7 @@ async function inspectHandler( return { handler: { method: handler.info.method.toString().toUpperCase(), - path: handler.info.path, + path: fullQualifiedUrl, }, response: await serializeResponse(result?.response), } From 72254399e14133688bd2464925b62a20958f1460 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 15:44:35 +0200 Subject: [PATCH 16/23] test: rewrite "response-stream" --- test/traffic/response-stream.test.ts | 31 ++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) 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', + }, + }, + ]) }) From 59d438e4c7e6235f0b8636ca93128b551d8256e1 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 15:46:21 +0200 Subject: [PATCH 17/23] test(response-order): assert response headers --- test/traffic/response-order.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/test/traffic/response-order.test.ts b/test/traffic/response-order.test.ts index 576dd2a..35d793c 100644 --- a/test/traffic/response-order.test.ts +++ b/test/traffic/response-order.test.ts @@ -1,13 +1,10 @@ import { fromTraffic } from '../../src/fromTraffic/fromTraffic' import { InspectedHandler, inspectHandlers } from '../support/inspectHandler' -import { normalizeLocalhost, readArchive } from './utils' - -const requestOrder = readArchive( - 'test/traffic/fixtures/archives/request-order.har', -) +import { _toHeaders, normalizeLocalhost, readArchive } from './utils' it('respects the response sequence when repeatedly requesting the same endpoint', async () => { - const handlers = fromTraffic(requestOrder, normalizeLocalhost) + 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. { @@ -18,7 +15,7 @@ it('respects the response sequence when repeatedly requesting the same endpoint' response: { status: 200, statusText: 'OK', - headers: expect.any(Array), + headers: _toHeaders(har.log.entries[0].response.headers), body: 'one', }, }, @@ -31,7 +28,7 @@ it('respects the response sequence when repeatedly requesting the same endpoint' response: { status: 200, statusText: 'OK', - headers: expect.any(Array), + headers: _toHeaders(har.log.entries[1].response.headers), body: 'two', }, }, From 96b09c8ea492cc048f501f69865ff2a0795a1523 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 15:54:19 +0200 Subject: [PATCH 18/23] chore: add ".toEqualBytes()" assertion --- src/fromTraffic/utils/harUtils.test.ts | 7 ++--- src/fromTraffic/utils/isEqualBytes.ts | 13 --------- test/traffic/fromTraffic.test.ts | 11 +++----- tsconfig.test.json | 2 +- vitest.config.ts | 1 + vitest.setup.ts | 39 ++++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 26 deletions(-) delete mode 100644 src/fromTraffic/utils/isEqualBytes.ts create mode 100644 vitest.setup.ts diff --git a/src/fromTraffic/utils/harUtils.test.ts b/src/fromTraffic/utils/harUtils.test.ts index 83e9c8b..e539269 100644 --- a/src/fromTraffic/utils/harUtils.test.ts +++ b/src/fromTraffic/utils/harUtils.test.ts @@ -1,5 +1,3 @@ -import { encodeBase64String } from './encodeBase64String' -import { isEqualBytes } from './isEqualBytes' import { toHeaders, toResponse, toResponseBody } from './harUtils' describe(toHeaders, () => { @@ -71,9 +69,8 @@ describe(toResponseBody, () => { encoding: 'base64', }) - expect(responseBody).toBeDefined() - expect(isEqualBytes(bodyBytes, responseBody as Uint8Array)).toBe(true) - }) + expect(responseBody).toEqualBytes(bodyBytes) + }) it.todo('handles a compressed response body') }) diff --git a/src/fromTraffic/utils/isEqualBytes.ts b/src/fromTraffic/utils/isEqualBytes.ts deleted file mode 100644 index 60035ec..0000000 --- a/src/fromTraffic/utils/isEqualBytes.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function isEqualBytes(bytes1: Uint8Array, bytes2: Uint8Array): boolean { - if (bytes1.length !== bytes2.length) { - return false - } - - for (let i = 0; i < bytes1.length; i++) { - if (bytes1[i] !== bytes2[i]) { - return false - } - } - - return true -} diff --git a/test/traffic/fromTraffic.test.ts b/test/traffic/fromTraffic.test.ts index c664f34..4268fba 100644 --- a/test/traffic/fromTraffic.test.ts +++ b/test/traffic/fromTraffic.test.ts @@ -3,12 +3,9 @@ */ import Har from 'har-format' import { fromTraffic } from '../../src/fromTraffic/fromTraffic' -import { decodeBase64String } from '../../src/fromTraffic/utils/decodeBase64String' import { readArchive } from './utils' import { toResponse } from '../../src/fromTraffic/utils/harUtils' import { inspectHandlers } from '../support/inspectHandler' -import { encodeBase64String } from '../../src/fromTraffic/utils/encodeBase64String' -import { isEqualBytes } from '../../src/fromTraffic/utils/isEqualBytes' describe('fromTraffic', () => { it('throws an exception given no HAR object', () => { @@ -120,12 +117,12 @@ describe('toResponseBody', () => { 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 response = toResponse( + createResponse(btoa(exepctedBody), { encoding: 'base64' }), + ) const responseText = await response.text() - expect( - responseText - ).toEqual(exepctedBody) + expect(responseText).toEqual(exepctedBody) }) it('returns a plain text response body as-is', async () => { diff --git a/tsconfig.test.json b/tsconfig.test.json index 260e48c..8a4045b 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -10,5 +10,5 @@ "types": ["vitest/globals"], "lib": ["dom", "DOM.Iterable"] }, - "include": ["test", "src/**/*.test.ts"] + "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.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..3317a88 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,39 @@ +import { invariant } from 'outvariant' + +expect.extend({ + toEqualBytes(expected: unknown, actual: unknown) { + invariant(isUint8Array(expected), '') + invariant(isUint8Array(actual), '') + + if (expected.length !== actual.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: () => '...', + } + }, +}) + +function isUint8Array(value: unknown): value is Uint8Array { + return value?.constructor.name === 'Uint8Array' +} From 722e019ed3196ff05aa74ab82b8cf76c278d2ab1 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 16:37:33 +0200 Subject: [PATCH 19/23] test(oas-servers): rewrite test --- src/fromOpenApi/utils/openApiUtils.ts | 5 +- test/oas/oas-servers.test.ts | 126 ++++++++++++++++---------- 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/fromOpenApi/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts index ed761d9..42b731b 100644 --- a/src/fromOpenApi/utils/openApiUtils.ts +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -1,3 +1,4 @@ +import { STATUS_CODES } from 'node:http' import type { ResponseResolver } from 'msw' import { OpenAPIV3 } from 'openapi-types' import { evolveJsonSchema } from '../schema/evolve' @@ -44,8 +45,10 @@ export function createResponseResolver( responseObject = fallbackResponse } + const status = Number(explicitResponseStatus || '200') return new Response(toBody(request, responseObject), { - status: Number(explicitResponseStatus || '200'), + status, + statusText: STATUS_CODES[status], headers: toHeaders(request, responseObject), }) } diff --git a/test/oas/oas-servers.test.ts b/test/oas/oas-servers.test.ts index 252ebc5..0290b87 100644 --- a/test/oas/oas-servers.test.ts +++ b/test/oas/oas-servers.test.ts @@ -1,15 +1,11 @@ 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: { @@ -27,28 +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) - const responseText = await res.text() - const responseJson = JSON.parse(responseText) - expect(responseJson).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: { @@ -63,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' }], @@ -93,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 () => { @@ -128,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']), + }, + }, + ]) }) From 0b200ef5f6997190b8c826c06e69c849d5d0e163 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 16:43:50 +0200 Subject: [PATCH 20/23] test(oas-response): rewrite test --- src/fromOpenApi/schema/evolve.ts | 1 + src/fromOpenApi/utils/openApiUtils.ts | 2 +- test/oas/oas-response.test.ts | 62 ++++++++++++++++----------- 3 files changed, 39 insertions(+), 26 deletions(-) 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/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts index 42b731b..5c240ed 100644 --- a/src/fromOpenApi/utils/openApiUtils.ts +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -226,7 +226,7 @@ export function toBody( mediaTypeObject.schema as OpenAPIV3.SchemaObject, ) - return JSON.stringify(resolvedResponse, null, 2) + return JSON.stringify(resolvedResponse) } return null 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', + }), + }, + }, + ]) }) From e58386e4bdcb739c6b354d6d5fb91ec9312f9364 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 16:45:40 +0200 Subject: [PATCH 21/23] test(oas-response-headers): rewrite test --- test/oas/oas-response-headers.test.ts | 40 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/test/oas/oas-response-headers.test.ts b/test/oas/oas-response-headers.test.ts index 1468526..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,19 +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', - // 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), + }, + }, + ]) }) From 74c129efa7b2cc8dfb42b825015665dc12c9da45 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 17:36:30 +0200 Subject: [PATCH 22/23] test(oas-json-schema): rewrite the test --- src/fromOpenApi/fromOpenApi.ts | 6 +- src/fromOpenApi/utils/openApiUtils.ts | 20 ++- test/oas/oas-json-schema.test.ts | 195 ++++++++++++++++---------- test/support/withHandlers.ts | 3 - vitest.d.ts | 1 + vitest.setup.ts | 57 +++++++- 6 files changed, 198 insertions(+), 84 deletions(-) diff --git a/src/fromOpenApi/fromOpenApi.ts b/src/fromOpenApi/fromOpenApi.ts index 71f4172..d4485f6 100644 --- a/src/fromOpenApi/fromOpenApi.ts +++ b/src/fromOpenApi/fromOpenApi.ts @@ -63,7 +63,11 @@ export async function fromOpenApi( const handler = new HttpHandler( method, requestUrl, - () => new Response('Not implemented', { status: 501 }), + () => + new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }), { /** * @fixme Support `once` the same as in HAR? diff --git a/src/fromOpenApi/utils/openApiUtils.ts b/src/fromOpenApi/utils/openApiUtils.ts index 5c240ed..ca5e955 100644 --- a/src/fromOpenApi/utils/openApiUtils.ts +++ b/src/fromOpenApi/utils/openApiUtils.ts @@ -12,10 +12,16 @@ export function createResponseResolver( // Treat operations that describe no responses as not implemented. if (responses == null) { - return new Response('Not implemented', { status: 501 }) + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) } if (Object.keys(responses).length === 0) { - return new Response('Not implemented', { status: 501 }) + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) } let responseObject: OpenAPIV3.ResponseObject @@ -29,7 +35,10 @@ export function createResponseResolver( ] as OpenAPIV3.ResponseObject if (!responseByStatus) { - return new Response('Not implemented', { status: 501 }) + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) } responseObject = responseByStatus @@ -39,7 +48,10 @@ export function createResponseResolver( (responses.default as OpenAPIV3.ResponseObject) if (!fallbackResponse) { - return new Response('Not implemented', { status: 501 }) + return new Response('Not Implemented', { + status: 501, + statusText: 'Not Implemented', + }) } responseObject = fallbackResponse diff --git a/test/oas/oas-json-schema.test.ts b/test/oas/oas-json-schema.test.ts index a07d221..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('Not implemented') - }) + 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('Not implemented') - }) + 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/support/withHandlers.ts b/test/support/withHandlers.ts index b58c31e..f6dca5d 100644 --- a/test/support/withHandlers.ts +++ b/test/support/withHandlers.ts @@ -5,9 +5,6 @@ import { setupServer } from 'msw/node' * Creates an MSW `server` instance, populates it * with the given `handlers`, runs the `callback`, * and cleans up afterward. - * - * @deprecated Remove this once all tests are migrated - * NOT to use this. Use `inspectHandlers` instead. */ export async function withHandlers( handlers: Array, diff --git a/vitest.d.ts b/vitest.d.ts index eeb6bba..be3c426 100644 --- a/vitest.d.ts +++ b/vitest.d.ts @@ -5,6 +5,7 @@ interface CustomMatchers { * Compare two `Uint8Array` arrays. */ toEqualBytes: (expected: Uint8Array) => R + toEqualResponse: (expected: Response) => Promise } declare module 'vitest' { diff --git a/vitest.setup.ts b/vitest.setup.ts index 3317a88..a6cfe7e 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,11 +1,11 @@ import { invariant } from 'outvariant' expect.extend({ - toEqualBytes(expected: unknown, actual: unknown) { - invariant(isUint8Array(expected), '') - invariant(isUint8Array(actual), '') + toEqualBytes(actual: unknown, expected: unknown) { + invariant(isUint8Array(actual), 'Expected actual to be a Uint8Array') + invariant(isUint8Array(expected), 'Expected expected to be a Uint8Array') - if (expected.length !== actual.length) { + if (actual.length !== expected.length) { return { pass: false, message: () => @@ -32,6 +32,55 @@ expect.extend({ 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 { From c8530156bb6f12fedb1f26bfd0f8727e63a88e81 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Mon, 27 May 2024 17:45:19 +0200 Subject: [PATCH 23/23] test(petstore): rewrite the test --- test/oas/petstore.test.ts | 147 ++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 76 deletions(-) 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 })) })