diff --git a/src/connection_string.ts b/src/connection_string.ts index 9afd992c2fe..92dab49adb0 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -19,7 +19,6 @@ import { ServerApi, ServerApiVersion } from './mongo_client'; -import type { OneOrMore } from './mongo_types'; import { PromiseProvider } from './promise_provider'; import { ReadConcern, ReadConcernLevel } from './read_concern'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; @@ -220,12 +219,7 @@ function getUint(name: string, value: unknown): number { return parsedValue; } -/** Wrap a single value in an array if the value is not an array */ -function toArray(value: OneOrMore): T[] { - return Array.isArray(value) ? value : [value]; -} - -function* entriesFromString(value: string) { +function* entriesFromString(value: string): Generator<[string, string]> { const keyValuePairs = value.split(','); for (const keyValue of keyValuePairs) { const [key, value] = keyValue.split(':'); @@ -333,11 +327,19 @@ export function parseOptions( ]); for (const key of allKeys) { - const values = [objectOptions, urlOptions, DEFAULT_OPTIONS].flatMap(optionsObject => { - const options = optionsObject.get(key) ?? []; - return toArray(options); - }); - + const values = []; + const objectOptionValue = objectOptions.get(key); + if (objectOptionValue != null) { + values.push(objectOptionValue); + } + const urlValue = urlOptions.get(key); + if (urlValue != null) { + values.push(...urlValue); + } + const defaultOptionsValue = DEFAULT_OPTIONS.get(key); + if (defaultOptionsValue != null) { + values.push(defaultOptionsValue); + } allOptions.set(key, values); } @@ -982,9 +984,18 @@ export const OPTIONS = { }, readPreferenceTags: { target: 'readPreference', - transform({ values, options }) { + transform({ + values, + options + }: { + values: Array[]>; + options: MongoClientOptions; + }) { + const tags: Array> = Array.isArray(values[0]) + ? values[0] + : (values as Array); const readPreferenceTags = []; - for (const tag of values) { + for (const tag of tags) { const readPreferenceTag: TagSet = Object.create(null); if (typeof tag === 'string') { for (const [k, v] of entriesFromString(tag)) { diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 989fcd16b4e..fa90c112e0f 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -46,16 +46,138 @@ describe('Connection String', function () { expect(options.hosts[0].port).to.equal(27017); }); - context('readPreferenceTags', function () { - it('should parse multiple readPreferenceTags when passed in the uri', () => { - const options = parseOptions( - 'mongodb://hostname?readPreferenceTags=bar:foo&readPreferenceTags=baz:bar' - ); - expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]); + describe('ca option', () => { + context('when set in the options object', () => { + it('should parse a string', () => { + const options = parseOptions('mongodb://localhost', { + ca: 'hello' + }); + expect(options).to.have.property('ca').to.equal('hello'); + }); + + it('should parse a NodeJS buffer', () => { + const options = parseOptions('mongodb://localhost', { + ca: Buffer.from([1, 2, 3, 4]) + }); + + expect(options) + .to.have.property('ca') + .to.deep.equal(Buffer.from([1, 2, 3, 4])); + }); + + it('should parse arrays with a single element', () => { + const options = parseOptions('mongodb://localhost', { + ca: ['hello'] + }); + expect(options).to.have.property('ca').to.deep.equal(['hello']); + }); + + it('should parse an empty array', () => { + const options = parseOptions('mongodb://localhost', { + ca: [] + }); + expect(options).to.have.property('ca').to.deep.equal([]); + }); + + it('should parse arrays with multiple elements', () => { + const options = parseOptions('mongodb://localhost', { + ca: ['hello', 'world'] + }); + expect(options).to.have.property('ca').to.deep.equal(['hello', 'world']); + }); + }); + + // TODO(NODE-4172): align uri behavior with object options behavior + context('when set in the uri', () => { + it('should parse a string value', () => { + const options = parseOptions('mongodb://localhost?ca=hello', {}); + expect(options).to.have.property('ca').to.equal('hello'); + }); + + it('should throw an error with a buffer value', () => { + const buffer = Buffer.from([1, 2, 3, 4]); + expect(() => { + parseOptions(`mongodb://localhost?ca=${buffer.toString()}`, {}); + }).to.throw(MongoAPIError); + }); + + it('should not parse multiple string values (array of options)', () => { + const options = parseOptions('mongodb://localhost?ca=hello,world', {}); + expect(options).to.have.property('ca').to.equal('hello,world'); + }); + }); + + it('should prioritize options set in the object over those set in the URI', () => { + const options = parseOptions('mongodb://localhost?ca=hello', { + ca: ['world'] + }); + expect(options).to.have.property('ca').to.deep.equal(['world']); }); + }); - it('should parse multiple readPreferenceTags when passed in options object', () => { - const options = parseOptions('mongodb://hostname?', { + 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' + ); + expect(options.readPreference.tags).to.deep.equal([ + { bar: 'foo' }, + { bar: 'banana' }, + { baz: 'bar' } + ]); + }); + }); + + context('when the option is passed in the options object', () => { + it('should not parse an empty readPreferenceTags object', () => { + const options = parseOptions('mongodb://hostname?', { + readPreferenceTags: [] + }); + 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' }] + }); + 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' }, { bar: 'banana' }, { baz: 'bar' }] + }); + expect(options.readPreference.tags).to.deep.equal([ + { bar: 'foo' }, + { bar: 'banana' }, + { baz: 'bar' } + ]); + }); + }); + + it('should prioritize options from the options object over the uri options', () => { + const options = parseOptions('mongodb://hostname?readPreferenceTags=a:b', { readPreferenceTags: [{ bar: 'foo' }, { baz: 'bar' }] }); expect(options.readPreference.tags).to.deep.equal([{ bar: 'foo' }, { baz: 'bar' }]); @@ -174,25 +296,25 @@ describe('Connection String', function () { context('when the options are provided in the URI', function () { context('when the options are equal', function () { context('when both options are true', function () { - const options = parseOptions('mongodb://localhost/?tls=true&ssl=true'); - it('sets the tls option', function () { + const options = parseOptions('mongodb://localhost/?tls=true&ssl=true'); expect(options.tls).to.be.true; }); it('does not set the ssl option', function () { + const options = parseOptions('mongodb://localhost/?tls=true&ssl=true'); expect(options).to.not.have.property('ssl'); }); }); context('when both options are false', function () { - const options = parseOptions('mongodb://localhost/?tls=false&ssl=false'); - it('sets the tls option', function () { + const options = parseOptions('mongodb://localhost/?tls=false&ssl=false'); expect(options.tls).to.be.false; }); it('does not set the ssl option', function () { + const options = parseOptions('mongodb://localhost/?tls=false&ssl=false'); expect(options).to.not.have.property('ssl'); }); }); @@ -210,38 +332,38 @@ describe('Connection String', function () { context('when the options are provided in the options', function () { context('when the options are equal', function () { context('when both options are true', function () { - const options = parseOptions('mongodb://localhost/', { tls: true, ssl: true }); - it('sets the tls option', function () { + const options = parseOptions('mongodb://localhost/', { tls: true, ssl: true }); expect(options.tls).to.be.true; }); it('does not set the ssl option', function () { + const options = parseOptions('mongodb://localhost/', { tls: true, ssl: true }); expect(options).to.not.have.property('ssl'); }); }); context('when both options are false', function () { context('when the URI is an SRV URI', function () { - const options = parseOptions('mongodb+srv://localhost/', { tls: false, ssl: false }); - it('overrides the tls option', function () { + const options = parseOptions('mongodb+srv://localhost/', { tls: false, ssl: false }); expect(options.tls).to.be.false; }); it('does not set the ssl option', function () { + const options = parseOptions('mongodb+srv://localhost/', { tls: false, ssl: false }); expect(options).to.not.have.property('ssl'); }); }); context('when the URI is not SRV', function () { - const options = parseOptions('mongodb://localhost/', { tls: false, ssl: false }); - it('sets the tls option', function () { + const options = parseOptions('mongodb://localhost/', { tls: false, ssl: false }); expect(options.tls).to.be.false; }); it('does not set the ssl option', function () { + const options = parseOptions('mongodb://localhost/', { tls: false, ssl: false }); expect(options).to.not.have.property('ssl'); }); });