Skip to content

Commit

Permalink
feat: Add dynamic retrieval for client password (#1926)
Browse files Browse the repository at this point in the history
* 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.

* 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: Add tests for dynamic password

* test: Simplify testAsync error handling

* fix: Clean up dynamic password error handling and misc style

* test: Remove extra semicolons

* test: Change testAsync(...) calls to use arrow functions

* fix: Wrap self.password() invocation in an arrow function

* test: Add a comment to testAsync(...)
  • Loading branch information
sehrope authored and brianc committed Jul 25, 2019
1 parent 0acaf9d commit 0894a3c
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 1 deletion.
19 changes: 18 additions & 1 deletion lib/client.js
Expand Up @@ -114,7 +114,24 @@ Client.prototype._connect = function (callback) {

function checkPgPass (cb) {
return function (msg) {
if (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
}
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 {
pgPass(self.connectionParameters, function (pass) {
Expand Down
113 changes: 113 additions & 0 deletions 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', () => {
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', () => {
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', () => {
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', () => {
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', () => {
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()
})
})
14 changes: 14 additions & 0 deletions test/suite.js
Expand Up @@ -72,6 +72,20 @@ class Suite {
const test = new Test(name, cb)
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()
.then(action)
.then(() => cb(null), cb)
})
this._queue.push(test)
}
}

process.on('unhandledRejection', (e) => {
Expand Down

0 comments on commit 0894a3c

Please sign in to comment.