【问题标题】:How to refactor my ExpressJS + Sequelize files so I can use Jest correctly?如何重构我的 ExpressJS + Sequelize 文件以便我可以正确使用 Jest?
【发布时间】:2021-10-14 08:16:28
【问题描述】:

在几年的时间里,我构建了一个 ExpressJS 服务器应用程序,供内部使用。如果您有兴趣,它是许多医院使用的临床决策支持工具。在这一点上它已经变得相当大,并且不容易改变。

无论如何,在测试套件真正成为事物之前,我是在编程中长大的,但我知道我现在应该使用它们。我已经决定使用 Jest。我发现虽然我的服务器定义和应用程序入口点在同一个文件中,但我需要将它们分开,以便在测试中只导入服务器内容。我进行了一些重构,现在我不仅拥有 index.js 文件,还拥有 index.js 和 server.js 文件。请参阅下面的代码。

然而,我遇到的问题是我的服务器文件有大量的导入,这些导入向服务器添加了功能,主要是中间件,而且还引用了一个名为 appDB.js 的文件,该文件加载了一堆 Sequelize 东西(使用繁琐(MSSQL))。当然,当我运行到目前为止我已经构建的几个测试时,测试通过了,但是我收到错误,指出在 Jest 环境被拆除后尝试导入(Tedious)。然后我收到与 Sequelize 不起作用相关的错误(因为它无法导入所需的内容)。

在 server.js 中,我导入了 appDB.js 文件(其中包含 Sequelize 引用),因为在 server.js 中再往下一点,我调用 seq.sync 来创建(如果它们不存在)主表的应用程序。再往下,我包括了会话表定义,因为中间件需要它。如有必要,我还会从数据库中加载权限层次结构 (AbilityGroups.findAll) 的副本。

有什么想法可以重构这个让 Jest 不会抛出错误吗?我想一旦没有错误,我应该“模拟”一些数据库功能,除非直接测试我的数据库,对吧?我发现所有将 Jest 与数据库一起使用的示例都不够充分,因为 a)他们直接使用 MongoDB(它有一个 Jest 的帮助模块)而不是 Sequelize,并且 b)没有人使用需要来自数据库的信息的中间件.

这是我的文件: server.js(定义服务器)-

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(我的程序入口文件)-

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(拉入各种模型文件并定义关系)-

/**
 * @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(我的第一个测试文件)-

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);
        });
    });
});

【问题讨论】:

    标签: javascript node.js express jestjs sequelize.js


    【解决方案1】:

    感谢这个帖子:passport.js passport.initialize() middleware not in use

    我能够弄清楚。对于那些可能有类似问题的人,以下是我调整 index.js 和 server.js 文件的方法。当我尝试将部分从 server.js 移动到 index.js 时,主要问题最终是中间件的顺序。上面的帖子将我指向路线部分,我并没有试图移动它,因为它没有 Sequelize 的东西。一旦我移动了它,运行 Jest 现在只有 Jest 输出,没有错误。

    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,
    };
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-08
      • 2021-11-02
      • 2021-06-24
      • 2019-10-19
      • 1970-01-01
      • 1970-01-01
      • 2014-02-26
      • 2021-07-26
      相关资源
      最近更新 更多