Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create ChargingModuleRequestLib #129

Merged
merged 12 commits into from
Feb 22, 2023
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')
})
})
})
})