From 7d3a9b2323f05a923e13b8655fb1b58ee24ae0ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:50:45 +0000 Subject: [PATCH 1/7] chore(deps-dev): bump @types/node from 22.17.0 to 24.1.0 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.17.0 to 24.1.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 24.1.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index edc1e08..ac42a30 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@types/chai": "^5.0.1", "@types/mocha": "^10.0.10", - "@types/node": "^22.9.0", + "@types/node": "^24.1.0", "@typescript-eslint/eslint-plugin": "^4.2.0", "@typescript-eslint/parser": "^4.2.0", "chai": "^4.2.0", From ecd7109f86b8d55f7b6d18a06558316bf88bcc94 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 13:12:06 -0600 Subject: [PATCH 2/7] adopt lint with usual plugins --- .eslintrc | 115 ++++++++++++++- package.json | 14 +- src/index.ts | 173 +++++++++++++++------- src/redact.ts | 66 +++++---- test/index.ts | 389 ++++++++++++++++++++++++++----------------------- test/redact.ts | 144 +++++++++++++----- 6 files changed, 585 insertions(+), 316 deletions(-) diff --git a/.eslintrc b/.eslintrc index fb511a4..714eb49 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,13 +1,112 @@ { + "root": true, "extends": [ - "semistandard", - "plugin:promise/recommended", + "eslint:recommended", + "plugin:prettier/recommended", + "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], + "env": { + "node": true, + "mocha": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2019 + }, + "plugins": [ + "@typescript-eslint", + "prettier" + ], + "parser": "@typescript-eslint/parser", "rules": { - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-empty-function": 0, - "no-return-assign": 0, - "space-before-function-paren": ["error", "never"] - } -} + "no-restricted-properties": [ + "error", + { + "object": "describe", + "property": "only" + }, + { + "object": "it", + "property": "only" + }, + { + "object": "context", + "property": "only" + } + ], + "prettier/prettier": "error", + "no-console": "error", + "valid-typeof": "error", + "eqeqeq": [ + "error", + "always", + { + "null": "ignore" + } + ], + "strict": [ + "error", + "global" + ], + "no-restricted-syntax": [ + "error", + { + "selector": "TSEnumDeclaration", + "message": "Do not declare enums" + }, + { + "selector": "BinaryExpression[operator=/[=!]==/] Identifier[name='undefined']", + "message": "Do not strictly check undefined" + }, + { + "selector": "BinaryExpression[operator=/[=!]==/] Literal[raw='null']", + "message": "Do not strictly check null" + }, + { + "selector": "BinaryExpression[operator=/[=!]==?/] Literal[value='undefined']", + "message": "Do not strictly check typeof undefined (NOTE: currently this rule only detects the usage of 'undefined' string literal so this could be a misfire)" + } + ], + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ] + }, + "overrides": [ + { + "files": [ + "lib/*.js" + ], + "parserOptions": { + "ecmaVersion": 2019, + "sourceType": "commonjs" + } + }, + { + "files": [ + "test/**/*ts" + ], + "rules": { + // chat `expect(..)` style chaining is considered + // an unused expression + "@typescript-eslint/no-unused-expressions": "off" + } + }, + { + // json configuration files + "files": [ + ".*.json" + ], + "rules": { + "@typescript-eslint/no-unused-expressions": "off" + } + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index ac42a30..aeca64c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ ".esm-wrapper.mjs" ], "scripts": { - "lint": "eslint \"{src,test}/**/*.ts\"", + "lint": "ESLINT_USE_FLAT_CONFIG=false eslint \"{src,test}/**/*.ts\"", "test": "npm run lint && npm run build && nyc mocha --colors -r ts-node/register test/*.ts", "build": "npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", "prepack": "npm run build", @@ -39,16 +39,16 @@ "@types/chai": "^5.0.1", "@types/mocha": "^10.0.10", "@types/node": "^24.1.0", - "@typescript-eslint/eslint-plugin": "^4.2.0", - "@typescript-eslint/parser": "^4.2.0", + "@typescript-eslint/eslint-plugin": "^8.39.1", + "@typescript-eslint/parser": "^8.39.1", "chai": "^4.2.0", - "eslint": "^7.9.0", - "eslint-config-semistandard": "^15.0.1", - "eslint-config-standard": "^14.1.1", + "eslint": "^9.33.0", "eslint-plugin-import": "^2.22.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.1.0", "eslint-plugin-standard": "^5.0.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", "gen-esm-wrapper": "^1.1.3", "mocha": "^11.0.1", "nyc": "^15.1.0", @@ -59,4 +59,4 @@ "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0 || ^13.0.0" } -} +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1c459f6..129d96d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,18 @@ -import { URL, URLSearchParams } from 'whatwg-url'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { URL, URLSearchParams } from "whatwg-url"; import { redactValidConnectionString, redactConnectionString, - ConnectionStringRedactionOptions -} from './redact'; + ConnectionStringRedactionOptions, +} from "./redact"; export { redactConnectionString, ConnectionStringRedactionOptions }; -const DUMMY_HOSTNAME = '__this_is_a_placeholder__'; +const DUMMY_HOSTNAME = "__this_is_a_placeholder__"; function connectionStringHasValidScheme(connectionString: string) { return ( - connectionString.startsWith('mongodb://') || - connectionString.startsWith('mongodb+srv://') + connectionString.startsWith("mongodb://") || + connectionString.startsWith("mongodb+srv://") ); } @@ -49,7 +50,9 @@ class CaseInsensitiveMap extends Map { } } -function caseInsenstiveURLSearchParams(Ctor: typeof URLSearchParams) { +function caseInsenstiveURLSearchParams( + Ctor: typeof URLSearchParams, +) { return class CaseInsenstiveURLSearchParams extends Ctor { append(name: K, value: any): void { return super.append(this._normalizeKey(name), value); @@ -111,7 +114,7 @@ abstract class URLWithoutHost extends URL { class MongoParseError extends Error { get name(): string { - return 'MongoParseError'; + return "MongoParseError"; } } @@ -130,7 +133,9 @@ export class ConnectionString extends URLWithoutHost { constructor(uri: string, options: ConnectionStringParsingOptions = {}) { const { looseValidation } = options; if (!looseValidation && !connectionStringHasValidScheme(uri)) { - throw new MongoParseError('Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"'); + throw new MongoParseError( + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"', + ); } const match = uri.match(HOSTS_REGEX); @@ -142,12 +147,14 @@ export class ConnectionString extends URLWithoutHost { if (!looseValidation) { if (!protocol || !hosts) { - throw new MongoParseError(`Protocol and host list are required in "${uri}"`); + throw new MongoParseError( + `Protocol and host list are required in "${uri}"`, + ); } try { - decodeURIComponent(username ?? ''); - decodeURIComponent(password ?? ''); + decodeURIComponent(username ?? ""); + decodeURIComponent(password ?? ""); } catch (err) { throw new MongoParseError((err as Error).message); } @@ -155,27 +162,34 @@ export class ConnectionString extends URLWithoutHost { // characters not permitted in username nor password Set([':', '/', '?', '#', '[', ']', '@']) const illegalCharacters = /[:/?#[\]@]/gi; if (username?.match(illegalCharacters)) { - throw new MongoParseError(`Username contains unescaped characters ${username}`); + throw new MongoParseError( + `Username contains unescaped characters ${username}`, + ); } if (!username || !password) { - const uriWithoutProtocol = uri.replace(`${protocol}://`, ''); - if (uriWithoutProtocol.startsWith('@') || uriWithoutProtocol.startsWith(':')) { - throw new MongoParseError('URI contained empty userinfo section'); + const uriWithoutProtocol = uri.replace(`${protocol}://`, ""); + if ( + uriWithoutProtocol.startsWith("@") || + uriWithoutProtocol.startsWith(":") + ) { + throw new MongoParseError("URI contained empty userinfo section"); } } if (password?.match(illegalCharacters)) { - throw new MongoParseError('Password contains unescaped characters'); + throw new MongoParseError("Password contains unescaped characters"); } } - let authString = ''; - if (typeof username === 'string') authString += username; - if (typeof password === 'string') authString += `:${password}`; - if (authString) authString += '@'; + let authString = ""; + if (typeof username === "string") authString += username; + if (typeof password === "string") authString += `:${password}`; + if (authString) authString += "@"; try { - super(`${protocol.toLowerCase()}://${authString}${DUMMY_HOSTNAME}${rest}`); + super( + `${protocol.toLowerCase()}://${authString}${DUMMY_HOSTNAME}${rest}`, + ); } catch (err: any) { if (looseValidation) { // Call the constructor again, this time with loose validation off, @@ -183,45 +197,67 @@ export class ConnectionString extends URLWithoutHost { // eslint-disable-next-line no-new new ConnectionString(uri, { ...options, - looseValidation: false + looseValidation: false, }); } - if (typeof err.message === 'string') { + if (typeof err.message === "string") { err.message = err.message.replace(DUMMY_HOSTNAME, hosts); } throw err; } - this._hosts = hosts.split(','); + this._hosts = hosts.split(","); if (!looseValidation) { if (this.isSRV && this.hosts.length !== 1) { - throw new MongoParseError('mongodb+srv URI cannot have multiple service names'); + throw new MongoParseError( + "mongodb+srv URI cannot have multiple service names", + ); } - if (this.isSRV && this.hosts.some(host => host.includes(':'))) { - throw new MongoParseError('mongodb+srv URI cannot have port number'); + if (this.isSRV && this.hosts.some((host) => host.includes(":"))) { + throw new MongoParseError("mongodb+srv URI cannot have port number"); } } if (!this.pathname) { - this.pathname = '/'; + this.pathname = "/"; } - Object.setPrototypeOf(this.searchParams, caseInsenstiveURLSearchParams(this.searchParams.constructor as any).prototype); + Object.setPrototypeOf( + this.searchParams, + caseInsenstiveURLSearchParams(this.searchParams.constructor as any) + .prototype, + ); } // The getters here should throw, but that would break .toString() because of // https://github.com/nodejs/node/issues/36887. Using 'never' as the type // should be enough to stop anybody from using them in TypeScript, though. - get host(): never { return DUMMY_HOSTNAME as never; } - set host(_ignored: never) { throw new Error('No single host for connection string'); } - get hostname(): never { return DUMMY_HOSTNAME as never; } - set hostname(_ignored: never) { throw new Error('No single host for connection string'); } - get port(): never { return '' as never; } - set port(_ignored: never) { throw new Error('No single host for connection string'); } - get href(): string { return this.toString(); } - set href(_ignored: string) { throw new Error('Cannot set href for connection strings'); } + get host(): never { + return DUMMY_HOSTNAME as never; + } + set host(_ignored: never) { + throw new Error("No single host for connection string"); + } + get hostname(): never { + return DUMMY_HOSTNAME as never; + } + set hostname(_ignored: never) { + throw new Error("No single host for connection string"); + } + get port(): never { + return "" as never; + } + set port(_ignored: never) { + throw new Error("No single host for connection string"); + } + get href(): string { + return this.toString(); + } + set href(_ignored: string) { + throw new Error("Cannot set href for connection strings"); + } get isSRV(): boolean { - return this.protocol.includes('srv'); + return this.protocol.includes("srv"); } get hosts(): string[] { @@ -233,12 +269,12 @@ export class ConnectionString extends URLWithoutHost { } toString(): string { - return super.toString().replace(DUMMY_HOSTNAME, this.hosts.join(',')); + return super.toString().replace(DUMMY_HOSTNAME, this.hosts.join(",")); } clone(): ConnectionString { return new ConnectionString(this.toString(), { - looseValidation: true + looseValidation: true, }); } @@ -246,15 +282,38 @@ export class ConnectionString extends URLWithoutHost { return redactValidConnectionString(this, options); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/ban-types - typedSearchParams() { - const sametype = (false as true) && new (caseInsenstiveURLSearchParams(URLSearchParams))(); - return this.searchParams as unknown as typeof sametype; + typedSearchParams>() { + const _sametype = + (false as true) && + new (caseInsenstiveURLSearchParams(URLSearchParams))(); + return this.searchParams as unknown as typeof _sametype; } - [Symbol.for('nodejs.util.inspect.custom')](): any { - const { href, origin, protocol, username, password, hosts, pathname, search, searchParams, hash } = this; - return { href, origin, protocol, username, password, hosts, pathname, search, searchParams, hash }; + [Symbol.for("nodejs.util.inspect.custom")](): any { + const { + href, + origin, + protocol, + username, + password, + hosts, + pathname, + search, + searchParams, + hash, + } = this; + return { + href, + origin, + protocol, + username, + password, + hosts, + pathname, + search, + searchParams, + hash, + }; } } @@ -262,24 +321,28 @@ export class ConnectionString extends URLWithoutHost { * Parses and serializes the format of the authMechanismProperties or * readPreferenceTags connection string parameters. */ -// eslint-disable-next-line @typescript-eslint/ban-types -export class CommaAndColonSeparatedRecord> extends CaseInsensitiveMap { +export class CommaAndColonSeparatedRecord< + K extends Record = Record, +> extends CaseInsensitiveMap { constructor(from?: string | null) { super(); - for (const entry of (from ?? '').split(',')) { + for (const entry of (from ?? "").split(",")) { if (!entry) continue; - const colonIndex = entry.indexOf(':'); + const colonIndex = entry.indexOf(":"); // Use .set() to properly account for case insensitivity if (colonIndex === -1) { - this.set(entry as (keyof K & string), ''); + this.set(entry as keyof K & string, ""); } else { - this.set(entry.slice(0, colonIndex) as (keyof K & string), entry.slice(colonIndex + 1)); + this.set( + entry.slice(0, colonIndex) as keyof K & string, + entry.slice(colonIndex + 1), + ); } } } toString(): string { - return [...this].map(entry => entry.join(':')).join(','); + return [...this].map((entry) => entry.join(":")).join(","); } } diff --git a/src/redact.ts b/src/redact.ts index f9636bb..7a3ced1 100644 --- a/src/redact.ts +++ b/src/redact.ts @@ -1,4 +1,4 @@ -import ConnectionString, { CommaAndColonSeparatedRecord } from './index'; +import ConnectionString, { CommaAndColonSeparatedRecord } from "./index"; export interface ConnectionStringRedactionOptions { redactUsernames?: boolean; @@ -7,51 +7,60 @@ export interface ConnectionStringRedactionOptions { export function redactValidConnectionString( inputUrl: Readonly, - options?: ConnectionStringRedactionOptions): ConnectionString { + options?: ConnectionStringRedactionOptions, +): ConnectionString { const url = inputUrl.clone(); - const replacementString = options?.replacementString ?? '_credentials_'; + const replacementString = options?.replacementString ?? "_credentials_"; const redactUsernames = options?.redactUsernames ?? true; if ((url.username || url.password) && redactUsernames) { url.username = replacementString; - url.password = ''; + url.password = ""; } else if (url.password) { url.password = replacementString; } - if (url.searchParams.has('authMechanismProperties')) { - const props = new CommaAndColonSeparatedRecord(url.searchParams.get('authMechanismProperties')); - if (props.get('AWS_SESSION_TOKEN')) { - props.set('AWS_SESSION_TOKEN', replacementString); - url.searchParams.set('authMechanismProperties', props.toString()); + if (url.searchParams.has("authMechanismProperties")) { + const props = new CommaAndColonSeparatedRecord( + url.searchParams.get("authMechanismProperties"), + ); + if (props.get("AWS_SESSION_TOKEN")) { + props.set("AWS_SESSION_TOKEN", replacementString); + url.searchParams.set("authMechanismProperties", props.toString()); } } - if (url.searchParams.has('tlsCertificateKeyFilePassword')) { - url.searchParams.set('tlsCertificateKeyFilePassword', replacementString); + if (url.searchParams.has("tlsCertificateKeyFilePassword")) { + url.searchParams.set("tlsCertificateKeyFilePassword", replacementString); } - if (url.searchParams.has('proxyUsername') && redactUsernames) { - url.searchParams.set('proxyUsername', replacementString); + if (url.searchParams.has("proxyUsername") && redactUsernames) { + url.searchParams.set("proxyUsername", replacementString); } - if (url.searchParams.has('proxyPassword')) { - url.searchParams.set('proxyPassword', replacementString); + if (url.searchParams.has("proxyPassword")) { + url.searchParams.set("proxyPassword", replacementString); } return url; } export function redactConnectionString( uri: string, - options?: ConnectionStringRedactionOptions): string { - const replacementString = options?.replacementString ?? ''; + options?: ConnectionStringRedactionOptions, +): string { + const replacementString = options?.replacementString ?? ""; const redactUsernames = options?.redactUsernames ?? true; let parsed: ConnectionString | undefined; try { parsed = new ConnectionString(uri); - } catch {} + } catch { + // squash errors + } if (parsed) { // If we can parse the connection string, use the more precise // redaction logic. - options = { ...options, replacementString: '___credentials___' }; - return parsed.redact(options).toString().replace(/___credentials___/g, replacementString); + options = { ...options, replacementString: "___credentials___" }; + return parsed + .redact(options) + .toString() + .replace(/___credentials___/g, replacementString); } // Note: The regexes here used to use lookbehind assertions, but we dropped that since @@ -59,15 +68,22 @@ export function redactConnectionString( const R = replacementString; // alias for conciseness const replacements: ((uri: string) => string)[] = [ // Username and password - uri => uri.replace(redactUsernames ? /(\/\/)(.*)(@)/g : /(\/\/[^@]*:)(.*)(@)/g, `$1${R}$3`), + (uri) => + uri.replace( + redactUsernames ? /(\/\/)(.*)(@)/g : /(\/\/[^@]*:)(.*)(@)/g, + `$1${R}$3`, + ), // AWS IAM Session Token as part of query parameter - uri => uri.replace(/(AWS_SESSION_TOKEN(:|%3A))([^,&]+)/gi, `$1${R}`), + (uri) => uri.replace(/(AWS_SESSION_TOKEN(:|%3A))([^,&]+)/gi, `$1${R}`), // tlsCertificateKeyFilePassword query parameter - uri => uri.replace(/(tlsCertificateKeyFilePassword=)([^&]+)/gi, `$1${R}`), + (uri) => uri.replace(/(tlsCertificateKeyFilePassword=)([^&]+)/gi, `$1${R}`), // proxyUsername query parameter - uri => redactUsernames ? uri.replace(/(proxyUsername=)([^&]+)/gi, `$1${R}`) : uri, + (uri) => + redactUsernames + ? uri.replace(/(proxyUsername=)([^&]+)/gi, `$1${R}`) + : uri, // proxyPassword query parameter - uri => uri.replace(/(proxyPassword=)([^&]+)/gi, `$1${R}`) + (uri) => uri.replace(/(proxyPassword=)([^&]+)/gi, `$1${R}`), ]; for (const replacer of replacements) { uri = replacer(uri); diff --git a/test/index.ts b/test/index.ts index 115bd22..e60245f 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,283 +1,310 @@ -import { expect } from 'chai'; -import ConnectionString, { CommaAndColonSeparatedRecord } from '../'; +import { expect } from "chai"; +import ConnectionString, { CommaAndColonSeparatedRecord } from "../"; -describe('ConnectionString', () => { - context('with valid URIs', () => { +describe("ConnectionString", () => { + context("with valid URIs", () => { for (const { uri, match } of [ { - uri: 'mongodb://localhost/', + uri: "mongodb://localhost/", match: { - href: 'mongodb://localhost/', - protocol: 'mongodb:', - username: '', - password: '', - pathname: '/', - search: '', - hash: '', + href: "mongodb://localhost/", + protocol: "mongodb:", + username: "", + password: "", + pathname: "/", + search: "", + hash: "", isSRV: false, - hosts: ['localhost'] - } + hosts: ["localhost"], + }, }, { - uri: 'mongodb+srv://localhost', + uri: "mongodb+srv://localhost", match: { - href: 'mongodb+srv://localhost/', - protocol: 'mongodb+srv:', - username: '', - password: '', - pathname: '/', - search: '', - hash: '', + href: "mongodb+srv://localhost/", + protocol: "mongodb+srv:", + username: "", + password: "", + pathname: "/", + search: "", + hash: "", isSRV: true, - hosts: ['localhost'] - } + hosts: ["localhost"], + }, }, { - uri: 'mongodb+srv://cat:meow@localhost/', + uri: "mongodb+srv://cat:meow@localhost/", match: { - href: 'mongodb+srv://cat:meow@localhost/', - protocol: 'mongodb+srv:', - username: 'cat', - password: 'meow', - pathname: '/', - search: '', - hash: '', + href: "mongodb+srv://cat:meow@localhost/", + protocol: "mongodb+srv:", + username: "cat", + password: "meow", + pathname: "/", + search: "", + hash: "", isSRV: true, - hosts: ['localhost'] - } + hosts: ["localhost"], + }, }, { - uri: 'mongodb://cat:meow@localhost:12345/db', + uri: "mongodb://cat:meow@localhost:12345/db", match: { - href: 'mongodb://cat:meow@localhost:12345/db', - protocol: 'mongodb:', - username: 'cat', - password: 'meow', - pathname: '/db', - search: '', - hash: '', + href: "mongodb://cat:meow@localhost:12345/db", + protocol: "mongodb:", + username: "cat", + password: "meow", + pathname: "/db", + search: "", + hash: "", isSRV: false, - hosts: ['localhost:12345'] - } + hosts: ["localhost:12345"], + }, }, { - uri: 'mongodb://localhost:12345,anotherHost/?directConnection=true', + uri: "mongodb://localhost:12345,anotherHost/?directConnection=true", match: { - href: 'mongodb://localhost:12345,anotherHost/?directConnection=true', - protocol: 'mongodb:', - username: '', - password: '', - pathname: '/', - search: '?directConnection=true', - hash: '', + href: "mongodb://localhost:12345,anotherHost/?directConnection=true", + protocol: "mongodb:", + username: "", + password: "", + pathname: "/", + search: "?directConnection=true", + hash: "", isSRV: false, - hosts: ['localhost:12345', 'anotherHost'] - } + hosts: ["localhost:12345", "anotherHost"], + }, }, { - uri: 'mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@', + uri: "mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@", match: { - href: 'mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@', - protocol: 'mongodb:', - username: 'database-meow', - password: '', - pathname: '/', - search: '?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@', - hash: '', + href: "mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@", + protocol: "mongodb:", + username: "database-meow", + password: "", + pathname: "/", + search: + "?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@", + hash: "", isSRV: false, - hosts: ['database-haha.mongo.blah.blah.com:8888'] - } - } + hosts: ["database-haha.mongo.blah.blah.com:8888"], + }, + }, ]) { it(`parses ${uri} correctly`, () => { const cs = new ConnectionString(uri); - for (const key of Object.keys(match) as (keyof typeof cs & keyof typeof match)[]) { + for (const key of Object.keys(match) as (keyof typeof cs & + keyof typeof match)[]) { expect(cs[key]).to.deep.equal(match[key]); } }); } }); - context('with an invalid schema on URI', () => { - it('throws an error mentioning the invalid schema', () => { + context("with an invalid schema on URI", () => { + it("throws an error mentioning the invalid schema", () => { try { // eslint-disable-next-line no-new - new ConnectionString('totallynotamongodb://outerspace'); + new ConnectionString("totallynotamongodb://outerspace"); } catch (err) { - expect((err as Error).message).to.equal('Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"'); - expect((err as Error).name).to.equal('MongoParseError'); + expect((err as Error).message).to.equal( + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"', + ); + expect((err as Error).name).to.equal("MongoParseError"); return; } - expect.fail('missed exception'); + expect.fail("missed exception"); }); }); - context('with invalid URIs', () => { + context("with invalid URIs", () => { for (const uri of [ - '', - '//', - '//@/', - 'mongodb://', - 'mongodb://@localhost/', - 'mongodb://:@localhost/', - 'mongodb://:pass@localhost/', - 'mongodb://%a@localhost/', - 'mongodb://:%a@localhost/', - 'mongodb://a[@localhost/', - 'mongodb://a:[@localhost/', - 'mongodb+srv://a,b,c/', - 'mongodb+srv://a:12345/', - 'mongodbabc://localhost', - 'totallynotamongodb://localhost', - 'mongodb+srv://Y:X@' + "", + "//", + "//@/", + "mongodb://", + "mongodb://@localhost/", + "mongodb://:@localhost/", + "mongodb://:pass@localhost/", + "mongodb://%a@localhost/", + "mongodb://:%a@localhost/", + "mongodb://a[@localhost/", + "mongodb://a:[@localhost/", + "mongodb+srv://a,b,c/", + "mongodb+srv://a:12345/", + "mongodbabc://localhost", + "totallynotamongodb://localhost", + "mongodb+srv://Y:X@", ]) { it(`parsing ${uri} throws an MongoParseError`, () => { try { // eslint-disable-next-line no-new new ConnectionString(uri); } catch (err) { - expect((err as Error).name).to.equal('MongoParseError'); + expect((err as Error).name).to.equal("MongoParseError"); return; } - expect.fail('missed exception'); + expect.fail("missed exception"); }); } }); - context('after modifications', () => { - it('allows changing hosts', () => { - const cs = new ConnectionString('mongodb://localhost'); - expect(cs.hosts).to.deep.equal(['localhost']); + context("after modifications", () => { + it("allows changing hosts", () => { + const cs = new ConnectionString("mongodb://localhost"); + expect(cs.hosts).to.deep.equal(["localhost"]); - cs.hosts.push('localhost2'); - expect(cs.hosts).to.deep.equal(['localhost', 'localhost2']); - expect(cs.toString()).to.equal('mongodb://localhost,localhost2/'); + cs.hosts.push("localhost2"); + expect(cs.hosts).to.deep.equal(["localhost", "localhost2"]); + expect(cs.toString()).to.equal("mongodb://localhost,localhost2/"); - cs.hosts = ['a', 'b', 'c']; - expect(cs.hosts).to.deep.equal(['a', 'b', 'c']); - expect(cs.toString()).to.equal('mongodb://a,b,c/'); + cs.hosts = ["a", "b", "c"]; + expect(cs.hosts).to.deep.equal(["a", "b", "c"]); + expect(cs.toString()).to.equal("mongodb://a,b,c/"); }); - it('performs case-insensitive matches on connection options', () => { - const cs = new ConnectionString('mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=100'); - cs.searchParams.set('serverSelectionTimeoutMS', '200'); - cs.searchParams.append('serverSelectionTimeoutMS', '300'); + it("performs case-insensitive matches on connection options", () => { + const cs = new ConnectionString( + "mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=100", + ); + cs.searchParams.set("serverSelectionTimeoutMS", "200"); + cs.searchParams.append("serverSelectionTimeoutMS", "300"); - expect(cs.toString()).to.equal('mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=200&SERVERSELECTIONTIMEOUTMS=300'); - expect(cs.searchParams.has('serverSelectionTimeoutMS')).to.equal(true); - expect(cs.searchParams.has('SERVERSELECTIONTIMEOUTMS')).to.equal(true); - expect(cs.searchParams.get('serverSelectionTimeoutMS')).to.equal('200'); - expect(cs.searchParams.getAll('serverSelectionTimeoutMS')).to.deep.equal(['200', '300']); + expect(cs.toString()).to.equal( + "mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=200&SERVERSELECTIONTIMEOUTMS=300", + ); + expect(cs.searchParams.has("serverSelectionTimeoutMS")).to.equal(true); + expect(cs.searchParams.has("SERVERSELECTIONTIMEOUTMS")).to.equal(true); + expect(cs.searchParams.get("serverSelectionTimeoutMS")).to.equal("200"); + expect(cs.searchParams.getAll("serverSelectionTimeoutMS")).to.deep.equal([ + "200", + "300", + ]); - cs.searchParams.delete('serverSelectionTimeoutMS'); - expect(cs.searchParams.has('serverSelectionTimeoutMS')).to.equal(false); - expect(cs.searchParams.has('SERVERSELECTIONTIMEOUTMS')).to.equal(false); + cs.searchParams.delete("serverSelectionTimeoutMS"); + expect(cs.searchParams.has("serverSelectionTimeoutMS")).to.equal(false); + expect(cs.searchParams.has("SERVERSELECTIONTIMEOUTMS")).to.equal(false); }); }); - context('cloning', () => { - it('can make copies of ConnectionString instances', () => { - const cs = new ConnectionString('mongodb://localhost'); - expect(cs.toString()).to.equal('mongodb://localhost/'); - expect(cs.clone().toString()).to.equal('mongodb://localhost/'); + context("cloning", () => { + it("can make copies of ConnectionString instances", () => { + const cs = new ConnectionString("mongodb://localhost"); + expect(cs.toString()).to.equal("mongodb://localhost/"); + expect(cs.clone().toString()).to.equal("mongodb://localhost/"); }); }); - context('URL methods that do not apply to connection strings as-is', () => { - it('throws/returns dummy values', () => { - const cs: any = new ConnectionString('mongodb://localhost'); - expect(cs.host).not.to.equal('localhost'); - expect(cs.hostname).not.to.equal('localhost'); - expect(cs.port).to.equal(''); - expect(cs.href).to.equal('mongodb://localhost/'); - expect(() => { cs.host = 'abc'; }).to.throw(Error); - expect(() => { cs.hostname = 'abc'; }).to.throw(Error); - expect(() => { cs.port = '1000'; }).to.throw(Error); - expect(() => { cs.href = 'mongodb://localhost'; }).to.throw(Error); + context("URL methods that do not apply to connection strings as-is", () => { + it("throws/returns dummy values", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cs: any = new ConnectionString("mongodb://localhost"); + expect(cs.host).not.to.equal("localhost"); + expect(cs.hostname).not.to.equal("localhost"); + expect(cs.port).to.equal(""); + expect(cs.href).to.equal("mongodb://localhost/"); + expect(() => { + cs.host = "abc"; + }).to.throw(Error); + expect(() => { + cs.hostname = "abc"; + }).to.throw(Error); + expect(() => { + cs.port = "1000"; + }).to.throw(Error); + expect(() => { + cs.href = "mongodb://localhost"; + }).to.throw(Error); }); }); - context('with loose validation', () => { - it('allows odd connection strings', () => { - const cs: any = new ConnectionString('mongodb://:password@x', { looseValidation: true }); - expect(cs.username).to.equal(''); - expect(cs.password).to.equal('password'); - expect(cs.port).to.equal(''); - expect(cs.href).to.equal('mongodb://:password@x/'); + context("with loose validation", () => { + it("allows odd connection strings", () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cs: any = new ConnectionString("mongodb://:password@x", { + looseValidation: true, + }); + expect(cs.username).to.equal(""); + expect(cs.password).to.equal("password"); + expect(cs.port).to.equal(""); + expect(cs.href).to.equal("mongodb://:password@x/"); }); - it('throws good error messages for invalid URLs', () => { + it("throws good error messages for invalid URLs", () => { try { - // eslint-disable-next-line no-new - new ConnectionString('-://:password@x', { looseValidation: true }); - expect.fail('missed exception'); + new ConnectionString("-://:password@x", { looseValidation: true }); + expect.fail("missed exception"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { - expect(err.message).to.equal('Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"'); + expect(err.message).to.equal( + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"', + ); } }); }); }); -describe('CommaAndColonSeparatedRecord', () => { - it('creates an empty map for empty input', () => { - expect(new CommaAndColonSeparatedRecord('').size).to.equal(0); +describe("CommaAndColonSeparatedRecord", () => { + it("creates an empty map for empty input", () => { + expect(new CommaAndColonSeparatedRecord("").size).to.equal(0); expect(new CommaAndColonSeparatedRecord().size).to.equal(0); expect(new CommaAndColonSeparatedRecord(null).size).to.equal(0); expect(new CommaAndColonSeparatedRecord(undefined).size).to.equal(0); }); - it('returns an empty string for empty input', () => { - expect(new CommaAndColonSeparatedRecord('').toString()).to.equal(''); - expect(new CommaAndColonSeparatedRecord().toString()).to.equal(''); - expect(new CommaAndColonSeparatedRecord(null).toString()).to.equal(''); - expect(new CommaAndColonSeparatedRecord(undefined).toString()).to.equal(''); + it("returns an empty string for empty input", () => { + expect(new CommaAndColonSeparatedRecord("").toString()).to.equal(""); + expect(new CommaAndColonSeparatedRecord().toString()).to.equal(""); + expect(new CommaAndColonSeparatedRecord(null).toString()).to.equal(""); + expect(new CommaAndColonSeparatedRecord(undefined).toString()).to.equal(""); }); - it('allows getting entries', () => { - const record = new CommaAndColonSeparatedRecord('A:B,C:D'); - expect(record.toString()).to.equal('A:B,C:D'); - expect(record.get('A')).to.equal('B'); - expect(record.get('C')).to.equal('D'); - expect(record.get('foo')).to.equal(undefined); + it("allows getting entries", () => { + const record = new CommaAndColonSeparatedRecord("A:B,C:D"); + expect(record.toString()).to.equal("A:B,C:D"); + expect(record.get("A")).to.equal("B"); + expect(record.get("C")).to.equal("D"); + expect(record.get("foo")).to.equal(undefined); }); - it('allows setting entries', () => { - const record = new CommaAndColonSeparatedRecord('A:B,C:D'); - record.set('A', '0'); - record.set('E', '1'); - expect(record.toString()).to.equal('A:0,C:D,E:1'); + it("allows setting entries", () => { + const record = new CommaAndColonSeparatedRecord("A:B,C:D"); + record.set("A", "0"); + record.set("E", "1"); + expect(record.toString()).to.equal("A:0,C:D,E:1"); }); - it('accepts cases of multiple-colon entries', () => { - const record = new CommaAndColonSeparatedRecord('A:B:C,D'); - expect(record.toString()).to.equal('A:B:C,D:'); - expect(record.get('A')).to.equal('B:C'); - expect(record.get('D')).to.equal(''); + it("accepts cases of multiple-colon entries", () => { + const record = new CommaAndColonSeparatedRecord("A:B:C,D"); + expect(record.toString()).to.equal("A:B:C,D:"); + expect(record.get("A")).to.equal("B:C"); + expect(record.get("D")).to.equal(""); }); - it('is case-insensitive', () => { - const record = new CommaAndColonSeparatedRecord('foo:bar,FOO:BAR'); - expect(record.toString()).to.equal('foo:BAR'); - expect(record.get('FOO')).to.equal('BAR'); - expect(record.get('foo')).to.equal('BAR'); - record.set('FOO', 'baz'); - expect(record.toString()).to.equal('foo:baz'); + it("is case-insensitive", () => { + const record = new CommaAndColonSeparatedRecord("foo:bar,FOO:BAR"); + expect(record.toString()).to.equal("foo:BAR"); + expect(record.get("FOO")).to.equal("BAR"); + expect(record.get("foo")).to.equal("BAR"); + record.set("FOO", "baz"); + expect(record.toString()).to.equal("foo:baz"); }); }); -describe('TypeScript support', () => { - it('allows specifying typed search parameters', () => { - const cs = new ConnectionString('mongodb://localhost/?tls=true&tls2=false'); +describe("TypeScript support", () => { + it("allows specifying typed search parameters", () => { + const cs = new ConnectionString("mongodb://localhost/?tls=true&tls2=false"); const sp = cs.typedSearchParams<{ tls: string }>(); - expect(sp.get('tls')).to.equal('true'); + expect(sp.get("tls")).to.equal("true"); // @ts-expect-error should fail - expect(sp.get('tls2')).to.equal('false'); + expect(sp.get("tls2")).to.equal("false"); }); - it('allows specifying typed comma-and-colon-separated-record types', () => { - const record = new CommaAndColonSeparatedRecord<{ foo: string }>('foo:bar,baz:quux'); - expect(record.get('foo')).to.equal('bar'); + it("allows specifying typed comma-and-colon-separated-record types", () => { + const record = new CommaAndColonSeparatedRecord<{ foo: string }>( + "foo:bar,baz:quux", + ); + expect(record.get("foo")).to.equal("bar"); // @ts-expect-error should fail - expect(record.get('baz')).to.equal('quux'); + expect(record.get("baz")).to.equal("quux"); }); }); diff --git a/test/redact.ts b/test/redact.ts index 501674e..36e6daf 100644 --- a/test/redact.ts +++ b/test/redact.ts @@ -1,62 +1,126 @@ -import { expect } from 'chai'; -import { redactConnectionString } from '../'; +import { expect } from "chai"; +import { redactConnectionString } from "../"; -describe('redact credentials', () => { - for (const protocol of ['mongodb', 'mongodb+srv', '+invalid+']) { +describe("redact credentials", () => { + for (const protocol of ["mongodb", "mongodb+srv", "+invalid+"]) { context(`when url contains credentials (protocol: ${protocol})`, () => { - it('returns the in output instead of password', () => { - expect(redactConnectionString(`${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin`)) - .to.equal(`${protocol}://@cats-data-sets-e08dy.mongodb.net/admin`); + it("returns the in output instead of password", () => { + expect( + redactConnectionString( + `${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin`, + ), + ).to.equal( + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin`, + ); }); - it('returns the keeping the username if desired', () => { - expect(redactConnectionString(`${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin`, { redactUsernames: false })) - .to.equal(`${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin`); + it("returns the keeping the username if desired", () => { + expect( + redactConnectionString( + `${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin`, + { redactUsernames: false }, + ), + ).to.equal( + `${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin`, + ); }); - it('returns the in output instead of IAM session token', () => { - expect(redactConnectionString(`${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken,else%3Amiau¶m=true`).replace(/%2C/g, ',')) - .to.equal(`${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A,else%3Amiau¶m=true`); - expect(redactConnectionString(`${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true`)) - .to.equal(`${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true`); - expect(redactConnectionString(`${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken`)) - .to.equal(`${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A`); + it("returns the in output instead of IAM session token", () => { + expect( + redactConnectionString( + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken,else%3Amiau¶m=true`, + ).replace(/%2C/g, ","), + ).to.equal( + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A,else%3Amiau¶m=true`, + ); + expect( + redactConnectionString( + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true`, + ), + ).to.equal( + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true`, + ); + expect( + redactConnectionString( + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken`, + ), + ).to.equal( + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A`, + ); }); - it('returns the in output instead of password and IAM session token', () => { - expect(redactConnectionString(`${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true`)) - .to.equal(`${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true`); + it("returns the in output instead of password and IAM session token", () => { + expect( + redactConnectionString( + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true`, + ), + ).to.equal( + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true`, + ); }); - it('returns the in output instead of tlsCertificateKeyFilePassword', () => { - expect(redactConnectionString(`${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=p4ssw0rd`)) - .to.equal(`${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=`); + it("returns the in output instead of tlsCertificateKeyFilePassword", () => { + expect( + redactConnectionString( + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=p4ssw0rd`, + ), + ).to.equal( + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=`, + ); }); - it('returns the in output instead of proxyPassword and proxyUsername', () => { - expect(redactConnectionString(`${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar¶m=true`)) - .to.equal(`${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=¶m=true`); - expect(redactConnectionString(`${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`)) - .to.equal(`${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=`); - expect(redactConnectionString(`${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, { redactUsernames: false })) - .to.equal(`${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=`); - expect(redactConnectionString(`${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, { replacementString: '****' })) - .to.equal(`${protocol}://****@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=****&proxyPassword=****`); + it("returns the in output instead of proxyPassword and proxyUsername", () => { + expect( + redactConnectionString( + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar¶m=true`, + ), + ).to.equal( + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=¶m=true`, + ); + expect( + redactConnectionString( + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, + ), + ).to.equal( + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=`, + ); + expect( + redactConnectionString( + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, + { redactUsernames: false }, + ), + ).to.equal( + `${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=`, + ); + expect( + redactConnectionString( + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, + { replacementString: "****" }, + ), + ).to.equal( + `${protocol}://****@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=****&proxyPassword=****`, + ); }); - it('redacts credentials when username is empty', () => { + it("redacts credentials when username is empty", () => { expect( - redactConnectionString(`${protocol}://:password@mongodb.net/`) + redactConnectionString(`${protocol}://:password@mongodb.net/`), ).to.equal(`${protocol}://@mongodb.net/`); }); }); - context('when url contains no credentials', () => { - it('does not alter input', () => { - expect(redactConnectionString(`${protocol}://127.0.0.1:27017/`)) - .to.equal(`${protocol}://127.0.0.1:27017/`); - expect(redactConnectionString(`${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`)) - .to.equal(`${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`); + context("when url contains no credentials", () => { + it("does not alter input", () => { + expect( + redactConnectionString(`${protocol}://127.0.0.1:27017/`), + ).to.equal(`${protocol}://127.0.0.1:27017/`); + expect( + redactConnectionString( + `${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`, + ), + ).to.equal( + `${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`, + ); }); }); } From ea2b0de5d8b6ca4b081079a05b8e1934a07a0594 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 13:12:25 -0600 Subject: [PATCH 3/7] last upgrades --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index aeca64c..dca1d57 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "eslint-plugin-import": "^2.22.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.1.0", - "eslint-plugin-standard": "^5.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "gen-esm-wrapper": "^1.1.3", From ba8ae8d05273b0a0a5aaab7aaa0ddee6ce080776 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 13:16:35 -0600 Subject: [PATCH 4/7] fix TS compilation issues --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dca1d57..02d349a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@types/chai": "^5.0.1", "@types/mocha": "^10.0.10", - "@types/node": "^24.1.0", + "@types/node": "^22.9.0", "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "chai": "^4.2.0", From ae090f9912e7d7810f5bca483c43ee2a9988b3e4 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 13:19:51 -0600 Subject: [PATCH 5/7] lint & test actions --- .github/dependabot.yml | 2 ++ .github/workflows/lint.yml | 18 ++++++++++++++++++ package.json | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 22ef6a2..bba2e94 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,6 +22,8 @@ updates: versions: [">=16.0.0"] # we ignore TS as a part of quarterly dependency updates. - dependency-name: "typescript" + - dependency-name: "@types/node" + versions: [">=24.0.0"] groups: development-dependencies: dependency-type: "development" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..52d493c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,18 @@ +on: [push, pull_request] + +name: CI + +jobs: + test: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install Dependencies + run: npm install + - name: Lint + run: npm run lint diff --git a/package.json b/package.json index 02d349a..019821d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ ], "scripts": { "lint": "ESLINT_USE_FLAT_CONFIG=false eslint \"{src,test}/**/*.ts\"", - "test": "npm run lint && npm run build && nyc mocha --colors -r ts-node/register test/*.ts", + "test": "npm run build && nyc mocha --colors -r ts-node/register test/*.ts", "build": "npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", "prepack": "npm run build", "compile-ts": "tsc -p tsconfig.json" From 2b81a7f1b011d02ef30039cffe11f70099055398 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 13:21:31 -0600 Subject: [PATCH 6/7] add permissions --- .github/workflows/lint.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 52d493c..67fc1ff 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,9 @@ on: [push, pull_request] name: CI - +permissions: + contents: read + jobs: test: name: Lint From 74a3015b55207b9504088bd619fd0f8a0cdb4da5 Mon Sep 17 00:00:00 2001 From: bailey Date: Tue, 12 Aug 2025 13:47:52 -0600 Subject: [PATCH 7/7] comments --- .eslintrc => .eslintrc.json | 0 .prettierrc.json | 7 + src/index.ts | 115 +++++------ src/redact.ts | 55 +++--- test/index.ts | 370 ++++++++++++++++++------------------ test/redact.ts | 116 ++++++----- 6 files changed, 312 insertions(+), 351 deletions(-) rename .eslintrc => .eslintrc.json (100%) create mode 100644 .prettierrc.json diff --git a/.eslintrc b/.eslintrc.json similarity index 100% rename from .eslintrc rename to .eslintrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..169dcfb --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "arrowParens": "avoid", + "trailingComma": "none" +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 129d96d..f661f4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { URL, URLSearchParams } from "whatwg-url"; +import { URL, URLSearchParams } from 'whatwg-url'; import { redactValidConnectionString, redactConnectionString, - ConnectionStringRedactionOptions, -} from "./redact"; + ConnectionStringRedactionOptions +} from './redact'; export { redactConnectionString, ConnectionStringRedactionOptions }; -const DUMMY_HOSTNAME = "__this_is_a_placeholder__"; +const DUMMY_HOSTNAME = '__this_is_a_placeholder__'; function connectionStringHasValidScheme(connectionString: string) { - return ( - connectionString.startsWith("mongodb://") || - connectionString.startsWith("mongodb+srv://") - ); + return connectionString.startsWith('mongodb://') || connectionString.startsWith('mongodb+srv://'); } // Adapted from the Node.js driver code: @@ -50,9 +47,7 @@ class CaseInsensitiveMap extends Map { } } -function caseInsenstiveURLSearchParams( - Ctor: typeof URLSearchParams, -) { +function caseInsenstiveURLSearchParams(Ctor: typeof URLSearchParams) { return class CaseInsenstiveURLSearchParams extends Ctor { append(name: K, value: any): void { return super.append(this._normalizeKey(name), value); @@ -114,7 +109,7 @@ abstract class URLWithoutHost extends URL { class MongoParseError extends Error { get name(): string { - return "MongoParseError"; + return 'MongoParseError'; } } @@ -134,7 +129,7 @@ export class ConnectionString extends URLWithoutHost { const { looseValidation } = options; if (!looseValidation && !connectionStringHasValidScheme(uri)) { throw new MongoParseError( - 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"', + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' ); } @@ -147,14 +142,12 @@ export class ConnectionString extends URLWithoutHost { if (!looseValidation) { if (!protocol || !hosts) { - throw new MongoParseError( - `Protocol and host list are required in "${uri}"`, - ); + throw new MongoParseError(`Protocol and host list are required in "${uri}"`); } try { - decodeURIComponent(username ?? ""); - decodeURIComponent(password ?? ""); + decodeURIComponent(username ?? ''); + decodeURIComponent(password ?? ''); } catch (err) { throw new MongoParseError((err as Error).message); } @@ -162,34 +155,27 @@ export class ConnectionString extends URLWithoutHost { // characters not permitted in username nor password Set([':', '/', '?', '#', '[', ']', '@']) const illegalCharacters = /[:/?#[\]@]/gi; if (username?.match(illegalCharacters)) { - throw new MongoParseError( - `Username contains unescaped characters ${username}`, - ); + throw new MongoParseError(`Username contains unescaped characters ${username}`); } if (!username || !password) { - const uriWithoutProtocol = uri.replace(`${protocol}://`, ""); - if ( - uriWithoutProtocol.startsWith("@") || - uriWithoutProtocol.startsWith(":") - ) { - throw new MongoParseError("URI contained empty userinfo section"); + const uriWithoutProtocol = uri.replace(`${protocol}://`, ''); + if (uriWithoutProtocol.startsWith('@') || uriWithoutProtocol.startsWith(':')) { + throw new MongoParseError('URI contained empty userinfo section'); } } if (password?.match(illegalCharacters)) { - throw new MongoParseError("Password contains unescaped characters"); + throw new MongoParseError('Password contains unescaped characters'); } } - let authString = ""; - if (typeof username === "string") authString += username; - if (typeof password === "string") authString += `:${password}`; - if (authString) authString += "@"; + let authString = ''; + if (typeof username === 'string') authString += username; + if (typeof password === 'string') authString += `:${password}`; + if (authString) authString += '@'; try { - super( - `${protocol.toLowerCase()}://${authString}${DUMMY_HOSTNAME}${rest}`, - ); + super(`${protocol.toLowerCase()}://${authString}${DUMMY_HOSTNAME}${rest}`); } catch (err: any) { if (looseValidation) { // Call the constructor again, this time with loose validation off, @@ -197,34 +183,31 @@ export class ConnectionString extends URLWithoutHost { // eslint-disable-next-line no-new new ConnectionString(uri, { ...options, - looseValidation: false, + looseValidation: false }); } - if (typeof err.message === "string") { + if (typeof err.message === 'string') { err.message = err.message.replace(DUMMY_HOSTNAME, hosts); } throw err; } - this._hosts = hosts.split(","); + this._hosts = hosts.split(','); if (!looseValidation) { if (this.isSRV && this.hosts.length !== 1) { - throw new MongoParseError( - "mongodb+srv URI cannot have multiple service names", - ); + throw new MongoParseError('mongodb+srv URI cannot have multiple service names'); } - if (this.isSRV && this.hosts.some((host) => host.includes(":"))) { - throw new MongoParseError("mongodb+srv URI cannot have port number"); + if (this.isSRV && this.hosts.some(host => host.includes(':'))) { + throw new MongoParseError('mongodb+srv URI cannot have port number'); } } if (!this.pathname) { - this.pathname = "/"; + this.pathname = '/'; } Object.setPrototypeOf( this.searchParams, - caseInsenstiveURLSearchParams(this.searchParams.constructor as any) - .prototype, + caseInsenstiveURLSearchParams(this.searchParams.constructor as any).prototype ); } @@ -235,29 +218,29 @@ export class ConnectionString extends URLWithoutHost { return DUMMY_HOSTNAME as never; } set host(_ignored: never) { - throw new Error("No single host for connection string"); + throw new Error('No single host for connection string'); } get hostname(): never { return DUMMY_HOSTNAME as never; } set hostname(_ignored: never) { - throw new Error("No single host for connection string"); + throw new Error('No single host for connection string'); } get port(): never { - return "" as never; + return '' as never; } set port(_ignored: never) { - throw new Error("No single host for connection string"); + throw new Error('No single host for connection string'); } get href(): string { return this.toString(); } set href(_ignored: string) { - throw new Error("Cannot set href for connection strings"); + throw new Error('Cannot set href for connection strings'); } get isSRV(): boolean { - return this.protocol.includes("srv"); + return this.protocol.includes('srv'); } get hosts(): string[] { @@ -269,12 +252,12 @@ export class ConnectionString extends URLWithoutHost { } toString(): string { - return super.toString().replace(DUMMY_HOSTNAME, this.hosts.join(",")); + return super.toString().replace(DUMMY_HOSTNAME, this.hosts.join(',')); } clone(): ConnectionString { return new ConnectionString(this.toString(), { - looseValidation: true, + looseValidation: true }); } @@ -284,12 +267,11 @@ export class ConnectionString extends URLWithoutHost { typedSearchParams>() { const _sametype = - (false as true) && - new (caseInsenstiveURLSearchParams(URLSearchParams))(); + (false as true) && new (caseInsenstiveURLSearchParams(URLSearchParams))(); return this.searchParams as unknown as typeof _sametype; } - [Symbol.for("nodejs.util.inspect.custom")](): any { + [Symbol.for('nodejs.util.inspect.custom')](): any { const { href, origin, @@ -300,7 +282,7 @@ export class ConnectionString extends URLWithoutHost { pathname, search, searchParams, - hash, + hash } = this; return { href, @@ -312,7 +294,7 @@ export class ConnectionString extends URLWithoutHost { pathname, search, searchParams, - hash, + hash }; } } @@ -322,27 +304,24 @@ export class ConnectionString extends URLWithoutHost { * readPreferenceTags connection string parameters. */ export class CommaAndColonSeparatedRecord< - K extends Record = Record, + K extends Record = Record > extends CaseInsensitiveMap { constructor(from?: string | null) { super(); - for (const entry of (from ?? "").split(",")) { + for (const entry of (from ?? '').split(',')) { if (!entry) continue; - const colonIndex = entry.indexOf(":"); + const colonIndex = entry.indexOf(':'); // Use .set() to properly account for case insensitivity if (colonIndex === -1) { - this.set(entry as keyof K & string, ""); + this.set(entry as keyof K & string, ''); } else { - this.set( - entry.slice(0, colonIndex) as keyof K & string, - entry.slice(colonIndex + 1), - ); + this.set(entry.slice(0, colonIndex) as keyof K & string, entry.slice(colonIndex + 1)); } } } toString(): string { - return [...this].map((entry) => entry.join(":")).join(","); + return [...this].map(entry => entry.join(':')).join(','); } } diff --git a/src/redact.ts b/src/redact.ts index 7a3ced1..4897c4d 100644 --- a/src/redact.ts +++ b/src/redact.ts @@ -1,4 +1,4 @@ -import ConnectionString, { CommaAndColonSeparatedRecord } from "./index"; +import ConnectionString, { CommaAndColonSeparatedRecord } from './index'; export interface ConnectionStringRedactionOptions { redactUsernames?: boolean; @@ -7,44 +7,42 @@ export interface ConnectionStringRedactionOptions { export function redactValidConnectionString( inputUrl: Readonly, - options?: ConnectionStringRedactionOptions, + options?: ConnectionStringRedactionOptions ): ConnectionString { const url = inputUrl.clone(); - const replacementString = options?.replacementString ?? "_credentials_"; + const replacementString = options?.replacementString ?? '_credentials_'; const redactUsernames = options?.redactUsernames ?? true; if ((url.username || url.password) && redactUsernames) { url.username = replacementString; - url.password = ""; + url.password = ''; } else if (url.password) { url.password = replacementString; } - if (url.searchParams.has("authMechanismProperties")) { - const props = new CommaAndColonSeparatedRecord( - url.searchParams.get("authMechanismProperties"), - ); - if (props.get("AWS_SESSION_TOKEN")) { - props.set("AWS_SESSION_TOKEN", replacementString); - url.searchParams.set("authMechanismProperties", props.toString()); + if (url.searchParams.has('authMechanismProperties')) { + const props = new CommaAndColonSeparatedRecord(url.searchParams.get('authMechanismProperties')); + if (props.get('AWS_SESSION_TOKEN')) { + props.set('AWS_SESSION_TOKEN', replacementString); + url.searchParams.set('authMechanismProperties', props.toString()); } } - if (url.searchParams.has("tlsCertificateKeyFilePassword")) { - url.searchParams.set("tlsCertificateKeyFilePassword", replacementString); + if (url.searchParams.has('tlsCertificateKeyFilePassword')) { + url.searchParams.set('tlsCertificateKeyFilePassword', replacementString); } - if (url.searchParams.has("proxyUsername") && redactUsernames) { - url.searchParams.set("proxyUsername", replacementString); + if (url.searchParams.has('proxyUsername') && redactUsernames) { + url.searchParams.set('proxyUsername', replacementString); } - if (url.searchParams.has("proxyPassword")) { - url.searchParams.set("proxyPassword", replacementString); + if (url.searchParams.has('proxyPassword')) { + url.searchParams.set('proxyPassword', replacementString); } return url; } export function redactConnectionString( uri: string, - options?: ConnectionStringRedactionOptions, + options?: ConnectionStringRedactionOptions ): string { - const replacementString = options?.replacementString ?? ""; + const replacementString = options?.replacementString ?? ''; const redactUsernames = options?.redactUsernames ?? true; let parsed: ConnectionString | undefined; @@ -56,7 +54,7 @@ export function redactConnectionString( if (parsed) { // If we can parse the connection string, use the more precise // redaction logic. - options = { ...options, replacementString: "___credentials___" }; + options = { ...options, replacementString: '___credentials___' }; return parsed .redact(options) .toString() @@ -68,22 +66,15 @@ export function redactConnectionString( const R = replacementString; // alias for conciseness const replacements: ((uri: string) => string)[] = [ // Username and password - (uri) => - uri.replace( - redactUsernames ? /(\/\/)(.*)(@)/g : /(\/\/[^@]*:)(.*)(@)/g, - `$1${R}$3`, - ), + uri => uri.replace(redactUsernames ? /(\/\/)(.*)(@)/g : /(\/\/[^@]*:)(.*)(@)/g, `$1${R}$3`), // AWS IAM Session Token as part of query parameter - (uri) => uri.replace(/(AWS_SESSION_TOKEN(:|%3A))([^,&]+)/gi, `$1${R}`), + uri => uri.replace(/(AWS_SESSION_TOKEN(:|%3A))([^,&]+)/gi, `$1${R}`), // tlsCertificateKeyFilePassword query parameter - (uri) => uri.replace(/(tlsCertificateKeyFilePassword=)([^&]+)/gi, `$1${R}`), + uri => uri.replace(/(tlsCertificateKeyFilePassword=)([^&]+)/gi, `$1${R}`), // proxyUsername query parameter - (uri) => - redactUsernames - ? uri.replace(/(proxyUsername=)([^&]+)/gi, `$1${R}`) - : uri, + uri => (redactUsernames ? uri.replace(/(proxyUsername=)([^&]+)/gi, `$1${R}`) : uri), // proxyPassword query parameter - (uri) => uri.replace(/(proxyPassword=)([^&]+)/gi, `$1${R}`), + uri => uri.replace(/(proxyPassword=)([^&]+)/gi, `$1${R}`) ]; for (const replacer of replacements) { uri = replacer(uri); diff --git a/test/index.ts b/test/index.ts index e60245f..1b1ce2c 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,310 +1,302 @@ -import { expect } from "chai"; -import ConnectionString, { CommaAndColonSeparatedRecord } from "../"; +import { expect } from 'chai'; +import ConnectionString, { CommaAndColonSeparatedRecord } from '../'; -describe("ConnectionString", () => { - context("with valid URIs", () => { +describe('ConnectionString', () => { + context('with valid URIs', () => { for (const { uri, match } of [ { - uri: "mongodb://localhost/", + uri: 'mongodb://localhost/', match: { - href: "mongodb://localhost/", - protocol: "mongodb:", - username: "", - password: "", - pathname: "/", - search: "", - hash: "", + href: 'mongodb://localhost/', + protocol: 'mongodb:', + username: '', + password: '', + pathname: '/', + search: '', + hash: '', isSRV: false, - hosts: ["localhost"], - }, + hosts: ['localhost'] + } }, { - uri: "mongodb+srv://localhost", + uri: 'mongodb+srv://localhost', match: { - href: "mongodb+srv://localhost/", - protocol: "mongodb+srv:", - username: "", - password: "", - pathname: "/", - search: "", - hash: "", + href: 'mongodb+srv://localhost/', + protocol: 'mongodb+srv:', + username: '', + password: '', + pathname: '/', + search: '', + hash: '', isSRV: true, - hosts: ["localhost"], - }, + hosts: ['localhost'] + } }, { - uri: "mongodb+srv://cat:meow@localhost/", + uri: 'mongodb+srv://cat:meow@localhost/', match: { - href: "mongodb+srv://cat:meow@localhost/", - protocol: "mongodb+srv:", - username: "cat", - password: "meow", - pathname: "/", - search: "", - hash: "", + href: 'mongodb+srv://cat:meow@localhost/', + protocol: 'mongodb+srv:', + username: 'cat', + password: 'meow', + pathname: '/', + search: '', + hash: '', isSRV: true, - hosts: ["localhost"], - }, + hosts: ['localhost'] + } }, { - uri: "mongodb://cat:meow@localhost:12345/db", + uri: 'mongodb://cat:meow@localhost:12345/db', match: { - href: "mongodb://cat:meow@localhost:12345/db", - protocol: "mongodb:", - username: "cat", - password: "meow", - pathname: "/db", - search: "", - hash: "", + href: 'mongodb://cat:meow@localhost:12345/db', + protocol: 'mongodb:', + username: 'cat', + password: 'meow', + pathname: '/db', + search: '', + hash: '', isSRV: false, - hosts: ["localhost:12345"], - }, + hosts: ['localhost:12345'] + } }, { - uri: "mongodb://localhost:12345,anotherHost/?directConnection=true", + uri: 'mongodb://localhost:12345,anotherHost/?directConnection=true', match: { - href: "mongodb://localhost:12345,anotherHost/?directConnection=true", - protocol: "mongodb:", - username: "", - password: "", - pathname: "/", - search: "?directConnection=true", - hash: "", + href: 'mongodb://localhost:12345,anotherHost/?directConnection=true', + protocol: 'mongodb:', + username: '', + password: '', + pathname: '/', + search: '?directConnection=true', + hash: '', isSRV: false, - hosts: ["localhost:12345", "anotherHost"], - }, + hosts: ['localhost:12345', 'anotherHost'] + } }, { - uri: "mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@", + uri: 'mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@', match: { - href: "mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@", - protocol: "mongodb:", - username: "database-meow", - password: "", - pathname: "/", + href: 'mongodb://database-meow@database-haha.mongo.blah.blah.com:8888/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@', + protocol: 'mongodb:', + username: 'database-meow', + password: '', + pathname: '/', search: - "?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@", - hash: "", + '?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@database-haha@', + hash: '', isSRV: false, - hosts: ["database-haha.mongo.blah.blah.com:8888"], - }, - }, + hosts: ['database-haha.mongo.blah.blah.com:8888'] + } + } ]) { it(`parses ${uri} correctly`, () => { const cs = new ConnectionString(uri); - for (const key of Object.keys(match) as (keyof typeof cs & - keyof typeof match)[]) { + for (const key of Object.keys(match) as (keyof typeof cs & keyof typeof match)[]) { expect(cs[key]).to.deep.equal(match[key]); } }); } }); - context("with an invalid schema on URI", () => { - it("throws an error mentioning the invalid schema", () => { + context('with an invalid schema on URI', () => { + it('throws an error mentioning the invalid schema', () => { try { // eslint-disable-next-line no-new - new ConnectionString("totallynotamongodb://outerspace"); + new ConnectionString('totallynotamongodb://outerspace'); } catch (err) { expect((err as Error).message).to.equal( - 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"', + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' ); - expect((err as Error).name).to.equal("MongoParseError"); + expect((err as Error).name).to.equal('MongoParseError'); return; } - expect.fail("missed exception"); + expect.fail('missed exception'); }); }); - context("with invalid URIs", () => { + context('with invalid URIs', () => { for (const uri of [ - "", - "//", - "//@/", - "mongodb://", - "mongodb://@localhost/", - "mongodb://:@localhost/", - "mongodb://:pass@localhost/", - "mongodb://%a@localhost/", - "mongodb://:%a@localhost/", - "mongodb://a[@localhost/", - "mongodb://a:[@localhost/", - "mongodb+srv://a,b,c/", - "mongodb+srv://a:12345/", - "mongodbabc://localhost", - "totallynotamongodb://localhost", - "mongodb+srv://Y:X@", + '', + '//', + '//@/', + 'mongodb://', + 'mongodb://@localhost/', + 'mongodb://:@localhost/', + 'mongodb://:pass@localhost/', + 'mongodb://%a@localhost/', + 'mongodb://:%a@localhost/', + 'mongodb://a[@localhost/', + 'mongodb://a:[@localhost/', + 'mongodb+srv://a,b,c/', + 'mongodb+srv://a:12345/', + 'mongodbabc://localhost', + 'totallynotamongodb://localhost', + 'mongodb+srv://Y:X@' ]) { it(`parsing ${uri} throws an MongoParseError`, () => { try { // eslint-disable-next-line no-new new ConnectionString(uri); } catch (err) { - expect((err as Error).name).to.equal("MongoParseError"); + expect((err as Error).name).to.equal('MongoParseError'); return; } - expect.fail("missed exception"); + expect.fail('missed exception'); }); } }); - context("after modifications", () => { - it("allows changing hosts", () => { - const cs = new ConnectionString("mongodb://localhost"); - expect(cs.hosts).to.deep.equal(["localhost"]); + context('after modifications', () => { + it('allows changing hosts', () => { + const cs = new ConnectionString('mongodb://localhost'); + expect(cs.hosts).to.deep.equal(['localhost']); - cs.hosts.push("localhost2"); - expect(cs.hosts).to.deep.equal(["localhost", "localhost2"]); - expect(cs.toString()).to.equal("mongodb://localhost,localhost2/"); + cs.hosts.push('localhost2'); + expect(cs.hosts).to.deep.equal(['localhost', 'localhost2']); + expect(cs.toString()).to.equal('mongodb://localhost,localhost2/'); - cs.hosts = ["a", "b", "c"]; - expect(cs.hosts).to.deep.equal(["a", "b", "c"]); - expect(cs.toString()).to.equal("mongodb://a,b,c/"); + cs.hosts = ['a', 'b', 'c']; + expect(cs.hosts).to.deep.equal(['a', 'b', 'c']); + expect(cs.toString()).to.equal('mongodb://a,b,c/'); }); - it("performs case-insensitive matches on connection options", () => { - const cs = new ConnectionString( - "mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=100", - ); - cs.searchParams.set("serverSelectionTimeoutMS", "200"); - cs.searchParams.append("serverSelectionTimeoutMS", "300"); + it('performs case-insensitive matches on connection options', () => { + const cs = new ConnectionString('mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=100'); + cs.searchParams.set('serverSelectionTimeoutMS', '200'); + cs.searchParams.append('serverSelectionTimeoutMS', '300'); expect(cs.toString()).to.equal( - "mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=200&SERVERSELECTIONTIMEOUTMS=300", + 'mongodb://localhost/?SERVERSELECTIONTIMEOUTMS=200&SERVERSELECTIONTIMEOUTMS=300' ); - expect(cs.searchParams.has("serverSelectionTimeoutMS")).to.equal(true); - expect(cs.searchParams.has("SERVERSELECTIONTIMEOUTMS")).to.equal(true); - expect(cs.searchParams.get("serverSelectionTimeoutMS")).to.equal("200"); - expect(cs.searchParams.getAll("serverSelectionTimeoutMS")).to.deep.equal([ - "200", - "300", - ]); + expect(cs.searchParams.has('serverSelectionTimeoutMS')).to.equal(true); + expect(cs.searchParams.has('SERVERSELECTIONTIMEOUTMS')).to.equal(true); + expect(cs.searchParams.get('serverSelectionTimeoutMS')).to.equal('200'); + expect(cs.searchParams.getAll('serverSelectionTimeoutMS')).to.deep.equal(['200', '300']); - cs.searchParams.delete("serverSelectionTimeoutMS"); - expect(cs.searchParams.has("serverSelectionTimeoutMS")).to.equal(false); - expect(cs.searchParams.has("SERVERSELECTIONTIMEOUTMS")).to.equal(false); + cs.searchParams.delete('serverSelectionTimeoutMS'); + expect(cs.searchParams.has('serverSelectionTimeoutMS')).to.equal(false); + expect(cs.searchParams.has('SERVERSELECTIONTIMEOUTMS')).to.equal(false); }); }); - context("cloning", () => { - it("can make copies of ConnectionString instances", () => { - const cs = new ConnectionString("mongodb://localhost"); - expect(cs.toString()).to.equal("mongodb://localhost/"); - expect(cs.clone().toString()).to.equal("mongodb://localhost/"); + context('cloning', () => { + it('can make copies of ConnectionString instances', () => { + const cs = new ConnectionString('mongodb://localhost'); + expect(cs.toString()).to.equal('mongodb://localhost/'); + expect(cs.clone().toString()).to.equal('mongodb://localhost/'); }); }); - context("URL methods that do not apply to connection strings as-is", () => { - it("throws/returns dummy values", () => { + context('URL methods that do not apply to connection strings as-is', () => { + it('throws/returns dummy values', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const cs: any = new ConnectionString("mongodb://localhost"); - expect(cs.host).not.to.equal("localhost"); - expect(cs.hostname).not.to.equal("localhost"); - expect(cs.port).to.equal(""); - expect(cs.href).to.equal("mongodb://localhost/"); + const cs: any = new ConnectionString('mongodb://localhost'); + expect(cs.host).not.to.equal('localhost'); + expect(cs.hostname).not.to.equal('localhost'); + expect(cs.port).to.equal(''); + expect(cs.href).to.equal('mongodb://localhost/'); expect(() => { - cs.host = "abc"; + cs.host = 'abc'; }).to.throw(Error); expect(() => { - cs.hostname = "abc"; + cs.hostname = 'abc'; }).to.throw(Error); expect(() => { - cs.port = "1000"; + cs.port = '1000'; }).to.throw(Error); expect(() => { - cs.href = "mongodb://localhost"; + cs.href = 'mongodb://localhost'; }).to.throw(Error); }); }); - context("with loose validation", () => { - it("allows odd connection strings", () => { + context('with loose validation', () => { + it('allows odd connection strings', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const cs: any = new ConnectionString("mongodb://:password@x", { - looseValidation: true, + const cs: any = new ConnectionString('mongodb://:password@x', { + looseValidation: true }); - expect(cs.username).to.equal(""); - expect(cs.password).to.equal("password"); - expect(cs.port).to.equal(""); - expect(cs.href).to.equal("mongodb://:password@x/"); + expect(cs.username).to.equal(''); + expect(cs.password).to.equal('password'); + expect(cs.port).to.equal(''); + expect(cs.href).to.equal('mongodb://:password@x/'); }); - it("throws good error messages for invalid URLs", () => { + it('throws good error messages for invalid URLs', () => { try { - new ConnectionString("-://:password@x", { looseValidation: true }); - expect.fail("missed exception"); + new ConnectionString('-://:password@x', { looseValidation: true }); + expect.fail('missed exception'); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { expect(err.message).to.equal( - 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"', + 'Invalid scheme, expected connection string to start with "mongodb://" or "mongodb+srv://"' ); } }); }); }); -describe("CommaAndColonSeparatedRecord", () => { - it("creates an empty map for empty input", () => { - expect(new CommaAndColonSeparatedRecord("").size).to.equal(0); +describe('CommaAndColonSeparatedRecord', () => { + it('creates an empty map for empty input', () => { + expect(new CommaAndColonSeparatedRecord('').size).to.equal(0); expect(new CommaAndColonSeparatedRecord().size).to.equal(0); expect(new CommaAndColonSeparatedRecord(null).size).to.equal(0); expect(new CommaAndColonSeparatedRecord(undefined).size).to.equal(0); }); - it("returns an empty string for empty input", () => { - expect(new CommaAndColonSeparatedRecord("").toString()).to.equal(""); - expect(new CommaAndColonSeparatedRecord().toString()).to.equal(""); - expect(new CommaAndColonSeparatedRecord(null).toString()).to.equal(""); - expect(new CommaAndColonSeparatedRecord(undefined).toString()).to.equal(""); + it('returns an empty string for empty input', () => { + expect(new CommaAndColonSeparatedRecord('').toString()).to.equal(''); + expect(new CommaAndColonSeparatedRecord().toString()).to.equal(''); + expect(new CommaAndColonSeparatedRecord(null).toString()).to.equal(''); + expect(new CommaAndColonSeparatedRecord(undefined).toString()).to.equal(''); }); - it("allows getting entries", () => { - const record = new CommaAndColonSeparatedRecord("A:B,C:D"); - expect(record.toString()).to.equal("A:B,C:D"); - expect(record.get("A")).to.equal("B"); - expect(record.get("C")).to.equal("D"); - expect(record.get("foo")).to.equal(undefined); + it('allows getting entries', () => { + const record = new CommaAndColonSeparatedRecord('A:B,C:D'); + expect(record.toString()).to.equal('A:B,C:D'); + expect(record.get('A')).to.equal('B'); + expect(record.get('C')).to.equal('D'); + expect(record.get('foo')).to.equal(undefined); }); - it("allows setting entries", () => { - const record = new CommaAndColonSeparatedRecord("A:B,C:D"); - record.set("A", "0"); - record.set("E", "1"); - expect(record.toString()).to.equal("A:0,C:D,E:1"); + it('allows setting entries', () => { + const record = new CommaAndColonSeparatedRecord('A:B,C:D'); + record.set('A', '0'); + record.set('E', '1'); + expect(record.toString()).to.equal('A:0,C:D,E:1'); }); - it("accepts cases of multiple-colon entries", () => { - const record = new CommaAndColonSeparatedRecord("A:B:C,D"); - expect(record.toString()).to.equal("A:B:C,D:"); - expect(record.get("A")).to.equal("B:C"); - expect(record.get("D")).to.equal(""); + it('accepts cases of multiple-colon entries', () => { + const record = new CommaAndColonSeparatedRecord('A:B:C,D'); + expect(record.toString()).to.equal('A:B:C,D:'); + expect(record.get('A')).to.equal('B:C'); + expect(record.get('D')).to.equal(''); }); - it("is case-insensitive", () => { - const record = new CommaAndColonSeparatedRecord("foo:bar,FOO:BAR"); - expect(record.toString()).to.equal("foo:BAR"); - expect(record.get("FOO")).to.equal("BAR"); - expect(record.get("foo")).to.equal("BAR"); - record.set("FOO", "baz"); - expect(record.toString()).to.equal("foo:baz"); + it('is case-insensitive', () => { + const record = new CommaAndColonSeparatedRecord('foo:bar,FOO:BAR'); + expect(record.toString()).to.equal('foo:BAR'); + expect(record.get('FOO')).to.equal('BAR'); + expect(record.get('foo')).to.equal('BAR'); + record.set('FOO', 'baz'); + expect(record.toString()).to.equal('foo:baz'); }); }); -describe("TypeScript support", () => { - it("allows specifying typed search parameters", () => { - const cs = new ConnectionString("mongodb://localhost/?tls=true&tls2=false"); +describe('TypeScript support', () => { + it('allows specifying typed search parameters', () => { + const cs = new ConnectionString('mongodb://localhost/?tls=true&tls2=false'); const sp = cs.typedSearchParams<{ tls: string }>(); - expect(sp.get("tls")).to.equal("true"); + expect(sp.get('tls')).to.equal('true'); // @ts-expect-error should fail - expect(sp.get("tls2")).to.equal("false"); + expect(sp.get('tls2')).to.equal('false'); }); - it("allows specifying typed comma-and-colon-separated-record types", () => { - const record = new CommaAndColonSeparatedRecord<{ foo: string }>( - "foo:bar,baz:quux", - ); - expect(record.get("foo")).to.equal("bar"); + it('allows specifying typed comma-and-colon-separated-record types', () => { + const record = new CommaAndColonSeparatedRecord<{ foo: string }>('foo:bar,baz:quux'); + expect(record.get('foo')).to.equal('bar'); // @ts-expect-error should fail - expect(record.get("baz")).to.equal("quux"); + expect(record.get('baz')).to.equal('quux'); }); }); diff --git a/test/redact.ts b/test/redact.ts index 36e6daf..f7e7ff4 100644 --- a/test/redact.ts +++ b/test/redact.ts @@ -1,126 +1,118 @@ -import { expect } from "chai"; -import { redactConnectionString } from "../"; +import { expect } from 'chai'; +import { redactConnectionString } from '../'; -describe("redact credentials", () => { - for (const protocol of ["mongodb", "mongodb+srv", "+invalid+"]) { +describe('redact credentials', () => { + for (const protocol of ['mongodb', 'mongodb+srv', '+invalid+']) { context(`when url contains credentials (protocol: ${protocol})`, () => { - it("returns the in output instead of password", () => { + it('returns the in output instead of password', () => { expect( redactConnectionString( - `${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin`, - ), - ).to.equal( - `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin`, - ); + `${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin` + ) + ).to.equal(`${protocol}://@cats-data-sets-e08dy.mongodb.net/admin`); }); - it("returns the keeping the username if desired", () => { + it('returns the keeping the username if desired', () => { expect( redactConnectionString( `${protocol}://admin:catsc@tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin`, - { redactUsernames: false }, - ), - ).to.equal( - `${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin`, - ); + { redactUsernames: false } + ) + ).to.equal(`${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin`); }); - it("returns the in output instead of IAM session token", () => { + it('returns the in output instead of IAM session token', () => { expect( redactConnectionString( - `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken,else%3Amiau¶m=true`, - ).replace(/%2C/g, ","), + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken,else%3Amiau¶m=true` + ).replace(/%2C/g, ',') ).to.equal( - `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A,else%3Amiau¶m=true`, + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A,else%3Amiau¶m=true` ); expect( redactConnectionString( - `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true`, - ), + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true` + ) ).to.equal( - `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true`, + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true` ); expect( redactConnectionString( - `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken`, - ), + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken` + ) ).to.equal( - `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A`, + `${protocol}://cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A` ); }); - it("returns the in output instead of password and IAM session token", () => { + it('returns the in output instead of password and IAM session token', () => { expect( redactConnectionString( - `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true`, - ), + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3Asampletoken¶m=true` + ) ).to.equal( - `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true`, + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN%3A¶m=true` ); }); - it("returns the in output instead of tlsCertificateKeyFilePassword", () => { + it('returns the in output instead of tlsCertificateKeyFilePassword', () => { expect( redactConnectionString( - `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=p4ssw0rd`, - ), + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=p4ssw0rd` + ) ).to.equal( - `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=`, + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?tls=true&tlsCertificateKeyFilePassword=` ); }); - it("returns the in output instead of proxyPassword and proxyUsername", () => { + it('returns the in output instead of proxyPassword and proxyUsername', () => { expect( redactConnectionString( - `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar¶m=true`, - ), + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar¶m=true` + ) ).to.equal( - `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=¶m=true`, + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=¶m=true` ); expect( redactConnectionString( - `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, - ), + `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar` + ) ).to.equal( - `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=`, + `${protocol}://@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=&proxyPassword=` ); expect( redactConnectionString( `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, - { redactUsernames: false }, - ), + { redactUsernames: false } + ) ).to.equal( - `${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=`, + `${protocol}://admin:@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=` ); expect( redactConnectionString( `${protocol}://admin:tscat3ca1s@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=foo&proxyPassword=bar`, - { replacementString: "****" }, - ), + { replacementString: '****' } + ) ).to.equal( - `${protocol}://****@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=****&proxyPassword=****`, + `${protocol}://****@cats-data-sets-e08dy.mongodb.net/admin?proxyUsername=****&proxyPassword=****` ); }); - it("redacts credentials when username is empty", () => { - expect( - redactConnectionString(`${protocol}://:password@mongodb.net/`), - ).to.equal(`${protocol}://@mongodb.net/`); + it('redacts credentials when username is empty', () => { + expect(redactConnectionString(`${protocol}://:password@mongodb.net/`)).to.equal( + `${protocol}://@mongodb.net/` + ); }); }); - context("when url contains no credentials", () => { - it("does not alter input", () => { - expect( - redactConnectionString(`${protocol}://127.0.0.1:27017/`), - ).to.equal(`${protocol}://127.0.0.1:27017/`); - expect( - redactConnectionString( - `${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`, - ), - ).to.equal( - `${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`, + context('when url contains no credentials', () => { + it('does not alter input', () => { + expect(redactConnectionString(`${protocol}://127.0.0.1:27017/`)).to.equal( + `${protocol}://127.0.0.1:27017/` ); + expect( + redactConnectionString(`${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`) + ).to.equal(`${protocol}://127.0.0.1:27017/?authMechanismProperties=IGNORE:ME`); }); }); }