-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create
ChargingModuleRequestLib
(#129)
https://eaflood.atlassian.net/browse/WATER-3903 Currently we have `RequestLib` which we use to make requests to external services. Having developed code which sends requests to the Charging Module we can see that there will be things we need to do which will be common to all CM requests -- for example, obtaining an access token, formatting the response in a particular way, etc. Before we go any further with developing more services which send requests to the CM we want to create a general `ChargingModuleRequestLib` which will do all of this for us, allowing our services to concentrate on what they actually need to do and not all the stuff surrounding the requests. This PR implements `ChargingModuleRequestLib` and two initial methods, `get` and `post`. As part of this, we want to access our server method `getChargingModuleToken()` to ensure that Cognito tokens are cached for all requests. In order to do this without passing the `server` object around from one service to another, we introduce a new `GlobalHapiServerMethodsPlugin`, which adds the Hapi server methods object to the global object, allowing us to access them from anywhere; this means we can get a token by calling `global.HapiServerMethods.getChargingModuleToken()`. In testing we confirmed that calling this and calling `server.methods.getChargingModuleToken()` gives the same token; however, for consistency we intend to use `global.HapiServerMethods.getChargingModuleToken()` at all times. Note that existing code which sends requests to the CM will be updated in a separate PR.
- Loading branch information
Showing
5 changed files
with
274 additions
and
1 deletion.
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 |
---|---|---|
@@ -0,0 +1,91 @@ | ||
'use strict' | ||
|
||
/** | ||
* Use for making http requests to the Charging Module | ||
* @module ChargingModuleRequestLib | ||
*/ | ||
|
||
const RequestLib = require('./request.lib.js') | ||
const servicesConfig = require('../../config/services.config.js') | ||
|
||
/** | ||
* Sends a GET request to the Charging Module for the provided route | ||
* | ||
* @param {string} route The route to send the request to | ||
* | ||
* @returns {Object} result An object representing the result of the request | ||
* @returns {boolean} result.succeeded Whether the request was successful | ||
* @returns {Object} result.response The Charging Module response if successful; or the error response if not | ||
*/ | ||
async function get (route) { | ||
const result = await _sendRequest(route, RequestLib.get) | ||
|
||
return _parseResult(result) | ||
} | ||
|
||
/** | ||
* Sends a POST request to the Charging Module for the provided route | ||
* | ||
* @param {string} route The route to send the request to | ||
* @param {Object} [body] The body of the request | ||
* | ||
* @returns {Object} result An object representing the result of the request | ||
* @returns {boolean} result.succeeded Whether the request was successful | ||
* @returns {Object} result.response The Charging Module response if successful; or the error response if not | ||
*/ | ||
async function post (route, body = {}) { | ||
const result = await _sendRequest(route, RequestLib.post, body) | ||
|
||
return _parseResult(result) | ||
} | ||
|
||
/** | ||
* Sends a request to the Charging Module to the provided using the provided RequestLib method | ||
* | ||
* @param {string} route The route that you wish to connect to | ||
* @param {Object} method An instance of a RequestLib method which will be used to send the request | ||
* @param {Object} [body] Optional body to be sent to the route as json | ||
* | ||
* @returns {Object} The result of the request passed back from RequestLib | ||
*/ | ||
async function _sendRequest (route, method, body = {}) { | ||
const url = new URL(route, servicesConfig.chargingModule.url) | ||
const authentication = await global.HapiServerMethods.getChargingModuleToken() | ||
const options = _requestOptions(authentication.accessToken, body) | ||
|
||
const result = await method(url.href, options) | ||
|
||
return result | ||
} | ||
|
||
function _requestOptions (accessToken, body) { | ||
return { | ||
headers: { | ||
authorization: `Bearer ${accessToken}` | ||
}, | ||
json: body | ||
} | ||
} | ||
|
||
/** | ||
* Parses the response from RequestLib. If the response contains a body then we convert it from JSON to an object. | ||
*/ | ||
function _parseResult (result) { | ||
let response = result.response | ||
|
||
// If the request got a response from the Charging Module we will have a response body. If the request errored, for | ||
// example a timeout because the Charging Module is down, response will be the instance of the error thrown by Got. | ||
if (response.body) { | ||
response = JSON.parse(response.body) | ||
} | ||
|
||
return { | ||
succeeded: result.succeeded, | ||
response | ||
} | ||
} | ||
|
||
module.exports = { | ||
get, | ||
post | ||
} |
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 |
---|---|---|
@@ -0,0 +1,20 @@ | ||
'use strict' | ||
|
||
/** | ||
* Plugin to add Hapi server methods to the global object | ||
* | ||
* The advantage of server methods is that their output can be cached. From our tests we have seen that accessing them | ||
* via `server.methods` and `global.HapiServerMethods` makes use of the same cache. Nevertheless, for consistency we | ||
* should only access them via `global.HapiServerMethods` even when the server object is available to us. | ||
* | ||
* @module GlobalHapiServerMethods | ||
*/ | ||
|
||
const GlobalHapiServerMethods = { | ||
name: 'global-hapi-server-methods', | ||
register: (server, _options) => { | ||
global.HapiServerMethods = server.methods | ||
} | ||
} | ||
|
||
module.exports = GlobalHapiServerMethods |
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 |
---|---|---|
@@ -0,0 +1,158 @@ | ||
'use strict' | ||
|
||
// Test framework dependencies | ||
const Lab = require('@hapi/lab') | ||
const Code = require('@hapi/code') | ||
const Sinon = require('sinon') | ||
|
||
const { describe, it, before, beforeEach, after, afterEach } = exports.lab = Lab.script() | ||
const { expect } = Code | ||
|
||
// Things we need to stub | ||
const RequestLib = require('../../app/lib/request.lib.js') | ||
|
||
// Thing under test | ||
const ChargingModuleRequestLib = require('../../app/lib/charging-module-request.lib.js') | ||
|
||
describe('ChargingModuleRequestLib', () => { | ||
const testRoute = 'TEST_ROUTE' | ||
|
||
before(async () => { | ||
// ChargingModuleRequestLib makes use of the getChargingModuleToken() server method, which we therefore need to stub | ||
// Note that we only need to do this once as it is unaffected by the Sinon.restore() in our afterEach() | ||
global.HapiServerMethods = { | ||
getChargingModuleToken: Sinon.stub().resolves({ | ||
accessToken: 'ACCESS_TOKEN', | ||
expiresIn: 3600 | ||
}) | ||
} | ||
}) | ||
|
||
afterEach(() => { | ||
Sinon.restore() | ||
}) | ||
|
||
after(() => { | ||
// Tidy up our global server methods stub once done | ||
delete global.HapiServerMethods | ||
}) | ||
|
||
describe('#get', () => { | ||
let result | ||
|
||
describe('when the request succeeds', () => { | ||
beforeEach(async () => { | ||
Sinon.stub(RequestLib, 'get').resolves({ | ||
succeeded: true, | ||
response: { | ||
statusCode: 200, | ||
body: '{"testObject": {"test": "yes"}}' | ||
} | ||
}) | ||
|
||
result = await ChargingModuleRequestLib.get(testRoute) | ||
}) | ||
|
||
it('calls the Charging Module with the required options', async () => { | ||
const requestArgs = RequestLib.get.firstCall.args | ||
|
||
expect(requestArgs[0]).to.endWith('/TEST_ROUTE') | ||
expect(requestArgs[1].headers).to.include({ authorization: 'Bearer ACCESS_TOKEN' }) | ||
}) | ||
|
||
it('returns a `true` success status', async () => { | ||
expect(result.succeeded).to.be.true() | ||
}) | ||
|
||
it('returns the response as an object', async () => { | ||
const { response } = result | ||
|
||
expect(response.testObject.test).to.equal('yes') | ||
}) | ||
}) | ||
|
||
describe('when the request fails', () => { | ||
beforeEach(async () => { | ||
Sinon.stub(RequestLib, 'get').resolves({ | ||
succeeded: false, | ||
response: { | ||
statusCode: 400, | ||
testError: 'TEST_ERROR' | ||
} | ||
}) | ||
|
||
result = await ChargingModuleRequestLib.get(testRoute) | ||
}) | ||
|
||
it('returns a `false` success status', async () => { | ||
expect(result.succeeded).to.be.false() | ||
}) | ||
|
||
it('returns the error response', async () => { | ||
const { response } = result | ||
|
||
expect(response.testError).to.equal('TEST_ERROR') | ||
}) | ||
}) | ||
}) | ||
|
||
describe('#post', () => { | ||
let result | ||
|
||
describe('when the request succeeds', () => { | ||
beforeEach(async () => { | ||
Sinon.stub(RequestLib, 'post').resolves({ | ||
succeeded: true, | ||
response: { | ||
statusCode: 200, | ||
body: '{"testObject": {"test": "yes"}}' | ||
} | ||
}) | ||
|
||
result = await ChargingModuleRequestLib.post(testRoute, { test: true }) | ||
}) | ||
|
||
it('calls the Charging Module with the required options', async () => { | ||
const requestArgs = RequestLib.post.firstCall.args | ||
|
||
expect(requestArgs[0]).to.endWith('/TEST_ROUTE') | ||
expect(requestArgs[1].headers).to.include({ authorization: 'Bearer ACCESS_TOKEN' }) | ||
expect(requestArgs[1].json).to.include({ test: true }) | ||
}) | ||
|
||
it('returns a `true` success status', async () => { | ||
expect(result.succeeded).to.be.true() | ||
}) | ||
|
||
it('returns the response as an object', async () => { | ||
const { response } = result | ||
|
||
expect(response.testObject.test).to.equal('yes') | ||
}) | ||
}) | ||
|
||
describe('when the request fails', () => { | ||
beforeEach(async () => { | ||
Sinon.stub(RequestLib, 'post').resolves({ | ||
succeeded: false, | ||
response: { | ||
statusCode: 400, | ||
testError: 'TEST_ERROR' | ||
} | ||
}) | ||
|
||
result = await ChargingModuleRequestLib.post(testRoute, { test: true }) | ||
}) | ||
|
||
it('returns a `false` success status', async () => { | ||
expect(result.succeeded).to.be.false() | ||
}) | ||
|
||
it('returns the error response', async () => { | ||
const { response } = result | ||
|
||
expect(response.testError).to.equal('TEST_ERROR') | ||
}) | ||
}) | ||
}) | ||
}) |