UNPKG

26.9 kBJavaScriptView Raw
1const test = require('tape')
2const {stub, useFakeTimers} = require('sinon')
3const nock = require('nock')
4
5const jwt = require('jsonwebtoken')
6
7const got = require('got')
8
9const FBJWTClient = require('./fb-jwt-client')
10
11/* test values */
12const userId = 'testUserId'
13const userToken = 'testUserToken'
14const serviceSlug = 'testServiceSlug'
15const serviceSecret = 'testServiceSecret'
16const serviceToken = 'testServiceToken'
17const microserviceUrl = 'https://microservice'
18const createEndpointUrl = `${microserviceUrl}/service/${serviceSlug}/user/${userId}`
19const data = {foo: 'bar'}
20const encryptedData = 'RRqDeJRQlZULKx1NYql/imRmDsy9AZshKozgLuY='
21const userIdTokenData = {userId, userToken}
22const encryptedUserIdTokenData = 'Ejo7ypk1TFQNAbbkUFW8NeQhcZt1Wxf1IJNLhDjbtpoUdfluylSqWDCRXuulEqMiCdiQzhjIeLHANj9mMK0sMl6jTA=='
23const expectedEncryptedData = 'pOXXs5YW9mUW1weBLNawiMRFdk6Hh92YBfGqmg8ych8PqnZ5l8JbcqHXHKjmcrKYJqZXn53sFr/eCq7Mbh5j9rj87w=='
24
25// Ensure that client is properly instantiated
26
27/**
28 * Convenience function for testing client instantiation
29 *
30 * @param {object} t
31 * Object containing tape methods
32 *
33 * @param {array} params
34 * Arguments to pass to client constructor
35 *
36 * @param {string} expectedCode
37 * Error code expected to be returned by client
38 *
39 * @param {string} expectedMessage
40 * Error message expected to be returned by client
41 *
42 * @return {undefined}
43 *
44 **/
45const testInstantiation = (t, params, expectedCode, expectedMessage) => {
46 let failedClient
47 try {
48 t.throws(failedClient = new FBJWTClient(...params))
49 } catch (e) {
50 t.equal(e.name, 'FBJWTClientError', 'it should return an error of the correct type')
51 t.equal(e.code, expectedCode, 'it should return the correct error code')
52 t.equal(e.message, expectedMessage, 'it should return the correct error message')
53 }
54 t.equal(failedClient, undefined, 'it should not return an instantiated client')
55 t.end()
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 class MyError extends FBJWTClient.prototype.ErrorClass { }
76 try {
77 t.throws(new FBJWTClient(null, serviceToken, serviceSlug, microserviceUrl, MyError))
78 } catch (e) {
79 t.equal(e.name, 'MyError', 'it should use the error class passed')
80 }
81 t.end()
82})
83
84// Set up a client to test the methods
85const jwtClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, microserviceUrl)
86
87// Injecting metrics instrumentation
88test('When injecting metrics instrumentation', t => {
89 const apiMetrics = {}
90 const requestMetrics = {}
91
92 const metricsClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, microserviceUrl)
93 metricsClient.setMetricsInstrumentation(apiMetrics, requestMetrics)
94
95 t.equal(metricsClient.apiMetrics, apiMetrics, 'it should update the apiMetrics method')
96 t.equal(metricsClient.requestMetrics, requestMetrics, 'it should update the apiMetrics method')
97
98 t.end()
99})
100
101// Endpoint URLs
102test('When asking for endpoint urls', t => {
103 const getUrl =
104 jwtClient.createEndpointUrl('/service/:serviceSlug/user/:userId', {serviceSlug, userId})
105 t.equal(getUrl, createEndpointUrl, 'it should return the correct value for the get endpoint')
106
107 t.end()
108})
109
110// JWT
111test('When generating json web token', async t => {
112 const clock = useFakeTimers({
113 now: 1483228800000
114 })
115 const accessToken = jwtClient.generateAccessToken({data: 'testData'})
116 const decodedAccessToken = jwt.verify(accessToken, serviceToken)
117 t.equal(decodedAccessToken.checksum, 'b5118e71a8ed3abbc8c40d4058b0dd54b9410ffd56ef888f602ed10026c46a3a', 'it should output a token containing the checksum for the data')
118 t.equal(decodedAccessToken.iat, 1483228800, 'it should output a token containing the iat property')
119
120 clock.restore()
121 t.end()
122})
123
124test('Wnen creating request options', t => {
125 const generateAccessTokenStub = stub(jwtClient, 'generateAccessToken')
126 generateAccessTokenStub.callsFake(() => 'testAccessToken')
127 const requestOptions = jwtClient.createRequestOptions('/foo', {}, {foo: 'bar'})
128 t.deepEqual(requestOptions, {
129 url: 'https://microservice/foo',
130 headers: {'x-access-token': 'testAccessToken'},
131 body: {foo: 'bar'},
132 json: true
133 }, 'it should set the correct url, headers and json object')
134
135 const requestGetOptions = jwtClient.createRequestOptions('/foo', {}, {foo: 'bar'}, true)
136 t.deepEqual(requestGetOptions, {
137 url: 'https://microservice/foo',
138 headers: {'x-access-token': 'testAccessToken'},
139 searchParams: {payload: 'eyJmb28iOiJiYXIifQ=='},
140 json: true
141 }, 'and when a querystring is specified, it should set json option to true and the searchParams option to the payload’s value')
142 generateAccessTokenStub.restore()
143 t.end()
144})
145
146// Decrypting user data
147test('When decrypting data', async t => {
148 const decryptedData = jwtClient.decrypt(userToken, encryptedData)
149 t.deepEqual(data, decryptedData, 'it should return the correct data from valid encrypted input')
150
151 t.end()
152})
153
154test('When decrypting invalid data', async t => {
155 try {
156 t.throws(jwtClient.decrypt(userToken, 'invalid'))
157 } catch (e) {
158 t.equal(e.name, 'FBJWTClientError', 'it should return an error object of the correct type')
159 t.equal(e.code, 500, 'it should return correct error code')
160 t.equal(e.message, 'EINVALIDPAYLOAD', 'it should return the correct error message')
161 }
162
163 t.end()
164})
165
166// Encrypting data
167test('When encrypting data', async t => {
168 const encryptedData = jwtClient.encrypt(userToken, data)
169 const decryptedData = jwtClient.decrypt(userToken, encryptedData)
170 t.deepEqual(data, decryptedData, 'it should encrypt the data correctly')
171 // NB. have to decrypt the encryptedData to check
172 // since the Initialization Vector guarantees the output will be different each time
173
174 const encryptedDataAgain = jwtClient.encrypt(userToken, data)
175 t.notEqual(encryptedDataAgain, encryptedData, 'it should not return the same value for the same input')
176
177 t.end()
178})
179
180test('When encrypting data with a provided IV seed', async t => {
181 const encryptedData = jwtClient.encrypt(userToken, data, 'ivSeed')
182 const decryptedData = jwtClient.decrypt(userToken, encryptedData)
183 t.deepEqual(data, decryptedData, 'it should encrypt the data correctly')
184
185 const encryptedDataAgain = jwtClient.encrypt(userToken, data, 'ivSeed')
186 t.equal(encryptedDataAgain, encryptedData, 'it should return the same value for the same input')
187
188 t.end()
189})
190
191// Encrypting user ID and token
192test('When encrypting the user ID and token', async t => {
193 const encryptedData = jwtClient.encryptUserIdAndToken(userId, userToken)
194 t.equal(encryptedData, expectedEncryptedData, 'it should encrypt the data correctly')
195
196 const encryptedDataAgain = jwtClient.encryptUserIdAndToken(userId, userToken)
197 t.equal(encryptedDataAgain, encryptedData, 'it should return the same value for the same input')
198
199 t.end()
200})
201
202// Decrypting user ID and token
203test('When decrypting the user’s ID and token', async t => {
204 const decryptedData = jwtClient.decryptUserIdAndToken(encryptedUserIdTokenData)
205 t.deepEqual(userIdTokenData, decryptedData, 'it should return the correct data from valid encrypted input')
206
207 t.end()
208})
209
210test('When decrypting invalid user ID and token', async t => {
211 try {
212 t.throws(jwtClient.decryptUserIdAndToken(userToken, 'invalid'))
213 } catch (e) {
214 t.equal(e.name, 'FBJWTClientError', 'it should return an error object of the correct type')
215 t.equal(e.code, 500, 'it should return correct error code')
216 t.equal(e.message, 'EINVALIDPAYLOAD', 'it should return the correct error message')
217 }
218
219 t.end()
220})
221
222// Sending gets
223test('When sending gets', async t => {
224 const stubAccessToken = stub(jwtClient, 'generateAccessToken')
225 stubAccessToken.callsFake(() => 'testAccessToken')
226 const gotStub = stub(got, 'get')
227 gotStub.callsFake(options => {
228 return Promise.resolve({
229 body: data
230 })
231 })
232
233 const fetchedData = await jwtClient.sendGet({
234 url: '/user/:userId',
235 context: {userId}
236 })
237
238 const callArgs = gotStub.getCall(0).args[0]
239
240 t.equal(callArgs.url, `${microserviceUrl}/user/testUserId`, 'it should call the correct url')
241 t.equal(callArgs.headers['x-access-token'], 'testAccessToken', 'it should add the correct x-access-token header')
242 t.deepEqual(fetchedData, data, 'it should return the unencrypted data')
243 stubAccessToken.restore()
244
245 await jwtClient.sendGet({
246 url: '/user/:userId',
247 context: {userId},
248 payload: {foo: 'bar'}
249 })
250
251 const callArgsB = gotStub.getCall(0).args[0]
252 // NB. querystring checking handled in createRequestOptions tests
253 // since searchParams options get stashed on request agent's internal self object
254 t.equal(callArgsB.url, `${microserviceUrl}/user/testUserId`, 'it should call the correct url')
255 t.equal(callArgsB.headers['x-access-token'], 'testAccessToken', 'it should add the correct x-access-token header')
256
257 stubAccessToken.restore()
258 gotStub.restore()
259 t.end()
260})
261
262// Sending posts
263test('When sending posts', async t => {
264 const gotStub = stub(got, 'post')
265 gotStub.callsFake(options => {
266 return Promise.resolve({
267 body: {foo: 'bar'}
268 })
269 })
270
271 const generateAccessTokenStub = stub(jwtClient, 'generateAccessToken')
272 generateAccessTokenStub.callsFake(() => 'accessToken')
273
274 const responseBody = await jwtClient.sendPost({
275 url: '/user/:userId',
276 context: {userId},
277 payload: data
278 })
279
280 const callArgs = gotStub.getCall(0).args[0]
281 t.equal(callArgs.url, `${microserviceUrl}/user/testUserId`, 'it should call the correct url')
282 t.deepEqual(callArgs.body, data, 'it should post the correct data')
283 t.deepEqual(callArgs.headers['x-access-token'], 'accessToken', 'it should add the x-access-token header')
284
285 t.deepEqual(responseBody, {foo: 'bar'}, 'it should return the response’s body parsed as JSON')
286
287 gotStub.restore()
288 generateAccessTokenStub.restore()
289 t.end()
290})
291
292test('When calling an endpoint successfully', async t => {
293 const nockedClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, 'http://testdomain')
294
295 nock('http://testdomain')
296 .get('/route')
297 .reply(200, {success: true})
298
299 const apiMetricsEndStub = stub()
300 const apiMetricsStartTimerStub = stub(nockedClient.apiMetrics, 'startTimer')
301 apiMetricsStartTimerStub.callsFake(() => apiMetricsEndStub)
302
303 const requestMetricsEndStub = stub()
304 const requestMetricsStartTimerStub = stub(nockedClient.requestMetrics, 'startTimer')
305 requestMetricsStartTimerStub.callsFake(() => requestMetricsEndStub)
306
307 await nockedClient.send('get', {
308 url: '/route'
309 })
310
311 t.deepEqual(apiMetricsStartTimerStub.getCall(0).args[0], {
312 client_name: 'FBJWTClient',
313 base_url: 'http://testdomain',
314 url: '/route',
315 method: 'get'
316 }, 'it should start the instrumentation timer with the correct args')
317 t.deepEqual(apiMetricsEndStub.getCall(0).args[0], {status_code: 200}, 'it should stop the instrumentation timer with the correct args')
318
319 t.deepEqual(requestMetricsStartTimerStub.getCall(0).args[0], {
320 client_name: 'FBJWTClient',
321 base_url: 'http://testdomain',
322 url: '/route',
323 method: 'get'
324 }, 'it should start the instrumentation timer with the correct args')
325 t.deepEqual(requestMetricsEndStub.getCall(0).args[0], {status_code: 200}, 'it should stop the instrumentation timer with the correct args')
326
327 apiMetricsStartTimerStub.restore()
328 requestMetricsStartTimerStub.restore()
329
330 t.end()
331})
332
333test('When calling an endpoint unsuccessfully', async t => {
334 const nockedClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, 'http://testdomain')
335
336 nock('http://testdomain')
337 .get('/notfound')
338 .reply(404, {code: 404, name: 'ENOTFOUND'})
339
340 const apiMetricsEndStub = stub()
341 const apiMetricsStartTimerStub = stub(nockedClient.apiMetrics, 'startTimer')
342 apiMetricsStartTimerStub.callsFake(() => apiMetricsEndStub)
343
344 const requestMetricsEndStub = stub()
345 const requestMetricsStartTimerStub = stub(nockedClient.requestMetrics, 'startTimer')
346 requestMetricsStartTimerStub.callsFake(() => requestMetricsEndStub)
347
348 try {
349 t.throws(await nockedClient.send('get', {
350 url: '/notfound'
351 }, {error: () => {}}))
352 } catch (e) {
353 //
354 }
355
356 t.deepEqual(apiMetricsStartTimerStub.getCall(0).args[0], {
357 client_name: 'FBJWTClient',
358 base_url: 'http://testdomain',
359 url: '/notfound',
360 method: 'get'
361 }, 'it should start the instrumentation timer with the correct args')
362 t.deepEqual(apiMetricsEndStub.getCall(0).args[0], {status_code: 404, status_message: 'Not Found', error_name: 'HTTPError'}, 'it should stop the instrumentation timer with the correct args')
363
364 t.deepEqual(requestMetricsStartTimerStub.getCall(0).args[0], {
365 client_name: 'FBJWTClient',
366 base_url: 'http://testdomain',
367 url: '/notfound',
368 method: 'get'
369 }, 'it should start the instrumentation timer with the correct args')
370 t.deepEqual(requestMetricsEndStub.getCall(0).args[0], {status_code: 404}, 'it should stop the instrumentation timer with the correct args')
371
372 apiMetricsStartTimerStub.restore()
373 requestMetricsStartTimerStub.restore()
374
375 t.end()
376})
377
378test('When retrying an endpoint', async t => {
379 const retryClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, 'http://testdomain')
380
381 const apiMetricsEndStub = stub()
382 const apiMetricsStartTimerStub = stub(retryClient.apiMetrics, 'startTimer')
383 apiMetricsStartTimerStub.callsFake(() => apiMetricsEndStub)
384
385 const requestMetricsEndStub = stub()
386 const requestMetricsStartTimerStub = stub(retryClient.requestMetrics, 'startTimer')
387 requestMetricsStartTimerStub.callsFake(() => requestMetricsEndStub)
388
389 let retryCounter = 0
390 const retries = () => {
391 if (retryCounter) {
392 return 0
393 }
394 retryCounter++
395 return 1
396 }
397 try {
398 t.throws(await retryClient.send('get', {
399 url: '/missing',
400 sendOptions: {
401 retry: {retries}
402 }
403 }, {error: () => {}}))
404 } catch (e) {
405 //
406 }
407
408 t.deepEqual(apiMetricsStartTimerStub.getCall(0).args[0], {
409 client_name: 'FBJWTClient',
410 base_url: 'http://testdomain',
411 url: '/missing',
412 method: 'get'
413 }, 'it should start the instrumentation timer with the correct args')
414 t.deepEqual(apiMetricsEndStub.getCall(0).args[0], {status_code: undefined, error_name: 'RequestError'}, 'it should stop the instrumentation timer with the correct args')
415 t.equal(apiMetricsStartTimerStub.callCount, 1, 'it should start the api instrumentation once')
416 t.equal(apiMetricsEndStub.callCount, 1, 'it should end the api instrumentation once')
417
418 t.deepEqual(requestMetricsStartTimerStub.getCall(1).args[0], {
419 client_name: 'FBJWTClient',
420 base_url: 'http://testdomain',
421 url: '/missing',
422 method: 'get'
423 }, 'it should start the instrumentation timer with the correct args')
424 t.deepEqual(requestMetricsEndStub.getCall(0).args[0], {status_code: undefined, error_name: 'RequestError'}, 'it should stop the instrumentation timer with the correct args')
425
426 t.equal(requestMetricsStartTimerStub.callCount, 2, 'it should start the request instrumentation for each individual request')
427 t.equal(requestMetricsEndStub.callCount, 2, 'it should start the request instrumentation for each individual request')
428
429 apiMetricsStartTimerStub.restore()
430 requestMetricsStartTimerStub.restore()
431
432 t.end()
433})
434
435const getNockedResponse = (url, response, code = 201) => {
436 const serviceUrl = 'http://testdomain'
437 const nockedClient = new FBJWTClient(serviceSecret, serviceToken, serviceSlug, serviceUrl)
438
439 nock(serviceUrl)
440 .get(url)
441 .reply(code, response)
442
443 return nockedClient.send('get', {url})
444}
445
446test('When calling an endpoint that returns json as it body', async t => {
447 const response = await getNockedResponse('/json-body', {success: true})
448 t.deepEqual(response, {success: true}, 'it should return the body')
449 t.end()
450})
451
452test('When calling an endpoint that returns empty json as it body', async t => {
453 const response = await getNockedResponse('/empty-json-body', {})
454 t.deepEqual(response, {}, 'it should return the body')
455 t.end()
456})
457
458test('When calling an endpoint that returns an empty string as it body', async t => {
459 const response = await getNockedResponse('/empty-string-body', '')
460 t.deepEqual(response, {}, 'it should return an empty json object')
461 t.end()
462})
463
464test('When calling an endpoint that returns an empty string as it body', async t => {
465 const response = await getNockedResponse('/undefined-body')
466 t.deepEqual(response, {}, 'it should return an empty json object')
467 t.end()
468})
469
470test('When calling an endpoint that returns spaces as it body', async t => {
471 const response = await getNockedResponse('/spaces-body', ' ')
472 t.deepEqual(response, {}, 'it should return an empty json object')
473 t.end()
474})
475
476test('When calling an endpoint that returns spaces as it body', async t => {
477 try {
478 t.throws(await getNockedResponse('/non-empty-body', ' hello'))
479 } catch (e) {
480 t.deepEqual(e.name, 'FBJWTClientError', 'it should return an empty json object')
481 }
482
483 t.end()
484})
485
486/**
487 * Convenience function for testing client error handling
488 *
489 * Stubs request[stubMethod], creates error object response and tests
490 * - error name
491 * - error code
492 * - error message
493 * - data is undefined
494 *
495 * @param {function} clientMethod
496 * Function providing call to client method to execute with args pre-populated
497 *
498 * @param {string} stubMethod
499 * Request method to stub
500 *
501 * @param {object} t
502 * Object containing tape methods
503 *
504 * @param {number|string} requestErrorCode
505 * Error code or status code returned by request
506 *
507 * @param {number} [applicationErrorCode]
508 * Error code expoected to be thrown by client (defaults to requestErrorCode)
509 *
510 * @param {number} [expectedRequestErrorCode]
511 * Error code expoected to be thrown if no code is returned by client (defaults to requestErrorCode)
512 *
513 * @return {undefined}
514 *
515 **/
516const testError = async (clientMethod, stubMethod, t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode) => {
517 applicationErrorCode = applicationErrorCode || requestErrorCode
518
519 const error = {}
520
521 if (typeof requestErrorCode === 'string') {
522 error.body = {
523 name: requestErrorCode
524 }
525 } else {
526 error.statusCode = requestErrorCode
527 }
528
529 expectedRequestErrorCode = expectedRequestErrorCode || requestErrorCode
530
531 const gotStub = stub(got, stubMethod)
532 gotStub.callsFake(options => {
533 return Promise.reject(error)
534 })
535
536 try {
537 t.throws(await clientMethod())
538 } catch (e) {
539 t.equal(e.name, 'FBJWTClientError', 'it should return an error object of the correct type')
540 t.equal(e.code, applicationErrorCode, `it should return correct error code (${applicationErrorCode})`)
541 t.equal(e.message, expectedRequestErrorCode, `it should return the correct error message (${expectedRequestErrorCode})`)
542 }
543
544 gotStub.restore()
545 t.end()
546}
547
548// Convenience function for testing client's sendGet method - calls generic testError function
549// Params same as for testError, minus the clientMethod and stubMethod ones
550const testGetError = async (t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode) => {
551 const clientMethod = async () => {
552 return jwtClient.sendGet({
553 url: '/url'
554 })
555 }
556 testError(clientMethod, 'get', t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode)
557}
558
559// Convenience function for testing client's sendPost method - calls generic testError function
560// Params same as for testError, minus the clientMethod and stubMethod one
561const testPostError = async (t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode) => {
562 const clientMethod = async () => {
563 return jwtClient.sendPost({
564 url: '/url',
565 payload: data
566 })
567 }
568 testError(clientMethod, 'post', t, requestErrorCode, applicationErrorCode, expectedRequestErrorCode)
569}
570
571// Test all the errors for jwtClient.sendGet
572
573test('When requesting a resource that does not exist', async t => {
574 testGetError(t, 404)
575})
576
577test('When making an unauthorized get request', async t => {
578 testGetError(t, 401)
579})
580
581test('When making an invalid get request', async t => {
582 testGetError(t, 403)
583})
584
585test('When get endpoint cannot be reached', async t => {
586 testGetError(t, 'ECONNREFUSED', 503)
587})
588
589test('When dns resolution for get endpoint fails', async t => {
590 testGetError(t, 'ENOTFOUND', 502)
591})
592
593test('When making a get request and an unspecified error code is returned', async t => {
594 testGetError(t, 'e.madeup', 500)
595})
596
597test('When making a get request and an error object without error code is returned', async t => {
598 testGetError(t, '', 500, 'EUNSPECIFIED')
599})
600
601test('When making a get request and an error occurs but no error code is present', async t => {
602 testGetError(t, undefined, 500, 'ENOERROR')
603})
604
605// // Test all the errors for jwtClient.sendPost
606
607test('When making an unauthorized post request', async t => {
608 testPostError(t, 401)
609})
610
611test('When making an invalid post request', async t => {
612 testPostError(t, 403)
613})
614
615test('When post endpoint cannot be reached', async t => {
616 testPostError(t, 'ECONNREFUSED', 503)
617})
618
619test('When dns resolution for post endpoint fails', async t => {
620 testPostError(t, 'ENOTFOUND', 502)
621})
622
623test('When making a post request and an unspecified error code is returned', async t => {
624 testPostError(t, 'e.madeup', 500)
625})
626
627test('When making a post request and an error object without error code is returned', async t => {
628 testPostError(t, '', 500, 'EUNSPECIFIED')
629})
630
631test('When making a post request and an error occurs but no error code is present', async t => {
632 testPostError(t, undefined, 500, 'ENOERROR')
633})
634
635test('When making a request and both a status code and an error object containing a name are present', async t => {
636 const gotStub = stub(got, 'get')
637 gotStub.callsFake(options => {
638 const error = new Error()
639 error.statusCode = 400
640 error.statusMessage = 'boom'
641 error.error = {
642 name: 'e.error.name',
643 code: 400
644 }
645 return Promise.reject(error)
646 })
647
648 try {
649 t.throws(await jwtClient.sendGet({url: '/url'}))
650 } catch (e) {
651 t.equals(e.name, 'FBJWTClientError', 'it should use the name specified in the error object as the message')
652 t.equals(e.code, 400, 'it should use the name specified in the error object as the message')
653 t.equals(e.message, 'e.error.name', 'it should use the name specified in the error object as the message')
654 }
655
656 gotStub.restore()
657 t.end()
658})
659
660test('When making a request and both a status code and an error object with a code but no name are present', async t => {
661 const gotStub = stub(got, 'get')
662 gotStub.callsFake(options => {
663 const error = new Error()
664 error.statusCode = 400
665 error.error = {
666 code: 409
667 }
668 return Promise.reject(error)
669 })
670
671 try {
672 t.throws(await jwtClient.sendGet({url: '/url'}))
673 } catch (e) {
674 t.equals(e.message, 409, 'it should use the code specified in the error object as the message')
675 }
676
677 gotStub.restore()
678 t.end()
679})
680
681test('When making a request and both a status code and an error object with no name and no code are present', async t => {
682 const gotStub = stub(got, 'get')
683 gotStub.callsFake(options => {
684 const error = new Error()
685 error.statusCode = 400
686 error.error = {
687 foo: 409
688 }
689 return Promise.reject(error)
690 })
691
692 try {
693 t.throws(await jwtClient.sendGet({url: '/url'}))
694 } catch (e) {
695 t.equals(e.message, 'EUNSPECIFIED', 'it should use ‘EUNSPECIFIED‘ as the message')
696 }
697
698 gotStub.restore()
699 t.end()
700})
701
702// Rethrow errors
703
704test('When client handles an error that it created', async t => {
705 const thrown = new jwtClient.ErrorClass('Boom', {error: {code: 'EBOOM'}})
706 try {
707 t.throws(jwtClient.handleRequestError(thrown))
708 } catch (e) {
709 t.equal(e, thrown, 'it should rethrow the error as is')
710 }
711
712 t.end()
713})
714
715// Logging errors
716
717test('When a get request results in an error and a logger instance has been provided', async t => {
718 const gotStub = stub(got, 'get')
719 const error = new Error()
720 error.code = 'EBANG'
721 error.statusCode = 400
722 error.statusMessage = 'boom'
723 error.body = {
724 name: 'e.error.name',
725 message: 'bang!',
726 code: 400
727 }
728 gotStub.callsFake(options => {
729 return Promise.reject(error)
730 })
731
732 const logErrorStub = stub()
733 const logger = {
734 error: logErrorStub
735 }
736
737 try {
738 t.throws(await jwtClient.sendGet({url: '/url'}, logger))
739 } catch (e) {
740 const callArgs = logErrorStub.getCall(0).args
741 t.equals(logErrorStub.calledOnce, true, 'it should use the provided logger instance to log the error')
742 t.deepEquals(callArgs[0], {
743 name: 'jwt_api_request_error',
744 client_name: 'FBJWTClient',
745 url: '/url',
746 base_url: 'https://microservice',
747 method: 'get',
748 error
749 }, 'it should set the error returned as the err property of the object passed to the logger')
750 t.deepEquals(callArgs[1], 'JWT API request error: FBJWTClient: GET https://microservice/url - Error - EBANG - 400 - boom - {"name":"e.error.name","message":"bang!","code":400}', 'it should generate a message explaining the error')
751 }
752
753 gotStub.restore()
754 logErrorStub.resetHistory()
755 t.end()
756})
757
758test('When a post request results in an error and a logger instance has been provided', async t => {
759 const gotStub = stub(got, 'post')
760 const error = new Error()
761 error.gotOptions = {
762 headers: {
763 'x-something': 'x-something-value'
764 }
765 }
766 gotStub.callsFake(options => {
767 return Promise.reject(error)
768 })
769
770 const logErrorStub = stub()
771 const logger = {
772 error: logErrorStub
773 }
774
775 try {
776 t.throws(await jwtClient.sendPost({url: '/url'}, logger))
777 } catch (e) {
778 const callArgs = logErrorStub.getCall(0).args
779 t.equals(logErrorStub.calledOnce, true, 'it should use the provided logger instance to log the error')
780 t.deepEquals(callArgs[0], {
781 name: 'jwt_api_request_error',
782 client_name: 'FBJWTClient',
783 url: '/url',
784 base_url: 'https://microservice',
785 method: 'post',
786 error
787 }, 'it should set the error returned as the err property of the object passed to the logger')
788 t.deepEquals(callArgs[1], 'JWT API request error: FBJWTClient: POST https://microservice/url - Error - - - - ', 'it should generate a message explaining the error')
789 }
790
791 gotStub.restore()
792 t.end()
793})