forked from ipfs-service-provider

This commit is contained in:
Chris Troutner
2021-11-23 08:38:01 -08:00
commit 8f7f3896a3
115 changed files with 58101 additions and 0 deletions
+30
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "standard",
"env": {
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 8
}
}
+69
View File
@@ -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
.ipfsdata
!README.md
+8
View File
@@ -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.
+108
View File
@@ -0,0 +1,108 @@
# ipfs-service-provider
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
This is a 'boilerplate' repository. It's intended to be forked to start new projects. This repository has been forked from the [koa-api-boilerplate](https://github.com/christroutner/koa-api-boilerplate). It has all the same features as that boilerplate:
- [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
### Development Environment
A development environment will allow you modify the code on-the-fly and contribute to the code base of this repository. [PM2](https://www.npmjs.com/package/pm2) is recommended for running this code base as an IPFS Circuit Relay.
- [Video: Installing ipfs-service-provider](https://youtu.be/Z0NsboIVN44)
- [Step-by-step installation instructions](https://gist.github.com/christroutner/3304a71d4c12a3a3e1664a438f64d9d0)
```bash
git clone https://github.com/Permissionless-Software-Foundation/ipfs-service-provider
cd ipfs-service-provider
./install-mongo-sh
sudo npm install -g node-pre-gyp
npm install
./ipfs-service-provider.sh
```
### Production Environment
The [docker](./production/docker) directory contains a Dockerfile for building a production deployment. However, there is currently [a bug](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider/issues/38) preventing the Docker container from being used as a Circuit Relay.
```
docker-compose build --no-cache
docker-compose up -d
```
### Operation Notes
- There is a memory leak in the version of js-ipfs. The app is currently configured to shut down every 8 hours to flush memory. It relies on a process manager like pm2, Docker, or systemd to restart the app after it shuts down, in order to ensure continuous operation.
## 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)
+4
View File
@@ -0,0 +1,4 @@
{
"sampleUrl": null
}
+111
View File
@@ -0,0 +1,111 @@
/*
This Koa server has two interfaces:
- REST API over HTTP
- JSON RPC over IPFS
The architecture of the code follows the Clean Architecture pattern:
https://troutsblog.com/blog/clean-architecture
*/
// 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 AdminLib = require('../src/adapters/admin')
const errorMiddleware = require('../src/controllers/rest-api/middleware/error')
const { wlogger } = require('../src/adapters/wlogger')
class Server {
constructor () {
this.adminLib = new AdminLib()
}
async startServer () {
try {
// 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.
console.log(
`Connecting to MongoDB with this connection string: ${config.database}`
)
await mongoose.connect(config.database, {
useUnifiedTopology: true,
useNewUrlParser: true
})
console.log(`Starting environment: ${config.env}`)
console.log(`Debug level: ${config.debugLevel}`)
// 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')
const controllers = new Controllers()
await controllers.attachRESTControllers(app)
app.controllers = controllers
// 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 this.adminLib.createSystemUser()
if (success) console.log('System admin user created.')
// Attach the other IPFS controllers
await controllers.attachControllers(app)
// ipfs-coord has a memory leak. This app shuts down after 4 hours. It
// expects to be run by Docker or pm2, which can automatically restart
// the app.
setTimeout(function () {
process.exit(0)
}, 60000 * 60 * 2) // 2 hours
return app
} catch (err) {
console.error('Could not start server. Error: ', err)
}
}
}
module.exports = Server
+82
View File
@@ -0,0 +1,82 @@
/*
This file is used to store unsecure, application-specific data common to all
environments.
*/
/* eslint no-unneeded-ternary:0 */
// Get the version from the package.json file.
const pkgInfo = require('../../package.json')
const version = pkgInfo.version
const ipfsCoordName = process.env.COORD_NAME
? process.env.COORD_NAME
: 'ipfs-bch-wallet-service'
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',
// FullStack.cash account information, used for automatic JWT handling.
getJwtAtStartup: process.env.GET_JWT_AT_STARTUP ? true : false,
authServer: process.env.AUTHSERVER
? process.env.AUTHSERVER
: 'https://auth.fullstack.cash',
apiServer: process.env.APISERVER
? process.env.APISERVER
: 'https://api.fullstack.cash/v5/',
fullstackLogin: process.env.FULLSTACKLOGIN
? process.env.FULLSTACKLOGIN
: 'demo@demo.com',
fullstackPassword: process.env.FULLSTACKPASS
? process.env.FULLSTACKPASS
: 'demo',
// IPFS settings.
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
// SSL domain used for websocket connection via browsers.
crDomain: process.env.CR_DOMAIN ? process.env.CR_DOMAIN : '',
// 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: ipfsCoordName,
version,
protocol: 'generic-service',
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'
}
},
// IPFS Ports
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 4001,
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 4003,
// BCH Mnemonic for generating encryption keys and payment address
mnemonic: process.env.MNEMONIC ? process.env.MNEMONIC : '',
debugLevel: process.env.DEBUG_LEVEL ? parseInt(process.env.DEBUG_LEVEL) : 2
}
+12
View File
@@ -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'
}
+18
View File
@@ -0,0 +1,18 @@
/*
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',
database: process.env.DBURL
? process.env.DBURL
: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
env: 'prod'
}
+12
View File
@@ -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'
}
+6
View File
@@ -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)
+149
View File
@@ -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>
+53
View File
@@ -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 }
+15
View File
@@ -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>"}}`
+4
View File
@@ -0,0 +1,4 @@
const Server = require('./bin/server.js')
const server = new Server()
server.startServer()
+7
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
This directory will hold the Winston daily logs. Any files saved to this directory will be ignored by Git.
+47074
View File
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
{
"name": "ipfs-service-provider",
"version": "1.3.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": {
"@chris.troutner/ipfs": "2.0.2",
"@psf/bch-js": "4.20.20",
"axios": "0.21.1",
"bcryptjs": "2.4.3",
"glob": "7.1.6",
"ipfs-coord": "6.7.4",
"jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1",
"jwt-bch-lib": "1.3.0",
"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",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.4.17",
"passport-local": "1.0.0",
"public-ip": "4.0.4",
"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"
]
}
}
+75
View File
@@ -0,0 +1,75 @@
# Create a Dockerized API server
#
#IMAGE BUILD COMMANDS
# ct-base-ubuntu = ubuntu 18.04 + nodejs v10 LTS
#FROM christroutner/ct-base-ubuntu
FROM ubuntu:20.04
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
#Update the OS and install any OS packages needed.
RUN apt-get update
RUN apt-get install -y sudo git curl nano gnupg wget
#Install Node and NPM
RUN curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
RUN bash nodesource_setup.sh
RUN apt-get install -y nodejs build-essential
#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'"
# Update to the latest version of npm.
# Working with npm@7.21.1
RUN npm install -g npm@7.23.0
# npm mirror to prevent direct dependency on npm.
RUN npm set registry http://94.130.170.209:4873/
# 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/Permissionless-Software-Foundation/ipfs-service-provider
# Switch to the desired branch. `master` is usually stable,
# and `stage` has the most up-to-date changes.
WORKDIR /home/safeuser/ipfs-service-provider
# For development: switch to unstable branch
RUN git checkout ct-unstable
# Install dependencies
#RUN mkdir .ipfsdata
#RUN npm install -g @mapbox/node-pre-gyp
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.sh start-production.sh
CMD ["./start-production.sh"]
#CMD ["npm", "start"]
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Remove all untagged docker images.
docker rmi $(docker images | grep "^<none>" | awk '{print $3}')
+33
View File
@@ -0,0 +1,33 @@
# Start the service with the command 'docker-compose up -d'
version: '2'
services:
mongo-ipfs-service:
image: mongo
container_name: mongo-ipfs-service
ports:
- '5555:27017' # <host port>:<container port>
volumes:
- ../data/database:/data/db
command: mongod --logpath=/dev/null # -- quiet
restart: always
ipfs-service:
build: .
container_name: ipfs-service
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '10'
mem_limit: 500mb
links:
- mongo-ipfs-service
ports:
- '5001:5001' # <host port>:<container port>
- '5268:5268' # IPFS TCP
- '5269:5269' # IPFS WS
volumes:
- ../data/ipfsdata:/home/safeuser/ipfs-service-provider/.ipfsdata
restart: always
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# BEGIN: Optional configuration settings
# This mnemonic is used to set up persistent public key for e2ee
# Replace this with your own 12-word mnemonic.
# You can get one at https://wallet.fullstack.cash.
export MNEMONIC="olive two muscle bottom coral ancient wait legend bronze useful process session"
# The human readable name this IPFS node identifies as.
export COORD_NAME=ipfs-service-provider-generic
# Allow this node to function as a circuit relay. It must not be behind a firewall.
#export ENABLE_CIRCUIT_RELAY=true
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
# and you must forward that subdomain to the IPFS_WS_PORT.
#export CR_DOMAIN=subdomain.yourdomain.com
# Debug level. 0 = minimal info. 2 = max info.
export DEBUG_LEVEL=1
# END: Optional configuration settings
# Production database connection string.
export DBURL=mongodb://172.17.0.1:5555/ipfs-service-prod
# Configure IPFS ports
export IPFS_TCP_PORT=5268
export IPFS_WS_PORT=5269
# Configure REST API port
export PORT=5001
export SVC_ENV=production
npm start
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# This script is an example for running a generic ipfs-service-provider instance.
# Ports
export PORT=5001 # REST API port
export IPFS_TCP_PORT=5268
export IPFS_WS_PORT=5269
# The human-readible name that is used when displaying data about this node.
export COORD_NAME=ipfs-service-provider-generic
# This is used for end-to-end encryption (e2ee).
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
# 0 = less verbose. 3 = most verbose
export DEBUG_LEVEL=1
# MongoDB connection string.
#export DBURL=mongodb://localhost:27017/bch-service-dev
npm start
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# This script is an example for running a ipfs-service-provider as a Circuit Relay.
# Circuit Relays are help other nodes on the network communicate. They are
# critical for reliable functioning of the network, and for circumventing
# censorship.
# Ports
export PORT=5001 # REST API port
export IPFS_TCP_PORT=5268
export IPFS_WS_PORT=5269
# The human-readible name that is used when displaying data about this node.
export COORD_NAME=ipfs-service-provider-generic
# This is used for end-to-end encryption (e2ee).
export MNEMONIC="churn aisle shield silver ladder swear hunt slim pen demand spoil veteran"
# 0 = less verbose. 3 = most verbose
export DEBUG_LEVEL=1
# MongoDB connection string.
#export DBURL=mongodb://localhost:27017/bch-service-dev
# Comment to disable circuit relay functionality. Or set to 1 to enable.
export ENABLE_CIRCUIT_RELAY=1
# For browsers to use your circuit realy, you must set up a domain, SSL certificate,
# and you must forward that subdomain to the IPFS_WS_PORT.
#export CR_DOMAIN=subdomain.yourdomain.com
npm start
+172
View File
@@ -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
+63
View File
@@ -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
+107
View File
@@ -0,0 +1,107 @@
/*
A library of utility functions for working with FullStack.cash JWT tokens.
Feel free to copy this library into your own app, as well as the unit tests
for this file.
*/
const JwtLib = require('jwt-bch-lib')
const BCHJS = require('@psf/bch-js')
class FullStackJWT {
constructor (localConfig = {}) {
// Input Validation
this.authServer = localConfig.authServer
if (!this.authServer || typeof this.authServer !== 'string') {
throw new Error(
'Must pass a url for the AUTH server when instantiating FullStackJWT class.'
)
}
this.apiServer = localConfig.apiServer
if (!this.apiServer || typeof this.apiServer !== 'string') {
throw new Error(
'Must pass a url for the API server when instantiating FullStackJWT class.'
)
}
this.login = localConfig.fullstackLogin
if (!this.login || typeof this.login !== 'string') {
throw new Error(
'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.'
)
}
this.password = localConfig.fullstackPassword
if (!this.password || typeof this.password !== 'string') {
throw new Error(
'Must pass a FullStack.cash account password when instantiating FullStackJWT class.'
)
}
// Encapsulate dependencies
this.jwtLib = new JwtLib({
// Overwrite default values with the values in the config file.
server: this.authServer,
login: this.login,
password: this.password
})
// State
this.apiToken = '' // Default value.
this.bchjs = {}
}
// Get's a JWT token from FullStack.cash.
async getJWT () {
try {
// Skip connecting FullStack.cash auth server to the network if this is an E2E test.
if (process.env.TEST_TYPE === 'e2e') {
this.apiToken = 'faketoken'
return this.apiToken
}
// Log into the auth server.
await this.jwtLib.register()
this.apiToken = this.jwtLib.userData.apiToken
if (!this.apiToken) {
throw new Error('This account does not have a JWT')
}
console.log(`Retrieved JWT token: ${this.apiToken}\n`)
// Ensure the JWT token is valid to use.
const isValid = await this.jwtLib.validateApiToken()
// Get a new token with the same API level, if the existing token is not
// valid (probably expired).
if (!isValid.isValid) {
this.apiToken = await this.jwtLib.getApiToken(
this.jwtLib.userData.apiLevel
)
console.log(
`The JWT token was not valid. Retrieved new JWT token: ${this.apiToken}\n`
)
} else {
console.log('JWT token is valid.\n')
}
return this.apiToken
} catch (err) {
console.error(
`Error trying to log into ${this.server} and retrieve JWT token.`
)
throw err
}
}
// Create an instance of bchjs with the validated JWT token. Returns this
// instance of bch-js.
instanceBchjs () {
this.bchjs = new BCHJS({
restURL: this.apiServer,
apiToken: this.apiToken
})
return this.bchjs
}
}
module.exports = FullStackJWT
+57
View File
@@ -0,0 +1,57 @@
/*
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
*/
// Public NPM libraries
const BCHJS = require('@psf/bch-js')
// 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')
const FullStackJWT = require('./fullstack-jwt')
const config = require('../../config')
class Adapters {
constructor (localConfig = {}) {
// Encapsulate dependencies
this.ipfs = new IPFSAdapter()
this.localdb = new LocalDB()
this.logapi = new LogsAPI()
this.passport = new Passport()
this.nodemailer = new Nodemailer()
this.jsonFiles = new JSONFiles()
this.bchjs = new BCHJS()
this.config = config
// Get a valid JWT API key and instance bch-js.
this.fullStackJwt = new FullStackJWT(config)
}
async start () {
try {
if (this.config.getJwtAtStartup) {
// Get a JWT token and instantiate bch-js with it. Then pass that instance
// to all the rest of the apps controllers and adapters.
await this.fullStackJwt.getJWT()
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
this.bchjs = await this.fullStackJwt.instanceBchjs()
}
// Start the IPFS node.
await this.ipfs.start()
} catch (err) {
console.error('Error in adapters/index.js/start()')
throw err
}
}
}
module.exports = Adapters
+54
View File
@@ -0,0 +1,54 @@
/*
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.process = process
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()')
// If error is due to a lock file issue. Kill the process, so that
// Docker or pm2 has a chance to restart the service.
if (err.message.includes('Lock already being held')) {
this.process.exit(1)
}
throw err
}
}
}
module.exports = IPFS
+95
View File
@@ -0,0 +1,95 @@
/*
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')
const publicIp = require('public-ip')
// 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.config = config
this.publicIp = publicIp
// Properties of this class instance.
this.isReady = false
_this = this
}
async start () {
const circuitRelayInfo = {}
// If configured as a Circuit Relay, get the public IP addresses for this node.
if (this.config.isCircuitRelay) {
try {
const ip4 = await this.publicIp.v4()
// const ip6 = await publicIp.v6()
circuitRelayInfo.ip4 = ip4
circuitRelayInfo.tcpPort = this.config.ipfsTcpPort
// Domain used by browser-based secure websocket connections.
circuitRelayInfo.crDomain = this.config.crDomain
} catch (err) {
/* exit quietly */
}
}
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,
circuitRelayInfo,
apiInfo: this.config.apiInfo,
announceJsonLd: this.config.announceJsonLd,
debugLevel: this.config.debugLevel
})
// Wait for the ipfs-coord library to signal that it is ready.
await this.ipfsCoord.start()
// 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.adapters.orbit.privateLog = router
} catch (err) {
console.error('Error in attachRPCRouter()')
throw err
}
}
}
module.exports = IpfsCoordAdapter
+85
View File
@@ -0,0 +1,85 @@
/*
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.
TODO: Add the external IP address to the list of multiaddrs advertised by
this node. See this GitHub Issue for details:
https://github.com/Permissionless-Software-Foundation/ipfs-service-provider/issues/38
*/
// Global npm libraries
// const IPFS = require('ipfs')
const IPFS = require('@chris.troutner/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/ipfs',
start: true,
config: {
relay: {
enabled: true, // enable circuit relay dialer and listener
hop: {
enabled: config.isCircuitRelay // 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')
// Debugging: Display IPFS config settings.
// const configSettings = await this.ipfs.config.getAll()
// console.log(`configSettings: ${JSON.stringify(configSettings, null, 2)}`)
// Signal that this adapter is ready.
this.isReady = true
return this.ipfs
} catch (err) {
console.error('Error in ipfs.js/start()')
throw err
}
}
async stop () {
await this.ipfs.stop()
return true
}
}
module.exports = IpfsAdapter
+75
View File
@@ -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
+15
View File
@@ -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
+54
View File
@@ -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)
+170
View File
@@ -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
+153
View File
@@ -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
+35
View File
@@ -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
+72
View File
@@ -0,0 +1,72 @@
/*
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')
class Wlogger {
constructor (localConfig = {}) {
this.config = config
// Configure daily-rotation transport.
this.transport = new winston.transports.DailyRotateFile({
filename: `${__dirname.toString()}/../../logs/koa-${
this.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()
)
})
this.transport.on('rotate', this.notifyRotation)
// This controls what goes into the log FILES
this.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' })
this.transport
]
})
}
notifyRotation (oldFilename, newFilename) {
this.wlogger.info('Rotating log files')
}
outputToConsole () {
this.wlogger.add(
new winston.transports.Console({
format: winston.format.simple(),
level: 'info'
})
)
}
}
const logger = new Wlogger()
// Allow the logger to write to the console.
logger.outputToConsole()
const wlogger = logger.wlogger
module.exports = { wlogger, Wlogger }
+64
View File
@@ -0,0 +1,64 @@
/*
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')
class Controllers {
constructor (localConfig = {}) {
this.adapters = new Adapters()
this.useCases = new UseCases({ adapters: this.adapters })
}
async attachControllers (app) {
// Wait for any startup processes to complete for the Adapters libraries.
await this.adapters.start()
// Attach the REST controllers to the Koa app.
// this.attachRESTControllers(app)
this.attachRPCControllers()
}
// Top-level function for this library.
// Start the various Controllers and attach them to the app.
attachRESTControllers (app) {
const restControllers = new RESTControllers({
adapters: this.adapters,
useCases: this.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.
attachRPCControllers () {
const jsonRpcController = new JSONRPC({
adapters: this.adapters,
useCases: this.useCases
})
// Attach the input of the JSON RPC router to the output of ipfs-coord.
this.adapters.ipfs.ipfsCoordAdapter.attachRPCRouter(
jsonRpcController.router
)
}
}
module.exports = Controllers
+46
View File
@@ -0,0 +1,46 @@
/*
This is the JSON RPC router for the users API
*/
// Public npm libraries
const jsonrpc = require('jsonrpc-lite')
// Local libraries
const config = require('../../../../config')
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,
message: JSON.stringify(config.announceJsonLd),
endpoint: 'about'
}
}
}
module.exports = AboutRPC
+176
View File
@@ -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
+193
View File
@@ -0,0 +1,193 @@
/*
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()
// Cache to store IDs of processed JSON RPC commands. Used to prevent
// duplicate processing.
this.msgCache = []
this.MSG_CACHE_SIZE = 30
_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('JSON RPC router recieved data from: ', from)
// Exit quietly if 'from' is not specified.
if (!from || typeof from !== 'string') {
wlogger.info(
'Warning: Can not send JSON RPC response. Can not determine which peer this message came from.'
)
return false
}
// Attempt to parse the incoming data as a JSON RPC string.
const parsedData = _this.jsonrpc.parse(str)
// wlogger.debug(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
// Exit quietly if the incoming string is an invalid JSON RPC string.
if (parsedData.type === 'invalid') {
wlogger.info('Rejecting invalid JSON RPC command.')
return false
}
// Check for duplicate entries with same 'id' value.
const alreadyProcessed = _this._checkIfAlreadyProcessed(parsedData)
if (alreadyProcessed) {
return false
} else {
// console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`)
// This node will regularly ping known circuit relays with an /about
// JSON RPC call. These will be handled by ipfs-coord, but will percolate
// up to this library. Ignore these messages.
if (
parsedData.type.includes('success') &&
parsedData.payload.method === undefined
) {
return false
}
// Log the incoming JSON RPC command.
wlogger.info(
`JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}, method: ${parsedData.payload.method}`
)
}
// 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.
// console.log('responding to JSON RPC command')
const thisNode = _this.ipfsCoord.thisNode
// console.log('thisNode: ', thisNode)
try {
await _this.ipfsCoord.useCases.peer.sendPrivateMessage(
from,
retStr,
thisNode
)
} catch (err) {
console.log('sendPrivateMessage() err: ', err)
}
// 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.
}
}
// Checks the ID of the JSON RPC call, to see if the message has already been
// processed. Returns true if the ID exists in the cache of processed messages.
// If the ID is new, the function adds it to the cache and return false.
_checkIfAlreadyProcessed (data) {
try {
// console.log('data: ', data)
const id = data.payload.id
// Check if the hash is in the array of already processed message.
const alreadyProcessed = this.msgCache.includes(id)
// Update the msgCache if this is a new message.
if (!alreadyProcessed) {
// Add the hash to the array.
this.msgCache.push(id)
// If the array is at its max size, then remove the oldest element.
if (this.msgCache.length > this.MSG_CACHE_SIZE) {
this.msgCache.shift()
}
}
return alreadyProcessed
} catch (err) {
console.error('Error in _checkIfAlreadyProcessed: ', err)
return true
}
}
// 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
+83
View File
@@ -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
+536
View File
@@ -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
+96
View File
@@ -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
+51
View File
@@ -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
+56
View File
@@ -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
+58
View File
@@ -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
+55
View File
@@ -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
+88
View File
@@ -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
+24
View File
@@ -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
+23
View File
@@ -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
+184
View File
@@ -0,0 +1,184 @@
/*
This library contains business-logic for dealing with users. Most of these
functions are called by the /user REST API endpoints.
*/
const UserEntity = require('../entities/user')
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.UserEntity = new UserEntity()
this.UserModel = this.adapters.localdb.Users
}
// Create a new user model and add it to the Mongo database.
async createUser (userObj) {
try {
// Input Validation
const userEntity = this.UserEntity.validate(userObj)
const user = new this.UserModel(userEntity)
// 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
+7
View File
@@ -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).
+124
View File
@@ -0,0 +1,124 @@
/*
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 Server = 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 () => {
const app = new Server()
// This should be the first instruction. It starts the REST API server.
await app.startServer()
// Stop the IPFS node for the rest of the e2e tests.
// await app.controllers.adapters.ipfs.stop()
// 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
}
})
})
})
+791
View File
@@ -0,0 +1,791 @@
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 adapters = new 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)
})
})
})
+178
View File
@@ -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')
}
})
})
})
+132
View File
@@ -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)
})
})
})
+118
View File
@@ -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')
}
})
})
})
+8
View File
@@ -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.
+129
View File
@@ -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,162 @@
/*
Unit tests for the jwt-bch-lib and fullstack-jwt.js adapter library.
*/
const assert = require('chai').assert
const sinon = require('sinon')
const FullStackJWT = require('../../../src/adapters/fullstack-jwt')
describe('#FullStackJWT', () => {
let sandbox
let uut
beforeEach(() => {
sandbox = sinon.createSandbox()
const localConfig = {
authServer: 'someserver',
apiServer: 'someserver',
fullstackLogin: 'somelogin',
fullstackPassword: 'somepassword'
}
uut = new FullStackJWT(localConfig)
})
afterEach(() => sandbox.restore())
describe('#constructor', () => {
it('should throw an error if auth server is not specified', () => {
try {
uut = new FullStackJWT()
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a url for the AUTH server when instantiating FullStackJWT class.'
)
}
})
it('should throw an error if api server is not specified', () => {
try {
const localConfig = {
authServer: 'someserver'
}
uut = new FullStackJWT(localConfig)
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a url for the API server when instantiating FullStackJWT class.'
)
}
})
it('should throw an error if login is not specified', () => {
try {
const localConfig = {
authServer: 'someserver',
apiServer: 'someserver'
}
uut = new FullStackJWT(localConfig)
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.'
)
}
})
it('should throw an error if login is not specified', () => {
try {
const localConfig = {
authServer: 'someserver',
apiServer: 'someserver',
fullstackLogin: 'somelogin'
}
uut = new FullStackJWT(localConfig)
assert.fail('Unexpected code path')
console.log(uut) // For linting.
} catch (err) {
assert.include(
err.message,
'Must pass a FullStack.cash account password when instantiating FullStackJWT class.'
)
}
})
})
describe('#getJWT', () => {
it('should return the JWT token', async () => {
// Mock dependencies to force a code path.
sandbox.stub(uut.jwtLib, 'register').resolves({})
uut.jwtLib.userData.apiToken = 'abc123'
sandbox.stub(uut.jwtLib, 'validateApiToken').resolves({ isValid: true })
const result = await uut.getJWT()
// console.log('result: ', result)
assert.equal(result, 'abc123')
})
it('should catch and throw an error', async () => {
try {
// Force an error
sandbox.stub(uut.jwtLib, 'register').rejects(new Error('test error'))
await uut.getJWT()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should throw an error if user does not have a JWT', async () => {
try {
// Mock dependencies to force a code path.
sandbox.stub(uut.jwtLib, 'register').resolves({})
await uut.getJWT()
assert.fail('Unexpected code path')
} catch (err) {
// console.log('err.message: ', err.message)
assert.include(err.message, 'This account does not have a JWT')
}
})
it('should retrieve a new JWT token if the old one invalid', async () => {
// Mock dependencies to force a code path.
sandbox.stub(uut.jwtLib, 'register').resolves({})
uut.jwtLib.userData.apiToken = 'abc123'
uut.jwtLib.userData.apiLevel = 30
sandbox.stub(uut.jwtLib, 'validateApiToken').resolves({ isValid: false })
sandbox.stub(uut.jwtLib, 'getApiToken').resolves('xyz789')
const result = await uut.getJWT()
// console.log('result: ', result)
assert.equal(result, 'xyz789')
})
})
describe('#instanceBchjs', () => {
it('should return an instance of bch-js', () => {
const result = uut.instanceBchjs()
// console.log('result: ', result)
assert.property(result, 'restURL')
})
})
})
@@ -0,0 +1,103 @@
/*
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)
})
it('should get the public IP address if this node is a Circuit Relay', async () => {
// Mock dependencies.
uut.IpfsCoord = IPFSCoordMock
sandbox.stub(uut.publicIp, 'v4').returns('123')
// Force Circuit Relay
uut.config.isCircuitRelay = true
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: {}
}
},
adapters: {
orbit: {
privateLog: () => {}
}
}
}
const router = console.log
uut.attachRPCRouter(router)
})
it('should catch and throw an error', () => {
try {
// Force an error
delete uut.ipfsCoord.adapters
const router = console.log
uut.attachRPCRouter(router)
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'Cannot read property')
}
})
})
})
@@ -0,0 +1,67 @@
/*
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')
}
})
it('should handle lock-file errors', async () => {
try {
// Force an error
sandbox
.stub(uut.ipfsAdapter, 'start')
.rejects(new Error('Lock already being held'))
// Prevent process from exiting
sandbox.stub(uut.process, 'exit').returns()
await uut.start()
assert.fail('Unexpected code path.')
} catch (err) {
assert.include(err.message, 'Lock already being held')
}
})
})
})
+63
View File
@@ -0,0 +1,63 @@
/*
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')
}
})
})
describe('#stop', () => {
it('should stop the IPFS node', async () => {
// Mock dependencies
uut.ipfs = {
stop: () => {}
}
const result = await uut.stop()
assert.equal(result, true)
})
})
})
@@ -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')
}
})
})
})
+231
View File
@@ -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')
}
})
})
})
+82
View File
@@ -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,40 @@
const assert = require('chai').assert
const { Wlogger } = require('../../../src/adapters/wlogger')
const sinon = require('sinon')
let uut
let sandbox
describe('#wlogger', () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
})
afterEach(() => {
sandbox.restore()
uut = new Wlogger()
})
describe('#constructor', () => {
it('should create a new wlogger instance', () => {
uut = new Wlogger()
// console.log('uut: ', uut)
assert.property(uut, 'transport')
})
})
describe('#notifyRotation', () => {
it('should notify of a log rotation', () => {
uut.notifyRotation()
})
})
describe('#envronment', () => {
it('should write to console in non-test environment', () => {
uut.outputToConsole()
})
})
})
+38
View File
@@ -0,0 +1,38 @@
/*
Unit tests for controllers index.js file.
*/
// Public npm libraries
// const assert = require('chai').assert
const sinon = require('sinon')
const Controllers = require('../../../src/controllers')
describe('#Controllers', () => {
let uut
let sandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new Controllers()
})
afterEach(() => sandbox.restore())
describe('#attachControllers', () => {
it('should attach the controllers', async () => {
// mock IPFS
sandbox.stub(uut.adapters, 'start').resolves({})
uut.adapters.ipfs.ipfsCoordAdapter = {
attachRPCRouter: () => {}
}
const app = {
use: () => {}
}
await uut.attachControllers(app)
})
})
})
@@ -0,0 +1,298 @@
/*
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)
})
it('should exit quietly for duplicate RPC message', async () => {
const id = uid()
const json = jsonrpc.request(id, 'unknownMethod', {})
const str = JSON.stringify(json)
// Call router once.
await uut.router(str, 'peerA')
// Call the router again with the same input.
const result = await uut.router(str, 'peerA')
assert.equal(result, false)
})
it('should ignore metric queries from ipfs-coord', async () => {
const id = uid()
const json = jsonrpc.request(id, 'unknownMethod', {})
const str = JSON.stringify(json)
// Force the RPC type to be 'success', to indicate an RPC that was
// processed internal to ipfs-coord.
sandbox.stub(uut.jsonrpc, 'parse').returns({
payload: {},
type: 'success'
})
// Call the router again with the same input.
const result = await uut.router(str, 'peerA')
assert.equal(result, false)
})
it('should report errors when trying to send messages to peers', async () => {
const id = uid()
const json = jsonrpc.request(id, 'unknownMethod', {})
const str = JSON.stringify(json)
// Force issue with sendPrivateMessage()
sandbox
.stub(uut.ipfsCoord.useCases.peer, 'sendPrivateMessage')
.rejects('test error')
// Call the router again with the same input.
const result = await uut.router(str, 'peerA')
// console.log('result: ', result)
assert.property(result, 'from')
assert.property(result, 'retStr')
})
})
describe('#_checkIfAlreadyProcessed', () => {
it('should return false the first time an RPC command is seen', () => {
const data = {
payload: {
jsonrpc: '2.0',
id: '6c515f3c-cf8a-42ec-870e-e416edd4923f',
method: 'unknownMethod',
params: {}
},
type: 'request'
}
const result = uut._checkIfAlreadyProcessed(data)
assert.equal(result, false)
})
it('should return true the second time an RPC command is seen', () => {
const data = {
payload: {
jsonrpc: '2.0',
id: '6c515f3c-cf8a-42ec-870e-e416edd4923f',
method: 'unknownMethod',
params: {}
},
type: 'request'
}
// First call.
uut._checkIfAlreadyProcessed(data)
// Second call.
const result = uut._checkIfAlreadyProcessed(data)
assert.equal(result, true)
})
it('should push out old data from the cache for new data', () => {
const data = {
payload: {
jsonrpc: '2.0',
id: '6c515f3c-cf8a-42ec-870e-e416edd4923f',
method: 'unknownMethod',
params: {}
},
type: 'request'
}
uut.MSG_CACHE_SIZE = 0
const result = uut._checkIfAlreadyProcessed(data)
assert.equal(result, false)
})
it('should return true on error', () => {
const result = uut._checkIfAlreadyProcessed()
assert.equal(result, true)
})
})
})
@@ -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')
})
})
})
+9
View File
@@ -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.'
)
}
})
})
})
+69
View File
@@ -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)
})
})
})
+77
View File
@@ -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)
})
})
})
+56
View File
@@ -0,0 +1,56 @@
/*
Mocks for the Adapter library.
*/
const ipfs = {
ipfsAdapter: {
ipfs: {}
},
ipfsCoordAdapter: {
ipfsCoord: {
useCases: {
peer: {
sendPrivateMessage: () => {}
}
}
}
}
}
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 }

Some files were not shown because too many files have changed in this diff Show More