0

I have built an ExpressJS server application over the course of a couple of years for internal use at work. If you're interested, it is a clinical decision support tool used in many hospitals. It has grown to be quite large at this point and is not so easily changed.

Anyway, I grew up programming before test suites were really a thing, but I know I should now use them. I have settled on using Jest. I found out that whereas I had my server definition and app entry point in the same file, I needed to break them apart so I can import just the server stuff in tests. I have done a bit of refactoring so that instead of just an index.js file, I now have both index.js and server.js files. See below code.

The problem I am having, however, is that my server file has a ton of imports which add functionality to the server, mostly middleware, but also a reference to a file called appDB.js which loads a bunch of Sequelize stuff (using Tedious (MSSQL)). Of course, when I run the few tests I have built so far the tests pass but I get errors stating that imports (of Tedious) were attempted after Jest environment was torn down. I then get errors related to Sequelize not working (since it couldn't import what it needed).

In server.js, I import the appDB.js file (which has the Sequelize references) because a little further down in server.js, I call seq.sync to create (if they don't exist) the main tables of the application. And further down, I include the session table definition since middleware needs it. I also load a copy of the permissions hierarchy (AbilityGroups.findAll) from the database, if necessary.

Any ideas for refactoring this so Jest will not throw errors? I suppose once there aren't errors, I should 'mock' some DB functionality unless directly testing my DB, right? All of the examples for using Jest with a DB that I have found have been inadequate because a) they used MongoDB (which has a helper module for Jest) directly and not Sequelize, and b) nobody was using middleware which needed info from the database.

Here are my files: server.js (defines the server)-

import 'babel-polyfill';
import http             from 'http';
import https            from 'https';
import fs               from 'fs';
import express          from 'express';
import session          from 'express-session';
import cors             from 'cors';
import path             from 'path';
import morgan           from 'morgan';
import helmet           from 'helmet';
import nocache          from 'nocache';
import passport         from 'passport';
import flash            from 'connect-flash';
import cookieParser     from 'cookie-parser';
import routes           from './routes';
// require('./sequelize-fixes');
import {Abilities, AbilityGroups, seq, Session} from './appDB'; // This only contains the admin tables, not the HL7 stuff
import winston          from './logging/winstonConfig';
// import Agenda           from 'agenda'; // TODO uncomment
// import Agendash         from 'agendash'; // TODO uncomment

let cacheProvider = require('./cache-provider')

/*
  Colors to use in console.log
  Reset = "\x1b[0m"
  Bright = "\x1b[1m"
  Dim = "\x1b[2m"
  Underscore = "\x1b[4m"
  Blink = "\x1b[5m"
  Reverse = "\x1b[7m"
  Hidden = "\x1b[8m"

  FgBlack = "\x1b[30m"
  FgRed = "\x1b[31m"
  FgGreen = "\x1b[32m"
  FgYellow = "\x1b[33m"
  FgBlue = "\x1b[34m"
  FgMagenta = "\x1b[35m"
  FgCyan = "\x1b[36m"
  FgWhite = "\x1b[37m"

  BgBlack = "\x1b[40m"
  BgRed = "\x1b[41m"
  BgGreen = "\x1b[42m"
  BgYellow = "\x1b[43m"
  BgBlue = "\x1b[44m"
  BgMagenta = "\x1b[45m"
  BgCyan = "\x1b[46m"
  BgWhite = "\x1b[47m"
 */

// @TODO remove the "force: true" or set it to false for production as this DROPs all tables each time we run the app
/**
 * Do this here so we can sync the WinstonSequelize model too
 */
seq.sync({
  force: false
})// TODO use .then() to log a Winston entry; can't log to DB until DB is ready
.catch(function(err) {
  throw new Error(err);
});

import { extDef }   from './model/session';

let debug = require('debug')('sql-rest-api:server');
require('./authN/passportConfig')(passport);
let SequelizeStore = require('connect-session-sequelize')(session.Store);

let app = express();

// const agendaConnectionOpts = {db: {address: 'localhost:27017/agenda', collection: 'agendaJobs', options: { useNewUrlParser: true }}}; // TODO uncomment

// const agenda = new Agenda(agendaConnectionOpts); // TODO uncomment

let corsOptions = {
  origin: true, // 'https://' + process.env.ORIGIN_DOMAIN, // TODO change this to whatever we use for the live site
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
  exposedHeaders: 'Content-Disposition',
  optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
  credentials: true // required to send cookies from browsers; didn't need this for postman
};

// view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'pug');


/**
 * Normalize a port into a number, string, or false.
 * @param {string} val - The port, input as a string to be parsed as an integer.
 * @return Return the original val if no integer can be parsed; return integer port number if val was parsable; otherwise return false.
 */
function normalizePort(val) {
  let port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Get port from environment and store in Express.
 */
let port = normalizePort(process.env.PORT || '3000');//== process.env.PORT is set by iisnode
app.set('port', port);

let whichServer = '';

/**
 * Create HTTPS server.
 */
if (process.env.NODE_ENV !== 'production') {
  whichServer = 'HTTPS';
  const options = {
    key: fs.readFileSync('./src/tls/sutternow.local.key.pem'),
    cert: fs.readFileSync('./src/tls/sutternow.local.crt.pem')
  };
  app.server = https.createServer(options, app);
} else {
  whichServer = 'HTTP';
  app.server = http.createServer(app);
}

/**
 * Setup morgan format
 */
morgan.token('id', function (req) {
  return req.sessionID
});
morgan.token('statusMessage', function (req, res) {
  if (typeof req.flash === 'function' && typeof req.flash('error') !== 'undefined' && req.flash('error').length !== 0) {
    return req.flash('error').slice(-1)[0];
  } else {
    return res.statusMessage;
  }
});

// This format is for Morgan (HTTP) messages only; see winstonConfig for other messages' format
// let morganFormat = ':status :statusMessage - SessionID\: :id :remote-addr ":method :url HTTP/:http-version" ":referrer" ":user-agent" ":res[content-length]"';
let morganFormat2 = new String('"status"\:":status","statusMessage"\:":statusMessage","sessionID"\:":id","remote"\:":remote-addr","method"\:":method","url"\:":url","http"\:":http-version","referrer"\:":referrer","agent"\:":user-agent","length"\:":res[content-length]"');
// let morganFormat2 = new String(':id :status :statusMessage :remote-addr :method :url :http-version :referrer :user-agent :res[content-length]');
app.use(express.static(path.join(__dirname, 'public')));
app.use(morgan(morganFormat2, { stream: winston.stream }));

app.use(express.json()); // https://expressjs.com/en/api.html for other settings including body size limit
app.use(express.urlencoded({ extended: true }));

// Middleware
app.use(helmet()); // Add Helmet as a middleware
app.use(nocache());
app.use(cors(corsOptions));
app.use(cookieParser());

//=========================================================================
//---------------------------BEGIN PASSPORT SETUP--------------------------
//=========================================================================
// see the express-session and connect-session-sequelize docs for parameter descriptions
let sessParams = {
  secret: process.env.SESSION_SECRET || 'secret',
  resave: false,
  saveUninitialized: false,// not sure if this should be true
  cookie: {
    domain: process.env.ORIGIN_DOMAIN,
    sameSite: 'strict',
    httpOnly: false, // if this isn't set to false, we can't read it via javascript on the client
    maxAge: 1000*60*20, // 20 minutes
    path: '/' // / is the root path of the domain
  },
  store: new SequelizeStore({
    db: seq,
    table: 'Session',
    extendDefaultFields: extDef,
    checkExpirationInterval: 0
  }),
  rolling: true, // this allows maxAge to be reset with every request, so it moves with activity
  unset: 'keep' // this is supposed to keep rows in the DB
};
if (app.get('env') !== 'production') { // TODO check that we only want this in dev
  // app.set('trust proxy', 1) // trust first proxy; set this if behind a proxy
  sessParams.cookie.secure = true // serve secure cookies
}
app.use(session(sessParams));
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session
//=========================================================================
//---------------------------END PASSPORT SETUP----------------------------
//=========================================================================

// API routes V1
// app.use('/dash', ensureAuthenticated, Agendash(agenda)); // TODO use this line instead of the next one
// app.use('/dash', Agendash(agenda)); // TODO uncomment for test
let virtualDir = ''
if (app.get('env') === 'production') {
  virtualDir = '/server'
}
app.use(virtualDir + '/v1', routes);

/**
 * error handler
 * error handling must be placed after all other app.use calls; https://expressjs.com/en/guide/error-handling.html
 */
app.use(function(err, req, res, next) {
  let route = '';
  console.log('Called second', err.message); //see catchall for what is called first
  console.log('Called status', err.status);
  console.log('Called statusCode', err.statusCode);

  // won't catch 401 Unauthorized errors. See authenticate.js for 401s
  if (!err.statusCode) err.statusCode = 500; // Sets a generic server error status code if none is part of the err
  // let data = JSON.parse(req.user.dataValues.data);
  // ${data.flash.error[0]}
  if (typeof err.route !== 'undefined') {
    route = err.route
  } else {
    route = 'Generic'
  }

  if (typeof req.user !== 'undefined' && req.user !== null) {
    if (typeof req.user.dataValues === 'undefined') {
      winston.error(`${err.statusCode || 500} ${err.message} - SessionID: ${req.sessionID} (${req.user.displayName})\" ${req.ip} \"${req.method} ${req.originalUrl} HTTP/${req.httpVersion}\" \"${req.headers['referer']}\" \"${req.headers['user-agent']}\" \"${req.headers['content-length']}\"`,
        {username: req.user.sAMAccountName, sessionID: req.sessionID, ip: req.ip, referrer: req.headers['referer'], url: req.originalUrl, query: req.method, route: route});
    } else {
      winston.error(`${err.statusCode || 500} ${err.message} - SessionID: ${req.user.dataValues.sid} (${req.user.dataValues.username})\" ${req.ip} \"${req.method} ${req.originalUrl} HTTP/${req.httpVersion}\" \"${req.headers['referer']}\" \"${req.headers['user-agent']}\" \"${req.headers['content-length']}\"`,
        {username: req.user.dataValues.username, sessionID: req.user.dataValues.sid, ip: req.ip, referrer: req.headers['referer'], url: req.originalUrl, query: req.method, route: route});
    }
  } else {
    winston.error(`${err.statusCode || 500} ${err.message} - \" ${req.ip} \"${req.method} ${req.originalUrl} HTTP/${req.httpVersion}\" \"${req.headers['referer']}\" \"${req.headers['user-agent']}\" \"${req.headers['content-length']}\"`,
      {sessionID: req.sessionID, ip: req.ip, referrer: req.headers['referer'], url: req.originalUrl, query: req.method, route: route});
  }

  // console.log('errorrrr', err.message);
  // console.log('reqqqq', req);
  if (err.shouldRedirect || err.statusCode === 500) {
    return res.status(err.statusCode).json({"Message": "Login failed. " + err.message});
    // res.status(err.statusCode).send({"customMessage": err.message});
    // return res.json({"Message": "Login failed. " + err.message});
    // res.send('error', { error: err }) // Renders a error.html for the user
  } else {
    //The below message can be found in the catch's error.response.data item
    return res.status(err.statusCode).json({"Message": "Login failed. " + err.message}); // If shouldRedirect is not defined in our error, sends our original err data
    // res.status(err.statusCode).send({"customMessage": err.message});
    // return res.json({"Message": "Login failed. " + err.message});
  }
});

// production error handler
/*const HTTP_SERVER_ERROR = 500;
app.use(function(err, req, res, next) {
  if (res.headersSent) {
    console.log('headers already sent');
    return next(err);
  }

  console.log('handling error');
  return res.status(err.status || HTTP_SERVER_ERROR).render('500');
});*/

cacheProvider.start(function (err) {
  if (err) console.error(err)
})

/**
 * Since we need the permissions set (CASL abilities) for every instance of this server app, let's load it immmediately
 * and put it in our cache
 */
let memCache = cacheProvider.instance()
let cachedAbilities = memCache.get('displayAbilities');
if (cachedAbilities === undefined) {
  AbilityGroups.findAll({include: Abilities}).then(abilities => {
    let newAbilitiesArray = []
    abilities.forEach((element) => {
      element.abilities.forEach((el) => {
        el.id = newAbilitiesArray.length + 1
        if (typeof el.inverted === 'undefined') {
          el.inverted = false
        }
        newAbilitiesArray.push(el)
      })
    })
    memCache.set('displayAbilities', newAbilitiesArray);
    console.log('\x1b[32m%s\x1b[0m', 'set displayAbilities');
    cachedAbilities = memCache.get('displayAbilities');
    // console.log('get displayAbilities', cachedAbilities);
    winston.info(`\"Fetched all abilities (display) for SYSTEM\" - SessionID: NONE\"`,
      {
        username: 'system',
        sessionID: 'none',
        ip: 'localhost',
        referrer: 'startup',
        url: 'none',
        query: 'N/A',
        route: 'Server Startup'
      });
  }).catch(err => {
    if (err) {
      // Not sure how to get an error here. ensureAuthenticated handles invalid users attempting this GET.
      // console.log(err);
      err.route = 'Main index';
      err.statusCode = 'GET_DISPLAY_ABILITIES_ERROR';
      err.status = 'GET DISPLAY ABILITIES ERROR';
      if (app.get('env') === 'production') {
        console.log('stack redacted');
        err.stack = '';// We want to obscure any data the user shouldn't see.
      }
      // next(err);
    }
  });
}

export {
  app,
  port,
  whichServer,
  normalizePort
  // agenda,
};

index.js (My program entry file)-

import { app, port, whichServer } from './server';

let myApp = app;

/**
 * Listen on provided port, on all network interfaces.
 */
myApp.server.listen(port);
myApp.server.on('error', onError);
myApp.server.on('listening', onListening);
process.on('unhandledRejection', onUnhandledRejection);
process.on('uncaughtException', onUncaughtException);

// async function graceful() {
  // await agenda.stop(); // TODO for agenda, this whole block must be moved to another file/module and imported. it will throw errors at runtime in prod, but will work in dev.
// }

// process.on('SIGINT', graceful);

function onUnhandledRejection(error) {
  // Will print "unhandledRejection err is not defined"
  console.log('unhandledRejection', error.message);
}

function onUncaughtException(error) {
  console.log('unhandledException', error.message);
}

/**
 * Event listener for HTTP server "error" event.
 * @param {Error} error - An error object.
 */
function onError(error) {
  console.log('onError');
  if (error.syscall !== 'listen') {
    throw error;
  }

  let bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // adding this line to include winston logging
  // winston.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  let addr = myApp.server.address();
  let bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  // the following probably won't get to the DB because it isn't online when it is called
  // winston.info('Listening on ' + bind, {sessionID: 'SYSTEM', username: 'SYSTEM', ip: addr});
  console.log('Listening on ' + bind + ', DBHOST: ' + process.env.APPDB_HOST + ', Protocol: ' + whichServer);
}

appDB.js (pulls in various model files and defines relationships)-

/**
 * @module appDB
 */
require('./sequelize-fixes');
import Sequelize from 'sequelize';
import config from './appDBConfig';
// user administration
import RolesModel from './model/userAdmin/roles';
import AbilityGroupModel from './model/userAdmin/ability_grouping';
import AbilitiesModel from './model/userAdmin/abilities';
import UsersRolesModel from './model/userAdmin/users_roles';
import RolePermsModel from './model/userAdmin/role_perms';
import UserModel from './model/userAdmin/user';
import GroupsRolesModel from './model/userAdmin/groups_roles';
import GroupModel from './model/userAdmin/group';
// security
import { SessionModel } from './model/session';
import WatchdogModel from './model/watchdog';
// application functions
import InterventionsModel from './model/interventions';
import MyReportsModel from './model/userReports/myReports';
import ReportLogModel from './model/userReports/reportLog';
import ReportListModel from './model/userReports/reportList';
import DowntimesModel from './model/downtimes';

import { app } from "./server";

/**
 * @constant {Sequelize} seq
 */
const seq = new Sequelize(config);

// user administration
const Roles         = RolesModel({ seq });
const AbilityGroups = AbilityGroupModel({ seq });
const Abilities     = AbilitiesModel({ seq });
const RolePerms     = RolePermsModel({ seq });
const Users         = UserModel({ seq });
const UsersRoles    = UsersRolesModel({ seq });
const Groups        = GroupModel({ seq });
const GroupsRoles   = GroupsRolesModel({ seq });

// security
const Session    = SessionModel({ seq });
const Watchdog   = WatchdogModel({ seq });

// application functions
const Interventions = InterventionsModel({ seq });
const MyReports     = MyReportsModel({ seq });
const ReportLog     = ReportLogModel({ seq });
const ReportList    = ReportListModel({ seq });
const Downtimes     = DowntimesModel({ seq });

AbilityGroups.hasMany(Abilities);
Abilities.belongsTo(AbilityGroups);

Users.belongsToMany(Roles, {through: 'users_roles', foreignKey: 'user_id'});
Roles.belongsToMany(Users, {through: 'users_roles', foreignKey: 'role_id'});

Groups.belongsToMany(Roles, {through: 'groups_roles', foreignKey: 'group_id'});
Roles.belongsToMany(Groups, {through: 'groups_roles', foreignKey: 'role_id'});

Abilities.belongsToMany(Roles, {through: 'role_perms', foreignKey: 'perm_id'});
Roles.belongsToMany(Abilities, {through: 'role_perms', foreignKey: 'role_id'});

Users.hasMany(MyReports, {foreignKey: 'user_id', sourceKey: 'id'});
MyReports.belongsTo(Users, {foreignKey: 'user_id', targetKey: 'id', onDelete: 'NO ACTION'});
Users.hasMany(MyReports, {foreignKey: 'username', sourceKey: 'username'});
MyReports.belongsTo(Users, {foreignKey: 'username', targetKey: 'username', onDelete: 'NO ACTION'});

Users.hasMany(ReportLog, {foreignKey: 'user_id', sourceKey: 'id'});
ReportLog.belongsTo(Users, {foreignKey: 'user_id', targetKey: 'id', onDelete: 'NO ACTION'});
Users.hasMany(ReportLog, {foreignKey: 'username', sourceKey: 'username'});
ReportLog.belongsTo(Users, {foreignKey: 'username', targetKey: 'username', onDelete: 'NO ACTION'});

const errorMsg = function (error) {
  if (app.get('env') === 'production') {
    console.log('stack redacted');
    error.stack = '';// We want to obscure any data the user shouldn't see.
  }
  if (error.name === 'SequelizeUniqueConstraintError') {
    // console.log('error: ', error);
    /* console.log('inside', error.errors);
    console.log('thingy', error.errors[0].message);
    console.log('name', error.name); */
    return 'Error: Field must be unique. "' + error.errors[0].value + '" already exists.'
  }

  if (error.name === 'SequelizeValidationError') {
    return 'Error: Validation failed. "' + error.errors[0].message + '".'
  }
};

export {
  seq,
  Roles,
  Abilities,
  AbilityGroups,
  RolePerms,
  Users,
  UsersRoles,
  Groups,
  GroupsRoles,
  Session,
  Watchdog,
  errorMsg,
  Interventions,
  MyReports,
  ReportLog,
  ReportList,
  Downtimes
};

server.test.js (my first test file)-

import { normalizePort } from './server';
import supertest from 'supertest';
jest.useFakeTimers()

describe('Normalize PORT', () => {
    describe('Named PIPE', () => {
        test('should return the named pipe', () => {
            expect(normalizePort('Foo')).toBe('Foo');
        });
    });

    describe('Number PORT', () => {
        test('should return the port number', () => {
            expect(normalizePort(8090)).toBe(8090);
        });
    });
});

1 Answers1

0

Thanks to this post: passport.js passport.initialize() middleware not in use

I was able to figure it out. For those who may have a similar issue, below is how I adjusted my index.js and server.js files. The main issue ended up being the order of middleware when I tried and tried to move sections from server.js over to index.js. The above post pointed me to the routes section, which I was not trying to move over because it didn't have Sequelize stuff in it. Once I moved that, running Jest only has Jest output now, with no errors.

index.js-

import { app, port, whichServer } from './server';
import cacheProvider from "./cache-provider";
import {Abilities, AbilityGroups, seq, Session} from "./appDB";
import winston from "./logging/winstonConfig";
import {extDef} from "./model/session";
import session from "express-session";
import passport from "passport";
import flash from "connect-flash";
import routes from "./routes";

let debug = require('debug')('sql-rest-api:server');
require('./authN/passportConfig')(passport);
let SequelizeStore = require('connect-session-sequelize')(session.Store);

// @TODO remove the "force: true" or set it to false for production as this DROPs all tables each time we run the app
/**
 * Do this here so we can sync the WinstonSequelize model too
 */
seq.sync({
  force: false
})// TODO use .then() to log a Winston entry; can't log to DB until DB is ready
.catch(function(err) {
  throw new Error(err);
});

let myApp = app;

//=========================================================================
//---------------------------BEGIN PASSPORT SETUP--------------------------
//=========================================================================
// see the express-session and connect-session-sequelize docs for parameter descriptions
let sessParams = {
  secret: process.env.SESSION_SECRET || 'secret',
  resave: false,
  saveUninitialized: false,// not sure if this should be true
  cookie: {
    domain: process.env.ORIGIN_DOMAIN,
    sameSite: 'strict',
    httpOnly: false, // if this isn't set to false, we can't read it via javascript on the client
    maxAge: 1000*60*20, // 20 minutes
    path: '/' // / is the root path of the domain
  },
  store: new SequelizeStore({
    db: seq,
    table: 'Session',
    extendDefaultFields: extDef,
    checkExpirationInterval: 0
  }),
  rolling: true, // this allows maxAge to be reset with every request, so it moves with activity
  unset: 'keep' // this is supposed to keep rows in the DB
};
if (myApp.get('env') !== 'production') { // TODO check that we only want this in dev
  // app.set('trust proxy', 1) // trust first proxy; set this if behind a proxy
  sessParams.cookie.secure = true // serve secure cookies
}
myApp.use(session(sessParams));
myApp.use(passport.initialize());
myApp.use(passport.session()); // persistent login sessions
myApp.use(flash()); // use connect-flash for flash messages stored in session
//=========================================================================
//---------------------------END PASSPORT SETUP----------------------------
//=========================================================================

// API routes V1
// app.use('/dash', ensureAuthenticated, Agendash(agenda)); // TODO use this line instead of the next one
// app.use('/dash', Agendash(agenda)); // TODO uncomment for test
let virtualDir = ''
if (myApp.get('env') === 'production') {
  virtualDir = '/server'
}
myApp.use(virtualDir + '/v1', routes);

cacheProvider.start(function (err) {
  if (err) console.error(err)
})

/**
 * Since we need the permissions set (CASL abilities) for every instance of this server app, let's load it immmediately
 * and put it in our cache
 */
let memCache = cacheProvider.instance()
let cachedAbilities = memCache.get('displayAbilities');
if (cachedAbilities === undefined) {
  AbilityGroups.findAll({include: Abilities}).then(abilities => {
    let newAbilitiesArray = []
    abilities.forEach((element) => {
      element.abilities.forEach((el) => {
        el.id = newAbilitiesArray.length + 1
        if (typeof el.inverted === 'undefined') {
          el.inverted = false
        }
        newAbilitiesArray.push(el)
      })
    })
    memCache.set('displayAbilities', newAbilitiesArray);
    console.log('\x1b[32m%s\x1b[0m', 'set displayAbilities');
    cachedAbilities = memCache.get('displayAbilities');
    // console.log('get displayAbilities', cachedAbilities);
    winston.info(`\"Fetched all abilities (display) for SYSTEM\" - SessionID: NONE\"`,
      {
        username: 'system',
        sessionID: 'none',
        ip: 'localhost',
        referrer: 'startup',
        url: 'none',
        query: 'N/A',
        route: 'Server Startup'
      });
  }).catch(err => {
    if (err) {
      // Not sure how to get an error here. ensureAuthenticated handles invalid users attempting this GET.
      // console.log(err);
      err.route = 'Main index';
      err.statusCode = 'GET_DISPLAY_ABILITIES_ERROR';
      err.status = 'GET DISPLAY ABILITIES ERROR';
      if (myApp.get('env') === 'production') {
        console.log('stack redacted');
        err.stack = '';// We want to obscure any data the user shouldn't see.
      }
      // next(err);
    }
  });
}

/**
 * Listen on provided port, on all network interfaces.
 */
myApp.server.listen(port);
myApp.server.on('error', onError);
myApp.server.on('listening', onListening);
process.on('unhandledRejection', onUnhandledRejection);
process.on('uncaughtException', onUncaughtException);

// async function graceful() {
  // await agenda.stop(); // TODO for agenda, this whole block must be moved to another file/module and imported. it will throw errors at runtime in prod, but will work in dev.
// }

// process.on('SIGINT', graceful);

function onUnhandledRejection(error) {
  // Will print "unhandledRejection err is not defined"
  console.log('unhandledRejection', error.message);
}

function onUncaughtException(error) {
  console.log('unhandledException', error.message);
}

/**
 * Event listener for HTTP server "error" event.
 * @param {Error} error - An error object.
 */
function onError(error) {
  console.log('onError');
  if (error.syscall !== 'listen') {
    throw error;
  }

  let bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // adding this line to include winston logging
  // winston.error(`${err.status || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  let addr = myApp.server.address();
  let bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  // the following probably won't get to the DB because it isn't online when it is called
  // winston.info('Listening on ' + bind, {sessionID: 'SYSTEM', username: 'SYSTEM', ip: addr});
  console.log('Listening on ' + bind + ', DBHOST: ' + process.env.APPDB_HOST + ', Protocol: ' + whichServer);
}

server.js-

import 'babel-polyfill';
import http             from 'http';
import https            from 'https';
import fs               from 'fs';
import express          from 'express';
import cors             from 'cors';
import path             from 'path';
import morgan           from 'morgan';
import helmet           from 'helmet';
import nocache          from 'nocache';
import cookieParser     from 'cookie-parser';
import winston          from './logging/winstonConfig';
// import Agenda           from 'agenda'; // TODO uncomment
// import Agendash         from 'agendash'; // TODO uncomment

/*
  Colors to use in console.log
  Reset = "\x1b[0m"
  Bright = "\x1b[1m"
  Dim = "\x1b[2m"
  Underscore = "\x1b[4m"
  Blink = "\x1b[5m"
  Reverse = "\x1b[7m"
  Hidden = "\x1b[8m"

  FgBlack = "\x1b[30m"
  FgRed = "\x1b[31m"
  FgGreen = "\x1b[32m"
  FgYellow = "\x1b[33m"
  FgBlue = "\x1b[34m"
  FgMagenta = "\x1b[35m"
  FgCyan = "\x1b[36m"
  FgWhite = "\x1b[37m"

  BgBlack = "\x1b[40m"
  BgRed = "\x1b[41m"
  BgGreen = "\x1b[42m"
  BgYellow = "\x1b[43m"
  BgBlue = "\x1b[44m"
  BgMagenta = "\x1b[45m"
  BgCyan = "\x1b[46m"
  BgWhite = "\x1b[47m"
 */

let app = express();

// const agendaConnectionOpts = {db: {address: 'localhost:27017/agenda', collection: 'agendaJobs', options: { useNewUrlParser: true }}}; // TODO uncomment

// const agenda = new Agenda(agendaConnectionOpts); // TODO uncomment

let corsOptions = {
  origin: true, // 'https://' + process.env.ORIGIN_DOMAIN, // TODO change this to whatever we use for the live site
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
  exposedHeaders: 'Content-Disposition',
  optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
  credentials: true // required to send cookies from browsers; didn't need this for postman
};

// view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'pug');

/**
 * Normalize a port into a number, string, or false.
 * @param {string} val - The port, input as a string to be parsed as an integer.
 * @return Return the original val if no integer can be parsed; return integer port number if val was parsable; otherwise return false.
 */
function normalizePort(val) {
  let port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Get port from environment and store in Express.
 */
let port = normalizePort(process.env.PORT || '3000');//== process.env.PORT is set by iisnode
app.set('port', port);

let whichServer = '';

/**
 * Create HTTPS server.
 */
if (process.env.NODE_ENV !== 'production') {
  whichServer = 'HTTPS';
  const options = {
    key: fs.readFileSync('./src/tls/sutternow.local.key.pem'),
    cert: fs.readFileSync('./src/tls/sutternow.local.crt.pem')
  };
  app.server = https.createServer(options, app);
} else {
  whichServer = 'HTTP';
  app.server = http.createServer(app);
}

/**
 * Setup morgan format
 */
morgan.token('id', function (req) {
  return req.sessionID
});
morgan.token('statusMessage', function (req, res) {
  if (typeof req.flash === 'function' && typeof req.flash('error') !== 'undefined' && req.flash('error').length !== 0) {
    return req.flash('error').slice(-1)[0];
  } else {
    return res.statusMessage;
  }
});

// This format is for Morgan (HTTP) messages only; see winstonConfig for other messages' format
// let morganFormat = ':status :statusMessage - SessionID\: :id :remote-addr ":method :url HTTP/:http-version" ":referrer" ":user-agent" ":res[content-length]"';
let morganFormat2 = new String('"status"\:":status","statusMessage"\:":statusMessage","sessionID"\:":id","remote"\:":remote-addr","method"\:":method","url"\:":url","http"\:":http-version","referrer"\:":referrer","agent"\:":user-agent","length"\:":res[content-length]"');
// let morganFormat2 = new String(':id :status :statusMessage :remote-addr :method :url :http-version :referrer :user-agent :res[content-length]');
app.use(express.static(path.join(__dirname, 'public')));
app.use(morgan(morganFormat2, { stream: winston.stream }));

app.use(express.json()); // https://expressjs.com/en/api.html for other settings including body size limit
app.use(express.urlencoded({ extended: true }));

// Middleware
app.use(helmet()); // Add Helmet as a middleware
app.use(nocache());
app.use(cors(corsOptions));
app.use(cookieParser());


/**
 * error handler
 * error handling must be placed after all other app.use calls; https://expressjs.com/en/guide/error-handling.html
 */
app.use(function(err, req, res, next) {
  let route = '';
  console.log('Called second', err.message); //see catchall for what is called first
  console.log('Called status', err.status);
  console.log('Called statusCode', err.statusCode);

  // won't catch 401 Unauthorized errors. See authenticate.js for 401s
  if (!err.statusCode) err.statusCode = 500; // Sets a generic server error status code if none is part of the err
  // let data = JSON.parse(req.user.dataValues.data);
  // ${data.flash.error[0]}
  if (typeof err.route !== 'undefined') {
    route = err.route
  } else {
    route = 'Generic'
  }

  if (typeof req.user !== 'undefined' && req.user !== null) {
    if (typeof req.user.dataValues === 'undefined') {
      winston.error(`${err.statusCode || 500} ${err.message} - SessionID: ${req.sessionID} (${req.user.displayName})\" ${req.ip} \"${req.method} ${req.originalUrl} HTTP/${req.httpVersion}\" \"${req.headers['referer']}\" \"${req.headers['user-agent']}\" \"${req.headers['content-length']}\"`,
        {username: req.user.sAMAccountName, sessionID: req.sessionID, ip: req.ip, referrer: req.headers['referer'], url: req.originalUrl, query: req.method, route: route});
    } else {
      winston.error(`${err.statusCode || 500} ${err.message} - SessionID: ${req.user.dataValues.sid} (${req.user.dataValues.username})\" ${req.ip} \"${req.method} ${req.originalUrl} HTTP/${req.httpVersion}\" \"${req.headers['referer']}\" \"${req.headers['user-agent']}\" \"${req.headers['content-length']}\"`,
        {username: req.user.dataValues.username, sessionID: req.user.dataValues.sid, ip: req.ip, referrer: req.headers['referer'], url: req.originalUrl, query: req.method, route: route});
    }
  } else {
    winston.error(`${err.statusCode || 500} ${err.message} - \" ${req.ip} \"${req.method} ${req.originalUrl} HTTP/${req.httpVersion}\" \"${req.headers['referer']}\" \"${req.headers['user-agent']}\" \"${req.headers['content-length']}\"`,
      {sessionID: req.sessionID, ip: req.ip, referrer: req.headers['referer'], url: req.originalUrl, query: req.method, route: route});
  }

  // console.log('errorrrr', err.message);
  // console.log('reqqqq', req);
  if (err.shouldRedirect || err.statusCode === 500) {
    return res.status(err.statusCode).json({"Message": "Login failed. " + err.message});
    // res.status(err.statusCode).send({"customMessage": err.message});
    // return res.json({"Message": "Login failed. " + err.message});
    // res.send('error', { error: err }) // Renders a error.html for the user
  } else {
    //The below message can be found in the catch's error.response.data item
    return res.status(err.statusCode).json({"Message": "Login failed. " + err.message}); // If shouldRedirect is not defined in our error, sends our original err data
    // res.status(err.statusCode).send({"customMessage": err.message});
    // return res.json({"Message": "Login failed. " + err.message});
  }
});

// production error handler
/*const HTTP_SERVER_ERROR = 500;
app.use(function(err, req, res, next) {
  if (res.headersSent) {
    console.log('headers already sent');
    return next(err);
  }

  console.log('handling error');
  return res.status(err.status || HTTP_SERVER_ERROR).render('500');
});*/



export {
  app,
  port,
  whichServer,
  normalizePort
  // agenda,
};