Compare commits

..
30 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
Chris Troutner 173f706a61 Merge pull request #63 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2025-01-12 17:14:50 -07:00
Chris Troutner 39fc919d35 fix(pedigree): Updating pedigree doc 2025-01-12 17:12:15 -07:00
Chris Troutner 6f0726aeb8 Synced with upstream ipfs-service-provider 2025-01-12 17:11:09 -07:00
Chris Troutner 2568818ea1 Merge pull request #62 from Permissionless-Software-Foundation/ct-unstable
fix(CORS): Adding OPTIONS as an allowed method
2025-01-12 17:03:02 -07:00
Chris Troutner c721da1562 Merge pull request #165 from Permissionless-Software-Foundation/usage
Usage
2024-12-07 21:28:57 -08:00
Chris Troutner 3606a2d31b code cleanup 2024-12-07 21:27:41 -08:00
Chris Troutner 764905b041 fix(usage): Adding endpoints to analyize 24 hour usage of the REST API 2024-12-07 21:25:10 -08:00
Chris Troutner 556c6ec522 fix(usage): Adding Usage REST API 2024-12-07 20:48:08 -08:00
Chris Troutner 04395b70a6 Syncing with ct-unstable. Fixing merge conflict 2024-12-07 18:08:25 -08:00
Chris Troutner 4564090830 fix(CORS): Adding OPTIONS as an allowed method 2024-10-01 08:17:17 -07:00
Chris Troutner 81521151f4 Merge pull request #164 from Permissionless-Software-Foundation/ct-unstable
fix(ipfs): Disabling webRTC again
2024-09-18 12:02:22 -07:00
Chris Troutner 5a819c1d9c linting 2024-09-18 12:01:10 -07:00
Chris Troutner f2f3208fda fix(ipfs): Disabling webRTC again 2024-09-18 12:00:56 -07:00
Chris Troutner 7fe888cdda Merge pull request #163 from Permissionless-Software-Foundation/ct-unstable
fix(webRTC): Renabling webRTC & circuit relays
2024-09-17 07:50:59 -07:00
Chris Troutner db3f1bf2cf fix(webRTC): Renabling webRTC & circuit relays 2024-09-17 07:49:43 -07:00
Chris Troutner 064ac36dc7 linting 2024-04-26 14:38:17 -07:00
Chris Troutner 5e2cf86f4b Checking in code before switching branches 2024-04-26 14:38:03 -07:00
Chris Troutner 425d2ada65 Binding 'this' object to some subfunctions 2024-04-20 07:14:17 -07:00
23 changed files with 1159 additions and 15 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
# Pedigree
This repository was forked from [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate). Code changes from that upstream repository is frequently pulled in and merged to this repository.
This repository was forked from [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider). Code changes from that upstream repository is frequently pulled in and merged to this repository.
+6 -1
View File
@@ -25,6 +25,7 @@ import config from '../config/index.js' // this first.
import AdminLib from '../src/adapters/admin.js'
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
import { usageMiddleware } from '../src/use-cases/usage-use-cases.js'
import wlogger from '../src/adapters/wlogger.js'
import Controllers from '../src/controllers/index.js'
import { applyPassportMods } from '../config/passport.js'
@@ -67,6 +68,7 @@ class Server {
app.use(bodyParser())
app.use(session())
app.use(errorMiddleware())
app.use(usageMiddleware())
// Used to generate the docs.
app.use(mount('/', serve(`${process.cwd()}/docs`)))
@@ -83,7 +85,10 @@ class Server {
// Enable CORS for testing
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
// Dev Note: This line must come BEFORE controllers.attachRESTControllers()
app.use(cors({ origin: '*' }))
app.use(cors({
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
}))
// Wait for any adapters to initialize.
await this.controllers.initAdapters()
+3 -3
View File
@@ -1,6 +1,6 @@
{
"password": "bDZWE3vEO7Ufvil2s8Ko",
"password": "eYrzProqwsAyO3CT0Ftf",
"email": "system@system.com",
"id": "66e38befd548c285159a7084",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY2ZTM4YmVmZDU0OGMyODUxNTlhNzA4NCIsImlhdCI6MTcyNjE4ODUyN30.h31T7o2Si1ZOTgCf91IC4KNdkmaCqs5FNWEXysdnYTg"
"id": "67a8af30c958c587f890cd8f",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhZjMwYzk1OGM1ODdmODkwY2Q4ZiIsImlhdCI6MTczOTEwODE0NH0.OFdVwH8ikbChHV0wjRpYvnL7vu46HDYS-od3FRtm9Nc"
}
+4 -4
View File
@@ -28,7 +28,7 @@
"datastore-fs": "9.1.7",
"glob": "7.1.6",
"helia": "4.2.5",
"helia-coord": "1.5.14",
"helia-coord": "1.5.15",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
"jwt-bch-lib": "1.3.0",
@@ -12582,9 +12582,9 @@
}
},
"node_modules/helia-coord": {
"version": "1.5.14",
"resolved": "http://94.130.170.209:4873/helia-coord/-/helia-coord-1.5.14.tgz",
"integrity": "sha512-HUd6jYla/bM0tAV2bc5NWQFPBUScBTljoxl2SulvYWcH19k8LAwL5cZjbBJG88/nzC6RwtQFRV1zMQxKjBy/WA==",
"version": "1.5.15",
"resolved": "http://94.130.170.209:4873/helia-coord/-/helia-coord-1.5.15.tgz",
"integrity": "sha512-j3Oe0HKRf60iGfM0zigkaTEOE+KArWyI4LArQuVyew+EJ10anIfz6lgl+NBsOd8IlgtbaORmJlf2/Qed76HKkQ==",
"license": "MIT",
"dependencies": {
"@chris.troutner/retry-queue": "1.0.10",
+1 -1
View File
@@ -43,7 +43,7 @@
"datastore-fs": "9.1.7",
"glob": "7.1.6",
"helia": "4.2.5",
"helia-coord": "1.5.14",
"helia-coord": "1.5.15",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
"jwt-bch-lib": "1.3.0",
+3 -3
View File
@@ -20,7 +20,7 @@ import { yamux } from '@chainsafe/libp2p-yamux'
// import { bootstrap } from '@libp2p/bootstrap'
// import { identifyService } from 'libp2p/identify'
import { identify } from '@libp2p/identify'
// import { circuitRelayServer, circuitRelayTransport } from 'libp2p/circuit-relay'
// import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { webSockets } from '@libp2p/websockets'
@@ -193,8 +193,8 @@ class IpfsAdapter {
listen: [
'/ip4/127.0.0.1/tcp/0',
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
'/webrtc'
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
// '/webrtc'
]
},
transports,
+7
View File
@@ -23,6 +23,13 @@ class Controllers {
this.useCases = new UseCases(localConfig)
this.timerControllers = new TimerControllers({ adapters: this.adapters, useCases: this.useCases })
this.config = config
// Bind 'this' object to all subfunction
this.initAdapters = this.initAdapters.bind(this)
this.initUseCases = this.initUseCases.bind(this)
this.attachRESTControllers = this.attachRESTControllers.bind(this)
this.attachControllers = this.attachControllers.bind(this)
this.attachRPCControllers = this.attachRPCControllers.bind(this)
}
// Spin up any adapter libraries that have async startup needs.
+8
View File
@@ -16,6 +16,7 @@ import IpfsRESTController from './ipfs/index.js'
import P2wdbRESTController from './p2wdb/index.js'
import PriceRESTController from './price/index.js'
import config from '../../../config/index.js'
import UsageRESTController from './usage/index.js'
class RESTControllers {
constructor (localConfig = {}) {
@@ -33,6 +34,9 @@ class RESTControllers {
)
}
// Bind 'this' object to all subfunctions.
this.attachRESTControllers = this.attachRESTControllers.bind(this)
// Encapsulate dependencies
this.config = config
}
@@ -76,6 +80,10 @@ class RESTControllers {
// Attach the REST API Controllers associated with the /price route
const priceRESTController = new PriceRESTController(dependencies)
priceRESTController.attach(app)
// Attach the REST API Controllers associated with the /usage route
const usageRESTController = new UsageRESTController(dependencies)
usageRESTController.attach(app)
}
}
+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())
@@ -0,0 +1,119 @@
/*
REST API Controller library for the /usage route
*/
// Global npm libraries
// Local libraries
import wlogger from '../../../adapters/wlogger.js'
class UsageRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'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 /usage REST Controller.'
)
}
// Encapsulate dependencies
// Bind 'this' object to all subfunctions
this.getStatus = this.getStatus.bind(this)
this.getTopIps = this.getTopIps.bind(this)
this.getTopEndpoints = this.getTopEndpoints.bind(this)
this.handleError = this.handleError.bind(this)
}
/**
* @api {get} /usage Get status on IPFS infrastructure
* @apiPermission public
* @apiName GetUsageStatus
* @apiGroup REST Usage
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage
*
*/
getStatus (ctx) {
try {
// const status = await this.adapters.ipfs.getStatus()
const status = this.useCases.usage.getRestSummary()
ctx.body = { status }
} catch (err) {
wlogger.error('Error in usage/controller.js/getStatus(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
/**
* @api {get} /usage/ips Get top IP addresses consuming the REST API
* @apiPermission public
* @apiName GetUsageIPs
* @apiGroup REST Usage
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/ips
*
*/
getTopIps (ctx) {
try {
const ips = this.useCases.usage.getTopIps()
ctx.body = { ips }
} catch (err) {
wlogger.error('Error in usage/controller.js/getTopIps(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
/**
* @api {get} /usage/endpoints Get top endpoints consumed from the REST API
* @apiPermission public
* @apiName GetUsageEndpoints
* @apiGroup REST Usage
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5020/usage/endpoints
*
*/
getTopEndpoints (ctx) {
try {
const endpoints = this.useCases.usage.getTopEndpoints()
ctx.body = { endpoints }
} catch (err) {
wlogger.error('Error in usage/controller.js/getTopEndpoints(): ')
// ctx.throw(422, err.message)
this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
// module.exports = IpfsRESTControllerLib
export default UsageRESTControllerLib
+65
View File
@@ -0,0 +1,65 @@
/*
REST API library for the /usage route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import UsageRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
// let _this
class UsageRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating IPFS REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating IPFS REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.usageRESTController = new UsageRESTControllerLib(dependencies)
this.validators = new Validators()
// Instantiate the router and set the base route.
const baseUrl = '/usage'
this.router = new Router({ prefix: baseUrl })
// _this = this
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.get('/', this.usageRESTController.getStatus)
this.router.get('/ips', this.usageRESTController.getTopIps)
this.router.get('/endpoints', this.usageRESTController.getTopEndpoints)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
// module.exports = BchRouter
export default UsageRouter
+18 -1
View File
@@ -28,6 +28,7 @@ class TimerControllers {
// Bind 'this' object to all subfunctions.
this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
this.cleanUsage = this.cleanUsage.bind(this)
// this.startTimers()
}
@@ -36,13 +37,15 @@ class TimerControllers {
startTimers () {
// Any new timer control functions can be added here. They will be started
// when the server starts.
this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 10)
this.optimizeWalletHandle = setInterval(this.exampleTimerFunc, 60000 * 60)
this.cleanUsageHandle = setInterval(this.cleanUsage, 60000 * 60) // 1 hour
return true
}
stopTimers () {
clearInterval(this.optimizeWalletHandle)
clearInterval(this.cleanusageHandle)
}
// Replace this example function with your own timer handler.
@@ -60,6 +63,20 @@ class TimerControllers {
return false
}
}
// Clean the usage state so that stats reflect the last 24 hours.
cleanUsage () {
try {
this.useCases.usage.cleanUsage()
return true
} catch (err) {
console.error('Error in time-controller.js/cleanUsage(): ', err)
// Note: Do not throw an error. This is a top-level function.
return false
}
}
}
export default TimerControllers
+2
View File
@@ -7,6 +7,7 @@
// Local libraries
import UserUseCases from './user.js'
import IpfsUseCases from './ipfs-use-cases.js'
import { UsageUseCases } from './usage-use-cases.js'
class UseCases {
constructor (localConfig = {}) {
@@ -26,6 +27,7 @@ class UseCases {
// console.log('use-cases/index.js localConfig: ', localConfig)
this.user = new UserUseCases(localConfig)
this.ipfs = new IpfsUseCases(localConfig)
this.usage = new UsageUseCases(localConfig)
}
// Run any startup Use Cases at the start of the app.
+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
+136
View File
@@ -0,0 +1,136 @@
/*
Use Case library for tracking usage. This library contains business logic
for tracking the usage of REST API and JSON RPC calls. This library is used
by admins to keep an eye on how many API calls were made in a 24-hour and
1-hour time period.
*/
// This global variable is used to share data between the REST middleware and
// the Usage Use Case class instance.
let restCalls = []
class UsageUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Usage Use Cases library.'
)
}
// 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)
// State
}
// Clean up the state by removing entries that are older than 24 hours. This
// ensures stats reflect only the last 24 hours.
// This function is called by a Timer Controller.
cleanUsage () {
try {
const now = new Date()
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
}
}
// Track the calls to a REST API
getRestSummary (inObj = {}) {
try {
console.log(`getRestSummary(): There have been ${restCalls.length} REST calls`)
return restCalls.length
} catch (err) {
console.error('Error in usage-use-cases.js/getRestSummary()')
throw err
}
}
// Get the top 20 IP addresses from the stats.
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 => {
countMap.set(ip, (countMap.get(ip) || 0) + 1)
})
// Convert the Map into an array of objects with `str` and `cnt` properties
const result = Array.from(countMap, ([ip, cnt]) => ({ ip, cnt }))
// Sort the results by the `cnt` property in descending order
result.sort((a, b) => b.cnt - a.cnt)
// Ensure the result has at most 20 elements
return result.slice(0, 20)
} catch (err) {
console.error('Error in usage-use-cases.js/getTopIps()')
throw err
}
}
// Get the top 20 most consumed endpoints.
getTopEndpoints () {
try {
const endpoints = restCalls.map(x => `${x.method} ${x.url}`)
// Create a Map to count occurrences of each IP address string
const countMap = new Map()
endpoints.forEach(endpoint => {
countMap.set(endpoint, (countMap.get(endpoint) || 0) + 1)
})
// Convert the Map into an array of objects with `str` and `cnt` properties
const result = Array.from(countMap, ([endpoint, cnt]) => ({ endpoint, cnt }))
// Sort the results by the `cnt` property in descending order
result.sort((a, b) => b.cnt - a.cnt)
// Ensure the result has at most 20 elements
return result.slice(0, 20)
} catch (err) {
console.error('Error in usage-use-cases.js/getTopEndpoints()')
throw err
}
}
}
// This Koa middleware is called any time there is a REST API. It logs the
// details from the request object.
function usageMiddleware () {
return async (ctx, next) => {
try {
await next()
// console.log('ctx.request: ', ctx.request)
const now = new Date()
const reqObj = {
ip: ctx.request.ip,
url: ctx.request.url,
method: ctx.request.method,
timestamp: now.getTime()
}
// console.log('reqObj: ', reqObj)
restCalls.push(reqObj)
} catch (err) {
ctx.status = err.status || 500
ctx.body = err.message
ctx.app.emit('error', err, ctx)
}
}
};
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)
}
})
})
})