mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex.git
synced 2026-09-21 16:52:00 -07:00
forked from koa-api-boilerplate
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
# A special property that should be specified at the top of the file outside of
|
||||
# any sections. Set to true to stop .editor config file search on current file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
# Indentation style
|
||||
# Possible values - tab, space
|
||||
indent_style = space
|
||||
|
||||
# Indentation size in single-spaced characters
|
||||
# Possible values - an integer, tab
|
||||
indent_size = 2
|
||||
|
||||
# Line ending file format
|
||||
# Possible values - lf, crlf, cr
|
||||
end_of_line = lf
|
||||
|
||||
# File character encoding
|
||||
# Possible values - latin1, utf-8, utf-16be, utf-16le
|
||||
charset = utf-8
|
||||
|
||||
# Denotes whether to trim whitespace at the end of lines
|
||||
# Possible values - true, false
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Denotes whether file should end with a newline
|
||||
# Possible values - true, false
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "standard",
|
||||
"env": {
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
!README.md
|
||||
@@ -0,0 +1,8 @@
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021 Chris Troutner <chris.troutner@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,116 @@
|
||||
# koa-api-boilerplate
|
||||
[](http://standardjs.com) [](https://coveralls.io/github/christroutner/babel-free-koa2-api-boilerplate?branch=unstable) [](https://github.com/semantic-release/semantic-release) [](https://greenkeeper.io/)
|
||||
|
||||
|
||||
This repository is a boilerplate for building APIs with
|
||||
[koa2](https://github.com/koajs/koa/tree/v2.x) and Mongo DB.
|
||||
This repository was originally forked from Adrian Obelmejias'
|
||||
[koa-api-boilerplate repository](https://github.com/adrianObel/koa2-api-boilerplate).
|
||||
It makes the following modifications:
|
||||
|
||||
- Removes babel as a dependency. This repository is now naively compatible with
|
||||
node v8.9 or higher.
|
||||
|
||||
- Replaced `bcrypt` dependency with `bcryptjs`. This improves compatibility across
|
||||
versions of node.js and across OSs.
|
||||
|
||||
- Configured for Jenkins (continuous integration), Coveralls (code coverage), Green Keeper (automated dependency management), and Semantic Release (automated versioning).
|
||||
|
||||
- 'Production' environment is targeted for packaging as a Docker container.
|
||||
|
||||
- 'admin' user type added in addition to standard 'user' type. Allows the creation
|
||||
of private vs public APIs that only be accessed by an admin. Useful for privileged
|
||||
commands like updating and deleting other users.
|
||||
|
||||
- Winston logging integrated for daily rotated logs and a maximum size of
|
||||
1 megabyte.
|
||||
|
||||
- Linting enforced with [Husky](https://github.com/typicode/husky) and [JavaScript Standard Style rules](https://www.npmjs.com/package/standard).
|
||||
|
||||
## Features
|
||||
This project covers basic necessities of most APIs.
|
||||
* Authentication (passport & jwt)
|
||||
* Database (mongoose)
|
||||
* Testing (mocha)
|
||||
* Doc generation with apidoc
|
||||
* Linting using standard
|
||||
* Packaged as a Docker container
|
||||
|
||||
|
||||
|
||||
## Requirements
|
||||
* node __^10.15.1__
|
||||
* npm __^6.7.0__
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
git clone https://github.com/christroutner/koa-api-boilerplate
|
||||
cd koa-api-boilerplate
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## Structure
|
||||
```
|
||||
├── bin
|
||||
│ └── server.js # Bootstrapping and entry point
|
||||
├── config # Server configuration settings
|
||||
│ ├── env # Environment specific config
|
||||
│ │ ├── common.js
|
||||
│ │ ├── development.js
|
||||
│ │ ├── production.js
|
||||
│ │ └── test.js
|
||||
│ ├── index.js # Config entrypoint - exports config according to envionrment and commons
|
||||
│ └── passport.js # Passportjs config of strategies
|
||||
|
|
||||
├── production # Dockerfile for build production container
|
||||
|
|
||||
├── src # Source code
|
||||
│ ├── lib # Business logic libraries
|
||||
│ ├── modules
|
||||
│ │ ├── controller.js # Module-specific controllers
|
||||
│ │ └── router.js # Router definitions for module
|
||||
│ ├── models # Mongoose models
|
||||
│ └── middleware # Custom middleware
|
||||
│ └── validators # Validation middleware
|
||||
└── test # Unit tests
|
||||
```
|
||||
|
||||
## Usage
|
||||
* `npm start` Start server on live mode
|
||||
* `npm run dev` Start server on dev mode with nodemon
|
||||
* `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
|
||||
v2.3.0 uploaded to IPFS:
|
||||
|
||||
- Get it: `ipfs get QmUz4b2KwNLNvHZRTYcgrPCuKAhMB73XWN8vY8LLVVEYV1`
|
||||
- Pin it: `ipfs pin add -r QmUz4b2KwNLNvHZRTYcgrPCuKAhMB73XWN8vY8LLVVEYV1`
|
||||
|
||||
## License
|
||||
MIT
|
||||
|
||||
test
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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/lib/admin')
|
||||
const adminLib = new AdminLib()
|
||||
|
||||
const errorMiddleware = require('../src/middleware')
|
||||
const wlogger = require('../src/lib/wlogger')
|
||||
|
||||
async function startServer () {
|
||||
// Create a Koa instance.
|
||||
const app = new Koa()
|
||||
app.keys = [config.session]
|
||||
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
// MIDDLEWARE START
|
||||
|
||||
app.use(convert(logger()))
|
||||
app.use(bodyParser())
|
||||
app.use(session())
|
||||
app.use(errorMiddleware())
|
||||
|
||||
// Used to generate the docs.
|
||||
app.use(mount('/', serve(`${process.cwd()}/docs`)))
|
||||
|
||||
// Mount the page for displaying logs.
|
||||
app.use(mount('/logs', serve(`${process.cwd()}/config/logs`)))
|
||||
|
||||
// User Authentication
|
||||
require('../config/passport')
|
||||
app.use(passport.initialize())
|
||||
app.use(passport.session())
|
||||
|
||||
// Custom Middleware Modules
|
||||
const modules = require('../src/modules')
|
||||
modules(app)
|
||||
|
||||
// Enable CORS for testing
|
||||
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
|
||||
app.use(cors({ origin: '*' }))
|
||||
|
||||
// MIDDLEWARE END
|
||||
|
||||
console.log(`Running server in environment: ${config.env}`)
|
||||
wlogger.info(`Running server in environment: ${config.env}`)
|
||||
|
||||
await app.listen(config.port)
|
||||
console.log(`Server started on ${config.port}`)
|
||||
|
||||
// Create the system admin user.
|
||||
const success = await adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
|
||||
return app
|
||||
}
|
||||
// startServer()
|
||||
|
||||
// export default app
|
||||
// module.exports = app
|
||||
module.exports = {
|
||||
startServer
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
This file is used to store unsecure, application-specific data common to all
|
||||
environments.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
port: process.env.PORT || 5001,
|
||||
logPass: 'test',
|
||||
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'
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
These are the environment settings for the DEVELOPMENT environment.
|
||||
This is the environment run by default with `npm start` if KOA_ENV is not
|
||||
specified.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/koa-server-dev',
|
||||
env: 'dev'
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
These are the environment settings for the PRODUCTION environment.
|
||||
This is the environment run with `npm start` if KOA_ENV=production.
|
||||
This is the environment run inside the Docker container.
|
||||
|
||||
It is assumed the MonogDB Docker container is accessed by port 5555
|
||||
so as not to conflict with the default host port of 27017 for MongoDB.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://172.17.0.1:5555/koa-server-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
These are the environment settings for the TEST environment.
|
||||
This is the environment run with `npm start` if KOA_ENV=test.
|
||||
This is the environment run by the test suite.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/koa-server-test',
|
||||
env: 'test'
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
const common = require('./env/common')
|
||||
|
||||
const env = process.env.KOA_ENV || 'development'
|
||||
const config = require(`./env/${env}`)
|
||||
|
||||
module.exports = Object.assign({}, common, config)
|
||||
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Logs</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<link href="/vendor/bootstrap.min.css" rel="stylesheet" media="screen" />
|
||||
<link href="/vendor/prettify.css" rel="stylesheet" media="screen" />
|
||||
<link href="/css/style.css" rel="stylesheet" media="screen, print" />
|
||||
<!-- <link href="img/favicon.ico" rel="icon" type="image/x-icon"> -->
|
||||
<script src="/vendor/polyfill.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<br />
|
||||
<div class="col-sm-4"></div>
|
||||
<div class="col-sm-4">
|
||||
<p id="outMsg"><p>
|
||||
</div>
|
||||
<div class="col-sm-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Password for accessing logs -->
|
||||
<div class="row loginForm">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="inputLogPass" class="col-sm-2 control-label"
|
||||
>Password</label
|
||||
>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="inputLogPass"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
onclick="viewLogs()"
|
||||
>
|
||||
View Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row logTable" style="visibility: hidden;">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<tr class="tableTemplate">
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/vendor/jquery.min.js"></script>
|
||||
<script src="/vendor/bootstrap.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function viewLogs() {
|
||||
try {
|
||||
const pass = $('#inputLogPass').val()
|
||||
|
||||
// if (pass === 'test') {
|
||||
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: pass
|
||||
})
|
||||
}
|
||||
const data = await fetch(`/logapi`, options)
|
||||
// console.log(`data.status: `, data.status)
|
||||
|
||||
if (data.status > 399) {
|
||||
$('#outMsg').text('Could not communicate with log server.')
|
||||
throw new Error(`Could not get log data`)
|
||||
}
|
||||
|
||||
const data2 = await data.json()
|
||||
// console.log(`data2: ${JSON.stringify(data2, null, 2)}`)
|
||||
|
||||
if (!data2.success) {
|
||||
$('#outMsg').text('Incorrect password')
|
||||
throw new Error(`Incorrect password`)
|
||||
} else {
|
||||
$('#outMsg').text('')
|
||||
}
|
||||
|
||||
$('.loginForm').css('visibility', 'hidden')
|
||||
$('.logTable').css('visibility', 'visible')
|
||||
|
||||
const logData = data2.data
|
||||
// console.log(`logData: ${JSON.stringify(logData, null, 2)}`)
|
||||
|
||||
// Clone the template row.
|
||||
const template = $('.tableTemplate')
|
||||
// debugger
|
||||
|
||||
// Loop through the array of log data.
|
||||
for (let i = 0; i < logData.length; i++) {
|
||||
const thisRow = template.clone()
|
||||
const cols = thisRow.find('td')
|
||||
|
||||
const time = new Date(logData[i].timestamp)
|
||||
// debugger
|
||||
|
||||
cols.first().text(time.toLocaleString())
|
||||
cols.next().text(logData[i].level)
|
||||
cols.next().next().text(logData[i].message)
|
||||
// debugger
|
||||
|
||||
$('.table').append(thisRow)
|
||||
}
|
||||
|
||||
|
||||
// } else {
|
||||
// console.log(`password fail`)
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error(`Error in viewLogs: `, err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
const passport = require('koa-passport')
|
||||
const User = require('../src/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'
|
||||
}, async (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)
|
||||
}
|
||||
}))
|
||||
@@ -0,0 +1,25 @@
|
||||
# Start the testnet server with the command 'docker-compose up -d'
|
||||
|
||||
koa-mongodb:
|
||||
image: mongo
|
||||
container_name: mongo-koa
|
||||
ports:
|
||||
- "5555:27017" # <host port>:<container port>
|
||||
volumes:
|
||||
- ./database:/data/db
|
||||
command: mongod --smallfiles --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
koa:
|
||||
build: ./production/
|
||||
dockerfile: Dockerfile
|
||||
container_name: koa
|
||||
links:
|
||||
- koa-mongodb
|
||||
ports:
|
||||
- "5001:5001" # <host port>:<container port>
|
||||
volumes:
|
||||
# - ./logs:/home/coinjoin/consolidating-coinjoin/logs
|
||||
- ./keys:/home/safeuser/keys
|
||||
|
||||
restart: always
|
||||
@@ -0,0 +1,3 @@
|
||||
const server = require('./bin/server.js')
|
||||
|
||||
server.startServer()
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -
|
||||
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y mongodb-org
|
||||
sudo service mongod start
|
||||
sudo systemctl enable mongod
|
||||
@@ -0,0 +1 @@
|
||||
This directory will hold the Winston daily logs. Any files saved to this directory will be ignored by Git.
|
||||
Generated
+30183
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "koa-api-boilerplate",
|
||||
"version": "3.0.0",
|
||||
"description": "Koa2 boilerplate covering essentials for REST API and auth.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "npm run test:all",
|
||||
"test:all": "export KOA_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/",
|
||||
"test:unit:lib": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/biz-logic/",
|
||||
"test:unit:rest": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/rest-api/",
|
||||
"test:e2e:auto": "export KOA_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export KOA_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/"
|
||||
},
|
||||
"keywords": [
|
||||
"koa-api-boilerplate",
|
||||
"api",
|
||||
"koa",
|
||||
"koa2",
|
||||
"boilerplate",
|
||||
"es6",
|
||||
"mongoose",
|
||||
"passportjs",
|
||||
"apidoc"
|
||||
],
|
||||
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
||||
"license": "MIT",
|
||||
"apidoc": {
|
||||
"title": "koa-api-boilerplate",
|
||||
"url": "localhost:5000"
|
||||
},
|
||||
"repository": "christroutner/koa-api-boilerplate",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"glob": "^7.1.6",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"kcors": "^2.2.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"koa-convert": "^2.0.0",
|
||||
"koa-generic-session": "^2.1.1",
|
||||
"koa-logger": "^3.2.1",
|
||||
"koa-mount": "^4.0.0",
|
||||
"koa-passport": "^4.1.3",
|
||||
"koa-router": "^10.0.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"line-reader": "^0.4.0",
|
||||
"mongoose": "^5.11.15",
|
||||
"nodemailer": "^6.4.17",
|
||||
"passport-local": "^1.0.0",
|
||||
"winston": "^3.3.3",
|
||||
"winston-daily-rotate-file": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "^0.26.0",
|
||||
"chai": "^4.3.0",
|
||||
"coveralls": "^3.1.0",
|
||||
"eslint": "7.19.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"husky": "^4.3.8",
|
||||
"mocha": "^8.2.1",
|
||||
"nyc": "^15.1.0",
|
||||
"semantic-release": "^17.3.7",
|
||||
"sinon": "^9.2.4",
|
||||
"standard": "^16.0.3"
|
||||
},
|
||||
"release": {
|
||||
"publish": [
|
||||
{
|
||||
"path": "@semantic-release/npm",
|
||||
"npmPublish": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Create a Dockerized API server
|
||||
#
|
||||
|
||||
#IMAGE BUILD COMMANDS
|
||||
# ct-base-ubuntu = ubuntu 18.04 + nodejs v10 LTS
|
||||
FROM christroutner/ct-base-ubuntu
|
||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||
|
||||
#Create the user 'safeuser' and add them to the sudo group.
|
||||
#RUN useradd -ms /bin/bash safeuser
|
||||
#RUN adduser safeuser sudo
|
||||
|
||||
#Set password to 'password' change value below if you want a different password
|
||||
#RUN echo safeuser:password | chpasswd
|
||||
|
||||
#Set the working directory to be the home directory
|
||||
WORKDIR /home/safeuser
|
||||
|
||||
#Setup NPM for non-root global install
|
||||
#RUN mkdir /home/safeuser/.npm-global
|
||||
#RUN chown -R safeuser .npm-global
|
||||
#RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
||||
#RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||
|
||||
# Switch to user account.
|
||||
USER safeuser
|
||||
# Prep 'sudo' commands.
|
||||
RUN echo 'abcd8765' | sudo -S pwd
|
||||
|
||||
# Clone the rest.bitcoin.com repository
|
||||
WORKDIR /home/safeuser
|
||||
RUN git clone https://github.com/christroutner/koa-api-boilerplate
|
||||
RUN mv koa-api-boilerplate koa
|
||||
RUN mkdir keys
|
||||
|
||||
# Switch to the desired branch. `master` is usually stable,
|
||||
# and `stage` has the most up-to-date changes.
|
||||
WORKDIR /home/safeuser/koa
|
||||
|
||||
# For development: switch to unstable branch
|
||||
# RUN git checkout unstable
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
EXPOSE 5001
|
||||
|
||||
# Start the application.
|
||||
COPY start-production start-production
|
||||
CMD ["./start-production"]
|
||||
|
||||
#CMD ["npm", "start"]
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
export KOA_ENV=production
|
||||
npm start
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
const axios = require('axios').default
|
||||
const mongoose = require('mongoose')
|
||||
const User = require('../models/users')
|
||||
const config = require('../../config')
|
||||
const JsonFiles = require('./utils/json-files')
|
||||
const jsonFiles = new JsonFiles()
|
||||
|
||||
const JSON_FILE = `system-user-${config.env}.json`
|
||||
const JSON_PATH = `${__dirname.toString()}/../../config/${JSON_FILE}`
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
const context = {}
|
||||
|
||||
let _this
|
||||
class Admin {
|
||||
constructor () {
|
||||
this.axios = axios
|
||||
this.User = User
|
||||
this.config = config
|
||||
this.jsonFiles = jsonFiles
|
||||
this.context = context
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Create the first user in the system. A 'admin' level system user that is
|
||||
// used by the Listing Manager and test scripts, in order access private API
|
||||
// functions.
|
||||
async createSystemUser () {
|
||||
// Create the system user.
|
||||
try {
|
||||
context.password = _this._randomString(20)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: 'system@system.com',
|
||||
password: context.password,
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log('admin.data: ', result.data)
|
||||
|
||||
context.email = result.data.user.email
|
||||
context.id = result.data.user._id
|
||||
context.token = result.data.token
|
||||
|
||||
// Get the mongoDB entry
|
||||
const user = await _this.User.findById(context.id)
|
||||
|
||||
// Change the user type to admin
|
||||
user.type = 'admin'
|
||||
// console.log(`user created: ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Save the user model.
|
||||
await user.save()
|
||||
|
||||
// console.log(`admin user created: ${JSON.stringify(result.body, null, 2)}`)
|
||||
// console.log(`with password: ${context.password}`)
|
||||
|
||||
// Write out the system user information to a JSON file that external
|
||||
// applications like the Task Manager and the test scripts can access.
|
||||
|
||||
await jsonFiles.writeJSON(context, JSON_PATH)
|
||||
|
||||
return context
|
||||
} catch (err) {
|
||||
// Handle existing system user.
|
||||
if (err.response.status === 422) {
|
||||
try {
|
||||
// Delete the existing user
|
||||
await _this.deleteExistingSystemUser()
|
||||
|
||||
// Call this function again.
|
||||
return _this.createSystemUser()
|
||||
} catch (err2) {
|
||||
console.error(
|
||||
'Error in admin.js/createSystemUser() while trying generate new system user.'
|
||||
)
|
||||
// process.end(1)
|
||||
throw err2
|
||||
}
|
||||
} else {
|
||||
console.log('Error in admin.js/createSystemUser: ')
|
||||
// process.end(1)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteExistingSystemUser () {
|
||||
try {
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
|
||||
await mongoose.connect(config.database, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true
|
||||
})
|
||||
|
||||
await _this.User.deleteOne({ email: 'system@system.com' })
|
||||
} catch (err) {
|
||||
console.log('Error in admin.js/deleteExistingSystemUser()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async loginAdmin () {
|
||||
// console.log(`loginAdmin() running.`)
|
||||
let existingUser
|
||||
|
||||
try {
|
||||
// Read the exising file
|
||||
existingUser = await _this.jsonFiles.readJSON(JSON_PATH)
|
||||
// console.log(`existingUser: ${JSON.stringify(existingUser, null, 2)}`)
|
||||
|
||||
// Log in as the user.
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
data: {
|
||||
email: 'system@system.com',
|
||||
password: existingUser.password
|
||||
}
|
||||
}
|
||||
const result = await _this.axios.request(options)
|
||||
// console.log(`result1: ${JSON.stringify(result, null, 2)}`)
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('Error in admin.js/loginAdmin().')
|
||||
|
||||
// console.error(`existingUser: ${JSON.stringify(existingUser, null, 2)}`)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
_randomString (length) {
|
||||
let text = ''
|
||||
const possible =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
for (let i = 0; i < length; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length))
|
||||
}
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Admin
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Retrieves the JWT token from the header of the API request.
|
||||
*/
|
||||
|
||||
module.exports = function 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
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
A library for controlling the sending 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
|
||||
}
|
||||
|
||||
// Validate email
|
||||
async validateEmail (email) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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!")
|
||||
}
|
||||
const isEmail = await _this.validateEmail(data.email)
|
||||
if (!isEmail) {
|
||||
throw new Error("Property 'email' must be email format!")
|
||||
}
|
||||
|
||||
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 'message' must be a string!")
|
||||
}
|
||||
|
||||
if (!data.subject || typeof data.subject !== 'string') {
|
||||
throw new Error("Property 'subject' must be a string!")
|
||||
}
|
||||
|
||||
if (!data.payloadTitle || typeof data.payloadTitle !== 'string') {
|
||||
throw new Error("Property 'payloadTitle' must be a string!")
|
||||
}
|
||||
|
||||
const msg = data.formMessage.replace(/(\r\n|\n|\r)/g, '<br />')
|
||||
|
||||
const now = new Date()
|
||||
|
||||
const subject = data.subject
|
||||
const to = data.to
|
||||
// const emailUser = data.email // email from the user who initiated the sharing email
|
||||
const payload = data.payloadTitle
|
||||
|
||||
const bodyJson = data
|
||||
delete bodyJson.to
|
||||
delete bodyJson.subject
|
||||
delete bodyJson.formMessage
|
||||
|
||||
// Prototyping email output.
|
||||
bodyJson.email = 'noreply@launchpadip.net'
|
||||
delete bodyJson.email
|
||||
delete bodyJson.emailList
|
||||
delete bodyJson.payloadTitle
|
||||
|
||||
bodyJson.message = msg
|
||||
|
||||
// Html body
|
||||
let htmlData = ''
|
||||
|
||||
// maps the object and converts it into html format
|
||||
Object.keys(bodyJson).forEach(function (key) {
|
||||
htmlData += `${key}: ${bodyJson[key]}<br/>`
|
||||
})
|
||||
// This paragraph just be added to the message for
|
||||
// emails that are not password reset ones
|
||||
const paragraphTag = 'This is a test email'
|
||||
|
||||
const htmlMsg = `<h3>${subject}:</h3>
|
||||
${payload === 'Email reset' ? '' : paragraphTag}
|
||||
<p>
|
||||
time: ${now.toLocaleString()}<br/>
|
||||
${htmlData}
|
||||
</p>`
|
||||
// send mail with defined transport object
|
||||
const info = await _this.transporter.sendMail({
|
||||
// from: `${data.email}`, // sender address
|
||||
from: 'noreply@launchpadip.net',
|
||||
to: `${to}`, // list of receivers
|
||||
// subject: `Pearson ${subject}`, // Subject line
|
||||
subject: subject,
|
||||
// html: '<b>This is a test email</b>' // html body
|
||||
html: htmlMsg
|
||||
})
|
||||
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!")
|
||||
}
|
||||
|
||||
// Iterates the array and validates each email format
|
||||
const isValid = await new Promise(resolve => {
|
||||
emailList.map(async (value, i) => {
|
||||
const isEmail = await _this.validateEmail(value)
|
||||
|
||||
if (!isEmail) {
|
||||
resolve(false)
|
||||
}
|
||||
if (i >= emailList.length - 1) {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Array must contain emails format!')
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
wlogger.error('Error in lib/nodemailer.js/validateEmailArray()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NodeMailer
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
koa-passport is an authorization library used for different authentication schemes.
|
||||
*/
|
||||
|
||||
const passport = require('koa-passport')
|
||||
|
||||
let _this
|
||||
class Passport {
|
||||
constructor () {
|
||||
_this = this
|
||||
this.passport = passport
|
||||
}
|
||||
|
||||
async authUser (ctx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
if (!ctx) throw new Error('ctx is required')
|
||||
|
||||
_this.passport.authenticate('local', (err, user) => {
|
||||
try {
|
||||
if (err) throw err
|
||||
|
||||
resolve(user)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})(ctx, null)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Passport
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
This library contains business-logic for dealing with users. Most of these
|
||||
functions are called by the /user REST API endpoints.
|
||||
*/
|
||||
|
||||
const UserModel = require('../models/users')
|
||||
const wlogger = require('./wlogger')
|
||||
|
||||
class UserLib {
|
||||
constructor (configObj) {
|
||||
// Encapsulate dependencies
|
||||
this.UserModel = UserModel
|
||||
}
|
||||
|
||||
// Create a new user model and add it to the Mongo database.
|
||||
async createUser (userObj) {
|
||||
try {
|
||||
// Input Validation
|
||||
if (!userObj.email || typeof userObj.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
if (!userObj.password || typeof userObj.password !== 'string') {
|
||||
throw new Error("Property 'password' must be a string!")
|
||||
}
|
||||
if (!userObj.name || typeof userObj.name !== 'string') {
|
||||
throw new Error("Property 'name' must be a string!")
|
||||
}
|
||||
|
||||
const user = new this.UserModel(userObj)
|
||||
|
||||
// Enforce default value of 'user'
|
||||
user.type = 'user'
|
||||
|
||||
// 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()
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserLib
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
A utility file for reading and writing JSON files.
|
||||
*/
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
|
||||
let _this
|
||||
|
||||
class JsonFiles {
|
||||
constructor () {
|
||||
this.fs = fs
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
// Writes out a JSON file of any object passed to the function.
|
||||
// This is used for testing.
|
||||
writeJSON (obj, fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
if (!obj) {
|
||||
throw new Error('obj property is required')
|
||||
}
|
||||
if (!fileName || typeof fileName !== 'string') {
|
||||
throw new Error('fileName property must be a string')
|
||||
}
|
||||
const fileStr = JSON.stringify(obj, null, 2)
|
||||
|
||||
_this.fs.writeFile(fileName, fileStr, function (err) {
|
||||
if (err) {
|
||||
console.error('Error while trying to write file: ')
|
||||
throw err
|
||||
} else {
|
||||
// console.log(`${fileName} written successfully!`)
|
||||
return resolve()
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error trying to write out object in util.js/_writeJSON().')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
readJSON (fileName) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
if (!fileName || typeof fileName !== 'string') {
|
||||
throw new Error('fileName property must be a string')
|
||||
}
|
||||
|
||||
_this.fs.readFile(fileName, (err, data) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log('Admin .json file not found!')
|
||||
} else {
|
||||
console.log(`err: ${JSON.stringify(err, null, 2)}`)
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
const obj = JSON.parse(data)
|
||||
|
||||
return resolve(obj)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error trying to read JSON file in util.js/_readJSON().')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JsonFiles
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Instantiates and configures the Winston logging library. This utitlity library
|
||||
can be called by other parts of the application to conveniently tap into the
|
||||
logging library.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const winston = require('winston')
|
||||
require('winston-daily-rotate-file')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
// Configure daily-rotation transport.
|
||||
const transport = new winston.transports.DailyRotateFile({
|
||||
filename: `${__dirname.toString()}/../../logs/koa-${config.env}-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxSize: '1m', // 1 megabyte
|
||||
maxFiles: '5d', // 5 days
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
)
|
||||
})
|
||||
|
||||
transport.on('rotate', function (oldFilename, newFilename) {
|
||||
wlogger.info('Rotating log files')
|
||||
})
|
||||
|
||||
// This controls what goes into the log FILES
|
||||
const wlogger = winston.createLogger({
|
||||
level: 'verbose',
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `combined.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
// new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
|
||||
// new winston.transports.File({ filename: 'logs/combined.log' })
|
||||
transport
|
||||
]
|
||||
})
|
||||
|
||||
// This controls the logs to CONSOLE
|
||||
/*
|
||||
wlogger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
level: "info"
|
||||
})
|
||||
)
|
||||
*/
|
||||
|
||||
module.exports = wlogger
|
||||
@@ -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,150 @@
|
||||
const User = require('../models/users')
|
||||
const config = require('../../config')
|
||||
const getToken = require('../lib/auth')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const wlogger = require('../lib/wlogger')
|
||||
|
||||
let _this
|
||||
|
||||
class Validators {
|
||||
constructor () {
|
||||
this.User = User
|
||||
this.getToken = getToken
|
||||
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()
|
||||
} 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()
|
||||
} 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.
|
||||
// TODO Tests must be developed before developing this function.
|
||||
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()
|
||||
} catch (error) {
|
||||
ctx.throw(401, error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Validators
|
||||
@@ -0,0 +1,79 @@
|
||||
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,
|
||||
validate: {
|
||||
validator: function (email) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email)
|
||||
},
|
||||
message: props => `${props.value} is not a valid Email format!`
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
User.pre('save', function preSave (next) {
|
||||
const user = this
|
||||
|
||||
if (!user.isModified('password')) {
|
||||
return next()
|
||||
}
|
||||
|
||||
new Promise((resolve, reject) => {
|
||||
bcrypt.genSalt(10, (err, salt) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(salt)
|
||||
})
|
||||
})
|
||||
.then(salt => {
|
||||
bcrypt.hash(user.password, salt, (err, hash) => {
|
||||
if (err) {
|
||||
throw new Error(err)
|
||||
}
|
||||
|
||||
user.password = hash
|
||||
|
||||
next(null)
|
||||
})
|
||||
})
|
||||
.catch(err => next(err))
|
||||
})
|
||||
|
||||
User.methods.validatePassword = function validatePassword (password) {
|
||||
const user = this
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
bcrypt.compare(password, user.password, (err, isMatch) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve(isMatch)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
User.methods.generateToken = function generateToken () {
|
||||
const user = this
|
||||
|
||||
const token = jwt.sign({ id: user.id }, config.token)
|
||||
// console.log(`config.token: ${config.token}`)
|
||||
// console.log(`generated token: ${token}`)
|
||||
return token
|
||||
}
|
||||
|
||||
// export default mongoose.model('user', User)
|
||||
module.exports = mongoose.model('user', User)
|
||||
@@ -0,0 +1,83 @@
|
||||
const Passport = require('../../lib/passport')
|
||||
const passport = new Passport()
|
||||
|
||||
let _this
|
||||
|
||||
class Auth {
|
||||
constructor () {
|
||||
_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 {
|
||||
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 = Auth
|
||||
@@ -0,0 +1,14 @@
|
||||
// import * as auth from './controller'
|
||||
const CONTROLLER = require('./controller')
|
||||
const controller = new CONTROLLER()
|
||||
// export const baseUrl = '/auth'
|
||||
module.exports.baseUrl = '/auth'
|
||||
|
||||
// export default [
|
||||
module.exports.routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/',
|
||||
handlers: [controller.authUser]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
const config = require('../../../config')
|
||||
|
||||
const NodeMailer = require('../../lib/nodemailer')
|
||||
const nodemailer = new NodeMailer()
|
||||
|
||||
let _this
|
||||
|
||||
class Contact {
|
||||
constructor () {
|
||||
_this = this
|
||||
_this.config = config
|
||||
_this.nodemailer = nodemailer
|
||||
}
|
||||
|
||||
async email (ctx) {
|
||||
try {
|
||||
const data = ctx.request.body
|
||||
|
||||
const emailObj = data.obj
|
||||
// Validate input
|
||||
if (!emailObj.email || typeof emailObj.email !== 'string') {
|
||||
throw new Error("Property 'email' must be a string!")
|
||||
}
|
||||
const isEmail = await _this.nodemailer.validateEmail(emailObj.email)
|
||||
|
||||
if (!isEmail) {
|
||||
throw new Error("Property 'email' must be email format!")
|
||||
}
|
||||
|
||||
if (!emailObj.formMessage || typeof emailObj.formMessage !== 'string') {
|
||||
throw new Error("Property 'message' must be a string!")
|
||||
}
|
||||
|
||||
if (!emailObj.payloadTitle || typeof emailObj.payloadTitle !== 'string') {
|
||||
throw new Error("Property 'payloadTitle' 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 to share a document with you.'
|
||||
emailObj.to = _to
|
||||
|
||||
await _this.nodemailer.sendEmail(emailObj)
|
||||
|
||||
ctx.body = {
|
||||
success: true
|
||||
}
|
||||
} catch (err) {
|
||||
// ctx.body = {
|
||||
// success: false
|
||||
// }
|
||||
// console.error(`Error: `, err)
|
||||
// throw err
|
||||
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = Contact
|
||||
@@ -0,0 +1,17 @@
|
||||
// const ensureUser = require('../../midleware/validators')
|
||||
|
||||
const ContactController = require('./controller')
|
||||
const contactController = new ContactController()
|
||||
|
||||
// export const baseUrl = '/users'
|
||||
module.exports.baseUrl = '/contact'
|
||||
|
||||
module.exports.routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/email',
|
||||
handlers: [
|
||||
contactController.email
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
const glob = require('glob')
|
||||
const Router = require('koa-router')
|
||||
|
||||
module.exports = function initModules (app) {
|
||||
glob(
|
||||
`${__dirname.toString()}/*`,
|
||||
{ ignore: '**/index.js' },
|
||||
(err, matches) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
// Loop through each sub-directory in the modules directory.
|
||||
matches.forEach((mod) => {
|
||||
// console.log(`router = ${mod}/router`)
|
||||
const router = require(`${mod}/router`)
|
||||
|
||||
const routes = router.routes
|
||||
const baseUrl = router.baseUrl
|
||||
const instance = new Router({ prefix: baseUrl })
|
||||
|
||||
// console.log(`routes: ${JSON.stringify(routes, null, 2)}`)
|
||||
|
||||
// Loop through each route defined in the router.js file.
|
||||
routes.forEach((config) => {
|
||||
// console.log(`modules/index.js config: ${JSON.stringify(config, null, 2)}`)
|
||||
// const {
|
||||
// method = '',
|
||||
// route = '',
|
||||
// handlers = []
|
||||
// } = config
|
||||
const method = config.method || ''
|
||||
const route = config.route || ''
|
||||
const handlers = config.handlers || []
|
||||
|
||||
const lastHandler = handlers.pop()
|
||||
|
||||
instance[method.toLowerCase()](
|
||||
route,
|
||||
...handlers,
|
||||
async function (ctx) {
|
||||
// console.log(`typeof lastHandler: ${typeof (lastHandler)}`)
|
||||
// return await lastHandler(ctx)
|
||||
return lastHandler(ctx)
|
||||
}
|
||||
)
|
||||
|
||||
// console.log(`instance: ${JSON.stringify(instance, null, 2)}`)
|
||||
|
||||
app.use(instance.routes()).use(instance.allowedMethods())
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
_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)) {
|
||||
ctx.body = {
|
||||
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)
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
data: filteredData
|
||||
}
|
||||
}
|
||||
|
||||
// Password does not match password in config file.
|
||||
} else {
|
||||
ctx.body = {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.message) {
|
||||
ctx.throw(500, err.message)
|
||||
} else {
|
||||
ctx.throw(500, 'Unhandled error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 logapi/controller.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 logapi/controller.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 readLines()')
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogsApi
|
||||
@@ -0,0 +1,35 @@
|
||||
// const validator = require('../../middleware/validators')
|
||||
const LogApi = require('./controller')
|
||||
const logApi = new LogApi()
|
||||
|
||||
module.exports.baseUrl = '/logapi'
|
||||
|
||||
module.exports.routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/',
|
||||
handlers: [logApi.getLogs]
|
||||
}
|
||||
/*
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/',
|
||||
handlers: [validator.ensureUser, user.getUsers]
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/:id',
|
||||
handlers: [validator.ensureUser, user.getUser]
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
route: '/:id',
|
||||
handlers: [validator.ensureTargetUserOrAdmin, user.getUser, user.updateUser]
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
route: '/:id',
|
||||
handlers: [validator.ensureTargetUserOrAdmin, user.getUser, user.deleteUser]
|
||||
}
|
||||
*/
|
||||
]
|
||||
@@ -0,0 +1,277 @@
|
||||
// User database model.
|
||||
const User = require('../../models/users')
|
||||
|
||||
// User library for business logic.
|
||||
const UserLib = require('../../lib/users')
|
||||
|
||||
const wlogger = require('../../lib/wlogger')
|
||||
|
||||
let _this
|
||||
class UserController {
|
||||
constructor () {
|
||||
// Encapsulate dependencies
|
||||
this.User = User
|
||||
this.userLib = new UserLib()
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /users Create a new user
|
||||
* @apiPermission user
|
||||
* @apiName CreateUser
|
||||
* @apiGroup Users
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "user": { "email": "email@format.com", "password": "secretpasas" } }' localhost:5001/users
|
||||
*
|
||||
* @apiParam {Object} user User object (required)
|
||||
* @apiParam {String} user.email Email.
|
||||
* @apiParam {String} user.password Password.
|
||||
*
|
||||
* @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"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @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.userLib.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 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.userLib.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 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.userLib.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 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.
|
||||
*
|
||||
* @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.userLib.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 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.userLib.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)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Email Format
|
||||
async validateEmail (email) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserController
|
||||
@@ -0,0 +1,50 @@
|
||||
const VALIDATOR = require('../../middleware/validators')
|
||||
const validator = new VALIDATOR()
|
||||
|
||||
const CONTROLLER = require('./controller')
|
||||
const controller = new CONTROLLER()
|
||||
|
||||
// export const baseUrl = '/users'
|
||||
module.exports.baseUrl = '/users'
|
||||
|
||||
module.exports.routes = [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/',
|
||||
handlers: [controller.createUser]
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/',
|
||||
handlers: [
|
||||
validator.ensureUser,
|
||||
controller.getUsers
|
||||
]
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
route: '/:id',
|
||||
handlers: [
|
||||
validator.ensureUser,
|
||||
controller.getUser
|
||||
]
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
route: '/:id',
|
||||
handlers: [
|
||||
validator.ensureTargetUserOrAdmin,
|
||||
controller.getUser,
|
||||
controller.updateUser
|
||||
]
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
route: '/:id',
|
||||
handlers: [
|
||||
validator.ensureTargetUserOrAdmin,
|
||||
controller.getUser,
|
||||
controller.deleteUser
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
# Automated End-to-end Tests
|
||||
|
||||
This contains the original boilerplate tests, which are end-to-end tests. These tests are fully automated and test the system directly by making REST API calls with axios.
|
||||
|
||||
These tests function exactly the same as a normal user would, by making real REST API calls to the software. As a result, they are fine for testing internal system components like authorization and user handling. However, they are inappropriate for testing sophisticated endpoints that involve complex operations. For example, interacting with a blockchain, pinging other network systems, or writing data to a secondary database.
|
||||
|
||||
There is some redundancy between these tests and the unit tests. The focus is on *how* the tests are executed. The unit tests call the libraries directly (internally). These e2e tests use the REST API (externally).
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
End-to-end tests for /auth endpoints.
|
||||
|
||||
This test sets up the environment for other e2e tests.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const axios = require('axios').default
|
||||
|
||||
// Local support libraries
|
||||
const config = require('../../../config')
|
||||
const app = require('../../../bin/server')
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
const AdminLib = require('../../../src/lib/admin')
|
||||
const adminLib = new AdminLib()
|
||||
|
||||
// const request = supertest.agent(app.listen())
|
||||
const context = {}
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,786 @@
|
||||
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/modules/users/controller')
|
||||
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(() => {
|
||||
uut = new UserController()
|
||||
|
||||
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.userLib, '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) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.equal(err.response.data, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /users/:id', () => {
|
||||
it('should not fetch user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw 404 if user doesn't exist", async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 404)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 422 for invalid input', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
}
|
||||
})
|
||||
|
||||
it('should fetch own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /users/:id', () => {
|
||||
it('should not update user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if non-admin updating other user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update user type', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'new name',
|
||||
type: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
// console.log(`Users: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
// assert(result.status === 200, 'Status Code 200 expected.')
|
||||
// assert(result.data.user.type === 'user', 'Type should be unchanged.')
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'type' can only be changed by Admin user"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update other user when not admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
name: 'This should not work'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if name property is wrong', async () => {
|
||||
try {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: {},
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (error) {
|
||||
assert.equal(error.response.status, 422)
|
||||
assert.include(error.response.data, "Property 'name' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if password property is not string', async () => {
|
||||
const { token } = context
|
||||
const _id = context.user._id
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
password: 1234,
|
||||
email: 'test@test.com',
|
||||
name: 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'password' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update if email is not string', async () => {
|
||||
const { token } = context
|
||||
const _id = context.user._id
|
||||
try {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 1234
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to update type property if is not string', async () => {
|
||||
try {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
type: 1,
|
||||
email: 'test@test.com',
|
||||
name: 'test',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(err.response.data, "Property 'type' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should be able to update other user when admin', async () => {
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
name: 'This should work',
|
||||
email: 'test4@test.com',
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
const userName = result.data.user.name
|
||||
assert.equal(userName, 'This should work')
|
||||
})
|
||||
|
||||
it('should update user with minimum inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: { email: 'testToUpdate@test.com' }
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
})
|
||||
|
||||
it('should update user with all inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: 'my name',
|
||||
username: 'myUsername'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'name')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.name, 'my name')
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
assert.equal(user.username, 'myUsername')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
it('should not delete user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if deleting invalid user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to delete other users unless admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should delete own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data.success)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
|
||||
it('should be able to delete other users when admin', async () => {
|
||||
const id = context.id2
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
# Unit Tests
|
||||
Unit tests are defined as testing the smallest possible unit of a function. They also do not make any live network calls.
|
||||
|
||||
Unit tests are broken up by directory:
|
||||
|
||||
- [biz-logic](./biz-logic) tests the business logic libraries.
|
||||
- [rest-api](./rest-api) tests the REST API specific handling of the router.
|
||||
- json-rpc (coming soon) tests the JSON-RPC routing using ipfs-coord library.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Business Logic Unit Tests
|
||||
|
||||
The unit tests in this directly are concerned with business logic libraries in the /src/lib folder. These are the methods that should be triggered by REST API endpoints. These tests are not concerned with the handling of the REST API request/response, but by the code that is triggered by those endpoints. It also tests any business logic that is not directly associated with a REST API endpoint.
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
Unit tests for the src/lib/users.js business logic library.
|
||||
|
||||
TODO: verify that an admin can change the type of a user
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const mongoose = require('mongoose')
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const config = require('../../../config')
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
|
||||
// Unit under test (uut)
|
||||
const UserLib = require('../../../src/lib/users')
|
||||
|
||||
describe('#users', () => {
|
||||
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
|
||||
})
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new UserLib()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
mongoose.connection.close()
|
||||
})
|
||||
|
||||
describe('#createUser', () => {
|
||||
it('should throw an error if no input is given', async () => {
|
||||
try {
|
||||
await uut.createUser()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
// assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if email is not provided', async () => {
|
||||
try {
|
||||
await uut.createUser({})
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if password is not provided', async () => {
|
||||
try {
|
||||
const usrObj = {
|
||||
email: 'test@test.com'
|
||||
}
|
||||
|
||||
await uut.createUser(usrObj)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'password' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if name is not provided', async () => {
|
||||
try {
|
||||
const usrObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'password'
|
||||
}
|
||||
|
||||
await uut.createUser(usrObj)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, "Property 'name' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should catch and throw DB errors', async () => {
|
||||
try {
|
||||
// Force an error with the database.
|
||||
sandbox.stub(uut, 'UserModel').throws(new Error('test error'))
|
||||
|
||||
const usrObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'test'
|
||||
}
|
||||
|
||||
await uut.createUser(usrObj)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should create a new user in the DB', async () => {
|
||||
// Note: The user created in this test is used by the getUser, update,
|
||||
// and delete tests.
|
||||
|
||||
const usrObj = {
|
||||
email: 'test01@test.com',
|
||||
password: 'test',
|
||||
name: 'test01'
|
||||
}
|
||||
|
||||
const { userData, token } = await uut.createUser(usrObj)
|
||||
|
||||
testUser = userData
|
||||
|
||||
// Assert that the user model has the expected properties with expected values.
|
||||
assert.property(userData, 'type')
|
||||
assert.equal(userData.type, 'user')
|
||||
assert.property(userData, '_id')
|
||||
assert.property(userData, 'email')
|
||||
assert.property(userData, 'name')
|
||||
|
||||
// Assert that the JWT token was generated for this user.
|
||||
assert.isString(token)
|
||||
assert.include(token, 'eyJ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getAllUsers', () => {
|
||||
it('should return all users from the database', async () => {
|
||||
const users = await uut.getAllUsers()
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
assert.isArray(users)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error.
|
||||
sandbox.stub(uut.UserModel, 'find').rejects(new Error('test error'))
|
||||
|
||||
await uut.getAllUsers()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getUser', () => {
|
||||
it('should throw 422 if no id given.', async () => {
|
||||
try {
|
||||
await uut.getUser()
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 422 for malformed id', async () => {
|
||||
try {
|
||||
const params = { id: 1 }
|
||||
await uut.getUser(params)
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 422)
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 404 if user is not found', async () => {
|
||||
try {
|
||||
const params = { id: '5fa4bd7ee1828f5f4d3ed004' }
|
||||
await uut.getUser(params)
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.equal(err.status, 404)
|
||||
assert.include(err.message, 'User not found')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return the user model', async () => {
|
||||
const params = { id: testUser._id }
|
||||
const result = await uut.getUser(params)
|
||||
// console.log('result: ', result)
|
||||
|
||||
// Replace the JSON model with an actual Mongoos model. Used by later
|
||||
// test cases.
|
||||
testUser = result
|
||||
|
||||
// Assert that the expected properties for the user model exist.
|
||||
assert.property(result, 'type')
|
||||
assert.property(result, '_id')
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#updateUser', () => {
|
||||
it('should throw an error if no input is given', async () => {
|
||||
try {
|
||||
await uut.updateUser()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if email is not a string', async () => {
|
||||
try {
|
||||
await uut.updateUser(testUser, {
|
||||
email: 1234
|
||||
})
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'email' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if name is not a string', async () => {
|
||||
try {
|
||||
const newData = {
|
||||
name: 1234
|
||||
}
|
||||
|
||||
await uut.updateUser(testUser, newData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'name' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if non-string password given', async () => {
|
||||
try {
|
||||
const newData = {
|
||||
email: 'test@test.com',
|
||||
name: 'test',
|
||||
password: 1234
|
||||
}
|
||||
|
||||
await uut.updateUser(testUser, newData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'password' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error for malformed type given', async () => {
|
||||
try {
|
||||
const newData = {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'test',
|
||||
type: 1234
|
||||
}
|
||||
|
||||
await uut.updateUser(testUser, newData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'type' must be a string!")
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if normal user tries to change themselves into an admin', async () => {
|
||||
try {
|
||||
const newData = {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'test',
|
||||
type: 'admin'
|
||||
}
|
||||
|
||||
await uut.updateUser(testUser, newData)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, "Property 'type' can only be changed by Admin user")
|
||||
}
|
||||
})
|
||||
|
||||
it('should update the user model', async () => {
|
||||
const newData = {
|
||||
email: 'test@test.com',
|
||||
password: 'password',
|
||||
name: 'testy tester'
|
||||
}
|
||||
|
||||
const result = await uut.updateUser(testUser, newData)
|
||||
|
||||
// Assert that expected properties and values exist.
|
||||
assert.property(result, '_id')
|
||||
assert.property(result, 'email')
|
||||
assert.equal(result.email, 'test@test.com')
|
||||
assert.property(result, 'name')
|
||||
assert.equal(result.name, 'testy tester')
|
||||
})
|
||||
|
||||
// TODO: verify that an admin can change the type of a user
|
||||
})
|
||||
|
||||
describe('#deleteUser', () => {
|
||||
it('should throw error if no user provided', async () => {
|
||||
try {
|
||||
await uut.deleteUser()
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Cannot read property')
|
||||
}
|
||||
})
|
||||
|
||||
it('should delete the user from the database', async () => {
|
||||
await uut.deleteUser(testUser)
|
||||
|
||||
assert.isOk('Not throwing an error is a pass!')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
// Ripped from https://github.com/koajs/koa/blob/master/test/helpers/context.js
|
||||
// Solution courtesy of user @fl0w. See: https://github.com/koajs/koa/issues/999#issuecomment-309270599
|
||||
// Take from this gist: https://gist.github.com/emmanuelnk/f1254eed8f947a81e8d715476d9cc92c
|
||||
|
||||
// if you want more comprehensive Koa Context object to test stuff like Cookies etc
|
||||
// then use https://www.npmjs.com/package/@shopify/jest-koa-mocks (requires Jest)
|
||||
|
||||
// INSTRUCTIONS:
|
||||
// Import in test file as below:
|
||||
//
|
||||
// const mockContext = require('./mocks/ctx-mock').context
|
||||
// const ctx = mockContext()
|
||||
// ...
|
||||
|
||||
const Stream = require('stream')
|
||||
const Koa = require('koa')
|
||||
|
||||
const context = (req, res, app) => {
|
||||
const socket = new Stream.Duplex()
|
||||
|
||||
req = Object.assign(
|
||||
{ headers: {}, socket },
|
||||
Stream.Readable.prototype,
|
||||
req || {}
|
||||
)
|
||||
res = Object.assign(
|
||||
{ _headers: {}, socket },
|
||||
Stream.Writable.prototype,
|
||||
res || {}
|
||||
)
|
||||
req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1'
|
||||
app = app || new Koa()
|
||||
res.getHeader = k => res._headers[k.toLowerCase()]
|
||||
res.setHeader = (k, v) => (res._headers[k.toLowerCase()] = v)
|
||||
res.removeHeader = (k, v) => delete res._headers[k.toLowerCase()]
|
||||
|
||||
const retApp = app.createContext(req, res)
|
||||
|
||||
return retApp
|
||||
}
|
||||
|
||||
const request = (req, res, app) => context(req, res, app).request
|
||||
|
||||
const response = (req, res, app) => context(req, res, app).response
|
||||
|
||||
module.exports = {
|
||||
context,
|
||||
request,
|
||||
response
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Mocks representing an array of logs for the
|
||||
// Unit tests of logapi
|
||||
|
||||
const data = [
|
||||
{
|
||||
message: 'Error in lib/nodemailer.js/validateEmailArray()',
|
||||
level: 'error',
|
||||
timestamp: '2020-11-14T12:15:55.230Z'
|
||||
},
|
||||
{
|
||||
message: 'Error in lib/nodemailer.js/validateEmailArray()',
|
||||
level: 'error',
|
||||
timestamp: '2020-11-14T12:15:55.231Z'
|
||||
},
|
||||
{
|
||||
message: 'Error in lib/nodemailer.js/validateEmailArray()',
|
||||
level: 'error',
|
||||
timestamp: '2020-11-14T12:15:55.230Z'
|
||||
},
|
||||
{
|
||||
message: 'Error in lib/nodemailer.js/validateEmailArray()',
|
||||
level: 'error',
|
||||
timestamp: '2020-11-14T12:15:55.231Z'
|
||||
}
|
||||
]
|
||||
module.exports = {
|
||||
data
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
const app = require('../../bin/server')
|
||||
const utils = require('./utils')
|
||||
const config = require('../../config')
|
||||
const assert = require('chai').assert
|
||||
|
||||
const axios = require('axios').default
|
||||
|
||||
// const request = supertest.agent(app.listen())
|
||||
const context = {}
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
// await utils.cleanDb() // This should be first instruction.
|
||||
|
||||
await app.startServer() // This should be second instruction.
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
const testUser = await utils.createUser(userObj)
|
||||
console.log(`TestUser : ${testUser}`)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
it('should throw 401 if credentials are incorrect', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
console.log(
|
||||
`result stringified: ${JSON.stringify(result.data, null, 2)}`
|
||||
)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if email is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'wrongEmail',
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
}
|
||||
|
||||
await axios(options)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert(err.response.status === 401, 'Error code 401 expected.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should auth user', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: 'test@test.com',
|
||||
password: 'pass'
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
assert(result.status === 200, 'Status Code 200 expected.')
|
||||
assert(
|
||||
result.data.user.email === 'test@test.com',
|
||||
'Email of test expected'
|
||||
)
|
||||
assert(
|
||||
result.data.user.password === undefined,
|
||||
'Password expected to be omited'
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,851 @@
|
||||
const testUtils = require('./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/modules/users/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
const mockContext = require('./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'
|
||||
}
|
||||
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: ${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 UserController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /users', () => {
|
||||
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 email property provided in wrong format', async () => {
|
||||
// try {
|
||||
// const options = {
|
||||
// method: 'POST',
|
||||
// url: `${LOCALHOST}/users`,
|
||||
// data: {
|
||||
// user: {
|
||||
// email: 'badEmailFormat',
|
||||
// password: 'test'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// await axios(options)
|
||||
//
|
||||
// assert(false, 'Unexpected result')
|
||||
// } catch (err) {
|
||||
// assert.equal(err.response.status, 422)
|
||||
// assert.include(
|
||||
// err.response.data,
|
||||
// "Property 'email' must be email format"
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
|
||||
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(false, 'Unexpected result')
|
||||
} catch (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'
|
||||
}
|
||||
}
|
||||
}
|
||||
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 catch and handle errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'find').rejects(new Error('test error'))
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
await uut.getUsers(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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/1`,
|
||||
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 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'
|
||||
)
|
||||
})
|
||||
|
||||
it('should catch and handle errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').rejects(new Error('test error'))
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
|
||||
await uut.getUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Internal Server Error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle user not found', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.User, 'findById').resolves(false)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: 1 }
|
||||
|
||||
await uut.getUser(ctx)
|
||||
|
||||
assert.fail('Unexpected result')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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: {
|
||||
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: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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 project property is not array', 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: {
|
||||
projects: 'projects'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'projects' must be a Array!"
|
||||
)
|
||||
}
|
||||
})
|
||||
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 if email is wrong format', 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: 'badEmailFormat'
|
||||
}
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 422)
|
||||
assert.include(
|
||||
err.response.data,
|
||||
"Property 'email' must be email format!"
|
||||
)
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result stringified: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
const userName = result.data.user.name
|
||||
assert.equal(userName, 'This should work')
|
||||
})
|
||||
it('should update user with minimum inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: { email: 'testToUpdate@test.com' }
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
})
|
||||
|
||||
it('should update user with all inputs', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
data: {
|
||||
user: {
|
||||
email: 'testToUpdate@test.com',
|
||||
name: 'my name',
|
||||
username: 'myUsername'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
|
||||
const user = result.data.user
|
||||
// console.log(`user: ${util.inspect(user)}`)
|
||||
|
||||
assert.property(user, 'type')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'name')
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.equal(user._id, _id)
|
||||
assert.notProperty(
|
||||
user,
|
||||
'password',
|
||||
'Password property should not be returned'
|
||||
)
|
||||
assert.equal(user.name, 'my name')
|
||||
assert.equal(user.email, 'testToUpdate@test.com')
|
||||
assert.equal(user.username, 'myUsername')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
it('should not delete user if token is invalid', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer 1'
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw 401 if deleting invalid user', async () => {
|
||||
const { token } = context
|
||||
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/1`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
|
||||
assert.equal(true, false, 'Unexpected behavior')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be able to delete other users unless admin', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${context.user2._id.toString()}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.token}`
|
||||
}
|
||||
}
|
||||
await axios(options)
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
}
|
||||
})
|
||||
|
||||
it('should delete own user', async () => {
|
||||
const _id = context.user._id
|
||||
const token = context.token
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${_id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data.success)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
|
||||
it('should be able to delete other users when admin', async () => {
|
||||
const id = context.id2
|
||||
const adminJWT = context.adminJWT
|
||||
|
||||
const options = {
|
||||
method: 'DELETE',
|
||||
url: `${LOCALHOST}/users/${id}`,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Bearer ${adminJWT}`
|
||||
}
|
||||
}
|
||||
const result = await axios(options)
|
||||
// console.log(`result: ${util.inspect(result.data)}`)
|
||||
|
||||
assert.equal(result.data.success, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
const assert = require('chai').assert
|
||||
|
||||
const NodeMailer = require('../../src/lib/nodemailer')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
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 email property is wrong format', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test',
|
||||
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 email format!')
|
||||
}
|
||||
})
|
||||
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 \'message\' 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 format', 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, 'Array must contain emails format!')
|
||||
}
|
||||
})
|
||||
|
||||
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 throw error if payloadTitle property is not provided', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
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 \'payloadTitle\' must be a string!')
|
||||
}
|
||||
})
|
||||
it('should throw error if payloadTitle property is not string', async () => {
|
||||
try {
|
||||
const data = {
|
||||
email: 'test@email.com',
|
||||
formMessage: 'test msg',
|
||||
name: 'test name',
|
||||
subject: 'test subject',
|
||||
to: ['test2@email.com'],
|
||||
payloadTitle: true
|
||||
|
||||
}
|
||||
await uut.sendEmail(data)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Property \'payloadTitle\' must be a string!')
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email', 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',
|
||||
payloadTitle: 'test title'
|
||||
}
|
||||
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 throw error if email list contain wrong format', async () => {
|
||||
try {
|
||||
const emailList = [
|
||||
'wrongEmail',
|
||||
'bad format'
|
||||
]
|
||||
await uut.validateEmailArray(emailList)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Array must contain emails format!')
|
||||
}
|
||||
})
|
||||
it('should return true if email list contain email format', 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')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,267 @@
|
||||
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('./mocks/ctx-mock').context
|
||||
const ContactController = require('../../src/modules/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 email property is wrong format', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email',
|
||||
formMessage: 'test 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 email format!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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 'message' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if payloadTitle property is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test 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 'payloadTitle' must be a string!"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw error if payloadTitle property is not string', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/contact/email`,
|
||||
data: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
payloadTitle: 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 'payloadTitle' 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',
|
||||
payloadTitle: 'title',
|
||||
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',
|
||||
payloadTitle: 'title',
|
||||
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.nodemailer, 'sendEmail').resolves(true)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
payloadTitle: 'title'
|
||||
}
|
||||
}
|
||||
}
|
||||
await uut.email(ctx)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
|
||||
it('should send email with all input', async () => {
|
||||
try {
|
||||
// Mock live network calls.
|
||||
sandbox.stub(uut.nodemailer, 'sendEmail').resolves(true)
|
||||
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
body: {
|
||||
obj: {
|
||||
email: 'email@email.com',
|
||||
formMessage: 'test message',
|
||||
payloadTitle: 'title',
|
||||
emailList: ['email@email.com']
|
||||
}
|
||||
}
|
||||
}
|
||||
await uut.email(ctx)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
const assert = require('chai').assert
|
||||
const PassportLib = require('../../src/lib/passport')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
describe('#passport.js', () => {
|
||||
beforeEach(() => {
|
||||
uut = new PassportLib()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('authUser()', () => {
|
||||
it('should throw error if ctx is not provided', async () => {
|
||||
try {
|
||||
await uut.authUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'ctx is required')
|
||||
}
|
||||
})
|
||||
|
||||
it('Should throw error if the passport library fails', async () => {
|
||||
try {
|
||||
const error = new Error('cant auth user')
|
||||
const user = null
|
||||
|
||||
// Mock calls
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.passport, 'authenticate').yields(error, user)
|
||||
|
||||
const ctx = {}
|
||||
await uut.authUser(ctx)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'cant auth user')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,251 @@
|
||||
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/modules/logapi/controller')
|
||||
const mockContext = require('./mocks/ctx-mock').context
|
||||
const mockData = require('./mocks/log-api-mock')
|
||||
|
||||
const context = {}
|
||||
let sandbox
|
||||
let uut
|
||||
describe('LogsApi', () => {
|
||||
beforeEach(() => {
|
||||
uut = new LogsController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('POST /logapi', () => {
|
||||
it('should return false if password is not provided', async () => {
|
||||
try {
|
||||
const options = {
|
||||
method: 'post',
|
||||
url: `${LOCALHOST}/logapi`,
|
||||
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}/logapi`,
|
||||
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, 'generateFileName').resolves('bad router')
|
||||
|
||||
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.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.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')
|
||||
}
|
||||
})
|
||||
})
|
||||
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,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/lib/utils/json-files')
|
||||
|
||||
const JSON_FILE = 'test-json-file.json'
|
||||
const JSON_PATH = `${__dirname.toString()}/${JSON_FILE}`
|
||||
|
||||
const deleteFile = filepath => {
|
||||
try {
|
||||
// Delete state if exist
|
||||
fs.unlinkSync(filepath)
|
||||
} catch (error) {}
|
||||
}
|
||||
let sandbox
|
||||
let uut
|
||||
describe('JsonFiles', () => {
|
||||
const obj = {
|
||||
json: 'file'
|
||||
}
|
||||
beforeEach(() => {
|
||||
uut = new JsonFiles()
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
deleteFile(JSON_PATH)
|
||||
})
|
||||
describe('writeJSON()', () => {
|
||||
it('should throw error if inputs is not provided', async () => {
|
||||
try {
|
||||
await uut.writeJSON()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'obj property is required')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename property is not provided', async () => {
|
||||
try {
|
||||
await uut.writeJSON(obj)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename property is not string', async () => {
|
||||
try {
|
||||
await uut.writeJSON(obj, 1)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if fs library return an error', async () => {
|
||||
try {
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.fs, 'writeFile').yields(new Error('test error'))
|
||||
|
||||
await uut.writeJSON(obj, JSON_PATH)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should write a json file', async () => {
|
||||
try {
|
||||
await uut.writeJSON(obj, JSON_PATH)
|
||||
|
||||
assert.isTrue(fs.existsSync(JSON_PATH))
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('readJSON()', () => {
|
||||
it('should throw error if filename property is not provided', async () => {
|
||||
try {
|
||||
await uut.readJSON(obj)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if filename property is not string', async () => {
|
||||
try {
|
||||
await uut.readJSON(obj, 1)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'fileName property must be a string')
|
||||
}
|
||||
})
|
||||
it('should throw error if fs library return an error', async () => {
|
||||
try {
|
||||
// https://sinonjs.org/releases/latest/stubs/
|
||||
// About yields
|
||||
sandbox.stub(uut.fs, 'readFile').yields(new Error('test error'))
|
||||
|
||||
await uut.readJSON(JSON_PATH)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should throw error if file not found', async () => {
|
||||
try {
|
||||
const testError = new Error('test error')
|
||||
testError.code = 'ENOENT'
|
||||
|
||||
sandbox.stub(uut.fs, 'readFile').yields(testError)
|
||||
|
||||
await uut.readJSON(JSON_PATH)
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should read a json file', async () => {
|
||||
try {
|
||||
const result = await uut.readJSON(JSON_PATH)
|
||||
|
||||
const objKeys = Object.keys(obj)
|
||||
const resultKeys = Object.keys(result)
|
||||
|
||||
assert.isObject(result)
|
||||
assert.equal(objKeys.length, resultKeys.length)
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,317 @@
|
||||
const assert = require('chai').assert
|
||||
const testUtils = require('./utils')
|
||||
|
||||
const Validators = require('../../src/middleware/validators')
|
||||
|
||||
const sinon = require('sinon')
|
||||
const mockContext = require('./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: 'test2@test.com',
|
||||
password: 'pass2'
|
||||
}
|
||||
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 trigger the "next" function if user is admin', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
// Function that execute if the validations
|
||||
// are successful
|
||||
const next = () => { return 'next function' }
|
||||
|
||||
const result = await uut.ensureUser(ctx, next)
|
||||
|
||||
assert.isString(result)
|
||||
assert.equal(result, 'next function')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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 trigger the "next" function if user is admin', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
// Function that execute if the validations
|
||||
// are successful
|
||||
const next = () => { return 'next function' }
|
||||
|
||||
const result = await uut.ensureAdmin(ctx, next)
|
||||
|
||||
assert.isString(result)
|
||||
assert.equal(result, 'next function')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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 trigger the "next" function if user is admin', async () => {
|
||||
try {
|
||||
// Mock the context object.
|
||||
const ctx = mockContext()
|
||||
ctx.params = { id: context.id }
|
||||
|
||||
ctx.request = {
|
||||
header: {
|
||||
authorization: `Bearer ${context.adminJWT}`
|
||||
}
|
||||
}
|
||||
// Function that execute if the validations
|
||||
// are successful
|
||||
const next = () => { return 'next function' }
|
||||
|
||||
const result = await uut.ensureTargetUserOrAdmin(ctx, next)
|
||||
|
||||
assert.isString(result)
|
||||
assert.equal(result, 'next function')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
const assert = require('chai').assert
|
||||
|
||||
const Admin = require('../../src/lib/admin')
|
||||
|
||||
const sinon = require('sinon')
|
||||
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
let sandbox
|
||||
let uut
|
||||
describe('Admin', () => {
|
||||
beforeEach(() => {
|
||||
uut = new Admin()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
describe('loginAdmin()', () => {
|
||||
it('should logind admin', async () => {
|
||||
try {
|
||||
const error = new Error('test error')
|
||||
error.response = {
|
||||
status: 422
|
||||
}
|
||||
// sandbox.stub(uut.axios, 'request').onFirstCall().throws(error)
|
||||
|
||||
const result = await uut.loginAdmin()
|
||||
const user = result.data.user
|
||||
|
||||
assert.property(user, '_id')
|
||||
assert.property(user, 'email')
|
||||
assert.property(user, 'type')
|
||||
|
||||
assert.isString(user._id)
|
||||
assert.isString(user.email)
|
||||
assert.isString(user.type)
|
||||
|
||||
assert.equal(user.type, 'admin')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
// Returns an erroneous password to force
|
||||
// an auth error
|
||||
sandbox
|
||||
.stub(uut.jsonFiles, 'readJSON').resolves({ password: 'wrong' })
|
||||
|
||||
await uut.loginAdmin()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.response.status, 401)
|
||||
assert.include(err.response.data, 'Unauthorized')
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('createSystemUser()', () => {
|
||||
it('should create admin', async () => {
|
||||
try {
|
||||
const result = await uut.createSystemUser()
|
||||
|
||||
assert.property(result, 'email')
|
||||
assert.property(result, 'password')
|
||||
assert.property(result, 'id')
|
||||
assert.property(result, 'token')
|
||||
} catch (err) {
|
||||
assert(false, 'Unexpected result')
|
||||
}
|
||||
})
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
const error2 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 500
|
||||
}
|
||||
// The loginAdmin() function in some use cases is recursive
|
||||
// after handling the 422 error, it gets called again
|
||||
sandbox
|
||||
.stub(uut.axios, 'request')
|
||||
.onFirstCall()
|
||||
.throws(error1)
|
||||
.onSecondCall()
|
||||
.throws(error2)
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
it('should handle errors when remove user', async () => {
|
||||
try {
|
||||
const error1 = new Error('test error')
|
||||
error1.response = {
|
||||
status: 422
|
||||
}
|
||||
sandbox
|
||||
.stub(uut.axios, 'request').throws(error1)
|
||||
sandbox
|
||||
.stub(uut.User, 'deleteOne').throws(new Error('test error'))
|
||||
|
||||
await uut.createSystemUser()
|
||||
assert(false, 'Unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
const mongoose = require('mongoose')
|
||||
const config = require('../../config')
|
||||
const axios = require('axios').default
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
// Remove all collections from the DB.
|
||||
async function cleanDb () {
|
||||
for (const collection in mongoose.connection.collections) {
|
||||
const collections = mongoose.connection.collections
|
||||
if (collections.collection) {
|
||||
// const thisCollection = mongoose.connection.collections[collection]
|
||||
// console.log(`thisCollection: ${JSON.stringify(thisCollection, null, 2)}`)
|
||||
|
||||
await collection.deleteMany()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used to create new users.
|
||||
// userObj = {
|
||||
// username,
|
||||
// password
|
||||
// }
|
||||
async function createUser (userObj) {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: userObj.email,
|
||||
password: userObj.password
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
const retObj = {
|
||||
user: result.data.user,
|
||||
token: result.data.token
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
console.log('Error in utils.js/createUser(): ' + JSON.stringify(err, null, 2))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function loginTestUser () {
|
||||
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)}`)
|
||||
|
||||
const retObj = {
|
||||
token: result.data.token,
|
||||
user: result.data.user.username,
|
||||
id: result.data.user._id.toString()
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
console.log('Error authenticating test user: ' + JSON.stringify(err, null, 2))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function loginAdminUser () {
|
||||
try {
|
||||
const FILENAME = `../../config/system-user-${config.env}.json`
|
||||
const adminUserData = require(FILENAME)
|
||||
console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: adminUserData.email,
|
||||
password: adminUserData.password
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
const retObj = {
|
||||
token: result.data.token,
|
||||
user: result.data.user.username,
|
||||
id: result.data.user._id.toString()
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
console.log('Error authenticating test admin user: ' + JSON.stringify(err, null, 2))
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the admin user JWT token from the JSON file it's saved at.
|
||||
async function getAdminJWT () {
|
||||
try {
|
||||
// process.env.KOA_ENV = process.env.KOA_ENV || 'dev'
|
||||
// console.log(`env: ${process.env.KOA_ENV}`)
|
||||
|
||||
const FILENAME = `../../config/system-user-${config.env}.json`
|
||||
const adminUserData = require(FILENAME)
|
||||
// console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
|
||||
|
||||
return adminUserData.token
|
||||
} catch (err) {
|
||||
console.error('Error in test/utils.js/getAdminJWT()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cleanDb,
|
||||
createUser,
|
||||
loginTestUser,
|
||||
loginAdminUser,
|
||||
getAdminJWT
|
||||
}
|
||||
@@ -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,254 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Local support libraries
|
||||
const config = require('../../../config')
|
||||
const testUtils = require('../../utils/test-utils')
|
||||
const User = require('../../../src/models/users')
|
||||
|
||||
const UserController = require('../../../src/modules/users/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('Users', () => {
|
||||
let testUser = {}
|
||||
|
||||
before(async () => {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
// console.log(`config: ${JSON.stringify(config, null, 2)}`)
|
||||
|
||||
// Create a second test user.
|
||||
// const userObj = {
|
||||
// email: 'test2@test.com',
|
||||
// password: 'pass2'
|
||||
// }
|
||||
// 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: ${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 UserController()
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
after(() => {
|
||||
mongoose.connection.close()
|
||||
})
|
||||
|
||||
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.userLib, '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.userLib, '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.userLib, '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.userLib, '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: existingUser
|
||||
}
|
||||
ctx.request.body = {
|
||||
user: testUser
|
||||
}
|
||||
|
||||
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 = await User.findById(testUser._id)
|
||||
|
||||
ctx.body = {
|
||||
user: existingUser
|
||||
}
|
||||
|
||||
await uut.deleteUser(ctx)
|
||||
|
||||
// Assert the expected HTTP response
|
||||
assert.equal(ctx.status, 200)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
Utility functions used to prepare the environment for tests.
|
||||
*/
|
||||
|
||||
// Public NPM libraries
|
||||
const mongoose = require('mongoose')
|
||||
const axios = require('axios').default
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../config')
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
// Remove all collections from the DB.
|
||||
async function cleanDb () {
|
||||
for (const collection in mongoose.connection.collections) {
|
||||
const collections = mongoose.connection.collections
|
||||
if (collections.collection) {
|
||||
// const thisCollection = mongoose.connection.collections[collection]
|
||||
// console.log(`thisCollection: ${JSON.stringify(thisCollection, null, 2)}`)
|
||||
|
||||
await collection.deleteMany()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all users in the database. This ensures there is no previous state
|
||||
// to confuse tests.
|
||||
async function deleteAllUsers () {
|
||||
try {
|
||||
// Get all the users in the DB.
|
||||
const users = await User.find({}, '-password')
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
// Delete each user.
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const thisUser = users[i]
|
||||
await thisUser.remove()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in test-utils.js/deleteAllUsers()')
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used to create new users.
|
||||
// userObj = {
|
||||
// username,
|
||||
// password
|
||||
// }
|
||||
async function createUser (userObj) {
|
||||
try {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/users`,
|
||||
data: {
|
||||
user: {
|
||||
email: userObj.email,
|
||||
password: userObj.password,
|
||||
name: userObj.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
const retObj = {
|
||||
user: result.data.user,
|
||||
token: result.data.token
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error in utils.js/createUser(): ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function loginTestUser () {
|
||||
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)}`)
|
||||
|
||||
const retObj = {
|
||||
token: result.data.token,
|
||||
user: result.data.user.username,
|
||||
id: result.data.user._id.toString()
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function loginAdminUser () {
|
||||
try {
|
||||
const FILENAME = `../../config/system-user-${config.env}.json`
|
||||
const adminUserData = require(FILENAME)
|
||||
console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: `${LOCALHOST}/auth`,
|
||||
data: {
|
||||
email: adminUserData.email,
|
||||
password: adminUserData.password,
|
||||
name: 'admin'
|
||||
}
|
||||
}
|
||||
|
||||
const result = await axios(options)
|
||||
|
||||
// console.log(`result: ${JSON.stringify(result.data, null, 2)}`)
|
||||
|
||||
const retObj = {
|
||||
token: result.data.token,
|
||||
user: result.data.user.username,
|
||||
id: result.data.user._id.toString()
|
||||
}
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'Error authenticating test admin user: ' + JSON.stringify(err, null, 2)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the admin user JWT token from the JSON file it's saved at.
|
||||
async function getAdminJWT () {
|
||||
try {
|
||||
// process.env.KOA_ENV = process.env.KOA_ENV || 'dev'
|
||||
// console.log(`env: ${process.env.KOA_ENV}`)
|
||||
|
||||
const FILENAME = `../../config/system-user-${config.env}.json`
|
||||
const adminUserData = require(FILENAME)
|
||||
// console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
|
||||
|
||||
return adminUserData.token
|
||||
} catch (err) {
|
||||
console.error('Error in test/utils.js/getAdminJWT()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cleanDb,
|
||||
createUser,
|
||||
loginTestUser,
|
||||
loginAdminUser,
|
||||
getAdminJWT,
|
||||
deleteAllUsers
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This directory contains utility functions for managing the database.
|
||||
@@ -0,0 +1,39 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const EMAIL = 'test@test.com'
|
||||
const PASSWORD = 'pass'
|
||||
|
||||
async function addUser () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(
|
||||
config.database,
|
||||
{ useNewUrlParser: true, useUnifiedTopology: true }
|
||||
)
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
const userData = {
|
||||
email: EMAIL,
|
||||
password: PASSWORD
|
||||
}
|
||||
|
||||
const user = new User(userData)
|
||||
|
||||
// Enforce default value of 'user'
|
||||
user.type = 'user'
|
||||
|
||||
await user.save()
|
||||
|
||||
await mongoose.connection.close()
|
||||
|
||||
console.log(`User ${EMAIL} created.`)
|
||||
}
|
||||
addUser()
|
||||
|
||||
module.exports = {
|
||||
addUser
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Force test environment
|
||||
// make sure environment variable is set before this file gets called.
|
||||
// see test script in package.json.
|
||||
// process.env.KOA_ENV = 'test'
|
||||
const config = require('../../config')
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
async function deleteUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true
|
||||
})
|
||||
|
||||
// Get all the users in the DB.
|
||||
const users = await User.find({}, '-password')
|
||||
// console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
// Delete each user.
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const thisUser = users[i]
|
||||
await thisUser.remove()
|
||||
}
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
|
||||
deleteUsers()
|
||||
@@ -0,0 +1,21 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const config = require('../../config')
|
||||
|
||||
const User = require('../../src/models/users')
|
||||
|
||||
async function getUsers () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(
|
||||
config.database,
|
||||
{ useNewUrlParser: true, useUnifiedTopology: true }
|
||||
)
|
||||
|
||||
const users = await User.find({}, '-password')
|
||||
console.log(`users: ${JSON.stringify(users, null, 2)}`)
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
getUsers()
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
Here's how to wipe the db:
|
||||
1. mongo
|
||||
2. use koa-server-dev
|
||||
3. db.dropDatabase()
|
||||
4. exit
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Utility app to wipe the test database.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// Force test environment
|
||||
process.env.KOA_ENV = 'test'
|
||||
const config = require('../config')
|
||||
|
||||
async function cleanDb () {
|
||||
// Connect to the Mongo Database.
|
||||
mongoose.Promise = global.Promise
|
||||
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
|
||||
await mongoose.connect(config.database, { useNewUrlParser: true })
|
||||
|
||||
console.log(`mongoose.connection.collections: ${JSON.stringify(mongoose.connection.collections, null, 2)}`)
|
||||
|
||||
for (const collection in mongoose.connection.collections) {
|
||||
const collections = mongoose.connection.collections
|
||||
if (collections.collection) {
|
||||
// const thisCollection = mongoose.connection.collections[collection]
|
||||
// console.log(`thisCollection: ${JSON.stringify(thisCollection, null, 2)}`)
|
||||
|
||||
await collection.deleteMany()
|
||||
}
|
||||
}
|
||||
|
||||
mongoose.connection.close()
|
||||
}
|
||||
cleanDb()
|
||||
Reference in New Issue
Block a user