From 83e8fa592fba3ef07d4bee75a3cf506afd6d99bc Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Wed, 17 Jul 2019 12:59:18 -0400 Subject: [PATCH 1/9] feat: Add dynamic retrieval for client password Adds option to specify a function for a client password. When the client is connected, if the value of password is a function then it is invoked to get the password to use for that connection. The function must return either a string or a Promise that resolves to a string. If the function throws or rejects with an error then it will be bubbled up to the client. --- lib/client.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 147637ee7..3661b7bd1 100644 --- a/lib/client.js +++ b/lib/client.js @@ -114,7 +114,28 @@ Client.prototype._connect = function (callback) { function checkPgPass (cb) { return function (msg) { - if (self.password !== null) { + if (typeof(self.password) === 'function') { + try { + self._Promise.resolve(self.password()) + .then(pass => { + if (undefined !== pass) { + if (typeof(pass) !== 'string') { + con.emit('error', new TypeError('Password must be a string')) + return + } + self.connectionParameters.password = self.password = pass + } else { + self.connectionParameters.password = self.password = null + } + cb(msg) + }) + .catch(err => { + con.emit('error', err) + }) + } catch (err) { + con.emit('error', err) + } + } else if (self.password !== null) { cb(msg) } else { pgPass(self.connectionParameters, function (pass) { From 1d331b41ecae3104568d31215641089a4de8f60c Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Thu, 18 Jul 2019 15:09:56 -0400 Subject: [PATCH 2/9] test: Add testAsync() helper to Suite Add testAsync() helper function to Suite to simplify running tests that return a Promise. The test action is executed and if a syncronous error is thrown then it is immediately considered failed. If the Promise resolves successfully then the test is considered successful. If the Promise rejects with an Error then the test is considered failed. --- test/suite.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/suite.js b/test/suite.js index eca96159e..5960f2327 100644 --- a/test/suite.js +++ b/test/suite.js @@ -72,6 +72,19 @@ class Suite { const test = new Test(name, cb) this._queue.push(test) } + + testAsync (name, action) { + const test = new Test(name, cb => { + try { + Promise.resolve(action()) + .then(() => cb(null)) + .catch((err) => cb(err)) + } catch (err) { + cb(err) + } + }) + this._queue.push(test) + } } process.on('unhandledRejection', (e) => { From 2d7f9fd4c03dce3ff403b80d165f21077f851c36 Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Thu, 18 Jul 2019 15:12:58 -0400 Subject: [PATCH 3/9] test: Add tests for dynamic password --- .../connection/dynamic-password.js | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/integration/connection/dynamic-password.js diff --git a/test/integration/connection/dynamic-password.js b/test/integration/connection/dynamic-password.js new file mode 100644 index 000000000..852d24ae8 --- /dev/null +++ b/test/integration/connection/dynamic-password.js @@ -0,0 +1,113 @@ +'use strict' +const assert = require('assert') +const helper = require('./../test-helper') +const suite = new helper.Suite() +const pg = require('../../../lib/index'); +const Client = pg.Client; + +const password = process.env.PGPASSWORD || null; +const sleep = millis => new Promise(resolve => setTimeout(resolve, millis)) + +suite.testAsync('Get password from a sync function', function () { + let wasCalled = false + function getPassword() { + wasCalled = true + return password; + } + const client = new Client({ + password: getPassword, + }) + return client.connect() + .then(() => { + assert.ok(wasCalled, 'Our password function should have been called') + return client.end() + }) +}) + +suite.testAsync('Throw error from a sync function', function () { + let wasCalled = false + const myError = new Error('Oops!') + function getPassword() { + wasCalled = true + throw myError + } + const client = new Client({ + password: getPassword, + }) + let wasThrown = false + return client.connect() + .catch(err => { + assert.equal(err, myError, 'Our sync error should have been thrown') + wasThrown = true + }) + .then(() => { + assert.ok(wasCalled, 'Our password function should have been called') + assert.ok(wasThrown, 'Our error should have been thrown') + return client.end() + }) +}) + +suite.testAsync('Get password from a function asynchronously', function () { + let wasCalled = false + function getPassword() { + wasCalled = true + return sleep(100).then(() => password) + } + const client = new Client({ + password: getPassword, + }) + return client.connect() + .then(() => { + assert.ok(wasCalled, 'Our password function should have been called') + return client.end() + }) +}) + +suite.testAsync('Throw error from an async function', function () { + let wasCalled = false + const myError = new Error('Oops!') + function getPassword() { + wasCalled = true + return sleep(100).then(() => { + throw myError + }) + } + const client = new Client({ + password: getPassword, + }) + let wasThrown = false + return client.connect() + .catch(err => { + assert.equal(err, myError, 'Our async error should have been thrown') + wasThrown = true + }) + .then(() => { + assert.ok(wasCalled, 'Our password function should have been called') + assert.ok(wasThrown, 'Our error should have been thrown') + return client.end() + }) +}) + +suite.testAsync('Password function must return a string', function () { + let wasCalled = false + function getPassword() { + wasCalled = true + // Return a password that is not a string + return 12345 + } + const client = new Client({ + password: getPassword, + }) + let wasThrown = false + return client.connect() + .catch(err => { + assert.ok(err instanceof TypeError, 'A TypeError should have been thrown') + assert.equal(err.message, 'Password must be a string') + wasThrown = true + }) + .then(() => { + assert.ok(wasCalled, 'Our password function should have been called') + assert.ok(wasThrown, 'Our error should have been thrown') + return client.end() + }) +}) From bafdad516075971e118f07e3c7e749708a8c465a Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Fri, 19 Jul 2019 08:18:59 -0400 Subject: [PATCH 4/9] test: Simplify testAsync error handling --- test/suite.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/suite.js b/test/suite.js index 5960f2327..e6671e198 100644 --- a/test/suite.js +++ b/test/suite.js @@ -75,13 +75,9 @@ class Suite { testAsync (name, action) { const test = new Test(name, cb => { - try { - Promise.resolve(action()) - .then(() => cb(null)) - .catch((err) => cb(err)) - } catch (err) { - cb(err) - } + Promise.resolve() + .then(action) + .then(() => cb(null), cb) }) this._queue.push(test) } From afc73882436b43d68fcf8a0ea184aa59fe64414e Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Fri, 19 Jul 2019 08:23:34 -0400 Subject: [PATCH 5/9] fix: Clean up dynamic password error handling and misc style --- lib/client.js | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/lib/client.js b/lib/client.js index 3661b7bd1..e6c97c5f8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -114,27 +114,23 @@ Client.prototype._connect = function (callback) { function checkPgPass (cb) { return function (msg) { - if (typeof(self.password) === 'function') { - try { - self._Promise.resolve(self.password()) - .then(pass => { - if (undefined !== pass) { - if (typeof(pass) !== 'string') { - con.emit('error', new TypeError('Password must be a string')) - return - } - self.connectionParameters.password = self.password = pass - } else { - self.connectionParameters.password = self.password = null + if (typeof self.password === 'function') { + self._Promise.resolve() + .then(self.password) + .then(pass => { + if (pass !== undefined) { + if (typeof pass !== 'string') { + con.emit('error', new TypeError('Password must be a string')) + return } - cb(msg) - }) - .catch(err => { - con.emit('error', err) - }) - } catch (err) { - con.emit('error', err) - } + self.connectionParameters.password = self.password = pass + } else { + self.connectionParameters.password = self.password = null + } + cb(msg) + }).catch(err => { + con.emit('error', err) + }) } else if (self.password !== null) { cb(msg) } else { From 14e4fb5d3816bd7c40730a20e035133ee4fda182 Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Fri, 19 Jul 2019 08:24:29 -0400 Subject: [PATCH 6/9] test: Remove extra semicolons --- test/integration/connection/dynamic-password.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/connection/dynamic-password.js b/test/integration/connection/dynamic-password.js index 852d24ae8..22fb25f6e 100644 --- a/test/integration/connection/dynamic-password.js +++ b/test/integration/connection/dynamic-password.js @@ -2,17 +2,17 @@ const assert = require('assert') const helper = require('./../test-helper') const suite = new helper.Suite() -const pg = require('../../../lib/index'); +const pg = require('../../../lib/index') const Client = pg.Client; -const password = process.env.PGPASSWORD || null; +const password = process.env.PGPASSWORD || null const sleep = millis => new Promise(resolve => setTimeout(resolve, millis)) suite.testAsync('Get password from a sync function', function () { let wasCalled = false function getPassword() { wasCalled = true - return password; + return password } const client = new Client({ password: getPassword, From fe5a2463bb0c12a05f7ccbcac9a7ef51a48ac6ef Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Fri, 19 Jul 2019 08:25:37 -0400 Subject: [PATCH 7/9] test: Change testAsync(...) calls to use arrow functions --- test/integration/connection/dynamic-password.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/connection/dynamic-password.js b/test/integration/connection/dynamic-password.js index 22fb25f6e..ebda433c1 100644 --- a/test/integration/connection/dynamic-password.js +++ b/test/integration/connection/dynamic-password.js @@ -8,7 +8,7 @@ const Client = pg.Client; const password = process.env.PGPASSWORD || null const sleep = millis => new Promise(resolve => setTimeout(resolve, millis)) -suite.testAsync('Get password from a sync function', function () { +suite.testAsync('Get password from a sync function', () => { let wasCalled = false function getPassword() { wasCalled = true @@ -24,7 +24,7 @@ suite.testAsync('Get password from a sync function', function () { }) }) -suite.testAsync('Throw error from a sync function', function () { +suite.testAsync('Throw error from a sync function', () => { let wasCalled = false const myError = new Error('Oops!') function getPassword() { @@ -47,7 +47,7 @@ suite.testAsync('Throw error from a sync function', function () { }) }) -suite.testAsync('Get password from a function asynchronously', function () { +suite.testAsync('Get password from a function asynchronously', () => { let wasCalled = false function getPassword() { wasCalled = true @@ -63,7 +63,7 @@ suite.testAsync('Get password from a function asynchronously', function () { }) }) -suite.testAsync('Throw error from an async function', function () { +suite.testAsync('Throw error from an async function', () => { let wasCalled = false const myError = new Error('Oops!') function getPassword() { @@ -88,7 +88,7 @@ suite.testAsync('Throw error from an async function', function () { }) }) -suite.testAsync('Password function must return a string', function () { +suite.testAsync('Password function must return a string', () => { let wasCalled = false function getPassword() { wasCalled = true From 3b1c2e46a9839ea8d69e9375bfdfeb1a654f642a Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Fri, 19 Jul 2019 09:01:57 -0400 Subject: [PATCH 8/9] fix: Wrap self.password() invocation in an arrow function --- lib/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index e6c97c5f8..cca5e66e8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -116,7 +116,7 @@ Client.prototype._connect = function (callback) { return function (msg) { if (typeof self.password === 'function') { self._Promise.resolve() - .then(self.password) + .then(() => self.password()) .then(pass => { if (pass !== undefined) { if (typeof pass !== 'string') { From e1d71f8f1daa7228bd9084ed7029741b3b04b069 Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Fri, 19 Jul 2019 09:29:06 -0400 Subject: [PATCH 9/9] test: Add a comment to testAsync(...) --- test/suite.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/suite.js b/test/suite.js index e6671e198..4161ddc0a 100644 --- a/test/suite.js +++ b/test/suite.js @@ -73,6 +73,11 @@ class Suite { this._queue.push(test) } + /** + * Run an async test that can return a Promise. If the Promise resolves + * successfully then the test will pass. If the Promise rejects with an + * error then the test will be considered failed. + */ testAsync (name, action) { const test = new Test(name, cb => { Promise.resolve()