Merge pull request #3 from christroutner/ct-unstable

Big merge
This commit is contained in:
Chris Troutner
2021-04-08 15:02:20 -07:00
committed by GitHub
25 changed files with 13469 additions and 655 deletions
+2
View File
@@ -62,5 +62,7 @@ docs
coverage
database/
system-user-*.json
orbitdb
ipfsdata
!README.md
+16 -29
View File
@@ -1,40 +1,30 @@
# koa-api-boilerplate
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![Coverage Status](https://coveralls.io/repos/github/christroutner/babel-free-koa2-api-boilerplate/badge.svg?branch=unstable)](https://coveralls.io/github/christroutner/babel-free-koa2-api-boilerplate?branch=unstable) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Greenkeeper badge](https://badges.greenkeeper.io/christroutner/koa-api-boilerplate.svg)](https://greenkeeper.io/)
# ipfs-service-provider
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
This is a 'boilerplate' repository. It's intended to be forked to start new projects. This repository has been forked from the [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate). It has all the same features as that boilerplate:
This repository is a boilerplate for building APIs with
[koa2](https://github.com/koajs/koa/tree/v2.x) and Mongo DB.
This repository was originally forked from Adrian Obelmejias'
[koa-api-boilerplate repository](https://github.com/adrianObel/koa2-api-boilerplate).
It makes the following modifications:
- [Koa](https://koajs.com/) framework for REST APIs
- User management
- Access control using [JWT tokens](https://jwt.io/).
- Removes babel as a dependency. This repository is now naively compatible with
node v8.9 or higher.
This boilerplate extends that code to provide the basic features required to be a 'service provider' on the IPFS network. See [this article](https://troutsblog.com/blog/ipfs-api) if you're new to the concept of service provides on IPFS. These basic features include:
- Replaced `bcrypt` dependency with `bcryptjs`. This improves compatibility across
versions of node.js and across OSs.
- [ipfs-coord](https://www.npmjs.com/package/ipfs-coord) for coordinating service providers and consumers across the IPFS network.
- JSON RPC for creating an API between providers and consumers.
- Configured for Jenkins (continuous integration), Coveralls (code coverage), Green Keeper (automated dependency management), and Semantic Release (automated versioning).
- 'Production' environment is targeted for packaging as a Docker container.
- 'admin' user type added in addition to standard 'user' type. Allows the creation
of private vs public APIs that only be accessed by an admin. Useful for privileged
commands like updating and deleting other users.
- Winston logging integrated for daily rotated logs and a maximum size of
1 megabyte.
- Linting enforced with [Husky](https://github.com/typicode/husky) and [JavaScript Standard Style rules](https://www.npmjs.com/package/standard).
If you are interested in creating your own service provider on the IPFS network, fork this repository and start building.
## Features
This project covers basic necessities of most APIs.
* [Koa](https://koajs.com/) framework for REST APIs
* Authentication (passport & jwt)
* Database (mongoose)
* Testing (mocha)
* Doc generation with apidoc
* Linting using standard
* Packaged as a Docker container
* [ipfs-coord](https://www.npmjs.com/package/ipfs-coord)
* JSON RPC for mirroring the REST API over IPFS
@@ -70,6 +60,7 @@ npm start
│ ├── modules
│ │ ├── controller.js # Module-specific controllers
│ │ └── router.js # Router definitions for module
| ├── rpc # JSON RPC over IPFS
│ ├── models # Mongoose models
│ └── middleware # Custom middleware
│ └── validators # Validation middleware
@@ -103,14 +94,10 @@ Visit `http://localhost:5000/docs/` to view docs
* [Mocha](https://mochajs.org/)
* [apidoc](http://apidocjs.com/)
* [ESLint](http://eslint.org/)
* [ipfs-coord](https://www.npmjs.com/package/ipfs-coord)
## IPFS
v2.3.0 uploaded to IPFS:
- Get it: `ipfs get QmUz4b2KwNLNvHZRTYcgrPCuKAhMB73XWN8vY8LLVVEYV1`
- Pin it: `ipfs pin add -r QmUz4b2KwNLNvHZRTYcgrPCuKAhMB73XWN8vY8LLVVEYV1`
Snapshots pinned to IPFS will be listed here.
## License
MIT
test
+7 -1
View File
@@ -12,9 +12,11 @@ const cors = require('kcors')
// Local libraries
const config = require('../config') // this first.
const IPFSLib = require('../src/lib/ipfs')
const AdminLib = require('../src/lib/admin')
const adminLib = new AdminLib()
// const JSONRPC = require('../src/rpc')
// const rpc = new JSONRPC()
const errorMiddleware = require('../src/middleware')
const wlogger = require('../src/lib/wlogger')
@@ -70,6 +72,10 @@ async function startServer () {
const success = await adminLib.createSystemUser()
if (success) console.log('System admin user created.')
// Start the IPFS node.
const ipfsLib = new IPFSLib()
await ipfsLib.start()
return app
}
// startServer()
+1 -1
View File
@@ -7,6 +7,6 @@
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/koa-server-dev',
database: 'mongodb://localhost:27017/ipfs-service-dev',
env: 'dev'
}
+1 -1
View File
@@ -10,6 +10,6 @@
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://172.17.0.1:5555/koa-server-prod',
database: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
env: 'prod'
}
+1 -1
View File
@@ -7,6 +7,6 @@
module.exports = {
session: 'secret-boilerplate-token',
token: 'secret-jwt-token',
database: 'mongodb://localhost:27017/koa-server-test',
database: 'mongodb://localhost:27017/ipfs-service-test',
env: 'test'
}
+1 -1
View File
@@ -1,6 +1,6 @@
const common = require('./env/common')
const env = process.env.KOA_ENV || 'development'
const env = process.env.SVC_ENV || 'development'
const config = require(`./env/${env}`)
module.exports = Object.assign({}, common, config)
+13
View File
@@ -0,0 +1,13 @@
# Examples
Below are a series of JSON RPC calls that can be manually entered at chat.fullstack.cash to interact with the JSON RPC of this IPFS Service Provider.
- `{"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "createUser", "email": "test555@test.com", "name": "testy tester", "password": "password"}}`
- `{"jsonrpc":"2.0","id":"556","method":"auth","params":{ "endpoint": "authUser", "login": "test555@test.com", "password": "password"}}`
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getAllUsers", "apiToken": "<JWT>"}}`
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "updateUser", "apiToken": "<JWT>", "userId": "<_id>", "name": "test999"}}`
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getUser", "apiToken": "<JWT>", "userId": "<_id>"}}`
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "deleteUser", "userId": "<_id>", "apiToken": "<JWT>"}}`
{"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "createUser", "email": "test555@test.com", "name": "testy tester", "password": "password"}}
+11745 -597
View File
File diff suppressed because it is too large Load Diff
+16 -10
View File
@@ -1,19 +1,20 @@
{
"name": "koa-api-boilerplate",
"version": "3.0.0",
"description": "Koa2 boilerplate covering essentials for REST API and auth.",
"name": "ipfs-service-provider",
"version": "1.0.0",
"description": "A Koa-based combination of a REST API and IPFS JSON RPC. Boilerplate for starting new projects.",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "npm run test:all",
"test:all": "export KOA_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/",
"test:unit:lib": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/biz-logic/",
"test:unit:rest": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/rest-api/",
"test:e2e:auto": "export KOA_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
"test:all": "export SVC_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/json-rpc/ test/unit/rest-api/ test/e2e/automated/",
"test:unit:lib": "export SVC_ENV=test && mocha --exit --timeout 15000 test/unit/biz-logic/",
"test:unit:rest": "export SVC_ENV=test && mocha --exit --timeout 15000 test/unit/rest-api/",
"test:unit:jsonrpc": "export SVC_ENV=test && mocha --exit --timeout 15000 test/unit/json-rpc/",
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
"lint": "standard --env mocha --fix",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "export KOA_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/"
"coverage:report": "export SVC_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/json-rpc/ test/unit/rest-api/ test/e2e/automated/"
},
"keywords": [
"koa-api-boilerplate",
@@ -29,14 +30,18 @@
"author": "Chris Troutner <chris.troutner@gmail.com>",
"license": "MIT",
"apidoc": {
"title": "koa-api-boilerplate",
"title": "ipfs-service-provider",
"url": "localhost:5000"
},
"repository": "christroutner/koa-api-boilerplate",
"repository": "christroutner/ipfs-service-provider",
"dependencies": {
"@psf/bch-js": "^4.17.6",
"axios": "^0.21.1",
"bcryptjs": "^2.4.3",
"glob": "^7.1.6",
"ipfs": "^0.54.4",
"ipfs-coord": "^2.2.10",
"jsonrpc-lite": "^2.2.0",
"jsonwebtoken": "^8.5.1",
"kcors": "^2.2.2",
"koa": "^2.13.1",
@@ -52,6 +57,7 @@
"mongoose": "^5.11.15",
"nodemailer": "^6.4.17",
"passport-local": "^1.0.0",
"uuid": "^8.3.2",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0"
},
+95
View File
@@ -0,0 +1,95 @@
/*
This support library handles the connection to the IPFS network. It instantiates
the IPFS node and starts the ipfs-coord library.
*/
// Global npm libraries
const IPFS = require('ipfs')
const IpfsCoord = require('ipfs-coord')
// const IpfsCoord = require('../../../ipfs-coord')
const BCHJS = require('@psf/bch-js')
// Local libraries
const JSONRPC = require('../rpc')
class IPFSLib {
constructor (localConfig) {
// Encapsulate dependencies
this.IPFS = IPFS
this.IpfsCoord = IpfsCoord
this.bchjs = new BCHJS()
this.rpc = new JSONRPC()
// this.rpc = {}
// if (localConfig.rpc) {
// this.rpc = localConfig.rpc
// }
}
// This is a 'macro' start method. It kicks off several smaller methods that
// start the various subcomponents of this IPFS library.
async start () {
await this.startIpfs()
await this.startIpfsCoord()
// Update the RPC instance with the instance of ipfs-coord.
this.rpc.ipfsCoord = this.ipfsCoord
console.log('IPFS is ready.')
}
async startIpfs () {
try {
// Ipfs Options
const ipfsOptions = {
repo: './ipfsdata',
start: true,
config: {
relay: {
enabled: true, // enable circuit relay dialer and listener
hop: {
enabled: true // enable circuit relay HOP (make this node a relay)
}
},
pubsub: true, // enable pubsub
Swarm: {
ConnMgr: {
HighWater: 30,
LowWater: 10
}
}
}
}
// Create a new IPFS node.
this.ipfs = await this.IPFS.create(ipfsOptions)
// Set the 'server' profile so the node does not scan private networks.
await this.ipfs.config.profiles.apply('server')
const nodeConfig = await this.ipfs.config.getAll()
console.log(`IPFS node configuration: ${JSON.stringify(nodeConfig, null, 2)}`)
} catch (err) {
console.error('Error in startIpfs()')
throw err
}
}
async startIpfsCoord () {
try {
this.ipfsCoord = new this.IpfsCoord({
ipfs: this.ipfs,
type: 'node.js',
bchjs: this.bchjs,
privateLog: this.rpc.router
})
await this.ipfsCoord.isReady()
} catch (err) {
console.error('Error in startIpfsCoord()')
throw err
}
}
}
module.exports = IPFSLib
+33
View File
@@ -30,6 +30,7 @@ class UserLib {
// Enforce default value of 'user'
user.type = 'user'
// console.log('user: ', user)
// Save the new user model to the database.
await user.save()
@@ -39,6 +40,7 @@ class UserLib {
// Convert the database model to a JSON object.
const userData = user.toJSON()
// console.log('userData: ', userData)
// Delete the password property.
delete userData.password
@@ -93,6 +95,9 @@ class UserLib {
async updateUser (existingUser, newData) {
try {
// console.log('existingUser: ', existingUser)
// console.log('newData: ', newData)
// Input Validation
// Optional inputs, but they must be strings if included.
if (newData.email && typeof newData.email !== 'string') {
@@ -145,6 +150,34 @@ class UserLib {
throw err
}
}
// Used to authenticate a user. If the login and password salt match a user in
// the database, then it returns the user model. The Koa REST API uses the
// Passport library for this functionality. This function is used to
// authenticate users who login via the JSON RPC.
async authUser (login, passwd) {
try {
// console.log('login: ', login)
// console.log('passwd: ', passwd)
const user = await UserModel.findOne({ email: login })
if (!user) {
throw new Error('User not found')
}
const isMatch = await user.validatePassword(passwd)
if (!isMatch) {
throw new Error('Login credential do not match')
}
return user
} catch (err) {
// console.error('Error in users.js/authUser()')
console.log('')
throw err
}
}
}
module.exports = UserLib
+8 -8
View File
@@ -44,13 +44,13 @@ const wlogger = winston.createLogger({
})
// This controls the logs to CONSOLE
/*
wlogger.add(
new winston.transports.Console({
format: winston.format.simple(),
level: "info"
})
)
*/
if (config.env !== 'test') {
wlogger.add(
new winston.transports.Console({
format: winston.format.simple(),
level: 'info'
})
)
}
module.exports = wlogger
+4 -1
View File
@@ -1,3 +1,7 @@
/*
REST API validator middleware.
*/
const User = require('../models/users')
const config = require('../../config')
const getToken = require('../lib/auth')
@@ -90,7 +94,6 @@ class Validators {
// the ID used in the JWT, or failing that, the ID used in the JWT matches
// an Admin user. This prevents situations like users updating other users
// profiles or non-admins deleting users.
// TODO Tests must be developed before developing this function.
async ensureTargetUserOrAdmin (ctx, next) {
try {
// console.log(`getToken: ${typeof (getToken)}`)
+2
View File
@@ -59,6 +59,8 @@ class Auth {
*/
async authUser (ctx, next) {
try {
// Retrieve the user from the database after they've proven the correct
// password.
const user = await _this.passport.authUser(ctx, next)
if (!user) {
ctx.throw(401)
+98
View File
@@ -0,0 +1,98 @@
/*
This is the JSON RPC router for the users API
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
// Local libraries
// const AuthLib = require('../../lib/auth')
const UserLib = require('../../lib/users')
const wlogger = require('../../lib/wlogger')
class AuthRPC {
constructor (localConfig) {
// Encapsulate dependencies
// this.authLib = new AuthLib()
this.jsonrpc = jsonrpc
this.userLib = new UserLib()
}
// Top-level router for this library. All other methods in this class are for
// a specific endpoint. This method routes incoming calls to one of those
// methods.
async authRouter (rpcData) {
let endpoint = 'unknown'
try {
// console.log('authRouter rpcData: ', rpcData)
endpoint = rpcData.payload.params.endpoint
// Route the call based on the requested endpoint.
switch (endpoint) {
case 'authUser':
return await this.authUser(rpcData)
}
} catch (err) {
console.error('Error in AuthRPC/authRouter()')
// throw err
return {
success: false,
status: 500,
message: err.message,
endpoint
}
}
}
async authUser (rpcData) {
try {
// console.log('authUser rpcData: ', rpcData)
if (!rpcData.payload.params.login) {
throw new Error('login must be specified')
}
if (!rpcData.payload.params.password) {
throw new Error('password must be specified')
}
const login = rpcData.payload.params.login
const password = rpcData.payload.params.password
const user = await this.userLib.authUser(login, password)
// console.log('user: ', user)
const token = user.generateToken()
const response = {
endpoint: 'authUser',
userId: user._id,
userType: user.type,
userName: user.name,
userEmail: user.email,
apiToken: token,
status: 200,
success: true,
message: ''
}
return response
} catch (err) {
// console.error('Error in authUser()')
wlogger.error('Error in authUser(): ', err)
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'authUser'
}
}
}
}
module.exports = AuthRPC
+115
View File
@@ -0,0 +1,115 @@
/*
This is the parent class library for the RPC controller.
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
// Local support libraries
const wlogger = require('../lib/wlogger')
const UserController = require('./users')
const AuthController = require('./auth')
let _this
class JSONRPC {
constructor (localConfig) {
// Encapsulate dependencies
this.jsonrpc = jsonrpc
this.userController = new UserController()
this.authController = new AuthController()
// This will be replaced once the ipfs-coord lib finishes initializing.
this.ipfsCoord = {}
_this = this
}
// This method takes a raw string of data from IPFS, parses it, and determins
// which controller to route the instruction to.
async router (str, from) {
try {
// console.log('router str: ', str)
// console.log('router from: ', from)
// Exit quietly if 'from' is not specified.
if (!from || typeof from !== 'string') {
// console.warn(
// 'Warning: Can not send JSON RPC response. Can not determine which peer this message came from.'
// )
return
}
// Attempt to parse the incoming data as a JSON RPC string.
const parsedData = _this.jsonrpc.parse(str)
// console.log('parsedData: ', parsedData)
// Exit quietly if the incoming string is an invalid JSON RPC string.
if (parsedData.type === 'invalid') {
return
}
// Default return string
let retObj = _this.defaultResponse()
// Route the command to the appropriate route handler.
switch (parsedData.payload.method) {
case 'users':
retObj = await _this.userController.userRouter(parsedData)
break
case 'auth':
retObj = await _this.authController.authRouter(parsedData)
break
}
// console.log('retObj: ', retObj)
// Convert the returned object into a JSON RPC response string.
const retJson = _this.jsonrpc.success(parsedData.payload.id, {
method: parsedData.payload.method,
reciever: from,
value: retObj
})
const retStr = JSON.stringify(retJson, null, 2)
// console.log('retStr: ', retStr)
// Encrypt and publish the response to the originators private OrbitDB,
// if ipfs-coord has been initialized and the peers ID is registered.
if (_this.ipfsCoord.ipfs) {
await _this.ipfsCoord.ipfs.orbitdb.sendToDb(from, retStr)
}
// Return the response and originator. Useful for testing.
return { from, retStr }
} catch (err) {
// console.error('Error in rpc router(): ', err)
wlogger.error('Error in rpc router(): ', err)
// Do not throw error. This is a top-level function.
}
}
// The default JSON RPC response if the incoming command could not be routed.
defaultResponse () {
try {
// const errorObj = this.jsonrpc.error(
// 'Can not route',
// new jsonrpc.JsonRpcError('Input does not match routing rules', 422)
// )
// const errorStr = JSON.stringify(errorObj)
// return errorStr
const errorObj = {
success: false,
status: 422,
message: 'Input does not match routing rules.'
}
return errorObj
} catch (err) {
console.error('Error in defaultResponse()')
throw err
}
}
}
module.exports = JSONRPC
+209
View File
@@ -0,0 +1,209 @@
/*
This is the JSON RPC router for the users API
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
// Local libraries
const UserLib = require('../../lib/users')
const Validators = require('../validators')
class UserRPC {
constructor (localConfig) {
// Encapsulate dependencies
this.userLib = new UserLib()
this.jsonrpc = jsonrpc
this.validators = new Validators()
}
// Top-level router for this library. All other methods in this class are for
// a specific endpoint. This method routes incoming calls to one of those
// methods.
async userRouter (rpcData) {
let endpoint = 'unknown'
try {
// console.log('userRouter rpcData: ', rpcData)
endpoint = rpcData.payload.params.endpoint
let user
// Route the call based on the value of the method property.
switch (endpoint) {
case 'createUser':
return await this.createUser(rpcData)
case 'getAllUsers':
await this.validators.ensureUser(rpcData)
return await this.getAll(rpcData)
case 'getUser':
user = await this.validators.ensureUser(rpcData)
return await this.getUser(rpcData, user)
case 'updateUser':
user = await this.validators.ensureTargetUserOrAdmin(rpcData)
return await this.updateUser(rpcData, user)
case 'deleteUser':
user = await this.validators.ensureTargetUserOrAdmin(rpcData)
return await this.deleteUser(rpcData, user)
}
} catch (err) {
console.error('Error in UsersRPC/rpcRouter()')
// throw err
return {
success: false,
status: 500,
message: err.message,
endpoint
}
}
}
// Create a new user
async createUser (rpcData) {
try {
// console.log('createUser rpcData: ', rpcData)
const retObj = await this.userLib.createUser(rpcData.payload.params)
// Add generic JSON RPC properties that every entry gets.
retObj.endpoint = 'createUser'
retObj.success = true
retObj.status = 200
retObj.message = ''
return retObj
} catch (err) {
// console.error('Error in createUser()')
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'createUser'
}
}
}
// Get all Users.
async getAll () {
try {
const users = await this.userLib.getAllUsers()
return {
users,
endpoint: 'getAllUsers',
success: true,
status: 200,
message: ''
}
} catch (err) {
// console.error('Error in getAll()')
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'getAllUsers'
}
}
}
// Get a specific user.
async getUser (rpcData, userModel) {
try {
// console.log('getUser rpcData: ', rpcData)
// Throw error if rpcData does not include 'userId' property for target user.
const userId = rpcData.payload.params.userId
const user = await this.userLib.getUser({ id: userId })
return {
user,
endpoint: 'getUser',
success: true,
status: 200,
message: ''
}
} catch (err) {
// console.error('Error in getUser()')
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'getUser'
}
}
}
async updateUser (rpcData, userModel) {
try {
// console.log('updateUser rpcData: ', rpcData)
const newData = rpcData.payload.params
const user = await this.userLib.updateUser(userModel, newData)
return {
user,
endpoint: 'updateUser',
success: true,
status: 200,
message: ''
}
} catch (err) {
// console.log('updateUser err: ', err)
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'updateUser'
}
}
}
async deleteUser (rpcData, userModel) {
try {
// console.log('deleteUser rpcData: ', rpcData)
await this.userLib.deleteUser(userModel)
const retObj = {
endpoint: 'deleteUser',
success: true,
status: 200,
message: ''
}
return retObj
} catch (err) {
// console.error('Error in deleteUser()')
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'deleteUser'
}
}
}
// TODO create deleteUser()
}
module.exports = UserRPC
+85
View File
@@ -0,0 +1,85 @@
/*
Validators for the JSON RPC
*/
/* eslint no-useless-catch: 0 */
// Public npm libraries
const jwt = require('jsonwebtoken')
// Local libraries
const config = require('../../config')
const UserModel = require('../models/users')
class Validators {
constructor () {
// Encapsulate dependencies
this.config = config
this.UserModel = UserModel
this.jwt = jwt
}
// Returns if user passes a valid JWT token that resolves to a valid user.
// Otherwise it throws an error.
async ensureUser (rpcData) {
try {
// console.log('rpcData: ', rpcData)
const apiToken = rpcData.payload.params.apiToken
if (!apiToken) throw new Error('apiToken JWT required as a parameter')
const decoded = this.jwt.verify(apiToken, this.config.token)
const user = await this.UserModel.findById(decoded.id, '-password')
if (!user) throw new Error('User not found!')
return user
} catch (err) {
// console.error('Error in ensureUser()')
throw err
}
}
// This middleware ensures that the :id used in the API endpoint matches the
// the ID used in the JWT, or failing that, the ID used in the JWT matches
// an Admin user. This prevents situations like users updating other users
// profiles or non-admins deleting users.
async ensureTargetUserOrAdmin (rpcData) {
try {
// console.log('rpcData: ', rpcData)
// Ensure the JWT is passed in.
const apiToken = rpcData.payload.params.apiToken
if (!apiToken) throw new Error('apiToken JWT required as a parameter')
// Ensure a target user ID is provided.
const targetUserId = rpcData.payload.params.userId
if (!targetUserId) throw new Error('userId must be specified')
// Decode the JWT token.
const decoded = this.jwt.verify(apiToken, this.config.token)
// Get the user described by the JWT token.
const user = await this.UserModel.findById(decoded.id, '-password')
if (!user) throw new Error('User not found!')
// If this current user is an admin, then quietly exit.
if (user.type === 'admin') return
// Throw an error if the JWT token does not match the targeted user.
if (user._id.toString() !== targetUserId) {
throw new Error('User is neither admin nor target user.')
}
// Get the user model for the targeted User
const targetedUser = await this.UserModel.findById(targetUserId, '-password')
// Return the user model.
return targetedUser
} catch (error) {
// console.error('Error in ensureUser()')
throw error
}
}
}
module.exports = Validators
+44 -5
View File
@@ -26,10 +26,13 @@ describe('#users', () => {
console.log(`Connecting to database: ${config.database}`)
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(config.database, {
useUnifiedTopology: true,
useNewUrlParser: true
})
await mongoose.connect(
config.database,
{
useUnifiedTopology: true,
useNewUrlParser: true
}
)
// Delete all previous users in the database.
await testUtils.deleteAllUsers()
@@ -312,7 +315,10 @@ describe('#users', () => {
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, "Property 'type' can only be changed by Admin user")
assert.include(
err.message,
"Property 'type' can only be changed by Admin user"
)
}
})
@@ -336,6 +342,39 @@ describe('#users', () => {
// TODO: verify that an admin can change the type of a user
})
describe('#authUser', () => {
it('should return a user db model after successful authentication', async () => {
const user = await uut.authUser('test@test.com', 'password')
// console.log('user: ', user)
assert.property(user, '_id')
assert.property(user, 'email')
assert.property(user, 'name')
})
it('should throw an error if no user matches the login', async () => {
try {
await uut.authUser('noone@nowhere.com', 'password')
// console.log('user: ', user)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'User not found')
}
})
it('should throw an error if password does not match', async () => {
try {
await uut.authUser('test@test.com', 'badpassword')
// console.log('user: ', user)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Login credential do not match')
}
})
})
describe('#deleteUser', () => {
it('should throw error if no user provided', async () => {
try {
+31
View File
@@ -0,0 +1,31 @@
/*
Unit tests for the rpc/index.js library.
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
const JSONRPC = require('../../../src/rpc')
describe('#JSON RPC', () => {
let uut
beforeEach(() => {
uut = new JSONRPC()
})
describe('#router', () => {
it('should do something', async () => {
// const request = {
// 'users', // id
// 'getAll', // method
// {}
// }
const json = jsonrpc.request('users', 'getAll', {})
const str = JSON.stringify(json)
await uut.router(str)
})
})
})
+107
View File
@@ -0,0 +1,107 @@
/*
Unit tests for the rpc/index.js library.
*/
// Public npm libraries
const assert = require('chai').assert
const jsonrpc = require('jsonrpc-lite')
const sinon = require('sinon')
const { v4: uid } = require('uuid')
// Set the environment variable to signal this is a test.
process.env.SVC_ENV = 'test'
// Local libraries.
const JSONRPC = require('../../../src/rpc')
describe('#JSON RPC', () => {
let uut
let sandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new JSONRPC()
})
afterEach(() => sandbox.restore())
describe('#router', () => {
it('should exit quietly if given a random string', async () => {
const str = 'random string message'
await uut.router(str)
assert.isOk('Not throwing an error is a pass.')
})
it('should exit quietly if invalid JSON RPC message received', async () => {
const malformedRpc = '{"jsonrpc":"2.0"}'
await uut.router(malformedRpc, 'peerA')
assert.isOk('Not throwing an error is a pass.')
})
it('should return default response if routing is not possible', async () => {
const id = uid()
const json = jsonrpc.request(id, 'unknownMethod', {})
const str = JSON.stringify(json)
const result = await uut.router(str, 'peerA')
// console.log('result: ', result)
const jsonObj = jsonrpc.parse(result.retStr)
// console.log(`jsonObj: ${JSON.stringify(jsonObj, null, 2)}`)
// Assert the expected properties exist on the returned object.
assert.property(jsonObj, 'payload')
assert.property(jsonObj, 'type')
assert.property(jsonObj.payload, 'jsonrpc')
assert.property(jsonObj.payload, 'id')
assert.property(jsonObj.payload, 'result')
assert.property(jsonObj.payload.result, 'reciever')
assert.property(jsonObj.payload.result.value, 'success')
assert.property(jsonObj.payload.result.value, 'message')
// Assert the expected values exist.
assert.equal(jsonObj.payload.id, id)
assert.equal(jsonObj.payload.result.value.success, false)
assert.equal(jsonObj.payload.result.value.status, 422)
assert.equal(
jsonObj.payload.result.value.message,
'Input does not match routing rules.'
)
})
it('should catch and handle errors', async () => {
// Force an error
sandbox.stub(uut.jsonrpc, 'parse').throws(new Error('test error'))
const malformedRpc = '{"jsonrpc":"2.0"}'
await uut.router(malformedRpc, 'peerA')
assert.isOk('Not throwing an error is a pass.')
})
it('should route to users handler', async () => {
const id = uid()
const userCall = jsonrpc.request(id, 'users', { endpoint: 'getAll' })
const jsonStr = JSON.stringify(userCall, null, 2)
// Mock the users controller.
sandbox.stub(uut.userController, 'userRouter').resolves('true')
const result = await uut.router(jsonStr, 'peerA')
// console.log(result)
const obj = JSON.parse(result.retStr)
// console.log('obj: ', obj)
assert.equal(obj.result.value, 'true')
assert.equal(obj.result.method, 'users')
assert.equal(obj.id, id)
})
})
})
+187
View File
@@ -0,0 +1,187 @@
/*
Unit tests for the rpc/auth/index.js file.
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
const mongoose = require('mongoose')
const sinon = require('sinon')
const assert = require('chai').assert
const { v4: uid } = require('uuid')
// Set the environment variable to signal this is a test.
process.env.SVC_ENV = 'test'
// Local libraries
const config = require('../../../config')
const AuthRPC = require('../../../src/rpc/auth')
const UserLib = require('../../../src/lib/users')
const userLib = new UserLib()
describe('#AuthRPC', () => {
let uut
let sandbox
let testUser
before(async () => {
// Connect to the Mongo Database.
console.log(`Connecting to database: ${config.database}`)
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
config.database,
{
useUnifiedTopology: true,
useNewUrlParser: true
}
)
// Create a test user.
testUser = await userLib.createUser({
email: 'test543@test.com',
name: 'tester543',
password: 'password'
})
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new AuthRPC()
})
afterEach(() => sandbox.restore())
after(async () => {
// Delete the test user.
testUser = await userLib.getUser({ id: testUser.userData._id })
await userLib.deleteUser(testUser)
mongoose.connection.close()
})
describe('#authRouter', () => {
it('should route to the authUser method', async () => {
// Mock dependencies
sandbox.stub(uut, 'authUser').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' })
const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.authRouter(rpcData)
assert.equal(result, true)
})
it('should return 500 status on routing issue', async () => {
// Mock dependencies
sandbox.stub(uut, 'authUser').rejects(new Error('test error'))
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' })
const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.authRouter(rpcData)
assert.equal(result.success, false)
assert.equal(result.status, 500)
assert.equal(result.message, 'test error')
assert.equal(result.endpoint, 'authUser')
})
})
describe('#authUser', () => {
it('should return a JWT token if user successfully authenticates', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const authCall = jsonrpc.request(id, 'auth', {
endpoint: 'authUser',
login: 'test543@test.com',
password: 'password'
})
const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.authUser(rpcData)
// console.log('response: ', response)
assert.equal(response.endpoint, 'authUser')
assert.property(response, 'userId')
assert.equal(response.userType, 'user')
assert.property(response, 'userName')
assert.property(response, 'userEmail')
assert.property(response, 'apiToken')
assert.equal(response.status, 200)
assert.equal(response.success, true)
assert.property(response, 'message')
})
it('should return an error for invalid credentials', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const authCall = jsonrpc.request(id, 'auth', {
endpoint: 'authUser',
login: 'test543@test.com',
password: 'badpassword'
})
const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.authUser(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'Login credential do not match')
assert.equal(response.endpoint, 'authUser')
})
it('should throw an error if login is not provided', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const authCall = jsonrpc.request(id, 'auth', {
endpoint: 'authUser'
})
const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.authUser(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'login must be specified')
assert.equal(response.endpoint, 'authUser')
})
it('should throw an error if password is not provided', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const authCall = jsonrpc.request(id, 'auth', {
endpoint: 'authUser',
login: 'test543@test.com'
})
const jsonStr = JSON.stringify(authCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const response = await uut.authUser(rpcData)
// console.log('response: ', response)
assert.equal(response.success, false)
assert.equal(response.status, 422)
assert.equal(response.message, 'password must be specified')
assert.equal(response.endpoint, 'authUser')
})
})
})
+270
View File
@@ -0,0 +1,270 @@
/*
Unit tests for the JSON RPC validator middleware.
TODO: ensureTargetUserOrAdmin: it should exit quietly if user is an admin.
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
const mongoose = require('mongoose')
const sinon = require('sinon')
const assert = require('chai').assert
const { v4: uid } = require('uuid')
// Set the environment variable to signal this is a test.
process.env.SVC_ENV = 'test'
// Local libraries
const config = require('../../../config')
const Validators = require('../../../src/rpc/validators')
const UserLib = require('../../../src/lib/users')
const userLib = new UserLib()
describe('#validators', () => {
let testUser
let uut
let sandbox
before(async () => {
// Connect to the Mongo Database.
console.log(`Connecting to database: ${config.database}`)
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
config.database,
{
useUnifiedTopology: true,
useNewUrlParser: true
}
)
// Create a test user.
testUser = await userLib.createUser({
email: 'test544@test.com',
name: 'tester544',
password: 'password'
})
// console.log('testUser: ', testUser)
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new Validators()
})
afterEach(() => sandbox.restore())
after(async () => {
// Delete the test user.
testUser = await userLib.getUser({ id: testUser.userData._id })
await userLib.deleteUser(testUser)
mongoose.connection.close()
})
describe('#ensureUser', () => {
it('should return user model for valid JWT token', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getAll',
apiToken: testUser.token
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const user = await uut.ensureUser(rpcData)
// console.log('user: ', user)
assert.property(user, 'type')
assert.property(user, '_id')
assert.property(user, 'email')
assert.property(user, 'name')
})
it('should throw an error if JWT token is not included', async () => {
try {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getAll'
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureUser(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'apiToken JWT required as a parameter')
}
})
it('should throw an error if JWT token can not be decoded', async () => {
try {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwNmQxYTlkNTgxNTVjMjIzNWFmMTNhMSIsImlhdCI6MTYxNzc2Mjk3M30.6JkM1v0n71Mzsd3qzClzlMKtq6HlD0umoauG23N9FFF'
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getAll',
apiToken: token
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureUser(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'invalid signature')
}
})
it('should throw an error if the user can not be found', async () => {
try {
// Force 'error not found' error
sandbox.stub(uut.UserModel, 'findById').resolves(null)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getAll',
apiToken: testUser.token
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureUser(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'User not found!')
}
})
})
describe('#ensureTargetUserOrAdmin', () => {
it('should return user model for valid JWT token', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'deleteUser',
apiToken: testUser.token,
userId: testUser.userData._id.toString()
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const user = await uut.ensureTargetUserOrAdmin(rpcData)
assert.property(user, 'type')
assert.property(user, '_id')
assert.property(user, 'email')
assert.property(user, 'name')
})
it('should throw error if JWT token is not provided', async () => {
try {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'deleteUser'
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureTargetUserOrAdmin(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'apiToken JWT required as a parameter')
}
})
it('should throw error if user ID is not specified', async () => {
try {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'deleteUser',
apiToken: testUser.token
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureTargetUserOrAdmin(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'userId must be specified')
}
})
it('should throw error if JWT token can not be decoded', async () => {
try {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwNmU0YzkxYzdlYWNjN2Q4NWJjOGI0NCIsImlhdCI6MTYxNzg0MTI5N30.n1sas7YlqtmhBlNDBY_IXxQCrIQTiE8UITqy0PJAFFF'
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'deleteUser',
apiToken: token,
userId: testUser.userData._id.toString()
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureTargetUserOrAdmin(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'invalid signature')
}
})
it('should throw an error if user can not be found', async () => {
try {
// Force an error
sandbox.stub(uut.UserModel, 'findById').rejects(new Error('test error'))
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'deleteUser',
apiToken: testUser.token,
userId: testUser.userData._id.toString()
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
await uut.ensureTargetUserOrAdmin(rpcData)
assert.fail('Unexpected code path')
} catch (err) {
// console.log(err)
assert.include(err.message, 'test error')
}
})
// TODO: it should exit quietly if user is an admin.
})
})
+378
View File
@@ -0,0 +1,378 @@
/*
Unit tests for the rpc/users/index.js file.
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
const mongoose = require('mongoose')
const sinon = require('sinon')
const assert = require('chai').assert
const { v4: uid } = require('uuid')
// Set the environment variable to signal this is a test.
process.env.SVC_ENV = 'test'
// Local libraries
const config = require('../../../config')
const UserRPC = require('../../../src/rpc/users')
const UserModel = require('../../../src/models/users')
describe('#UserRPC', () => {
let uut
let sandbox
let testUser
before(async () => {
// Connect to the Mongo Database.
console.log(`Connecting to database: ${config.database}`)
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
await mongoose.connect(
config.database,
{
useUnifiedTopology: true,
useNewUrlParser: true
}
)
})
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new UserRPC()
})
afterEach(() => sandbox.restore())
after(() => {
mongoose.connection.close()
})
describe('#createUser', () => {
it('should create a new user', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'createUser',
email: 'test973@test.com',
name: 'test973',
password: 'password'
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.createUser(rpcData)
// console.log('result: ', result)
// CreateUser() specific return values.
assert.equal(result.userData.type, 'user')
assert.equal(result.userData.email, 'test973@test.com')
assert.equal(result.userData.name, 'test973')
assert.property(result.userData, '_id')
assert.property(result, 'token')
// Generic JSON RPC return values
assert.equal(result.endpoint, 'createUser')
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.equal(result.message, '')
// Save the user ID for future tests.
testUser = result
})
it('should return error data if biz logic throws an error', async () => {
// Force an error
sandbox.stub(uut.userLib, 'createUser').rejects(new Error('test error'))
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'createUser',
email: 'test973@test.com',
name: 'test973',
password: 'password'
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.createUser(rpcData)
// console.log('result: ', result)
// Generic JSON RPC return values
assert.equal(result.endpoint, 'createUser')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.equal(result.message, 'test error')
})
})
describe('#userRouter', () => {
it('should route to the createUser method', async () => {
// Mock dependencies
sandbox.stub(uut, 'createUser').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' })
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.userRouter(rpcData)
assert.equal(result, true)
})
it('should route to the getAllUsers method', async () => {
// Mock dependencies
sandbox.stub(uut, 'getAll').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getAllUsers',
apiToken: testUser.token
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.userRouter(rpcData)
assert.equal(result, true)
})
it('should route to the updateUser method', async () => {
// Mock dependencies
sandbox.stub(uut, 'updateUser').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'updateUser',
apiToken: testUser.token,
userId: testUser.userData._id
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.userRouter(rpcData)
// console.log('result: ', result)
assert.equal(result, true)
})
it('should route to the getUser method', async () => {
// Mock dependencies
sandbox.stub(uut, 'getUser').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getUser',
apiToken: testUser.token
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.userRouter(rpcData)
assert.equal(result, true)
})
it('should route to the deleteUsers method', async () => {
// Mock dependencies
sandbox.stub(uut, 'deleteUser').resolves(true)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'deleteUser',
apiToken: testUser.token,
userId: testUser.userData._id
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.userRouter(rpcData)
// console.log('result: ', result)
assert.equal(result, true)
})
it('should return 500 status on routing issue', async () => {
// Force an error
sandbox.stub(uut, 'createUser').rejects(new Error('test error'))
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' })
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.userRouter(rpcData)
// console.log('result: ', result)
assert.equal(result.success, false)
assert.equal(result.status, 500)
assert.equal(result.message, 'test error')
assert.equal(result.endpoint, 'createUser')
})
})
describe('#getAllUsers', () => {
it('should return all users', async () => {
const result = await uut.getAll()
// console.log('getAll result: ', result)
// Endpoint specific properties
assert.property(result, 'users')
assert.isArray(result.users)
// Generic JSON RPC return values
assert.equal(result.endpoint, 'getAllUsers')
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.equal(result.message, '')
})
it('should return error data if biz logic throws an error', async () => {
// Force an error
sandbox.stub(uut.userLib, 'getAllUsers').rejects(new Error('test error'))
const result = await uut.getAll()
// console.log('result: ', result)
// Generic JSON RPC return values
assert.equal(result.endpoint, 'getAllUsers')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.equal(result.message, 'test error')
})
})
describe('#updateUser', () => {
it('should update a user', async () => {
// Get the user model for the test user.
const testUserModel = await UserModel.findById(
testUser.userData._id,
'-password'
)
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'updateUser',
userId: testUser.userData._id.toString(),
name: 'test777'
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.updateUser(rpcData, testUserModel)
// console.log('updateUser result: ', result)
// Endpoint specific properties
assert.property(result, 'user')
assert.property(result.user, 'type')
assert.property(result.user, '_id')
assert.property(result.user, 'email')
assert.property(result.user, 'name')
// Generic JSON RPC return values
assert.equal(result.endpoint, 'updateUser')
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.equal(result.message, '')
})
it('should return error data if biz logic throws an error', async () => {
// Force an error by not specifying an user ID.
const result = await uut.updateUser()
// console.log('result: ', result)
// Generic JSON RPC return values
assert.equal(result.endpoint, 'updateUser')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.include(result.message, 'Cannot read property')
})
})
describe('#getUser', () => {
it('should return a specific user', async () => {
// Generate the parsed data that the main router would pass to this
// endpoint.
const id = uid()
const userCall = jsonrpc.request(id, 'users', {
endpoint: 'getUser',
userId: testUser.userData._id.toString()
})
const jsonStr = JSON.stringify(userCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
const result = await uut.getUser(rpcData)
// console.log('getUser result: ', result)
// Endpoint specific properties
assert.property(result, 'user')
assert.property(result.user, 'type')
assert.property(result.user, '_id')
assert.property(result.user, 'email')
assert.property(result.user, 'name')
// Generic JSON RPC return values
assert.equal(result.endpoint, 'getUser')
assert.equal(result.success, true)
assert.equal(result.status, 200)
assert.equal(result.message, '')
})
it('should return error data if biz logic throws an error', async () => {
// Force an error by not specifying an user ID.
const result = await uut.getUser()
// console.log('result: ', result)
// Generic JSON RPC return values
assert.equal(result.endpoint, 'getUser')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.include(result.message, 'Cannot read property')
})
})
describe('#deleteUser', () => {
it('should delete a user', async () => {
// Get the user model for the test user.
const testUserModel = await UserModel.findById(
testUser.userData._id,
'-password'
)
await uut.deleteUser({}, testUserModel)
// console.log(result)
assert.isOk('Not throwing an error is a success')
})
it('should return error data if biz logic throws an error', async () => {
// Force an error by not specifying an user ID.
const result = await uut.deleteUser()
// console.log('result: ', result)
// Generic JSON RPC return values
assert.equal(result.endpoint, 'deleteUser')
assert.equal(result.success, false)
assert.equal(result.status, 422)
assert.include(result.message, 'Cannot read property')
})
})
})