Skip to content

Commit 5560e2b

Browse files
committed
feat(NODE-3920)!: validate options are not repeated in connection string
1 parent ada1f75 commit 5560e2b

File tree

4 files changed

+43
-34
lines changed

4 files changed

+43
-34
lines changed

src/connection_string.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ function getUIntFromOptions(name: string, value: unknown): number {
205205
}
206206

207207
function* entriesFromString(value: string): Generator<[string, string]> {
208+
if (value === '') {
209+
return;
210+
}
208211
const keyValuePairs = value.split(',');
209212
for (const keyValue of keyValuePairs) {
210213
const [key, value] = keyValue.split(/:(.*)/);
@@ -291,9 +294,17 @@ export function parseOptions(
291294
}
292295

293296
for (const key of url.searchParams.keys()) {
294-
const values = [...url.searchParams.getAll(key)];
297+
const values = url.searchParams.getAll(key);
298+
299+
const isReadPreferenceTags = /readPreferenceTags/i.test(key);
300+
301+
if (!isReadPreferenceTags && values.length > 1) {
302+
throw new MongoInvalidArgumentError(
303+
`URI option "${key}" cannot appear more than once in the connection string`
304+
);
305+
}
295306

296-
if (values.includes('')) {
307+
if (!isReadPreferenceTags && values.includes('')) {
297308
throw new MongoAPIError('URI cannot contain options with no value');
298309
}
299310

test/unit/connection_string.spec.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const skipTests = [
1111

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

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

25-
executeUriValidationTest(
26-
test,
27-
testsThatDoNotThrowOnWarn.some(t => t === test.description)
28-
);
24+
executeUriValidationTest(test);
2925
});
3026
}
3127
});

test/unit/connection_string.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,21 +122,18 @@ describe('Connection String', function () {
122122

123123
describe('readPreferenceTags option', function () {
124124
context('when the option is passed in the uri', () => {
125-
it('should throw an error if no value is passed for readPreferenceTags', () => {
126-
expect(() => parseOptions('mongodb://hostname?readPreferenceTags=')).to.throw(
127-
MongoAPIError
128-
);
129-
});
130125
it('should parse a single read preference tag', () => {
131126
const options = parseOptions('mongodb://hostname?readPreferenceTags=bar:foo');
132127
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }]);
133128
});
129+
134130
it('should parse multiple readPreferenceTags', () => {
135131
const options = parseOptions(
136132
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar'
137133
);
138134
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]);
139135
});
136+
140137
it('should parse multiple readPreferenceTags for the same key', () => {
141138
const options = parseOptions(
142139
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=bar:banana&readPreferenceTags=baz:bar'
@@ -147,6 +144,19 @@ describe('Connection String', function () {
147144
{ baz: 'bar' }
148145
]);
149146
});
147+
148+
it('should parse multiple and empty readPreferenceTags', () => {
149+
const options = parseOptions(
150+
'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar&readPreferenceTags='
151+
);
152+
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }, {}]);
153+
});
154+
155+
it('will set "__proto__" as own property on readPreferenceTag', () => {
156+
const options = parseOptions('mongodb://hostname?readPreferenceTags=__proto__:foo');
157+
expect(options.readPreference.tags?.[0]).to.have.own.property('__proto__', 'foo');
158+
expect(Object.getPrototypeOf(options.readPreference.tags?.[0])).to.be.null;
159+
});
150160
});
151161

152162
context('when the option is passed in the options object', () => {
@@ -156,12 +166,14 @@ describe('Connection String', function () {
156166
});
157167
expect(options.readPreference.tags).to.deep.equal([]);
158168
});
169+
159170
it('should parse a single readPreferenceTags object', () => {
160171
const options = parseOptions('mongodb://hostname?', {
161172
readPreferenceTags: [{ bar: 'foo' }]
162173
});
163174
expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }]);
164175
});
176+
165177
it('should parse multiple readPreferenceTags', () => {
166178
const options = parseOptions('mongodb://hostname?', {
167179
readPreferenceTags: [{ bar: 'foo' }, { baz: 'bar' }]
@@ -493,6 +505,18 @@ describe('Connection String', function () {
493505
);
494506
});
495507

508+
it('throws an error for repeated options that can only appear once', function () {
509+
// At the time of writing, readPreferenceTags is the only options that can be repeated
510+
expect(() => parseOptions('mongodb://localhost/?compressors=zstd&compressors=zstd')).to.throw(
511+
MongoInvalidArgumentError,
512+
/cannot appear more than once/
513+
);
514+
expect(() => parseOptions('mongodb://localhost/?tls=true&tls=true')).to.throw(
515+
MongoInvalidArgumentError,
516+
/cannot appear more than once/
517+
);
518+
});
519+
496520
it('should validate authMechanism', function () {
497521
expect(() => parseOptions('mongodb://localhost/?authMechanism=DOGS')).to.throw(
498522
MongoParseError,

test/unit/mongo_client.test.js

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -401,18 +401,6 @@ describe('MongoOptions', function () {
401401
});
402402
});
403403

404-
it('throws an error if multiple tls parameters are not all set to the same value', () => {
405-
expect(() => parseOptions('mongodb://localhost?tls=true&tls=false')).to.throw(
406-
'All values of tls/ssl must be the same.'
407-
);
408-
});
409-
410-
it('throws an error if multiple ssl parameters are not all set to the same value', () => {
411-
expect(() => parseOptions('mongodb://localhost?ssl=true&ssl=false')).to.throw(
412-
'All values of tls/ssl must be the same.'
413-
);
414-
});
415-
416404
it('throws an error if tls and ssl parameters are not all set to the same value', () => {
417405
expect(() => parseOptions('mongodb://localhost?tls=true&ssl=false')).to.throw(
418406
'All values of tls/ssl must be the same.'
@@ -422,16 +410,6 @@ describe('MongoOptions', function () {
422410
);
423411
});
424412

425-
it('correctly sets tls if multiple tls parameters are all set to the same value', () => {
426-
expect(parseOptions('mongodb://localhost?tls=true&tls=true')).to.have.property('tls', true);
427-
expect(parseOptions('mongodb://localhost?tls=false&tls=false')).to.have.property('tls', false);
428-
});
429-
430-
it('correctly sets tls if multiple ssl parameters are all set to the same value', () => {
431-
expect(parseOptions('mongodb://localhost?ssl=true&ssl=true')).to.have.property('tls', true);
432-
expect(parseOptions('mongodb://localhost?ssl=false&ssl=false')).to.have.property('tls', false);
433-
});
434-
435413
it('correctly sets tls if tls and ssl parameters are all set to the same value', () => {
436414
expect(parseOptions('mongodb://localhost?ssl=true&tls=true')).to.have.property('tls', true);
437415
expect(parseOptions('mongodb://localhost?ssl=false&tls=false')).to.have.property('tls', false);

0 commit comments

Comments
 (0)