diff --git a/src/cloud-sql-instance.ts b/src/cloud-sql-instance.ts index f316e30..a830acf 100644 --- a/src/cloud-sql-instance.ts +++ b/src/cloud-sql-instance.ts @@ -14,7 +14,7 @@ import {IpAddressTypes, selectIpAddress} from './ip-addresses'; import {InstanceConnectionInfo} from './instance-connection-info'; -import {parseInstanceConnectionName} from './parse-instance-connection-name'; +import {resolveInstanceName} from './parse-instance-connection-name'; import {InstanceMetadata} from './sqladmin-fetcher'; import {generateKeys} from './crypto'; import {RSAKeys} from './rsa-keys'; @@ -38,6 +38,7 @@ interface Fetcher { interface CloudSQLInstanceOptions { authType: AuthTypes; instanceConnectionName: string; + domainName?: string; ipType: IpAddressTypes; limitRateInterval?: number; sqlAdminFetcher: Fetcher; @@ -54,7 +55,13 @@ export class CloudSQLInstance { static async getCloudSQLInstance( options: CloudSQLInstanceOptions ): Promise { - const instance = new CloudSQLInstance(options); + const instance = new CloudSQLInstance({ + options: options, + instanceInfo: await resolveInstanceName( + options.instanceConnectionName, + options.domainName + ), + }); await instance.refresh(); return instance; } @@ -80,17 +87,17 @@ export class CloudSQLInstance { public dnsName = ''; constructor({ - ipType, - authType, - instanceConnectionName, - sqlAdminFetcher, - limitRateInterval = 30 * 1000, // 30s default - }: CloudSQLInstanceOptions) { - this.authType = authType; - this.instanceInfo = parseInstanceConnectionName(instanceConnectionName); - this.ipType = ipType; - this.limitRateInterval = limitRateInterval; - this.sqlAdminFetcher = sqlAdminFetcher; + options, + instanceInfo, + }: { + options: CloudSQLInstanceOptions; + instanceInfo: InstanceConnectionInfo; + }) { + this.instanceInfo = instanceInfo; + this.authType = options.authType || AuthTypes.PASSWORD; + this.ipType = options.ipType || IpAddressTypes.PUBLIC; + this.limitRateInterval = options.limitRateInterval || 30 * 1000; // 30 seconds + this.sqlAdminFetcher = options.sqlAdminFetcher; } // p-throttle library has to be initialized in an async scope in order to @@ -286,6 +293,7 @@ export class CloudSQLInstance { } cancelRefresh(): void { + // If refresh has not yet started, then cancel the setTimeout if (this.scheduledRefreshID) { clearTimeout(this.scheduledRefreshID); } @@ -305,4 +313,8 @@ export class CloudSQLInstance { this.closed = true; this.cancelRefresh(); } + + isClosed(): boolean { + return this.closed; + } } diff --git a/src/connector.ts b/src/connector.ts index acde4a7..f7159a2 100644 --- a/src/connector.ts +++ b/src/connector.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {Server, Socket, createServer} from 'node:net'; +import {createServer, Server, Socket} from 'node:net'; import tls from 'node:tls'; import {promisify} from 'node:util'; import {AuthClient, GoogleAuth} from 'google-auth-library'; @@ -43,6 +43,8 @@ export declare interface ConnectionOptions { authType?: AuthTypes; ipType?: IpAddressTypes; instanceConnectionName: string; + domainName?: string; + limitRateInterval?: number; } export declare interface SocketConnectionOptions extends ConnectionOptions { @@ -72,71 +74,102 @@ export declare interface TediousDriverOptions { connector: PromisedStreamFunction; encrypt: boolean; } +// CacheEntry holds the promise and resolved instance metadata for +// the connector's instances. The instance field will be set when +// the promise resolves. +class CacheEntry { + promise: Promise; + instance?: CloudSQLInstance; + err?: Error; + + constructor(promise: Promise) { + this.promise = promise; + this.promise + .then(inst => (this.instance = inst)) + .catch(err => (this.err = err)); + } + + isResolved(): boolean { + return Boolean(this.instance); + } + isError(): boolean { + return Boolean(this.err); + } +} // Internal mapping of the CloudSQLInstances that // adds extra logic to async initialize items. -class CloudSQLInstanceMap extends Map { - async loadInstance({ - ipType, - authType, - instanceConnectionName, - sqlAdminFetcher, - }: { - ipType: IpAddressTypes; - authType: AuthTypes; - instanceConnectionName: string; - sqlAdminFetcher: SQLAdminFetcher; - }): Promise { +class CloudSQLInstanceMap extends Map { + private readonly sqlAdminFetcher: SQLAdminFetcher; + + constructor(sqlAdminFetcher: SQLAdminFetcher) { + super(); + this.sqlAdminFetcher = sqlAdminFetcher; + } + + private cacheKey(opts: ConnectionOptions): string { + //TODO: for now, the cache key function must be synchronous. + // When we implement the async connection info from + // https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector/pull/426 + // then the cache key should contain both the domain name + // and the resolved instance name. + return ( + (opts.instanceConnectionName || opts.domainName) + + '-' + + opts.authType + + '-' + + opts.ipType + ); + } + + async loadInstance(opts: ConnectionOptions): Promise { // in case an instance to that connection name has already // been setup there's no need to set it up again - if (this.has(instanceConnectionName)) { - const instance = this.get(instanceConnectionName); - if (instance.authType && instance.authType !== authType) { - throw new CloudSQLConnectorError({ - message: - `getOptions called for instance ${instanceConnectionName} with authType ${authType}, ` + - `but was previously called with authType ${instance.authType}. ` + - 'If you require both for your use case, please use a new connector object.', - code: 'EMISMATCHAUTHTYPE', - }); + const key = this.cacheKey(opts); + const entry = this.get(key); + if (entry) { + if (entry.isResolved()) { + if (!entry.instance?.isClosed()) { + // The instance is open and the domain has not changed. + // use the cached instance. + return; + } + } else if (entry.isError()) { + // The instance failed it's initial refresh. Remove it from the + // cache and throw the error. + this.delete(key); + throw entry.err; + } else { + // The instance initial refresh is in progress. + await entry.promise; + return; } - return; } - const connectionInstance = await CloudSQLInstance.getCloudSQLInstance({ - ipType, - authType, - instanceConnectionName, - sqlAdminFetcher: sqlAdminFetcher, + + // Start the refresh and add a cache entry. + const promise = CloudSQLInstance.getCloudSQLInstance({ + instanceConnectionName: opts.instanceConnectionName, + domainName: opts.domainName, + authType: opts.authType || AuthTypes.PASSWORD, + ipType: opts.ipType || IpAddressTypes.PUBLIC, + limitRateInterval: opts.limitRateInterval || 30 * 1000, // 30 sec + sqlAdminFetcher: this.sqlAdminFetcher, }); - this.set(instanceConnectionName, connectionInstance); + this.set(key, new CacheEntry(promise)); + + // Wait for the cache entry to resolve. + await promise; } - getInstance({ - instanceConnectionName, - authType, - }: { - instanceConnectionName: string; - authType: AuthTypes; - }): CloudSQLInstance { - const connectionInstance = this.get(instanceConnectionName); - if (!connectionInstance) { + getInstance(opts: ConnectionOptions): CloudSQLInstance { + const connectionInstance = this.get(this.cacheKey(opts)); + if (!connectionInstance || !connectionInstance.instance) { throw new CloudSQLConnectorError({ - message: `Cannot find info for instance: ${instanceConnectionName}`, + message: `Cannot find info for instance: ${opts.instanceConnectionName}`, code: 'ENOINSTANCEINFO', }); - } else if ( - connectionInstance.authType && - connectionInstance.authType !== authType - ) { - throw new CloudSQLConnectorError({ - message: - `getOptions called for instance ${instanceConnectionName} with authType ${authType}, ` + - `but was previously called with authType ${connectionInstance.authType}. ` + - 'If you require both for your use case, please use a new connector object.', - code: 'EMISMATCHAUTHTYPE', - }); } - return connectionInstance; + return connectionInstance.instance; } } @@ -160,13 +193,13 @@ export class Connector { private readonly sockets: Set; constructor(opts: ConnectorOptions = {}) { - this.instances = new CloudSQLInstanceMap(); this.sqlAdminFetcher = new SQLAdminFetcher({ loginAuth: opts.auth, sqlAdminAPIEndpoint: opts.sqlAdminAPIEndpoint, universeDomain: opts.universeDomain, userAgent: opts.userAgent, }); + this.instances = new CloudSQLInstanceMap(this.sqlAdminFetcher); this.localProxies = new Set(); this.sockets = new Set(); } @@ -182,25 +215,13 @@ export class Connector { // }); // const pool = new Pool(opts) // const res = await pool.query('SELECT * FROM pg_catalog.pg_tables;') - async getOptions({ - authType = AuthTypes.PASSWORD, - ipType = IpAddressTypes.PUBLIC, - instanceConnectionName, - }: ConnectionOptions): Promise { + async getOptions(opts: ConnectionOptions): Promise { const {instances} = this; - await instances.loadInstance({ - ipType, - authType, - instanceConnectionName, - sqlAdminFetcher: this.sqlAdminFetcher, - }); + await instances.loadInstance(opts); return { stream() { - const cloudSqlInstance = instances.getInstance({ - instanceConnectionName, - authType, - }); + const cloudSqlInstance = instances.getInstance(opts); const { instanceInfo, ephemeralCert, @@ -228,7 +249,7 @@ export class Connector { privateKey, serverCaCert, serverCaMode, - dnsName, + dnsName: instanceInfo.domainName || dnsName, // use the configured domain name, or the instance dnsName. }); tlsSocket.once('error', () => { cloudSqlInstance.forceRefresh(); @@ -333,7 +354,7 @@ export class Connector { // Also clear up any local proxy servers and socket connections. close(): void { for (const instance of this.instances.values()) { - instance.close(); + instance.promise.then(inst => inst.close()); } for (const server of this.localProxies) { server.close(); diff --git a/test/cloud-sql-instance.ts b/test/cloud-sql-instance.ts index 613ea99..c32bf74 100644 --- a/test/cloud-sql-instance.ts +++ b/test/cloud-sql-instance.ts @@ -107,11 +107,13 @@ t.test('CloudSQLInstance', async t => { }, }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: failedFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: failedFetcher, + limitRateInterval: 50, + }, }); await t.rejects( @@ -125,11 +127,13 @@ t.test('CloudSQLInstance', async t => { const start = Date.now(); let refreshCount = 0; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: fetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: fetcher, + limitRateInterval: 50, + }, }); instance.refresh = () => { if (refreshCount === 2) { @@ -165,11 +169,13 @@ t.test('CloudSQLInstance', async t => { }, }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: failedFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: failedFetcher, + limitRateInterval: 50, + }, }); await (() => new Promise((res): void => { @@ -219,11 +225,13 @@ t.test('CloudSQLInstance', async t => { }, }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: failedFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: failedFetcher, + limitRateInterval: 50, + }, }); await (() => new Promise((res): void => { @@ -247,11 +255,13 @@ t.test('CloudSQLInstance', async t => { t.test('forceRefresh', async t => { const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: fetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: fetcher, + limitRateInterval: 50, + }, }); await instance.refresh(); @@ -283,11 +293,13 @@ t.test('CloudSQLInstance', async t => { t.test('forceRefresh ongoing cycle', async t => { const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: fetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: fetcher, + limitRateInterval: 50, + }, }); let cancelRefreshCalled = false; @@ -318,11 +330,13 @@ t.test('CloudSQLInstance', async t => { t.test('refresh post-forceRefresh', async t => { const instance = new CloudSQLInstance({ - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - ipType: IpAddressTypes.PUBLIC, - limitRateInterval: 0, - sqlAdminFetcher: fetcher, + options: { + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + ipType: IpAddressTypes.PUBLIC, + limitRateInterval: 0, + sqlAdminFetcher: fetcher, + }, }); const start = Date.now(); @@ -355,11 +369,13 @@ t.test('CloudSQLInstance', async t => { t.test('refresh rate limit', async t => { const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - limitRateInterval: 50, - sqlAdminFetcher: fetcher, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + limitRateInterval: 50, + sqlAdminFetcher: fetcher, + }, }); const start = Date.now(); // starts out refresh logic @@ -400,11 +416,13 @@ t.test('CloudSQLInstance', async t => { }, }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: slowFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: slowFetcher, + limitRateInterval: 50, + }, }); // starts a new refresh cycle but do not await on it @@ -425,11 +443,13 @@ t.test('CloudSQLInstance', async t => { }, }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: slowFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: slowFetcher, + limitRateInterval: 50, + }, }); // simulates an ongoing instance, already has data @@ -459,11 +479,13 @@ t.test('CloudSQLInstance', async t => { }, }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: failAndSlowFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: failAndSlowFetcher, + limitRateInterval: 50, + }, }); await instance.refresh(); @@ -492,11 +514,13 @@ t.test('CloudSQLInstance', async t => { }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: failAndSlowFetcher, - limitRateInterval: 50, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: failAndSlowFetcher, + limitRateInterval: 50, + }, }); await instance.refresh(); @@ -557,11 +581,13 @@ t.test('CloudSQLInstance', async t => { }; const instance = new CloudSQLInstance({ - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - instanceConnectionName: 'my-project:us-east1:my-instance', - sqlAdminFetcher: updateFetcher, - limitRateInterval: 0, + options: { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + instanceConnectionName: 'my-project:us-east1:my-instance', + sqlAdminFetcher: updateFetcher, + limitRateInterval: 0, + }, }); await (() => new Promise((res): void => { diff --git a/test/connector.ts b/test/connector.ts index f7b8fa1..7f74204 100644 --- a/test/connector.ts +++ b/test/connector.ts @@ -218,6 +218,9 @@ t.test('start only a single instance info per connection name', async t => { return { ipType: IpAddressTypes.PUBLIC, authType: AuthTypes.PASSWORD, + isClosed() { + return false; + }, }; }, }, @@ -237,124 +240,68 @@ t.test('start only a single instance info per connection name', async t => { }); }); -t.test('Connector reusing instance on mismatching auth type', async t => { - setupCredentials(t); - - // mocks sql admin fetcher and generateKeys modules - // so that they can return a deterministic result - const {Connector} = t.mockRequire('../src/connector', { - '../src/sqladmin-fetcher': { - SQLAdminFetcher: class { - getInstanceMetadata() { - return Promise.resolve({ - ipAddresses: { - public: '127.0.0.1', - }, - serverCaCert: { - cert: CA_CERT, +t.test( + 'Connector with mismatching auth type creates separate instances', + async t => { + setupCredentials(t); + let instancesCreated = 0; + // mocks sql admin fetcher and generateKeys modules + // so that they can return a deterministic result + const {Connector} = t.mockRequire('../src/connector', { + '../src/sqladmin-fetcher': { + SQLAdminFetcher: class { + getInstanceMetadata() { + return Promise.resolve({ + ipAddresses: { + public: '127.0.0.1', + }, + serverCaCert: { + cert: CA_CERT, + expirationTime: '2033-01-06T10:00:00.232Z', + }, + }); + } + getEphemeralCertificate() { + return Promise.resolve({ + cert: CLIENT_CERT, expirationTime: '2033-01-06T10:00:00.232Z', - }, - }); - } - getEphemeralCertificate() { - return Promise.resolve({ - cert: CLIENT_CERT, - expirationTime: '2033-01-06T10:00:00.232Z', - }); - } + }); + } + }, }, - }, - '../src/cloud-sql-instance': { - CloudSQLInstance: { - async getCloudSQLInstance() { - return { - ipType: IpAddressTypes.PUBLIC, - authType: AuthTypes.PASSWORD, - }; + '../src/cloud-sql-instance': { + CloudSQLInstance: { + async getCloudSQLInstance() { + instancesCreated++; + return { + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.PASSWORD, + }; + }, }, }, - }, - }); + }); - const connector = new Connector(); - await connector.getOptions({ - ipType: 'PUBLIC', - authType: 'PASSWORD', - instanceConnectionName: 'foo:bar:baz', - }); + const connector = new Connector(); + await connector.getOptions({ + ipType: 'PUBLIC', + authType: 'PASSWORD', + instanceConnectionName: 'foo:bar:baz', + }); - return t.rejects( - connector.getOptions({ + await connector.getOptions({ ipType: 'PUBLIC', authType: 'IAM', instanceConnectionName: 'foo:bar:baz', - }), - { - message: - 'getOptions called for instance foo:bar:baz' + - ' with authType IAM, but was previously called with authType PASSWORD.' + - ' If you require both for your use case, please use a new connector object.', - code: 'EMISMATCHAUTHTYPE', - }, - 'should throw error' - ); -}); - -t.test('Connector factory method mismatch auth type', async t => { - setupCredentials(t); // setup google-auth credentials mocks - - // mocks sql admin fetcher and generateKeys modules - // so that they can return a deterministic result - const {Connector} = t.mockRequire('../src/connector', { - '../src/sqladmin-fetcher': { - SQLAdminFetcher: class { - getInstanceMetadata() { - return Promise.resolve({ - ipAddresses: { - public: '127.0.0.1', - }, - serverCaCert: { - cert: CA_CERT, - expirationTime: '2033-01-06T10:00:00.232Z', - }, - }); - } - getEphemeralCertificate() { - return Promise.resolve({ - cert: CLIENT_CERT, - expirationTime: '2033-01-06T10:00:00.232Z', - }); - } - }, - }, - '../src/cloud-sql-instance': { - CloudSQLInstance: { - async getCloudSQLInstance() { - return { - authType: 'IAM', - ipType: 'PUBLIC', - }; - }, - }, - }, - }); + }); - const connector = new Connector(); - const opts = await connector.getOptions({ - authType: 'PASSWORD', - ipType: 'PUBLIC', - instanceConnectionName: 'foo:bar:baz', - }); - t.throws( - () => { - opts.stream(); // calls factory method that returns new socket - }, - { - code: 'EMISMATCHAUTHTYPE', - }, - 'should throw a mismatching auth type error' - ); -}); + t.same( + instancesCreated, + 2, + 'An instance created for each different configuration' + ); + } +); t.test('Connector, supporting Tedious driver', async t => { setupCredentials(t); // setup google-auth credentials mocks @@ -565,3 +512,87 @@ t.test('Connector, custom userAgent', async t => { t.same(actualUserAgent, expectedUserAgent); }); + +function setupConnectorModule(t) { + setupCredentials(t); + const response = { + instancesCreated: 0, + resolveTxtResponse: 'project:region1:instance', + Connector: null, + }; + // mocks sql admin fetcher and generateKeys modules + // so that they can return a deterministic result + const {Connector} = t.mockRequire('../src/connector', { + '../src/sqladmin-fetcher': { + SQLAdminFetcher: class { + getInstanceMetadata() { + return Promise.resolve({ + ipAddresses: { + public: '127.0.0.1', + }, + serverCaCert: { + cert: CA_CERT, + expirationTime: '2033-01-06T10:00:00.232Z', + }, + }); + } + getEphemeralCertificate() { + return Promise.resolve({ + cert: CLIENT_CERT, + expirationTime: '2033-01-06T10:00:00.232Z', + }); + } + }, + }, + '../src/cloud-sql-instance': t.mockRequire('../src/cloud-sql-instance', { + '../src/crypto': { + generateKeys: async () => ({ + publicKey: '-----BEGIN PUBLIC KEY-----', + privateKey: CLIENT_KEY, + }), + }, + '../src/dns-lookup': { + async resolveTxtRecord(): Promise { + return response.resolveTxtResponse; + }, + }, + }), + '../src/dns-lookup': { + async resolveTxtRecord(): Promise { + return response.resolveTxtResponse; + }, + }, + }); + response.Connector = Connector; + + return response; +} + +t.test('Connector by domain resolves and creates instance', async t => { + const th = setupConnectorModule(t); + const connector = new th.Connector(); + t.after(() => { + connector.close(); + }); + + // Get options twice + await connector.getOptions({ + ipType: 'PUBLIC', + authType: 'PASSWORD', + domainName: 'db.example.com', + }); + + await connector.getOptions({ + ipType: 'PUBLIC', + authType: 'PASSWORD', + domainName: 'db.example.com', + }); + + // Ensure there is only one entry. + t.same(connector.instances.size, 1); + const newInstance = connector.instances.get( + 'db.example.com-PASSWORD-PUBLIC' + ).instance; + t.same(newInstance.instanceInfo.domainName, 'db.example.com'); + t.same(newInstance.instanceInfo.instanceId, 'instance'); +});