mirror of
https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-service.git
synced 2026-09-21 16:52:03 -07:00
Forked from ipfs-service-provider
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
# A special property that should be specified at the top of the file outside of
|
||||
# any sections. Set to true to stop .editor config file search on current file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Indentation style
|
||||
# Possible values - tab, space
|
||||
indent_style = space
|
||||
|
||||
# Indentation size in single-spaced characters
|
||||
# Possible values - an integer, tab
|
||||
indent_size = 2
|
||||
|
||||
# Line ending file format
|
||||
# Possible values - lf, crlf, cr
|
||||
end_of_line = lf
|
||||
|
||||
# File character encoding
|
||||
# Possible values - latin1, utf-8, utf-16be, utf-16le
|
||||
charset = utf-8
|
||||
|
||||
# Denotes whether to trim whitespace at the end of lines
|
||||
# Possible values - true, false
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Denotes whether file should end with a newline
|
||||
# Possible values - true, false
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
|
||||
|
||||
# Created by https://www.gitignore.io/api/node,sublimetext
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
#logs
|
||||
logs/*.json
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
|
||||
### SublimeText ###
|
||||
# cache files for sublime text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using SublimeText
|
||||
*.sublime-project
|
||||
|
||||
# sftp configuration file
|
||||
sftp-config.json
|
||||
|
||||
#Documentation
|
||||
docs
|
||||
|
||||
.nyc_output
|
||||
coverage
|
||||
database/
|
||||
system-user-*.json
|
||||
orbitdb
|
||||
ipfsdata
|
||||
ipfs-service-provider.sh
|
||||
|
||||
!README.md
|
||||
@@ -0,0 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021 Permissionless Software Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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:
|
||||
|
||||
- [Koa](https://koajs.com/) framework for REST APIs
|
||||
- User management
|
||||
- Access and rate-limit control using [JWT tokens](https://jwt.io/).
|
||||
|
||||
This boilerplate extends that code to provide the basic features required to be a 'service provider' on the [IPFS](https://ipfs.io) network. See [this article](https://troutsblog.com/blog/ipfs-api) if you're new to the concept of service providers on IPFS. These basic features include:
|
||||
|
||||
- [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.
|
||||
|
||||
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](https://github.com/standard/standard)
|
||||
- Packaged for production environment as a Docker container
|
||||
- [ipfs-coord](https://www.npmjs.com/package/ipfs-coord) for coordinating peers over IPFS
|
||||
- JSON RPC for mirroring the REST API over IPFS
|
||||
|
||||
## Requirements
|
||||
|
||||
- node **^14.17.0**
|
||||
- npm **^7.13.0**
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Permissionless-Software-Foundation/ipfs-service-provider
|
||||
cd ipfs-service-provider
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
The file layout of this repository differs from the koa-api-boilerplate. Instead, it follows the file layout of [Clean Architecture](troutsblog.com/blog/clean-architecture).
|
||||
|
||||
## Usage
|
||||
|
||||
- `npm start` Start server on live mode
|
||||
- `npm run docs` Generate API documentation
|
||||
- `npm test` Run mocha tests
|
||||
- `docker-compose build` Build a 'production' Docker container
|
||||
- `docker-compose up` Run the docker container
|
||||
|
||||
## Documentation
|
||||
|
||||
API documentation is written inline and generated by [apidoc](http://apidocjs.com/).
|
||||
|
||||
Visit `http://localhost:5000/docs/` to view docs
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [koa2](https://github.com/koajs/koa/tree/v2.x)
|
||||
- [koa-router](https://github.com/alexmingoia/koa-router)
|
||||
- [koa-bodyparser](https://github.com/koajs/bodyparser)
|
||||
- [koa-generic-session](https://github.com/koajs/generic-session)
|
||||
- [koa-logger](https://github.com/koajs/logger)
|
||||
- [MongoDB](http://mongodb.org/)
|
||||
- [Mongoose](http://mongoosejs.com/)
|
||||
- [Passport](http://passportjs.org/)
|
||||
- [Nodemon](http://nodemon.io/)
|
||||
- [Mocha](https://mochajs.org/)
|
||||
- [apidoc](http://apidocjs.com/)
|
||||
- [ESLint](http://eslint.org/)
|
||||
- [ipfs-coord](https://www.npmjs.com/package/ipfs-coord)
|
||||
|
||||
## IPFS
|
||||
|
||||
Snapshots pinned to IPFS will be listed here.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE.md)
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sampleUrl": null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// npm libraries
|
||||
const Koa = require('koa')
|
||||
const bodyParser = require('koa-bodyparser')
|
||||
const convert = require('koa-convert')
|
||||
const logger = require('koa-logger')
|
||||
const mongoose = require('mongoose')
|
||||
const session = require('koa-generic-session')
|
||||
const passport = require('koa-passport')
|
||||
const mount = require('koa-mount')
|
||||
const serve = require('koa-static')
|
||||
const cors = require('kcors')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../config') // this first.
|
||||
// const IPFSLib = require('../src/lib/ipfs')
|
||||
const AdminLib = require('../src/adapters/admin')
|
||||
const adminLib = new AdminLib()
|
||||
// const JSONRPC = require('../src/rpc')
|
||||
// const rpc = new JSONRPC()
|
||||
|
||||
const errorMiddleware = require('../src/controllers/rest-api/middleware/error')
|
||||
const { wlogger } = require('../src/adapters/wlogger')
|
||||
|
||||
async function startServer () {
|
||||
// Create a Koa instance.
|
||||
const app = new Koa()
|
||||
app.keys = [config.session]
|
||||
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
// MIDDLEWARE START
|
||||
|
||||
app.use(convert(logger()))
|
||||
app.use(bodyParser())
|
||||
app.use(session())
|
||||
app.use(errorMiddleware())
|
||||
|
||||
// Used to generate the docs.
|
||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||
|
||||
// Mount the page for displaying logs.
|
||||
app.use(mount('/logs', serve(`${process.cwd()}/config/logs`)))
|
||||
|
||||
// User Authentication
|
||||
require('../config/passport')
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
|
||||
// Attach REST API and JSON RPC controllers to the app.
|
||||
const controllers = require('../src/controllers')
|
||||
controllers.attachControllers(app)
|
||||
|
||||
// Enable CORS for testing
|
||||
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
|
||||
app.use(cors({ origin: '*' }))
|
||||
|
||||
// MIDDLEWARE END
|
||||
|
||||
console.log(`Running server in environment: ${config.env}`)
|
||||
wlogger.info(`Running server in environment: ${config.env}`)
|
||||
|
||||
await app.listen(config.port)
|
||||
console.log(`Server started on ${config.port}`)
|
||||
|
||||
// Create the system admin user.
|
||||
const success = await adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
|
||||
return app
|
||||
}
|
||||
// startServer()
|
||||
|
||||
// export default app
|
||||
// module.exports = app
|
||||
module.exports = {
|
||||
startServer
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
This file should be customized for your app. The string in this file is
|
||||
displayed when a user makes the 'about' JSON RPC call.
|
||||
*/
|
||||
|
||||
module.exports = `
|
||||
This is the about string for the ipfs-service-provider repository:
|
||||
https://github.com/Permissionless-Software-Foundation/ipfs-service-provider
|
||||
|
||||
Brought to you by the Permissionless Software Foundation:
|
||||
https://PSFoundation.cash
|
||||
|
||||
Documentation:
|
||||
https://ipfs-service-provider.fullstack.cash/
|
||||
`
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
This file is used to store unsecure, application-specific data common to all
|
||||
environments.
|
||||
*/
|
||||
|
||||
/* eslint no-unneeded-ternary:0 */
|
||||
|
||||
module.exports = {
|
||||
// Configure TCP port.
|
||||
port: process.env.PORT || 5001,
|
||||
|
||||
// Password for HTML UI that displays logs.
|
||||
logPass: 'test',
|
||||
|
||||
// Email server settings if nodemailer email notifications are used.
|
||||
emailServer: process.env.EMAILSERVER ? process.env.EMAILSERVER : 'mail.someserver.com',
|
||||
emailUser: process.env.EMAILUSER ? process.env.EMAILUSER : 'noreply@someserver.com',
|
||||
emailPassword: process.env.EMAILPASS ? process.env.EMAILPASS : 'emailpassword',
|
||||
|
||||
// IPFS settings.
|
||||
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
|
||||
|
||||
// Information passed to other IPFS peers about this node.
|
||||
apiInfo: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
|
||||
// JSON-LD and Schema.org schema with info about this app.
|
||||
announceJsonLd: {
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'WebAPI',
|
||||
name: 'ipfs-service-provider',
|
||||
description: 'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
|
||||
documentation: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: 'Permissionless Software Foundation',
|
||||
url: 'https://PSFoundation.cash'
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
These are the environment settings for the DEVELOPMENT environment.
|
||||
This is the environment run by default with `npm start` if KOA_ENV is not
|
||||
specified.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/ipfs-service-dev',
|
||||
env: 'dev'
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
These are the environment settings for the PRODUCTION environment.
|
||||
This is the environment run with `npm start` if KOA_ENV=production.
|
||||
This is the environment run inside the Docker container.
|
||||
|
||||
It is assumed the MonogDB Docker container is accessed by port 5555
|
||||
so as not to conflict with the default host port of 27017 for MongoDB.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
These are the environment settings for the TEST environment.
|
||||
This is the environment run with `npm start` if KOA_ENV=test.
|
||||
This is the environment run by the test suite.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/ipfs-service-test',
|
||||
env: 'test'
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const common = require('./env/common')
|
||||
|
||||
const env = process.env.SVC_ENV || 'development'
|
||||
const config = require(`./env/${env}`)
|
||||
|
||||
module.exports = Object.assign({}, common, config)
|
||||
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Logs</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet" media="screen" />
|
||||
<link href="/vendor/prettify.css" rel="stylesheet" media="screen" />
|
||||
<link href="/css/style.css" rel="stylesheet" media="screen, print" />
|
||||
<!-- <link href="img/favicon.ico" rel="icon" type="image/x-icon"> -->
|
||||
<script src="/vendor/polyfill.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<br />
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<p id="outMsg"><p>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Password for accessing logs -->
|
||||
<div class="row loginForm">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="inputLogPass" class="col-sm-2 control-label"
|
||||
>Password</label
|
||||
>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="inputLogPass"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
onclick="viewLogs()"
|
||||
>
|
||||
View Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row logTable" style="visibility: hidden;">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<tr class="tableTemplate">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
<script src="/vendor/bootstrap.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function viewLogs() {
|
||||
try {
|
||||
const pass = $('#inputLogPass').val()
|
||||
|
||||
// if (pass === 'test') {
|
||||
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: pass
|
||||
})
|
||||
}
|
||||
const data = await fetch(`/logapi`, options)
|
||||
// console.log(`data.status: `, data.status)
|
||||
|
||||
if (data.status > 399) {
|
||||
$('#outMsg').text('Could not communicate with log server.')
|
||||
throw new Error(`Could not get log data`)
|
||||
}
|
||||
|
||||
const data2 = await data.json()
|
||||
// console.log(`data2: ${JSON.stringify(data2, null, 2)}`)
|
||||
|
||||
if (!data2.success) {
|
||||
$('#outMsg').text('Incorrect password')
|
||||
throw new Error(`Incorrect password`)
|
||||
} else {
|
||||
$('#outMsg').text('')
|
||||
}
|
||||
|
||||
$('.loginForm').css('visibility', 'hidden')
|
||||
$('.logTable').css('visibility', 'visible')
|
||||
|
||||
const logData = data2.data
|
||||
// console.log(`logData: ${JSON.stringify(logData, null, 2)}`)
|
||||
|
||||
// Clone the template row.
|
||||
const template = $('.tableTemplate')
|
||||
// debugger
|
||||
|
||||
// Loop through the array of log data.
|
||||
for (let i = 0; i < logData.length; i++) {
|
||||
const thisRow = template.clone()
|
||||
const cols = thisRow.find('td')
|
||||
|
||||
const time = new Date(logData[i].timestamp)
|
||||
// debugger
|
||||
|
||||
cols.first().text(time.toLocaleString())
|
||||
cols.next().text(logData[i].level)
|
||||
cols.next().next().text(logData[i].message)
|
||||
// debugger
|
||||
|
||||
$('.table').append(thisRow)
|
||||
}
|
||||
|
||||
|
||||
// } else {
|
||||
// console.log(`password fail`)
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error(`Error in viewLogs: `, err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
const passport = require('koa-passport')
|
||||
const User = require('../src/adapters/localdb/models/users')
|
||||
const Strategy = require('passport-local')
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id)
|
||||
})
|
||||
|
||||
passport.deserializeUser(async (id, done) => {
|
||||
try {
|
||||
const user = await User.findById(id, '-password')
|
||||
done(null, user)
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
})
|
||||
|
||||
passport.use(
|
||||
'local',
|
||||
new Strategy(
|
||||
{
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
},
|
||||
passportCallback
|
||||
)
|
||||
)
|
||||
|
||||
async function passportCallback (email, password, done) {
|
||||
try {
|
||||
const user = await User.findOne({ email })
|
||||
if (!user) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
try {
|
||||
const isMatch = await user.validatePassword(password)
|
||||
|
||||
if (!isMatch) {
|
||||
return done(null, false)
|
||||
}
|
||||
|
||||
done(null, user)
|
||||
} catch (err) {
|
||||
done(err)
|
||||
}
|
||||
} catch (err) {
|
||||
return done(err)
|
||||
}
|
||||
}
|
||||
|
||||
// For testing
|
||||
module.exports = { passport, passportCallback }
|
||||
@@ -0,0 +1,25 @@
|
||||
# Start the testnet server with the command 'docker-compose up -d'
|
||||
|
||||
koa-mongodb:
|
||||
image: mongo
|
||||
container_name: mongo-koa
|
||||
ports:
|
||||
- "5555:27017" # <host port>:<container port>
|
||||
volumes:
|
||||
- ./database:/data/db
|
||||
command: mongod --smallfiles --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
koa:
|
||||
build: ./production/
|
||||
dockerfile: Dockerfile
|
||||
container_name: koa
|
||||
links:
|
||||
- koa-mongodb
|
||||
ports:
|
||||
- "5001:5001" # <host port>:<container port>
|
||||
volumes:
|
||||
# - ./logs:/home/coinjoin/consolidating-coinjoin/logs
|
||||
- ./keys:/home/safeuser/keys
|
||||
|
||||
restart: always
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"556","method":"auth","params":{ "endpoint": "authUser", "login": "test555@test.com", "password": "password"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getAllUsers", "apiToken": "<JWT>"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "updateUser", "apiToken": "<JWT>", "userId": "<_id>", "name": "test999"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getUser", "apiToken": "<JWT>", "userId": "<_id>"}}`<br />
|
||||
|
||||
- `{"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "deleteUser", "userId": "<_id>", "apiToken": "<JWT>"}}`
|
||||
@@ -0,0 +1,3 @@
|
||||
const server = require('./bin/server.js')
|
||||
|
||||
server.startServer()
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -
|
||||
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mongodb-org
|
||||
sudo service mongod start
|
||||
sudo systemctl enable mongod
|
||||
@@ -0,0 +1 @@
|
||||
This directory will hold the Winston daily logs. Any files saved to this directory will be ignored by Git.
|
||||
Generated
+48098
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"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 SVC_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||
"test:unit": "export SVC_ENV=test && mocha --exit --timeout 15000 --recursive test/unit/",
|
||||
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
|
||||
"test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export SVC_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 --recursive test/unit/ test/e2e/automated/"
|
||||
},
|
||||
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
||||
"license": "MIT",
|
||||
"apidoc": {
|
||||
"title": "ipfs-service-provider",
|
||||
"url": "localhost:5000"
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/ipfs-service-provider",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "^4.20.1",
|
||||
"axios": "^0.21.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"glob": "^7.1.6",
|
||||
"ipfs": "^0.54.4",
|
||||
"ipfs-coord": "^3.2.0",
|
||||
"jsonrpc-lite": "^2.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"kcors": "^2.2.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"koa-convert": "^2.0.0",
|
||||
"koa-generic-session": "^2.1.1",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-mount": "^4.0.0",
|
||||
"koa-passport": "^4.1.3",
|
||||
"koa-router": "^10.0.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"koa2-ratelimit": "^0.9.0",
|
||||
"line-reader": "^0.4.0",
|
||||
"mongoose": "^5.11.15",
|
||||
"nodemailer": "^6.4.17",
|
||||
"passport-local": "^1.0.0",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "^0.26.0",
|
||||
"chai": "^4.3.0",
|
||||
"coveralls": "^3.1.0",
|
||||
"eslint": "7.19.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"husky": "^4.3.8",
|
||||
"mocha": "^8.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"semantic-release": "^17.4.4",
|
||||
"sinon": "^9.2.4",
|
||||
"standard": "^16.0.3",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"release": {
|
||||
"publish": [
|
||||
{
|
||||
"path": "@semantic-release/npm",
|
||||
"npmPublish": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"/test/unit/mocks/**/*.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Create a Dockerized API server
|
||||
#
|
||||
|
||||
#IMAGE BUILD COMMANDS
|
||||
# ct-base-ubuntu = ubuntu 18.04 + nodejs v10 LTS
|
||||
FROM christroutner/ct-base-ubuntu
|
||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||
|
||||
#Create the user 'safeuser' and add them to the sudo group.
|
||||
#RUN useradd -ms /bin/bash safeuser
|
||||
#RUN adduser safeuser sudo
|
||||
|
||||
#Set password to 'password' change value below if you want a different password
|
||||
#RUN echo safeuser:password | chpasswd
|
||||
|
||||
#Set the working directory to be the home directory
|
||||
WORKDIR /home/safeuser
|
||||
|
||||
#Setup NPM for non-root global install
|
||||
#RUN mkdir /home/safeuser/.npm-global
|
||||
#RUN chown -R safeuser .npm-global
|
||||
#RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
||||
#RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||
|
||||
# Switch to user account.
|
||||
USER safeuser
|
||||
# Prep 'sudo' commands.
|
||||
RUN echo 'abcd8765' | sudo -S pwd
|
||||
|
||||
# Clone the rest.bitcoin.com repository
|
||||
WORKDIR /home/safeuser
|
||||
RUN git clone https://github.com/christroutner/koa-api-boilerplate
|
||||
RUN mv koa-api-boilerplate koa
|
||||
RUN mkdir keys
|
||||
|
||||
# Switch to the desired branch. `master` is usually stable,
|
||||
# and `stage` has the most up-to-date changes.
|
||||
WORKDIR /home/safeuser/koa
|
||||
|
||||
# For development: switch to unstable branch
|
||||
# RUN git checkout unstable
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
EXPOSE 5001
|
||||
|
||||
# Start the application.
|
||||
COPY start-production start-production
|
||||
CMD ["./start-production"]
|
||||
|
||||
#CMD ["npm", "start"]
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
export KOA_ENV=production
|
||||
npm start
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
A library for working with the system admin user. This is an auto-generated
|
||||
account with 'admin' privledges, for interacting with private APIs.
|
||||
|
||||
The admin account is regenerated every time the server is started. This improves
|
||||
security by not having stale passwords for the account. The login information
|
||||
and JWT token for the admin account is written to a JSON file, for easy
|
||||
retrieval by other apps running on the server that may need admin privledges
|
||||
to access private APIs.
|
||||
|
||||
This library is really more of an Adapter to the internal systems default
|
||||
admin user. It's not really a central Entity, which is why this library lives
|
||||
in the Adapter directory.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
const axios = require('axios').default
|
||||
const mongoose = require('mongoose')
|
||||
const User = require('../adapters/localdb/models/users')
|
||||
const config = require('../../config')
|
||||
const JsonFiles = require('../adapters/json-files')
|
||||
const jsonFiles = new JsonFiles()
|
||||
|
||||
const JSON_FILE = `system-user-${config.env}.json`
|
||||
const JSON_PATH = `${__dirname.toString()}/../../config/${JSON_FILE}`
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
const context = {}
|
||||
|
||||
let _this
|
||||
class Admin {
|
||||
constructor () {
|
||||
this.axios = axios
|
||||
this.User = User
|
||||
this.config = config
|
||||
this.jsonFiles = jsonFiles
|
||||
this.context = context
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Create the first user in the system. A 'admin' level system user that is
|
||||
// used by the Listing Manager and test scripts, in order access private API
|
||||
// functions.
|
||||
async createSystemUser () {
|
||||
// Create the system user.
|
||||
try {
|
||||
context.password = _this._randomString(20)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'system@system.com',
|
||||
password: context.password,
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log('admin.data: ', result.data)
|
||||
|
||||
context.email = result.data.user.email
|
||||
context.id = result.data.user._id
|
||||
context.token = result.data.token
|
||||
|
||||
// Get the mongoDB entry
|
||||
const user = await _this.User.findById(context.id)
|
||||
|
||||
// Change the user type to admin
|
||||
user.type = 'admin'
|
||||
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Save the user model.
|
||||
await user.save()
|
||||
|
||||
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
|
||||
// console.log(`with password: ${context.password}`)
|
||||
|
||||
// Write out the system user information to a JSON file that external
|
||||
// applications like the Task Manager and the test scripts can access.
|
||||
|
||||
await jsonFiles.writeJSON(context, JSON_PATH)
|
||||
|
||||
return context
|
||||
} catch (err) {
|
||||
// Handle existing system user.
|
||||
if (err.response.status === 422) {
|
||||
try {
|
||||
// Delete the existing user
|
||||
await _this.deleteExistingSystemUser()
|
||||
|
||||
// Call this function again.
|
||||
return _this.createSystemUser()
|
||||
} catch (err2) {
|
||||
console.error(
|
||||
'Error in admin.js/createSystemUser() while trying generate new system user.'
|
||||
)
|
||||
// process.end(1)
|
||||
throw err2
|
||||
}
|
||||
} else {
|
||||
console.log('Error in admin.js/createSystemUser: ')
|
||||
// process.end(1)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteExistingSystemUser () {
|
||||
try {
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
|
||||
await mongoose.connect(config.database, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
await _this.User.deleteOne({ email: 'system@system.com' })
|
||||
} catch (err) {
|
||||
console.log('Error in admin.js/deleteExistingSystemUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async loginAdmin () {
|
||||
// console.log(`loginAdmin() running.`)
|
||||
let existingUser
|
||||
|
||||
try {
|
||||
// Read the exising file
|
||||
existingUser = await _this.jsonFiles.readJSON(JSON_PATH)
|
||||
// console.log(`existingUser: ${JSON.stringify(existingUser, null, 2)}`)
|
||||
|
||||
// Log in as the user.
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
data: {
|
||||
email: 'system@system.com',
|
||||
password: existingUser.password
|
||||
}
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log(`result1: ${JSON.stringify(result, null, 2)}`)
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('Error in admin.js/loginAdmin().')
|
||||
|
||||
// console.error(`existingUser: ${JSON.stringify(existingUser, null, 2)}`)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
_randomString (length) {
|
||||
let text = ''
|
||||
const possible =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
for (let i = 0; i < length; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Admin
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Business logic for the /contact endpoint.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-useless-escape */
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const NodeMailer = require('../adapters/nodemailer')
|
||||
const nodemailer = new NodeMailer()
|
||||
const { wlogger } = require('../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class ContactLib {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.config = config
|
||||
_this.nodemailer = nodemailer
|
||||
}
|
||||
|
||||
async sendEmail (emailObj) {
|
||||
try {
|
||||
// Validate input
|
||||
if (!emailObj.email || typeof emailObj.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
|
||||
if (!emailObj.formMessage || typeof emailObj.formMessage !== 'string') {
|
||||
throw new Error("Property 'formMessage' must be a string!")
|
||||
}
|
||||
|
||||
// If an email list exists, the email will be sended to that list
|
||||
// otherwhise will be sended by default to the variable "_this.config.emailUser"
|
||||
let _to = [_this.config.emailUser]
|
||||
|
||||
// Email list is optional
|
||||
if (emailObj.emailList) {
|
||||
if (
|
||||
!Array.isArray(emailObj.emailList) ||
|
||||
!emailObj.emailList.length > 0
|
||||
) {
|
||||
throw new Error("Property 'emailList' must be a array of emails!")
|
||||
} else {
|
||||
_to = emailObj.emailList
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Trying send message to : ${_to}`)
|
||||
|
||||
emailObj.subject = 'Someone wants contact with you.'
|
||||
emailObj.to = _to
|
||||
|
||||
const result = await _this.nodemailer.sendEmail(emailObj)
|
||||
return result
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/contact.js/sendEmail()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = ContactLib
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
This is a top-level library that encapsulates all the additional Adapters.
|
||||
The concept of Adapters comes from Clean Architecture:
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Load individual adapter libraries.
|
||||
const IPFSAdapter = require('./ipfs')
|
||||
const LocalDB = require('./localdb')
|
||||
const LogsAPI = require('./logapi')
|
||||
const Passport = require('./passport')
|
||||
const Nodemailer = require('./nodemailer')
|
||||
const { wlogger } = require('./wlogger')
|
||||
const JSONFiles = require('./json-files')
|
||||
|
||||
// Instantiate adapter libraries.
|
||||
const ipfs = new IPFSAdapter()
|
||||
const localdb = new LocalDB()
|
||||
const logapi = new LogsAPI()
|
||||
const passport = new Passport()
|
||||
const nodemailer = new Nodemailer()
|
||||
const jsonFiles = new JSONFiles()
|
||||
|
||||
module.exports = {
|
||||
ipfs,
|
||||
localdb,
|
||||
logapi,
|
||||
passport,
|
||||
nodemailer,
|
||||
wlogger,
|
||||
jsonFiles
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
top-level IPFS library that combines the individual IPFS-based libraries.
|
||||
*/
|
||||
|
||||
const IpfsAdapter = require('./ipfs')
|
||||
const IpfsCoordAdapter = require('./ipfs-coord')
|
||||
|
||||
class IPFS {
|
||||
constructor (localConfig = {}) {
|
||||
// Encapsulate dependencies
|
||||
this.ipfsAdapter = new IpfsAdapter()
|
||||
this.IpfsCoordAdapter = IpfsCoordAdapter
|
||||
|
||||
this.ipfsCoordAdapter = {} // placeholder
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
}
|
||||
|
||||
// Provides a global start() function that triggers the start() function in
|
||||
// the underlying libraries.
|
||||
async start () {
|
||||
try {
|
||||
// Start IPFS
|
||||
await this.ipfsAdapter.start()
|
||||
console.log('IPFS is ready.')
|
||||
|
||||
// this.ipfs is a Promise that will resolve into an instance of an IPFS node.
|
||||
this.ipfs = this.ipfsAdapter.ipfs
|
||||
|
||||
// Start ipfs-coord
|
||||
this.ipfsCoordAdapter = new this.IpfsCoordAdapter({
|
||||
ipfs: this.ipfs
|
||||
})
|
||||
await this.ipfsCoordAdapter.start()
|
||||
console.log('ipfs-coord is ready.')
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Error in adapters/ipfs/index.js/start()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IPFS
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Clean Architecture Adapter for ipfs-coord.
|
||||
This library deals with ipfs-coord library so that the apps business logic
|
||||
doesn't need to have any specific knowledge of the library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
const IpfsCoord = require('ipfs-coord')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../config')
|
||||
// const JSONRPC = require('../../controllers/json-rpc/')
|
||||
|
||||
let _this
|
||||
|
||||
class IpfsCoordAdapter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency injection.
|
||||
this.ipfs = localConfig.ipfs
|
||||
if (!this.ipfs) {
|
||||
throw new Error(
|
||||
'Instance of IPFS must be passed when instantiating ipfs-coord.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.IpfsCoord = IpfsCoord
|
||||
this.ipfsCoord = {}
|
||||
this.bchjs = new BCHJS()
|
||||
// this.rpc = new JSONRPC()
|
||||
this.config = config
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
async start () {
|
||||
this.ipfsCoord = new this.IpfsCoord({
|
||||
ipfs: this.ipfs,
|
||||
type: 'node.js',
|
||||
// type: 'browser',
|
||||
bchjs: this.bchjs,
|
||||
privateLog: console.log, // Default to console.log
|
||||
isCircuitRelay: this.config.isCircuitRelay,
|
||||
apiInfo: this.config.apiInfo,
|
||||
announceJsonLd: this.config.announceJsonLd
|
||||
})
|
||||
|
||||
// Wait for the ipfs-coord library to signal that it is ready.
|
||||
await this.ipfsCoord.isReady()
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
|
||||
return this.isReady
|
||||
}
|
||||
|
||||
// Expects router to be a function, which handles the input data from the
|
||||
// pubsub channel. It's expected to be capable of routing JSON RPC commands.
|
||||
attachRPCRouter (router) {
|
||||
try {
|
||||
_this.ipfsCoord.privateLog = router
|
||||
_this.ipfsCoord.ipfs.orbitdb.privateLog = router
|
||||
} catch (err) {
|
||||
console.error('Error in attachRPCRouter()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IpfsCoordAdapter
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Clean Architecture Adapter for IPFS.
|
||||
This library deals with IPFS so that the apps business logic doesn't need
|
||||
to have any specific knowledge of the js-ipfs library.
|
||||
*/
|
||||
|
||||
// Global npm libraries
|
||||
const IPFS = require('ipfs')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../config')
|
||||
|
||||
class IpfsAdapter {
|
||||
constructor (localConfig) {
|
||||
// Encapsulate dependencies
|
||||
this.IPFS = IPFS
|
||||
|
||||
// Properties of this class instance.
|
||||
this.isReady = false
|
||||
this.config = config
|
||||
}
|
||||
|
||||
// Start an IPFS node.
|
||||
async start () {
|
||||
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
|
||||
}
|
||||
},
|
||||
Addresses: {
|
||||
Swarm: [
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
|
||||
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)}`
|
||||
// )
|
||||
|
||||
// Stop the IPFS node if we're running tests.
|
||||
if (this.config.env === 'test') {
|
||||
await this.ipfs.stop()
|
||||
}
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
|
||||
return this.ipfs
|
||||
} catch (err) {
|
||||
console.error('Error in ipfs.js/start()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IpfsAdapter
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
A utility file for reading and writing JSON files.
|
||||
*/
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
|
||||
let _this
|
||||
|
||||
class JsonFiles {
|
||||
constructor () {
|
||||
this.fs = fs
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Writes out a JSON file of any object passed to the function.
|
||||
// This is used for testing.
|
||||
writeJSON (obj, fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
if (!obj) {
|
||||
throw new Error('obj property is required')
|
||||
}
|
||||
if (!fileName || typeof fileName !== 'string') {
|
||||
throw new Error('fileName property must be a string')
|
||||
}
|
||||
const fileStr = JSON.stringify(obj, null, 2)
|
||||
|
||||
_this.fs.writeFile(fileName, fileStr, function (err) {
|
||||
if (err) {
|
||||
console.error('Error while trying to write file: ')
|
||||
throw err
|
||||
} else {
|
||||
// console.log(`${fileName} written successfully!`)
|
||||
return resolve()
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error trying to write out object in util.js/_writeJSON().')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
readJSON (fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
if (!fileName || typeof fileName !== 'string') {
|
||||
throw new Error('fileName property must be a string')
|
||||
}
|
||||
|
||||
_this.fs.readFile(fileName, (err, data) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log('Admin .json file not found!')
|
||||
} else {
|
||||
console.log(`err: ${JSON.stringify(err, null, 2)}`)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
const obj = JSON.parse(data)
|
||||
|
||||
return resolve(obj)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error trying to read JSON file in util.js/_readJSON().')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JsonFiles
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
This library encapsulates code concerned with MongoDB and Mongoose models.
|
||||
*/
|
||||
|
||||
// Load Mongoose models.
|
||||
const Users = require('./models/users')
|
||||
|
||||
class LocalDB {
|
||||
constructor () {
|
||||
// Encapsulate dependencies
|
||||
this.Users = Users
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalDB
|
||||
@@ -0,0 +1,54 @@
|
||||
const mongoose = require('mongoose')
|
||||
const bcrypt = require('bcryptjs')
|
||||
const config = require('../../../../config')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
const User = new mongoose.Schema({
|
||||
type: { type: String, default: 'user' },
|
||||
name: { type: String },
|
||||
username: { type: String },
|
||||
password: { type: String, required: true },
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
}
|
||||
})
|
||||
|
||||
// Before saving, convert the password to a hash.
|
||||
User.pre('save', async function preSave (next) {
|
||||
const user = this
|
||||
|
||||
if (!user.isModified('password')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const salt = await bcrypt.genSalt(10)
|
||||
const hash = await bcrypt.hash(user.password, salt)
|
||||
|
||||
user.password = hash
|
||||
|
||||
next(null)
|
||||
})
|
||||
|
||||
// Validate the password by comparing to the saved hash.
|
||||
User.methods.validatePassword = async function validatePassword (password) {
|
||||
const user = this
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password)
|
||||
|
||||
return isMatch
|
||||
}
|
||||
|
||||
// Generate a JWT token.
|
||||
User.methods.generateToken = function generateToken () {
|
||||
const user = this
|
||||
|
||||
const token = jwt.sign({ id: user.id }, config.token)
|
||||
// console.log(`config.token: ${config.token}`)
|
||||
// console.log(`generated token: ${token}`)
|
||||
return token
|
||||
}
|
||||
|
||||
// export default mongoose.model('user', User)
|
||||
module.exports = mongoose.model('user', User)
|
||||
@@ -0,0 +1,170 @@
|
||||
const lineReader = require('line-reader')
|
||||
const fs = require('fs')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
let _this
|
||||
|
||||
class LogsApi {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.fs = fs
|
||||
_this.lineReader = lineReader
|
||||
_this.config = config
|
||||
}
|
||||
|
||||
async getLogs (password) {
|
||||
try {
|
||||
// console.log('entering getLogs()')
|
||||
_this.password = password
|
||||
// console.log(`password: ${password}`)
|
||||
|
||||
// Password matches the password set in the config file.
|
||||
if (password === _this.config.logPass) {
|
||||
// Generate the full path and file name for the current log file.
|
||||
const fullPath = _this.generateFileName()
|
||||
// console.log(`fullPath: ${JSON.stringify(fullPath, null, 2)}`)
|
||||
|
||||
// Throw an error if the file does not exist.
|
||||
if (!_this.fs.existsSync(fullPath)) {
|
||||
return {
|
||||
success: false,
|
||||
data: 'file does not exist'
|
||||
}
|
||||
} else {
|
||||
// Read in the data from the log file.
|
||||
const data = await _this.readLines(fullPath)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
// Filter the logs before passing them to the front end.
|
||||
const filteredData = _this.filterLogs(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: filteredData
|
||||
}
|
||||
}
|
||||
|
||||
// Password does not match password in config file.
|
||||
} else {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in lib/logapi.js/getLogs()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Sorts the log data by their timestamp. Returns the LIMIT or less elements.
|
||||
filterLogs (data, LIMIT = 100) {
|
||||
try {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Data must be array')
|
||||
}
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
// const LIMIT = 100 // Max number of entries to return.
|
||||
|
||||
// Sort the elements by date.
|
||||
data.sort(function (a, b) {
|
||||
let dateA = new Date(a.timestamp)
|
||||
dateA = dateA.getTime()
|
||||
|
||||
let dateB = new Date(b.timestamp)
|
||||
dateB = dateB.getTime()
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
|
||||
// Limit the number of elements.
|
||||
if (data.length > LIMIT) {
|
||||
return data.slice(0, LIMIT)
|
||||
}
|
||||
|
||||
// else
|
||||
return data
|
||||
} catch (err) {
|
||||
console.error('Error in lib/logapi.js/filterLogs()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
generateFileName () {
|
||||
try {
|
||||
const now = new Date()
|
||||
let thisDate = now.getDate()
|
||||
thisDate = ('0' + thisDate).slice(-2)
|
||||
|
||||
let thisMonth = now.getMonth() + 1
|
||||
thisMonth = ('0' + thisMonth).slice(-2)
|
||||
// console.log(`thisMonth: ${thisMonth}`)
|
||||
|
||||
const thisYear = now.getFullYear()
|
||||
|
||||
const filename = `koa-${
|
||||
_this.config.env
|
||||
}-${thisYear}-${thisMonth}-${thisDate}.log`
|
||||
// console.log(`filename: ${filename}`)
|
||||
const logDir = `${__dirname.toString()}/../../logs/`
|
||||
const fullPath = `${logDir}${filename}`
|
||||
// console.log(`fullPath: ${fullPath}`)
|
||||
|
||||
return fullPath
|
||||
} catch (err) {
|
||||
console.error('Error in lib/logapi.js/generateFileName()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Promise based read-file
|
||||
/* readFile (path, opts = 'utf8') {
|
||||
return new Promise((resolve, reject) => {
|
||||
_this.fs.readFile(path, opts, (err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(data)
|
||||
})
|
||||
})
|
||||
} */
|
||||
|
||||
// Returns an array with each element containing a line of the file.
|
||||
readLines (filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!filename || typeof filename !== 'string') {
|
||||
throw new Error('filename must be a string')
|
||||
}
|
||||
// Throw an error if the file does not exist.
|
||||
|
||||
if (!_this.fs.existsSync(filename)) {
|
||||
throw new Error('file does not exist')
|
||||
}
|
||||
|
||||
const data = []
|
||||
|
||||
// let i = 0
|
||||
|
||||
_this.lineReader.eachLine(filename, function (line, last) {
|
||||
try {
|
||||
data.push(JSON.parse(line))
|
||||
|
||||
// Uncomment to display the raw data in each line of the winston log file.
|
||||
// console.log(`line ${i}: ${line}`)
|
||||
// i++
|
||||
|
||||
if (last) return resolve(data)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
if (last) return resolve(data)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('Error in lib/logapi.js/readLines()')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsApi
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
A library for controlling the sending of email.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
const nodemailer = require('nodemailer')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const { wlogger } = require('./wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class NodeMailer {
|
||||
constructor () {
|
||||
this.nodemailer = nodemailer
|
||||
this.config = config
|
||||
|
||||
_this = this
|
||||
_this.transporter = _this.createTransporter()
|
||||
}
|
||||
|
||||
// Define an email server 'transport' for nodemailer
|
||||
createTransporter () {
|
||||
const transporter = _this.nodemailer.createTransport({
|
||||
host: _this.config.emailServer,
|
||||
port: 587,
|
||||
secure: false, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: _this.config.emailUser, // generated ethereal user
|
||||
pass: _this.config.emailPassword // generated ethereal password
|
||||
}
|
||||
})
|
||||
return transporter
|
||||
}
|
||||
|
||||
// Handles the sending of data via email.
|
||||
async sendEmail (data) {
|
||||
try {
|
||||
// Validate input
|
||||
if (!data.email || typeof data.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
|
||||
if (!data.to || !Array.isArray(data.to)) {
|
||||
throw new Error("Property 'to' must be a array!")
|
||||
}
|
||||
|
||||
await _this.validateEmailArray(data.to)
|
||||
|
||||
if (!data.formMessage || typeof data.formMessage !== 'string') {
|
||||
throw new Error("Property 'formMessage' must be a string!")
|
||||
}
|
||||
|
||||
if (!data.subject || typeof data.subject !== 'string') {
|
||||
throw new Error("Property 'subject' must be a string!")
|
||||
}
|
||||
|
||||
// Use the provided html or use a default html generated from the input data
|
||||
|
||||
const html = data.htmlData || _this.getHtmlFromObject(data)
|
||||
const sendObj = {
|
||||
// from: `${data.email}`, // sender address
|
||||
from: data.email,
|
||||
to: data.to, // list of receivers
|
||||
// subject: `Pearson ${subject}`, // Subject line
|
||||
subject: data.subject,
|
||||
// html: '<b>This is a test email</b>' // html body
|
||||
html
|
||||
}
|
||||
|
||||
// send mail with defined transport object
|
||||
const info = await _this.transporter.sendMail(sendObj)
|
||||
console.log('Message sent: %s', info.messageId)
|
||||
|
||||
return info
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/nodemailer.js/sendEmail()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async validateEmailArray (emailList) {
|
||||
try {
|
||||
if (!emailList || !Array.isArray(emailList)) {
|
||||
throw new Error("Property 'emailList' must be a array!")
|
||||
}
|
||||
// Email list can't be empty
|
||||
if (!emailList.length > 0) {
|
||||
throw new Error("Property 'emailList' cant be empty!")
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/nodemailer.js/validateEmailArray()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// get the email html from object
|
||||
getHtmlFromObject (objectData) {
|
||||
try {
|
||||
if (!objectData || typeof objectData !== 'object') {
|
||||
throw new Error("Property 'objectData' must be a object!")
|
||||
}
|
||||
if (!objectData.subject) {
|
||||
throw new Error("Property 'subject' must be a string!")
|
||||
}
|
||||
if (!objectData.formMessage) {
|
||||
throw new Error("Property 'formMessage' must be a string!")
|
||||
}
|
||||
|
||||
const obj = {}
|
||||
Object.assign(obj, objectData)
|
||||
|
||||
// neccesary data
|
||||
const msg = obj.formMessage.replace(/(\r\n|\n|\r)/g, '<br />')
|
||||
const now = new Date()
|
||||
const subject = obj.subject
|
||||
|
||||
// Delete unneccesary data if it exist
|
||||
delete obj.to
|
||||
delete obj.subject
|
||||
delete obj.from
|
||||
delete obj.emailList
|
||||
delete obj.formMessage
|
||||
|
||||
const bodyJson = obj
|
||||
bodyJson.message = msg
|
||||
|
||||
// Html body
|
||||
let htmlBody = ''
|
||||
|
||||
// maps the object and converts it into html format
|
||||
Object.keys(bodyJson).forEach(function (key) {
|
||||
htmlBody += `${key}: ${bodyJson[key]}<br/>`
|
||||
})
|
||||
|
||||
const defaultHtmlData = `<h3>${subject}:</h3>
|
||||
<p>
|
||||
time: ${now.toLocaleString()}<br/>
|
||||
${htmlBody}
|
||||
</p>`
|
||||
|
||||
return defaultHtmlData
|
||||
} catch (error) {
|
||||
wlogger.error('Error in lib/nodemailer.js/getHtmlFromObject()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NodeMailer
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
koa-passport is an authorization library used for different authentication schemes.
|
||||
*/
|
||||
|
||||
const passport = require('koa-passport')
|
||||
|
||||
let _this
|
||||
class Passport {
|
||||
constructor () {
|
||||
_this = this
|
||||
this.passport = passport
|
||||
}
|
||||
|
||||
async authUser (ctx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!ctx) throw new Error('ctx is required')
|
||||
|
||||
_this.passport.authenticate('local', (err, user) => {
|
||||
try {
|
||||
if (err) throw err
|
||||
|
||||
resolve(user)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})(ctx, null)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Passport
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Instantiates and configures the Winston logging library. This utitlity library
|
||||
can be called by other parts of the application to conveniently tap into the
|
||||
logging library.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const winston = require('winston')
|
||||
require('winston-daily-rotate-file')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
// Configure daily-rotation transport.
|
||||
const transport = new winston.transports.DailyRotateFile({
|
||||
filename: `${__dirname.toString()}/../../logs/koa-${config.env}-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxSize: '1m', // 1 megabyte
|
||||
maxFiles: '5d', // 5 days
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
)
|
||||
})
|
||||
|
||||
transport.on('rotate', notifyRotation)
|
||||
|
||||
function notifyRotation (oldFilename, newFilename) {
|
||||
wlogger.info('Rotating log files')
|
||||
}
|
||||
|
||||
// This controls what goes into the log FILES
|
||||
const wlogger = winston.createLogger({
|
||||
level: 'verbose',
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `combined.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
// new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
// new winston.transports.File({ filename: 'logs/combined.log' })
|
||||
transport
|
||||
]
|
||||
})
|
||||
|
||||
function outputToConsole () {
|
||||
wlogger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
level: 'info'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// This controls the logs to CONSOLE
|
||||
// if (config.env !== 'test') {
|
||||
// outputToConsole()
|
||||
// }
|
||||
|
||||
module.exports = { wlogger, notifyRotation, outputToConsole }
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
This is a top-level library that encapsulates all the additional Controllers.
|
||||
The concept of Controllers comes from Clean Architecture:
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
|
||||
// Load the Clean Architecture Adapters library
|
||||
const adapters = require('../adapters')
|
||||
|
||||
// Load the JSON RPC Controller.
|
||||
const JSONRPC = require('./json-rpc')
|
||||
|
||||
// Load the Clean Architecture Use Case libraries.
|
||||
const UseCases = require('../use-cases')
|
||||
const useCases = new UseCases({ adapters })
|
||||
|
||||
// Load the REST API Controllers.
|
||||
const RESTControllers = require('./rest-api')
|
||||
|
||||
// Top-level function for this library.
|
||||
// Start the various Controllers and attach them to the app.
|
||||
async function attachControllers (app) {
|
||||
// Attach the REST controllers to the Koa app.
|
||||
attachRESTControllers(app)
|
||||
|
||||
// Start IPFS.
|
||||
await adapters.ipfs.start()
|
||||
|
||||
attachRPCControllers()
|
||||
}
|
||||
|
||||
function attachRESTControllers (app) {
|
||||
const rESTControllers = new RESTControllers({
|
||||
adapters,
|
||||
useCases
|
||||
})
|
||||
|
||||
// Attach the REST API Controllers associated with the boilerplate code to the Koa app.
|
||||
rESTControllers.attachRESTControllers(app)
|
||||
}
|
||||
|
||||
// Add the JSON RPC router to the ipfs-coord adapter.
|
||||
function attachRPCControllers () {
|
||||
const jsonRpcController = new JSONRPC({ adapters, useCases })
|
||||
|
||||
// Attach the input of the JSON RPC router to the output of ipfs-coord.
|
||||
adapters.ipfs.ipfsCoordAdapter.attachRPCRouter(jsonRpcController.router)
|
||||
}
|
||||
|
||||
module.exports = { attachControllers }
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
This is the JSON RPC router for the users API
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local libraries
|
||||
const aboutStr = require('../../../../config/about')
|
||||
|
||||
class AboutRPC {
|
||||
constructor (localConfig) {
|
||||
// Encapsulate dependencies
|
||||
this.jsonrpc = jsonrpc
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /about About IPFS Node
|
||||
* @apiPermission public
|
||||
* @apiName About
|
||||
* @apiGroup JSON About
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"about"}
|
||||
*
|
||||
* @apiDescription
|
||||
* This endpoint can be customized so that users can retrieve information about
|
||||
* your IPFS node and Service Provider application. This is a great place to
|
||||
* put a website URL, an IPFS hash, an other basic information.
|
||||
*/
|
||||
|
||||
// This is the top-level router for this library.
|
||||
// This is a bit different than other router libraries, because there is
|
||||
// only one response, which is a string about this node.
|
||||
async aboutRouter (rpcData) {
|
||||
return {
|
||||
success: true,
|
||||
status: 200,
|
||||
message: aboutStr,
|
||||
endpoint: 'about'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AboutRPC
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
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('../../../use-cases/user')
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
const RateLimit = require('../rate-limit')
|
||||
|
||||
class AuthRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Auth JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Auth JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
// this.authLib = new AuthLib()
|
||||
this.jsonrpc = jsonrpc
|
||||
this.userLib = this.useCases.user
|
||||
this.rateLimit = new RateLimit()
|
||||
}
|
||||
|
||||
// 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':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.authUser(rpcData)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in AuthRPC/authRouter()')
|
||||
// throw err
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: 500,
|
||||
message: err.message,
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /auth Get JWT Token
|
||||
* @apiPermission public
|
||||
* @apiName AuthUser
|
||||
* @apiGroup JSON Auth
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"556","method":"auth","params":{ "endpoint": "authUser", "login": "test555@test.com", "password": "password"}}
|
||||
*
|
||||
* @apiParam {String} login Email(required).
|
||||
* @apiParam {String} password Password(required).
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.email User email
|
||||
* @apiSuccess {String} token JWT.
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "556",
|
||||
* "result": {
|
||||
* "method": "auth",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "endpoint": "authUser",
|
||||
* "userId": "607de52d426f3d3148b3a467",
|
||||
* "userType": "user",
|
||||
* "userName": "testy tester",
|
||||
* "userEmail": "test555@test.com",
|
||||
* "apiToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwN2RlNTJkNDI2ZjNkMzE0OGIzYTQ2NyIsImlhdCI6MTYxODg2NTcwM30.acGe5ZiBAAcbOcPQDIhvc3z0KjnuYZd1Y5pJJJC9mJQ",
|
||||
* "status": 200,
|
||||
* "success": true,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "556",
|
||||
* "result": {
|
||||
* "method": "auth",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "User not found",
|
||||
* "endpoint": "authUser"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
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,123 @@
|
||||
/*
|
||||
This is the parent class library for the RPC controller.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local support libraries
|
||||
const { wlogger } = require('../../adapters/wlogger')
|
||||
const UserController = require('./users')
|
||||
const AuthController = require('./auth')
|
||||
const AboutController = require('./about')
|
||||
|
||||
let _this
|
||||
|
||||
class JSONRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating JSON RPC Controllers.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating JSON RPC Controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.ipfsCoord = this.adapters.ipfs.ipfsCoordAdapter.ipfsCoord
|
||||
this.jsonrpc = jsonrpc
|
||||
this.userController = new UserController(localConfig)
|
||||
this.authController = new AuthController(localConfig)
|
||||
this.aboutController = new AboutController()
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
// Added the property "from" to the parsedData object;
|
||||
// necessary for calculating rate limits (based on the IPFS ID).
|
||||
parsedData.from = from
|
||||
|
||||
// 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
|
||||
case 'about':
|
||||
retObj = await _this.aboutController.aboutRouter(parsedData)
|
||||
}
|
||||
|
||||
// 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 () {
|
||||
const errorObj = {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: 'Input does not match routing rules.'
|
||||
}
|
||||
|
||||
return errorObj
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JSONRPC
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Rate limit
|
||||
*/
|
||||
/* eslint no-useless-catch: 0 */
|
||||
|
||||
// Local libraries
|
||||
const RateLimitLib = require('koa2-ratelimit').RateLimit
|
||||
|
||||
class RateLimit {
|
||||
constructor (options) {
|
||||
// Encapsulate dependencies
|
||||
this.RateLimitLib = RateLimitLib
|
||||
|
||||
// Set default rate limit options.
|
||||
this.defaultOptions = {
|
||||
interval: { min: 1 },
|
||||
max: 60,
|
||||
onLimitReached: this.onLimitReached
|
||||
}
|
||||
|
||||
// ctx obj
|
||||
this.context = {
|
||||
state: {
|
||||
user: ''
|
||||
},
|
||||
request: {
|
||||
ip: ''
|
||||
},
|
||||
user: '',
|
||||
set: () => {}
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// `this.defaultOptions: ${JSON.stringify(this.defaultOptions, null, 2)}`
|
||||
// )
|
||||
// console.log(`options: ${JSON.stringify(options, null, 2)}`)
|
||||
|
||||
// Set rate limit settings. Default values are overwritten if user passes
|
||||
// in an options object.
|
||||
this.rateLimitOptions = Object.assign({}, this.defaultOptions, options)
|
||||
// console.log(
|
||||
// `this.rateLimitOptions: ${JSON.stringify(this.rateLimitOptions, null, 2)}`
|
||||
// )
|
||||
this.rateLimit = this.RateLimitLib.middleware(this.rateLimitOptions)
|
||||
}
|
||||
|
||||
// This function is called when the user hits their rate limits.
|
||||
onLimitReached () {
|
||||
try {
|
||||
const error = new Error() // Establish provided options as the default options.
|
||||
error.message = 'Too many requests, please try again later.'
|
||||
error.status = 429
|
||||
throw error
|
||||
} catch (error) {
|
||||
// console.log("Error in onLimitReached()", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// This is the middleware function called by the router.
|
||||
async limiter (from) {
|
||||
try {
|
||||
if (!from || typeof from !== 'string') {
|
||||
throw new Error('from must be a string')
|
||||
}
|
||||
|
||||
// Set context.limiter
|
||||
// This overrides the default koa behavior and adapts the rate limiter
|
||||
// to work with the JSON RPC over IPFS.
|
||||
this.context.state.user = from
|
||||
this.context.request.ip = from
|
||||
this.context.user = from
|
||||
|
||||
await this.rateLimit(this.context, () => {})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error in rate-limit.js/limiter()')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RateLimit
|
||||
@@ -0,0 +1,536 @@
|
||||
/*
|
||||
This is the JSON RPC router for the users API
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
|
||||
// Local libraries
|
||||
// const UserLib = require('../../../use-cases/user')
|
||||
const Validators = require('../validators')
|
||||
const RateLimit = require('../rate-limit')
|
||||
|
||||
class UserRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating User JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating User JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.userLib = this.useCases.user
|
||||
this.jsonrpc = jsonrpc
|
||||
this.validators = new Validators(localConfig)
|
||||
this.rateLimit = new RateLimit()
|
||||
}
|
||||
|
||||
// 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':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.createUser(rpcData)
|
||||
|
||||
case 'getAllUsers':
|
||||
await this.validators.ensureUser(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.getAll(rpcData)
|
||||
|
||||
case 'getUser':
|
||||
user = await this.validators.ensureUser(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.getUser(rpcData, user)
|
||||
|
||||
case 'updateUser':
|
||||
user = await this.validators.ensureTargetUserOrAdmin(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.updateUser(rpcData, user)
|
||||
|
||||
case 'deleteUser':
|
||||
user = await this.validators.ensureTargetUserOrAdmin(rpcData)
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.deleteUser(rpcData, user)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in UsersRPC/rpcRouter()')
|
||||
// throw err
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: err.status || 500,
|
||||
message: err.message,
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Create a new user
|
||||
* @apiPermission public
|
||||
* @apiName CreateUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "createUser", "email": "test555@test.com", "name": "testy tester", "password": "password"}}
|
||||
*
|
||||
* @apiParam {String} email Email(required).
|
||||
* @apiParam {String} password Password(required).
|
||||
* @apiParam {String} name name or handle(optional).
|
||||
* @apiParam {string} endpoint (required)
|
||||
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "555",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "userData": {
|
||||
* "type": "user",
|
||||
* "_id": "607dd3e6426f3d3148b3a466",
|
||||
* "email": "test555@test.com",
|
||||
* "name": "testy tester",
|
||||
* "__v": 0
|
||||
* },
|
||||
* "token": "eyJhbGciOiJIUzI1NiIs1nR5cCI6IkpXVTJ9.eyJpZCI6IjYwN2RkM2U2NDI2ZjNkMzE0OGIzYTQ2NiIsImlhdCI6MTYxODg1ODk4Mn0.in4vzxDqqyCd7LpuhG3xlXeBqrJ5bp9GJPwhaoVzldI",
|
||||
* "endpoint": "createUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity",
|
||||
* "endpoint": "getUser"
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*/
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Get all users
|
||||
* @apiPermission public
|
||||
* @apiName GetAllUsers
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"users","params":{ "endpoint": "getAllUsers", "apiToken": "<JWT>"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccess {Object[]} users Array of user objects
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "users": [
|
||||
* {
|
||||
* "type": "user",
|
||||
* "_id": "6070bc6da931e73d4d9e108d",
|
||||
* "email": "test678@test.com",
|
||||
* "name": "testy tester",
|
||||
* "__v": 0
|
||||
* }
|
||||
* ],
|
||||
* "endpoint": "getAllUsers",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "",
|
||||
* "endpoint": "getAllUsers"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Get a user
|
||||
* @apiPermission public
|
||||
* @apiName GetAUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "getUser", "apiToken": "<JWT>", "userId": "<_id>"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {String} userId (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccess {Object[]} users Array of user objects
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "user": {
|
||||
* "type": "user",
|
||||
* "_id": "607dd3e6426f3d3148b3a466",
|
||||
* "email": "test555@test.com",
|
||||
* "name": "testy tester",
|
||||
* "__v": 0
|
||||
* },
|
||||
* "endpoint": "getUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
*}
|
||||
*
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity",
|
||||
* "endpoint": "getUser"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Update a user
|
||||
* @apiPermission public
|
||||
* @apiName UpdateAUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "updateUser", "apiToken": "<JWT>", "userId": "<_id>", "name": "test999"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {String} userId (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
* @apiParam {String} email Email(Optional).
|
||||
* @apiParam {String} password Password(Optional).
|
||||
* @apiParam {String} name name or handle(Optional).
|
||||
*
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name Updated name
|
||||
* @apiSuccess {String} users.username Updated username
|
||||
* @apiSuccess {String} users.email Updated email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "user": {
|
||||
* "type": "user",
|
||||
* "_id": "607dd3e6426f3d3148b3a466",
|
||||
* "email": "test555@test.com",
|
||||
* "name": "test001",
|
||||
* "__v": 0
|
||||
* },
|
||||
* "endpoint": "updateUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity.",
|
||||
* "endpoint": "updateUser"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /users Delete a user
|
||||
* @apiPermission public
|
||||
* @apiName DeleteAUser
|
||||
* @apiGroup JSON Users
|
||||
*
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"123","method":"users","params":{ "endpoint": "deleteUser", "userId": "<_id>", "apiToken": "<JWT>"}}
|
||||
*
|
||||
* @apiParam {String} apiToken (required)
|
||||
* @apiParam {String} userId (required)
|
||||
* @apiParam {string} endpoint (required)
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* result": {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "endpoint": "deleteUser",
|
||||
* "success": true,
|
||||
* "status": 200,
|
||||
* "message": ""
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "jsonrpc": "2.0",
|
||||
* "id": "123",
|
||||
* "result": {
|
||||
* "method": "users",
|
||||
* "reciever": "Qmc2uJhg7yrqaNaoTJRDkzrAyVe82e9JMFQcxrBUjbdXyC",
|
||||
* "value": {
|
||||
* "success": false,
|
||||
* "status": 422,
|
||||
* "message": "Unprocessable Entity",
|
||||
* "endpoint": "deleteUser"
|
||||
* }
|
||||
* }
|
||||
*}
|
||||
*
|
||||
*
|
||||
*/
|
||||
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,96 @@
|
||||
/*
|
||||
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('../../adapters/localdb/models/users')
|
||||
|
||||
class Validators {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating JSON RPC Validators library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.config = config
|
||||
this.jwt = jwt
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
}
|
||||
|
||||
// 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 true
|
||||
|
||||
// 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
|
||||
@@ -0,0 +1,99 @@
|
||||
const Passport = require('../../../adapters/passport')
|
||||
const passport = new Passport()
|
||||
|
||||
let _this
|
||||
|
||||
class AuthRESTController {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Auth REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Auth REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
_this = this
|
||||
this.passport = passport
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDefine TokenError
|
||||
* @apiError Unauthorized Invalid JWT token
|
||||
*
|
||||
* @apiErrorExample {json} Unauthorized-Error:
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "status": 401,
|
||||
* "error": "Unauthorized"
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @api {post} /auth Authenticate user
|
||||
* @apiName AuthUser
|
||||
* @apiGroup Auth
|
||||
*
|
||||
* @apiParam {String} username User username.
|
||||
* @apiParam {String} password User password.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "username": "johndoe@gmail.com", "password": "foo" }' localhost:5000/auth
|
||||
*
|
||||
* @apiSuccess {Object} user User object
|
||||
* @apiSuccess {ObjectId} user._id User id
|
||||
* @apiSuccess {String} user.name User name
|
||||
* @apiSuccess {String} user.username User username
|
||||
* @apiSuccess {String} token Encoded JWT
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "username": "johndoe"
|
||||
* },
|
||||
* "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
|
||||
* }
|
||||
*
|
||||
* @apiError Unauthorized Incorrect credentials
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 401 Unauthorized
|
||||
* {
|
||||
* "status": 401,
|
||||
* "error": "Unauthorized"
|
||||
* }
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
const token = user.generateToken()
|
||||
|
||||
const response = user.toJSON()
|
||||
|
||||
delete response.password
|
||||
|
||||
ctx.body = {
|
||||
token,
|
||||
user: response
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(401)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthRESTController
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
REST API library for auth route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const AuthRESTController = require('./controller')
|
||||
|
||||
class AuthRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.authRESTController = new AuthRESTController(localConfig)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/auth'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.authRESTController.authUser)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthRouter
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Controller for the /contact REST API endpoints.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-useless-escape */
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
const ContactLib = require('../../../adapters/contact')
|
||||
const contactLib = new ContactLib()
|
||||
|
||||
let _this
|
||||
|
||||
class ContactController {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.contactLib = contactLib
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /contact/email Send Email
|
||||
* @apiName SendMail
|
||||
* @apiGroup Contact
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "obj": { "email": "email@format.com", "formMessage": "a message" } }' localhost:5001/contact/email
|
||||
*
|
||||
* @apiParam {Object} obj object (required)
|
||||
* @apiParam {String} obj.email Sender Email.
|
||||
* @apiParam {String} obj.formMessage Message.
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
*
|
||||
* success:true
|
||||
*
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async email (ctx) {
|
||||
try {
|
||||
const data = ctx.request.body
|
||||
const emailObj = data.obj
|
||||
await _this.contactLib.sendEmail(emailObj)
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = ContactController
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
REST API library for /contact route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const ContactRESTControllerLib = require('./controller')
|
||||
|
||||
class ContactRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Contact REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Contact REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.contactRESTController = new ContactRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/contact'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/email', this.contactRESTController.email)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ContactRouter
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This index file for the Clean Architecture Controllers loads dependencies,
|
||||
creates instances, and attaches the controller to REST API endpoints for
|
||||
Koa.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
|
||||
// Load the REST API Controllers.
|
||||
const AuthRESTController = require('./auth')
|
||||
const UserRouter = require('./users')
|
||||
const ContactRESTController = require('./contact')
|
||||
const LogsRESTController = require('./logs')
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating REST Controller libraries.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating REST Controller libraries.'
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('Controllers localConfig: ', localConfig)
|
||||
}
|
||||
|
||||
attachRESTControllers (app) {
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Attach the REST API Controllers associated with the /auth route
|
||||
const authRESTController = new AuthRESTController(dependencies)
|
||||
authRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /user route
|
||||
const userRouter = new UserRouter(dependencies)
|
||||
userRouter.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /contact route
|
||||
const contactRESTController = new ContactRESTController(dependencies)
|
||||
contactRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /logs route
|
||||
const logsRESTController = new LogsRESTController(dependencies)
|
||||
logsRESTController.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RESTControllers
|
||||
@@ -0,0 +1,65 @@
|
||||
const LogsApiLib = require('../../../adapters/logapi')
|
||||
const logsApiLib = new LogsApiLib()
|
||||
let _this
|
||||
|
||||
class LogsApi {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.logsApiLib = logsApiLib
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /logapi Parse and return the log files.
|
||||
* @apiPermission public
|
||||
* @apiName LogApi
|
||||
* @apiGroup Logs
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "password": "secretpasas" }' localhost:5000/logapi
|
||||
*
|
||||
* @apiParam {String} password Password (required)
|
||||
*
|
||||
* @apiSuccess {Array} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "username": "johndoe"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async getLogs (ctx) {
|
||||
try {
|
||||
// console.log('entering getLogs()')
|
||||
|
||||
// Get the user-provided password.
|
||||
const password = ctx.request.body.password
|
||||
const result = await _this.logsApiLib.getLogs(password)
|
||||
ctx.body = result
|
||||
} catch (err) {
|
||||
if (err && err.message) {
|
||||
ctx.throw(422, err.message)
|
||||
} else {
|
||||
ctx.throw(500, 'Unhandled error')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsApi
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
REST API library for /logs route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const LogsRESTControllerLib = require('./controller')
|
||||
|
||||
class LogsRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating Logs REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating Logs REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
this.logsRESTController = new LogsRESTControllerLib(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/logs'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.logsRESTController.getLogs)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsRouter
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = function errorMiddleware () {
|
||||
return async (ctx, next) => {
|
||||
try {
|
||||
await next()
|
||||
} catch (err) {
|
||||
ctx.status = err.status || 500
|
||||
ctx.body = err.message
|
||||
ctx.app.emit('error', err, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
REST API validator middleware.
|
||||
*/
|
||||
|
||||
const User = require('../../../adapters/localdb/models/users')
|
||||
const config = require('../../../../config')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class Validators {
|
||||
constructor () {
|
||||
this.User = User
|
||||
this.jwt = jwt
|
||||
this.config = config
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
async ensureUser (ctx, next) {
|
||||
try {
|
||||
// console.log(`getToken: ${typeof (getToken)}`)
|
||||
const token = _this.getToken(ctx)
|
||||
|
||||
if (!token) {
|
||||
// console.log(`Err: Token not provided.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
let decoded = null
|
||||
try {
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
decoded = _this.jwt.verify(token, config.token)
|
||||
} catch (err) {
|
||||
// console.log(`Err: Token could not be decoded: ${err}`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
ctx.state.user = await _this.User.findById(decoded.id, '-password')
|
||||
if (!ctx.state.user) {
|
||||
// console.log(`Err: Could not find user.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
// return next()
|
||||
return true
|
||||
} catch (error) {
|
||||
ctx.throw(401)
|
||||
}
|
||||
}
|
||||
|
||||
// This funciton is almost identical to ensureUser, except at the end, it verifies
|
||||
// that the 'type' associated with the user equals 'admin'.
|
||||
async ensureAdmin (ctx, next) {
|
||||
try {
|
||||
// console.log(`getToken: ${typeof (getToken)}`)
|
||||
const token = _this.getToken(ctx)
|
||||
|
||||
if (!token) {
|
||||
// console.log(`Err: Token not provided.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
let decoded = null
|
||||
try {
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
decoded = _this.jwt.verify(token, config.token)
|
||||
} catch (err) {
|
||||
// console.log(`Err: Token could not be decoded: ${err}`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
ctx.state.user = await _this.User.findById(decoded.id, '-password')
|
||||
if (!ctx.state.user) {
|
||||
// console.log(`Err: Could not find user.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
if (ctx.state.user.type !== 'admin') {
|
||||
ctx.throw(401, 'not admin')
|
||||
}
|
||||
|
||||
// return next()
|
||||
return true
|
||||
} catch (error) {
|
||||
ctx.throw(401, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (ctx, next) {
|
||||
try {
|
||||
// console.log(`getToken: ${typeof (getToken)}`)
|
||||
const token = _this.getToken(ctx)
|
||||
|
||||
if (!token) {
|
||||
// console.log(`Err: Token not provided.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
// The user ID targeted in this API call.
|
||||
const targetId = ctx.params.id
|
||||
// console.log(`targetId: ${JSON.stringify(targetId, null, 2)}`)
|
||||
|
||||
let decoded = null
|
||||
try {
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
decoded = _this.jwt.verify(token, config.token)
|
||||
} catch (err) {
|
||||
// console.log(`Err: Token could not be decoded: ${err}`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
|
||||
ctx.state.user = await _this.User.findById(decoded.id, '-password')
|
||||
if (!ctx.state.user) {
|
||||
// console.log(`Err: Could not find user.`)
|
||||
ctx.throw(401)
|
||||
}
|
||||
// console.log('ctx.state.user: ', ctx.state.user)
|
||||
|
||||
// console.log(`ctx.state.user: ${JSON.stringify(ctx.state.user, null, 2)}`)
|
||||
// Ensure the calling user and the target user are the same.
|
||||
|
||||
if (ctx.state.user._id.toString() !== targetId.toString()) {
|
||||
wlogger.verbose(
|
||||
`Calling user and target user do not match! Calling user: ${ctx.state.user._id}, Target user: ${targetId}`
|
||||
)
|
||||
|
||||
// If they don't match, then the calling user better be an admin.
|
||||
if (ctx.state.user.type !== 'admin') {
|
||||
ctx.throw(401, 'not admin')
|
||||
} else {
|
||||
wlogger.verbose("It's ok. The user is an admin.")
|
||||
}
|
||||
}
|
||||
|
||||
// return next()
|
||||
return true
|
||||
} catch (error) {
|
||||
ctx.throw(401, error.message)
|
||||
}
|
||||
}
|
||||
|
||||
getToken (ctx) {
|
||||
const header = ctx.request.header.authorization
|
||||
if (!header) {
|
||||
return null
|
||||
}
|
||||
const parts = header.split(' ')
|
||||
if (parts.length !== 2) {
|
||||
return null
|
||||
}
|
||||
const scheme = parts[0]
|
||||
const token = parts[1]
|
||||
if (/^Bearer$/i.test(scheme)) {
|
||||
return token
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Validators
|
||||
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
REST API Controller library for the /user route
|
||||
*/
|
||||
|
||||
const { wlogger } = require('../../../adapters/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class UserRESTControllerLib {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating /users REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating /users REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
// this.userUseCases = this.useCases.user
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /users Create a new user
|
||||
* @apiPermission user
|
||||
* @apiName CreateUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "user": { "email": "email@format.com", "name": "my name", "password": "secretpasas" } }' localhost:5001/users
|
||||
*
|
||||
* @apiParam {Object} user User object (required)
|
||||
* @apiParam {String} user.email Email
|
||||
* @apiParam {String} user.password Password
|
||||
* @apiParam {String} user.name name or handle
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "email": "email@format.com",
|
||||
* "password": "somestrongpassword"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async createUser (ctx) {
|
||||
try {
|
||||
const userObj = ctx.request.body.user
|
||||
|
||||
const { userData, token } = await _this.useCases.user.createUser(userObj)
|
||||
// console.log('userData: ', userData)
|
||||
// console.log('token: ', token)
|
||||
|
||||
ctx.body = {
|
||||
user: userData,
|
||||
token
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(`err.message: ${err.message}`)
|
||||
// console.log('err: ', err)
|
||||
// ctx.throw(422, err.message)
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /users Get all users
|
||||
* @apiPermission user
|
||||
* @apiName GetUsers
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5000/users
|
||||
*
|
||||
* @apiSuccess {Object[]} users Array of user objects
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "users": [{
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "email": "email@format.com"
|
||||
* }]
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async getUsers (ctx) {
|
||||
try {
|
||||
const users = await _this.useCases.user.getAllUsers()
|
||||
|
||||
ctx.body = { users }
|
||||
} catch (err) {
|
||||
wlogger.error('Error in users/controller.js/getUsers(): '.err)
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /users/:id Get user by id
|
||||
* @apiPermission user
|
||||
* @apiName GetUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X GET localhost:5000/users/56bd1da600a526986cf65c80
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name User name
|
||||
* @apiSuccess {String} users.username User username
|
||||
* @apiSuccess {String} users.email User email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "John Doe"
|
||||
* "email": "email@format.com"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async getUser (ctx, next) {
|
||||
try {
|
||||
const user = await _this.useCases.user.getUser(ctx.params)
|
||||
|
||||
ctx.body = {
|
||||
user
|
||||
}
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
|
||||
if (next) {
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {put} /users/:id Update a user
|
||||
* @apiPermission user
|
||||
* @apiName UpdateUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X PUT -d '{ "user": { "name": "Cool new Name" } }' localhost:5000/users/56bd1da600a526986cf65c80
|
||||
*
|
||||
* @apiParam {Object} user User object (required)
|
||||
* @apiParam {String} user.name Name.
|
||||
* @apiParam {String} user.email Email.
|
||||
* @apiParam {String} user.password Password. (optional)
|
||||
*
|
||||
* @apiSuccess {Object} users User object
|
||||
* @apiSuccess {ObjectId} users._id User id
|
||||
* @apiSuccess {String} user.type User type (admin or user)
|
||||
* @apiSuccess {String} users.name Updated name
|
||||
* @apiSuccess {String} users.username Updated username
|
||||
* @apiSuccess {String} users.email Updated email
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "user": {
|
||||
* "_id": "56bd1da600a526986cf65c80"
|
||||
* "name": "Cool new name"
|
||||
* "email": "email@format.com"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async updateUser (ctx) {
|
||||
try {
|
||||
const existingUser = ctx.body.user
|
||||
const newData = ctx.request.body.user
|
||||
|
||||
const user = await _this.useCases.user.updateUser(existingUser, newData)
|
||||
|
||||
ctx.body = {
|
||||
user
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {delete} /users/:id Delete a user
|
||||
* @apiPermission user
|
||||
* @apiName DeleteUser
|
||||
* @apiGroup REST Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X DELETE localhost:5000/users/56bd1da600a526986cf65c80
|
||||
*
|
||||
* @apiSuccess {StatusCode} 200
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* "success": true
|
||||
* }
|
||||
*
|
||||
* @apiUse TokenError
|
||||
*/
|
||||
async deleteUser (ctx) {
|
||||
try {
|
||||
const user = ctx.body.user
|
||||
|
||||
// await user.remove()
|
||||
await _this.useCases.user.deleteUser(user)
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserRESTControllerLib
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
REST API library for /user route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const UserRESTControllerLib = require('./controller')
|
||||
const Validators = require('../middleware/validators')
|
||||
|
||||
let _this
|
||||
|
||||
class UserRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.userRESTController = new UserRESTControllerLib(dependencies)
|
||||
this.validators = new Validators()
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/users'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/', this.userRESTController.createUser)
|
||||
this.router.get('/', this.getAll)
|
||||
this.router.get('/:id', this.getById)
|
||||
this.router.put('/:id', this.updateUser)
|
||||
this.router.delete('/:id', this.deleteUser)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
|
||||
async getAll (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.userRESTController.getUsers(ctx, next)
|
||||
}
|
||||
|
||||
async getById (ctx, next) {
|
||||
await _this.validators.ensureUser(ctx, next)
|
||||
await _this.userRESTController.getUser(ctx, next)
|
||||
}
|
||||
|
||||
async updateUser (ctx, next) {
|
||||
await _this.validators.ensureTargetUserOrAdmin(ctx, next)
|
||||
await _this.userRESTController.getUser(ctx, next)
|
||||
await _this.userRESTController.updateUser(ctx, next)
|
||||
}
|
||||
|
||||
async deleteUser (ctx, next) {
|
||||
await _this.validators.ensureTargetUserOrAdmin(ctx, next)
|
||||
await _this.userRESTController.getUser(ctx, next)
|
||||
await _this.userRESTController.deleteUser(ctx, next)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserRouter
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
User Entity
|
||||
*/
|
||||
|
||||
class User {
|
||||
validate ({ name, email, password } = {}) {
|
||||
// Input Validation
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
if (!password || typeof password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
if (!name || typeof name !== 'string') {
|
||||
throw new Error("Property 'name' must be a string!")
|
||||
}
|
||||
|
||||
const userData = { name, email, password }
|
||||
|
||||
return userData
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
This is a top-level library that encapsulates all the additional Use Cases.
|
||||
The concept of Use Cases comes from Clean Architecture:
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
const UserUseCases = require('./user')
|
||||
|
||||
class UseCases {
|
||||
constructor (localConfig = {}) {
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// console.log('use-cases/index.js localConfig: ', localConfig)
|
||||
this.user = new UserUseCases(localConfig)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UseCases
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
This library contains business-logic for dealing with users. Most of these
|
||||
functions are called by the /user REST API endpoints.
|
||||
*/
|
||||
|
||||
// const UserModel = require('../adapters/localdb/models/users')
|
||||
const { wlogger } = require('../adapters/wlogger')
|
||||
|
||||
class UserLib {
|
||||
constructor (localConfig = {}) {
|
||||
// console.log('User localConfig: ', localConfig)
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of adapters must be passed in when instantiating User Use Cases library.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.UserModel = this.adapters.localdb.Users
|
||||
}
|
||||
|
||||
// Create a new user model and add it to the Mongo database.
|
||||
async createUser (userObj) {
|
||||
try {
|
||||
// Input Validation
|
||||
if (!userObj.email || typeof userObj.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
if (!userObj.password || typeof userObj.password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
if (!userObj.name || typeof userObj.name !== 'string') {
|
||||
throw new Error("Property 'name' must be a string!")
|
||||
}
|
||||
|
||||
const user = new this.UserModel(userObj)
|
||||
|
||||
// Enforce default value of 'user'
|
||||
user.type = 'user'
|
||||
// console.log('user: ', user)
|
||||
|
||||
// Save the new user model to the database.
|
||||
await user.save()
|
||||
|
||||
// Generate a JWT token for the user.
|
||||
const token = user.generateToken()
|
||||
|
||||
// Convert the database model to a JSON object.
|
||||
const userData = user.toJSON()
|
||||
// console.log('userData: ', userData)
|
||||
|
||||
// Delete the password property.
|
||||
delete userData.password
|
||||
|
||||
return { userData, token }
|
||||
} catch (err) {
|
||||
// console.log('createUser() error: ', err)
|
||||
wlogger.error('Error in lib/users.js/createUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Returns an array of all user models in the Mongo database.
|
||||
async getAllUsers () {
|
||||
try {
|
||||
// Get all user models. Delete the password property from each model.
|
||||
const users = await this.UserModel.find({}, '-password')
|
||||
|
||||
return users
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/users.js/getAllUsers()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the model for a specific user.
|
||||
async getUser (params) {
|
||||
try {
|
||||
const { id } = params
|
||||
|
||||
const user = await this.UserModel.findById(id, '-password')
|
||||
|
||||
// Throw a 404 error if the user isn't found.
|
||||
if (!user) {
|
||||
const err = new Error('User not found')
|
||||
err.status = 404
|
||||
throw err
|
||||
}
|
||||
|
||||
return user
|
||||
} catch (err) {
|
||||
// console.log('Error in getUser: ', err)
|
||||
|
||||
if (err.status === 404) throw err
|
||||
|
||||
// Return 422 for any other error
|
||||
err.status = 422
|
||||
err.message = 'Unprocessable Entity'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
if (newData.name && typeof newData.name !== 'string') {
|
||||
throw new Error("Property 'name' must be a string!")
|
||||
}
|
||||
if (newData.password && typeof newData.password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
|
||||
// Save a copy of the original user type.
|
||||
const userType = existingUser.type
|
||||
// console.log('userType: ', userType)
|
||||
|
||||
// If user 'type' property is sent by the client
|
||||
if (newData.type) {
|
||||
if (typeof newData.type !== 'string') {
|
||||
throw new Error("Property 'type' must be a string!")
|
||||
}
|
||||
|
||||
// Unless the calling user is an admin, they can not change the user type.
|
||||
if (userType !== 'admin') {
|
||||
throw new Error("Property 'type' can only be changed by Admin user")
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite any existing data with the new data.
|
||||
Object.assign(existingUser, newData)
|
||||
|
||||
// Save the changes to the database.
|
||||
await existingUser.save()
|
||||
|
||||
// Delete the password property.
|
||||
delete existingUser.password
|
||||
|
||||
return existingUser
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/users.js/updateUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser (user) {
|
||||
try {
|
||||
await user.remove()
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/users.js/deleteUser()')
|
||||
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 this.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
|
||||
@@ -0,0 +1,7 @@
|
||||
# Automated End-to-end Tests
|
||||
|
||||
This contains the original boilerplate tests, which are end-to-end tests. These tests are fully automated and test the system directly by making REST API calls with axios.
|
||||
|
||||
These tests function exactly the same as a normal user would, by making real REST API calls to the software. As a result, they are fine for testing internal system components like authorization and user handling. However, they are inappropriate for testing sophisticated endpoints that involve complex operations. For example, interacting with a blockchain, pinging other network systems, or writing data to a secondary database.
|
||||
|
||||
There is some redundancy between these tests and the unit tests. The focus is on *how* the tests are executed. The unit tests call the libraries directly (internally). These e2e tests use the REST API (externally).
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
End-to-end tests for /auth endpoints.
|
||||
|
||||
This test sets up the environment for other e2e tests.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const axios = require('axios').default
|
||||
|
||||
// Local support libraries
|
||||
const config = require('../../../config')
|
||||
const app = require('../../../bin/server')
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
const AdminLib = require('../../../src/adapters/admin')
|
||||
const adminLib = new AdminLib()
|
||||
|
||||
// const request = supertest.agent(app.listen())
|
||||
const context = {}
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,790 @@
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
const assert = require('chai').assert
|
||||
const config = require('../../../config')
|
||||
const axios = require('axios').default
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
const context = {}
|
||||
|
||||
const UserController = require('../../../src/controllers/rest-api/users/controller')
|
||||
const adapters = require('../../../src/adapters')
|
||||
const UseCases = require('../../../src/use-cases/')
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
// const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Users', () => {
|
||||
before(async () => {
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
|
||||
// Create a second test user.
|
||||
const userObj = {
|
||||
email: 'test2@test.com',
|
||||
password: 'pass2',
|
||||
name: 'test2'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
|
||||
|
||||
context.user2 = testUser.user
|
||||
context.token2 = testUser.token
|
||||
context.id2 = testUser.user._id
|
||||
|
||||
// Get the JWT used to log in as the admin 'system' user.
|
||||
const adminJWT = await testUtils.getAdminJWT()
|
||||
// console.log(`adminJWT: ${admi nJWT}`)
|
||||
context.adminJWT = adminJWT
|
||||
|
||||
// const admin = await testUtils.loginAdminUser()
|
||||
// context.adminJWT = admin.token
|
||||
|
||||
// const admin = await adminLib.loginAdmin()
|
||||
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCases({ adapters })
|
||||
uut = new UserController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /users - Create User', () => {
|
||||
it('should reject signup when data is incomplete', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
email: 'test2@test.com'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
/* console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
) */
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 422, 'Error code 422 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject signup if no email property is provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
password: 'pass2'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log('err', err)
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string")
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject signup if no password property is provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'test2@test.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'password' must be a string"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should reject if name property property is not string', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'test322@test.com',
|
||||
password: 'supersecretpassword',
|
||||
name: 1234
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'name' must be a string")
|
||||
}
|
||||
})
|
||||
|
||||
it("should signup of type 'user' by default", async () => {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'test3@test.com',
|
||||
password: 'supersecretpassword',
|
||||
name: 'test3'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
context.user = result.data.user
|
||||
context.token = result.data.token
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test3@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
assert.property(result.data, 'token', 'Token property exists.')
|
||||
assert.equal(result.data.user.type, 'user')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should not fetch users if the authorization header is missing', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not fetch users if the authorization header is missing the scheme', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: '1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not fetch users if the authorization header has invalid scheme', async () => {
|
||||
const { token } = context
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Unknown ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not fetch users if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should fetch all users', async () => {
|
||||
const { token } = context
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const users = result.data.users
|
||||
// console.log(`users: ${util.inspect(users)}`)
|
||||
|
||||
assert.hasAnyKeys(users[0], ['type', '_id', 'email'])
|
||||
assert.isNumber(users.length)
|
||||
})
|
||||
|
||||
it('should return a 422 http status if biz-logic throws an error', async () => {
|
||||
try {
|
||||
const { token } = context
|
||||
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.useCases.user, 'getAllUsers')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.fail('Unexpected code path!')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.equal(err.response.data, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users/:id', () => {
|
||||
it('should not fetch user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw 404 if user doesn't exist", async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 404)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 422 for invalid input', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
}
|
||||
})
|
||||
|
||||
it('should fetch own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /users/:id', () => {
|
||||
it('should not update user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if non-admin updating other user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update user type', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'new name',
|
||||
type: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
// console.log(`Users: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
// assert(result.status === 200, 'Status Code 200 expected.')
|
||||
// assert(result.data.user.type === 'user', 'Type should be unchanged.')
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'type' can only be changed by Admin user"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update other user when not admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
name: 'This should not work'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if name property is wrong', async () => {
|
||||
try {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: {},
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (error) {
|
||||
assert.equal(error.response.status, 422)
|
||||
assert.include(error.response.data, "Property 'name' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if password property is not string', async () => {
|
||||
const { token } = context
|
||||
const _id = context.user._id
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
password: 1234,
|
||||
email: 'test@test.com',
|
||||
name: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'password' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if email is not string', async () => {
|
||||
const { token } = context
|
||||
const _id = context.user._id
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 1234
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update type property if is not string', async () => {
|
||||
try {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
type: 1,
|
||||
email: 'test@test.com',
|
||||
name: 'test',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'type' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should be able to update other user when admin', async () => {
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
name: 'This should work',
|
||||
email: 'test4@test.com',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
const userName = result.data.user.name
|
||||
assert.equal(userName, 'This should work')
|
||||
})
|
||||
|
||||
it('should update user with minimum inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: { email: 'testToUpdate@test.com' }
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
})
|
||||
|
||||
it('should update user with all inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: 'my name',
|
||||
username: 'myUsername'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'name')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.name, 'my name')
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
assert.equal(user.username, 'myUsername')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
it('should not delete user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if deleting invalid user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to delete other users unless admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should delete own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data.success)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
|
||||
it('should be able to delete other users when admin', async () => {
|
||||
const id = context.id2
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,178 @@
|
||||
const config = require('../../../config')
|
||||
const axios = require('axios').default
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Mock data
|
||||
// const mockData = require('./mocks/contact-mocks')
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
const ContactController = require('../../../src/controllers/rest-api/contact/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
describe('Contact', () => {
|
||||
beforeEach(() => {
|
||||
uut = new ContactController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /contact/email', () => {
|
||||
it('should throw error if email property is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
formMessage: 'message'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if formMessage property is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'formMessage' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list provided is not a array', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
emailList: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'emailList' must be a array of emails!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list provided is a empty array', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
emailList: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'emailList' must be a array of emails!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email with minimun input', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.contactLib, 'sendEmail').resolves(true)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message'
|
||||
}
|
||||
}
|
||||
}
|
||||
await uut.email(ctx)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email with all inputs', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.contactLib, 'sendEmail').resolves(true)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
emailList: ['email@email.com']
|
||||
}
|
||||
}
|
||||
}
|
||||
await uut.email(ctx)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
const config = require('../../../config')
|
||||
const assert = require('chai').assert
|
||||
|
||||
const axios = require('axios').default
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
const LogsController = require('../../../src/controllers/rest-api/logs/controller')
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('LogsApi', () => {
|
||||
beforeEach(() => {
|
||||
uut = new LogsController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /logs', () => {
|
||||
it('should return false if password is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/logs`,
|
||||
data: {}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
assert.isFalse(result.data.success)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return log', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/logs`,
|
||||
data: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
assert.isTrue(result.data.success)
|
||||
assert.isArray(result.data.data)
|
||||
assert.property(result.data.data[0], 'message')
|
||||
assert.property(result.data.data[0], 'level')
|
||||
assert.property(result.data.data[0], 'timestamp')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return false if files are not found!', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.logsApiLib, 'getLogs').resolves({
|
||||
success: false,
|
||||
data: 'file does not exist'
|
||||
})
|
||||
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.isFalse(ctx.body.success)
|
||||
assert.include(ctx.body.data, 'file does not exist')
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and handle errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.logsApiLib.fs, 'existsSync')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
ctx.request = {
|
||||
body: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw unhandled error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.logsApiLib.fs, 'existsSync').throws(new Error())
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
ctx.request = {
|
||||
body: {
|
||||
password: 'test'
|
||||
}
|
||||
}
|
||||
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unhandled error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,305 @@
|
||||
const assert = require('chai').assert
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
|
||||
const Validators = require('../../../src/controllers/rest-api/middleware/validators')
|
||||
|
||||
const sinon = require('sinon')
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const context = {}
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Validators', () => {
|
||||
before(async () => {
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
|
||||
// Create a second test user.
|
||||
const userObj = {
|
||||
email: 'testvalidator@test.com',
|
||||
password: 'pass2',
|
||||
name: 'testvalidator'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
context.id = testUser.user._id
|
||||
|
||||
// Get the JWT used to log in as the admin 'system' user.
|
||||
const adminJWT = await testUtils.getAdminJWT()
|
||||
// console.log(`adminJWT: ${adminJWT}`)
|
||||
context.adminJWT = adminJWT
|
||||
|
||||
// const admin = await testUtils.loginAdminUser()
|
||||
// context.adminJWT = admin.token
|
||||
|
||||
// const admin = await adminLib.loginAdmin()
|
||||
// console.log(`admin: ${JSON.stringify(admin, null, 2)}`)
|
||||
})
|
||||
beforeEach(() => {
|
||||
uut = new Validators()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('ensureUser()', () => {
|
||||
it('should throw 401 if user cant be found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
|
||||
await uut.ensureUser(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token not found', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
await uut.ensureUser(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token is invalid', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await uut.ensureUser(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is admin', async () => {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.ensureUser(ctx)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureAdmin()', () => {
|
||||
it('should throw 401 if token not found', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token is invalid', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user cant be found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user is not admin type', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'not admin')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is admin', async () => {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.ensureAdmin(ctx)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureTargetUserOrAdmin()', () => {
|
||||
it('should throw 401 if token not found', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if token is invalid', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user cant be found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if user is not admin type', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: 'Target Id' }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 401)
|
||||
assert.include(err.message, 'not admin')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is admin', async () => {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.ensureTargetUserOrAdmin(ctx)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
const assert = require('chai').assert
|
||||
|
||||
const Admin = require('../../../src/adapters/admin')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Admin', () => {
|
||||
beforeEach(() => {
|
||||
uut = new Admin()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
# Unit Tests
|
||||
Unit tests are defined as testing the smallest possible unit of a function. They also do not make any live network calls.
|
||||
|
||||
Unit tests are broken up by directory:
|
||||
|
||||
- [biz-logic](./biz-logic) tests the business logic libraries.
|
||||
- [rest-api](./rest-api) tests the REST API specific handling of the router.
|
||||
- json-rpc (coming soon) tests the JSON-RPC routing using ipfs-coord library.
|
||||
@@ -0,0 +1,129 @@
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const ContactLib = require('../../../src/adapters/contact')
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
describe('Contact', () => {
|
||||
beforeEach(() => {
|
||||
uut = new ContactLib()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('sendEmail()', () => {
|
||||
it('should throw error if email property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
formMessage: 'test msg'
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if formMessage property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com'
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'formMessage' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list provided is not a array', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true)
|
||||
|
||||
const data = {
|
||||
formMessage: 'test msg',
|
||||
email: 'test@email.com',
|
||||
emailList: 'test@email.com'
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'emailList' must be a array of emails!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list provided is a empty array', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true)
|
||||
|
||||
const data = {
|
||||
formMessage: 'test msg',
|
||||
email: 'test@email.com',
|
||||
emailList: []
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
"Property 'emailList' must be a array of emails!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email to default server email', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true)
|
||||
|
||||
const data = {
|
||||
formMessage: 'test msg',
|
||||
email: 'test@email.com'
|
||||
}
|
||||
const result = await uut.sendEmail(data)
|
||||
assert.isTrue(result)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and throw nodemailer lib error', async () => {
|
||||
try {
|
||||
// Force an error with the database.
|
||||
sandbox
|
||||
.stub(uut.nodemailer, 'sendEmail')
|
||||
.throws(new Error('test error'))
|
||||
|
||||
const data = {
|
||||
formMessage: 'test msg',
|
||||
email: 'test@email.com'
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email to specifics email list', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true)
|
||||
|
||||
const data = {
|
||||
formMessage: 'test msg',
|
||||
email: 'test@email.com',
|
||||
emailList: ['testcontact@email.com']
|
||||
}
|
||||
const result = await uut.sendEmail(data)
|
||||
assert.isTrue(result)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Unit tests for the IPFS Adapter.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const IPFSCoordAdapter = require('../../../src/adapters/ipfs/ipfs-coord')
|
||||
const IPFSMock = require('../mocks/ipfs-mock')
|
||||
const IPFSCoordMock = require('../mocks/ipfs-coord-mock')
|
||||
|
||||
describe('#IPFS', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
const ipfs = IPFSMock.create()
|
||||
uut = new IPFSCoordAdapter({ ipfs })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if ipfs instance is not included', () => {
|
||||
try {
|
||||
uut = new IPFSCoordAdapter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of IPFS must be passed when instantiating ipfs-coord.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#start', () => {
|
||||
it('should return a promise that resolves into an instance of IPFS.', async () => {
|
||||
// Mock dependencies.
|
||||
uut.IpfsCoord = IPFSCoordMock
|
||||
|
||||
const result = await uut.start()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attachRPCRouter', () => {
|
||||
it('should attached a router output', async () => {
|
||||
// Mock dependencies
|
||||
uut.ipfsCoord = {
|
||||
privateLog: {},
|
||||
ipfs: {
|
||||
orbitdb: {
|
||||
privateLog: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = console.log
|
||||
|
||||
uut.attachRPCRouter(router)
|
||||
})
|
||||
|
||||
it('should throw an error if ipfs-coord has not been instantiated', () => {
|
||||
try {
|
||||
const router = console.log
|
||||
|
||||
uut.attachRPCRouter(router)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Unit tests for the index.js file for the IPFS and ipfs-coord libraries.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const IPFSLib = require('../../../src/adapters/ipfs')
|
||||
const IPFSMock = require('../mocks/ipfs-mock')
|
||||
const IPFSCoordMock = require('../mocks/ipfs-coord-mock')
|
||||
|
||||
describe('#IPFS-adapter-index', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new IPFSLib()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#start', () => {
|
||||
it('should return a promise that resolves into an instance of IPFS.', async () => {
|
||||
// Mock dependencies.
|
||||
uut.ipfsAdapter = new IPFSMock()
|
||||
uut.IpfsCoordAdapter = IPFSCoordMock
|
||||
|
||||
const result = await uut.start()
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.ipfsAdapter, 'start').rejects(new Error('test error'))
|
||||
|
||||
await uut.start()
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Unit tests for the IPFS Adapter.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const IPFSLib = require('../../../src/adapters/ipfs/ipfs')
|
||||
const IPFSMock = require('../mocks/ipfs-mock')
|
||||
|
||||
describe('#IPFS-adapter', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new IPFSLib()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#start', () => {
|
||||
it('should return a promise that resolves into an instance of IPFS.', async () => {
|
||||
// Mock dependencies.
|
||||
uut.IPFS = IPFSMock
|
||||
|
||||
const result = await uut.start()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(uut.isReady, true)
|
||||
|
||||
assert.property(result, 'config')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.IPFS, 'create').rejects(new Error('test error'))
|
||||
|
||||
await uut.start()
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
const assert = require('chai').assert
|
||||
const fs = require('fs')
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const JsonFiles = require('../../../src/adapters/json-files')
|
||||
|
||||
const JSON_FILE = 'test-json-file.json'
|
||||
const JSON_PATH = `${__dirname.toString()}/${JSON_FILE}`
|
||||
|
||||
const deleteFile = filepath => {
|
||||
try {
|
||||
// Delete state if exist
|
||||
fs.unlinkSync(filepath)
|
||||
} catch (error) {}
|
||||
}
|
||||
let sandbox
|
||||
let uut
|
||||
describe('JsonFiles', () => {
|
||||
const obj = {
|
||||
json: 'file'
|
||||
}
|
||||
beforeEach(() => {
|
||||
uut = new JsonFiles()
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
deleteFile(JSON_PATH)
|
||||
})
|
||||
describe('writeJSON()', () => {
|
||||
it('should throw error if inputs is not provided', async () => {
|
||||
try {
|
||||
await uut.writeJSON()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'obj property is required')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename property is not provided', async () => {
|
||||
try {
|
||||
await uut.writeJSON(obj)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename property is not string', async () => {
|
||||
try {
|
||||
await uut.writeJSON(obj, 1)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if fs library return an error', async () => {
|
||||
try {
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.fs, 'writeFile').yields(new Error('test error'))
|
||||
|
||||
await uut.writeJSON(obj, JSON_PATH)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should write a json file', async () => {
|
||||
try {
|
||||
await uut.writeJSON(obj, JSON_PATH)
|
||||
|
||||
assert.isTrue(fs.existsSync(JSON_PATH))
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('readJSON()', () => {
|
||||
it('should throw error if filename property is not provided', async () => {
|
||||
try {
|
||||
await uut.readJSON(obj)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename property is not string', async () => {
|
||||
try {
|
||||
await uut.readJSON(obj, 1)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if fs library return an error', async () => {
|
||||
try {
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.fs, 'readFile').yields(new Error('test error'))
|
||||
|
||||
await uut.readJSON(JSON_PATH)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should throw error if file not found', async () => {
|
||||
try {
|
||||
const testError = new Error('test error')
|
||||
testError.code = 'ENOENT'
|
||||
|
||||
sandbox.stub(uut.fs, 'readFile').yields(testError)
|
||||
|
||||
await uut.readJSON(JSON_PATH)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should read a json file', async () => {
|
||||
try {
|
||||
const result = await uut.readJSON(JSON_PATH)
|
||||
|
||||
const objKeys = Object.keys(obj)
|
||||
const resultKeys = Object.keys(result)
|
||||
|
||||
assert.isObject(result)
|
||||
assert.equal(objKeys.length, resultKeys.length)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,231 @@
|
||||
const assert = require('chai').assert
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
const LogsApiLib = require('../../../src/adapters/logapi')
|
||||
const mockData = require('../mocks/log-api-mock')
|
||||
|
||||
const context = {}
|
||||
let sandbox
|
||||
let uut
|
||||
describe('#LogsApiLib', () => {
|
||||
beforeEach(() => {
|
||||
uut = new LogsApiLib()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#getLogs()', () => {
|
||||
it('should return false if password is not provided', async () => {
|
||||
try {
|
||||
const result = await uut.getLogs()
|
||||
assert.property(result, 'success')
|
||||
assert.isFalse(result.success)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return log', async () => {
|
||||
try {
|
||||
const pass = 'test'
|
||||
const result = await uut.getLogs(pass)
|
||||
// console.log('result', result)
|
||||
|
||||
assert.isTrue(result.success)
|
||||
assert.isArray(result.data)
|
||||
assert.property(result.data[0], 'message')
|
||||
assert.property(result.data[0], 'level')
|
||||
assert.property(result.data[0], 'timestamp')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return false if files are not found!', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'generateFileName').resolves('bad router')
|
||||
|
||||
const password = 'test'
|
||||
|
||||
const result = await uut.getLogs(password)
|
||||
// console.log(result)
|
||||
|
||||
assert.isFalse(result.success)
|
||||
assert.include(result.data, 'file does not exist')
|
||||
} catch (err) {
|
||||
console.log('ERRROR', err)
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and handle errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.fs, 'existsSync').throws(new Error('test error'))
|
||||
const password = 'test'
|
||||
|
||||
await uut.getLogs(password)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw unhandled error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.fs, 'existsSync').throws(new Error('Unhandled error'))
|
||||
const password = 'test'
|
||||
|
||||
await uut.getLogs(password)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unhandled error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#filterLogs()', () => {
|
||||
it('should throw error if data is not provided', async () => {
|
||||
try {
|
||||
await uut.filterLogs()
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Data must be array')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if data provided is not an array', async () => {
|
||||
try {
|
||||
const data = 'data'
|
||||
await uut.filterLogs(data)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Data must be array')
|
||||
}
|
||||
})
|
||||
|
||||
it('should sort the log data', async () => {
|
||||
try {
|
||||
const data = mockData.data
|
||||
const result = await uut.filterLogs(data)
|
||||
assert.isArray(result)
|
||||
assert.property(result[1], 'message')
|
||||
assert.property(result[1], 'level')
|
||||
assert.property(result[1], 'timestamp')
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should sort the log data with a limit', async () => {
|
||||
try {
|
||||
const data = mockData.data
|
||||
const limit = 1
|
||||
const result = await uut.filterLogs(data, limit)
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, limit)
|
||||
assert.property(result[0], 'message')
|
||||
assert.property(result[0], 'level')
|
||||
assert.property(result[0], 'timestamp')
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#generateFileName()', () => {
|
||||
it('should return file name', async () => {
|
||||
try {
|
||||
const fileName = await uut.generateFileName()
|
||||
assert.isString(fileName)
|
||||
context.fileName = fileName
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if something fails', async () => {
|
||||
try {
|
||||
uut.config = null
|
||||
await uut.generateFileName()
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.exists(err)
|
||||
assert.isString(err.message)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#readLines()', () => {
|
||||
it('should throw error if fileName is not provided', async () => {
|
||||
try {
|
||||
await uut.readLines()
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'filename must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if fileName provided is not string', async () => {
|
||||
try {
|
||||
const fileName = true
|
||||
await uut.readLines(fileName)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'filename must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if the file does not exist', async () => {
|
||||
try {
|
||||
const fileName = 'test/logs/'
|
||||
await uut.readLines(fileName)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'file does not exist')
|
||||
}
|
||||
})
|
||||
|
||||
it('should ignore fileReader callback errors', async () => {
|
||||
try {
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.lineReader, 'eachLine').yieldsRight({}, true)
|
||||
|
||||
const fileName = context.fileName
|
||||
const result = await uut.readLines(fileName)
|
||||
assert.isArray(result)
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return data', async () => {
|
||||
try {
|
||||
const fileName = context.fileName
|
||||
const result = await uut.readLines(fileName)
|
||||
|
||||
assert.isArray(result)
|
||||
assert.property(result[1], 'message')
|
||||
assert.property(result[1], 'level')
|
||||
assert.property(result[1], 'timestamp')
|
||||
} catch (err) {
|
||||
assert.fail('Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
Unit tests for the nodemailer.js library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const NodeMailer = require('../../../src/adapters/nodemailer')
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
|
||||
describe('NodeMailer', () => {
|
||||
beforeEach(() => {
|
||||
uut = new NodeMailer()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('sendEmail()', () => {
|
||||
it('should throw error if email property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
formMessage: 'test msg',
|
||||
name: 'test name',
|
||||
subject: 'test subject',
|
||||
to: ['test2@email.com']
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if formMessage property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
name: 'test name',
|
||||
subject: 'test subject',
|
||||
to: ['test2@email.com']
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'formMessage' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if <to> property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
name: 'test name',
|
||||
subject: 'test subject'
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'to' must be a array!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if <to> is wrong type', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
formMessage: 'test msg',
|
||||
name: 'test name',
|
||||
subject: 'test subject',
|
||||
to: 'test'
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'to' must be a array!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if subject Property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
formMessage: 'test msg',
|
||||
name: 'test name',
|
||||
to: ['test2@email.com']
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'subject' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email with default html data', async () => {
|
||||
try {
|
||||
sandbox
|
||||
.stub(uut.transporter, 'sendMail')
|
||||
.resolves({ messageId: 'messageId' })
|
||||
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
formMessage: 'test msg',
|
||||
to: ['test2@email.com'],
|
||||
subject: 'test subject'
|
||||
}
|
||||
|
||||
const info = await uut.sendEmail(data)
|
||||
|
||||
assert.isObject(info)
|
||||
assert.isString(info.messageId)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email if htmlData is provided', async () => {
|
||||
try {
|
||||
sandbox
|
||||
.stub(uut.transporter, 'sendMail')
|
||||
.resolves({ messageId: 'messageId' })
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
formMessage: 'test msg',
|
||||
name: 'test name',
|
||||
to: ['test2@email.com'],
|
||||
subject: 'test subject',
|
||||
htmlData: '<p> Unit test </p>'
|
||||
}
|
||||
const info = await uut.sendEmail(data)
|
||||
assert.isObject(info)
|
||||
assert.isString(info.messageId)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEmailArray()', () => {
|
||||
it('should throw error if email list is not provided ', async () => {
|
||||
try {
|
||||
await uut.validateEmailArray()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'emailList' must be a array!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if email list is empty', async () => {
|
||||
try {
|
||||
const emailList = []
|
||||
await uut.validateEmailArray(emailList)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'emailList' cant be empty!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true ', async () => {
|
||||
try {
|
||||
const emailList = ['test@email.com', 'simple@email.com']
|
||||
const result = await uut.validateEmailArray(emailList)
|
||||
assert.isTrue(result)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHtmlFromObject()', () => {
|
||||
it('should throw error if the input is not provided ', async () => {
|
||||
try {
|
||||
await uut.getHtmlFromObject()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'objectData' must be a object!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if the object is empty', async () => {
|
||||
try {
|
||||
await uut.getHtmlFromObject({})
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'subject' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if "formMessage" property is not provided', async () => {
|
||||
try {
|
||||
const obj = {
|
||||
subject: 'unit'
|
||||
}
|
||||
await uut.getHtmlFromObject(obj)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'formMessage' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should return the html', async () => {
|
||||
try {
|
||||
const obj = {
|
||||
subject: 'unit ',
|
||||
formMessage: 'test',
|
||||
value1: 'value1',
|
||||
value2: 'value2',
|
||||
value3: 'value3'
|
||||
}
|
||||
const result = await uut.getHtmlFromObject(obj)
|
||||
assert.isString(result)
|
||||
assert.include(result, '<p>', 'expect html tag')
|
||||
assert.include(result, '</p>', 'expect html tag')
|
||||
assert.include(
|
||||
result,
|
||||
'value1',
|
||||
'Expect value 1 is included in the html'
|
||||
)
|
||||
assert.include(result, 'value2', 'expect is included in the html')
|
||||
assert.include(result, 'value3', 'expect is included in the html')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
const assert = require('chai').assert
|
||||
const PassportLib = require('../../../src/adapters/passport')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
describe('#passport.js', () => {
|
||||
beforeEach(() => {
|
||||
uut = new PassportLib()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('authUser()', () => {
|
||||
it('should throw error if ctx is not provided', async () => {
|
||||
try {
|
||||
await uut.authUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'ctx is required')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should throw error if the passport library fails', async () => {
|
||||
try {
|
||||
const error = new Error('cant auth user')
|
||||
const user = null
|
||||
|
||||
// Mock calls
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.passport, 'authenticate').yields(error, user)
|
||||
|
||||
const ctx = {}
|
||||
await uut.authUser(ctx)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'cant auth user')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
Unit tests for the users Mongoose model.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Set the environment variable to signal this is a test.
|
||||
process.env.SVC_ENV = 'test'
|
||||
|
||||
const User = require('../../../src/adapters/localdb/models/users')
|
||||
const config = require('../../../config')
|
||||
|
||||
describe('#User-Adapter', () => {
|
||||
// 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
|
||||
})
|
||||
|
||||
testuser = new User({
|
||||
email: 'test983@test.com',
|
||||
name: 'test983',
|
||||
password: 'password'
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(async () => {
|
||||
await testuser.remove()
|
||||
|
||||
mongoose.connection.close()
|
||||
})
|
||||
|
||||
describe('#save', () => {
|
||||
it('should replace the password with a salt', async () => {
|
||||
await testuser.save()
|
||||
// console.log('testuser: ', testuser)
|
||||
|
||||
assert.notEqual(testuser.password, 'password')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#validatePassword', () => {
|
||||
it('should return true when password matches', async () => {
|
||||
const result = await testuser.validatePassword('password')
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return false when password does not match', async () => {
|
||||
const result = await testuser.validatePassword('wrongpassword')
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#generateToken', () => {
|
||||
it('should generate a JWT token', () => {
|
||||
const token = testuser.generateToken()
|
||||
// console.log('token: ', token)
|
||||
|
||||
assert.include(token, 'eyJ')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
// const assert = require('chai').assert
|
||||
const {
|
||||
notifyRotation,
|
||||
outputToConsole
|
||||
} = require('../../../src/adapters/wlogger')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
// let uut
|
||||
let sandbox
|
||||
|
||||
describe('#wlogger.js', () => {
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#notifyRotation', () => {
|
||||
it('should notify of a log rotation', () => {
|
||||
notifyRotation()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#envronment', () => {
|
||||
it('should write to console in non-test environment', () => {
|
||||
outputToConsole()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Unit tests for controllers index.js file.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
// const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const adapters = require('../../../src/adapters')
|
||||
const { attachControllers } = require('../../../src/controllers')
|
||||
|
||||
describe('#Controllers', () => {
|
||||
// let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#attachControllers', () => {
|
||||
it('should attach the controllers', async () => {
|
||||
// mock IPFS
|
||||
sandbox.stub(adapters.ipfs, 'start').resolves({})
|
||||
adapters.ipfs.ipfsCoordAdapter = {
|
||||
attachRPCRouter: () => {}
|
||||
}
|
||||
|
||||
const app = {
|
||||
use: () => {}
|
||||
}
|
||||
|
||||
await attachControllers(app)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
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/controllers/json-rpc')
|
||||
const adapters = require('../../mocks/adapters')
|
||||
const UseCasesMock = require('../../mocks/use-cases')
|
||||
|
||||
describe('#JSON RPC', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new JSONRPC({ adapters, useCases })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new JSONRPC()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating JSON RPC Controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new JSONRPC({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating JSON RPC Controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('should route to auth handler', async () => {
|
||||
const id = uid()
|
||||
const userCall = jsonrpc.request(id, 'auth', { endpoint: 'getAll' })
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
|
||||
// Mock the controller.
|
||||
sandbox.stub(uut.authController, 'authRouter').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, 'auth')
|
||||
assert.equal(obj.id, id)
|
||||
})
|
||||
|
||||
it('should route to about handler', async () => {
|
||||
const id = uid()
|
||||
const userCall = jsonrpc.request(id, 'about', { endpoint: 'getAll' })
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
|
||||
// Mock the controller.
|
||||
sandbox.stub(uut.aboutController, 'aboutRouter').resolves('true')
|
||||
|
||||
// Force ipfs-coord communication.
|
||||
uut.ipfsCoord.ipfs = {
|
||||
orbitdb: {
|
||||
sendToDb: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
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, 'about')
|
||||
assert.equal(obj.id, id)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
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 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 Validators = require('../../../../src/controllers/json-rpc/validators')
|
||||
const adapters = require('../../mocks/adapters')
|
||||
|
||||
describe('#validators', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new Validators({ adapters })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters is not passed in.', () => {
|
||||
try {
|
||||
uut = new Validators()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating JSON RPC Validators library.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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: 'fakeJWTToken'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
// Mock external dependencies.
|
||||
sandbox.stub(uut.jwt, 'verify').returns(true)
|
||||
sandbox.stub(uut.UserModel, 'findById').resolves(true)
|
||||
|
||||
const user = await uut.ensureUser(rpcData)
|
||||
// console.log('user: ', user)
|
||||
|
||||
// For this test, we return a value of 'true' instead of actual user data.
|
||||
assert.equal(user, true)
|
||||
})
|
||||
|
||||
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)
|
||||
sandbox.stub(uut.jwt, 'verify').returns(true)
|
||||
|
||||
// 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: 'fakeJWTToken'
|
||||
})
|
||||
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: 'fakeJWTToken',
|
||||
userId: 'abc123'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
// Mock external dependencies.
|
||||
sandbox.stub(uut.jwt, 'verify').returns(true)
|
||||
sandbox.stub(uut.UserModel, 'findById').resolves({ _id: 'abc123' })
|
||||
|
||||
const user = await uut.ensureTargetUserOrAdmin(rpcData)
|
||||
// console.log('user: ', user)
|
||||
|
||||
// Assert that the mocked data expected is returned.
|
||||
assert.equal(user._id, 'abc123')
|
||||
})
|
||||
|
||||
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: 'fakeJWTToken'
|
||||
})
|
||||
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: 'abc123'
|
||||
})
|
||||
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 {
|
||||
// Mock external dependencies.
|
||||
sandbox.stub(uut.jwt, 'verify').returns(true)
|
||||
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: 'deleteUser',
|
||||
apiToken: 'fakeJWTToken',
|
||||
userId: 'abc123'
|
||||
})
|
||||
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, 'User not found!')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if JWT is from a different user', async () => {
|
||||
try {
|
||||
// Mock external dependencies.
|
||||
sandbox.stub(uut.jwt, 'verify').returns(true)
|
||||
sandbox.stub(uut.UserModel, 'findById').resolves({ _id: 'badId' })
|
||||
|
||||
// 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: 'fakeJWTToken',
|
||||
userId: 'abc123'
|
||||
})
|
||||
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, 'User is neither admin nor target user.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return true if user is an admin', 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: 'fakeJWTToken',
|
||||
userId: 'abc123'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
// Mock external dependencies.
|
||||
sandbox.stub(uut.jwt, 'verify').returns(true)
|
||||
sandbox
|
||||
.stub(uut.UserModel, 'findById')
|
||||
.resolves({ _id: 'abc123', type: 'admin' })
|
||||
|
||||
const user = await uut.ensureTargetUserOrAdmin(rpcData)
|
||||
// console.log('user: ', user)
|
||||
|
||||
// Assert that the mocked data expected is returned.
|
||||
assert.equal(user, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
Unit tests for the JSON RPC validator middleware.
|
||||
|
||||
TODO: ensureTargetUserOrAdmin: it should exit quietly if user is an admin.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const sinon = require('sinon')
|
||||
const assert = require('chai').assert
|
||||
|
||||
// Set the environment variable to signal this is a test.
|
||||
process.env.SVC_ENV = 'test'
|
||||
|
||||
// Local libraries
|
||||
const RateLimit = require('../../../../src/controllers/json-rpc/rate-limit')
|
||||
|
||||
describe('#rate-limit', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new RateLimit()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should use the options provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
interval: { min: 10 },
|
||||
delayAfter: 1,
|
||||
timeWait: { sec: 5 },
|
||||
max: 2,
|
||||
onLimitReached: () => {
|
||||
throw new Error('custom message error')
|
||||
}
|
||||
}
|
||||
const _uut = new RateLimit(options)
|
||||
|
||||
// Assert options
|
||||
assert.equal(_uut.rateLimitOptions.interval.min, options.interval.min)
|
||||
assert.equal(_uut.rateLimitOptions.delayAfter, options.delayAfter)
|
||||
assert.equal(_uut.rateLimitOptions.timeWait.sec, options.timeWait.sec)
|
||||
|
||||
const from = 'constructor test'
|
||||
const firstRequest = await _uut.limiter(from)
|
||||
assert.isTrue(firstRequest)
|
||||
|
||||
const secondRequest = await _uut.limiter(from)
|
||||
assert.isTrue(secondRequest)
|
||||
|
||||
await _uut.limiter(from)
|
||||
assert.fail('unexpected error')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'custom message error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#onLimitReached', () => {
|
||||
it('should throw error', async () => {
|
||||
try {
|
||||
uut.onLimitReached()
|
||||
assert.fail('unexpected error')
|
||||
} catch (error) {
|
||||
assert.equal(error.status, 429)
|
||||
assert.include(
|
||||
error.message,
|
||||
'Too many requests, please try again later.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#limiter', () => {
|
||||
it('should throw error if "from" input is not provider', async () => {
|
||||
try {
|
||||
await uut.limiter()
|
||||
assert.fail('unexpected error')
|
||||
} catch (error) {
|
||||
assert.include(error.message, 'from must be a string')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error 429', async () => {
|
||||
try {
|
||||
const _uut = new RateLimit({ max: 1 })
|
||||
const from = 'Origin request'
|
||||
|
||||
const firstRequest = await _uut.limiter(from)
|
||||
assert.isTrue(firstRequest)
|
||||
|
||||
const secondRequest = await _uut.limiter(from)
|
||||
assert.isTrue(secondRequest)
|
||||
|
||||
await _uut.limiter(from)
|
||||
assert.fail('unexpected error')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'Too many requests, please try again later.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Unit tests for the json-rpc/about/index.js file.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const sinon = require('sinon')
|
||||
const assert = require('chai').assert
|
||||
|
||||
// Local libraries
|
||||
const AboutRPC = require('../../../../src/controllers/json-rpc/about')
|
||||
|
||||
describe('#AboutRPC', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new AboutRPC()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#aboutRouter', () => {
|
||||
it('should return information about the service', async () => {
|
||||
const result = await uut.aboutRouter()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'success')
|
||||
assert.equal(result.success, true)
|
||||
assert.property(result, 'status')
|
||||
assert.equal(result.status, 200)
|
||||
assert.property(result, 'message')
|
||||
assert.property(result, 'endpoint')
|
||||
assert.equal(result.endpoint, 'about')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
Unit tests for the json-rpc/auth/index.js file.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
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 AuthRPC = require('../../../../src/controllers/json-rpc/auth')
|
||||
const RateLimit = require('../../../../src/controllers/json-rpc/rate-limit')
|
||||
const adapters = require('../../mocks/adapters')
|
||||
const UseCasesMock = require('../../mocks/use-cases')
|
||||
|
||||
describe('#AuthRPC', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
const useCases = new UseCasesMock()
|
||||
|
||||
uut = new AuthRPC({ adapters, useCases })
|
||||
uut.rateLimit = new RateLimit({ max: 100 })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new AuthRPC()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating Auth JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new AuthRPC({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating Auth JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
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)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
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)
|
||||
|
||||
// Force an error.
|
||||
sandbox
|
||||
.stub(uut.userLib, 'authUser')
|
||||
.rejects(new Error('Login credential do not match'))
|
||||
|
||||
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,412 @@
|
||||
/*
|
||||
Unit tests for the rpc/users/index.js file.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
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 UserRPC = require('../../../../src/controllers/json-rpc/users')
|
||||
const adapters = require('../../mocks/adapters')
|
||||
const UseCasesMock = require('../../mocks/use-cases')
|
||||
|
||||
describe('#UserRPC', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
let testUser
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
const useCases = new UseCasesMock()
|
||||
|
||||
uut = new UserRPC({ adapters, useCases })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UserRPC()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating User JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UserRPC({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating User JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
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)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
// Force middleware to pass.
|
||||
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
|
||||
|
||||
const result = await uut.userRouter(rpcData)
|
||||
// console.log('result', result)
|
||||
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: 'fakeJWTToken',
|
||||
userId: 'abc123'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
// Force middleware to pass.
|
||||
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
|
||||
|
||||
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)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
// Force middleware to pass.
|
||||
sandbox.stub(uut.validators, 'ensureUser').resolves(true)
|
||||
|
||||
const result = await uut.userRouter(rpcData)
|
||||
// console.log('result: ', result)
|
||||
|
||||
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: 'fakeJWTToken',
|
||||
userId: 'abc123'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
// Force middleware to pass.
|
||||
sandbox.stub(uut.validators, 'ensureTargetUserOrAdmin').resolves(true)
|
||||
|
||||
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)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
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')
|
||||
|
||||
// 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(),
|
||||
userId: 'abc123',
|
||||
name: 'test777'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const result = await uut.updateUser(rpcData, {})
|
||||
// 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: 'abc123'
|
||||
})
|
||||
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({}, {})
|
||||
// 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:
|
||||
sandbox
|
||||
.stub(uut.userLib, 'deleteUser')
|
||||
.rejects(new Error('Cannot read property'))
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
# REST API Unit Tests
|
||||
|
||||
The tests in this directory are unit tests of REST API. These tests are not
|
||||
concerned with the business logic behind the endpoints. They are only concerned
|
||||
with the handling of the REST API endpoint. These tests answer questions like:
|
||||
|
||||
- Is the endpoint responding properly when the business logic throws an error?
|
||||
- When returning an error, is it returning the proper HTTP response?
|
||||
- When returning success, is it returning the correct payload?
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const AuthRESTController = require('../../../../../src/controllers/rest-api/auth/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Auth-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new AuthRESTController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new AuthRESTController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating Auth REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new AuthRESTController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating Auth REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#authUser', () => {
|
||||
it('should authorize a user', async () => {
|
||||
// Mock dependencies
|
||||
const user = {
|
||||
toJSON: () => {
|
||||
return { password: 'password' }
|
||||
},
|
||||
generateToken: () => {}
|
||||
}
|
||||
sandbox.stub(uut.passport, 'authUser').resolves(user)
|
||||
|
||||
await uut.authUser(ctx)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.passport, 'authUser').rejects('test error')
|
||||
|
||||
await uut.authUser(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const AuthRouter = require('../../../../../src/controllers/rest-api/auth')
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Auth-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new AuthRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new AuthRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new AuthRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attached REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const ContactController = require('../../../../../src/controllers/rest-api/contact/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Contact', () => {
|
||||
before(async () => {})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new ContactController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#POST /contact', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
await uut.email(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
sandbox.stub(uut.contactLib, 'sendEmail').resolves(true)
|
||||
|
||||
ctx.request.body = {
|
||||
email: 'test02@test.com',
|
||||
formMessage: 'test'
|
||||
}
|
||||
|
||||
await uut.email(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'success')
|
||||
assert.isTrue(ctx.response.body.success)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should pass an error message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'Unprocessable Entity'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
|
||||
it('should still throw error if there is no message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 404
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const ContactRouter = require('../../../../../src/controllers/rest-api/contact')
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Contact-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new ContactRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new ContactRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating Contact REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new ContactRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating Contact REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const LogsApiController = require('../../../../../src/controllers/rest-api/logs/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Logapi', () => {
|
||||
before(async () => {})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new LogsApiController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#POST /logapi', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 500 status on biz logic Unhandled error', async () => {
|
||||
try {
|
||||
// eslint-disable
|
||||
sandbox
|
||||
.stub(uut.logsApiLib, 'getLogs')
|
||||
.returns(Promise.reject(new Error()))
|
||||
|
||||
ctx.request.body = {
|
||||
password: 'test'
|
||||
}
|
||||
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 500)
|
||||
assert.include(err.message, 'Unhandled error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.logsApiLib, 'getLogs').resolves({})
|
||||
|
||||
ctx.request.body = {
|
||||
password: 'test'
|
||||
}
|
||||
|
||||
await uut.getLogs(ctx)
|
||||
|
||||
assert.isOk(ctx.body)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const LogsRouter = require('../../../../../src/controllers/rest-api/logs')
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Contact-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new LogsRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new LogsRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating Logs REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new LogsRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating Logs REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Unit tests for the REST API controllers/rest-api/index.js library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local libraries
|
||||
const RESTControllers = require('../../../../src/controllers/rest-api/')
|
||||
// const mockContext = require('../../../unit/mocks/ctx-mock').context
|
||||
const adapters = require('../../mocks/adapters')
|
||||
const UseCasesMock = require('../../mocks/use-cases')
|
||||
|
||||
describe('#RESTControllers', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
before(async () => {})
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new RESTControllers({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new RESTControllers()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating REST Controller libraries.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new RESTControllers({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
|
||||
// use to prevent complaints from linter.
|
||||
console.log('uut: ', uut)
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating REST Controller libraries.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
|
||||
const UserController = require('../../../../../src/controllers/rest-api/users/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Users-REST-Controller', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UserController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UserController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating /users REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UserController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating /users REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#POST /users', () => {
|
||||
it('should return 422 status on biz logic error', async () => {
|
||||
try {
|
||||
await uut.createUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
ctx.request.body = {
|
||||
user: {
|
||||
email: 'test02@test.com',
|
||||
password: 'test',
|
||||
name: 'test02'
|
||||
}
|
||||
}
|
||||
|
||||
await uut.createUser(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'user')
|
||||
assert.property(ctx.response.body, 'token')
|
||||
|
||||
// Used by downstream tests.
|
||||
// testUser = ctx.response.body.user
|
||||
// console.log('testUser: ', testUser)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should return 422 status on arbitrary biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.useCases.user, 'getAllUsers')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
await uut.getUsers(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
await uut.getUsers(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'users')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users/:id', () => {
|
||||
it('should return 422 status on arbitrary biz logic error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.useCases.user, 'getUser')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
await uut.getUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.useCases.user, 'getUser').resolves({ _id: '123' })
|
||||
|
||||
await uut.getUser(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'user')
|
||||
})
|
||||
|
||||
it('should return other error status passed by biz logic', async () => {
|
||||
try {
|
||||
// Mock dependencies
|
||||
const testErr = new Error('test error')
|
||||
testErr.status = 404
|
||||
sandbox.stub(uut.useCases.user, 'getUser').rejects(testErr)
|
||||
|
||||
await uut.getUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.status, 404)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /users/:id', () => {
|
||||
it('should return 422 if no input data given', async () => {
|
||||
try {
|
||||
await uut.updateUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 on success', async () => {
|
||||
// Prep the testUser data.
|
||||
// console.log('testUser: ', testUser)
|
||||
// testUser.password = 'password'
|
||||
// delete testUser.type
|
||||
|
||||
// Replace the testUser variable with an actual model from the DB.
|
||||
// const existingUser = await User.findById(testUser._id)
|
||||
|
||||
ctx.body = {
|
||||
user: {}
|
||||
}
|
||||
ctx.request.body = {
|
||||
user: {}
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.useCases.user, 'updateUser').resolves({})
|
||||
|
||||
await uut.updateUser(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
|
||||
// Assert that expected properties exist in the returned data.
|
||||
assert.property(ctx.response.body, 'user')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
it('should return 422 if no input data given', async () => {
|
||||
try {
|
||||
await uut.deleteUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 200 status on success', async () => {
|
||||
// Replace the testUser variable with an actual model from the DB.
|
||||
const existingUser = {}
|
||||
|
||||
ctx.body = {
|
||||
user: existingUser
|
||||
}
|
||||
|
||||
await uut.deleteUser(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should still throw error if there is no message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 404
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const UserRouter = require('../../../../../src/controllers/rest-api/users')
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#Users-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new UserRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new UserRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new UserRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating PostEntry REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Unit tests for the User entity library.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const User = require('../../../src/entities/user')
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
|
||||
describe('#User-Entity', () => {
|
||||
before(async () => {})
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new User()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#validate', () => {
|
||||
it('should throw an error if email is not provided', () => {
|
||||
try {
|
||||
uut.validate()
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if password is not provided', () => {
|
||||
try {
|
||||
uut.validate({ email: 'test@test.com' })
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'password' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if name is not provided', () => {
|
||||
try {
|
||||
uut.validate({ email: 'test@test.com', password: 'test' })
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'name' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should return a User object', () => {
|
||||
const inputData = {
|
||||
email: 'test@test.com',
|
||||
password: 'test',
|
||||
name: 'test'
|
||||
}
|
||||
|
||||
const entry = uut.validate(inputData)
|
||||
// console.log('entry: ', entry)
|
||||
|
||||
assert.property(entry, 'email')
|
||||
assert.equal(entry.email, inputData.email)
|
||||
|
||||
assert.property(entry, 'password')
|
||||
assert.equal(entry.password, inputData.password)
|
||||
|
||||
assert.property(entry, 'name')
|
||||
assert.equal(entry.name, inputData.name)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Unit tests for the passport library.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
// const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local libraries
|
||||
const User = require('../../../src/adapters/localdb/models/users')
|
||||
const { passport, passportCallback } = require('../../../config/passport')
|
||||
const adaptersMock = require('../mocks/adapters')
|
||||
|
||||
describe('#passport', () => {
|
||||
let sandbox
|
||||
let id
|
||||
let done
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
id = 'abc123'
|
||||
done = () => {}
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#serializeUser', () => {
|
||||
it('should serialize a user', () => {
|
||||
const user = {
|
||||
id: 'abc123'
|
||||
}
|
||||
const done = () => {}
|
||||
|
||||
passport.serializeUser(user, done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#deserializeUser', () => {
|
||||
it('should deserialize a user', () => {
|
||||
// Mock Users model.
|
||||
sandbox.stub(User, 'findById').resolves({ id })
|
||||
|
||||
passport.deserializeUser(id, done)
|
||||
})
|
||||
|
||||
it('should catch and handle errors', () => {
|
||||
// Force an error
|
||||
sandbox.stub(User, 'findById').rejects(new Error('test error'))
|
||||
|
||||
passport.deserializeUser(id, done)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#passportCallback', () => {
|
||||
it('should return if user is found', () => {
|
||||
// Mock Users model.
|
||||
sandbox.stub(User, 'findOne').resolves({ id })
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
|
||||
it('should return if password is validated', () => {
|
||||
// Mock Users model.
|
||||
sandbox.stub(User, 'findOne').resolves(new adaptersMock.localdb.Users())
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
|
||||
it('should catch a high-level error', () => {
|
||||
// Force an error
|
||||
sandbox.stub(User, 'findOne').rejects(new Error('test error'))
|
||||
|
||||
passportCallback(id, 'password', done)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Mocks for the Adapter library.
|
||||
*/
|
||||
|
||||
const ipfs = {
|
||||
ipfsAdapter: {
|
||||
ipfs: {}
|
||||
},
|
||||
ipfsCoordAdapter: {
|
||||
ipfsCoord: {}
|
||||
}
|
||||
}
|
||||
|
||||
const localdb = {
|
||||
Users: class Users {
|
||||
static findById () {}
|
||||
static find () {}
|
||||
static findOne () {
|
||||
return {
|
||||
validatePassword: localdb.validatePassword
|
||||
}
|
||||
}
|
||||
|
||||
async save () {
|
||||
return {}
|
||||
}
|
||||
|
||||
generateToken () {
|
||||
return '123'
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return {}
|
||||
}
|
||||
|
||||
async remove () {
|
||||
return true
|
||||
}
|
||||
|
||||
async validatePassword () {
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
validatePassword: () => {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ipfs, localdb }
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Mocks for Koa 'app' object.
|
||||
*/
|
||||
|
||||
const app = {
|
||||
use: () => {}
|
||||
}
|
||||
|
||||
module.exports = app
|
||||
@@ -0,0 +1,50 @@
|
||||
// Ripped from https://github.com/koajs/koa/blob/master/test/helpers/context.js
|
||||
// Solution courtesy of user @fl0w. See: https://github.com/koajs/koa/issues/999#issuecomment-309270599
|
||||
// Take from this gist: https://gist.github.com/emmanuelnk/f1254eed8f947a81e8d715476d9cc92c
|
||||
|
||||
// if you want more comprehensive Koa Context object to test stuff like Cookies etc
|
||||
// then use https://www.npmjs.com/package/@shopify/jest-koa-mocks (requires Jest)
|
||||
|
||||
// INSTRUCTIONS:
|
||||
// Import in test file as below:
|
||||
//
|
||||
// const mockContext = require('./mocks/ctx-mock').context
|
||||
// const ctx = mockContext()
|
||||
// ...
|
||||
|
||||
const Stream = require('stream')
|
||||
const Koa = require('koa')
|
||||
|
||||
const context = (req, res, app) => {
|
||||
const socket = new Stream.Duplex()
|
||||
|
||||
req = Object.assign(
|
||||
{ headers: {}, socket },
|
||||
Stream.Readable.prototype,
|
||||
req || {}
|
||||
)
|
||||
res = Object.assign(
|
||||
{ _headers: {}, socket },
|
||||
Stream.Writable.prototype,
|
||||
res || {}
|
||||
)
|
||||
req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1'
|
||||
app = app || new Koa()
|
||||
res.getHeader = k => res._headers[k.toLowerCase()]
|
||||
res.setHeader = (k, v) => (res._headers[k.toLowerCase()] = v)
|
||||
res.removeHeader = (k, v) => delete res._headers[k.toLowerCase()]
|
||||
|
||||
const retApp = app.createContext(req, res)
|
||||
|
||||
return retApp
|
||||
}
|
||||
|
||||
const request = (req, res, app) => context(req, res, app).request
|
||||
|
||||
const response = (req, res, app) => context(req, res, app).response
|
||||
|
||||
module.exports = {
|
||||
context,
|
||||
request,
|
||||
response
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Mocks for the ipfs-coord library
|
||||
*/
|
||||
|
||||
class IPFSCoord {
|
||||
async isReady () {
|
||||
return true
|
||||
}
|
||||
|
||||
async start () {}
|
||||
}
|
||||
|
||||
module.exports = IPFSCoord
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Mocks for the js-ipfs
|
||||
*/
|
||||
|
||||
class IPFS {
|
||||
constructor () {
|
||||
this.ipfs = {}
|
||||
}
|
||||
|
||||
static create () {
|
||||
const mockIpfs = new MockIpfsInstance()
|
||||
|
||||
return mockIpfs
|
||||
}
|
||||
|
||||
async start () {}
|
||||
}
|
||||
|
||||
class MockIpfsInstance {
|
||||
constructor () {
|
||||
this.config = {
|
||||
profiles: {
|
||||
apply: () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stop () {}
|
||||
}
|
||||
|
||||
module.exports = IPFS
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user