diff --git a/package-lock.json b/package-lock.json index d14bd290..94d57d8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "yoti", - "version": "3.7.1", + "version": "3.7.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a834f247..fec1134e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yoti", - "version": "3.7.1", + "version": "3.7.2", "description": "Yoti NodeJS SDK for back-end integration", "author": "Yoti LTD (https://www.yoti.com/developers)", "license": "MIT", diff --git a/sonar-project.properties b/sonar-project.properties index 8e6c3934..8698c507 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey = yoti-web-sdk:node sonar.projectName = node-sdk -sonar.projectVersion = 3.7.1 +sonar.projectVersion = 3.7.2 sonar.exclusions=tests/**,examples/**,node_modules/**,coverage/** sonar.javascript.lcov.reportPaths=coverage/lcov.info sonar.verbose = true \ No newline at end of file diff --git a/src/request/request.builder.js b/src/request/request.builder.js index fe978b8b..ef043bb7 100644 --- a/src/request/request.builder.js +++ b/src/request/request.builder.js @@ -162,7 +162,7 @@ class RequestBuilder { // Merge provided query params with nonce and timestamp. const queryString = buildQueryString(Object.assign( - this.queryParams || {}, + this.queryParams, { nonce: uuid.v4(), timestamp: Date.now(), diff --git a/src/request/request.handler.js b/src/request/request.handler.js index d18140c0..6dd43b63 100644 --- a/src/request/request.handler.js +++ b/src/request/request.handler.js @@ -6,29 +6,46 @@ const yotiCommon = require('../yoti_common'); * Default HTTP request handler. * * @param {YotiRequest} yotiRequest + * @param {boolean} buffer Return the response as a Buffer. * * @returns {Promise} Resolves {YotiResponse} */ -module.exports.execute = yotiRequest => new Promise((resolve, reject) => { +module.exports.execute = (yotiRequest, buffer = false) => new Promise((resolve, reject) => { const request = superagent(yotiRequest.getMethod(), yotiRequest.getUrl()); if (yotiCommon.requestCanSendPayload(yotiRequest.getMethod())) { request.send(yotiRequest.getPayload().getPayloadJSON()); } + if (buffer === true) { + request.buffer(buffer); + } + if (yotiRequest.getHeaders()) { request.set(yotiRequest.getHeaders()); } request .then((response) => { - try { - const parsedResponse = JSON.parse(response.text); - const receipt = parsedResponse.receipt || null; - return resolve(new YotiResponse(parsedResponse, response.statusCode, receipt)); - } catch (err) { - return reject(err); + let parsedResponse = null; + let body = null; + let receipt = null; + + if (response.body instanceof Buffer) { + body = response.body; + parsedResponse = response.body; + } else if (response.text) { + body = response.text; + parsedResponse = response.headers['content-type'] ? response.body : JSON.parse(response.text); + receipt = parsedResponse.receipt || null; } + + return resolve(new YotiResponse( + parsedResponse, + response.statusCode, + receipt, + body + )); }) .catch((err) => { console.log(`Error getting data from Connect API: ${err.message}`); diff --git a/src/request/request.js b/src/request/request.js index 6ff9361c..6a4421b0 100644 --- a/src/request/request.js +++ b/src/request/request.js @@ -62,10 +62,12 @@ class YotiRequest { /** * Executes the request. * + * @param {boolean} buffer Return the response as a Buffer. + * * @returns {Promise} Resolves {YotiResponse} */ - execute() { - return requestHandler.execute(this); + execute(buffer = false) { + return requestHandler.execute(this, buffer); } } diff --git a/src/request/response.js b/src/request/response.js index c06337c0..027aedca 100644 --- a/src/request/response.js +++ b/src/request/response.js @@ -8,11 +8,13 @@ class YotiResponse { * @param {*} parsedResponse * @param {int} statusCode * @param {Object|null} receipt + * @param {Buffer|string|null} body */ - constructor(parsedResponse, statusCode, receipt = null) { + constructor(parsedResponse, statusCode, receipt = null, body = null) { this.parsedResponse = parsedResponse; this.statusCode = statusCode; this.receipt = receipt; + this.body = body; } /** @@ -29,6 +31,13 @@ class YotiResponse { return this.parsedResponse; } + /** + * @returns {Buffer|string|null} The response body. + */ + getBody() { + return this.body; + } + /** * @returns {int} Response status code. */ diff --git a/tests/request/request.builder.spec.js b/tests/request/request.builder.spec.js index 61c55a2b..a6803ed3 100644 --- a/tests/request/request.builder.spec.js +++ b/tests/request/request.builder.spec.js @@ -148,6 +148,30 @@ describe('RequestBuilder', () => { assertExpectedRequest(request, done); }); }); + describe('#withGet', () => { + it('should set method to GET', () => { + const request = new RequestBuilder() + .withBaseUrl(API_BASE_URL) + .withPemFilePath(PEM_FILE_PATH) + .withEndpoint(API_ENDPOINT) + .withGet() + .build(); + + expect(request.getMethod()).toBe('GET'); + }); + }); + describe('#withPost', () => { + it('should set method to POST', () => { + const request = new RequestBuilder() + .withBaseUrl(API_BASE_URL) + .withPemFilePath(PEM_FILE_PATH) + .withEndpoint(API_ENDPOINT) + .withPost() + .build(); + + expect(request.getMethod()).toBe('POST'); + }); + }); describe('#withHeader', () => { it('should only accept string header value', () => { expect(() => { diff --git a/tests/request/request.handler.spec.js b/tests/request/request.handler.spec.js new file mode 100644 index 00000000..176c6868 --- /dev/null +++ b/tests/request/request.handler.spec.js @@ -0,0 +1,176 @@ +const nock = require('nock'); +const fs = require('fs'); +const yotiRequestHandler = require('../../src/request/request.handler'); +const { RequestBuilder } = require('../../src/request/request.builder'); +const { Payload } = require('../../src/request/payload'); + +const SOME_BASE_URL = 'https://someapi.yoti.com'; +const SOME_ENDPOINT = '/some/endpoint'; +const SOME_ENDPOINT_REG_EXP = new RegExp(`^${SOME_ENDPOINT}`); +const SOME_PEM_STRING = fs.readFileSync('./tests/sample-data/keys/node-sdk-test.pem', 'utf8'); +const ALLOWED_METHODS = ['POST', 'PUT', 'PATCH', 'GET', 'DELETE']; +const SOME_JSON_DATA = { some: 'json' }; +const SOME_JSON_DATA_STRING = JSON.stringify(SOME_JSON_DATA); +const SOME_JSON_RECEIPT_DATA = { receipt: 'some receipt' }; +const SOME_JSON_RECEIPT_DATA_STRING = JSON.stringify(SOME_JSON_RECEIPT_DATA); +const SOME_DATA = 'someData'; +const SOME_REQUEST = new RequestBuilder() + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .withMethod('GET') + .withPemString(SOME_PEM_STRING) + .build(); + +/** + * @param {string} method + * @param {string} uri + * @param {integer} responseCode + * @param {string} body + */ +const mockResponse = (method, uri, responseCode, body, headers) => { + const scope = nock(SOME_BASE_URL); + const interceptor = scope[method.toLowerCase()](uri); + interceptor.reply(responseCode, body, headers); +}; + +describe('yotiRequest', () => { + afterEach((done) => { + nock.cleanAll(); + done(); + }); + + ALLOWED_METHODS.forEach((ALLOWED_METHOD) => { + describe(`when empty response is returned for ${ALLOWED_METHOD} method`, () => { + beforeEach((done) => { + mockResponse(ALLOWED_METHOD, SOME_ENDPOINT_REG_EXP, 200, '', { + 'content-type': 'application/json', + }); + done(); + }); + + it('should return YotiResponse', (done) => { + const request = new RequestBuilder() + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .withMethod(ALLOWED_METHOD) + .withPayload(new Payload('')) + .withPemString(SOME_PEM_STRING) + .build(); + + yotiRequestHandler + .execute(request) + .then((response) => { + expect(response.getParsedResponse()).toBeNull(); + expect(response.getBody()).toBeNull(); + expect(response.getStatusCode()).toBe(200); + done(); + }) + .catch(done); + }); + }); + describe(`when JSON response is returned for ${ALLOWED_METHOD} method`, () => { + beforeEach((done) => { + mockResponse(ALLOWED_METHOD, SOME_ENDPOINT_REG_EXP, 200, SOME_JSON_DATA_STRING, { + 'content-type': 'application/json', + }); + done(); + }); + + it('should return YotiResponse', (done) => { + const request = new RequestBuilder() + .withBaseUrl(SOME_BASE_URL) + .withEndpoint(SOME_ENDPOINT) + .withMethod(ALLOWED_METHOD) + .withPayload(new Payload('')) + .withPemString(SOME_PEM_STRING) + .build(); + + yotiRequestHandler + .execute(request) + .then((response) => { + expect(response.getParsedResponse()).toStrictEqual(SOME_JSON_DATA); + expect(response.getBody()).toBe(SOME_JSON_DATA_STRING); + expect(response.getReceipt()).toBeNull(); + expect(response.getStatusCode()).toBe(200); + done(); + }) + .catch(done); + }); + }); + }); + describe('when receipt is returned', () => { + beforeEach((done) => { + nock(SOME_BASE_URL) + .get(SOME_ENDPOINT_REG_EXP) + .reply(200, SOME_JSON_RECEIPT_DATA_STRING, { + 'content-type': 'application/json', + }); + done(); + }); + + it('should return YotiResponse', (done) => { + yotiRequestHandler + .execute(SOME_REQUEST) + .then((response) => { + expect(response.getParsedResponse()).toStrictEqual(SOME_JSON_RECEIPT_DATA); + expect(response.getBody()).toBe(SOME_JSON_RECEIPT_DATA_STRING); + expect(response.getReceipt()).toStrictEqual(SOME_JSON_RECEIPT_DATA.receipt); + expect(response.getStatusCode()).toBe(200); + done(); + }) + .catch(done); + }); + }); + [ + 'application/octet-stream', + 'application/pdf', + 'image/jpeg', + 'image/png', + ].forEach((mimeType) => { + describe(`when ${mimeType} content is returned`, () => { + beforeEach((done) => { + nock(SOME_BASE_URL) + .get(SOME_ENDPOINT_REG_EXP) + .reply(200, SOME_DATA, { + 'Content-Type': mimeType, + }); + done(); + }); + it('should return YotiResponse', (done) => { + yotiRequestHandler + .execute(SOME_REQUEST, true) + .then((response) => { + expect(response.getParsedResponse()).toBeInstanceOf(Buffer); + expect(response.getParsedResponse().toString()).toBe(SOME_DATA); + expect(response.getBody().toString()).toBe(SOME_DATA); + expect(response.getStatusCode()).toBe(200); + done(); + }) + .catch(done); + }); + }); + }); + [ + 'text/plain', + ].forEach((mimeType) => { + describe(`when ${mimeType} content is returned`, () => { + beforeEach((done) => { + nock(SOME_BASE_URL) + .get(SOME_ENDPOINT_REG_EXP) + .reply(200, SOME_DATA, { + 'Content-Type': mimeType, + }); + done(); + }); + it('should return YotiResponse', (done) => { + yotiRequestHandler + .execute(SOME_REQUEST) + .then((response) => { + expect(response.getBody()).toBe(SOME_DATA); + done(); + }) + .catch(done); + }); + }); + }); +}); diff --git a/tests/request/request.spec.js b/tests/request/request.spec.js index 7b58631b..939f32fd 100644 --- a/tests/request/request.spec.js +++ b/tests/request/request.spec.js @@ -1,6 +1,9 @@ const { YotiRequest } = require('../../src/request/request'); +const requestHandler = require('../../src/request/request.handler'); const { Payload } = require('../../src/request/payload'); +jest.mock('../../src/request/request.handler'); + const SOME_URL = 'https://api.example.com/some-endpoint'; const SOME_METHOD = 'POST'; const SOME_PAYLOAD = new Payload('some payload'); @@ -10,6 +13,20 @@ const SOME_HEADERS = { const SOME_REQUEST = new YotiRequest(SOME_METHOD, SOME_URL, SOME_HEADERS, SOME_PAYLOAD); describe('YotiRequest', () => { + describe('#execute', () => { + it('should execute the request handler', () => { + SOME_REQUEST.execute(); + expect(requestHandler.execute).toHaveBeenCalledWith(SOME_REQUEST, false); + }); + it('should execute the request handler with buffer disabled', () => { + SOME_REQUEST.execute(false); + expect(requestHandler.execute).toHaveBeenCalledWith(SOME_REQUEST, false); + }); + it('should execute the request handler with buffer enabled', () => { + SOME_REQUEST.execute(true); + expect(requestHandler.execute).toHaveBeenCalledWith(SOME_REQUEST, true); + }); + }); describe('#getUrl', () => { it('should return the URL', () => { expect(SOME_REQUEST.getUrl()).toBe(SOME_URL); diff --git a/tests/request/response.spec.js b/tests/request/response.spec.js new file mode 100644 index 00000000..f3aa18ce --- /dev/null +++ b/tests/request/response.spec.js @@ -0,0 +1,51 @@ +const { YotiResponse } = require('../../src/request/response'); + +const SOME_BODY = '{"some":"response"}'; +const SOME_RECEIPT = { some: 'receipt' }; +const SOME_PARSED_RESPONSE = JSON.parse(SOME_BODY); +const SOME_RESPONSE = new YotiResponse( + SOME_PARSED_RESPONSE, + 200, + SOME_RECEIPT, + SOME_BODY +); + +describe('YotiResponse', () => { + describe('#getBody', () => { + it('should return the body', () => { + expect(SOME_RESPONSE.getBody()).toBe(SOME_BODY); + }); + }); + describe('#getParsedResponse', () => { + it('should return the parsed response', () => { + expect(SOME_RESPONSE.getParsedResponse()).toBe(SOME_PARSED_RESPONSE); + }); + }); + describe('#getReceipt', () => { + it('should return the receipt', () => { + expect(SOME_RESPONSE.getReceipt()).toBe(SOME_RECEIPT); + }); + }); + describe('#getStatusCode', () => { + it('should return the status code', () => { + expect(SOME_RESPONSE.getStatusCode()).toBe(200); + }); + }); + describe('#constructor', () => { + it('should not require receipt', () => { + const SOME_RESPONSE_WITHOUT_RECEIPT = new YotiResponse( + SOME_PARSED_RESPONSE, + 200 + ); + expect(SOME_RESPONSE_WITHOUT_RECEIPT.getReceipt()).toBeNull(); + }); + it('should not require body', () => { + const SOME_RESPONSE_WITHOUT_BODY = new YotiResponse( + SOME_PARSED_RESPONSE, + 200, + SOME_RECEIPT + ); + expect(SOME_RESPONSE_WITHOUT_BODY.getBody()).toBeNull(); + }); + }); +});