Skip to content

Commit

Permalink
Create ChargingModuleRequestLib (#129)
Browse files Browse the repository at this point in the history
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
StuAA78 authored Feb 22, 2023
1 parent f669eca commit 5055ec2
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .labrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ module.exports = {
'GLOBAL_AGENT','ROARR',
// GlobalNotifier is added by us a global in a server plugin. It's how we make logging available anywhere in the app
// whilst avoiding having to pass it around
'GlobalNotifier'
'GlobalNotifier',
// HapiServerMethods is added by us in a server plugin to allow us to access server methods globally.
'HapiServerMethods'
].join(',')
}
91 changes: 91 additions & 0 deletions app/lib/charging-module-request.lib.js
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
}
20 changes: 20 additions & 0 deletions app/plugins/global-hapi-server-methods.plugin.js
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
2 changes: 2 additions & 0 deletions app/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const AirbrakePlugin = require('./plugins/airbrake.plugin.js')
const BlippPlugin = require('./plugins/blipp.plugin.js')
const ChargingModuleTokenCachePlugin = require('./plugins/charging-module-token-cache.plugin.js')
const ErrorPagesPlugin = require('./plugins/error-pages.plugin.js')
const GlobalHapiServerMethodsPlugin = require('./plugins/global-hapi-server-methods.plugin.js')
const GlobalNotifierPlugin = require('./plugins/global-notifier.plugin.js')
const HapiPinoPlugin = require('./plugins/hapi-pino.plugin.js')
const RequestNotifierPlugin = require('./plugins/request-notifier.plugin.js')
Expand All @@ -27,6 +28,7 @@ const registerPlugins = async (server) => {
await server.register(ErrorPagesPlugin)
await server.register(RequestNotifierPlugin)
await server.register(ViewsPlugin)
await server.register(GlobalHapiServerMethodsPlugin)

// Register non-production plugins
if (ServerConfig.environment === 'development') {
Expand Down
158 changes: 158 additions & 0 deletions test/lib/charging-module-request.lib.test.js
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')
})
})
})
})

0 comments on commit 5055ec2

Please sign in to comment.