diff --git a/packages/roc-package-web-app/package.json b/packages/roc-package-web-app/package.json index 60c41a3..635aef0 100644 --- a/packages/roc-package-web-app/package.json +++ b/packages/roc-package-web-app/package.json @@ -26,21 +26,21 @@ "dependencies": { "config": "~1.16.0", "debug": "~2.2.0", - "koa": "~1.1.1", - "koa-accesslog": "~0.0.2", - "koa-add-trailing-slashes": "~1.1.0", - "koa-compressor": "~1.0.3", + "koa": "~2.4.1", + "koa-add-trailing-slashes": "~2.0.1", + "koa-compress": "~2.0.0", "koa-conditional-get": "~1.0.3", - "koa-errors": "~1.0.1", - "koa-etag": "~2.0.0", - "koa-favicon": "~1.2.0", - "koa-helmet": "~0.3.0", - "koa-logger": "~1.3.0", - "koa-lowercase-path": "~1.0.0", - "koa-normalize-path": "~1.0.0", - "koa-remove-trailing-slashes": "~1.0.0", - "koa-static": "~2.0.0", + "koa-error": "~3.1.1", + "koa-etag": "~3.0.0", + "koa-favicon": "~2.0.0", + "koa-helmet": "~3.3.0", + "koa-logger": "~3.1.0", + "koa-lowercase-path": "~2.0.0", + "koa-normalize-path": "~2.0.0", + "koa-remove-trailing-slashes": "~2.0.0", + "koa-static": "~4.0.2", "lodash": "4.13.1", + "moment": "~2.20.1", "roc": "^1.0.0-rc.23", "roc-package-webpack-node": "^1.0.0", "roc-package-webpack-web": "^1.0.0" diff --git a/packages/roc-package-web-app/src/app/createServer.js b/packages/roc-package-web-app/src/app/createServer.js index 25b5a2a..267407d 100644 --- a/packages/roc-package-web-app/src/app/createServer.js +++ b/packages/roc-package-web-app/src/app/createServer.js @@ -3,7 +3,7 @@ import http from 'http'; import https from 'https'; import debug from 'debug'; -import koa from 'koa'; +import Koa from 'koa'; import serve from 'koa-static'; import addTrailingSlash from 'koa-add-trailing-slashes'; import normalizePath from 'koa-normalize-path'; @@ -11,7 +11,7 @@ import lowercasePath from 'koa-lowercase-path'; import removeTrailingSlash from 'koa-remove-trailing-slashes'; import { merge, getSettings, getAbsolutePath } from 'roc'; -import basepathSupport from './basepathSupport'; +import basepathSupport from './middlewares/basepathSupport'; /** * Creates a server instance. @@ -41,7 +41,7 @@ export default function createServer(options = {}, beforeUserMiddlewares = [], { const logger = debug('roc:server'); logger.log = console.info.bind(console); - const server = koa(); + const server = new Koa(); const runtimeSettings = merge(getSettings('runtime'), options); // Add support for rocPath @@ -49,7 +49,7 @@ export default function createServer(options = {}, beforeUserMiddlewares = [], { if (useDefaultKoaMiddlewares) { // eslint-disable-next-line - const middlewares = require('./middlewares').default(runtimeSettings, { dev, dist }); + const middlewares = require('./defaultKoaMiddlewares').default(runtimeSettings, { dev, dist }); middlewares.forEach((middleware) => server.use(middleware)); } diff --git a/packages/roc-package-web-app/src/app/middlewares.js b/packages/roc-package-web-app/src/app/defaultKoaMiddlewares.js similarity index 68% rename from packages/roc-package-web-app/src/app/middlewares.js rename to packages/roc-package-web-app/src/app/defaultKoaMiddlewares.js index 205529e..75b1e4c 100644 --- a/packages/roc-package-web-app/src/app/middlewares.js +++ b/packages/roc-package-web-app/src/app/defaultKoaMiddlewares.js @@ -1,11 +1,12 @@ -import koaErrors from 'koa-errors'; -import helmet from 'koa-helmet'; +import koaError from 'koa-error'; +import koaHelmet from 'koa-helmet'; import koaEtag from 'koa-etag'; -import koaCompressor from 'koa-compressor'; +import koaCompress from 'koa-compress'; import koaFavicon from 'koa-favicon'; -import koaAccesslog from 'koa-accesslog'; import koaLogger from 'koa-logger'; +import accesslog from './middlewares/accesslog'; + /** * Returns the middlewares to be used. * @@ -16,17 +17,17 @@ export default function middlewares(config, { dev, dist }) { const middlewaresList = []; if (dev) { - middlewaresList.push(koaErrors()); + middlewaresList.push(koaError()); } // Security headers - middlewaresList.push(helmet()); + middlewaresList.push(koaHelmet()); middlewaresList.push(koaEtag()); // We only enable gzip in dist if (dist) { - middlewaresList.push(koaCompressor()); + middlewaresList.push(koaCompress()); } const favicon = config.favicon; @@ -35,7 +36,7 @@ export default function middlewares(config, { dev, dist }) { } if (dist) { - middlewaresList.push(koaAccesslog()); + middlewaresList.push(accesslog()); } else { middlewaresList.push(koaLogger()); } diff --git a/packages/roc-package-web-app/src/app/defaultKoaMiddlewares.spec.js b/packages/roc-package-web-app/src/app/defaultKoaMiddlewares.spec.js new file mode 100644 index 0000000..525180c --- /dev/null +++ b/packages/roc-package-web-app/src/app/defaultKoaMiddlewares.spec.js @@ -0,0 +1,64 @@ +import koaErrorMock from 'koa-error'; +import koaHelmetMock from 'koa-helmet'; +import koaEtagMock from 'koa-etag'; +import koaLoggerMock from 'koa-logger'; +import koaCompressMock from 'koa-compress'; +import koaFaviconMock from 'koa-favicon'; + +import accessLogMock from './middlewares/accesslog'; +import getKoaMiddlewares from './defaultKoaMiddlewares'; + +jest.mock('koa-error', () => jest.fn(() => function koaError() {})); +jest.mock('koa-helmet', () => jest.fn(() => function koaHelmet() {})); +jest.mock('koa-etag', () => jest.fn(() => function koaEtag() {})); +jest.mock('koa-logger', () => jest.fn(() => function koaLogger() {})); +jest.mock('koa-compress', () => jest.fn(() => function koaCompress() {})); +jest.mock('koa-favicon', () => jest.fn(() => function koaFavicon() {})); +jest.mock('./middlewares/accesslog', () => jest.fn(() => function accessLog() {})); + + +describe('defaultKoaMiddleware', () => { + afterEach(() => { + koaErrorMock.mockClear(); + koaHelmetMock.mockClear(); + koaEtagMock.mockClear(); + koaLoggerMock.mockClear(); + koaCompressMock.mockClear(); + }); + + it('should return default array of middlewares even when all options are falsy', () => { + const middlewares = getKoaMiddlewares({}, { dev: false, dist: false }); + + expect(middlewares.length).toEqual(3); + expect(middlewares.map(f => f.name)).toEqual(['koaHelmet', 'koaEtag', 'koaLogger']); + expect(koaHelmetMock).toHaveBeenCalled(); + expect(koaEtagMock).toHaveBeenCalled(); + expect(koaLoggerMock).toHaveBeenCalled(); + }); + + it('should include koa-error when dev is set to true', () => { + const middlewares = getKoaMiddlewares({}, { dev: true, dist: false }); + + expect(middlewares.length).toEqual(4); + expect(middlewares.map(f => f.name)).toEqual(['koaError', 'koaHelmet', 'koaEtag', 'koaLogger']); + expect(koaErrorMock).toHaveBeenCalled(); + }); + + it('should include koa-compress and accessLog instead of koa-logger when dist is set to true', () => { + const middlewares = getKoaMiddlewares({}, { dev: false, dist: true }); + + expect(middlewares.length).toEqual(4); + expect(middlewares.map(f => f.name)).toEqual(['koaHelmet', 'koaEtag', 'koaCompress', 'accessLog']); + expect(koaCompressMock).toHaveBeenCalled(); + expect(accessLogMock).toHaveBeenCalled(); + }); + + it('should include koa-favicon when favicon is specified in config', () => { + const config = { favicon: './pathToFavicon.ico' }; + const middlewares = getKoaMiddlewares(config, { dev: false, dist: false }); + + expect(middlewares.length).toEqual(4); + expect(middlewares.map(f => f.name)).toEqual(['koaHelmet', 'koaEtag', 'koaFavicon', 'koaLogger']); + expect(koaFaviconMock).toHaveBeenCalledWith(config.favicon); + }); +}); diff --git a/packages/roc-package-web-app/src/app/middlewares/accesslog.js b/packages/roc-package-web-app/src/app/middlewares/accesslog.js new file mode 100644 index 0000000..2b729b5 --- /dev/null +++ b/packages/roc-package-web-app/src/app/middlewares/accesslog.js @@ -0,0 +1,18 @@ +import util from 'util'; + +import moment from 'moment'; + +/* + Implementation based on koa-accesslog + */ +export default function accesslog(stream = process.stdout) { + return async function(ctx, next) { + await next(); + + const format = '%s - - [%s] "%s %s HTTP/1.X" %d %s\n'; + const length = ctx.length ? ctx.length.toString() : '-'; + const date = moment().format('D/MMM/YYYY:HH:mm:ss ZZ'); + + stream.write(util.format(format, ctx.ip, date, ctx.method, ctx.path, ctx.status, length)); + }; +} diff --git a/packages/roc-package-web-app/src/app/middlewares/accesslog.spec.js b/packages/roc-package-web-app/src/app/middlewares/accesslog.spec.js new file mode 100644 index 0000000..4f52f79 --- /dev/null +++ b/packages/roc-package-web-app/src/app/middlewares/accesslog.spec.js @@ -0,0 +1,52 @@ +import { format as formatMock } from 'util'; + +import createAccessLogMiddleware from './accesslog'; + +jest.mock('util', () => ({ + format: jest.fn(), +})); + +describe('middlewares/accesslog', () => { + const next = async () => {}; + + afterEach(() => { + formatMock.mockClear(); + }); + + it('should call write method of passed stream', async () => { + const streamWriteMock = jest.fn(); + const streamMock = { + write: streamWriteMock, + }; + const accessLogMiddleware = createAccessLogMiddleware(streamMock); + await accessLogMiddleware({}, next); + + expect(streamWriteMock).toHaveBeenCalled(); + }); + + it('should call util.format with certain ctx properties', async () => { + const streamWriteMock = jest.fn(); + const streamMock = { + write: streamWriteMock, + }; + const accessLogMiddleware = createAccessLogMiddleware(streamMock); + const ctx = { + length: 42, + ip: '127.0.0.1', + method: 'GET', + path: '/app', + status: 200, + }; + await accessLogMiddleware(ctx, next); + + expect(formatMock).toHaveBeenCalledWith( + '%s - - [%s] \"%s %s HTTP/1.X\" %d %s\n', + ctx.ip, + expect.anything(), + ctx.method, + ctx.path, + ctx.status, + ctx.length.toString(), + ); + }); +}); diff --git a/packages/roc-package-web-app/src/app/basepathSupport.js b/packages/roc-package-web-app/src/app/middlewares/basepathSupport.js similarity index 82% rename from packages/roc-package-web-app/src/app/basepathSupport.js rename to packages/roc-package-web-app/src/app/middlewares/basepathSupport.js index 1d6c36f..03df57e 100644 --- a/packages/roc-package-web-app/src/app/basepathSupport.js +++ b/packages/roc-package-web-app/src/app/middlewares/basepathSupport.js @@ -26,16 +26,16 @@ export default function basepathSupport(basepath) { return newPath; } - return function* (next) { + return async function(ctx, next) { // Do nothing if the basepath is / if (basepath === '/') { - return yield next; + return await next(); } - const newPath = matcher(this.path); + const newPath = matcher(ctx.path); if (newPath) { - this.path = newPath; - return yield next; + ctx.path = newPath; + return await next(); } // If the path does not match Koa will render a default 404 Not Found page. diff --git a/packages/roc-package-web-app/src/app/middlewares/basepathSupport.spec.js b/packages/roc-package-web-app/src/app/middlewares/basepathSupport.spec.js new file mode 100644 index 0000000..07288e9 --- /dev/null +++ b/packages/roc-package-web-app/src/app/middlewares/basepathSupport.spec.js @@ -0,0 +1,39 @@ +import createBasepathSupportMiddleware from './basepathSupport'; + +describe('middlewares/basepathSupport', () => { + const next = async () => {}; + + it('should throw an error when basepath does not start with "/"', () => { + const veryWrongBasepath = 'veryWrongBasepath'; + expect(() => { + createBasepathSupportMiddleware(veryWrongBasepath); + }).toThrowError(`The basepath must start with "/", was ${veryWrongBasepath}`); + }); + + it('should set new ctx.path with eased basepath', async () => { + const basepath = '/basepath'; + const ctx = { path: '/basepath/application' }; + const basepathMiddleware = createBasepathSupportMiddleware(basepath); + await basepathMiddleware(ctx, next); + + expect(ctx.path).toEqual('/application'); + }); + + it('should set new ctx.path to "/" if its the same as basepath', async () => { + const basepath = '/basepath'; + const ctx = { path: '/basepath' }; + const basepathMiddleware = createBasepathSupportMiddleware(basepath); + await basepathMiddleware(ctx, next); + + expect(ctx.path).toEqual('/'); + }); + + it('should return an undefined when basepath is different than "/" and no new path is set', async () => { + const basepath = '/'; + const ctx = { path: '/' }; + const basepathMiddleware = createBasepathSupportMiddleware(basepath); + const result = await basepathMiddleware(ctx, next); + + expect(result).toEqual(undefined); + }); +});