diff --git a/package-lock.json b/package-lock.json index 0df9e92..acee5ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "dotenv": "^16.3.1", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", + "msw": "^2.12.11", "nock": "^13.4.0", "typedoc": "^0.28.14", "typedoc-plugin-markdown": "^4.9.0", @@ -901,6 +902,94 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -974,6 +1063,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1012,6 +1119,31 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1437,6 +1569,13 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1943,6 +2082,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2355,6 +2504,49 @@ "node": "*" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2408,6 +2600,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2616,6 +2822,13 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/engine.io-client": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", @@ -3442,6 +3655,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -3597,6 +3820,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphql": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3688,6 +3921,13 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3937,6 +4177,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -3996,6 +4246,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4637,6 +4894,61 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.12.11", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.11.tgz", + "integrity": "sha512-dVg20zi2I2EvnwH/+WupzsOC2mCa7qsIhyMAWtfRikn6RKtwL9+7SaF1IQ5LyZry4tlUtf6KyTVhnlQiZXozTQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.41.2", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.10.1", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4855,6 +5167,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -4955,6 +5274,13 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -5196,6 +5522,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -5227,6 +5563,13 @@ "node": ">=4" } }, + "node_modules/rettime": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", + "dev": true, + "license": "MIT" + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5600,6 +5943,16 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -5621,6 +5974,28 @@ "node": ">= 0.4" } }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -5680,6 +6055,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5762,6 +6150,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5852,6 +6253,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz", + "integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.26" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.26", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz", + "integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5875,6 +6296,19 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -5937,6 +6371,22 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", + "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -6156,6 +6606,16 @@ "dev": true, "license": "MIT" }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6491,6 +6951,21 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6527,6 +7002,16 @@ "node": ">=0.4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6550,6 +7035,35 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -6562,6 +7076,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 4c2c3f0..b1cba56 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "dotenv": "^16.3.1", "eslint": "^9.39.2", "eslint-plugin-import": "^2.32.0", - "nock": "^13.4.0", + "msw": "^2.12.11", "typedoc": "^0.28.14", "typedoc-plugin-markdown": "^4.9.0", "typescript": "^5.3.2", diff --git a/tests/mocks/server.ts b/tests/mocks/server.ts new file mode 100644 index 0000000..c37bdd2 --- /dev/null +++ b/tests/mocks/server.ts @@ -0,0 +1,47 @@ +/** + * MSW (Mock Service Worker) server for unit tests. + * + * ## How to add new handlers + * + * Call `server.use()` inside a test to register per-test handlers. + * They are automatically removed after each test by the global `afterEach` + * in `tests/setup.js` (via `server.resetHandlers()`). + * + * ```ts + * import { http, HttpResponse } from 'msw'; + * import { server } from '../mocks/server'; + * + * test('my test', async () => { + * server.use( + * http.get('https://api.base44.com/api/apps/test-app-id/entities/Todo', () => + * HttpResponse.json([{ id: '1', title: 'Test' }]) + * ) + * ); + * // ... test code + * }); + * ``` + * + * ## Architecture + * + * ``` + * Vitest test → SDK (axios / fetch) → MSW Node server → handler → fake response + * ``` + * + * MSW intercepts requests at the Node.js http layer (`@mswjs/interceptors`) + * and also intercepts native `fetch` calls. No axios mocking or `vi.stubGlobal` + * needed. + * + * ## Modules and their base URL patterns + * + * | Module | Base path | + * |--------------|------------------------------------------------------------------| + * | entities | `/api/apps/:appId/entities/:entityName` | + * | auth | `/api/apps/:appId/entities/User/me`, `/api/apps/:appId/auth/...` | + * | functions | `/api/apps/:appId/functions/:name`, `/api/functions/:name` | + * | integrations | `/api/apps/:appId/integration-endpoints/:pkg/:endpoint` | + * | custom-int | `/api/apps/:appId/integrations/custom/:slug/:operationId` | + * | connectors | `/api/apps/:appId/external-auth/tokens/:type` | + */ +import { setupServer } from 'msw/node'; + +export const server = setupServer(); diff --git a/tests/setup.js b/tests/setup.js index 901e0f2..9382856 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -1,7 +1,8 @@ // Load environment variables from .env file import dotenv from 'dotenv'; import './utils/circular-json-handler.js'; -import { beforeAll, afterAll, test } from 'vitest'; +import { beforeAll, afterAll, afterEach } from 'vitest'; +import { server } from './mocks/server.ts'; try { dotenv.config({ path: './tests/.env' }); @@ -16,13 +17,18 @@ try { console.warn('Failed to load circular JSON handler:', err.message); } -// Global beforeAll and afterAll hooks +// MSW server lifecycle beforeAll(() => { + server.listen({ onUnhandledRequest: 'warn' }); console.log('Starting Base44 SDK tests...'); - // Add any global setup here +}); + +afterEach(() => { + // Remove per-test handlers registered via server.use() + server.resetHandlers(); }); afterAll(() => { + server.close(); console.log('Completed Base44 SDK tests'); - // Add any global teardown here -}); \ No newline at end of file +}); diff --git a/tests/unit/auth.test.js b/tests/unit/auth.test.js index 81aa9aa..30a5a0f 100644 --- a/tests/unit/auth.test.js +++ b/tests/unit/auth.test.js @@ -1,13 +1,15 @@ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'; -import nock from 'nock'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; import { createClient } from '../../src/index.ts'; describe('Auth Module', () => { let base44; - let scope; const appId = 'test-app-id'; const serverUrl = 'https://api.base44.com'; const appBaseUrl = 'https://api.base44.com'; + const meUrl = `${serverUrl}/api/apps/${appId}/entities/User/me`; + const loginUrl = `${serverUrl}/api/apps/${appId}/auth/login`; beforeEach(() => { // Mock window.addEventListener and document for analytics module @@ -24,36 +26,18 @@ describe('Auth Module', () => { }; } - // Create a new client for each test - base44 = createClient({ - serverUrl, - appId, - appBaseUrl, - }); - - // Create a nock scope for mocking API calls - scope = nock(serverUrl); - - // Enable request debugging for Nock - nock.disableNetConnect(); - nock.emitter.on('no match', (req) => { - console.log(`Nock: No match for ${req.method} ${req.path}`); - console.log('Headers:', req.getHeaders()); - }); + base44 = createClient({ serverUrl, appId, appBaseUrl }); }); - + afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); - nock.emitter.removeAllListeners('no match'); - nock.enableNetConnect(); - + base44.cleanup(); + // Clean up localStorage if it exists if (typeof window !== 'undefined' && window.localStorage) { window.localStorage.clear(); } }); - + describe('me()', () => { test('should fetch current user information', async () => { const mockUser = { @@ -62,219 +46,143 @@ describe('Auth Module', () => { name: 'Test User', role: 'user' }; - - // Mock the API response - scope.get(`/api/apps/${appId}/entities/User/me`) - .reply(200, mockUser); - - // Call the API + + server.use(http.get(meUrl, () => HttpResponse.json(mockUser))); + const result = await base44.auth.me(); - - // Verify the response - auth methods return data directly, not wrapped + expect(result).toEqual(mockUser); expect(result.id).toBe('user-123'); expect(result.email).toBe('test@example.com'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('should handle authentication errors', async () => { - // Mock the API error response - scope.get(`/api/apps/${appId}/entities/User/me`) - .reply(401, { detail: 'Unauthorized' }); - - // Call the API and expect an error + server.use(http.get(meUrl, () => HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }))); + await expect(base44.auth.me()).rejects.toThrow(); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); }); - + describe('updateMe()', () => { test('should update current user data', async () => { - const updateData = { - name: 'Updated Name', - email: 'updated@example.com' - }; - - const updatedUser = { - id: 'user-123', - ...updateData, - role: 'user' - }; - - // Mock the API response - scope.put(`/api/apps/${appId}/entities/User/me`, updateData) - .reply(200, updatedUser); - - // Call the API + const updateData = { name: 'Updated Name', email: 'updated@example.com' }; + const updatedUser = { id: 'user-123', ...updateData, role: 'user' }; + + server.use(http.put(meUrl, () => HttpResponse.json(updatedUser))); + const result = await base44.auth.updateMe(updateData); - - // Verify the response - auth methods return data directly, not wrapped + expect(result).toEqual(updatedUser); expect(result.name).toBe('Updated Name'); expect(result.email).toBe('updated@example.com'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('should handle validation errors', async () => { - const invalidData = { - email: 'invalid-email' - }; - - // Mock the API error response - scope.put(`/api/apps/${appId}/entities/User/me`, invalidData) - .reply(400, { detail: 'Invalid email format' }); - - // Call the API and expect an error - await expect(base44.auth.updateMe(invalidData)).rejects.toThrow(); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + server.use(http.put(meUrl, () => HttpResponse.json({ detail: 'Invalid email format' }, { status: 400 }))); + + await expect(base44.auth.updateMe({ email: 'invalid-email' })).rejects.toThrow(); }); }); - + describe('login()', () => { test('should throw error when not in browser environment', () => { - // Mock window as undefined to simulate non-browser environment const originalWindow = global.window; delete global.window; - + expect(() => { base44.auth.redirectToLogin('/dashboard'); }).toThrow('Login method can only be used in a browser environment'); - - // Restore window + global.window = originalWindow; }); - + test('should redirect to login page with correct URL in browser environment', () => { - // Mock window object const mockLocation = { href: '' }; const originalWindow = global.window; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; const nextUrl = 'https://example.com/dashboard'; base44.auth.redirectToLogin(nextUrl); - // Verify the redirect URL was set correctly expect(mockLocation.href).toBe( `${appBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}` ); - // Restore window global.window = originalWindow; }); - + test('should use current URL when nextUrl is not provided', () => { - // Mock window object const currentUrl = 'https://example.com/current-page'; const mockLocation = { href: currentUrl }; const originalWindow = global.window; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; base44.auth.redirectToLogin(); - // Verify the redirect URL uses current URL expect(mockLocation.href).toBe( `${appBaseUrl}/login?from_url=${encodeURIComponent(currentUrl)}` ); - // Restore window global.window = originalWindow; }); test('should use appBaseUrl for login redirect when provided', () => { const customAppBaseUrl = 'https://custom-app.example.com'; - const clientWithCustomUrl = createClient({ - serverUrl, - appId, - appBaseUrl: customAppBaseUrl, - }); + const clientWithCustomUrl = createClient({ serverUrl, appId, appBaseUrl: customAppBaseUrl }); - // Mock window.location const originalWindow = global.window; const mockLocation = { href: '' }; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; - const nextUrl = 'https://example.com/dashboard'; - clientWithCustomUrl.auth.redirectToLogin(nextUrl); + clientWithCustomUrl.auth.redirectToLogin('https://example.com/dashboard'); - // Verify the redirect URL uses the custom appBaseUrl expect(mockLocation.href).toBe( - `${customAppBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}` + `${customAppBaseUrl}/login?from_url=${encodeURIComponent('https://example.com/dashboard')}` ); - // Restore window global.window = originalWindow; }); test('should use relative URL for login redirect when appBaseUrl is not provided', () => { - // Create a client without appBaseUrl - const clientWithoutAppBaseUrl = createClient({ - serverUrl, - appId, - }); + const clientWithoutAppBaseUrl = createClient({ serverUrl, appId }); - // Mock window.location const originalWindow = global.window; const mockLocation = { href: '', origin: 'https://current-app.com' }; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; - const nextUrl = 'https://example.com/dashboard'; - clientWithoutAppBaseUrl.auth.redirectToLogin(nextUrl); + clientWithoutAppBaseUrl.auth.redirectToLogin('https://example.com/dashboard'); - // Verify the redirect URL uses a relative path (no appBaseUrl prefix) expect(mockLocation.href).toBe( - `/login?from_url=${encodeURIComponent(nextUrl)}` + `/login?from_url=${encodeURIComponent('https://example.com/dashboard')}` ); - // Restore window global.window = originalWindow; }); }); - + describe('logout()', () => { test('should remove token from axios headers', async () => { - // Set a token first base44.auth.setToken('test-token', false); - - // Mock the API response for me() call - scope.get(`/api/apps/${appId}/entities/User/me`) - .matchHeader('Authorization', 'Bearer test-token') - .reply(200, { id: 'user-123', email: 'test@example.com' }); - - // Verify token is set by making a request + + server.use( + http.get(meUrl, ({ request }) => { + if (request.headers.get('Authorization') === 'Bearer test-token') { + return HttpResponse.json({ id: 'user-123', email: 'test@example.com' }); + } + return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }); + }) + ); + + // Token is set — should succeed await base44.auth.me(); - expect(scope.isDone()).toBe(true); - - // Call logout + base44.auth.logout(); - - // Mock another me() call to verify no Authorization header is sent - scope.get(`/api/apps/${appId}/entities/User/me`) - .matchHeader('Authorization', (val) => !val) // Should not have Authorization header - .reply(401, { detail: 'Unauthorized' }); - - // Verify no Authorization header is sent after logout (should throw 401) + + // After logout no Authorization header — should fail await expect(base44.auth.me()).rejects.toThrow(); - expect(scope.isDone()).toBe(true); }); - + test('should remove token from localStorage in browser environment', async () => { - // Mock window and localStorage const mockLocalStorage = { removeItem: vi.fn(), getItem: vi.fn(), @@ -284,28 +192,21 @@ describe('Auth Module', () => { const originalWindow = global.window; global.window = { localStorage: mockLocalStorage, - location: { - reload: vi.fn() - } + location: { reload: vi.fn() } }; - - // Set a token to localStorage first + base44.auth.setToken('test-token', true); expect(mockLocalStorage.setItem).toHaveBeenCalledWith('base44_access_token', 'test-token'); - - // Call logout + base44.auth.logout(); - - // Verify token was removed from localStorage + expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('base44_access_token'); expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('token'); - - // Restore window + global.window = originalWindow; }); - + test('should handle localStorage errors gracefully', async () => { - // Mock window and localStorage with error const mockLocalStorage = { removeItem: vi.fn().mockImplementation(() => { throw new Error('localStorage error'); @@ -315,79 +216,63 @@ describe('Auth Module', () => { const originalWindow = global.window; global.window = { localStorage: mockLocalStorage, - location: { - reload: vi.fn() - } + location: { reload: vi.fn() } }; - - // Call logout - should not throw + base44.auth.logout(); - - // Verify error was logged + expect(consoleSpy).toHaveBeenCalledWith('Failed to remove token from localStorage:', expect.any(Error)); - - // Restore + consoleSpy.mockRestore(); global.window = originalWindow; }); - + test('should redirect to specified URL after logout', async () => { - // Mock window object const mockLocation = { href: '' }; const originalWindow = global.window; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; const redirectUrl = 'https://example.com/logout-success'; base44.auth.logout(redirectUrl); - // Verify redirect to server-side logout endpoint with from_url parameter const expectedUrl = `${appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent(redirectUrl)}`; expect(mockLocation.href).toBe(expectedUrl); - // Restore window global.window = originalWindow; }); - + test('should redirect to logout endpoint when no redirect URL is provided', async () => { - // Mock window object const mockLocation = { href: 'https://example.com/current-page' }; const originalWindow = global.window; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; - // Call logout without redirect URL base44.auth.logout(); - // Verify redirect to server-side logout endpoint with current page as from_url const expectedUrl = `${appBaseUrl}/api/apps/auth/logout?from_url=${encodeURIComponent('https://example.com/current-page')}`; expect(mockLocation.href).toBe(expectedUrl); - // Restore window global.window = originalWindow; }); }); - + describe('setToken()', () => { test('should set token in axios headers', async () => { const token = 'test-access-token'; - base44.auth.setToken(token, false); - - // Mock the API response for me() call - scope.get(`/api/apps/${appId}/entities/User/me`) - .matchHeader('Authorization', `Bearer ${token}`) - .reply(200, { id: 'user-123', email: 'test@example.com' }); - - // Verify token is set by making a request + + let capturedAuth = null; + server.use( + http.get(meUrl, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ id: 'user-123', email: 'test@example.com' }); + }) + ); + await base44.auth.me(); - expect(scope.isDone()).toBe(true); + expect(capturedAuth).toBe(`Bearer ${token}`); }); - + test('should save token to localStorage when requested', () => { - // Mock window and localStorage const mockLocalStorage = { setItem: vi.fn(), getItem: vi.fn(), @@ -395,22 +280,16 @@ describe('Auth Module', () => { clear: vi.fn() }; const originalWindow = global.window; - global.window = { - localStorage: mockLocalStorage - }; - - const token = 'test-access-token'; - base44.auth.setToken(token, true); - - // Verify token was saved to localStorage - expect(mockLocalStorage.setItem).toHaveBeenCalledWith('base44_access_token', token); - - // Restore window + global.window = { localStorage: mockLocalStorage }; + + base44.auth.setToken('test-access-token', true); + + expect(mockLocalStorage.setItem).toHaveBeenCalledWith('base44_access_token', 'test-access-token'); + global.window = originalWindow; }); - + test('should not save token to localStorage when not requested', () => { - // Mock window and localStorage const mockLocalStorage = { setItem: vi.fn(), getItem: vi.fn(), @@ -418,35 +297,31 @@ describe('Auth Module', () => { clear: vi.fn() }; const originalWindow = global.window; - global.window = { - localStorage: mockLocalStorage - }; - - const token = 'test-access-token'; - base44.auth.setToken(token, false); - - // Verify token was not saved to localStorage + global.window = { localStorage: mockLocalStorage }; + + base44.auth.setToken('test-access-token', false); + expect(mockLocalStorage.setItem).not.toHaveBeenCalled(); - - // Restore window + global.window = originalWindow; }); - + test('should handle empty token gracefully', async () => { base44.auth.setToken('', false); - - // Mock the API response for me() call - scope.get(`/api/apps/${appId}/entities/User/me`) - .matchHeader('Authorization', (val) => !val) // Should not have Authorization header - .reply(401, { detail: 'Unauthorized' }); - - // Verify no Authorization header is sent (should throw 401) + + server.use( + http.get(meUrl, ({ request }) => { + if (!request.headers.has('Authorization')) { + return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }); + } + return HttpResponse.json({ id: 'user-123' }); + }) + ); + await expect(base44.auth.me()).rejects.toThrow(); - expect(scope.isDone()).toBe(true); }); - + test('should handle localStorage errors gracefully', () => { - // Mock window and localStorage with error const mockLocalStorage = { setItem: vi.fn().mockImplementation(() => { throw new Error('localStorage error'); @@ -454,193 +329,111 @@ describe('Auth Module', () => { }; const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const originalWindow = global.window; - global.window = { - localStorage: mockLocalStorage - }; - - const token = 'test-access-token'; - base44.auth.setToken(token, true); - - // Verify error was logged + global.window = { localStorage: mockLocalStorage }; + + base44.auth.setToken('test-access-token', true); + expect(consoleSpy).toHaveBeenCalledWith('Failed to save token to localStorage:', expect.any(Error)); - - // Restore + consoleSpy.mockRestore(); global.window = originalWindow; }); }); - + describe('loginViaEmailPassword()', () => { test('should login successfully with email and password', async () => { - const loginData = { - email: 'test@example.com', - password: 'password123' - }; - const mockResponse = { access_token: 'test-access-token', - user: { - id: 'user-123', - email: 'test@example.com', - name: 'Test User' - } + user: { id: 'user-123', email: 'test@example.com', name: 'Test User' } }; - - // Mock the API response - scope.post(`/api/apps/${appId}/auth/login`, loginData) - .reply(200, mockResponse); - - // Call the API - const result = await base44.auth.loginViaEmailPassword( - loginData.email, - loginData.password + + server.use( + http.post(loginUrl, () => HttpResponse.json(mockResponse)), + http.get(meUrl, ({ request }) => { + if (request.headers.get('Authorization') === 'Bearer test-access-token') { + return HttpResponse.json({ id: 'user-123', email: 'test@example.com' }); + } + return HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }); + }) ); - - // Verify the response + + const result = await base44.auth.loginViaEmailPassword('test@example.com', 'password123'); + expect(result.access_token).toBe('test-access-token'); expect(result.user.email).toBe('test@example.com'); - - // Verify token was set in axios headers by making a subsequent request - scope.get(`/api/apps/${appId}/entities/User/me`) - .matchHeader('Authorization', 'Bearer test-access-token') - .reply(200, { id: 'user-123', email: 'test@example.com' }); - + + // Token should be set — subsequent me() should succeed await base44.auth.me(); - expect(scope.isDone()).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('should login with turnstile token when provided', async () => { - const loginData = { - email: 'test@example.com', - password: 'password123', - turnstile_token: 'turnstile-token-123' - }; - + let capturedBody = null; const mockResponse = { access_token: 'test-access-token', - user: { - id: 'user-123', - email: 'test@example.com' - } + user: { id: 'user-123', email: 'test@example.com' } }; - - // Mock the API response - scope.post(`/api/apps/${appId}/auth/login`, loginData) - .reply(200, mockResponse); - - // Call the API + + server.use( + http.post(loginUrl, async ({ request }) => { + capturedBody = await request.json(); + return HttpResponse.json(mockResponse); + }), + http.get(meUrl, () => HttpResponse.json({ id: 'user-123', email: 'test@example.com' })) + ); + const result = await base44.auth.loginViaEmailPassword( - loginData.email, - loginData.password, - loginData.turnstile_token + 'test@example.com', + 'password123', + 'turnstile-token-123' ); - - // Verify the response + expect(result.access_token).toBe('test-access-token'); - - // Verify token was set in axios headers by making a subsequent request - scope.get(`/api/apps/${appId}/entities/User/me`) - .matchHeader('Authorization', 'Bearer test-access-token') - .reply(200, { id: 'user-123', email: 'test@example.com' }); - + expect(capturedBody.turnstile_token).toBe('turnstile-token-123'); + await base44.auth.me(); - expect(scope.isDone()).toBe(true); }); - + test('should handle authentication errors and logout', async () => { - const loginData = { - email: 'test@example.com', - password: 'wrongpassword' - }; - - // Mock the API error response - scope.post(`/api/apps/${appId}/auth/login`, loginData) - .reply(401, { detail: 'Invalid credentials' }); - - // Set a token first to test logout + server.use( + http.post(loginUrl, () => HttpResponse.json({ detail: 'Invalid credentials' }, { status: 401 })) + ); + base44.auth.setToken('existing-token', false); - - // Call the API and expect an error + await expect( - base44.auth.loginViaEmailPassword(loginData.email, loginData.password) + base44.auth.loginViaEmailPassword('test@example.com', 'wrongpassword') ).rejects.toThrow(); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('should handle network errors', async () => { - const loginData = { - email: 'test@example.com', - password: 'password123' - }; - - // Mock network error - scope.post(`/api/apps/${appId}/auth/login`, loginData) - .replyWithError('Network error'); - - // Call the API and expect an error + server.use(http.post(loginUrl, () => HttpResponse.error())); + await expect( - base44.auth.loginViaEmailPassword(loginData.email, loginData.password) + base44.auth.loginViaEmailPassword('test@example.com', 'password123') ).rejects.toThrow(); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); }); - + describe('isAuthenticated()', () => { test('should return true when token is valid', async () => { - const mockUser = { - id: 'user-123', - email: 'test@example.com' - }; - - // Mock the API response - scope.get(`/api/apps/${appId}/entities/User/me`) - .reply(200, mockUser); - - // Call the API + server.use(http.get(meUrl, () => HttpResponse.json({ id: 'user-123', email: 'test@example.com' }))); + const result = await base44.auth.isAuthenticated(); - - // Verify the response expect(result).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('should return false when token is invalid', async () => { - // Mock the API error response - scope.get(`/api/apps/${appId}/entities/User/me`) - .reply(401, { detail: 'Unauthorized' }); - - // Call the API + server.use(http.get(meUrl, () => HttpResponse.json({ detail: 'Unauthorized' }, { status: 401 }))); + const result = await base44.auth.isAuthenticated(); - - // Verify the response expect(result).toBe(false); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('should return false on network errors', async () => { - // Mock network error - scope.get(`/api/apps/${appId}/entities/User/me`) - .replyWithError('Network error'); - - // Call the API + server.use(http.get(meUrl, () => HttpResponse.error())); + const result = await base44.auth.isAuthenticated(); - - // Verify the response expect(result).toBe(false); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index 4a7b3ef..5d00eca 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -1,28 +1,28 @@ import { createClient, createClientFromRequest } from '../../src/index.ts'; -import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import nock from 'nock'; +import { describe, test, expect, afterEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; describe('Client Creation', () => { test('should create a client with default options', () => { - const client = createClient({ - appId: 'test-app-id', - }); - + const client = createClient({ appId: 'test-app-id' }); + expect(client).toBeDefined(); expect(client.entities).toBeDefined(); expect(client.integrations).toBeDefined(); expect(client.auth).toBeDefined(); expect(client.analytics).toBeDefined(); - + const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); expect(config.serverUrl).toBe('https://base44.app'); expect(config.requiresAuth).toBe(false); - - // Should throw error when accessing asServiceRole without service token + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + + client.cleanup(); }); - + test('should create a client with custom options', () => { const client = createClient({ appId: 'test-app-id', @@ -30,21 +30,20 @@ describe('Client Creation', () => { requiresAuth: true, token: 'test-token', }); - + expect(client).toBeDefined(); - + const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); expect(config.serverUrl).toBe('https://custom-server.com'); expect(config.requiresAuth).toBe(true); + + client.cleanup(); }); test('should create a client with service token', () => { - const client = createClient({ - appId: 'test-app-id', - serviceToken: 'service-token-123', - }); - + const client = createClient({ appId: 'test-app-id', serviceToken: 'service-token-123' }); + expect(client).toBeDefined(); expect(client.entities).toBeDefined(); expect(client.integrations).toBeDefined(); @@ -53,8 +52,9 @@ describe('Client Creation', () => { expect(client.asServiceRole.entities).toBeDefined(); expect(client.asServiceRole.integrations).toBeDefined(); expect(client.asServiceRole.functions).toBeDefined(); - // Service role should not have auth module expect(client.asServiceRole.auth).toBeUndefined(); + + client.cleanup(); }); test('should create a client with both user token and service token', () => { @@ -74,60 +74,45 @@ describe('Client Creation', () => { expect(client.asServiceRole.integrations).toBeDefined(); expect(client.asServiceRole.functions).toBeDefined(); expect(client.asServiceRole.auth).toBeUndefined(); - }); + client.cleanup(); + }); }); describe('appBaseUrl Normalization', () => { test('should use appBaseUrl when provided as a string', () => { const customAppBaseUrl = 'https://custom-app.example.com'; - const client = createClient({ - appId: 'test-app-id', - appBaseUrl: customAppBaseUrl, - }); + const client = createClient({ appId: 'test-app-id', appBaseUrl: customAppBaseUrl }); - // Mock window.location const originalWindow = global.window; const mockLocation = { href: '', origin: 'https://current-app.com' }; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; - const nextUrl = 'https://example.com/dashboard'; - client.auth.redirectToLogin(nextUrl); + client.auth.redirectToLogin('https://example.com/dashboard'); - // Verify the redirect URL uses the custom appBaseUrl expect(mockLocation.href).toBe( - `${customAppBaseUrl}/login?from_url=${encodeURIComponent(nextUrl)}` + `${customAppBaseUrl}/login?from_url=${encodeURIComponent('https://example.com/dashboard')}` ); - // Restore window global.window = originalWindow; + client.cleanup(); }); test('should normalize appBaseUrl to empty string when not provided', () => { - const client = createClient({ - appId: 'test-app-id', - // appBaseUrl not provided - }); + const client = createClient({ appId: 'test-app-id' }); - // Mock window.location const originalWindow = global.window; const mockLocation = { href: '', origin: 'https://current-app.com' }; - global.window = { - location: mockLocation - }; + global.window = { location: mockLocation }; - const nextUrl = 'https://example.com/dashboard'; - client.auth.redirectToLogin(nextUrl); + client.auth.redirectToLogin('https://example.com/dashboard'); - // Verify the redirect URL uses empty string (relative path) expect(mockLocation.href).toBe( - `/login?from_url=${encodeURIComponent(nextUrl)}` + `/login?from_url=${encodeURIComponent('https://example.com/dashboard')}` ); - // Restore window global.window = originalWindow; + client.cleanup(); }); }); @@ -148,57 +133,57 @@ describe('createClientFromRequest', () => { }; const client = createClientFromRequest(mockRequest); - + expect(client).toBeDefined(); expect(client.entities).toBeDefined(); expect(client.integrations).toBeDefined(); expect(client.auth).toBeDefined(); expect(client.asServiceRole).toBeDefined(); - + const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); expect(config.serverUrl).toBe('https://custom-server.com'); + + client.cleanup(); }); test('should create client from request with minimal headers', () => { const mockRequest = { headers: { get: (name) => { - const headers = { - 'Base44-App-Id': 'minimal-app-id' - }; + const headers = { 'Base44-App-Id': 'minimal-app-id' }; return headers[name] || null; } } }; const client = createClientFromRequest(mockRequest); - + expect(client).toBeDefined(); const config = client.getConfig(); expect(config.appId).toBe('minimal-app-id'); - expect(config.serverUrl).toBe('https://base44.app'); // Default value + expect(config.serverUrl).toBe('https://base44.app'); + + client.cleanup(); }); test('should create client with only user token', () => { const mockRequest = { headers: { get: (name) => { - const headers = { - 'Authorization': 'Bearer user-only-token', - 'Base44-App-Id': 'user-app-id' - }; + const headers = { 'Authorization': 'Bearer user-only-token', 'Base44-App-Id': 'user-app-id' }; return headers[name] || null; } } }; const client = createClientFromRequest(mockRequest); - + expect(client).toBeDefined(); expect(client.auth).toBeDefined(); - // Should throw error when accessing asServiceRole without service token expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + + client.cleanup(); }); test('should create client with only service token', () => { @@ -215,19 +200,19 @@ describe('createClientFromRequest', () => { }; const client = createClientFromRequest(mockRequest); - + expect(client).toBeDefined(); expect(client.auth).toBeDefined(); expect(client.asServiceRole).toBeDefined(); + + client.cleanup(); }); test('should throw error when Base44-App-Id header is missing', () => { const mockRequest = { headers: { get: (name) => { - const headers = { - 'Authorization': 'Bearer some-token' - }; + const headers = { 'Authorization': 'Bearer some-token' }; return headers[name] || null; } } @@ -252,7 +237,6 @@ describe('createClientFromRequest', () => { } }; - // Should throw error for malformed headers instead of continuing silently expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer "'); }); @@ -270,7 +254,6 @@ describe('createClientFromRequest', () => { } }; - // Should throw error for empty headers instead of continuing silently expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer "'); }); @@ -278,199 +261,160 @@ describe('createClientFromRequest', () => { const mockRequest = { headers: { get: (name) => { - const headers = { - 'Base44-App-Id': 'test-app-id', - 'Base44-State': '192.168.1.100' - }; + const headers = { 'Base44-App-Id': 'test-app-id', 'Base44-State': '192.168.1.100' }; return headers[name] || null; } } }; const client = createClientFromRequest(mockRequest); - + expect(client).toBeDefined(); const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); + + client.cleanup(); }); test('should work without Base44-State header', () => { const mockRequest = { headers: { get: (name) => { - const headers = { - 'Base44-App-Id': 'test-app-id' - }; + const headers = { 'Base44-App-Id': 'test-app-id' }; return headers[name] || null; } } }; const client = createClientFromRequest(mockRequest); - + expect(client).toBeDefined(); const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); + + client.cleanup(); }); }); describe('Service Role Authorization Headers', () => { - - let scope; const appId = 'test-app-id'; const serverUrl = 'https://api.base44.com'; - - beforeEach(() => { - // Create a nock scope for mocking API calls - scope = nock(serverUrl); - - // Enable request debugging for Nock - nock.disableNetConnect(); - nock.emitter.on('no match', (req) => { - console.log(`Nock: No match for ${req.method} ${req.path}`); - console.log('Headers:', req.getHeaders()); - }); - }); - - afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); - nock.emitter.removeAllListeners('no match'); - nock.enableNetConnect(); - }); + const entitiesBase = `${serverUrl}/api/apps/${appId}/entities`; + const intBase = `${serverUrl}/api/apps/${appId}/integration-endpoints`; + const functionsBase = `${serverUrl}/api/apps/${appId}/functions`; test('should use user token for regular client operations and service token for service role operations', async () => { const userToken = 'user-token-123'; const serviceToken = 'service-token-456'; - - const client = createClient({ - serverUrl, - appId, - token: userToken, - serviceToken: serviceToken, - }); - - // Mock user entities request (should use user token) - scope.get(`/api/apps/${appId}/entities/Todo`) - .matchHeader('Authorization', `Bearer ${userToken}`) - .reply(200, { items: [], total: 0 }); - - // Mock service role entities request (should use service token) - scope.get(`/api/apps/${appId}/entities/Todo`) - .matchHeader('Authorization', `Bearer ${serviceToken}`) - .reply(200, { items: [], total: 0 }); + const client = createClient({ serverUrl, appId, token: userToken, serviceToken }); + + const userCalls = []; + const serviceCalls = []; + + server.use( + http.get(`${entitiesBase}/Todo`, ({ request }) => { + const auth = request.headers.get('Authorization'); + if (auth === `Bearer ${userToken}`) userCalls.push('user'); + if (auth === `Bearer ${serviceToken}`) serviceCalls.push('service'); + return HttpResponse.json({ items: [], total: 0 }); + }) + ); - // Make requests await client.entities.Todo.list(); await client.asServiceRole.entities.Todo.list(); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + expect(userCalls).toHaveLength(1); + expect(serviceCalls).toHaveLength(1); + + client.cleanup(); }); test('should use service token for service role entities operations', async () => { const serviceToken = 'service-token-only-123'; - - const client = createClient({ - serverUrl, - appId, - serviceToken: serviceToken, - }); - - // Mock service role entities request - scope.get(`/api/apps/${appId}/entities/User/123`) - .matchHeader('Authorization', `Bearer ${serviceToken}`) - .reply(200, { id: '123', name: 'Test User' }); + const client = createClient({ serverUrl, appId, serviceToken }); + + let capturedAuth = null; + server.use( + http.get(`${entitiesBase}/User/123`, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ id: '123', name: 'Test User' }); + }) + ); - // Make request const result = await client.asServiceRole.entities.User.get('123'); - // Verify response expect(result.id).toBe('123'); expect(result.name).toBe('Test User'); + expect(capturedAuth).toBe(`Bearer ${serviceToken}`); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + client.cleanup(); }); test('should use service token for service role integrations operations', async () => { const serviceToken = 'service-token-integration-456'; - - const client = createClient({ - serverUrl, - appId, - serviceToken: serviceToken, - }); - - // Mock service role integrations request - scope.post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`) - .matchHeader('Authorization', `Bearer ${serviceToken}`) - .reply(200, { success: true, messageId: '123' }); + const client = createClient({ serverUrl, appId, serviceToken }); + + let capturedAuth = null; + server.use( + http.post(`${intBase}/Core/SendEmail`, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ success: true, messageId: '123' }); + }) + ); - // Make request - const result = await client.asServiceRole.integrations.Core.SendEmail({ + const result = await client.asServiceRole.integrations.Core.SendEmail({ to: 'test@example.com', subject: 'Test', body: 'Test message' }); - // Verify response expect(result.success).toBe(true); expect(result.messageId).toBe('123'); + expect(capturedAuth).toBe(`Bearer ${serviceToken}`); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + client.cleanup(); }); test('should use service token for service role functions operations', async () => { const serviceToken = 'service-token-functions-789'; - - const client = createClient({ - serverUrl, - appId, - serviceToken: serviceToken, - }); - - // Mock service role functions request - scope.post(`/api/apps/${appId}/functions/testFunction`, { param: 'test' }) - .matchHeader('Authorization', `Bearer ${serviceToken}`) - .reply(200, { result: 'function executed' }); + const client = createClient({ serverUrl, appId, serviceToken }); + + let capturedAuth = null; + server.use( + http.post(`${functionsBase}/testFunction`, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ result: 'function executed' }); + }) + ); - // Make request - const result = await client.asServiceRole.functions.invoke('testFunction', { - param: 'test' - }); + const result = await client.asServiceRole.functions.invoke('testFunction', { param: 'test' }); - // Verify response expect(result.data.result).toBe('function executed'); + expect(capturedAuth).toBe(`Bearer ${serviceToken}`); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + client.cleanup(); }); test('should use user token for regular operations when both tokens are present', async () => { const userToken = 'user-token-regular-123'; const serviceToken = 'service-token-regular-456'; - - const client = createClient({ - serverUrl, - appId, - token: userToken, - serviceToken: serviceToken, - }); - - // Mock regular user entities request (should use user token) - scope.get(`/api/apps/${appId}/entities/Task`) - .matchHeader('Authorization', `Bearer ${userToken}`) - .reply(200, { items: [{ id: 'task1', title: 'User Task' }], total: 1 }); - - // Mock regular integrations request (should use user token) - scope.post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`) - .matchHeader('Authorization', `Bearer ${userToken}`) - .reply(200, { success: true, messageId: 'email123' }); + const client = createClient({ serverUrl, appId, token: userToken, serviceToken }); + + let taskAuth = null; + let emailAuth = null; + + server.use( + http.get(`${entitiesBase}/Task`, ({ request }) => { + taskAuth = request.headers.get('Authorization'); + return HttpResponse.json({ items: [{ id: 'task1', title: 'User Task' }], total: 1 }); + }), + http.post(`${intBase}/Core/SendEmail`, ({ request }) => { + emailAuth = request.headers.get('Authorization'); + return HttpResponse.json({ success: true, messageId: 'email123' }); + }) + ); - // Make requests using regular client (not service role) const taskResult = await client.entities.Task.list(); const emailResult = await client.integrations.Core.SendEmail({ to: 'user@example.com', @@ -478,39 +422,36 @@ describe('Service Role Authorization Headers', () => { body: 'User message' }); - // Verify responses expect(taskResult.items[0].title).toBe('User Task'); expect(emailResult.success).toBe(true); expect(emailResult.messageId).toBe('email123'); + expect(taskAuth).toBe(`Bearer ${userToken}`); + expect(emailAuth).toBe(`Bearer ${userToken}`); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + client.cleanup(); }); test('should work without authorization header when no tokens are provided', async () => { - const client = createClient({ - serverUrl, - appId, - }); - - // Mock request without authorization header - scope.get(`/api/apps/${appId}/entities/PublicData`) - .matchHeader('Authorization', (val) => !val) // Should not have Authorization header - .reply(200, { items: [{ id: 'public1', data: 'public' }], total: 1 }); + const client = createClient({ serverUrl, appId }); + + let capturedAuth = null; + server.use( + http.get(`${entitiesBase}/PublicData`, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ items: [{ id: 'public1', data: 'public' }], total: 1 }); + }) + ); - // Make request const result = await client.entities.PublicData.list(); - // Verify response expect(result.items[0].data).toBe('public'); + expect(capturedAuth).toBeNull(); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + client.cleanup(); }); test('should propagate Base44-State header in API requests when created from request', async () => { const clientIp = '192.168.1.100'; - const mockRequest = { headers: { get: (name) => { @@ -527,17 +468,22 @@ describe('Service Role Authorization Headers', () => { const client = createClientFromRequest(mockRequest); - // Mock entities request and verify Base44-State header is present - scope.get(`/api/apps/${appId}/entities/Todo`) - .matchHeader('Base44-State', clientIp) - .matchHeader('Authorization', 'Bearer user-token-123') - .reply(200, { items: [], total: 0 }); + let capturedState = null; + let capturedAuth = null; + server.use( + http.get(`${entitiesBase}/Todo`, ({ request }) => { + capturedState = request.headers.get('Base44-State'); + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ items: [], total: 0 }); + }) + ); - // Make request await client.entities.Todo.list(); - // Verify all mocks were called (including header match) - expect(scope.isDone()).toBe(true); + expect(capturedState).toBe(clientIp); + expect(capturedAuth).toBe('Bearer user-token-123'); + + client.cleanup(); }); test('should not include Base44-State header when not present in original request', async () => { @@ -556,22 +502,23 @@ describe('Service Role Authorization Headers', () => { const client = createClientFromRequest(mockRequest); - // Mock entities request and verify Base44-State header is NOT present - scope.get(`/api/apps/${appId}/entities/Todo`) - .matchHeader('Base44-State', (val) => !val) // Should not have this header - .matchHeader('Authorization', 'Bearer user-token-123') - .reply(200, { items: [], total: 0 }); + let capturedState = null; + server.use( + http.get(`${entitiesBase}/Todo`, ({ request }) => { + capturedState = request.headers.get('Base44-State'); + return HttpResponse.json({ items: [], total: 0 }); + }) + ); - // Make request await client.entities.Todo.list(); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + expect(capturedState).toBeNull(); + + client.cleanup(); }); test('should propagate Base44-State header in service role API requests', async () => { const clientIp = '10.0.0.50'; - const mockRequest = { headers: { get: (name) => { @@ -588,20 +535,22 @@ describe('Service Role Authorization Headers', () => { const client = createClientFromRequest(mockRequest); - // Mock service role entities request and verify Base44-State header is present - scope.get(`/api/apps/${appId}/entities/User/123`) - .matchHeader('Base44-State', clientIp) - .matchHeader('Authorization', 'Bearer service-token-123') - .reply(200, { id: '123', name: 'Test User' }); + let capturedState = null; + let capturedAuth = null; + server.use( + http.get(`${entitiesBase}/User/123`, ({ request }) => { + capturedState = request.headers.get('Base44-State'); + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ id: '123', name: 'Test User' }); + }) + ); - // Make request using service role const result = await client.asServiceRole.entities.User.get('123'); - // Verify response expect(result.id).toBe('123'); + expect(capturedState).toBe(clientIp); + expect(capturedAuth).toBe('Bearer service-token-123'); - // Verify all mocks were called (including header match) - expect(scope.isDone()).toBe(true); + client.cleanup(); }); - -}); \ No newline at end of file +}); diff --git a/tests/unit/connectors.test.ts b/tests/unit/connectors.test.ts index 47d5761..8d17ea2 100644 --- a/tests/unit/connectors.test.ts +++ b/tests/unit/connectors.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; -import nock from "nock"; +import { http, HttpResponse } from "msw"; +import { server } from "../mocks/server"; import { createClient } from "../../src/index.ts"; describe("Connectors module – getConnection", () => { @@ -7,81 +8,62 @@ describe("Connectors module – getConnection", () => { const serverUrl = "https://base44.app"; const serviceToken = "service-token-123"; let base44: ReturnType; - let scope: nock.Scope; + const tokensBase = `${serverUrl}/api/apps/${appId}/external-auth/tokens`; beforeEach(() => { - base44 = createClient({ - serverUrl, - appId, - serviceToken, - }); - scope = nock(serverUrl); + base44 = createClient({ serverUrl, appId, serviceToken }); }); afterEach(() => { - nock.cleanAll(); + base44.cleanup(); }); test("extracts accessToken and connectionConfig from API response", async () => { - const apiResponse = { - access_token: "oauth-token-abc123", - integration_type: "jira", - connection_config: { subdomain: "my-company" }, - }; - - scope - .get(`/api/apps/${appId}/external-auth/tokens/jira`) - .reply(200, apiResponse); - - const connection = await base44.asServiceRole.connectors.getConnection( - "jira" + server.use( + http.get(`${tokensBase}/jira`, () => + HttpResponse.json({ + access_token: "oauth-token-abc123", + integration_type: "jira", + connection_config: { subdomain: "my-company" }, + }) + ) ); + const connection = await base44.asServiceRole.connectors.getConnection("jira"); + expect(connection).toBeDefined(); expect(connection.accessToken).toBe("oauth-token-abc123"); - expect(connection.connectionConfig).toEqual({ - subdomain: "my-company", - }); - expect(scope.isDone()).toBe(true); + expect(connection.connectionConfig).toEqual({ subdomain: "my-company" }); }); test("returns connectionConfig as null when API omits connection_config", async () => { - const apiResponse = { - access_token: "token-only", - integration_type: "slack", - }; - - scope - .get(`/api/apps/${appId}/external-auth/tokens/slack`) - .reply(200, apiResponse); - - const connection = await base44.asServiceRole.connectors.getConnection( - "slack" + server.use( + http.get(`${tokensBase}/slack`, () => + HttpResponse.json({ access_token: "token-only", integration_type: "slack" }) + ) ); + const connection = await base44.asServiceRole.connectors.getConnection("slack"); + expect(connection.accessToken).toBe("token-only"); expect(connection.connectionConfig).toBeNull(); - expect(scope.isDone()).toBe(true); }); test("returns connectionConfig as null when API sends null connection_config", async () => { - const apiResponse = { - access_token: "token-only", - integration_type: "github", - connection_config: null, - }; - - scope - .get(`/api/apps/${appId}/external-auth/tokens/github`) - .reply(200, apiResponse); - - const connection = await base44.asServiceRole.connectors.getConnection( - "github" + server.use( + http.get(`${tokensBase}/github`, () => + HttpResponse.json({ + access_token: "token-only", + integration_type: "github", + connection_config: null, + }) + ) ); + const connection = await base44.asServiceRole.connectors.getConnection("github"); + expect(connection.accessToken).toBe("token-only"); expect(connection.connectionConfig).toBeNull(); - expect(scope.isDone()).toBe(true); }); test("throws when integrationType is empty string", async () => { @@ -92,9 +74,7 @@ describe("Connectors module – getConnection", () => { test("throws when integrationType is not a string", async () => { await expect( - base44.asServiceRole.connectors.getConnection( - null as unknown as string - ) + base44.asServiceRole.connectors.getConnection(null as unknown as string) ).rejects.toThrow("Integration type is required and must be a string"); }); }); diff --git a/tests/unit/custom-integrations.test.ts b/tests/unit/custom-integrations.test.ts index ab34bea..69aae4b 100644 --- a/tests/unit/custom-integrations.test.ts +++ b/tests/unit/custom-integrations.test.ts @@ -1,159 +1,138 @@ import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import nock from 'nock'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; import { createClient } from '../../src/index.ts'; describe('Custom Integrations Module', () => { let base44: ReturnType; - let scope: nock.Scope; const appId = 'test-app-id'; const serverUrl = 'https://base44.app'; + const customBase = `${serverUrl}/api/apps/${appId}/integrations/custom`; + + // The SDK URL-encodes only curly braces in operationIds (not : or /) + function sdkOperationUrl(slug: string, operationId: string): string { + const encoded = operationId.replace(/{/g, '%7B').replace(/}/g, '%7D'); + return `${customBase}/${slug}/${encoded}`; + } + + // Build a RegExp that matches the SDK-generated URL for a given slug + operationId + function operationPattern(slug: string, operationId: string): RegExp { + const encoded = operationId.replace(/{/g, '%7B').replace(/}/g, '%7D') + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex special chars + .replace(/%7B/g, '%7B') // restore our intentional encoding + .replace(/%7D/g, '%7D'); + const escapedBase = customBase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`^${escapedBase}/${slug}/${encoded}$`); + } beforeEach(() => { - // Create a new client for each test - base44 = createClient({ - serverUrl, - appId, - }); - - // Create a nock scope for mocking API calls - scope = nock(serverUrl); + base44 = createClient({ serverUrl, appId }); }); afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); + base44.cleanup(); }); test('custom.call() should convert camelCase params to snake_case for backend', async () => { const slug = 'github'; const operationId = 'get:/repos/{owner}/{repo}/issues'; - - // SDK call uses camelCase (JS convention) - const sdkParams = { + + let capturedBody: Record | null = null; + server.use( + http.post(operationPattern(slug, operationId), async ({ request }) => { + capturedBody = await request.json() as Record; + return HttpResponse.json({ + success: true, + status_code: 200, + data: { issues: [{ id: 1, title: 'Test Issue' }] }, + }); + }) + ); + + const result = await base44.integrations.custom.call(slug, operationId, { payload: { title: 'Test Issue' }, pathParams: { owner: 'testuser', repo: 'testrepo' }, queryParams: { state: 'open' }, - }; - - // Backend expects snake_case (Python convention) - const expectedBody = { - payload: { title: 'Test Issue' }, - path_params: { owner: 'testuser', repo: 'testrepo' }, - query_params: { state: 'open' }, - }; - - const mockResponse = { - success: true, - status_code: 200, - data: { issues: [{ id: 1, title: 'Test Issue' }] }, - }; - - // Mock expects snake_case body (curly braces in operationId must be URL-encoded for nock matching) - const encodedOperationId = operationId.replace(/{/g, '%7B').replace(/}/g, '%7D'); - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${encodedOperationId}`, expectedBody) - .reply(200, mockResponse); - - // SDK call uses camelCase - const result = await base44.integrations.custom.call(slug, operationId, sdkParams); + }); - // Verify the response expect(result.success).toBe(true); expect(result.status_code).toBe(200); expect(result.data.issues).toHaveLength(1); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + // Verify camelCase was converted to snake_case + expect(capturedBody).toMatchObject({ + payload: { title: 'Test Issue' }, + path_params: { owner: 'testuser', repo: 'testrepo' }, + query_params: { state: 'open' }, + }); }); test('custom.call() should work with empty params', async () => { - const slug = 'github'; - const operationId = 'getAuthenticatedUser'; - - const mockResponse = { - success: true, - status_code: 200, - data: { login: 'testuser', id: 123 }, - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${operationId}`, {}) - .reply(200, mockResponse); + server.use( + http.post(`${customBase}/github/getAuthenticatedUser`, () => + HttpResponse.json({ + success: true, + status_code: 200, + data: { login: 'testuser', id: 123 }, + }) + ) + ); - // Call without params - const result = await base44.integrations.custom.call(slug, operationId); + const result = await base44.integrations.custom.call('github', 'getAuthenticatedUser'); - // Verify the response expect(result.success).toBe(true); expect(result.data.login).toBe('testuser'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should handle 404 error for non-existent integration', async () => { - const slug = 'nonexistent'; - const operationId = 'someEndpoint'; - - // Mock a 404 error response - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${operationId}`, {}) - .reply(404, { - detail: `Custom integration '${slug}' not found in workspace`, - }); + server.use( + http.post(`${customBase}/nonexistent/someEndpoint`, () => + HttpResponse.json( + { detail: "Custom integration 'nonexistent' not found in workspace" }, + { status: 404 } + ) + ) + ); - // Call the API and expect an error - await expect(base44.integrations.custom.call(slug, operationId)).rejects.toMatchObject({ + await expect(base44.integrations.custom.call('nonexistent', 'someEndpoint')).rejects.toMatchObject({ status: 404, name: 'Base44Error', }); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should handle 404 error for non-existent operation', async () => { - const slug = 'github'; - const operationId = 'nonExistentOperation'; - - // Mock a 404 error response - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${operationId}`, {}) - .reply(404, { - detail: `Operation '${operationId}' not found in integration '${slug}'`, - }); + server.use( + http.post(`${customBase}/github/nonExistentOperation`, () => + HttpResponse.json( + { detail: "Operation 'nonExistentOperation' not found in integration 'github'" }, + { status: 404 } + ) + ) + ); - // Call the API and expect an error - await expect(base44.integrations.custom.call(slug, operationId)).rejects.toMatchObject({ + await expect(base44.integrations.custom.call('github', 'nonExistentOperation')).rejects.toMatchObject({ status: 404, name: 'Base44Error', }); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should handle 502 error from external API', async () => { const slug = 'github'; const operationId = 'get:/repos/{owner}/{repo}/issues'; - // Mock a 502 error response (external API failure) - curly braces in operationId must be URL-encoded - const encodedOperationId = operationId.replace(/{/g, '%7B').replace(/}/g, '%7D'); - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${encodedOperationId}`, {}) - .reply(502, { - detail: 'Failed to connect to external API: Connection refused', - }); + server.use( + http.post(operationPattern(slug, operationId), () => + HttpResponse.json( + { detail: 'Failed to connect to external API: Connection refused' }, + { status: 502 } + ) + ) + ); - // Call the API and expect an error await expect(base44.integrations.custom.call(slug, operationId)).rejects.toMatchObject({ status: 502, name: 'Base44Error', }); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should throw error when slug is missing', async () => { @@ -195,166 +174,106 @@ describe('Custom Integrations Module', () => { }); test('custom.call() should handle large payloads', async () => { - const slug = 'myapi'; - const operationId = 'bulkCreate'; - - // Create a large payload with many items const largeArray = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}`, description: 'A'.repeat(100), metadata: { key: `value_${i}` }, })); - - const sdkParams = { - payload: { items: largeArray }, - }; - const mockResponse = { - success: true, - status_code: 200, - data: { created: 1000 }, - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${operationId}`, sdkParams) - .reply(200, mockResponse); + server.use( + http.post(`${customBase}/myapi/bulkCreate`, () => + HttpResponse.json({ success: true, status_code: 200, data: { created: 1000 } }) + ) + ); - // Call the API with large payload - const result = await base44.integrations.custom.call(slug, operationId, sdkParams); + const result = await base44.integrations.custom.call('myapi', 'bulkCreate', { + payload: { items: largeArray }, + }); - // Verify the response expect(result.success).toBe(true); expect(result.data.created).toBe(1000); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should include custom headers in request', async () => { - const slug = 'myapi'; - const operationId = 'getData'; - const sdkParams = { - headers: { 'X-Custom-Header': 'custom-value' }, - }; - - const mockResponse = { - success: true, - status_code: 200, - data: { result: 'ok' }, - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${operationId}`, sdkParams) - .reply(200, mockResponse); + server.use( + http.post(`${customBase}/myapi/getData`, () => + HttpResponse.json({ success: true, status_code: 200, data: { result: 'ok' } }) + ) + ); - // Call the API - const result = await base44.integrations.custom.call(slug, operationId, sdkParams); + const result = await base44.integrations.custom.call('myapi', 'getData', { + headers: { 'X-Custom-Header': 'custom-value' }, + }); - // Verify the response expect(result.success).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should pass through multiple headers', async () => { - const slug = 'myapi'; - const operationId = 'secureEndpoint'; - const sdkParams = { + server.use( + http.post(`${customBase}/myapi/secureEndpoint`, () => + HttpResponse.json({ success: true, status_code: 200, data: { authenticated: true } }) + ) + ); + + const result = await base44.integrations.custom.call('myapi', 'secureEndpoint', { headers: { 'X-API-Key': 'secret-key-123', 'X-Request-ID': 'req-456', 'Accept-Language': 'en-US', 'X-Custom-Auth': 'Bearer token123', }, - }; - - const mockResponse = { - success: true, - status_code: 200, - data: { authenticated: true }, - }; - - // Mock the API response - verify all headers are passed in the body - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${operationId}`, sdkParams) - .reply(200, mockResponse); - - // Call the API - const result = await base44.integrations.custom.call(slug, operationId, sdkParams); + }); - // Verify the response expect(result.success).toBe(true); expect(result.data.authenticated).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test('custom.call() should only include defined params in body', async () => { const slug = 'github'; const operationId = 'get:/users/{username}'; - - // SDK call with only pathParams - const sdkParams = { - pathParams: { username: 'octocat' }, - }; - - // Expected body should only have path_params, not empty payload/query_params/headers - const expectedBody = { - path_params: { username: 'octocat' }, - }; - const mockResponse = { - success: true, - status_code: 200, - data: { login: 'octocat' }, - }; - - // Curly braces in operationId must be URL-encoded for nock matching - const encodedOperationId = operationId.replace(/{/g, '%7B').replace(/}/g, '%7D'); - scope - .post(`/api/apps/${appId}/integrations/custom/${slug}/${encodedOperationId}`, expectedBody) - .reply(200, mockResponse); + let capturedBody: Record | null = null; + server.use( + http.post(operationPattern(slug, operationId), async ({ request }) => { + capturedBody = await request.json() as Record; + return HttpResponse.json({ + success: true, + status_code: 200, + data: { login: 'octocat' }, + }); + }) + ); - const result = await base44.integrations.custom.call(slug, operationId, sdkParams); + const result = await base44.integrations.custom.call(slug, operationId, { + pathParams: { username: 'octocat' }, + }); expect(result.success).toBe(true); - expect(scope.isDone()).toBe(true); + // Only path_params should be present, not empty payload/query_params/headers + expect(capturedBody).toEqual({ path_params: { username: 'octocat' } }); }); test('custom property should not interfere with other integration packages', async () => { - // Test that Core still works - const coreParams = { + const intBase = `${serverUrl}/api/apps/${appId}/integration-endpoints`; + + server.use( + http.post(`${intBase}/Core/SendEmail`, () => + HttpResponse.json({ success: true }) + ), + http.post(`${intBase}/installable/SomePackage/integration-endpoints/SomeEndpoint`, () => + HttpResponse.json({ success: true }) + ) + ); + + const coreResult = await base44.integrations.Core.SendEmail({ to: 'test@example.com', subject: 'Test', body: 'Test body', - }; - - scope - .post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`, coreParams) - .reply(200, { success: true }); - - const coreResult = await base44.integrations.Core.SendEmail(coreParams); + }); expect(coreResult.success).toBe(true); - // Test that custom packages still work - const customPackageParams = { param: 'value' }; - - scope - .post( - `/api/apps/${appId}/integration-endpoints/installable/SomePackage/integration-endpoints/SomeEndpoint`, - customPackageParams - ) - .reply(200, { success: true }); - - const packageResult = await base44.integrations.SomePackage.SomeEndpoint(customPackageParams); + const packageResult = await base44.integrations.SomePackage.SomeEndpoint({ param: 'value' }); expect(packageResult.success).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); }); diff --git a/tests/unit/entities.test.ts b/tests/unit/entities.test.ts index e655a05..3834d61 100644 --- a/tests/unit/entities.test.ts +++ b/tests/unit/entities.test.ts @@ -1,5 +1,6 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; -import nock from "nock"; +import { http, HttpResponse } from "msw"; +import { server } from "../mocks/server"; import { createClient } from "../../src/index.ts"; import type { DeleteResult, UpdateManyResult } from "../../src/modules/entities.types.ts"; @@ -21,33 +22,16 @@ declare module "../../src/modules/entities.types.ts" { describe("Entities Module", () => { let base44: ReturnType; - let scope: nock.Scope; const appId = "test-app-id"; const serverUrl = "https://api.base44.com"; + const baseUrl = `${serverUrl}/api/apps/${appId}/entities`; beforeEach(() => { - // Create a new client for each test - base44 = createClient({ - serverUrl, - appId, - }); - - // Create a nock scope for mocking API calls - scope = nock(serverUrl); - - // Enable request debugging for Nock - nock.disableNetConnect(); - nock.emitter.on("no match", (req) => { - console.log(`Nock: No match for ${req.method} ${req.path}`); - console.log("Headers:", req.getHeaders()); - }); + base44 = createClient({ serverUrl, appId }); }); afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); - nock.emitter.removeAllListeners("no match"); - nock.enableNetConnect(); + base44.cleanup(); }); test("list() should fetch entities with correct parameters", async () => { @@ -56,242 +40,148 @@ describe("Entities Module", () => { { id: "2", title: "Task 2", completed: true }, ]; - // Mock the API response - scope - .get(`/api/apps/${appId}/entities/Todo`) - .query(true) // Accept any query parameters - .reply(200, mockTodos); + server.use( + http.get(`${baseUrl}/Todo`, () => HttpResponse.json(mockTodos)) + ); - // Call the API - const result = await base44.entities.Todo.list("title", 10, 0, [ - "id", - "title", - ]); + const result = await base44.entities.Todo.list("title", 10, 0, ["id", "title"]); - // Verify the response expect(result).toHaveLength(2); expect(result[0].title).toBe("Task 1"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("filter() should send correct query parameters", async () => { - const filterQuery: Partial = { completed: true }; const mockTodos: Todo[] = [{ id: "2", title: "Task 2", completed: true }]; - // Mock the API response - scope - .get(`/api/apps/${appId}/entities/Todo`) - .query((query) => { - // Verify the query contains our filter - const parsedQ = JSON.parse(query.q as string); - return parsedQ.completed === true; + server.use( + http.get(`${baseUrl}/Todo`, ({ request }) => { + const url = new URL(request.url); + const q = url.searchParams.get("q"); + if (q && JSON.parse(q).completed === true) { + return HttpResponse.json(mockTodos); + } + return HttpResponse.json([], { status: 400 }); }) - .reply(200, mockTodos); + ); - // Call the API - const result = await base44.entities.Todo.filter(filterQuery); + const result = await base44.entities.Todo.filter({ completed: true }); - // Verify the response expect(result).toHaveLength(1); expect(result[0].completed).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("get() should fetch a single entity", async () => { const todoId = "123"; - const mockTodo: Todo = { - id: todoId, - title: "Get milk", - completed: false, - }; + const mockTodo: Todo = { id: todoId, title: "Get milk", completed: false }; - // Mock the API response - scope.get(`/api/apps/${appId}/entities/Todo/${todoId}`).reply(200, mockTodo); + server.use( + http.get(`${baseUrl}/Todo/${todoId}`, () => HttpResponse.json(mockTodo)) + ); - // Call the API const todo = await base44.entities.Todo.get(todoId); - // Verify the response expect(todo.id).toBe(todoId); expect(todo.title).toBe("Get milk"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("create() should send correct data", async () => { - const newTodo: Partial = { - title: "New task", - completed: false, - }; - const createdTodo: Todo = { - id: "123", - title: "New task", - completed: false, - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/entities/Todo`, newTodo as nock.RequestBodyMatcher) - .reply(201, createdTodo); - - // Call the API + const newTodo: Partial = { title: "New task", completed: false }; + const createdTodo: Todo = { id: "123", title: "New task", completed: false }; + + server.use( + http.post(`${baseUrl}/Todo`, () => HttpResponse.json(createdTodo, { status: 201 })) + ); + const todo = await base44.entities.Todo.create(newTodo); - // Verify the response expect(todo.id).toBe("123"); expect(todo.title).toBe("New task"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("update() should send correct data", async () => { const todoId = "123"; - const updates: Partial = { - title: "Updated task", - completed: true, - }; - const updatedTodo: Todo = { - id: todoId, - title: "Updated task", - completed: true, - }; - - // Mock the API response - scope - .put( - `/api/apps/${appId}/entities/Todo/${todoId}`, - updates as nock.RequestBodyMatcher - ) - .reply(200, updatedTodo); - - // Call the API - const todo = await base44.entities.Todo.update(todoId, updates); - - // Verify the response + const updatedTodo: Todo = { id: todoId, title: "Updated task", completed: true }; + + server.use( + http.put(`${baseUrl}/Todo/${todoId}`, () => HttpResponse.json(updatedTodo)) + ); + + const todo = await base44.entities.Todo.update(todoId, { title: "Updated task", completed: true }); + expect(todo.id).toBe(todoId); expect(todo.title).toBe("Updated task"); expect(todo.completed).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("delete() should call correct endpoint and return DeleteResult", async () => { const todoId = "123"; const deleteResult: DeleteResult = { success: true }; - // Mock the API response - scope - .delete(`/api/apps/${appId}/entities/Todo/${todoId}`) - .reply(200, deleteResult); + server.use( + http.delete(`${baseUrl}/Todo/${todoId}`, () => HttpResponse.json(deleteResult)) + ); - // Call the API const result = await base44.entities.Todo.delete(todoId); - // Verify the response matches DeleteResult type expect(result.success).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("updateMany() should send query and data to correct endpoint", async () => { - const mockResult: UpdateManyResult = { - success: true, - updated: 3, - has_more: false, - }; - - // Mock the API response - scope - .patch(`/api/apps/${appId}/entities/Todo/update-many`, { - query: { completed: false }, - data: { $set: { completed: true } }, + const mockResult: UpdateManyResult = { success: true, updated: 3, has_more: false }; + + server.use( + http.patch(`${baseUrl}/Todo/update-many`, async ({ request }) => { + const body = await request.json() as Record; + expect(body.query).toEqual({ completed: false }); + expect(body.data).toEqual({ $set: { completed: true } }); + return HttpResponse.json(mockResult); }) - .reply(200, mockResult); + ); - // Call the API const result = await base44.entities.Todo.updateMany( { completed: false }, { $set: { completed: true } } ); - // Verify the response expect(result.success).toBe(true); expect(result.updated).toBe(3); expect(result.has_more).toBe(false); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("updateMany() should handle has_more response", async () => { - const mockResult: UpdateManyResult = { - success: true, - updated: 500, - has_more: true, - }; - - // Mock the API response - scope - .patch(`/api/apps/${appId}/entities/Todo/update-many`, { - query: {}, - data: { $inc: { view_count: 1 } }, - }) - .reply(200, mockResult); + const mockResult: UpdateManyResult = { success: true, updated: 500, has_more: true }; - // Call the API - const result = await base44.entities.Todo.updateMany( - {}, - { $inc: { view_count: 1 } } + server.use( + http.patch(`${baseUrl}/Todo/update-many`, () => HttpResponse.json(mockResult)) ); - // Verify the response + const result = await base44.entities.Todo.updateMany({}, { $inc: { view_count: 1 } }); + expect(result.success).toBe(true); expect(result.updated).toBe(500); expect(result.has_more).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("bulkUpdate() should send array of updates to correct endpoint", async () => { - const updatePayload = [ - { id: "1", title: "Updated Task 1", completed: true }, - { id: "2", title: "Updated Task 2" }, - ]; const mockResponse: Todo[] = [ { id: "1", title: "Updated Task 1", completed: true }, { id: "2", title: "Updated Task 2", completed: false }, ]; - // Mock the API response - scope - .put( - `/api/apps/${appId}/entities/Todo/bulk`, - updatePayload as nock.RequestBodyMatcher - ) - .reply(200, mockResponse); + server.use( + http.put(`${baseUrl}/Todo/bulk`, () => HttpResponse.json(mockResponse)) + ); - // Call the API - const result = await base44.entities.Todo.bulkUpdate(updatePayload); + const result = await base44.entities.Todo.bulkUpdate([ + { id: "1", title: "Updated Task 1", completed: true }, + { id: "2", title: "Updated Task 2" }, + ]); - // Verify the response expect(result).toHaveLength(2); expect(result[0].id).toBe("1"); expect(result[0].title).toBe("Updated Task 1"); expect(result[0].completed).toBe(true); expect(result[1].id).toBe("2"); expect(result[1].title).toBe("Updated Task 2"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - }); diff --git a/tests/unit/functions.test.ts b/tests/unit/functions.test.ts index 9a55379..67ad9de 100644 --- a/tests/unit/functions.test.ts +++ b/tests/unit/functions.test.ts @@ -1,5 +1,6 @@ -import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; -import nock from "nock"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { http, HttpResponse } from "msw"; +import { server } from "../mocks/server"; import { createClient } from "../../src/index.ts"; // Module augmentation: register function names in FunctionNameRegistry @@ -13,519 +14,317 @@ declare module "../../src/modules/functions.types.ts" { describe("Functions Module", () => { let base44: ReturnType; - let scope; - let fetchMock: ReturnType; const appId = "test-app-id"; const serverUrl = "https://api.base44.com"; + const functionsBase = `${serverUrl}/api/apps/${appId}/functions`; beforeEach(() => { - // Create a new client for each test - base44 = createClient({ - serverUrl, - appId, - }); - - // Create a nock scope for mocking API calls - scope = nock(serverUrl); - - // Enable request debugging for Nock - nock.disableNetConnect(); - nock.emitter.on("no match", (req) => { - console.log(`Nock: No match for ${req.method} ${req.path}`); - console.log("Headers:", req.getHeaders()); - }); - - fetchMock = vi.fn(); - vi.stubGlobal("fetch", fetchMock); + base44 = createClient({ serverUrl, appId }); }); afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); - nock.emitter.removeAllListeners("no match"); - nock.enableNetConnect(); - vi.unstubAllGlobals(); - vi.clearAllMocks(); + base44.cleanup(); }); test("should call a function with JSON data", async () => { - const functionName = "sendNotification"; - const functionData = { + server.use( + http.post(`${functionsBase}/sendNotification`, () => + HttpResponse.json({ success: true, messageId: "msg-456" }) + ) + ); + + const result = await base44.functions.invoke("sendNotification", { userId: "123", message: "Hello World", priority: "high", - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(200, { - success: true, - messageId: "msg-456", - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); + }); - // Verify the response expect(result.data.success).toBe(true); expect(result.data.messageId).toBe("msg-456"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should handle function with empty object parameters", async () => { - const functionName = "getStatus"; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, {}) - .matchHeader("Content-Type", "application/json") - .reply(200, { - status: "healthy", - timestamp: "2024-01-01T00:00:00Z", - }); + server.use( + http.post(`${functionsBase}/getStatus`, () => + HttpResponse.json({ status: "healthy", timestamp: "2024-01-01T00:00:00Z" }) + ) + ); - // Call the function - const result = await base44.functions.invoke(functionName, {}); + const result = await base44.functions.invoke("getStatus", {}); - // Verify the response expect(result.data.status).toBe("healthy"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should handle function with complex nested objects", async () => { - const functionName = "processData"; - const functionData = { + server.use( + http.post(`${functionsBase}/processData`, () => + HttpResponse.json({ processed: true, userId: "123" }) + ) + ); + + const result = await base44.functions.invoke("processData", { user: { id: "123", - profile: { - name: "John Doe", - preferences: { - theme: "dark", - notifications: true, - }, - }, - }, - settings: { - timeout: 5000, - retries: 3, + profile: { name: "John Doe", preferences: { theme: "dark", notifications: true } }, }, - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(200, { - processed: true, - userId: "123", - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); + settings: { timeout: 5000, retries: 3 }, + }); - // Verify the response expect(result.data.processed).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should handle file uploads with FormData", async () => { - const functionName = "uploadFile"; + server.use( + http.post(`${functionsBase}/uploadFile`, () => + HttpResponse.json({ fileId: "file-789", filename: "test.txt", size: 12 }) + ) + ); + const file = new File(["test content"], "test.txt", { type: "text/plain" }); - const functionData = { - file: file, + const result = await base44.functions.invoke("uploadFile", { + file, description: "Test file upload 2", category: "documents", - }; - - // Mock the API response - // TODO: Add validation to the request body - scope - .post(`/api/apps/${appId}/functions/${functionName}`) - .matchHeader("Content-Type", /^multipart\/form-data/) - .reply(() => { - return [ - 200, - { - fileId: "file-789", - filename: "test.txt", - size: 12, - }, - ]; - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); - - // Verify the response + }); + expect(result.data.fileId).toBe("file-789"); expect(result.data.filename).toBe("test.txt"); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should handle mixed data with files and regular data", async () => { - const functionName = "processDocument"; - const file = new File(["document content"], "document.pdf", { - type: "application/pdf", - }); - const functionData = { - file: file, - metadata: { - title: "Important Document", - author: "Jane Smith", - tags: ["important", "confidential"], - }, + server.use( + http.post(`${functionsBase}/processDocument`, () => + HttpResponse.json({ documentId: "doc-123", processed: true, extractedText: "document content" }) + ) + ); + + const file = new File(["document content"], "document.pdf", { type: "application/pdf" }); + const result = await base44.functions.invoke("processDocument", { + file, + metadata: { title: "Important Document", author: "Jane Smith", tags: ["important", "confidential"] }, priority: "high", - }; - - // Mock the API response - // TODO: Add validation to the request body - scope - .post(`/api/apps/${appId}/functions/${functionName}`) - .matchHeader("Content-Type", /^multipart\/form-data/) - .reply(200, { - documentId: "doc-123", - processed: true, - extractedText: "document content", - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); - - // Verify the response + }); + expect(result.data.documentId).toBe("doc-123"); expect(result.data.processed).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should handle FormData input directly", async () => { - const functionName = "submitForm"; + server.use( + http.post(`${functionsBase}/submitForm`, () => + HttpResponse.json({ formId: "form-456", submitted: true }) + ) + ); + const formData = new FormData(); formData.append("name", "John Doe"); formData.append("email", "john@example.com"); formData.append("message", "Hello there"); - // Mock the API response - // TODO: Add validation to the request body - scope - .post(`/api/apps/${appId}/functions/${functionName}`) - .matchHeader("Content-Type", /^multipart\/form-data/) - .reply(200, { - formId: "form-456", - submitted: true, - }); - - // Call the function - const result = await base44.functions.invoke(functionName, formData); + const result = await base44.functions.invoke("submitForm", formData); - // Verify the response expect(result.data.formId).toBe("form-456"); expect(result.data.submitted).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should throw error for string input instead of object", async () => { - const functionName = "processData"; - - // Call the function with string input (should throw) await expect( // @ts-expect-error - base44.functions.invoke(functionName, "invalid string input") + base44.functions.invoke("processData", "invalid string input") ).rejects.toThrow( - `Function ${functionName} must receive an object with named parameters, received: invalid string input` + `Function processData must receive an object with named parameters, received: invalid string input` ); }); test("should handle function names with special characters", async () => { - const functionName = "process-data_v2"; - const functionData = { - input: "test data", - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(200, { - processed: true, - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); - - // Verify the response - expect(result.data.processed).toBe(true); + server.use( + http.post(`${functionsBase}/process-data_v2`, () => + HttpResponse.json({ processed: true }) + ) + ); + + const result = await base44.functions.invoke("process-data_v2", { input: "test data" }); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + expect(result.data.processed).toBe(true); }); test("should handle API errors gracefully", async () => { - const functionName = "failingFunction"; - const functionData = { - param: "value", - }; - - // Mock the API error response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(500, { - error: "Internal server error", - code: "INTERNAL_ERROR", - }); - - // Call the function and expect it to throw - await expect( - base44.functions.invoke(functionName, functionData) - ).rejects.toThrow(); + server.use( + http.post(`${functionsBase}/failingFunction`, () => + HttpResponse.json({ error: "Internal server error", code: "INTERNAL_ERROR" }, { status: 500 }) + ) + ); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + await expect(base44.functions.invoke("failingFunction", { param: "value" })).rejects.toThrow(); }); test("should handle 404 errors for non-existent functions", async () => { - const functionName = "nonExistentFunction"; - const functionData = { - param: "value", - }; - - // Mock the API 404 response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(404, { - error: "Function not found", - code: "FUNCTION_NOT_FOUND", - }); - - // Call the function and expect it to throw - await expect( - base44.functions.invoke(functionName, functionData) - ).rejects.toThrow(); + server.use( + http.post(`${functionsBase}/nonExistentFunction`, () => + HttpResponse.json({ error: "Function not found", code: "FUNCTION_NOT_FOUND" }, { status: 404 }) + ) + ); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + await expect(base44.functions.invoke("nonExistentFunction", { param: "value" })).rejects.toThrow(); }); test("should handle null and undefined values in data", async () => { - const functionName = "handleNullValues"; - const functionData = { + server.use( + http.post(`${functionsBase}/handleNullValues`, () => + HttpResponse.json({ received: true }) + ) + ); + + const result = await base44.functions.invoke("handleNullValues", { stringValue: "test", nullValue: null, undefinedValue: undefined, emptyString: "", - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(200, { - received: true, - values: functionData, - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); + }); - // Verify the response expect(result.data.received).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should handle array values in data", async () => { - const functionName = "processArray"; - const functionData = { + server.use( + http.post(`${functionsBase}/processArray`, () => + HttpResponse.json({ processed: true, count: 3 }) + ) + ); + + const result = await base44.functions.invoke("processArray", { numbers: [1, 2, 3, 4, 5], strings: ["a", "b", "c"], mixed: [1, "two", { three: 3 }], - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .reply(200, { - processed: true, - count: 3, - }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); + }); - // Verify the response expect(result.data.processed).toBe(true); expect(result.data.count).toBe(3); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should create FormData correctly when files are present", async () => { - const functionName = "uploadFile"; + server.use( + http.post(`${functionsBase}/uploadFile`, () => + HttpResponse.json({ success: true }) + ) + ); + const file = new File(["test content"], "test.txt", { type: "text/plain" }); - const functionData = { - file: file, + const result = await base44.functions.invoke("uploadFile", { + file, description: "Test file upload", category: "documents", - }; - - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`) - .matchHeader("Content-Type", /^multipart\/form-data/) - .reply(200, { success: true }); - - // Call the function - const result = await base44.functions.invoke(functionName, functionData); + }); - // Verify the response expect(result.data.success).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should create FormData correctly when FormData is passed directly", async () => { - const functionName = "submitForm"; + server.use( + http.post(`${functionsBase}/submitForm`, () => + HttpResponse.json({ success: true }) + ) + ); + const formData = new FormData(); formData.append("name", "John Doe"); formData.append("email", "john@example.com"); - // Mock the API response - scope - .post(`/api/apps/${appId}/functions/${functionName}`) - .matchHeader("Content-Type", /^multipart\/form-data/) - .reply(200, { success: true }); - - // Call the function - const result = await base44.functions.invoke(functionName, formData); + const result = await base44.functions.invoke("submitForm", formData); - // Verify the response expect(result.data.success).toBe(true); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); test("should send user token as Authorization header when invoking functions", async () => { - const functionName = "testAuth"; const userToken = "user-test-token"; - const functionData = { - test: "data", - }; - - // Create client with user token - const authenticatedBase44 = createClient({ - serverUrl, - appId, - token: userToken, - }); - - // Mock the API response, verifying the Authorization header - scope - .post(`/api/apps/${appId}/functions/${functionName}`, functionData) - .matchHeader("Content-Type", "application/json") - .matchHeader("Authorization", `Bearer ${userToken}`) - .reply(200, { - success: true, - authenticated: true, - }); + const authenticatedBase44 = createClient({ serverUrl, appId, token: userToken }); + + let capturedAuth: string | null = null; + server.use( + http.post(`${functionsBase}/testAuth`, ({ request }) => { + capturedAuth = request.headers.get("Authorization"); + return HttpResponse.json({ success: true, authenticated: true }); + }) + ); - // Call the function - const result = await authenticatedBase44.functions.invoke(functionName, functionData); + const result = await authenticatedBase44.functions.invoke("testAuth", { test: "data" }); - // Verify the response expect(result.data.success).toBe(true); expect(result.data.authenticated).toBe(true); + expect(capturedAuth).toBe(`Bearer ${userToken}`); - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + authenticatedBase44.cleanup(); }); test("should fetch function endpoint directly", async () => { - fetchMock.mockResolvedValueOnce(new Response("ok", { status: 200 })); + let capturedUrl: string | null = null; + server.use( + http.get(`${serverUrl}/api/functions/my_function`, ({ request }) => { + capturedUrl = request.url; + return new HttpResponse("ok", { status: 200 }); + }) + ); - await base44.functions.fetch("/my_function", { - method: "GET", - }); + await base44.functions.fetch("/my_function", { method: "GET" }); - expect(fetchMock).toHaveBeenCalledTimes(1); - expect(fetchMock).toHaveBeenCalledWith( - `${serverUrl}/api/functions/my_function`, - expect.any(Object) - ); + expect(capturedUrl).toBe(`${serverUrl}/api/functions/my_function`); }); - test("should include Authorization header when using functions.fetch", async () => { const userToken = "user-streaming-token"; - const authenticatedBase44 = createClient({ - serverUrl, - appId, - token: userToken, - }); - fetchMock.mockResolvedValueOnce(new Response("ok", { status: 200 })); + const authenticatedBase44 = createClient({ serverUrl, appId, token: userToken }); + + let capturedAuth: string | null = null; + server.use( + http.post(`${serverUrl}/api/functions/streaming_demo`, ({ request }) => { + capturedAuth = request.headers.get("Authorization"); + return new HttpResponse("ok", { status: 200 }); + }) + ); await authenticatedBase44.functions.fetch("streaming_demo", { method: "POST", body: JSON.stringify({ mode: "text" }), }); - const requestInit = fetchMock.mock.calls[0][1]; - const headers = new Headers(requestInit.headers); - expect(headers.get("Authorization")).toBe(`Bearer ${userToken}`); + expect(capturedAuth).toBe(`Bearer ${userToken}`); + + authenticatedBase44.cleanup(); }); test("should normalize path with and without leading slash", async () => { - // Test with leading slash - fetchMock.mockResolvedValueOnce(new Response("ok", { status: 200 })); - await base44.functions.fetch("/my_function"); - expect(fetchMock).toHaveBeenCalledWith( - `${serverUrl}/api/functions/my_function`, - expect.any(Object) + const calledUrls: string[] = []; + server.use( + http.get(`${serverUrl}/api/functions/my_function`, ({ request }) => { + calledUrls.push(request.url); + return new HttpResponse("ok", { status: 200 }); + }) ); - // Test without leading slash - fetchMock.mockResolvedValueOnce(new Response("ok", { status: 200 })); + await base44.functions.fetch("/my_function"); await base44.functions.fetch("my_function"); - expect(fetchMock).toHaveBeenCalledWith( - `${serverUrl}/api/functions/my_function`, - expect.any(Object) - ); + + expect(calledUrls).toHaveLength(2); + expect(calledUrls[0]).toBe(`${serverUrl}/api/functions/my_function`); + expect(calledUrls[1]).toBe(`${serverUrl}/api/functions/my_function`); }); test("should include service role Authorization header when using asServiceRole.functions.fetch", async () => { const serviceToken = "service-role-token"; - const serviceRoleBase44 = createClient({ - serverUrl, - appId, - serviceToken, - }); - fetchMock.mockResolvedValueOnce(new Response("ok", { status: 200 })); + const serviceRoleBase44 = createClient({ serverUrl, appId, serviceToken }); + + let capturedAuth: string | null = null; + server.use( + http.get(`${serverUrl}/api/functions/service_function`, ({ request }) => { + capturedAuth = request.headers.get("Authorization"); + return new HttpResponse("ok", { status: 200 }); + }) + ); - await serviceRoleBase44.asServiceRole.functions.fetch("/service_function", { - method: "GET", - }); + await serviceRoleBase44.asServiceRole.functions.fetch("/service_function", { method: "GET" }); + + expect(capturedAuth).toBe(`Bearer ${serviceToken}`); - const requestInit = fetchMock.mock.calls[0][1]; - const headers = new Headers(requestInit.headers); - expect(headers.get("Authorization")).toBe(`Bearer ${serviceToken}`); + serviceRoleBase44.cleanup(); }); }); diff --git a/tests/unit/integrations.test.js b/tests/unit/integrations.test.js index 159c47e..fc979eb 100644 --- a/tests/unit/integrations.test.js +++ b/tests/unit/integrations.test.js @@ -1,122 +1,93 @@ import { describe, test, expect, beforeEach, afterEach } from 'vitest'; -import nock from 'nock'; +import { http, HttpResponse } from 'msw'; +import { server } from '../mocks/server'; import { createClient } from '../../src/index.ts'; describe('Integrations Module', () => { let base44; - let scope; const appId = 'test-app-id'; const serverUrl = 'https://base44.app'; - + const intBase = `${serverUrl}/api/apps/${appId}/integration-endpoints`; + beforeEach(() => { - // Create a new client for each test - base44 = createClient({ - serverUrl, - appId, - }); - - // Create a nock scope for mocking API calls - scope = nock(serverUrl); + base44 = createClient({ serverUrl, appId }); }); - + afterEach(() => { - // Clean up any pending mocks - nock.cleanAll(); + base44.cleanup(); }); - + test('Core integration should send requests to the correct endpoint', async () => { - const emailParams = { + server.use( + http.post(`${intBase}/Core/SendEmail`, () => + HttpResponse.json({ success: true, messageId: '123456' }) + ) + ); + + const result = await base44.integrations.Core.SendEmail({ to: 'test@example.com', subject: 'Test Email', body: 'This is a test email' - }; - - // Mock the API response - scope.post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`, emailParams) - .reply(200, { success: true, messageId: '123456' }); - - // Call the API - const result = await base44.integrations.Core.SendEmail(emailParams); - - // Verify the response + }); + expect(result.success).toBe(true); expect(result.messageId).toBe('123456'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('Custom package integration should send requests to the correct endpoint', async () => { - const customParams = { + server.use( + http.post(`${intBase}/installable/CustomPackage/integration-endpoints/CustomEndpoint`, () => + HttpResponse.json({ success: true, result: 'custom result' }) + ) + ); + + const result = await base44.integrations.CustomPackage.CustomEndpoint({ param1: 'value1', param2: 'value2' - }; - - // Mock the API response - scope.post(`/api/apps/${appId}/integration-endpoints/installable/CustomPackage/integration-endpoints/CustomEndpoint`, customParams) - .reply(200, { success: true, result: 'custom result' }); - - // Call the API - const result = await base44.integrations.CustomPackage.CustomEndpoint(customParams); - - // Verify the response + }); + expect(result.success).toBe(true); expect(result.result).toBe('custom result'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('Integration should handle file uploads correctly', async () => { - // Mock a file + server.use( + http.post(`${intBase}/Core/UploadFile`, () => + HttpResponse.json({ success: true, fileId: 'file123' }) + ) + ); + const mockFile = new Blob(['file content'], { type: 'text/plain' }); mockFile.name = 'test.txt'; - - const uploadParams = { + + const result = await base44.integrations.Core.UploadFile({ file: mockFile, metadata: { type: 'document' } - }; - - // Mock the API response - note that we can't easily check FormData contents with nock - // so we just make sure the endpoint is called - scope.post(`/api/apps/${appId}/integration-endpoints/Core/UploadFile`) - .reply(200, { success: true, fileId: 'file123' }); - - // Call the API - const result = await base44.integrations.Core.UploadFile(uploadParams); - - // Verify the response + }); + expect(result.success).toBe(true); expect(result.fileId).toBe('file123'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); - + test('Integration should throw error with string parameters', async () => { - // Expect error when trying to call with a string instead of object await expect(async () => { await base44.integrations.Core.SendEmail('invalid string parameter'); }).rejects.toThrow('Integration SendEmail must receive an object with named parameters'); }); - + test('Integration should handle API errors correctly', async () => { - const params = { invalid: 'params' }; - - // Mock an API error response - scope.post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`, params) - .reply(400, { detail: 'Invalid parameters', code: 'INVALID_PARAMS' }); - - // Call the API and expect an error - await expect(base44.integrations.Core.SendEmail(params)) + server.use( + http.post(`${intBase}/Core/SendEmail`, () => + HttpResponse.json({ detail: 'Invalid parameters', code: 'INVALID_PARAMS' }, { status: 400 }) + ) + ); + + await expect(base44.integrations.Core.SendEmail({ invalid: 'params' })) .rejects.toMatchObject({ status: 400, name: 'Base44Error', message: 'Invalid parameters', code: 'INVALID_PARAMS' }); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); }); -}); \ No newline at end of file +});