diff --git a/.gitignore b/.gitignore index ffc4acb3e9..ff5518925b 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ uri.txt crypt_shared.sh *keytab +driver.bundle.js +test/tools/runner/bundle/ diff --git a/etc/build-runtime-barrel.mjs b/etc/build-runtime-barrel.mjs new file mode 100644 index 0000000000..afb34360a9 --- /dev/null +++ b/etc/build-runtime-barrel.mjs @@ -0,0 +1,21 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// eslint-disable-next-line no-restricted-globals +const useBundled = process.env.MONGODB_BUNDLED === 'true'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.join(__dirname, '..'); +const outputBarrelFile = path.join(rootDir, 'test/mongodb_runtime-testing.ts'); +const source = useBundled ? './mongodb_bundled' : './mongodb'; + +const contents = + `// This file is auto-generated. Do not edit.\n` + + `// Run 'npm run build:runtime-barrel' to regenerate.\n` + + `export const runNodelessTests = ${useBundled};\n` + + `export * from '${source}';\n`; +await fs.writeFile(outputBarrelFile, contents); + +// eslint-disable-next-line no-console +console.log(`✓ ${outputBarrelFile} now re-exports from ${source}`); diff --git a/etc/bundle-driver.mjs b/etc/bundle-driver.mjs new file mode 100755 index 0000000000..f007d00f06 --- /dev/null +++ b/etc/bundle-driver.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import { isBuiltin } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import * as esbuild from 'esbuild'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.join(__dirname, '..'); + +const outdir = path.join(rootDir, 'test/tools/runner/bundle/'); +await fs.rm(outdir, { recursive: true, force: true }); + +const outputBundleFile = path.join(outdir, 'driver-bundle.js'); +await esbuild.build({ + entryPoints: [path.join(rootDir, 'test/mongodb.ts')], + bundle: true, + outfile: outputBundleFile, + platform: 'node', + format: 'cjs', + target: 'node20', + external: [ + 'bson', + 'mongodb-connection-string-url', + '@mongodb-js/saslprep', + '@mongodb-js/zstd', + 'mongodb-client-encryption', + 'snappy', + '@napi-rs/snappy*', + 'kerberos', + 'gcp-metadata', + '@aws-sdk/credential-providers' + ], + plugins: [ + { + name: 'externalize-node-builtins', + setup(build) { + build.onResolve({ filter: /.*/ }, args => { + if (isBuiltin(args.path)) { + return { path: args.path, external: true }; + } + }); + } + } + ], + sourcemap: 'inline', + logLevel: 'info' +}); + +// eslint-disable-next-line no-console +console.log(`✓ Driver bundle created at ${outputBundleFile}`); diff --git a/package-lock.json b/package-lock.json index b1def2b859..79889203ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "chai": "^4.4.1", "chai-subset": "^1.6.0", "chalk": "^4.1.2", + "esbuild": "^0.27.2", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-mocha": "^10.4.1", @@ -1142,6 +1143,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -4664,6 +5107,48 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index 1f97a7595b..8fac3b1665 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "chai": "^4.4.1", "chai-subset": "^1.6.0", "chalk": "^4.1.2", + "esbuild": "^0.27.2", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-mocha": "^10.4.1", @@ -141,6 +142,7 @@ "check:search-indexes": "nyc mocha --config test/mocha_mongodb.js test/manual/search-index-management.prose.test.ts", "check:test": "mocha --config test/mocha_mongodb.js test/integration", "check:unit": "nyc mocha test/unit", + "check:unit-bundled": "npm run build:bundle && npm run switch:to-bundled && npm run check:unit ; npm run switch:to-unbundled", "check:ts": "node ./node_modules/typescript/bin/tsc -v && node ./node_modules/typescript/bin/tsc --noEmit", "check:atlas": "nyc mocha --config test/manual/mocharc.js test/manual/atlas_connectivity.test.ts", "check:drivers-atlas-testing": "nyc mocha --config test/mocha_mongodb.js test/atlas/drivers_atlas_testing.test.ts", @@ -154,9 +156,17 @@ "check:csfle": "nyc mocha --config test/mocha_mongodb.js test/integration/client-side-encryption", "check:snappy": "nyc mocha test/unit/assorted/snappy.test.js", "check:x509": "nyc mocha test/manual/x509_auth.test.ts", + "check:runtime-independence": "ts-node test/tools/runner/vm_context_helper.ts test/integration/change-streams/change_stream.test.ts", + "check:test-bundled": "npm run build:bundle && npm run switch:to-bundled && npm run check:test ; npm run switch:to-unbundled", + "build:bundle": "npm run bundle:driver && npm run bundle:types", + "build:runtime-barrel": "node etc/build-runtime-barrel.mjs", + "bundle:driver": "node etc/bundle-driver.mjs", + "bundle:types": "npx tsc --project ./tsconfig.json --declaration --emitDeclarationOnly --declarationDir test/tools/runner/bundle/types", "fix:eslint": "npm run check:eslint -- --fix", "prepare": "node etc/prepare.js", "preview:docs": "ts-node etc/docs/preview.ts", + "switch:to-bundled": "MONGODB_BUNDLED=true npm run build:runtime-barrel", + "switch:to-unbundled": "MONGODB_BUNDLED=false npm run build:runtime-barrel", "test": "npm run check:lint && npm run test:all", "test:all": "npm run check:unit && npm run check:test", "update:docs": "npm run build:docs -- --yes" diff --git a/test/integration/crud/crud_api.test.ts b/test/integration/crud/crud_api.test.ts index 6ec29becc4..fced0597ac 100644 --- a/test/integration/crud/crud_api.test.ts +++ b/test/integration/crud/crud_api.test.ts @@ -13,14 +13,15 @@ import { type MongoClient, MongoServerError, ObjectId, - ReturnDocument -} from '../../mongodb'; -import { type FailCommandFailPoint } from '../../tools/utils'; + ReturnDocument, + runNodelessTests +} from '../../mongodb_runtime-testing'; +import { ensureTypeByName, type FailCommandFailPoint } from '../../tools/utils'; import { assert as test } from '../shared'; const DB_NAME = 'crud_api_tests'; -describe('CRUD API', function () { +describe.only('CRUD API', function () { let client: MongoClient; beforeEach(async function () { @@ -103,9 +104,13 @@ describe('CRUD API', function () { const spy = sinon.spy(Collection.prototype, 'find'); const result = await collection.findOne({}); expect(result).to.deep.equal({ _id: 1 }); - expect(events.at(0)).to.be.instanceOf(CommandSucceededEvent); - expect(spy.returnValues.at(0)).to.have.property('closed', true); - expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + if (runNodelessTests) { + expect(ensureTypeByName(events.at(0), 'CommandSucceededEvent')).to.be.true; + } else { + expect(events.at(0)).to.be.instanceOf(CommandSucceededEvent); + expect(spy.returnValues.at(0)).to.have.property('closed', true); + expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + } }); }); @@ -135,10 +140,14 @@ describe('CRUD API', function () { it('the cursor for findOne is closed', async function () { const spy = sinon.spy(Collection.prototype, 'find'); const error = await collection.findOne({}).catch(error => error); - expect(error).to.be.instanceOf(MongoServerError); - expect(events.at(0)).to.be.instanceOf(CommandFailedEvent); - expect(spy.returnValues.at(0)).to.have.property('closed', true); - expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + if (runNodelessTests) { + expect(ensureTypeByName(error, 'MongoServerError')).to.be.true; + } else { + expect(error).to.be.instanceOf(MongoServerError); + expect(events.at(0)).to.be.instanceOf(CommandFailedEvent); + expect(spy.returnValues.at(0)).to.have.property('closed', true); + expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + } }); }); }); @@ -173,9 +182,13 @@ describe('CRUD API', function () { const spy = sinon.spy(Collection.prototype, 'aggregate'); const result = await collection.countDocuments({}); expect(result).to.deep.equal(2); - expect(events[0]).to.be.instanceOf(CommandSucceededEvent); - expect(spy.returnValues[0]).to.have.property('closed', true); - expect(spy.returnValues[0]).to.have.nested.property('session.hasEnded', true); + if (runNodelessTests) { + expect(ensureTypeByName(events[0], 'CommandSucceededEvent')).to.be.true; + } else { + expect(events[0]).to.be.instanceOf(CommandSucceededEvent); + expect(spy.returnValues[0]).to.have.property('closed', true); + expect(spy.returnValues[0]).to.have.nested.property('session.hasEnded', true); + } }); }); @@ -205,10 +218,14 @@ describe('CRUD API', function () { it('the cursor for countDocuments is closed', async function () { const spy = sinon.spy(Collection.prototype, 'aggregate'); const error = await collection.countDocuments({}).catch(error => error); - expect(error).to.be.instanceOf(MongoServerError); - expect(events.at(0)).to.be.instanceOf(CommandFailedEvent); - expect(spy.returnValues.at(0)).to.have.property('closed', true); - expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + if (runNodelessTests) { + expect(ensureTypeByName(error, 'MongoServerError')).to.be.true; + } else { + expect(error).to.be.instanceOf(MongoServerError); + expect(events.at(0)).to.be.instanceOf(CommandFailedEvent); + expect(spy.returnValues.at(0)).to.have.property('closed', true); + expect(spy.returnValues.at(0)).to.have.nested.property('session.hasEnded', true); + } }); }); }); @@ -785,7 +802,11 @@ describe('CRUD API', function () { .bulkWrite(ops, { ordered: false, writeConcern: { w: 1 } }) .catch(error => error); - expect(error).to.be.instanceOf(MongoBulkWriteError); + if (runNodelessTests) { + expect(ensureTypeByName(error, 'MongoBulkWriteError')).to.be.true; + } else { + expect(error).to.be.instanceOf(MongoBulkWriteError); + } // 1004 because one of them is duplicate key // but since it is unordered we continued to write expect(error).to.have.property('insertedCount', 1004); @@ -808,7 +829,8 @@ describe('CRUD API', function () { .collection('t20_1') .bulkWrite(ops, { ordered: true, writeConcern: { w: 1 } }) .catch(err => err); - expect(err).to.be.instanceOf(MongoBulkWriteError); + // expect(err).to.be.instanceOf(MongoBulkWriteError); + expect(ensureTypeByName(err, 'MongoBulkWriteError')).to.be.true; }); describe('sort support', function () { diff --git a/test/mongodb.ts b/test/mongodb.ts index 52b5d1b0f5..9c3234acd7 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -61,8 +61,11 @@ export * from '../src/cursor/change_stream_cursor'; export * from '../src/cursor/client_bulk_write_cursor'; export * from '../src/cursor/explainable_cursor'; export * from '../src/cursor/find_cursor'; +export * from '../src/cursor/find_cursor'; +export * from '../src/cursor/list_collections_cursor'; export * from '../src/cursor/list_collections_cursor'; export * from '../src/cursor/list_indexes_cursor'; +export * from '../src/cursor/list_indexes_cursor'; export * from '../src/cursor/list_search_indexes_cursor'; export * from '../src/cursor/run_command_cursor'; export * from '../src/db'; @@ -76,10 +79,15 @@ export * from '../src/gridfs/upload'; export * from '../src/mongo_client'; export * from '../src/mongo_client_auth_providers'; export * from '../src/mongo_logger'; +export * from '../src/mongo_logger'; +export * from '../src/mongo_types'; export * from '../src/mongo_types'; export * from '../src/operations/aggregate'; +export * from '../src/operations/aggregate'; export * from '../src/operations/client_bulk_write/client_bulk_write'; export * from '../src/operations/client_bulk_write/command_builder'; +export * from '../src/operations/client_bulk_write/command_builder'; +export * from '../src/operations/client_bulk_write/common'; export * from '../src/operations/client_bulk_write/common'; export * from '../src/operations/client_bulk_write/executor'; export * from '../src/operations/client_bulk_write/results_merger'; @@ -131,3 +139,6 @@ export * from '../src/timeout'; export * from '../src/transactions'; export * from '../src/utils'; export * from '../src/write_concern'; + +// Must be last for precedence +export * from '../src/index'; diff --git a/test/mongodb_bundled.ts b/test/mongodb_bundled.ts new file mode 100644 index 0000000000..5d8c0f260c --- /dev/null +++ b/test/mongodb_bundled.ts @@ -0,0 +1,80 @@ +import { loadContextifiedMongoDBModule } from './tools/runner/vm_context_helper'; + +type all = typeof import('./mongodb'); +let exportSource: all; +try { + exportSource = loadContextifiedMongoDBModule() as all; +} catch (error) { + throw new Error( + `Failed to load contextified MongoDB module: ${error instanceof Error ? error.message : String(error)}` + ); +} + +// Export public API from the contextified module +export const { + aws4Sign, + Collection, + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + CSOTTimeoutContext, + Db, + Double, + HostAddress, + isHello, + LegacyTimeoutContext, + Long, + MongoAPIError, + MongoBulkWriteError, + MongoClient, + MongoCredentials, + MongoInvalidArgumentError, + MongoLoggableComponent, + MongoLogger, + MongoParseError, + MongoServerError, + MongoRuntimeError, + ObjectId, + parseOptions, + ReadConcern, + ReadPreference, + resolveSRVRecord, + ReturnDocument, + ServerApiVersion, + SeverityLevel, + Timeout, + TimeoutContext, + TimeoutError, + TopologyType, + WriteConcern +} = exportSource; + +// Export types from the contextified module +export type { + AuthMechanism, + CompressorName, + MongoClientOptions, + ServerApi, + WriteConcernSettings +} from './tools/runner/bundle/types/index'; + +// Export "clashing" types from the contextified module. +// These are types that clash with the objects of the same name (eg Collection), so we need to export them separately to avoid type errors. +import type { + Collection as _CollectionType, + CommandFailedEvent as _CommandFailedEventType, + CommandStartedEvent as _CommandStartedEventType, + CommandSucceededEvent as _CommandSucceededEventType, + HostAddress as _HostAddressType, + MongoClient as _MongoClientType, + Timeout as _TimeoutType, + TopologyType as _TopologyType +} from './tools/runner/bundle/types/index'; +export type Collection = _CollectionType; +export type CommandFailedEvent = _CommandFailedEventType; +export type CommandStartedEvent = _CommandStartedEventType; +export type CommandSucceededEvent = _CommandSucceededEventType; +export type HostAddress = _HostAddressType; +export type MongoClient = _MongoClientType; +export type Timeout = _TimeoutType; +export type TopologyType = _TopologyType; diff --git a/test/mongodb_runtime-testing.ts b/test/mongodb_runtime-testing.ts new file mode 100644 index 0000000000..d0b07fef47 --- /dev/null +++ b/test/mongodb_runtime-testing.ts @@ -0,0 +1,4 @@ +// This file is auto-generated. Do not edit. +// Run 'npm run build:runtime-barrel' to regenerate. +export const runNodelessTests = false; +export * from './mongodb'; diff --git a/test/readme.md b/test/readme.md index 9baec61585..6085f4029a 100644 --- a/test/readme.md +++ b/test/readme.md @@ -43,9 +43,15 @@ about the types of tests and how to run them. - [AWS Authentication tests](#aws-authentication-tests) - [Running AWS tests](#running-aws-tests) - [Container Tests](#container-tests) + - [Node-less Runtime Testing](#node-less-runtime-testing) + - [Design](#design) + - [Adding tests to be tested with Node-less Runtime](#adding-tests-to-be-tested-with-node-less-runtime) + - [Running tests in Node-less Runtime](#running-tests-in-node-less-runtime) + - [Running tests manually in Node-less Runtime](#running-tests-manually-in-node-less-runtime) - [GCP](#gcp) - [Azure](#azure) - [AWS](#aws) + - [Running tests with TLS](#running-tests-with-tls) - [TODO Special Env Sections](#todo-special-env-sections) - [Testing driver changes with mongosh](#testing-driver-changes-with-mongosh) - [Point mongosh to the driver](#point-mongosh-to-the-driver) @@ -715,6 +721,64 @@ our existing integration test suite and run Evergreen patches against a single i _Note that in cases where the tests need to run longer than one hour to ensure that tokens expire that the mocha timeout must be increased in order for the test not to timeout._ +### Node-less Runtime Testing + +We are starting to remove explicit Node requirements, and are making it possible for users to provide us with the required functionality. + +To that end, need to run our tests in a Node-less environment. These tests need to fail if our code erroneously uses Node. + +#### Design + +The approach we are taking is to modify our unit and integration tests to run against a patched version of the driver, where any illegal `require` calls are blocked. + +Here are a few of the relevant components of this system: + +1. [test/mongodb.ts](test/mongodb.ts) + - Test entrypoint that exports Driver and all internal types. +2. [etc/bundle-driver.mjs](etc/bundle-driver.mjs) + - Creates a CommonJS bundle (Driver and all internal types) from `test/mongodb.ts`. +3. [test/tools/runner/vm_context_helper.ts](./tools/runner/vm_context_helper.ts) + - Special VM that blocks specific `require` calls. +4. [test/mongodb_bundled.ts](./mongodb_bundled.ts) + - Exports MongoDB from CommonJS bundle created by `etc/bundle-driver.mjs`, using `vm_context_helper.ts` to detect usages of blocked `require` calls. + - This file is currently maintained by hand and needs to export types explicitly. We may want to generate this file as well. +5. [test/mongodb_runtime-testing.ts](./mongodb_runtime-testing.ts) + - Generated "barrel file". It exports either `test/mongodb.ts` (Driver + all internal types) or `test/mongodb_bundled.ts` (Driver + all internal types, loaded from bundle, and using `vm_context_helper.ts` to block `require` calls.) +6. [etc/build-runtime-barrel.mjs](./etc/build-runtime-barrel.mjs) + - Generates the barrel file `test/mongodb_runtime-testing.ts` based on `MONGODB_BUNDLED` env var. + +#### Adding tests to be tested with Node-less Runtime + +Change the test's import from + `} from '../../mongodb';` +to + `} from '../../mongodb_runtime-testing';` + +#### Running tests in Node-less Runtime + +To run opted-in unit or integration tests in a Node-less runtime, run: + + `npm run check:unit-bundled` +or + `npm run check:test-bundled` + +Either command will: + +1. Create a bundle from `test/mongodb.ts` +2. Regenerate the barrel file to export from the generated bundle +3. Run unit or integ tests +4. Regenerate the barrel file to export from the test entry point + +#### Running tests manually in Node-less Runtime + +Call the following command to regenerate the barrel file to import from the bundle: + `npm run switch:to-bundled` + +Call the following command to rebuild the bundle: + `npm run build:bundle` + +You can now run the unit or integ tests locally and they will be importing from the patched bundle. + ## GCP 1. Add a new GCP prose test to `test/integration/auth/mongodb_oidc_gcp.prose.06.test.ts` that mimics the behaviour that @@ -748,7 +812,7 @@ We tests TLS in two suites in CI: Setting up a local environment is the same for both suites. -First, configure a server with TLS enabled by following the steps in [Runing the Tests Locally](#Running-the-Tests-Locally) with the environment +First, configure a server with TLS enabled by following the steps in [Runing the Tests Locally](#Running-the-Tests-Locally) with the environment variable `SSL=SSL` set. Then, in addition to setting the `MONGODB_URI` environment varialbe, set the following environment variables: diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index a16661f5d6..265be7a926 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -17,10 +17,11 @@ import { MongoClient, type MongoClientOptions, ObjectId, + runNodelessTests, type ServerApi, TopologyType, type WriteConcernSettings -} from '../../mongodb'; +} from '../../mongodb_runtime-testing'; import { getEnvironmentalOptions } from '../utils'; import { type Filter } from './filters/filter'; import { flakyTests } from './flaky'; @@ -220,7 +221,10 @@ export class TestConfiguration { return uri.indexOf('MONGODB-OIDC') > -1 && uri.indexOf(`ENVIRONMENT:${env}`) > -1; } - newClient(urlOrQueryOptions?: string | Record, serverOptions?: MongoClientOptions) { + newClient( + urlOrQueryOptions?: string | Record, + serverOptions?: MongoClientOptions + ): MongoClient { const baseOptions: MongoClientOptions = this.compressor ? { compressors: this.compressor @@ -228,6 +232,14 @@ export class TestConfiguration { : {}; serverOptions = Object.assign(baseOptions, getEnvironmentalOptions(), serverOptions); + // If using contextified mongodb, inject Node.js runtime adapters + if (runNodelessTests) { + serverOptions.runtimeAdapters = { + // eslint-disable-next-line @typescript-eslint/no-require-imports + os: require('os'), + ...serverOptions.runtimeAdapters + }; + } if (this.loggingEnabled && !Object.hasOwn(serverOptions, 'mongodbLogPath')) { serverOptions = this.setupLogging(serverOptions); @@ -239,7 +251,8 @@ export class TestConfiguration { throw new Error(`Cannot use options to specify host/port, must be in ${urlOrQueryOptions}`); } - return new MongoClient(urlOrQueryOptions, serverOptions); + const newClient: MongoClient = new MongoClient(urlOrQueryOptions, serverOptions); + return newClient; } const queryOptions = urlOrQueryOptions ?? {}; diff --git a/test/tools/runner/vm_context_helper.ts b/test/tools/runner/vm_context_helper.ts new file mode 100644 index 0000000000..c19b7cd16c --- /dev/null +++ b/test/tools/runner/vm_context_helper.ts @@ -0,0 +1,133 @@ +/* eslint-disable no-restricted-globals, @typescript-eslint/no-require-imports */ + +import * as fs from 'node:fs'; +import { isBuiltin } from 'node:module'; +import * as path from 'node:path'; +import * as vm from 'node:vm'; + +/** + * Creates a require function that blocks access to specified core modules + */ +function createRestrictedRequire() { + const blockedModules = new Set(['os']); + + return function restrictedRequire(moduleName: string) { + // Block core modules + if (isBuiltin(moduleName) && blockedModules.has(moduleName)) { + const sourceFile = new Error().stack.split('\n')[2]?.replace('at', '').trim(); + const source = sourceFile ? `from ${sourceFile}` : 'from an unknown source'; + throw new Error( + `Access to core module '${moduleName}' (${source}) is restricted in this context` + ); + } + + return require(moduleName); + } as NodeRequire; +} + +// Create a sandbox context with necessary globals +const sandbox = vm.createContext({ + __proto__: null, + + // Console and timing + console: console, + AbortController: AbortController, + AbortSignal: AbortSignal, + Date: global.Date, + Error: global.Error, + URL: global.URL, + URLSearchParams: global.URLSearchParams, + queueMicrotask: queueMicrotask, + performance: global.performance, + setTimeout: global.setTimeout, + clearTimeout: global.clearTimeout, + setInterval: global.setInterval, + clearInterval: global.clearInterval, + setImmediate: global.setImmediate, + clearImmediate: global.clearImmediate, + + // Process + process: process, + + // Global objects needed for runtime + Buffer: Buffer, + Headers: global.Headers, + Promise: Promise, + Map: Map, + Set: Set, + WeakMap: WeakMap, + WeakSet: WeakSet, + ArrayBuffer: ArrayBuffer, + SharedArrayBuffer: SharedArrayBuffer, + Atomics: Atomics, + DataView: DataView, + Int8Array: Int8Array, + Uint8Array: Uint8Array, + Uint8ClampedArray: Uint8ClampedArray, + Int16Array: Int16Array, + Uint16Array: Uint16Array, + Int32Array: Int32Array, + Uint32Array: Uint32Array, + Float32Array: Float32Array, + Float64Array: Float64Array, + BigInt64Array: BigInt64Array, + BigUint64Array: BigUint64Array, + + // Other necessary globals + TextEncoder: global.TextEncoder, + TextDecoder: global.TextDecoder, + BigInt: global.BigInt, + Symbol: Symbol, + Proxy: Proxy, + Reflect: Reflect, + Object: Object, + Array: Array, + Function: Function, + String: String, + Number: Number, + Boolean: Boolean, + RegExp: RegExp, + Math: Math, + JSON: JSON, + Intl: global.Intl, + crypto: global.crypto, + + // Custom require that blocks core modules + require: createRestrictedRequire(), + + // Needed for some modules + global: undefined as any, + globalThis: undefined as any +}); + +// Make global and globalThis point to the sandbox +sandbox.global = sandbox; +sandbox.globalThis = sandbox; + +/** + * Load the bundled MongoDB driver module in a VM context + * This allows us to control the globals that the driver has access to + */ +export function loadContextifiedMongoDBModule() { + const bundlePath = path.join(__dirname, 'bundle/driver-bundle.js'); + + if (!fs.existsSync(bundlePath)) { + throw new Error(`Driver bundle not found at ${bundlePath}. Run 'npm run bundle:driver' first.`); + } + + const bundleCode = fs.readFileSync(bundlePath, 'utf8'); + + const exportsContainer = {}; + const moduleContainer = { exports: exportsContainer }; + + // Wrap the bundle in a CommonJS-style wrapper + const wrapper = `(function(exports, module, require) {${bundleCode}})`; + + const script = new vm.Script(wrapper, { filename: bundlePath }); + const fn = script.runInContext(sandbox); + + // Execute the bundle with the restricted require from the sandbox + fn(moduleContainer.exports, moduleContainer, sandbox.require); + + return moduleContainer.exports; +} diff --git a/test/tools/utils.ts b/test/tools/utils.ts index d587e9864d..cb26f844ad 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -32,6 +32,11 @@ export function ensureCalledWith(stub: any, args: any[]) { args.forEach((m: any) => expect(stub).to.have.been.calledWith(m)); } +export function ensureTypeByName(obj: any, typeName: string) { + const isType = obj != null && obj.constructor != null && obj.constructor.name === typeName; + return isType; +} + export class EventCollector { private _events: Record; private _timeout: number; diff --git a/test/unit/timeout.test.ts b/test/unit/timeout.test.ts index 1dd7e83feb..5fbdf0c7a5 100644 --- a/test/unit/timeout.test.ts +++ b/test/unit/timeout.test.ts @@ -8,7 +8,7 @@ import { Timeout, TimeoutContext, TimeoutError -} from '../mongodb'; +} from '../mongodb_runtime-testing'; describe('Timeout', function () { let timeout: Timeout; diff --git a/tsconfig.json b/tsconfig.json index 74fb09c6ff..6604ef2871 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -43,4 +43,4 @@ "include": [ "src/**/*" ] -} \ No newline at end of file +}