diff --git a/config/system-user-dev.json b/config/system-user-dev.json index 6f72a6e..5eb21c7 100644 --- a/config/system-user-dev.json +++ b/config/system-user-dev.json @@ -1,6 +1,6 @@ { - "password": "RPWjFwYWyJ9S8ByjRDbi", + "password": "jxR0kPRm6GI7RETZ36KW", "email": "system@system.com", - "id": "678459df8ec91cd6c3973b63", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ODQ1OWRmOGVjOTFjZDZjMzk3M2I2MyIsImlhdCI6MTczNjcyNzAwN30.AHLNjmznprA5GzQ_z8GbhrYHOJUIlMzVC64SGRy-Xvs" + "id": "6796f3e8e473169ab68aa9b5", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3OTZmM2U4ZTQ3MzE2OWFiNjhhYTliNSIsImlhdCI6MTczNzk0NjA4OH0.q1Dy0WXO2E2Q5y2eNB2INq_YD3AnNirgX1vrk9KxkH0" } \ No newline at end of file diff --git a/src/controllers/rest-api/usage/controller.js b/src/controllers/rest-api/usage/controller.js index d78db1e..f7f3432 100644 --- a/src/controllers/rest-api/usage/controller.js +++ b/src/controllers/rest-api/usage/controller.js @@ -1,5 +1,5 @@ /* - REST API Controller library for the /ipfs route + REST API Controller library for the /usage route */ // Global npm libraries @@ -13,19 +13,17 @@ class UsageRESTControllerLib { this.adapters = localConfig.adapters if (!this.adapters) { throw new Error( - 'Instance of Adapters library required when instantiating /ipfs REST Controller.' + 'Instance of Adapters library required when instantiating /usage REST Controller.' ) } this.useCases = localConfig.useCases if (!this.useCases) { throw new Error( - 'Instance of Use Cases library required when instantiating /ipfs REST Controller.' + 'Instance of Use Cases library required when instantiating /usage REST Controller.' ) } // Encapsulate dependencies - // this.UserModel = this.adapters.localdb.Users - // this.userUseCases = this.useCases.user // Bind 'this' object to all subfunctions this.getStatus = this.getStatus.bind(this) diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index 1186bfa..2c58279 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -68,6 +68,8 @@ class TimerControllers { cleanUsage () { try { this.useCases.usage.cleanUsage() + + return true } catch (err) { console.error('Error in time-controller.js/cleanUsage(): ', err) diff --git a/src/use-cases/usage-use-cases.js b/src/use-cases/usage-use-cases.js index 356485a..6684b44 100644 --- a/src/use-cases/usage-use-cases.js +++ b/src/use-cases/usage-use-cases.js @@ -20,6 +20,7 @@ class UsageUseCases { } // Bind 'this' object to all subfunctions + this.cleanUsage = this.cleanUsage.bind(this) this.getRestSummary = this.getRestSummary.bind(this) this.getTopIps = this.getTopIps.bind(this) this.getTopEndpoints = this.getTopEndpoints.bind(this) @@ -36,6 +37,7 @@ class UsageUseCases { const twentyFourHoursAgo = now.getTime() - (60000 * 60 * 24) restCalls = restCalls.filter(x => x.timestamp > twentyFourHoursAgo) + return restCalls } catch (err) { console.error('Error in usage-use-cases.js/cleanUsage()') throw err @@ -58,7 +60,6 @@ class UsageUseCases { getTopIps () { try { const ips = restCalls.map(x => x.ip) - // Create a Map to count occurrences of each IP address string const countMap = new Map() ips.forEach(ip => { @@ -132,4 +133,4 @@ function usageMiddleware () { } }; -export { UsageUseCases, usageMiddleware } +export { UsageUseCases, usageMiddleware, restCalls } diff --git a/test/e2e/automated/a10-usage.rest-e2e.js b/test/e2e/automated/a10-usage.rest-e2e.js new file mode 100644 index 0000000..30f6904 --- /dev/null +++ b/test/e2e/automated/a10-usage.rest-e2e.js @@ -0,0 +1,74 @@ +/* +End-to-end tests for /usage endpoints. +*/ + +import config from '../../../config/index.js' +import { assert } from 'chai' +import axios from 'axios' +import sinon from 'sinon' +import util from 'util' + +util.inspect.defaultOptions = { depth: 1 } + +const LOCALHOST = `http://localhost:${config.port}` + +let sandbox + +describe('Usage', () => { + beforeEach(() => { + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('GET /usage', () => { + it('should return usage status', async () => { + try { + const options = { + method: 'get', + url: `${LOCALHOST}/usage` + } + + const result = await axios(options) + + assert.property(result.data, 'status') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) + + describe('GET /usage/ips', () => { + it('should return ips', async () => { + try { + const options = { + method: 'get', + url: `${LOCALHOST}/usage/ips` + } + + const result = await axios(options) + + assert.property(result.data, 'ips') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) + + describe('GET /usage/endpoints', () => { + it('should return ips', async () => { + try { + const options = { + method: 'get', + url: `${LOCALHOST}/usage/endpoints` + } + + const result = await axios(options) + + assert.property(result.data, 'endpoints') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/middleware/error-unit.js b/test/unit/controllers/rest-api/middleware/error-unit.js new file mode 100644 index 0000000..a71cc0f --- /dev/null +++ b/test/unit/controllers/rest-api/middleware/error-unit.js @@ -0,0 +1,66 @@ +/* +Unit tests for the REST API middleware that handle response errors. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local libraries +import errorMiddleware from '../../../../../src/controllers/rest-api/middleware/error.js' +import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js' + +describe('#Validators', () => { + let ctx + let sandbox + + beforeEach(() => { + // Mock the context object. + ctx = mockContext() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('#errorMiddleware', () => { + it('should run next function', async () => { + // Spy on next + const next = sinon.spy(() => { }) + errorMiddleware()(ctx, next) + + assert.isTrue(next.calledOnce) + }) + + it('should handle unknown status error', async () => { + try { + const next = async () => { + const e = new Error('test error') + e.status = null + throw e + } + + await errorMiddleware()(ctx, next) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(ctx.status, 500) + assert.equal(ctx.body, 'test error') + } + }) + it('should handle known status error', async () => { + try { + const next = async () => { + const e = new Error('test error') + e.status = 422 + throw e + } + + await errorMiddleware()(ctx, next) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(ctx.status, 422) + assert.equal(ctx.body, 'test error') + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/usage/usage.rest.controller.unit.js b/test/unit/controllers/rest-api/usage/usage.rest.controller.unit.js new file mode 100644 index 0000000..85dbbe7 --- /dev/null +++ b/test/unit/controllers/rest-api/usage/usage.rest.controller.unit.js @@ -0,0 +1,163 @@ +/* + Unit tests for the REST API handler for the /usage endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' +import UseCasesMock from '../../../mocks/use-cases/index.js' +import UsageController from '../../../../../src/controllers/rest-api/usage/controller.js' + +import { context as mockContext } from '../../../mocks/ctx-mock.js' + +let uut +let sandbox +let ctx + +describe('#Usage-REST-Controller', () => { + // const testUser = {} + + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new UsageController({ adapters, useCases }) + + sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new UsageController() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating /usage REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new UsageController({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating /usage REST Controller.' + ) + } + }) + }) + + describe('#Get /usage', () => { + it('should return 422 status on biz logic error', async () => { + try { + sandbox.stub(uut.useCases.usage, 'getRestSummary').throws(new Error('test error')) + await uut.getStatus(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + await uut.getStatus(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'status') + }) + }) + + describe('#Get /ips', () => { + it('should return 422 status on biz logic error', async () => { + try { + sandbox.stub(uut.useCases.usage, 'getTopIps').throws(new Error('test error')) + await uut.getTopIps(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + await uut.getTopIps(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'ips') + }) + }) + + describe('#Get /endpoints', () => { + it('should return 422 status on biz logic error', async () => { + try { + sandbox.stub(uut.useCases.usage, 'getTopEndpoints').throws(new Error('test error')) + await uut.getTopEndpoints(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + await uut.getTopEndpoints(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'endpoints') + }) + }) + + describe('#handleError', () => { + it('should pass an error message', () => { + try { + const err = { + status: 422, + message: 'Unprocessable Entity' + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Unprocessable Entity') + } + }) + it('should still throw error if there is no message', () => { + try { + const err = { + status: 404 + } + + uut.handleError(ctx, err) + } catch (err) { + assert.include(err.message, 'Not Found') + } + }) + }) +}) diff --git a/test/unit/controllers/rest-api/usage/usage.rest.router.unit.js b/test/unit/controllers/rest-api/usage/usage.rest.router.unit.js new file mode 100644 index 0000000..bfb237b --- /dev/null +++ b/test/unit/controllers/rest-api/usage/usage.rest.router.unit.js @@ -0,0 +1,75 @@ +/* +Unit tests for the REST API handler for the /usage endpoints. +*/ + +// Public npm libraries +import { assert } from 'chai' + +import sinon from 'sinon' + +// Local support libraries +import adapters from '../../../mocks/adapters/index.js' + +import UseCasesMock from '../../../mocks/use-cases/index.js' + +import UsageRouter from '../../../../../src/controllers/rest-api/usage/index.js' + +let uut +let sandbox +// let ctx + +// const mockContext = require('../../../../unit/mocks/ctx-mock').context + +describe('#Usage-REST-Router', () => { + beforeEach(() => { + const useCases = new UseCasesMock() + uut = new UsageRouter({ adapters, useCases }) + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new UsageRouter() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Adapters library required when instantiating IPFS REST Controller.' + ) + } + }) + + it('should throw an error if useCases are not passed in', () => { + try { + uut = new UsageRouter({ adapters }) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of Use Cases library required when instantiating IPFS REST Controller.' + ) + } + }) + }) + + describe('#attach', () => { + it('should throw an error if app is not passed in.', () => { + try { + uut.attach() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Must pass app object when attaching REST API controllers.' + ) + } + }) + }) +}) diff --git a/test/unit/controllers/timer-controllers.unit.js b/test/unit/controllers/timer-controllers.unit.js index 277284a..fda1b72 100644 --- a/test/unit/controllers/timer-controllers.unit.js +++ b/test/unit/controllers/timer-controllers.unit.js @@ -79,4 +79,19 @@ describe('#Timer-Controllers', () => { assert.equal(result, false) }) }) + + describe('#cleanUsage', () => { + it('should kick off the Use Case', async () => { + const result = await uut.cleanUsage() + + assert.equal(result, true) + }) + + it('should return false on error', async () => { + sandbox.stub(uut.useCases.usage, 'cleanUsage').throws(new Error('test error')) + const result = await uut.cleanUsage() + + assert.equal(result, false) + }) + }) }) diff --git a/test/unit/mocks/use-cases/index.js b/test/unit/mocks/use-cases/index.js index 3f73f19..7ba11dc 100644 --- a/test/unit/mocks/use-cases/index.js +++ b/test/unit/mocks/use-cases/index.js @@ -49,6 +49,24 @@ class BchUseCaseMock { } } +class UsageUseCaseMock { + async cleanUsage() { + return {} + } + + async getRestSummary() { + return true + } + + async getTopIps(params) { + return true + } + + async getTopEndpoints(existingUser, newData) { + return true + } +} + class UseCasesMock { constuctor(localConfig = {}) { // this.user = new UserUseCaseMock(localConfig) @@ -58,6 +76,7 @@ class UseCasesMock { user = new UserUseCaseMock() bch = new BchUseCaseMock() + usage = new UsageUseCaseMock() } export default UseCasesMock; diff --git a/test/unit/use-cases/usage.use-case.unit.js b/test/unit/use-cases/usage.use-case.unit.js new file mode 100644 index 0000000..5dac264 --- /dev/null +++ b/test/unit/use-cases/usage.use-case.unit.js @@ -0,0 +1,255 @@ +/* +Unit tests for the use-cases/usage-use-cases.js business logic library. + +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' + +// Local support libraries +import adapters from '../mocks/adapters/index.js' + +// Mock +import { context as mockContext } from '../mocks/ctx-mock.js' + +// Unit under test (uut) +import { UsageUseCases, restCalls, usageMiddleware } from '../../../src/use-cases/usage-use-cases.js' + +describe('#usage-use-case', () => { + let uut + let sandbox + let ctx + + before(async () => { + + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + uut = new UsageUseCases({ adapters }) + + // Set as empty array + restCalls.splice(0, restCalls.length) + + ctx = mockContext() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should throw an error if adapters are not passed in', () => { + try { + uut = new UsageUseCases() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include( + err.message, + 'Instance of adapters must be passed in when instantiating Usage Use Cases library.' + ) + } + }) + }) + + describe('#cleanUsage', () => { + it('should delete older data than 24 hours', () => { + const now = new Date() // Mock date + + // set older mock data + restCalls.push({ + timestamp: now.getTime() - (60000 * 60 * 24), + ip: '127.0.0.1' + }) + + // Set recently mock data + restCalls.push({ + timestamp: now.getTime(), + ip: 'localhost' + }) + + const result = uut.cleanUsage() + + assert.isArray(result) + assert.equal(result.length, 1) + assert.equal(result[0].ip, 'localhost') + }) + + it('should handle error', () => { + try { + // Force an error + sandbox.stub(restCalls, 'filter').throws(new Error('uut error')) + + uut.cleanUsage() + + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'uut error') + } + }) + }) + describe('#getRestSummary', () => { + it('should get the number of rest calls', () => { + // Set mock data + restCalls.push({ + ip: 'localhost' + }) + + const result = uut.getRestSummary() + + assert.isNumber(result) + assert.equal(result, 1) + }) + + it('should handle error', () => { + try { + // Force an error + sandbox.stub(console, 'log').throws(new Error('uut error')) + + uut.getRestSummary() + + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'uut error') + } + }) + }) + + describe('#getTopIps', () => { + it('should get top IPs', () => { + // Set mock data + restCalls.push({ + ip: 'localhost' + }) + + // Set mock data + restCalls.push({ + ip: 'localhost' + }) + + const result = uut.getTopIps() + + assert.isArray(result) + + assert.property(result[0], 'ip') + assert.property(result[0], 'cnt') + + assert.equal(result[0].ip, 'localhost') + assert.equal(result[0].cnt, '2') + }) + it('should return a maximum of 20 values', () => { + // Fill Array with 21 values + for (let i = 0; i < 21; i++) { + restCalls.push({ + ip: `localhost-${i}` + }) + } + + const result = uut.getTopIps() + + assert.isArray(result) + + assert.property(result[0], 'ip') + assert.property(result[0], 'cnt') + + assert.equal(result.length, 20) + }) + it('should handle error', () => { + try { + // Set mock data + restCalls.push(null) + + uut.getTopIps() + + assert.fail('Unexpected code path') + } catch (error) { + assert.include(error.message, 'Cannot read properties') + } + }) + }) + + describe('#getTopEndpoints', () => { + it('should get top Endpoints', () => { + // Set mock data + restCalls.push({ + ip: 'localhost', + url: '/api/v1/users', + method: 'GET' + }) + + // Set mock data + restCalls.push({ + ip: 'localhost', + url: '/api/v1/users', + method: 'GET' + }) + + const result = uut.getTopEndpoints() + + assert.isArray(result) + + assert.property(result[0], 'endpoint') + assert.property(result[0], 'cnt') + + assert.equal(result[0].endpoint, 'GET /api/v1/users') + assert.equal(result[0].cnt, '2') + }) + it('should return a maximum of 20 values', () => { + // Fill Array with 21 values + for (let i = 0; i < 21; i++) { + restCalls.push({ + ip: 'localhost', + url: `/api/v1/users-${i}`, + method: 'GET' + }) + } + + const result = uut.getTopEndpoints() + + assert.isArray(result) + + assert.property(result[0], 'endpoint') + assert.property(result[0], 'cnt') + + assert.equal(result.length, 20) + }) + + it('should handle error', () => { + try { + // Set mock data + restCalls.push(null) + + uut.getTopEndpoints() + + assert.fail('Unexpected code path') + } catch (error) { + assert.include(error.message, 'Cannot read properties') + } + }) + }) + + describe('#usageMiddleware', () => { + it('should update restCalls state', async () => { + // Spy on next + const next = sinon.spy(() => { }) + + await usageMiddleware()(ctx, next) + + assert.equal(restCalls.length, 1) + assert.isTrue(next.called) + }) + + it('should handle error', async () => { + try { + const next = () => { throw new Error('uut error') } + + await usageMiddleware()(ctx, next) + assert.fail('Unexpected code path') + } catch (error) { + assert.equal(error.message, 'uut error') + assert.equal(ctx.status, 500) + assert.equal(restCalls.length, 0) + } + }) + }) +})