First commit
This commit is contained in:
commit
2403bdfcee
18
.env.example
Normal file
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
################################################################################
|
||||||
|
# Environment Configuration
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
NODE_ENV=development
|
||||||
|
HOST=localhost
|
||||||
|
|
||||||
|
# Enable/disable protocols
|
||||||
|
HTTP=true
|
||||||
|
HTTPS=false
|
||||||
|
|
||||||
|
# Server ports
|
||||||
|
HTTP_PORT=8000
|
||||||
|
HTTPS_PORT=3443
|
||||||
|
|
||||||
|
# Certificate paths are relative to the server.js file
|
||||||
|
HTTPS_CONFIG_KEY=localhost.key
|
||||||
|
HTTPS_CONFIG_CERT=localhost.crt
|
73
.gitignore
vendored
Normal file
73
.gitignore
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Created by https://www.gitignore.io/api/node,osx,webstorm,eclipse
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.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
|
||||||
|
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
|
||||||
|
### OSX ###
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Don't commit generated docs
|
||||||
|
public/docs/*
|
||||||
|
public/api-docs/*
|
||||||
|
public/coverage/*
|
||||||
|
|
||||||
|
# Don't commit environment file
|
||||||
|
.env
|
111
.jscsrc
Normal file
111
.jscsrc
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"disallowEmptyBlocks": true,
|
||||||
|
"disallowKeywords": [
|
||||||
|
"with",
|
||||||
|
"eval"
|
||||||
|
],
|
||||||
|
"disallowKeywordsOnNewLine": [
|
||||||
|
"else"
|
||||||
|
],
|
||||||
|
"disallowMixedSpacesAndTabs": true,
|
||||||
|
"disallowMultipleLineBreaks": true,
|
||||||
|
"disallowMultipleLineStrings": true,
|
||||||
|
"disallowQuotedKeysInObjects": true,
|
||||||
|
"disallowSpaceAfterObjectKeys": true,
|
||||||
|
"disallowSpaceAfterPrefixUnaryOperators": false,
|
||||||
|
"disallowSpaceBeforeBinaryOperators": [
|
||||||
|
","
|
||||||
|
],
|
||||||
|
"disallowSpaceBeforeComma": {
|
||||||
|
"allExcept": [
|
||||||
|
"sparseArrays"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"disallowSpaceBeforePostfixUnaryOperators": [
|
||||||
|
"++",
|
||||||
|
"--"
|
||||||
|
],
|
||||||
|
"disallowSpaceBeforeSemicolon": true,
|
||||||
|
"disallowSpacesInCallExpression": true,
|
||||||
|
"disallowSpacesInFunctionDeclaration": {
|
||||||
|
"beforeOpeningRoundBrace": true
|
||||||
|
},
|
||||||
|
"disallowSpacesInNamedFunctionExpression": {
|
||||||
|
"beforeOpeningRoundBrace": true
|
||||||
|
},
|
||||||
|
"disallowSpacesInsideArrayBrackets": true,
|
||||||
|
"disallowSpacesInsideBrackets": true,
|
||||||
|
"disallowSpacesInsideParentheses": {
|
||||||
|
"only": [
|
||||||
|
"!",
|
||||||
|
"{",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"disallowTrailingWhitespace": true,
|
||||||
|
"disallowYodaConditions": true,
|
||||||
|
"esnext": true,
|
||||||
|
"requireBlocksOnNewline": 1,
|
||||||
|
"requireCamelCaseOrUpperCaseIdentifiers": true,
|
||||||
|
"requireCapitalizedConstructors": true,
|
||||||
|
"requireCommaBeforeLineBreak": true,
|
||||||
|
"requireCurlyBraces": [
|
||||||
|
"if",
|
||||||
|
"else",
|
||||||
|
"for",
|
||||||
|
"while",
|
||||||
|
"do",
|
||||||
|
"try",
|
||||||
|
"catch"
|
||||||
|
],
|
||||||
|
"requireDotNotation": {
|
||||||
|
"allExcept": [
|
||||||
|
"keywords"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"requireLineFeedAtFileEnd": false,
|
||||||
|
"requirePaddingNewLinesAfterBlocks": true,
|
||||||
|
"requirePaddingNewLinesBeforeLineComments": {
|
||||||
|
"allExcept": "firstAfterCurly"
|
||||||
|
},
|
||||||
|
"requireParenthesesAroundIIFE": true,
|
||||||
|
"requireSemicolons": true,
|
||||||
|
"requireSpaceAfterBinaryOperators": true,
|
||||||
|
"requireSpaceAfterComma": true,
|
||||||
|
"requireSpaceAfterKeywords": [
|
||||||
|
"if",
|
||||||
|
"else",
|
||||||
|
"for",
|
||||||
|
"while",
|
||||||
|
"do",
|
||||||
|
"switch",
|
||||||
|
"case",
|
||||||
|
"return",
|
||||||
|
"try",
|
||||||
|
"catch",
|
||||||
|
"typeof"
|
||||||
|
],
|
||||||
|
"requireSpaceBeforeBinaryOperators": true,
|
||||||
|
"requireSpaceBeforeBlockStatements": true,
|
||||||
|
"requireSpaceBetweenArguments": true,
|
||||||
|
"requireSpacesInAnonymousFunctionExpression": {
|
||||||
|
"beforeOpeningRoundBrace": true,
|
||||||
|
"allExcept": [
|
||||||
|
"shorthand"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"requireSpacesInConditionalExpression": true,
|
||||||
|
"requireSpacesInForStatement": true,
|
||||||
|
"requireSpacesInsideObjectBrackets": "all",
|
||||||
|
"requireTrailingComma": {
|
||||||
|
"ignoreSingleLine": true
|
||||||
|
},
|
||||||
|
"safeContextKeyword": "_this",
|
||||||
|
"validateIndentation": "\t",
|
||||||
|
"validateLineBreaks": "LF",
|
||||||
|
"validateQuoteMarks": {
|
||||||
|
"mark": "'",
|
||||||
|
"escape": true,
|
||||||
|
"ignoreJSX": true
|
||||||
|
}
|
||||||
|
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Timothy Warren
|
||||||
|
|
||||||
|
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.
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# ProgBlog
|
||||||
|
|
||||||
|
A simple node blog with built-in code snippet highlighting
|
123
app/Container.js
Normal file
123
app/Container.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const errors = require('errors');
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
const autoLoad = require('./config/container-autoload');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the appropriate path to a module relative to the 'app' folder
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} modulePath - the raw path to the module
|
||||||
|
* @return {string} - the normalized path to the module
|
||||||
|
*/
|
||||||
|
function normalizeIncludePath(modulePath) {
|
||||||
|
const basePath = path.resolve(path.join(__dirname, '../'));
|
||||||
|
|
||||||
|
let includePath = modulePath;
|
||||||
|
|
||||||
|
// Allow referencing local modules without using a ./
|
||||||
|
// eg. util/route-loader instead of ./util/route-loader
|
||||||
|
if (
|
||||||
|
modulePath.includes('/') &&
|
||||||
|
! (modulePath.startsWith('./') || modulePath.includes(__dirname))
|
||||||
|
) {
|
||||||
|
includePath = path.join(__dirname, modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return includePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for keeping track of dependencies
|
||||||
|
*/
|
||||||
|
class Container {
|
||||||
|
constructor() {
|
||||||
|
const app = express();
|
||||||
|
let container = new Map();
|
||||||
|
|
||||||
|
// Save the base app object
|
||||||
|
container.set('app', app);
|
||||||
|
|
||||||
|
// Preload some configured modules
|
||||||
|
autoLoad.map((module) => {
|
||||||
|
let moduleMap = module;
|
||||||
|
|
||||||
|
// Normalize config into [key, value]
|
||||||
|
if (! Array.isArray(module)) {
|
||||||
|
moduleMap = [module, module];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually require the module
|
||||||
|
moduleMap[1] = require(normalizeIncludePath(moduleMap[1]));
|
||||||
|
|
||||||
|
container.set.apply(container, moduleMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if an item exists in the container
|
||||||
|
*
|
||||||
|
* @param {string} name - name of the item
|
||||||
|
* @return {boolean} - whether the item exists in the container
|
||||||
|
*/
|
||||||
|
has(name) {
|
||||||
|
return this._container.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an existing object instance
|
||||||
|
*
|
||||||
|
* @param {string} name - name of the item
|
||||||
|
* @return {Object|undefined} - the item, or undefined if it doesn't exist
|
||||||
|
*/
|
||||||
|
get(name) {
|
||||||
|
if (this.has(name)) {
|
||||||
|
return this._container.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this._require(name);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an object in the container
|
||||||
|
*
|
||||||
|
* @param {string} name - name to associate with object
|
||||||
|
* @param {Object} object - item to keep track of
|
||||||
|
* @return {Container} - the container instance
|
||||||
|
*/
|
||||||
|
set(name, object) {
|
||||||
|
this._container.set(name, object);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a native require, relative to the lib folder,
|
||||||
|
* and returns the value
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} modulePath - name of the module to require
|
||||||
|
* @return {*} - the value returned from require
|
||||||
|
*/
|
||||||
|
_require(modulePath) {
|
||||||
|
// If the value is already saved, just return it
|
||||||
|
if (this.has(modulePath)) {
|
||||||
|
return this.get(modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the item for later
|
||||||
|
let item = require(normalizeIncludePath(modulePath));
|
||||||
|
this.set(modulePath, item);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Container();
|
104
app/base/ApiModel.js
Normal file
104
app/base/ApiModel.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('../Container'),
|
||||||
|
axios = require('axios');
|
||||||
|
const Model = container.get('base/Model');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for API Models
|
||||||
|
*
|
||||||
|
* Wraps convenience methods for making API requests.
|
||||||
|
*
|
||||||
|
* @param {string} [baseUrl] - a base url for api requests
|
||||||
|
* @extends Model
|
||||||
|
*/
|
||||||
|
class ApiModel extends Model {
|
||||||
|
/**
|
||||||
|
* Create a new APIModel
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {string} [baseUrl] - a base url for api requests
|
||||||
|
*/
|
||||||
|
constructor(baseUrl) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
let apiConfig = {};
|
||||||
|
|
||||||
|
if (baseUrl) {
|
||||||
|
apiConfig.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = axios(apiConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a 'HEAD' HTTP request
|
||||||
|
*
|
||||||
|
* @param {string} url - the url to request
|
||||||
|
* @param {Object} [config] - options for the current request
|
||||||
|
* @return {Promise} - promise wrapping the request
|
||||||
|
*/
|
||||||
|
head(url, config) {
|
||||||
|
return this.client.head(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a 'GET' HTTP request
|
||||||
|
*
|
||||||
|
* @param {string} url - the url to request
|
||||||
|
* @param {Object} [config] - options for the current request
|
||||||
|
* @return {Promise} - promise wrapping the request
|
||||||
|
*/
|
||||||
|
get(url, config) {
|
||||||
|
return this.client.get(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a 'DELETE' HTTP request
|
||||||
|
*
|
||||||
|
* @param {string} url - the url to request
|
||||||
|
* @param {Object} [config] - options for the current request
|
||||||
|
* @return {Promise} - promise wrapping the request
|
||||||
|
*/
|
||||||
|
delete(url, config) {
|
||||||
|
return this.client.delete(url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a 'POST' HTTP request
|
||||||
|
*
|
||||||
|
* @param {string} url - the url to request
|
||||||
|
* @param {Object} data - the data for the current request
|
||||||
|
* @param {Object} [config] - options for the current request
|
||||||
|
* @return {Promise} - promise wrapping the request
|
||||||
|
*/
|
||||||
|
post(url, data, config) {
|
||||||
|
return this.client.post(url, data, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a 'PUT' HTTP request
|
||||||
|
*
|
||||||
|
* @param {string} url - the url to request
|
||||||
|
* @param {Object} data - the data for the current request
|
||||||
|
* @param {Object} [config] - options for the current request
|
||||||
|
* @return {Promise} - promise wrapping the request
|
||||||
|
*/
|
||||||
|
put(url, data, config) {
|
||||||
|
return this.client.put(url, data, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a 'PATCH' HTTP request
|
||||||
|
*
|
||||||
|
* @param {string} url - the url to request
|
||||||
|
* @param {Object} data - the data for the current request
|
||||||
|
* @param {Object} [config] - options for the current request
|
||||||
|
* @return {Promise} - promise wrapping the request
|
||||||
|
*/
|
||||||
|
patch(url, data, config) {
|
||||||
|
return this.client.patch(url, data, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiModel;
|
180
app/base/Config.js
Normal file
180
app/base/Config.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('../Container');
|
||||||
|
const path = container.get('path');
|
||||||
|
|
||||||
|
// Load environment file
|
||||||
|
require('dotenv').config({
|
||||||
|
path: path.resolve(__dirname, '../../.env'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const glob = require('glob');
|
||||||
|
const configSslPath = path.resolve(__dirname, '../config/ssl');
|
||||||
|
const configPath = path.resolve(__dirname, '../config');
|
||||||
|
|
||||||
|
const defaultConfig = new Map([
|
||||||
|
['host', 'localhost'],
|
||||||
|
['http', true],
|
||||||
|
['http-port', 80],
|
||||||
|
['https', false],
|
||||||
|
['https-port', 443],
|
||||||
|
['node-env', 'development'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const configFiles = glob.sync(`${configPath}/**/*.js`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config management class
|
||||||
|
*
|
||||||
|
* Hierarchy of config options
|
||||||
|
* 1. Directly defined config options
|
||||||
|
* 2. Environment variables
|
||||||
|
* 3. Default values
|
||||||
|
*/
|
||||||
|
class Config {
|
||||||
|
constructor() {
|
||||||
|
let _configMap = new Map();
|
||||||
|
|
||||||
|
// Load files in config folder under their
|
||||||
|
// own respective namespaces
|
||||||
|
configFiles.forEach((fullPath) => {
|
||||||
|
let key = path.basename(fullPath, '.js');
|
||||||
|
_configMap.set(key, require(fullPath));
|
||||||
|
});
|
||||||
|
|
||||||
|
this._config = _configMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of available configuration keys
|
||||||
|
*
|
||||||
|
* @return {array} - the list of configuration keys
|
||||||
|
*/
|
||||||
|
keys() {
|
||||||
|
let keys = [];
|
||||||
|
|
||||||
|
for (let key of this._config.keys()) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a config value exists
|
||||||
|
*
|
||||||
|
* @param {string|symbol} key - the config value key
|
||||||
|
* @return {boolean} - whether the config value exists
|
||||||
|
*/
|
||||||
|
has(key) {
|
||||||
|
return this._config.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether an environment variable is currently defined
|
||||||
|
*
|
||||||
|
* @param {string} key - the environment varaible to look for
|
||||||
|
* @return {boolean} - whether the environment variable is defined
|
||||||
|
*/
|
||||||
|
hasEnv(key) {
|
||||||
|
return process.env[this._envName(key)] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a config value
|
||||||
|
*
|
||||||
|
* @param {string|symbol} key - the name of the config value
|
||||||
|
* @return {*} - the configuration value
|
||||||
|
*/
|
||||||
|
get(key) {
|
||||||
|
if (! this.has(key)) {
|
||||||
|
// Fallback to environment variables
|
||||||
|
let envKey = key.toUpperCase().replace('-', '_');
|
||||||
|
if (this.hasEnv(envKey)) {
|
||||||
|
let envValue = this.getEnv(envKey);
|
||||||
|
this._config.set(key, envValue);
|
||||||
|
return envValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default values
|
||||||
|
if (defaultConfig.has(key)) {
|
||||||
|
let defaultValue = defaultConfig.get(key);
|
||||||
|
this._config.set(key, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._config.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the value of an environment variable
|
||||||
|
*
|
||||||
|
* @param {string} key - the environment variable to get
|
||||||
|
* @return {string|undefined} - the value of the environment variable
|
||||||
|
*/
|
||||||
|
getEnv(key) {
|
||||||
|
let raw = process.env[this._envName(key)];
|
||||||
|
return this._normalizeValue(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get equivalent environment variable for config key
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} key - the config key
|
||||||
|
* @return {string} - the environment variable name
|
||||||
|
*/
|
||||||
|
_envName(key) {
|
||||||
|
return key.toUpperCase().replace('-', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to parse javascript types from strings
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} val - the string value
|
||||||
|
* @return {string|number|boolean} - the 'parsed' value
|
||||||
|
*/
|
||||||
|
_normalizeValue(val) {
|
||||||
|
let bool = /^true|false$/;
|
||||||
|
let integer = /^([0-9]+)$/;
|
||||||
|
|
||||||
|
if (String(val).search(integer) !== -1) {
|
||||||
|
return parseInt(val, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(val).search(bool) !== -1) {
|
||||||
|
switch (val) {
|
||||||
|
case 'true':
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// return overrides break
|
||||||
|
|
||||||
|
case 'false':
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// return overrides break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a config variable (mainly for testing)
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {mixed} key - the key to set
|
||||||
|
* @param {mixed} val - the value for the key
|
||||||
|
* @return {Config} - the config instance
|
||||||
|
*/
|
||||||
|
_set(key, val) {
|
||||||
|
this._config.set(key, val);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new Config();
|
74
app/base/Controller.js
Normal file
74
app/base/Controller.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('../Container');
|
||||||
|
const express = require('express');
|
||||||
|
const errors = require('errors');
|
||||||
|
const getArgs = require('getargs');
|
||||||
|
const loadRoutes = container.get('base/util/route-loader');
|
||||||
|
const _ = container.get('_');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller setup and utility class
|
||||||
|
*/
|
||||||
|
class Controller {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get route mapping
|
||||||
|
*
|
||||||
|
* @param {string} baseRoutePath - the path to the folder containing
|
||||||
|
* controllers/routes
|
||||||
|
* @return {map} - Maps route prefixes to their respective functions
|
||||||
|
*/
|
||||||
|
static getRouteMap(baseRoutePath) {
|
||||||
|
const routeMap = {};
|
||||||
|
const pathFileMap = loadRoutes(baseRoutePath);
|
||||||
|
|
||||||
|
_(pathFileMap).forEach((routeFile, routePath) => {
|
||||||
|
let rawRoutes = require(routeFile);
|
||||||
|
routeMap[routePath] = Controller._parseRoutes(rawRoutes);
|
||||||
|
});
|
||||||
|
|
||||||
|
return routeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplify route generation with an object
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Object} routeObject - the object laying out the routes for the
|
||||||
|
* current controller
|
||||||
|
* @return {Router} - the updated express.Router object
|
||||||
|
*/
|
||||||
|
static _parseRoutes(routeObject) {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
_(routeObject).forEach((httpMethods, currentPath) => {
|
||||||
|
_(httpMethods).forEach((routeFunction, currentHttpMethod) => {
|
||||||
|
let routerMethod = router[currentHttpMethod];
|
||||||
|
return router[currentHttpMethod](currentPath, routeFunction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a specific http error code
|
||||||
|
*
|
||||||
|
* @param {number} code - The error Number
|
||||||
|
* @param {Object} [options={}] - Options, such as message and explanation
|
||||||
|
* @param {function} next - The callback to pass the error to
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
static HttpError(/*code, options, next*/) {
|
||||||
|
let args = getArgs('code:Number, [options]:object, next:function', arguments);
|
||||||
|
args.options = args.options || {};
|
||||||
|
|
||||||
|
let methodName = `Http${args.code}Error`;
|
||||||
|
let err = new errors[methodName](args.options);
|
||||||
|
|
||||||
|
return args.next(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Controller;
|
33
app/base/HttpServer.js
Normal file
33
app/base/HttpServer.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const http = require('http'),
|
||||||
|
logger = require('winston');
|
||||||
|
|
||||||
|
const Server = require('./Server');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for creating an http server
|
||||||
|
*
|
||||||
|
* @extends Server
|
||||||
|
* @param {Express} app - current express instance
|
||||||
|
* @param {number} port - the port to listen on
|
||||||
|
*/
|
||||||
|
class HttpServer extends Server {
|
||||||
|
constructor(app, port) {
|
||||||
|
super();
|
||||||
|
let server = http.createServer(app);
|
||||||
|
server.listen(port);
|
||||||
|
server.on('error', this.onError);
|
||||||
|
server.on('listening', () => {
|
||||||
|
let addr = server.address();
|
||||||
|
let bind = typeof addr === 'string'
|
||||||
|
? `pipe ${addr}`
|
||||||
|
: `port ${addr.port}`;
|
||||||
|
logger.info(`Listening on ${bind}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HttpServer;
|
34
app/base/HttpsServer.js
Normal file
34
app/base/HttpsServer.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const https = require('https'),
|
||||||
|
logger = require('winston');
|
||||||
|
|
||||||
|
const Server = require('./Server');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for creating an https server
|
||||||
|
*
|
||||||
|
* @extends Server
|
||||||
|
* @param {Express} app - current express instance
|
||||||
|
* @param {number} port - the port to listen on
|
||||||
|
* @param {Object} options - https server options
|
||||||
|
*/
|
||||||
|
class HttpsServer extends Server {
|
||||||
|
constructor(app, port, options) {
|
||||||
|
super();
|
||||||
|
let server = https.createServer(options, app);
|
||||||
|
server.listen(port);
|
||||||
|
server.on('error', this.onError);
|
||||||
|
server.on('listening', () => {
|
||||||
|
let addr = server.address();
|
||||||
|
let bind = typeof addr === 'string'
|
||||||
|
? `pipe ${addr}`
|
||||||
|
: `port ${addr.port}`;
|
||||||
|
logger.info(`Listening on ${bind}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HttpsServer;
|
10
app/base/Model.js
Normal file
10
app/base/Model.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Model class
|
||||||
|
*/
|
||||||
|
class Model {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Model;
|
42
app/base/Server.js
Normal file
42
app/base/Server.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Class for wrapping HTTP/HTTPS servers
|
||||||
|
*/
|
||||||
|
class Server {
|
||||||
|
constructor() {}
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP(s) server "error" event.
|
||||||
|
*
|
||||||
|
* @param {error} error - the error object
|
||||||
|
* @return {null} - Does not return a value
|
||||||
|
* @throws {error}
|
||||||
|
*/
|
||||||
|
onError(error) {
|
||||||
|
if (error.syscall !== 'listen') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = this.server.address().port;
|
||||||
|
|
||||||
|
let bind = typeof port === 'string'
|
||||||
|
? `Pipe ${port}`
|
||||||
|
: `Port ${port}`;
|
||||||
|
|
||||||
|
// handle specific listen errors with friendly messages
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
logger.error(`${bind} requires elevated privileges`);
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
logger.error(`${bind} is already in use`);
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Server;
|
30
app/base/util/route-loader.js
Normal file
30
app/base/util/route-loader.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map Routes to route files
|
||||||
|
*
|
||||||
|
* @param {string} path - folder with Routes
|
||||||
|
* @return {Object} - Object mapping routes to their files
|
||||||
|
*/
|
||||||
|
module.exports = function routeLoader(path) {
|
||||||
|
|
||||||
|
const basePath = path.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
let paths = glob.sync(`${path}/**/*.js`);
|
||||||
|
paths = paths.sort();
|
||||||
|
paths = paths.map((path) => path.replace('\\', '/'));
|
||||||
|
|
||||||
|
let routes = {};
|
||||||
|
|
||||||
|
paths.forEach((path) => {
|
||||||
|
let routePath = path.replace(basePath, '')
|
||||||
|
.replace('.js', '')
|
||||||
|
.replace('index', '');
|
||||||
|
|
||||||
|
routes[routePath] = path;
|
||||||
|
});
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
};
|
31
app/bootstrap.js
vendored
Normal file
31
app/bootstrap.js
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('./Container');
|
||||||
|
const _ = container.get('_');
|
||||||
|
const app = container.get('app');
|
||||||
|
const path = container.get('path');
|
||||||
|
const Controller = container.get('base/Controller');
|
||||||
|
const Config = container.get('base/Config');
|
||||||
|
|
||||||
|
module.exports = (function () {
|
||||||
|
const baseRoutePath = path.join(__dirname, 'controllers');
|
||||||
|
|
||||||
|
// Set up templating
|
||||||
|
const view = Config.get('view-engine');
|
||||||
|
view.setup(app);
|
||||||
|
|
||||||
|
// load middleware
|
||||||
|
Config.get('middleware').forEach((mw) => app.use(mw));
|
||||||
|
|
||||||
|
// automatically set up routing by folder structure
|
||||||
|
let routeMap = Controller.getRouteMap(baseRoutePath);
|
||||||
|
_(routeMap).forEach((routeFunction, routePrefix) => {
|
||||||
|
app.use(routePrefix, routeFunction);
|
||||||
|
});
|
||||||
|
|
||||||
|
// load error handlers
|
||||||
|
Config.get('error-handlers').forEach((handler) => app.use(handler));
|
||||||
|
|
||||||
|
return app;
|
||||||
|
|
||||||
|
}());
|
15
app/config/container-autoload.js
Normal file
15
app/config/container-autoload.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of modules to insert into the Container at start up
|
||||||
|
*
|
||||||
|
* Modules to be loaded with an alias should have an array with the alias
|
||||||
|
* then the module name. Otherwise, just a string with the module name
|
||||||
|
*
|
||||||
|
* @type {array}
|
||||||
|
*/
|
||||||
|
module.exports = [
|
||||||
|
['_', 'lodash'],
|
||||||
|
'path',
|
||||||
|
['promisify', 'helpers/promisify'],
|
||||||
|
];
|
43
app/config/error-handlers.js
Normal file
43
app/config/error-handlers.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Error handlers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const container = require('../Container');
|
||||||
|
const app = container.get('app');
|
||||||
|
const HTTP_CODE_MAP = require('http').STATUS_CODES;
|
||||||
|
const errors = require('errors');
|
||||||
|
|
||||||
|
let errorHandlers = new Set([
|
||||||
|
|
||||||
|
function handle404(req, res, next) {
|
||||||
|
// if no route matches, send a 404
|
||||||
|
if (! req.route) {
|
||||||
|
let err = new errors.Http404Error();
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// general error handler
|
||||||
|
function handleError(err, req, res, next) {
|
||||||
|
let httpStatus = err.status || 500;
|
||||||
|
let message = err.message || HTTP_CODE_MAP[httpStatus];
|
||||||
|
|
||||||
|
res.status(httpStatus);
|
||||||
|
|
||||||
|
let output = {
|
||||||
|
status: httpStatus,
|
||||||
|
message: message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show stack trace in development environment
|
||||||
|
if (app.get('env') === 'development') {
|
||||||
|
output.error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(output);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
module.exports = errorHandlers;
|
40
app/config/middleware.js
Normal file
40
app/config/middleware.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Middleware
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
|
const express = require('express');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const requestLogger = require('morgan');
|
||||||
|
const path = require('path');
|
||||||
|
const session = require('express-session');
|
||||||
|
|
||||||
|
let middleware = new Set([
|
||||||
|
|
||||||
|
// some security settings controlled by helmet
|
||||||
|
helmet.frameguard(),
|
||||||
|
helmet.hidePoweredBy(),
|
||||||
|
helmet.ieNoOpen(),
|
||||||
|
helmet.noSniff(),
|
||||||
|
helmet.xssFilter(),
|
||||||
|
|
||||||
|
// session setup
|
||||||
|
session({
|
||||||
|
name: 'blogid',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
secret: '76al;p9vse',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// basic express middleware
|
||||||
|
requestLogger('combined'),
|
||||||
|
bodyParser.json(),
|
||||||
|
bodyParser.urlencoded({ extended: false }),
|
||||||
|
cookieParser(),
|
||||||
|
express.static(path.join(__dirname, '../../public')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
module.exports = middleware;
|
15
app/config/view-engine.js
Normal file
15
app/config/view-engine.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Stupid template engine requires coffescript for some reason
|
||||||
|
const cs = require('coffee-script');
|
||||||
|
cs.register();
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const hulk = require('hulk-hogan');
|
||||||
|
|
||||||
|
module.exports.setup = (app) => {
|
||||||
|
let viewPath = path.resolve(__dirname, '../views');
|
||||||
|
app.set('views', viewPath);
|
||||||
|
app.set('view options', { layout: false });
|
||||||
|
app.set('view engine', 'hulk');
|
||||||
|
};
|
20
app/controllers/index.js
Normal file
20
app/controllers/index.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'/': {
|
||||||
|
|
||||||
|
// Get homepage
|
||||||
|
get: (req, res) => {
|
||||||
|
return res.json({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
index: { title: 'Express' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
put: (req, res, next) => {
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
26
app/helpers/promisify.js
Normal file
26
app/helpers/promisify.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*eslint-disable prefer-arrow-callback*/
|
||||||
|
/**
|
||||||
|
* Function to convert a callback function into a promise
|
||||||
|
*
|
||||||
|
* @see http://eddmann.com/posts/promisifying-error-first-asynchronous-callbacks-in-javascript/
|
||||||
|
* @example promisify(fs.readFile)('hello.txt', 'utf8')
|
||||||
|
* .then(console.log)
|
||||||
|
* .catch(console.error)
|
||||||
|
* @param {Function} fn - the callback function to convert
|
||||||
|
* @return {Promise} - the new promise
|
||||||
|
*/
|
||||||
|
function promisify(fn) {
|
||||||
|
return function () {
|
||||||
|
let args = [].slice.call(arguments);
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
fn.apply(undefined, args.concat((error, value) => {
|
||||||
|
return error ? reject(error) : resolve(value);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = promisify;
|
||||||
|
/*eslint-enable prefer-arrow-callback*/
|
245
gulpfile.js
Normal file
245
gulpfile.js
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const apidoc = require('gulp-apidoc'),
|
||||||
|
documentation = require('gulp-documentation'),
|
||||||
|
eslint = require('gulp-eslint'),
|
||||||
|
gulp = require('gulp'),
|
||||||
|
istanbul = require('gulp-istanbul'),
|
||||||
|
jscs = require('gulp-jscs'),
|
||||||
|
mocha = require('gulp-mocha'),
|
||||||
|
pipe = require('gulp-pipe'),
|
||||||
|
nsp = require('gulp-nsp');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path(s) to all source files
|
||||||
|
*/
|
||||||
|
const SRC_FILES = [
|
||||||
|
'app/base/**/*.js',
|
||||||
|
'app/config/**/*.js',
|
||||||
|
'app/controllers/**/*.js',
|
||||||
|
'app/helpers/**/*.js',
|
||||||
|
'app/models/**/*.js',
|
||||||
|
'app/*.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to unit test files
|
||||||
|
*/
|
||||||
|
const UNIT_TEST_FILES = ['test/unit/**/*_test.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to integration test files
|
||||||
|
*/
|
||||||
|
const INTEGRATION_TEST_FILES = ['test/integration/**/*_test.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path(s) to all test files
|
||||||
|
*/
|
||||||
|
const TEST_FILES = ['test/**/*_test.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configuration values for eslint
|
||||||
|
*/
|
||||||
|
const ESLINT_SETTINGS = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Each rule has an error level (0-2), and some have extra parameters
|
||||||
|
// 0 turns a rule off
|
||||||
|
// 1 makes a rule give a warning
|
||||||
|
// 2 makes a rule fail linting
|
||||||
|
rules: {
|
||||||
|
'linebreak-style': [2, 'unix'], // Only unix line endings
|
||||||
|
'arrow-parens': [2, 'always'], // No parens on arrow functions with one param
|
||||||
|
'no-console': [1], // Avoid using console methods
|
||||||
|
'no-constant-condition': [1],
|
||||||
|
'no-extra-semi': [1], // Enliminate extra semicolons
|
||||||
|
'no-func-assign': [1],
|
||||||
|
'no-obj-calls': [2],
|
||||||
|
'no-unexpected-multiline': [2], // Catch syntax errors due to automatic semicolon insertion
|
||||||
|
'no-unneeded-ternary': [2], // Avoid redundant ternary expressions
|
||||||
|
radix: [2], // Require radix parameter on parseInt
|
||||||
|
'no-with': [2], // No use of with construct
|
||||||
|
'no-eval': [2], // No use of eval
|
||||||
|
'no-unreachable': [1], // Avoid code that is not reachable
|
||||||
|
'no-irregular-whitespace': [1], // Highlight whitespace that isn't a tab or space
|
||||||
|
'no-new-wrappers': [2], // Avoid using primitive constructors
|
||||||
|
'no-new-func': [2], // Avoid Function constructor eval
|
||||||
|
curly: [2, 'multi-line'], // Require braces for if,for,while,do contructs that are not on the same line
|
||||||
|
'no-implied-eval': [2], // Avoid camoflauged eval
|
||||||
|
'no-invalid-this': [2],
|
||||||
|
'constructor-super': [2],
|
||||||
|
'no-dupe-args': [2], // Disallow functions to have more than one parameter with the same name
|
||||||
|
'no-dupe-keys': [2], // Disaalow objects to have more than one property with the same name
|
||||||
|
'no-dupe-class-members': [2], // Disallow classes to have more than one method/memeber with the same name
|
||||||
|
'no-this-before-super': [2],
|
||||||
|
'prefer-arrow-callback': [1], // Prefer arrow functions for callbacks
|
||||||
|
'no-var': [2], // Use let or const instead of var
|
||||||
|
'valid-jsdoc': [1],
|
||||||
|
semi: [2, 'always'], // Require use of semicolons
|
||||||
|
strict: [2, 'global'], // have a global 'use strict' in every code file
|
||||||
|
'callback-return': [1], // return when invoking a callback
|
||||||
|
'object-shorthand': [1, 'methods'], // Prefer shorthand for functions in object literals/classes, but avoid property shorthand
|
||||||
|
'prefer-template': [1], // Prefer template strings eg. `Hello ${name}`, to string concatenation
|
||||||
|
'no-case-declarations': [2], // Don't define variables in switch labels
|
||||||
|
'no-const-assign': [2], // Highlight instances where assigning to const declaration
|
||||||
|
'no-new-symbol': [2], // Symbol is not a constructor, don't use the new keyword
|
||||||
|
'no-unused-labels': [2], // Error on labels in code that aren't used
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configuration values for mocha
|
||||||
|
*/
|
||||||
|
const MOCHA_SETTINGS = {
|
||||||
|
ui: 'tdd',
|
||||||
|
bail: true,
|
||||||
|
slow: 1000,
|
||||||
|
timeout: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check syntax and style of test/miscellaneous files
|
||||||
|
*/
|
||||||
|
gulp.task('lint-tests', () => {
|
||||||
|
const LINT_TESTS_FILES = TEST_FILES.concat([
|
||||||
|
'gulpfile.js',
|
||||||
|
'server.js',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// eslint
|
||||||
|
pipe(gulp.src(LINT_TESTS_FILES), [
|
||||||
|
eslint(ESLINT_SETTINGS),
|
||||||
|
eslint.format(),
|
||||||
|
eslint.failAfterError(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// JSCS rules are defined in /.jscsrc
|
||||||
|
pipe(gulp.src(['test/**/*.js', 'gulpfile.js']), [
|
||||||
|
jscs(),
|
||||||
|
jscs.reporter(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check syntax and style of source files
|
||||||
|
*/
|
||||||
|
gulp.task('lint-src', () => {
|
||||||
|
// eslint
|
||||||
|
pipe(gulp.src(SRC_FILES), [
|
||||||
|
eslint(ESLINT_SETTINGS),
|
||||||
|
eslint.format(),
|
||||||
|
eslint.failAfterError(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// JSCS
|
||||||
|
// JSCS rules are defined in /.jscsrc
|
||||||
|
pipe(gulp.src(SRC_FILES), [
|
||||||
|
jscs(),
|
||||||
|
jscs.reporter(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all lint tasks
|
||||||
|
*/
|
||||||
|
gulp.task('lint', ['lint-src', 'lint-tests']);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create internal method documentation of source files
|
||||||
|
*/
|
||||||
|
gulp.task('src-docs', () => {
|
||||||
|
pipe(gulp.src(SRC_FILES), [
|
||||||
|
documentation({
|
||||||
|
format: 'html',
|
||||||
|
}),
|
||||||
|
gulp.dest('public/docs'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create api documentation from source files
|
||||||
|
*/
|
||||||
|
gulp.task('api-docs', (done) => {
|
||||||
|
apidoc({
|
||||||
|
src: 'app/',
|
||||||
|
dest: 'public/api-docs/',
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all documentation generation tasks
|
||||||
|
*/
|
||||||
|
gulp.task('docs', ['src-docs', 'api-docs']);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run integration tests
|
||||||
|
*/
|
||||||
|
gulp.task('integration-test', ['lint'], () => {
|
||||||
|
return gulp.src(INTEGRATION_TEST_FILES)
|
||||||
|
.pipe(mocha(MOCHA_SETTINGS));
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run unit tests
|
||||||
|
*/
|
||||||
|
gulp.task('unit-test', ['lint'], () => {
|
||||||
|
return gulp.src(UNIT_TEST_FILES)
|
||||||
|
.pipe(mocha(MOCHA_SETTINGS));
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all tests
|
||||||
|
*/
|
||||||
|
gulp.task('test', ['lint'], () => {
|
||||||
|
return gulp.src(TEST_FILES)
|
||||||
|
.pipe(mocha(MOCHA_SETTINGS));
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run hooks for istanbul coverage generation
|
||||||
|
*/
|
||||||
|
gulp.task('pre-coverage', () => {
|
||||||
|
return pipe(gulp.src(SRC_FILES), [
|
||||||
|
istanbul(),
|
||||||
|
istanbul.hookRequire(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run unit tests and generate code coverage
|
||||||
|
*
|
||||||
|
* Does not run integration tests
|
||||||
|
*/
|
||||||
|
gulp.task('coverage', ['lint', 'pre-coverage'], () => {
|
||||||
|
return pipe(gulp.src(UNIT_TEST_FILES), [
|
||||||
|
mocha(MOCHA_SETTINGS),
|
||||||
|
istanbul.writeReports({
|
||||||
|
dir: 'public/coverage',
|
||||||
|
reporters:['lcov', 'lcovonly', 'html', 'text'],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check dependencies for known security vulnerabilites
|
||||||
|
*/
|
||||||
|
gulp.task('audit', (cb) => {
|
||||||
|
nsp({
|
||||||
|
package: `${__dirname}/package.json`,
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all tasks
|
||||||
|
*/
|
||||||
|
gulp.task('default', [
|
||||||
|
'audit',
|
||||||
|
'lint',
|
||||||
|
'docs',
|
||||||
|
'coverage',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// End of gulpfile.js
|
74
package.json
Normal file
74
package.json
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"author": "Timothy J. Warren",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.9.1",
|
||||||
|
"body-parser": "~1.13.2",
|
||||||
|
"ci-node-query": "^3.1.0",
|
||||||
|
"coffee-script": "^1.10.0",
|
||||||
|
"cookie-parser": "~1.3.5",
|
||||||
|
"debug": "~2.2.0",
|
||||||
|
"dotenv": "^2.0.0",
|
||||||
|
"errors": "^0.3.0",
|
||||||
|
"eslint": "^1.10.3",
|
||||||
|
"express": "4.*",
|
||||||
|
"express-session": "^1.13.0",
|
||||||
|
"getargs": "0.0.8",
|
||||||
|
"glob": "^6.0.4",
|
||||||
|
"helmet": "^1.1.0",
|
||||||
|
"highlight.js": "^9.1.0",
|
||||||
|
"hulk-hogan": "0.0.9",
|
||||||
|
"lodash": "^4.5.0",
|
||||||
|
"marked": "^0.3.5",
|
||||||
|
"morgan": "~1.6.1",
|
||||||
|
"nodemon": "^1.9.0",
|
||||||
|
"winston": "^2.1.1"
|
||||||
|
},
|
||||||
|
"description": "An Opinionated Take on express with use of ES6 features",
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^3.4.1",
|
||||||
|
"chai-as-promised": "^5.2.0",
|
||||||
|
"eslint": "1.10.*",
|
||||||
|
"gulp": "3.9.*",
|
||||||
|
"gulp-apidoc": "0.2.*",
|
||||||
|
"gulp-documentation": "2.1.*",
|
||||||
|
"gulp-eslint": "^2.0.0",
|
||||||
|
"gulp-istanbul": "^0.10.3",
|
||||||
|
"gulp-jscs": "3.0.*",
|
||||||
|
"gulp-mocha": "2.2.*",
|
||||||
|
"gulp-nsp": "^2.3.0",
|
||||||
|
"gulp-pipe": "1.0.*",
|
||||||
|
"istanbul": "0.4.*",
|
||||||
|
"mocha": "2.3.*",
|
||||||
|
"pre-commit": "^1.1.2",
|
||||||
|
"supertest": "^1.1.0"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"lib": "./lib"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">4.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/",
|
||||||
|
"server.js"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./server.js",
|
||||||
|
"name": "crispy-train",
|
||||||
|
"pre-commit": {
|
||||||
|
"silent": false,
|
||||||
|
"run": [
|
||||||
|
"gulp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/timw4mail/crispy-train.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "nodemon server.js",
|
||||||
|
"gulp": "gulp default",
|
||||||
|
"test": "gulp test"
|
||||||
|
},
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
0
public/.gitkeep
Normal file
0
public/.gitkeep
Normal file
51
server.js
Normal file
51
server.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const container = require('./app/Container');
|
||||||
|
const app = container.get('./bootstrap');
|
||||||
|
const config = container.get('base/Config');
|
||||||
|
const HttpServer = container.get('base/HttpServer');
|
||||||
|
const HttpsServer = container.get('base/HttpsServer');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a port into a number, string, or false.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {mixed} val - port value
|
||||||
|
* @return {mixed} - normalized value
|
||||||
|
*/
|
||||||
|
function normalizePort(val) {
|
||||||
|
let port = parseInt(val, 10);
|
||||||
|
|
||||||
|
if (isNaN(port)) {
|
||||||
|
// named pipe
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port >= 0) {
|
||||||
|
// port number
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTP Server
|
||||||
|
if (true === config.get('http')) {
|
||||||
|
let port = normalizePort(config.get('http-port'));
|
||||||
|
app.set('port', port);
|
||||||
|
container.set('http-server', new HttpServer(app, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTTPs Server
|
||||||
|
if (true === config.get('https')) {
|
||||||
|
const httpsPort = normalizePort(config.get('https-port'));
|
||||||
|
const httpsConfig = {
|
||||||
|
key: fs.readFileSync(config.get('https-config-key')),
|
||||||
|
cert: fs.readFileSync(config.get('https-config-cert')),
|
||||||
|
};
|
||||||
|
app.set('https-port', httpsPort);
|
||||||
|
container.set('https-server', new HttpsServer(app, httpsPort, httpsConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of server.js
|
53
test/integration/index_test.js
Normal file
53
test/integration/index_test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const testBase = require('../test-base');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
const supertest = require('supertest');
|
||||||
|
|
||||||
|
let server = supertest.agent('http://localhost:3000');
|
||||||
|
|
||||||
|
suite('Example integration tests', () => {
|
||||||
|
test('Index page works as expected', (done) => {
|
||||||
|
server.get('/')
|
||||||
|
.expect('Content-type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(res.status).to.equal(200);
|
||||||
|
expect(res.body.error).to.be.undefined;
|
||||||
|
expect(res.body).to.deep.equal({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
index: { title: 'Express' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('404 page works as expected', (done) => {
|
||||||
|
server.get('/foo')
|
||||||
|
.expect('Content-type', /json/)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(res.status).to.equal(404);
|
||||||
|
expect(res.body.status).to.equal(String(res.status));
|
||||||
|
expect(res.body.message).to.be.ok;
|
||||||
|
|
||||||
|
let expected = {
|
||||||
|
status: '404',
|
||||||
|
message: 'Not Found',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
expected.error = {
|
||||||
|
code: '404',
|
||||||
|
status: '404',
|
||||||
|
name: 'Http404Error',
|
||||||
|
message: 'Not Found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(res.body).to.deep.equal(expected);
|
||||||
|
|
||||||
|
return done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
5
test/mocha.opts
Normal file
5
test/mocha.opts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
test/**/*.js
|
||||||
|
--ui tdd
|
||||||
|
--reporter spec
|
||||||
|
--slow 1000
|
||||||
|
--timeout 5000
|
71
test/test-base.js
Normal file
71
test/test-base.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Set up chai as promised to allow for
|
||||||
|
// better testing of promises
|
||||||
|
const chai = require('chai');
|
||||||
|
const chaiAsPromised = require('chai-as-promised');
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
// Load environment file
|
||||||
|
require('dotenv').config({
|
||||||
|
path: path.resolve(__dirname, '../.env'),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Object for unit test utilities
|
||||||
|
*/
|
||||||
|
const testBase = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chai expect assertion library
|
||||||
|
*/
|
||||||
|
expect: chai.expect,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the appropriate path to a module relative to the root folder
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - the raw path to the module
|
||||||
|
* @return {String} - the normalized path to the module
|
||||||
|
*/
|
||||||
|
_normalizeIncludePath(modulePath) {
|
||||||
|
const basePath = path.resolve(path.join(__dirname, '../'));
|
||||||
|
|
||||||
|
let includePath = modulePath;
|
||||||
|
|
||||||
|
// Allow referencing local modules without using a ./
|
||||||
|
// eg. util/route-loader instead of ./util/route-loader
|
||||||
|
if (modulePath.includes('/') && ! modulePath.startsWith('./')) {
|
||||||
|
includePath = path.join(basePath, modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return includePath;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a module relative to the root folder
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - path to the module, relative to the tests/ folder
|
||||||
|
* @return {mixed} - whatever the module returns
|
||||||
|
*/
|
||||||
|
require(modulePath) {
|
||||||
|
const includePath = testBase._normalizeIncludePath(modulePath);
|
||||||
|
return require(includePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a module relative to the root folder, but first delete
|
||||||
|
* the module from the require cache
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - path to the module, relative to the tests/ folder
|
||||||
|
* @return {mixed} - whatever the module returns
|
||||||
|
*/
|
||||||
|
requireNoCache(modulePath) {
|
||||||
|
const includePath = testBase._normalizeIncludePath(modulePath);
|
||||||
|
delete require.cache[includePath];
|
||||||
|
return require(includePath);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = testBase;
|
0
test/unit/.gitkeep
Normal file
0
test/unit/.gitkeep
Normal file
109
test/unit/base/Config_test.js
Normal file
109
test/unit/base/Config_test.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const testBase = require('../../test-base');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
const glob = require('glob');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
suite('Config class tests', () => {
|
||||||
|
let config = null;
|
||||||
|
setup(() => {
|
||||||
|
config = testBase.require('app/base/Config');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get a non-existent config value', () => {
|
||||||
|
let actual = config.get('foo');
|
||||||
|
expect(actual).to.be.undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Value cascade', () => {
|
||||||
|
|
||||||
|
suite('Explicit values are loaded first', () => {
|
||||||
|
setup(() => {
|
||||||
|
config._set('foo', 'bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`'foo' exists in config`, () => {
|
||||||
|
expect(config.has('foo')).to.be.true;
|
||||||
|
});
|
||||||
|
test(`value of 'foo' is returned by get method`, () => {
|
||||||
|
expect(config.get('foo')).to.be.equal('bar');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Environment values are loaded before defaults', () => {
|
||||||
|
setup(() => {
|
||||||
|
process.env.BAR = 'baz';
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`'bar' is not directly defined in config`, () => {
|
||||||
|
expect(config.has('bar')).to.be.false;
|
||||||
|
});
|
||||||
|
test(`'bar' is defined in environment`, () => {
|
||||||
|
expect(config.hasEnv('bar')).to.be.true;
|
||||||
|
expect(config.getEnv('bar')).to.be.equal(process.env.BAR);
|
||||||
|
});
|
||||||
|
test(`Gets 'bar' from environment`, () => {
|
||||||
|
expect(config.get('bar')).to.be.equal(config.getEnv('bar'));
|
||||||
|
});
|
||||||
|
test(`'foo' is now cached in config`, () => {
|
||||||
|
expect(config.has('bar')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(() => {
|
||||||
|
delete process.env.BAR;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Default values are loaded as a last resort', () => {
|
||||||
|
setup(() => {
|
||||||
|
delete process.env.HOST;
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`'host' is not directly defined in config`, () => {
|
||||||
|
expect(config.has('host')).to.be.false;
|
||||||
|
});
|
||||||
|
test(`'host' is not defined in environment`, () => {
|
||||||
|
expect(config.hasEnv('host')).to.be.false;
|
||||||
|
});
|
||||||
|
test(`Get default value of 'host'`, () => {
|
||||||
|
expect(config.get('host')).to.equal('localhost');
|
||||||
|
});
|
||||||
|
test(`'host' is now cached in config`, () => {
|
||||||
|
expect(config.has('host')).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('normalizeValue', () => {
|
||||||
|
test(`'true' returns true`, () => {
|
||||||
|
let val = config._normalizeValue('true');
|
||||||
|
expect(val).to.be.true;
|
||||||
|
});
|
||||||
|
test(`'false' returns false`, () => {
|
||||||
|
let val = config._normalizeValue('false');
|
||||||
|
expect(val).to.be.false;
|
||||||
|
});
|
||||||
|
test(`'436' returns 436`, () => {
|
||||||
|
let val = config._normalizeValue('436');
|
||||||
|
expect(val).to.be.equal(436);
|
||||||
|
});
|
||||||
|
test(`'foo' returns 'foo'`, () => {
|
||||||
|
let val = config._normalizeValue('foo');
|
||||||
|
expect(val).to.be.equal('foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Keys returns values in config', () => {
|
||||||
|
// Get the list of keys for files
|
||||||
|
let configFiles = glob.sync(
|
||||||
|
path.resolve(__dirname, '../../../app/config/**/*.js')
|
||||||
|
);
|
||||||
|
let expected = configFiles.map((fullPath) => {
|
||||||
|
return path.basename(fullPath, '.js');
|
||||||
|
});
|
||||||
|
let actual = config.keys();
|
||||||
|
|
||||||
|
expect(actual).to.be.deep.equal(expected);
|
||||||
|
});
|
||||||
|
});
|
77
test/unit/base/Container_test.js
Normal file
77
test/unit/base/Container_test.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const testBase = require('../../test-base');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
|
||||||
|
suite('Dependency Container tests', () => {
|
||||||
|
let container = null;
|
||||||
|
|
||||||
|
setup(() => {
|
||||||
|
// Delete cached version of container class that may have been required
|
||||||
|
// at an earlier time. The container module has a saved state to keep track
|
||||||
|
// of modules during normal use. Removing the cached version ensures that
|
||||||
|
// a new instance of the module is used.
|
||||||
|
container = testBase.requireNoCache('app/Container');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple requires return the same instance', () => {
|
||||||
|
let container2 = testBase.require('app/Container');
|
||||||
|
expect(container2).to.be.equal(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('has method', () => {
|
||||||
|
setup(() => {
|
||||||
|
container.set('foobar', {
|
||||||
|
foo: {
|
||||||
|
bar: [1, 2, 3],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Item "foobar" exists', () => {
|
||||||
|
expect(container.has('foobar')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Item "abc" does not exist', () => {
|
||||||
|
expect(container.has('abc')).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Get/set functionality', () => {
|
||||||
|
let obj = {
|
||||||
|
foo: {
|
||||||
|
bar: [1, 2, 3],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Set method returns Container', () => {
|
||||||
|
let actual = container.set('foobar', obj);
|
||||||
|
expect(actual).to.be.equal(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get method returns set object', () => {
|
||||||
|
let actual = container.get('foobar');
|
||||||
|
expect(actual).to.be.equal(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Attempt to get non-existent item returns undefined', () => {
|
||||||
|
expect(container.get('aseiutj')).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('get method require', () => {
|
||||||
|
test('Returns same object as testInclude', () => {
|
||||||
|
let containerFile = container.get('./bootstrap');
|
||||||
|
let testFile = testBase.require('app/bootstrap');
|
||||||
|
|
||||||
|
expect(containerFile).to.be.equal(testFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Returns same object as native require', () => {
|
||||||
|
let containerFile = container.get('express');
|
||||||
|
let nativeRequire = require('express');
|
||||||
|
|
||||||
|
expect(containerFile).to.be.equal(nativeRequire);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
28
test/unit/base/Controller_test.js
Normal file
28
test/unit/base/Controller_test.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const testBase = require('../../test-base');
|
||||||
|
const errors = require('errors');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
|
||||||
|
suite('Controller tests', () => {
|
||||||
|
let controller = null;
|
||||||
|
|
||||||
|
setup(() => {
|
||||||
|
controller = testBase.requireNoCache('app/base/Controller');
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('HttpError Tests', () => {
|
||||||
|
test('500 Error', (done) => {
|
||||||
|
controller.HttpError(500, (err) => {
|
||||||
|
expect(err).to.deep.equal(new errors.Http500Error());
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('401 Error', (done) => {
|
||||||
|
controller.HttpError(401, (err) => {
|
||||||
|
expect(err).to.deep.equal(new errors.Http401Error());
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
25
test/unit/base/util/route-loader_test.js
Normal file
25
test/unit/base/util/route-loader_test.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const testBase = require('../../../test-base');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
|
||||||
|
let routeLoader = testBase.require('app/base/util/route-loader');
|
||||||
|
|
||||||
|
function getPath(filePath) {
|
||||||
|
return path.join(__dirname, filePath)
|
||||||
|
.replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
suite('Util - Route Loader', () => {
|
||||||
|
test('routeLoader creates accurate route mapping', () => {
|
||||||
|
let actual = routeLoader(path.join(__dirname, 'test-routes'));
|
||||||
|
let expected = {
|
||||||
|
'/api/foo/bar': getPath('test-routes/api/foo/bar.js'),
|
||||||
|
'/api/foo': getPath('test-routes/api/foo.js'),
|
||||||
|
'/': getPath('test-routes/index.js'),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(expected).to.be.deep.equal(actual);
|
||||||
|
});
|
||||||
|
});
|
13
test/unit/base/util/test-routes/api/foo.js
Normal file
13
test/unit/base/util/test-routes/api/foo.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
index: { title: 'Express' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
13
test/unit/base/util/test-routes/api/foo/bar.js
Normal file
13
test/unit/base/util/test-routes/api/foo/bar.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
index: { title: 'Express' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
13
test/unit/base/util/test-routes/index.js
Normal file
13
test/unit/base/util/test-routes/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
index: { title: 'Express' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
21
test/unit/helpers/promisify_test.js
Normal file
21
test/unit/helpers/promisify_test.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const testBase = require('../../test-base');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
const fs = require('fs');
|
||||||
|
const promisify = testBase.require('app/helpers/promisify');
|
||||||
|
|
||||||
|
suite('Promisify', () => {
|
||||||
|
test('Promisify returns a promise', () => {
|
||||||
|
let actual = promisify(fs.readFile)('../../test-base.js');
|
||||||
|
expect(actual).to.be.a('Promise');
|
||||||
|
});
|
||||||
|
test('Promisify fs.readFile resolves', () => {
|
||||||
|
let actual = promisify(fs.readFile)('../../test-base.js');
|
||||||
|
expect(actual).to.be.fulfilled;
|
||||||
|
});
|
||||||
|
test('Promisify fs.readFile fails on non-existent file', () => {
|
||||||
|
let actual = promisify(fs.readFile)('foo.txt');
|
||||||
|
expect(actual).to.be.rejected;
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user