Compare commits

...
12 Commits
Author SHA1 Message Date
Chris Troutner 8fe78037c1 Merge pull request #66 from Permissionless-Software-Foundation/ct-unstable
GET /ipfs/cid2json
2025-02-09 06:53:22 -07:00
Chris Troutner 33a447cd61 fix(cid2json): Added error handler for corner case 2025-02-09 06:39:07 -07:00
Chris Troutner 8a70d6a169 fix(cid2json): Works with CID files and directories 2025-02-09 06:12:47 -07:00
Chris Troutner 9ae5e890ae feat(cid2json): Got basic functionality in place, no tests yet, known issues 2025-02-09 05:52:48 -07:00
Chris Troutner 1bf28cbc21 Merge pull request #65 from Permissionless-Software-Foundation/ct-unstable
Removing logging of usage middleware
2025-01-27 19:00:06 -07:00
Chris Troutner d7bb6e4cb2 Merge remote-tracking branch 'upstream/master' into ct-unstable 2025-01-27 18:58:48 -07:00
Chris Troutner 98a1631498 Merge pull request #167 from Permissionless-Software-Foundation/ct-unstable
Removing logging of usage middleware
2025-01-27 18:55:29 -07:00
Chris Troutner 8dce8cbf8d Removing logging of usage middleware 2025-01-27 18:53:17 -07:00
Chris Troutner f097ee024a Merge pull request #64 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2025-01-26 19:53:20 -07:00
Chris Troutner 7d21fe02f8 Syncing with upstream ipfs-service-provider 2025-01-26 19:48:38 -07:00
Chris Troutner 0b3049138b Merge pull request #166 from Permissionless-Software-Foundation/dh-usage-tests
feat(tests): Added Usage Unit Tests
2025-01-17 05:32:27 -07:00
Daniel Gonzalez d7136583b8 feat(tests): Added Usage Unit Tests 2025-01-15 17:59:09 -04:00
14 changed files with 799 additions and 13 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
{
"password": "RPWjFwYWyJ9S8ByjRDbi",
"password": "eYrzProqwsAyO3CT0Ftf",
"email": "system@system.com",
"id": "678459df8ec91cd6c3973b63",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3ODQ1OWRmOGVjOTFjZDZjMzk3M2I2MyIsImlhdCI6MTczNjcyNzAwN30.AHLNjmznprA5GzQ_z8GbhrYHOJUIlMzVC64SGRy-Xvs"
"id": "67a8af30c958c587f890cd8f",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhZjMwYzk1OGM1ODdmODkwY2Q4ZiIsImlhdCI6MTczOTEwODE0NH0.OFdVwH8ikbChHV0wjRpYvnL7vu46HDYS-od3FRtm9Nc"
}
+32 -1
View File
@@ -39,6 +39,7 @@ class IpfsRESTControllerLib {
this.getService = this.getService.bind(this)
this.getFileInfo = this.getFileInfo.bind(this)
this.getPins = this.getPins.bind(this)
this.cid2json = this.cid2json.bind(this)
}
/**
@@ -220,7 +221,7 @@ class IpfsRESTControllerLib {
ctx.body = metadata
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/getService(): ', err)
console.log('Error in ipfs/controller.js/getFileInfo(): ', err)
this.handleError(ctx, err)
}
}
@@ -253,8 +254,37 @@ class IpfsRESTControllerLib {
}
}
/**
* @api {get} /ipfs/cid2json/:cid Given a CID, retrieve a JSON object
* @apiPermission public
* @apiName GetCid2Json
* @apiGroup REST IPFS
* @apiDescription Given a CID, retrieve a JSON object.
* If the CID does not resolves to a JSON file, then an error is thrown.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/cid2json/bafkreigbgrvpagnmrqz2vhofifrqobigsxkdvnvikf5iqrkrbwrzirazhm
*
*/
async cid2json (ctx) {
try {
const { cid } = ctx.params
const json = await this.useCases.ipfs.cid2json({ cid })
ctx.body = json
} catch (err) {
// wlogger.error('Error in ipfs/controller.js/viewFile(): ', err)
console.log('Error in ipfs/controller.js/cid2json(): ', err.message)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// console.log('handleError() err.status: ', err.status)
// console.log('handleError() err.message: ', err.message)
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
@@ -263,6 +293,7 @@ class IpfsRESTControllerLib {
ctx.throw(err.status)
}
} else {
// console.log(`handleError() err.message: ${err.message}`)
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
+1
View File
@@ -60,6 +60,7 @@ class IpfsRouter {
this.router.get('/service', this.ipfsRESTController.getService)
this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo)
this.router.get('/pins', this.ipfsRESTController.getPins)
this.router.get('/cid2json/:cid', this.ipfsRESTController.cid2json)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
+3 -5
View File
@@ -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)
+2
View File
@@ -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)
+86
View File
@@ -33,6 +33,7 @@ class IpfsUseCases {
this.downloadCid = this.downloadCid.bind(this)
// this.downloadCid2 = this.downloadCid2.bind(this)
this.getWritePrice = this.getWritePrice.bind(this)
this.cid2json = this.cid2json.bind(this)
// State
this.lastWritePriceUpdate = null // Used to periodically update write price.
@@ -151,6 +152,91 @@ class IpfsUseCases {
}
}
// Given a CID, this function will retrieve a JSON object from the
// ipfs-file-pin-service, using the IPFS JSON-RPC.
async cid2json (inObj = {}) {
try {
const { cid } = inObj
console.log('cid2json() cid: ', cid)
// Throw an error if ipfs-bch-wallet-consumer has not yet connected to an instance of ipfs-file-pin-service.
const ipfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider
if (!ipfsFileProvider) {
throw new Error('No IPFS File Provider Service is available yet. Try again in a few seconds.')
}
// Throw an error if ipfs-bch-wallet-consumer can not communicate with the ipfs-file-pin-service.
const ipfsFiles = this.adapters.ipfsFiles
const metadata = await ipfsFiles.getFileMetadata({ cid })
if (metadata.success === false) {
throw new Error(`Could not communicate with instance of ipfs-file-pin-service ${ipfsFileProvider}. Try again in a few seconds.`)
}
// Throw an error if the file is not pinned.
const fileMetadata = metadata.fileMetadata
if (!fileMetadata.dataPinned) {
throw new Error(`CID ${cid} has not been pinned by ipfs-file-pin-service instance ${ipfsFileProvider}`)
}
// console.log('fileMetadata: ', fileMetadata)
// Throw an error if this is not a JSON file
const filename = fileMetadata.filename
if (!filename.endsWith('.json')) {
throw new Error(`CID ${cid} does not resolve to a JSON file.`)
}
// Retrieve the CID content and store it in a Buffer.
const helia = this.adapters.ipfs.ipfs
const fileChunks = []
// list cid content. This is used to determine if the CID is a file or a directory.
const contentArray = []
for await (const file of helia.fs.ls(cid)) {
contentArray.push(file)
}
// console.log('contentArray: ', contentArray)
try {
// Handle CIDs that are files.
for await (const chunk of helia.fs.cat(cid)) {
fileChunks.push(chunk)
}
} catch (err) {
// Extra logic here to look at the filename of the first file and make sure
// it ends in .json.
const name = contentArray[0].name
if (!name.endsWith('.json')) {
throw new Error(`CID ${cid} is a directory and its first file does not resolve to a valid JSON file.`)
}
// Handle CIDs that are directorys containing a JSON file. (TokenTiger.com tokens)
for await (const chunk of helia.fs.cat(`${cid}/${name}`)) {
fileChunks.push(chunk)
}
}
const fileBuf = Buffer.concat(fileChunks)
// Convert the Buffer into a string.
const jsonStr = fileBuf.toString()
// Parse the string into a JSON object.
let json = null
try {
json = JSON.parse(jsonStr)
} catch (err) {
throw new Error(`CID ${cid} does not resolve to a valid JSON object.`)
}
return {
success: true,
json
}
} catch (err) {
console.error('Error in ipfs-use-cases.js/cid2json(): ', err.message)
throw err
}
}
// async downloadCid2 (inObj = {}) {
// try {
// const { cid } = inObj
+5 -4
View File
@@ -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 => {
@@ -112,7 +113,7 @@ function usageMiddleware () {
try {
await next()
console.log('ctx.request: ', ctx.request)
// console.log('ctx.request: ', ctx.request)
const now = new Date()
const reqObj = {
@@ -121,7 +122,7 @@ function usageMiddleware () {
method: ctx.request.method,
timestamp: now.getTime()
}
console.log('reqObj: ', reqObj)
// console.log('reqObj: ', reqObj)
restCalls.push(reqObj)
} catch (err) {
@@ -132,4 +133,4 @@ function usageMiddleware () {
}
};
export { UsageUseCases, usageMiddleware }
export { UsageUseCases, usageMiddleware, restCalls }
+74
View File
@@ -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')
}
})
})
})
@@ -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')
}
})
})
})
@@ -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')
}
})
})
})
@@ -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.'
)
}
})
})
})
@@ -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)
})
})
})
+19
View File
@@ -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;
+255
View File
@@ -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)
}
})
})
})