From 256212f75a31accd79ec39b68ab93b0e48527cb2 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Sat, 9 Dec 2023 12:05:34 +0100 Subject: [PATCH] Rework merging DB connection configuration (#214) - Ability to provide partialConfig object when using DB URI in constructor (Resolves: #196) - Fix encoding password in MongoDB connection URI (Resolves: #209) --- cli/src/options.ts | 2 +- core/package-lock.json | 9 + core/package.json | 1 + core/src/config.ts | 113 ++++++++++- core/src/database/config.ts | 86 --------- core/src/database/database-connector.ts | 69 +------ core/src/database/database.ts | 85 ++++++++- core/src/database/index.ts | 1 - core/src/index.ts | 24 ++- core/test/integration/database-connector.ts | 25 +-- core/test/integration/database.ts | 8 +- core/test/integration/index.ts | 10 +- core/test/unit/config.ts | 200 +++++++++++++++++--- core/test/unit/database-connector.ts | 94 ++------- 14 files changed, 429 insertions(+), 298 deletions(-) delete mode 100644 core/src/database/config.ts diff --git a/cli/src/options.ts b/cli/src/options.ts index 80686d4..8155f61 100644 --- a/cli/src/options.ts +++ b/cli/src/options.ts @@ -5,7 +5,7 @@ import { CommandLineArguments, PartialCliOptions, } from './types'; -import { SeederDatabaseConfigObjectOptions } from 'mongo-seeding/dist/database'; +import { SeederDatabaseConfigObjectOptions } from 'mongo-seeding'; import { Seeder, SeederCollectionReadingOptions } from 'mongo-seeding'; export const DEFAULT_INPUT_PATH = './'; diff --git a/core/package-lock.json b/core/package-lock.json index cee6664..0aa3eb1 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "bson": "^6.1.0", + "connection-string": "^4.4.0", "debug": "^4.3.1", "extend": "^3.0.0", "import-fresh": "^3.3.0", @@ -1583,6 +1584,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/connection-string": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/connection-string/-/connection-string-4.4.0.tgz", + "integrity": "sha512-D4xsUjSoE8m/B5yMOvCIHY+2ME6FIZhCq0NzBBT57Q8BuL7ArFhBK04osOfReoW4KFr5ztzFwWRdmnv9rCvu2w==", + "engines": { + "node": ">=14" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", diff --git a/core/package.json b/core/package.json index f3928a7..c625135 100755 --- a/core/package.json +++ b/core/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "bson": "^6.1.0", + "connection-string": "^4.4.0", "debug": "^4.3.1", "extend": "^3.0.0", "import-fresh": "^3.3.0", diff --git a/core/src/config.ts b/core/src/config.ts index bad24c9..f3b2fce 100644 --- a/core/src/config.ts +++ b/core/src/config.ts @@ -1,8 +1,9 @@ import * as extend from 'extend'; import { SeederCollection, DeepPartial } from './common'; -import { SeederDatabaseConfig, defaultDatabaseConfigObject } from './database'; import { BulkWriteOptions, MongoClientOptions } from 'mongodb'; import { EJSONOptions } from 'bson'; +import { ConnectionString } from 'connection-string'; +import { parseSeederDatabaseConfig } from './database'; /** * Defines configuration for database seeding. @@ -11,7 +12,7 @@ export interface SeederConfig { /** * Database connection URI or configuration object. */ - database: SeederDatabaseConfig; + database?: SeederDatabaseConfig; /** * Maximum time of waiting for successful MongoDB connection in milliseconds. Ignored when `mongoClientOptions` are passed. */ @@ -38,11 +39,12 @@ export interface SeederConfig { bulkWriteOptions?: BulkWriteOptions; } +export type SeederConfigWithoutDatabase = Omit; + /** * Stores default configuration for database seeding. */ -export const defaultSeederConfig: SeederConfig = { - database: defaultDatabaseConfigObject, +export const defaultSeederConfig: SeederConfigWithoutDatabase = { databaseReconnectTimeout: 10000, dropDatabase: false, dropCollections: false, @@ -50,25 +52,122 @@ export const defaultSeederConfig: SeederConfig = { }; /** - * Merges configuration for database seeding. + * Represents database connection configuration. It can be a URI string or object. + */ +export type SeederDatabaseConfig = string | SeederDatabaseConfigObject; + +/** + * Defines configuration for Database connection in a form of an object. + */ +export interface SeederDatabaseConfigObject { + /** + * Database connection protocol + */ + protocol: string; + + /** + * Database connection host + */ + host: string; + + /** + * Database connection port + */ + port: number; + + /** + * Database name. + */ + name: string; + + /** + * Database Username. + */ + username?: string; + + /** + * Database password. + */ + password?: string; + + /** + * Options for MongoDB Database Connection URI. + * Read more on: https://docs.mongodb.com/manual/reference/connection-string. + */ + options?: SeederDatabaseConfigObjectOptions; +} + +/** + * Defines options for MongoDB Database Connection URI. + * Read more on: https://docs.mongodb.com/manual/reference/connection-string. + */ +export interface SeederDatabaseConfigObjectOptions { + [key: string]: unknown; +} + +/** + * Merges configuration for seeding and deletes database property. * * @param partial Partial config object. If not specified, returns a default config object. * @param previous Previous config object. If not specified, uses a default config object as a base. */ -export const mergeSeederConfig = ( +export const mergeSeederConfigAndDeleteDb = ( partial?: DeepPartial, previous?: SeederConfig, -): SeederConfig => { +): SeederConfigWithoutDatabase => { const source = previous ? previous : defaultSeederConfig; + if ('database' in source) { + delete source.database; + } if (!partial) { return source; } const config = {}; + delete partial.database; return extend(true, config, source, partial); }; +export const mergeConnection = ( + partial?: DeepPartial, + previous?: ConnectionString, +): ConnectionString => { + const source = previous ?? parseSeederDatabaseConfig(undefined); + if (source.hosts && source.hosts.length > 1) { + source.hosts = [source.hosts[0]]; + } + if (!partial) { + return source; + } + + if (typeof partial === 'string') { + return parseSeederDatabaseConfig(partial); + } + + const partialConn = parseSeederDatabaseConfig(partial, true); + + // override hosts manually + if ( + partialConn.hosts && + partialConn.hosts.length > 0 && + source.hosts && + source.hosts.length > 0 + ) { + const newHost = partialConn.hosts[0]; + if (!newHost.name) { + newHost.name = source.hosts[0].name; + } + + if (!newHost.port) { + newHost.port = source.hosts[0].port; + } + } + + const config = new ConnectionString(); + return extend(true, config, source, partialConn); +}; + /** * Defines collection reading configuration. */ diff --git a/core/src/database/config.ts b/core/src/database/config.ts deleted file mode 100644 index 5e76749..0000000 --- a/core/src/database/config.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Represents database connection configuration. It can be a URI string or object. - */ -export type SeederDatabaseConfig = string | SeederDatabaseConfigObject; - -/** - * Defines configuration for Database connection in a form of an object. - */ -export interface SeederDatabaseConfigObject { - /** - * Database connection protocol - */ - protocol: string; - - /** - * Database connection host - */ - host: string; - - /** - * Database connection port - */ - port: number; - - /** - * Database name. - */ - name: string; - - /** - * Database Username. - */ - username?: string; - - /** - * Database password. - */ - password?: string; - - /** - * Options for MongoDB Database Connection URI. - * Read more on: https://docs.mongodb.com/manual/reference/connection-string. - */ - options?: SeederDatabaseConfigObjectOptions; -} - -/** - * Defines options for MongoDB Database Connection URI. - * Read more on: https://docs.mongodb.com/manual/reference/connection-string. - */ -export interface SeederDatabaseConfigObjectOptions { - [key: string]: string; -} - -/** - * Stores default values for database connection. - */ -export const defaultDatabaseConfigObject: SeederDatabaseConfigObject = { - protocol: 'mongodb', - host: '127.0.0.1', - port: 27017, - name: 'database', - username: undefined, - password: undefined, - options: undefined, -}; - -/** - * Checks if an object is valid database connection configuration. - * - * @param object Input object - */ -export function isSeederDatabaseConfigObject(object: unknown): boolean { - if (typeof object !== 'object' && object === null) { - return false; - } - - const obj = object as { [key: string]: unknown }; - - return ( - typeof obj.protocol === 'string' && - typeof obj.host === 'string' && - typeof obj.port === 'number' && - typeof obj.name === 'string' - ); -} diff --git a/core/src/database/database-connector.ts b/core/src/database/database-connector.ts index 5c83f58..0a127bb 100644 --- a/core/src/database/database-connector.ts +++ b/core/src/database/database-connector.ts @@ -1,11 +1,5 @@ import { MongoClient, MongoClientOptions } from 'mongodb'; -import { URLSearchParams } from 'url'; -import { - Database, - SeederDatabaseConfig, - isSeederDatabaseConfigObject, - SeederDatabaseConfigObject, -} from '.'; +import { Database } from '.'; import { LogFn } from '../common'; /** @@ -61,70 +55,19 @@ export class DatabaseConnector { /** * Connects to database. * - * @param config Database configuration + * @param connectionString Database connection string */ - async connect(config: SeederDatabaseConfig): Promise { - const dbConnectionUri = this.getUri(config); - const mongoClient = new MongoClient(dbConnectionUri, this.clientOptions); + async connect(connectionString: string): Promise { + const mongoClient = new MongoClient(connectionString, this.clientOptions); - this.log(`Connecting to ${this.maskUriCredentials(dbConnectionUri)}...`); - - try { - await mongoClient.connect(); - } catch (err) { - const e = err as Error; - throw new Error(`Error connecting to database: ${e.name}: ${e.message}`); - } + this.log(`Connecting to ${this.maskUriCredentials(connectionString)}...`); + await mongoClient.connect(); this.log('Connection with database established.'); return new Database(mongoClient); } - /** - * Gets MongoDB Connection URI from config. - * - * @param config Database configuration - */ - private getUri(config: SeederDatabaseConfig): string { - if (typeof config === 'string') { - return config; - } - - if (isSeederDatabaseConfigObject(config as unknown)) { - return this.getDbConnectionUri(config); - } - - throw new Error( - 'Connection URI or database config object is required to connect to database', - ); - } - - /** - * Constructs database connection URI from database configuration object. - * - * @param param0 Database connection object - */ - private getDbConnectionUri({ - protocol, - host, - port, - name, - username, - password, - options, - }: SeederDatabaseConfigObject) { - const credentials = username - ? `${username}${password ? `:${password}` : ''}@` - : ''; - const optsUriPart = options - ? `?${new URLSearchParams(options).toString()}` - : ''; - const portUriPart = protocol !== 'mongodb+srv' ? `:${port}` : ''; - - return `${protocol}://${credentials}${host}${portUriPart}/${name}${optsUriPart}`; - } - /** * Detects database connection credentials and masks them, replacing with masked URI credentials token. * diff --git a/core/src/database/database.ts b/core/src/database/database.ts index a141b05..0b3276a 100644 --- a/core/src/database/database.ts +++ b/core/src/database/database.ts @@ -1,5 +1,7 @@ import { Db, MongoClient, BulkWriteOptions } from 'mongodb'; -import { LogFn } from '../common'; +import { DeepPartial, LogFn } from '../common'; +import { SeederDatabaseConfigObject } from '../config'; +import { ConnectionString, IConnectionDefaults } from 'connection-string'; /** * Provides functionality for managing documents, collections in database. @@ -114,3 +116,84 @@ export class Database { await this.client.close(true); } } + +/** + * Parses a database config object or a connection URI into a ConnectionString. + * @param input The database config object or connection URI to parse. + * @param mergeWithDefaults Whether to merge the parsed config with default values. + */ +export function parseSeederDatabaseConfig( + input?: string | DeepPartial, + disableMergingWithDefaults?: boolean, +): ConnectionString { + const defaultParams = defaultConnParams(); + let uri: string | null = null; + if (!input) { + return new ConnectionString(null, defaultParams); + } + + switch (typeof input) { + case 'object': + // parse the object into a URI first, and then parse the URI into a ConnectionString + uri = parseUriFromObject(input).toString(); + break; + case 'string': + uri = input; + break; + default: + throw new Error( + 'Connection URI or database config object is required to connect to database', + ); + } + + if (disableMergingWithDefaults) { + return new ConnectionString(uri); + } + + const out = new ConnectionString(uri, defaultParams); + if (out.hosts && out.hosts.length > 1) { + out.hosts = [out.hosts[0]]; + } + return out; +} + +/** + * Parses a database config object into a URI. + * @param config The database config object to parse. + */ +function parseUriFromObject( + config: Partial, +): ConnectionString { + return new ConnectionString(null, { + protocol: config.protocol, + hosts: [ + { + name: config.host, + port: config.port, + }, + ], + user: config.username, + password: config.password, + path: config.name ? [config.name] : undefined, + params: config.options, + }); +} + +/** + * Returns default connection parameters. + */ +function defaultConnParams(): IConnectionDefaults { + return { + protocol: 'mongodb', + hosts: [ + { + name: '127.0.0.1', + port: 27017, + }, + ], + path: ['database'], + user: undefined, + password: undefined, + params: undefined, + }; +} diff --git a/core/src/database/index.ts b/core/src/database/index.ts index e925465..2df3429 100644 --- a/core/src/database/index.ts +++ b/core/src/database/index.ts @@ -1,3 +1,2 @@ -export * from './config'; export * from './database'; export * from './database-connector'; diff --git a/core/src/index.ts b/core/src/index.ts index 3d48e87..754fec5 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -11,9 +11,12 @@ import { SeederCollectionReadingOptions, defaultSeederConfig, SeederConfig, - mergeSeederConfig, + mergeSeederConfigAndDeleteDb, mergeCollectionReadingOptions, + SeederConfigWithoutDatabase, + mergeConnection, } from './config'; +import { ConnectionString } from 'connection-string'; export * from './config'; export * from './helpers'; @@ -35,7 +38,12 @@ export class Seeder { /** * Configuration for seeding database. */ - config: SeederConfig = defaultSeederConfig; + config: SeederConfigWithoutDatabase = defaultSeederConfig; + + /** + * Database connection. + */ + connection: ConnectionString; /** * Constructs a new `Seeder` instance and loads configuration for data import. @@ -43,7 +51,8 @@ export class Seeder { * @param config Optional partial object with database seeding configuration. The object is merged with the default configuration object. To use all default settings, simply omit this parameter. */ constructor(config?: DeepPartial) { - this.config = mergeSeederConfig(config); + this.connection = mergeConnection(config?.database); + this.config = mergeSeederConfigAndDeleteDb(config); this.log = NewLoggerInstance(); } @@ -97,7 +106,12 @@ export class Seeder { } this.log('Starting collection import...'); - const config = mergeSeederConfig(partialConfig, this.config); + const connection = mergeConnection( + partialConfig?.database, + this.connection, + ); + const config = mergeSeederConfigAndDeleteDb(partialConfig, this.config); + const databaseConnector = new DatabaseConnector( config.databaseReconnectTimeout, config.mongoClientOptions, @@ -106,7 +120,7 @@ export class Seeder { let database: Database | undefined; try { - database = await databaseConnector.connect(config.database); + database = await databaseConnector.connect(connection.toString()); if (!config.dropDatabase && config.dropCollections) { this.log('Dropping collections...'); diff --git a/core/test/integration/database-connector.ts b/core/test/integration/database-connector.ts index 262a419..6f59bc2 100644 --- a/core/test/integration/database-connector.ts +++ b/core/test/integration/database-connector.ts @@ -2,32 +2,15 @@ import { Db } from 'mongodb'; import { DatabaseConnector, Database, - defaultDatabaseConfigObject, + parseSeederDatabaseConfig, } from '../../src/database'; describe('DatabaseConnector', () => { - it('should connect to database and close connection with config object', async () => { + it('should connect to database and close connection', async () => { const databaseConnector = new DatabaseConnector(); - const database = await databaseConnector.connect({ - ...defaultDatabaseConfigObject, - name: 'coredb', - }); - const collections = await database.db.listCollections().toArray(); - - expect(database).toBeInstanceOf(Database); - expect(database.db).toBeInstanceOf(Db); - expect(collections).toBeInstanceOf(Array); - - await expect(database.closeConnection()).resolves.toBeUndefined(); - }); - - it('should connect to database and close connection using URI', async () => { - const databaseConnector = new DatabaseConnector(); - - const database = await databaseConnector.connect( - 'mongodb://127.0.0.1:27017/testing', - ); + const connStr = parseSeederDatabaseConfig({ name: 'coredb' }).toString(); + const database = await databaseConnector.connect(connStr); const collections = await database.db.listCollections().toArray(); expect(database).toBeInstanceOf(Database); diff --git a/core/test/integration/database.ts b/core/test/integration/database.ts index 30ef1a7..ca05809 100644 --- a/core/test/integration/database.ts +++ b/core/test/integration/database.ts @@ -1,7 +1,7 @@ import { DatabaseConnector, - defaultDatabaseConfigObject, Database, + parseSeederDatabaseConfig, } from '../../src/database'; import { removeUnderscoreIdProperty, @@ -13,10 +13,8 @@ const databaseConnector = new DatabaseConnector(); let database: Database; beforeAll(async () => { - database = await databaseConnector.connect({ - ...defaultDatabaseConfigObject, - name: 'coredb', - }); + const connStr = parseSeederDatabaseConfig({ name: 'coredb' }).toString(); + database = await databaseConnector.connect(connStr); await database.db.dropDatabase(); }); diff --git a/core/test/integration/index.ts b/core/test/integration/index.ts index 78a753a..a691be0 100644 --- a/core/test/integration/index.ts +++ b/core/test/integration/index.ts @@ -1,7 +1,7 @@ import { DatabaseConnector, Database, - defaultDatabaseConfigObject, + parseSeederDatabaseConfig, } from '../../src/database'; import { DeepPartial } from '../../src/common'; import { Seeder, SeederConfig } from '../../src'; @@ -18,10 +18,8 @@ const databaseConnector = new DatabaseConnector(); let database: Database; beforeAll(async () => { - database = await databaseConnector.connect({ - ...defaultDatabaseConfigObject, - name: DATABASE_NAME, - }); + const connStr = parseSeederDatabaseConfig({ name: DATABASE_NAME }).toString(); + database = await databaseConnector.connect(connStr); await database.drop(); }); @@ -241,7 +239,7 @@ describe('Mongo Seeding', () => { const collections = seeder.readCollectionsFromPath(path); await expect(() => seeder.import(collections)).rejects.toThrow( - `Error connecting to database: MongoServerError: Authentication failed.`, + `Authentication failed.`, ); }); }); diff --git a/core/test/unit/config.ts b/core/test/unit/config.ts index 9515702..728fc39 100644 --- a/core/test/unit/config.ts +++ b/core/test/unit/config.ts @@ -1,42 +1,29 @@ import { DeepPartial } from '../../src/common'; import { SeederConfig, - mergeSeederConfig, + mergeSeederConfigAndDeleteDb, defaultSeederConfig, SeederCollectionReadingOptions, mergeCollectionReadingOptions, defaultCollectionReadingOptions, + SeederDatabaseConfig, + mergeConnection, } from '../../src'; describe('SeederConfig', () => { it('should merge config with default one', () => { const partialConfig: DeepPartial = { - database: { - port: 3000, - host: 'mongo', - username: 'test', - password: '123', - }, databaseReconnectTimeout: 20000, }; - const expectedConfig: SeederConfig = { - database: { - protocol: 'mongodb', - host: 'mongo', - port: 3000, - name: 'database', - username: 'test', - password: '123', - }, + const expectedConfig: Exclude = { dropDatabase: false, dropCollections: false, removeAllDocuments: false, databaseReconnectTimeout: 20000, }; - const config = mergeSeederConfig(partialConfig); - - expect(config).toEqual(expectedConfig); + const seederConfig = mergeSeederConfigAndDeleteDb(partialConfig); + expect(seederConfig).toEqual(expectedConfig); }); it('should replace undefined values with default ones', () => { @@ -49,20 +36,183 @@ describe('SeederConfig', () => { databaseReconnectTimeout: undefined, }; - const config = mergeSeederConfig(partialConfig); + const config = mergeSeederConfigAndDeleteDb(partialConfig); expect(config).toEqual(defaultSeederConfig); }); - it('should override default database config object with connection URI', () => { + it('should delete database property', () => { const partialConfig: DeepPartial = { - database: 'testURI', - databaseReconnectTimeout: undefined, + database: { + name: 'test', + port: 3000, + host: 'mongo', + }, + databaseReconnectTimeout: 20000, + }; + const expectedConfig: Exclude = { + dropDatabase: false, + dropCollections: false, + removeAllDocuments: false, + databaseReconnectTimeout: 20000, + }; + + const config = mergeSeederConfigAndDeleteDb(partialConfig); + + expect(config).toEqual(expectedConfig); + }); +}); + +describe('Connection', () => { + it('should merge config with default one', () => { + const partialConfig: DeepPartial = { + port: 3000, + host: 'mongo', + username: 'test', + password: '123', + }; + const expectedConnectionString = 'mongodb://test:123@mongo:3000/database'; + + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should replace config with a new object', () => { + const partialConfig: DeepPartial = { + host: 'newhost', + username: 'newuser', + password: 'newpass', + name: 'newdb', + }; + const oldConn: DeepPartial = { + host: 'oldhost', + port: 3000, + username: 'olduser', + password: 'oldpass', + name: 'olddb', }; + const expectedConnectionString = + 'mongodb://newuser:newpass@newhost:3000/newdb'; - const config = mergeSeederConfig(partialConfig); + const connection1 = mergeConnection(oldConn); + const connection = mergeConnection(partialConfig, connection1); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should update previous URI config with a new object', () => { + const partialConfig: DeepPartial = { + username: 'newuser', + name: 'newdb', + }; + const oldConn = 'mongodb://foo:bar@aaa:3000/olddb'; + const expectedConnectionString = 'mongodb://newuser:bar@aaa:3000/newdb'; + + const prevConnection = mergeConnection(oldConn); + const connection = mergeConnection(partialConfig, prevConnection); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should replace config with a new URI', () => { + const partialConfig: DeepPartial = + 'mongodb://bbbb:3000/newdb'; + const oldConn: DeepPartial = { + host: 'aaaa', + port: 3000, + username: 'olduser', + password: 'oldpass', + name: 'olddb', + }; + const expectedConnectionString = 'mongodb://bbbb:3000/newdb'; + + const connection1 = mergeConnection(oldConn); + const connection = mergeConnection(partialConfig, connection1); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should replace undefined values with default ones', () => { + const partialConfig: DeepPartial = {}; + const expectedConnectionString = 'mongodb://127.0.0.1:27017/database'; + + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should override default database config object with connection URI', () => { + const partialConfig: DeepPartial = 'myhost:3233'; + const expectedConnectionString = 'mongodb://myhost:3233/database'; + + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should return valid DB connection URI with Mongo 3.6 protocol', () => { + const partialConfig: DeepPartial = { + protocol: 'mongodb+srv', + host: '127.0.0.1', + port: 27017, + name: 'database', + }; + const expectedConnectionString = 'mongodb+srv://127.0.0.1:27017/database'; + + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should return valid DB connection URI with username only', () => { + const partialConfig: DeepPartial = { + protocol: 'mongodb', + username: 'user', + host: '10.10.10.1', + port: 27017, + name: 'authDb', + options: { + ssl: 'false', + foo: 'bar', + }, + }; + const expectedConnectionString = + 'mongodb://user@10.10.10.1:27017/authDb?ssl=false&foo=bar'; + + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should return valid DB connection URI with username and login', () => { + const partialConfig: DeepPartial = { + protocol: 'mongodb', + username: 'user', + password: 'pass', + host: '10.10.10.1', + port: 27017, + name: 'mydb', + options: { + foo: 'bar', + }, + }; + const expectedConnectionString = + 'mongodb://user:pass@10.10.10.1:27017/mydb?foo=bar'; + + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); + }); + + it('should return valid escaped DB connection URI with username and login', () => { + const partialConfig: DeepPartial = { + protocol: 'mongodb', + username: 'user', + password: 'my#pass?test', + host: '10.10.10.1', + port: 27017, + name: 'mydb', + options: { + foo: 'bar', + }, + }; + const expectedConnectionString = + 'mongodb://user:my%23pass%3Ftest@10.10.10.1:27017/mydb?foo=bar'; - expect(config).toHaveProperty('database', 'testURI'); + const connection = mergeConnection(partialConfig); + expect(connection.toString()).toEqual(expectedConnectionString); }); }); diff --git a/core/test/unit/database-connector.ts b/core/test/unit/database-connector.ts index 0b07974..7d90213 100644 --- a/core/test/unit/database-connector.ts +++ b/core/test/unit/database-connector.ts @@ -1,12 +1,17 @@ -import { DatabaseConnector, SeederDatabaseConfig } from '../../src/database'; +import { DatabaseConnector } from '../../src/database'; import { MongoClient, MongoClientOptions } from 'mongodb'; +import { ConnectionString } from 'connection-string'; -const dbConfig: SeederDatabaseConfig = { +const dbConfig = new ConnectionString('', { protocol: 'mongodb', - host: '127.0.0.1', - port: 27017, - name: 'database', -}; + hosts: [ + { + name: '127.0.0.1', + port: 27017, + }, + ], + path: ['database'], +}); const uri = 'mongodb://foo.bar'; @@ -27,71 +32,6 @@ describe('DatabaseConnector', () => { connectMock.mockClear(); }); - it('should throw error when trying connecting without config', async () => { - const databaseConnector = new DatabaseConnector(); - // @ts-ignore - await expect(databaseConnector.connect({})).rejects.toThrow(); - }); - - it('should return valid DB connection URI', () => { - const databaseConnector = new DatabaseConnector(); - - const expectedUri = 'mongodb://127.0.0.1:27017/database'; - // @ts-ignore - const uri = databaseConnector.getDbConnectionUri(dbConfig); - expect(uri).toBe(expectedUri); - }); - - it('should return valid DB connection URI with Mongo 3.6 protocol', () => { - const databaseConnector = new DatabaseConnector(); - const dbConfig: SeederDatabaseConfig = { - protocol: 'mongodb+srv', - host: '127.0.0.1', - port: 27017, - name: 'database', - }; - const expectedUri = 'mongodb+srv://127.0.0.1/database'; - // @ts-ignore - const uri = databaseConnector.getDbConnectionUri(dbConfig); - expect(uri).toBe(expectedUri); - }); - - it('should return valid DB connection URI with username only', () => { - const databaseConnector = new DatabaseConnector(); - const authConfig: SeederDatabaseConfig = { - protocol: 'mongodb', - username: 'user', - host: '10.10.10.1', - port: 27017, - name: 'authDb', - options: { - ssl: 'false', - foo: 'bar', - }, - }; - const expectedUri = - 'mongodb://user@10.10.10.1:27017/authDb?ssl=false&foo=bar'; - // @ts-ignore - const uri = databaseConnector.getDbConnectionUri(authConfig); - expect(uri).toBe(expectedUri); - }); - - it('should return valid DB connection URI with username and login', () => { - const databaseConnector = new DatabaseConnector(); - const authConfig: SeederDatabaseConfig = { - protocol: 'mongodb', - username: 'user', - password: 'pass', - host: '10.10.10.1', - port: 27017, - name: 'authDb', - }; - // @ts-ignore - const uri = databaseConnector.getDbConnectionUri(authConfig); - const expectedUri = 'mongodb://user:pass@10.10.10.1:27017/authDb'; - expect(uri).toBe(expectedUri); - }); - it('should mask user credentials in database connection URI', () => { const databaseConnector = new DatabaseConnector(); @@ -138,9 +78,9 @@ describe('DatabaseConnector', () => { }), ); - await expect(databaseConnector.connect(dbConfig)).rejects.toThrowError( - 'MongoError', - ); + await expect( + databaseConnector.connect(dbConfig.toString()), + ).rejects.toThrow('MongoError'); }); it('should allow passing custom Mongo client options', async () => { @@ -159,8 +99,8 @@ describe('DatabaseConnector', () => { await connector.connect(uri); - expect(MongoClient).toBeCalledWith(uri, opts); - expect(MongoClient).toBeCalledTimes(1); - expect(connectMock).toBeCalledTimes(1); + expect(MongoClient).toHaveBeenCalledWith(uri, opts); + expect(MongoClient).toHaveBeenCalledTimes(1); + expect(connectMock).toHaveBeenCalledTimes(1); }); });