mirror of
https://github.com/Permissionless-Software-Foundation/x402-base-facilitator.git
synced 2026-09-21 16:52:01 -07:00
@@ -62,5 +62,7 @@ docs
|
||||
coverage
|
||||
database/
|
||||
system-user-*.json
|
||||
orbitdb
|
||||
ipfsdata
|
||||
|
||||
!README.md
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
# koa-api-boilerplate
|
||||
[](http://standardjs.com) [](https://coveralls.io/github/christroutner/babel-free-koa2-api-boilerplate?branch=unstable) [](https://github.com/semantic-release/semantic-release) [](https://greenkeeper.io/)
|
||||
# ipfs-service-provider
|
||||
[](http://standardjs.com) [](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
@@ -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()
|
||||
|
||||
Vendored
+1
-1
@@ -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'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"}}
|
||||
Generated
+11745
-597
File diff suppressed because it is too large
Load Diff
+16
-10
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)}`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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.
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user