UNPKG

16.9 kBJavaScriptView Raw
1const test = require('tape')
2const {stub, useFakeTimers} = require('sinon')
3
4const jwt = require('jsonwebtoken')
5
6const request = require('request-promise-native')
7
8const FBJWTClient = require('./fb-jwt-client')
9
10/* test values */
11const userId = 'testUserId'
12const userToken = 'testUserToken'
13const serviceSlug = 'testServiceSlug'
14const serviceSecret = 'testServiceSecret'
15const serviceToken = 'testServiceToken'
16const microserviceUrl = 'https://microservice'
17const createEndpointUrl = `${microserviceUrl}/service/${serviceSlug}/user/${userId}`
18const data = {foo: 'bar'}
19const encryptedData = 'RRqDeJRQlZULKx1NYql/imRmDsy9AZshKozgLuY='
20const userIdTokenData = {userId, userToken}
21const encryptedUserIdTokenData = 'Ejo7ypk1TFQNAbbkUFW8NeQhcZt1Wxf1IJNLhDjbtpoUdfluylSqWDCRXuulEqMiCdiQzhjIeLHANj9mMK0sMl6jTA=='
22const expectedEncryptedData = 'pOXXs5YW9mUW1weBLNawiMRFdk6Hh92YBfGqmg8ych8PqnZ5l8JbcqHXHKjmcrKYJqZXn53sFr/eCq7Mbh5j9rj87w=='
23
24// Ensure that client is properly instantiated
25
26/**
27 * Convenience function for testing client instantiation
28 *
29 * @param {object} t
30 * Object containing tape methods
31 *
32 * @param {array} params
33 * Arguments to pass to client constructor
34 *
35 * @param {string} expectedCode
36 * Error code expected to be returned by client
37 *
38 * @param {string} expectedMessage
39 * Error message expected to be returned by client
40 *
41 * @return {undefined}
42 *
43 **/
44const testInstantiation = (t, params, expectedCode, expectedMessage) => {
45 t.plan(4)
46
47 let failedClient
48 try {
49 failedClient = new FBJWTClient(...params)
50 } catch (e) {
51 t.equal(e.name, 'FBJWTClientError', 'it should return an error of the correct type')
52 t.equal(e.code, expectedCode, 'it should return the correct error code')
53 t.equal(e.message, expectedMessage, 'it should return the correct error message')
54 }
55 t.equal(failedClient, undefined, 'it should not return an instantiated client')
56}
57
58test('When instantiating client without a service secret', t => {
59 testInstantiation(t, [], 'ENOSERVICESECRET', 'No service secret passed to client')
60})
61
62test('When instantiating client without a service token', t => {
63 testInstantiation(t, [serviceSecret], 'ENOSERVICETOKEN', 'No service token passed to client')
64})
65
66test('When instantiating client without a service slug', t => {
67 testInstantiation(t, [serviceSecret, serviceToken], 'ENOSERVICESLUG', 'No service slug passed to client')
68})
69
70test('When instantiating client without a service url', t => {
71 testInstantiation(t, [serviceSecret, serviceToken, serviceSlug], 'ENOMICROSERVICEURL', 'No microservice url passed to client')
72})
73
74test('When instantiating client with a custom error', t => {
75 t.plan(1)
76
77 class MyError extends FBJWTClient.prototype.ErrorClass {}
78 try {
79 const jwtClient = new FBJWTClient(null, serviceToken, serviceSlug, microserviceUrl, MyError) // eslint-disable-line no-unused-vars
80 } catch (e) {
81 t.equal(e.name, 'MyError', 'it should use the error class passed')
82 }
83})
84
85// Set up a client to test the methods
86const jwtClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, microserviceUrl)
87
88// Endpoint URLs
89test('When asking for endpoint urls', t => {
90 const getUrl =
91 jwtClient.createEndpointUrl('/service/:serviceSlug/user/:userId', {serviceSlug, userId})
92 t.equal(getUrl, createEndpointUrl, 'it should return the correct value for the get endpoint')
93
94 t.end()
95})
96
97// JWT
98test('When generating json web token', async t => {
99 const clock = useFakeTimers({
100 now: 1483228800000
101 })
102 const accessToken = jwtClient.generateAccessToken({data: 'testData'})
103 const decodedAccessToken = jwt.verify(accessToken, serviceToken)
104 t.equal(decodedAccessToken.checksum, 'b5118e71a8ed3abbc8c40d4058b0dd54b9410ffd56ef888f602ed10026c46a3a', 'it should output a token containing the checksum for the data')
105 t.equal(decodedAccessToken.iat, 1483228800, 'it should output a token containing the iat property')
106
107 clock.restore()
108 t.end()
109})
110
111test('Wnen creating request options', t => {
112 const generateAccessTokenStub = stub(jwtClient, 'generateAccessToken')
113 generateAccessTokenStub.callsFake(() => 'testAccessToken')
114 const requestOptions = jwtClient.createRequestOptions('/foo', {}, {foo: 'bar'})
115 t.deepEqual(requestOptions, {
116 url: 'https://microservice/foo',
117 headers: {'x-access-token': 'testAccessToken'},
118 json: {foo: 'bar'}
119 }, 'it should set the correct url, headers and json object')
120
121 const requestGetOptions = jwtClient.createRequestOptions('/foo', {}, {foo: 'bar'}, true)
122 t.deepEqual(requestGetOptions, {
123 url: 'https://microservice/foo',
124 headers: {'x-access-token': 'testAccessToken'},
125 json: true,
126 qs: {payload: 'eyJmb28iOiJiYXIifQ=='}
127 }, 'and when a querystring is specified, it should set json option to true and the qs option to the payload’s value')
128 generateAccessTokenStub.restore()
129 t.end()
130})
131
132// Decrypting user data
133test('When decrypting data', async t => {
134 const decryptedData = jwtClient.decrypt(userToken, encryptedData)
135 t.deepEqual(data, decryptedData, 'it should return the correct data from valid encrypted input')
136
137 t.end()
138})
139
140test('When decrypting invalid data', async t => {
141 t.plan(4)
142 let invalidData
143 try {
144 invalidData = jwtClient.decrypt(userToken, 'invalid')
145 } catch (e) {
146 t.equal(e.name, 'FBJWTClientError', 'it should return an error object of the correct type')
147 t.equal(e.code, 500, 'it should return correct error code')
148 t.equal(e.message, 'EINVALIDPAYLOAD', 'it should return the correct error message')
149 }
150 t.equal(invalidData, undefined, 'it should not return anything if data is invalid')
151
152 t.end()
153})
154
155// Encrypting data
156test('When encrypting data', async t => {
157 const encryptedData = jwtClient.encrypt(userToken, data)
158 const decryptedData = jwtClient.decrypt(userToken, encryptedData)
159 t.deepEqual(data, decryptedData, 'it should encrypt the data correctly')
160 // NB. have to decrypt the encryptedData to check
161 // since the Initialization Vector guarantees the output will be different each time
162
163 const encryptedDataAgain = jwtClient.encrypt(userToken, data)
164 t.notEqual(encryptedDataAgain, encryptedData, 'it should not return the same value for the same input')
165
166 t.end()
167})
168
169test('When encrypting data with a provided IV seed', async t => {
170 const encryptedData = jwtClient.encrypt(userToken, data, 'ivSeed')
171 const decryptedData = jwtClient.decrypt(userToken, encryptedData)
172 t.deepEqual(data, decryptedData, 'it should encrypt the data correctly')
173
174 const encryptedDataAgain = jwtClient.encrypt(userToken, data, 'ivSeed')
175 t.equal(encryptedDataAgain, encryptedData, 'it should return the same value for the same input')
176
177 t.end()
178})
179
180// Encrypting user ID and token
181test('When encrypting the user ID and token', async t => {
182 const encryptedData = jwtClient.encryptUserIdAndToken(userId, userToken)
183 t.equal(encryptedData, expectedEncryptedData, 'it should encrypt the data correctly')
184
185 const encryptedDataAgain = jwtClient.encryptUserIdAndToken(userId, userToken)
186 t.equal(encryptedDataAgain, encryptedData, 'it should return the same value for the same input')
187
188 t.end()
189})
190
191// Decrypting user ID and token
192test('When decrypting the user’s ID and token', async t => {
193 const decryptedData = jwtClient.decryptUserIdAndToken(encryptedUserIdTokenData)
194 t.deepEqual(userIdTokenData, decryptedData, 'it should return the correct data from valid encrypted input')
195
196 t.end()
197})
198
199test('When decrypting invalid user ID and token', async t => {
200 t.plan(4)
201 let invalidData
202 try {
203 invalidData = jwtClient.decryptUserIdAndToken(userToken, 'invalid')
204 } catch (e) {
205 t.equal(e.name, 'FBJWTClientError', 'it should return an error object of the correct type')
206 t.equal(e.code, 500, 'it should return correct error code')
207 t.equal(e.message, 'EINVALIDPAYLOAD', 'it should return the correct error message')
208 }
209 t.equal(invalidData, undefined, 'it should not return anything if data is invalid')
210
211 t.end()
212})
213
214// Sending gets
215test('When sending gets', async t => {
216 t.plan(7)
217
218 const stubAccessToken = stub(jwtClient, 'generateAccessToken')
219 stubAccessToken.callsFake(() => 'testAccessToken')
220 const stubRequest = stub(request, 'get')
221 stubRequest.callsFake(options => {
222 return Promise.resolve(data)
223 })
224
225 const fetchedData = await jwtClient.sendGet('/user/:userId', {userId})
226
227 const callArgs = stubRequest.getCall(0).args[0]
228
229 t.equal(callArgs.url, `${microserviceUrl}/user/testUserId`, 'it should call the correct url')
230 t.equal(callArgs.headers['x-access-token'], 'testAccessToken', 'it should add the correct x-access-token header')
231 t.equal(callArgs.json, true, 'it should expect a json response')
232 t.deepEqual(fetchedData, data, 'it should return the unencrypted data')
233 stubAccessToken.restore()
234
235 await jwtClient.sendGet('/user/:userId', {userId}, {foo: 'bar'})
236
237 const callArgsB = stubRequest.getCall(0).args[0]
238 // NB. querystring checking handled in createRequestOptions tests
239 // since qs options get stashed on request agent's internal self object
240 t.equal(callArgsB.url, `${microserviceUrl}/user/testUserId`, 'it should call the correct url')
241 t.equal(callArgsB.headers['x-access-token'], 'testAccessToken', 'it should add the correct x-access-token header')
242 t.equal(callArgsB.json, true, 'it should expect a json response')
243
244 stubAccessToken.restore()
245 stubRequest.restore()
246 t.end()
247})
248
249// Sending posts
250test('When sending posts', async t => {
251 t.plan(4)
252
253 const stubRequest = stub(request, 'post')
254 stubRequest.callsFake(options => {
255 return Promise.resolve({
256 response: 'body'
257 })
258 })
259
260 const generateAccessTokenStub = stub(jwtClient, 'generateAccessToken')
261 generateAccessTokenStub.callsFake(() => 'accessToken')
262
263 const responseBody = await jwtClient.sendPost('/user/:userId', {userId}, data)
264
265 const callArgs = stubRequest.getCall(0).args[0]
266 t.equal(callArgs.url, `${microserviceUrl}/user/testUserId`, 'it should call the correct url')
267 t.deepEqual(callArgs.json, data, 'it should post the correct data')
268 t.equal(callArgs.headers['x-access-token'], 'accessToken', 'it should add the x-access-token header')
269
270 t.deepEqual(responseBody, {response: 'body'}, 'it should return the response’s content')
271
272 stubRequest.restore()
273 generateAccessTokenStub.restore()
274 t.end()
275})
276
277/**
278 * Convenience function for testing client error handling
279 *
280 * Stubs request[stubMethod], creates error object response and tests
281 * - error name
282 * - error code
283 * - error message
284 * - data is undefined
285 *
286 * @param {function} clientMethod
287 * Function providing call to client method to execute with args pre-populated
288 *
289 * @param {string} stubMethod
290 * Request method to stub
291 *
292 * @param {object} t
293 * Object containing tape methods
294 *
295 * @param {number|string} requestErrorCode
296 * Error code or status code returned by request
297 *
298 * @param {number} [applicationErrorCode]
299 * Error code expoected to be thrown by client (defaults to requestErrorCode)
300 *
301 * @param {number} [expectedRequestErrorCode]
302 * Error code expoected to be thrown if no code is returned by client (defaults to requestErrorCode)
303 *
304 * @return {undefined}
305 *
306 **/
307const testError = async (clientMethod, stubMethod, t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode) => {
308 applicationErrorCode = applicationErrorCode || requestErrorCode
309
310 const error = {}
311
312 if (typeof requestErrorCode === 'string') {
313 error.error = {
314 name: requestErrorCode
315 }
316 } else {
317 error.statusCode = requestErrorCode
318 }
319
320 expectedRequestErrorCode = expectedRequestErrorCode || requestErrorCode
321
322 const stubRequest = stub(request, stubMethod)
323 stubRequest.callsFake(options => {
324 return Promise.reject(error)
325 })
326
327 t.plan(4)
328 let decryptedData
329 try {
330 decryptedData = await clientMethod()
331 } catch (e) {
332 t.equal(e.name, 'FBJWTClientError', 'it should return an error object of the correct type')
333 t.equal(e.code, applicationErrorCode, `it should return correct error code (${applicationErrorCode})`)
334 t.equal(e.message, expectedRequestErrorCode, `it should return the correct error message (${expectedRequestErrorCode})`)
335 }
336 t.equal(decryptedData, undefined, 'it should not return a value for the data')
337
338 stubRequest.restore()
339}
340
341// Convenience function for testing client's sendGet method - calls generic testError function
342// Params same as for testError, minus the clientMethod and stubMethod ones
343const testGetError = async (t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode) => {
344 const clientMethod = async () => {
345 return jwtClient.sendGet('/url', {})
346 }
347 testError(clientMethod, 'get', t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode)
348}
349
350// Convenience function for testing client's sendPost method - calls generic testError function
351// Params same as for testError, minus the clientMethod and stubMethod one
352const testPostError = async (t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode) => {
353 const clientMethod = async () => {
354 return jwtClient.sendPost('/url', {}, data)
355 }
356 testError(clientMethod, 'post', t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode)
357}
358
359// Test all the errors for jwtClient.sendGet
360
361test('When requesting a resource that does not exist', async t => {
362 testGetError(t, 404)
363})
364
365test('When making an unauthorized get request', async t => {
366 testGetError(t, 401)
367})
368
369test('When making an invalid get request', async t => {
370 testGetError(t, 403)
371})
372
373test('When get endpoint cannot be reached', async t => {
374 testGetError(t, 'ECONNREFUSED', 503)
375})
376
377test('When dns resolution for get endpoint fails', async t => {
378 testGetError(t, 'ENOTFOUND', 502)
379})
380
381test('When making a get request and an unspecified error code is returned', async t => {
382 testGetError(t, 'e.madeup', 500)
383})
384
385test('When making a get request and an error object without error code is returned', async t => {
386 testGetError(t, '', 500, 'EUNSPECIFIED')
387})
388
389test('When making a get request and an error occurs but no error code is present', async t => {
390 testGetError(t, undefined, 500, 'ENOERROR')
391})
392
393// Test all the errors for jwtClient.sendPost
394
395test('When making an unauthorized post request', async t => {
396 testPostError(t, 401)
397})
398
399test('When making an invalid post request', async t => {
400 testPostError(t, 403)
401})
402
403test('When post endpoint cannot be reached', async t => {
404 testPostError(t, 'ECONNREFUSED', 503)
405})
406
407test('When dns resolution for post endpoint fails', async t => {
408 testPostError(t, 'ENOTFOUND', 502)
409})
410
411test('When making a post request and an unspecified error code is returned', async t => {
412 testPostError(t, 'e.madeup', 500)
413})
414
415test('When making a post request and an error object without error code is returned', async t => {
416 testPostError(t, '', 500, 'EUNSPECIFIED')
417})
418
419test('When making a post request and an error occurs but no error code is present', async t => {
420 testPostError(t, undefined, 500, 'ENOERROR')
421})
422
423test('When making a request and both a status code and an error object containing a name are present', async t => {
424 t.plan(1)
425 const stubRequest = stub(request, 'get')
426 stubRequest.callsFake(options => {
427 let error = new Error()
428 error.statusCode = 400
429 error.error = {
430 name: 'e.error.name',
431 code: 400
432 }
433 return Promise.reject(error)
434 })
435
436 try {
437 await jwtClient.sendGet('/url', {})
438 } catch (e) {
439 t.equals(e.message, 'e.error.name', 'it should use the name specified in the error object as the message')
440 }
441 stubRequest.restore()
442})
443
444test('When making a request and both a status code and an error object with a code but no name are present', async t => {
445 t.plan(1)
446 const stubRequest = stub(request, 'get')
447 stubRequest.callsFake(options => {
448 let error = new Error()
449 error.statusCode = 400
450 error.error = {
451 code: 409
452 }
453 return Promise.reject(error)
454 })
455
456 try {
457 await jwtClient.sendGet('/url', {})
458 } catch (e) {
459 t.equals(e.message, 409, 'it should use the code specified in the error object as the message')
460 }
461 stubRequest.restore()
462})
463
464test('When making a request and both a status code and an error object with no name and no code are present', async t => {
465 t.plan(1)
466 const stubRequest = stub(request, 'get')
467 stubRequest.callsFake(options => {
468 let error = new Error()
469 error.statusCode = 400
470 error.error = {
471 foo: 409
472 }
473 return Promise.reject(error)
474 })
475
476 try {
477 await jwtClient.sendGet('/url', {})
478 } catch (e) {
479 t.equals(e.message, 'EUNSPECIFIED', 'it should use ‘EUNSPECIFIED‘ as the message')
480 }
481 stubRequest.restore()
482})
483
484// Rethrow errors
485
486test('When client handles an error that it created', async t => {
487 const thrown = new jwtClient.ErrorClass('Boom', {error: {code: 'EBOOM'}})
488 try {
489 jwtClient.handleRequestError(thrown)
490 } catch (e) {
491 t.equal(e, thrown, 'it should rethrow the error as is')
492 }
493 t.end()
494})