-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create and implement async/sync test helpers (#523)
It is difficult to write tests that ensure that both the asynchronous and synchronous calls to the sign and verify functions had the same result. These helpers ensure that the calls are the same and return the common result. As a proof of concept, the iat claim tests have been updated to use the new helpers.
- Loading branch information
Showing
2 changed files
with
150 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,125 @@ | ||
'use strict'; | ||
|
||
const jwt = require('../'); | ||
const expect = require('chai').expect; | ||
const sinon = require('sinon'); | ||
|
||
/** | ||
* Correctly report errors that occur in an asynchronous callback | ||
* @param {function(err): void} done The mocha callback | ||
* @param {function(): void} testFunction The assertions function | ||
*/ | ||
function asyncCheck(done, testFunction) { | ||
try { | ||
testFunction(); | ||
done(); | ||
} | ||
catch(err) { | ||
done(err); | ||
} | ||
} | ||
|
||
/** | ||
* Assert that two errors are equal | ||
* @param e1 {Error} The first error | ||
* @param e2 {Error} The second error | ||
*/ | ||
// chai does not do deep equality on errors: https://github.com/chaijs/chai/issues/1009 | ||
function expectEqualError(e1, e2) { | ||
// message and name are not always enumerable, so manually reference them | ||
expect(e1.message, 'Async/Sync Error equality: message').to.equal(e2.message); | ||
expect(e1.name, 'Async/Sync Error equality: name').to.equal(e2.name); | ||
|
||
// compare other enumerable error properties | ||
for(const propertyName in e1) { | ||
expect(e1[propertyName], `Async/Sync Error equality: ${propertyName}`).to.deep.equal(e2[propertyName]); | ||
} | ||
} | ||
|
||
/** | ||
* Base64-url encode a string | ||
* @param str {string} The string to encode | ||
* @returns {string} The encoded string | ||
*/ | ||
function base64UrlEncode(str) { | ||
return Buffer.from(str).toString('base64') | ||
.replace(/\=/g, "") | ||
.replace(/[=]/g, "") | ||
.replace(/\+/g, "-") | ||
.replace(/\//g, "_") | ||
; | ||
} | ||
|
||
/** | ||
* Verify a JWT, ensuring that the asynchronous and synchronous calls to `verify` have the same result | ||
* @param {string} jwtString The JWT as a string | ||
* @param {string} secretOrPrivateKey The shared secret or private key | ||
* @param {object} options Verify options | ||
* @param {function(err, token):void} callback | ||
*/ | ||
function verifyJWTHelper(jwtString, secretOrPrivateKey, options, callback) { | ||
// freeze the time to ensure the clock remains stable across the async and sync calls | ||
const fakeClock = sinon.useFakeTimers({now: Date.now()}); | ||
let error; | ||
let syncVerified; | ||
try { | ||
syncVerified = jwt.verify(jwtString, secretOrPrivateKey, options); | ||
} | ||
catch (err) { | ||
error = err; | ||
} | ||
jwt.verify(jwtString, secretOrPrivateKey, options, (err, asyncVerifiedToken) => { | ||
try { | ||
if (error) { | ||
expectEqualError(err, error); | ||
callback(err); | ||
} | ||
else { | ||
expect(syncVerified, 'Async/Sync token equality').to.deep.equal(asyncVerifiedToken); | ||
callback(null, syncVerified); | ||
} | ||
} | ||
finally { | ||
if (fakeClock) { | ||
fakeClock.restore(); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Sign a payload to create a JWT, ensuring that the asynchronous and synchronous calls to `sign` have the same result | ||
* @param {object} payload The JWT payload | ||
* @param {string} secretOrPrivateKey The shared secret or private key | ||
* @param {object} options Sign options | ||
* @param {function(err, token):void} callback | ||
*/ | ||
function signJWTHelper(payload, secretOrPrivateKey, options, callback) { | ||
// freeze the time to ensure the clock remains stable across the async and sync calls | ||
const fakeClock = sinon.useFakeTimers({now: Date.now()}); | ||
let error; | ||
let syncSigned; | ||
try { | ||
syncSigned = jwt.sign(payload, secretOrPrivateKey, options); | ||
} | ||
catch (err) { | ||
error = err; | ||
} | ||
jwt.sign(payload, secretOrPrivateKey, options, (err, asyncSigned) => { | ||
fakeClock.restore(); | ||
if (error) { | ||
expectEqualError(err, error); | ||
callback(err); | ||
} | ||
else { | ||
expect(syncSigned, 'Async/Sync token equality').to.equal(asyncSigned); | ||
callback(null, syncSigned); | ||
} | ||
}); | ||
} | ||
|
||
module.exports = { | ||
asyncCheck, | ||
base64UrlEncode, | ||
signJWTHelper, | ||
verifyJWTHelper, | ||
}; |