Skip to content

Commit

Permalink
feat(NODE-3920)!: validate options are not repeated in connection string
Browse files Browse the repository at this point in the history
  • Loading branch information
nbbeeken committed Jul 28, 2023
1 parent ada1f75 commit 5759f84
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 2 deletions.
15 changes: 13 additions & 2 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ function getUIntFromOptions(name: string, value: unknown): number {
}

function* entriesFromString(value: string): Generator<[string, string]> {
if (value === '') {
return;
}
const keyValuePairs = value.split(',');
for (const keyValue of keyValuePairs) {
const [key, value] = keyValue.split(/:(.*)/);
Expand Down Expand Up @@ -291,9 +294,17 @@ export function parseOptions(
}

for (const key of url.searchParams.keys()) {
const values = [...url.searchParams.getAll(key)];
const values = url.searchParams.getAll(key);

const isReadPreferenceTags = /readPreferenceTags/i.test(key);

if (!isReadPreferenceTags && values.length > 1) {
throw new MongoInvalidArgumentError(
`URI option "${key}" cannot appear more than once in the connection string`
);
}

if (values.includes('')) {
if (!isReadPreferenceTags && values.includes('')) {
throw new MongoAPIError('URI cannot contain options with no value');
}

Expand Down
27 changes: 27 additions & 0 deletions test/unit/connection_string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ describe('Connection String', function () {
{ baz: 'bar' }
]);
});

it('should parse multiple and empty readPreferenceTags', () => {
const options = parseOptions(
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar&readPreferenceTags='
);
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }, {}]);
});

it('will set "__proto__" as own property on readPreferenceTag', () => {
const options = parseOptions('mongodb://hostname?readPreferenceTags=__proto__:foo');
expect(options.readPreference.tags?.[0]).to.have.own.property('__proto__', 'foo');
expect(Object.getPrototypeOf(options.readPreference.tags?.[0])).to.be.null;
});
});

context('when the option is passed in the options object', () => {
Expand All @@ -156,12 +169,14 @@ describe('Connection String', function () {
});
expect(options.readPreference.tags).to.deep.equal([]);
});

it('should parse a single readPreferenceTags object', () => {
const options = parseOptions('mongodb://hostname?', {
readPreferenceTags: [{ bar: 'foo' }]
});
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }]);
});

it('should parse multiple readPreferenceTags', () => {
const options = parseOptions('mongodb://hostname?', {
readPreferenceTags: [{ bar: 'foo' }, { baz: 'bar' }]
Expand Down Expand Up @@ -493,6 +508,18 @@ describe('Connection String', function () {
);
});

it('throws an error for repeated options that can only appear once', function () {
// At the time of writing, readPreferenceTags is the only options that can be repeated
let thrownError;
try {
parseOptions('mongodb://localhost/?compressors=zstd&compressors=zstd');
} catch (error) {
thrownError = error;
}
expect(thrownError).to.be.instanceOf(MongoInvalidArgumentError);
expect(thrownError.message).to.match(/cannot appear more than once/);
});

it('should validate authMechanism', function () {
expect(() => parseOptions('mongodb://localhost/?authMechanism=DOGS')).to.throw(
MongoParseError,
Expand Down

0 comments on commit 5759f84

Please sign in to comment.