Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-3920)!: validate options are not repeated in connection string #3788

Merged
merged 3 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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