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 5560e2b
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 34 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
6 changes: 1 addition & 5 deletions test/unit/connection_string.spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const skipTests = [

describe('Connection String spec tests', function () {
// TODO(NODE-3920): validate repeated options
const testsThatDoNotThrowOnWarn = ['Repeated option keys'];
const suites = loadSpecTests('connection-string');

for (const suite of suites) {
Expand All @@ -22,10 +21,7 @@ describe('Connection String spec tests', function () {
return this.skip();
}

executeUriValidationTest(
test,
testsThatDoNotThrowOnWarn.some(t => t === test.description)
);
executeUriValidationTest(test);
});
}
});
Expand Down
34 changes: 29 additions & 5 deletions test/unit/connection_string.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,21 +122,18 @@ describe('Connection String', function () {

describe('readPreferenceTags option', function () {
context('when the option is passed in the uri', () => {
it('should throw an error if no value is passed for readPreferenceTags', () => {
expect(() => parseOptions('mongodb://hostname?readPreferenceTags=')).to.throw(
MongoAPIError
);
});
it('should parse a single read preference tag', () => {
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&readPreferenceTags=baz:bar'
);
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]);
});

it('should parse multiple readPreferenceTags for the same key', () => {
const options = parseOptions(
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=bar:banana&readPreferenceTags=baz:bar'
Expand All @@ -147,6 +144,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 +166,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 +505,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
expect(() => parseOptions('mongodb://localhost/?compressors=zstd&compressors=zstd')).to.throw(
MongoInvalidArgumentError,
/cannot appear more than once/
);
expect(() => parseOptions('mongodb://localhost/?tls=true&tls=true')).to.throw(
MongoInvalidArgumentError,
/cannot appear more than once/
);
});

it('should validate authMechanism', function () {
expect(() => parseOptions('mongodb://localhost/?authMechanism=DOGS')).to.throw(
MongoParseError,
Expand Down
22 changes: 0 additions & 22 deletions test/unit/mongo_client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,18 +401,6 @@ describe('MongoOptions', function () {
});
});

it('throws an error if multiple tls parameters are not all set to the same value', () => {
expect(() => parseOptions('mongodb://localhost?tls=true&tls=false')).to.throw(
'All values of tls/ssl must be the same.'
);
});

it('throws an error if multiple ssl parameters are not all set to the same value', () => {
expect(() => parseOptions('mongodb://localhost?ssl=true&ssl=false')).to.throw(
'All values of tls/ssl must be the same.'
);
});

it('throws an error if tls and ssl parameters are not all set to the same value', () => {
expect(() => parseOptions('mongodb://localhost?tls=true&ssl=false')).to.throw(
'All values of tls/ssl must be the same.'
Expand All @@ -422,16 +410,6 @@ describe('MongoOptions', function () {
);
});

it('correctly sets tls if multiple tls parameters are all set to the same value', () => {
expect(parseOptions('mongodb://localhost?tls=true&tls=true')).to.have.property('tls', true);
expect(parseOptions('mongodb://localhost?tls=false&tls=false')).to.have.property('tls', false);
});

it('correctly sets tls if multiple ssl parameters are all set to the same value', () => {
expect(parseOptions('mongodb://localhost?ssl=true&ssl=true')).to.have.property('tls', true);
expect(parseOptions('mongodb://localhost?ssl=false&ssl=false')).to.have.property('tls', false);
});

it('correctly sets tls if tls and ssl parameters are all set to the same value', () => {
expect(parseOptions('mongodb://localhost?ssl=true&tls=true')).to.have.property('tls', true);
expect(parseOptions('mongodb://localhost?ssl=false&tls=false')).to.have.property('tls', false);
Expand Down

0 comments on commit 5560e2b

Please sign in to comment.