From 01ab3ca06ba6f041963c637e505e5419f062bd60 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Thu, 18 Aug 2016 18:24:00 +0200 Subject: [PATCH 1/8] Setup tests --- .gitignore | 1 + package.json | 9 +++++++++ tests/all.js | 5 +++++ tests/validate.test.js | 10 ++++++++++ 4 files changed, 25 insertions(+) create mode 100644 tests/all.js create mode 100644 tests/validate.test.js diff --git a/.gitignore b/.gitignore index eb39261..1c41f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist .webpack +coverage diff --git a/package.json b/package.json index 4b54fe0..fcb1d48 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,20 @@ "url": "https://github.com/elastic-coders/serverless-webpack/issues" }, "homepage": "https://github.com/elastic-coders/serverless-webpack#readme", + "scripts": { + "test": "istanbul cover _mocha tests/all -- -R spec --recursive" + }, "dependencies": { "bluebird": "^3.4.0", "body-parser": "^1.15.2", "express": "^4.14.0", "fs-extra": "^0.26.7", "webpack": "^1.13.1" + }, + "devDependencies": { + "chai": "^3.5.0", + "istanbul": "^0.4.4", + "mocha": "^3.0.2", + "sinon": "^1.17.5" } } diff --git a/tests/all.js b/tests/all.js new file mode 100644 index 0000000..33f15dc --- /dev/null +++ b/tests/all.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('serverless-webpack', () => { + require('./validate.test'); +}); diff --git a/tests/validate.test.js b/tests/validate.test.js new file mode 100644 index 0000000..e47e1f4 --- /dev/null +++ b/tests/validate.test.js @@ -0,0 +1,10 @@ +'use strict'; + +const expect = require('chai').expect; +const validate = require('../lib/validate'); + +describe('validate', () => { + it('should expose a `validate` method', () => { + expect(validate.validate).to.be.a('function'); + }); +}); From 8e0a5edc8260c1109ed1783d6fab7c6959bc8543 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 00:28:38 +0200 Subject: [PATCH 2/8] Covered validate module --- .npmignore | 3 +- lib/utils.js | 2 + package.json | 5 +- tests/fs-extra.mock.js | 14 +++ tests/validate.test.js | 218 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 tests/fs-extra.mock.js diff --git a/.npmignore b/.npmignore index a387873..12b9715 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,3 @@ node_modules -example +examples +tests diff --git a/lib/utils.js b/lib/utils.js index ec8749f..38aca3d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,5 @@ +'use strict'; + function guid() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) diff --git a/package.json b/package.json index fcb1d48..37e450c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "chai": "^3.5.0", "istanbul": "^0.4.4", "mocha": "^3.0.2", - "sinon": "^1.17.5" + "mockery": "^1.7.0", + "serverless": "^1.0.0-beta.2", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" } } diff --git a/tests/fs-extra.mock.js b/tests/fs-extra.mock.js new file mode 100644 index 0000000..e640aad --- /dev/null +++ b/tests/fs-extra.mock.js @@ -0,0 +1,14 @@ +'use strict'; + +const sinon = require('sinon'); + +module.exports = () => ({ + _resetSpies() { + for (let p in this) { + if (this.hasOwnProperty(p) && p !== '_resetSpies') { + this[p].reset(); + } + } + }, + removeSync: sinon.spy(), +}); diff --git a/tests/validate.test.js b/tests/validate.test.js index e47e1f4..2a8a05d 100644 --- a/tests/validate.test.js +++ b/tests/validate.test.js @@ -1,10 +1,222 @@ 'use strict'; -const expect = require('chai').expect; -const validate = require('../lib/validate'); +const chai = require('chai'); +const sinon = require('sinon'); +const mockery = require('mockery'); +const Serverless = require('serverless'); +const path = require('path'); +const makeFsExtraMock = require('./fs-extra.mock'); +chai.use(require('sinon-chai')); +const expect = chai.expect; describe('validate', () => { + let fsExtraMock; + let baseModule; + let module; + let serverless; + + before(() => { + mockery.enable({ warnOnUnregistered: false }); + fsExtraMock = makeFsExtraMock(); + mockery.registerMock('fs-extra', fsExtraMock); + baseModule = require('../lib/validate'); + }); + + after(() => { + mockery.disable(); + mockery.deregisterAll(); + }); + + beforeEach(() => { + serverless = new Serverless(); + fsExtraMock._resetSpies(); + module = Object.assign({ + serverless, + options: {}, + }, baseModule); + }); + it('should expose a `validate` method', () => { - expect(validate.validate).to.be.a('function'); + expect(module.validate).to.be.a('function'); + }); + + it('should set `webpackConfig` in the context to `custom.webpack` option', () => { + const testConfig = { + entry: 'test', + context: 'testcontext', + output: {}, + }; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(module.webpackConfig).to.eql(testConfig); + }); + }); + + it('should delete the output path', () => { + const testOutPath = 'test'; + const testConfig = { + entry: 'test', + context: 'testcontext', + output: { + path: testOutPath, + }, + }; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(fsExtraMock.removeSync).to.have.been.calledWith(testOutPath); + }); + }); + + it('should override the output path if `out` option is specified', () => { + const testConfig = { + entry: 'test', + context: 'testcontext', + output: { + path: 'originalpath', + filename: 'filename', + }, + }; + const testServicePath = 'testpath'; + const testOptionsOut = 'testdir'; + module.options.out = testOptionsOut; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(module.webpackConfig.output).to.eql({ + path: `${testServicePath}/${testOptionsOut}`, + filename: 'filename', + }); + }); + }); + + it('should set a default `webpackConfig.context` if not present', () => { + const testConfig = { + entry: 'test', + output: {}, + }; + const testServicePath = 'testpath'; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(module.webpackConfig.context).to.equal(testServicePath); + }); + }); + + describe('default output', () => { + it('should set a default `webpackConfig.output` if not present', () => { + const testEntry = 'testentry'; + const testConfig = { + entry: testEntry, + }; + const testServicePath = 'testpath'; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(module.webpackConfig.output).to.eql({ + libraryTarget: 'commonjs', + path: `${testServicePath}/.webpack`, + filename: testEntry, + }); + }); + }); + + it('should set a default `webpackConfig.output.filename` if `entry` is an array', () => { + const testEntry = ['first', 'second', 'last']; + const testConfig = { + entry: testEntry, + }; + const testServicePath = 'testpath'; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(module.webpackConfig.output).to.eql({ + libraryTarget: 'commonjs', + path: `${testServicePath}/.webpack`, + filename: 'last', + }); + }); + }); + + it('should set a default `webpackConfig.output.filename` if `entry` is not defined', () => { + const testConfig = {}; + const testServicePath = 'testpath'; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + return module + .validate() + .then(() => { + expect(module.webpackConfig.output).to.eql({ + libraryTarget: 'commonjs', + path: `${testServicePath}/.webpack`, + filename: 'handler.js', + }); + }); + }); + }); + + describe('config file load', () => { + it('should load a webpack config from file if `custom.webpack` is a string', () => { + const testConfig = 'testconfig' + const testServicePath = 'testpath'; + const requiredPath = `${testServicePath}/${testConfig}`; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + serverless.utils.fileExistsSync = sinon.stub().returns(true); + const loadedConfig = { + entry: 'testentry', + }; + mockery.registerMock(requiredPath, loadedConfig); + return module + .validate() + .then(() => { + expect(serverless.utils.fileExistsSync).to.have.been.calledWith(requiredPath); + expect(module.webpackConfig).to.eql(loadedConfig); + mockery.deregisterMock(requiredPath); + }); + }); + + it('should throw if providing an invalid file', () => { + const testConfig = 'testconfig' + const testServicePath = 'testpath'; + const requiredPath = `${testServicePath}/${testConfig}`; + module.serverless.config.servicePath = testServicePath; + module.serverless.service.custom.webpack = testConfig; + serverless.utils.fileExistsSync = sinon.stub().returns(false); + const loadedConfig = { + entry: 'testentry', + }; + expect(module.validate.bind(module)).to.throw(/could not find/); + }); + + it('should load a default file if no custom config is provided', () => { + const testConfig = 'webpack.config.js'; + const testServicePath = 'testpath'; + const requiredPath = `${testServicePath}/${testConfig}`; + module.serverless.config.servicePath = testServicePath; + serverless.utils.fileExistsSync = sinon.stub().returns(true); + const loadedConfig = { + entry: 'testentry', + }; + mockery.registerMock(requiredPath, loadedConfig); + return module + .validate() + .then(() => { + expect(serverless.utils.fileExistsSync).to.have.been.calledWith(requiredPath); + expect(module.webpackConfig).to.eql(loadedConfig); + mockery.deregisterMock(requiredPath); + }); + }); }); }); From 78a8b39499d3af448e977f47f1373537db807a36 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 01:13:04 +0200 Subject: [PATCH 3/8] Covered compile module --- tests/all.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/all.js b/tests/all.js index 33f15dc..5a20b83 100644 --- a/tests/all.js +++ b/tests/all.js @@ -2,4 +2,5 @@ describe('serverless-webpack', () => { require('./validate.test'); + require('./compile.test'); }); From 0db7b745df21f2e5ca8d6490bc26188066941225 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 11:54:42 +0200 Subject: [PATCH 4/8] Covered run module --- tests/all.js | 1 + tests/compile.test.js | 79 +++++++++++++++++ tests/run.test.js | 196 ++++++++++++++++++++++++++++++++++++++++++ tests/utils.mock.js | 15 ++++ tests/webpack.mock.js | 33 +++++++ 5 files changed, 324 insertions(+) create mode 100644 tests/compile.test.js create mode 100644 tests/run.test.js create mode 100644 tests/utils.mock.js create mode 100644 tests/webpack.mock.js diff --git a/tests/all.js b/tests/all.js index 5a20b83..4b1faaf 100644 --- a/tests/all.js +++ b/tests/all.js @@ -3,4 +3,5 @@ describe('serverless-webpack', () => { require('./validate.test'); require('./compile.test'); + require('./run.test'); }); diff --git a/tests/compile.test.js b/tests/compile.test.js new file mode 100644 index 0000000..4d668df --- /dev/null +++ b/tests/compile.test.js @@ -0,0 +1,79 @@ +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const mockery = require('mockery'); +const Serverless = require('serverless'); +const makeWebpackMock = require('./webpack.mock'); +chai.use(require('sinon-chai')); +const expect = chai.expect; + +describe('compile', () => { + let webpackMock; + let baseModule; + let module; + let serverless; + + before(() => { + mockery.enable({ warnOnUnregistered: false }); + webpackMock = makeWebpackMock(); + mockery.registerMock('webpack', webpackMock); + baseModule = require('../lib/compile'); + }); + + after(() => { + mockery.disable(); + mockery.deregisterAll(); + }); + + beforeEach(() => { + serverless = new Serverless(); + serverless.cli = { log: sinon.spy() }; + webpackMock._resetSpies(); + module = Object.assign({ + serverless, + options: {}, + }, baseModule); + }); + + it('should expose a `compile` method', () => { + expect(module.compile).to.be.a('function'); + }); + + it('should compile with webpack from a context configuration', () => { + const testWebpackConfig = 'testconfig'; + module.webpackConfig = testWebpackConfig; + return module + .compile() + .then(() => { + expect(webpackMock).to.have.been.calledWith(testWebpackConfig); + expect(webpackMock.compilerMock.run).to.have.callCount(1); + }); + }); + + it('should fail if there are compilation errors', () => { + module.webpackConfig = 'testconfig'; + webpackMock.statsMock.compilation.errors = ['error']; + return module + .compile() + .catch((err) => { + expect(err.toString()).to.match(/compilation error/); + }); + }); + + it('should set context `webpackOutputPath`, `originalServicePath`, `serverless.config.servicePath`', () => { + const testWebpackConfig = 'testconfig'; + module.webpackConfig = testWebpackConfig; + const testServicePath = 'testServicePath'; + module.serverless.config.servicePath = testServicePath; + const testOutputPath = 'testOutputPath'; + webpackMock.statsMock.compilation.compiler.outputPath = testOutputPath; + return module + .compile() + .then(() => { + expect(module.webpackOutputPath).to.equal(testOutputPath); + expect(module.originalServicePath).to.equal(testServicePath); + expect(module.serverless.config.servicePath).to.equal(testOutputPath); + }); + }); +}); diff --git a/tests/run.test.js b/tests/run.test.js new file mode 100644 index 0000000..9a3fbf0 --- /dev/null +++ b/tests/run.test.js @@ -0,0 +1,196 @@ +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const mockery = require('mockery'); +const Serverless = require('serverless'); +const makeWebpackMock = require('./webpack.mock'); +const makeUtilsMock = require('./utils.mock'); +chai.use(require('sinon-chai')); +const expect = chai.expect; + +describe('run', () => { + let webpackMock; + let utilsMock; + let baseModule; + let module; + let serverless; + + before(() => { + mockery.enable({ warnOnUnregistered: false }); + webpackMock = makeWebpackMock(); + utilsMock = makeUtilsMock(); + mockery.registerMock('webpack', webpackMock); + mockery.registerMock('./utils', utilsMock); + baseModule = require('../lib/run'); + }); + + after(() => { + mockery.disable(); + mockery.deregisterAll(); + }); + + beforeEach(() => { + serverless = new Serverless(); + serverless.cli = { log: sinon.spy() }; + webpackMock._resetSpies(); + utilsMock._resetSpies(); + module = Object.assign({ + serverless, + options: {}, + }, baseModule); + }); + + describe('utils', () => { + it('should expose utils methods', () => { + expect(module.loadHandler).to.be.a('function'); + expect(module.getEvent).to.be.a('function'); + expect(module.getContext).to.be.a('function'); + }); + + it('should require the output file with `loadHandler`', () => { + const testPath = '/testpath'; + const testFilename = 'testfilename'; + const testModuleName = `${testPath}/${testFilename}`; + const testStats = { + compilation: { + options: { + output: { + path: testPath, + filename: testFilename, + }, + }, + }, + }; + const testModule = {}; + mockery.registerMock(testModuleName, testModule); + const res = module.loadHandler(testStats); + mockery.deregisterMock(testModuleName); + expect(res).to.equal(testModule); + expect(utilsMock.purgeCache).to.have.callCount(1); + }); + + it('should return a default event with `getEvent` and no option path', () => { + module.options.path = null; + const res = module.getEvent(); + expect(res).to.equal(null); + }); + + it('should load an event object from disk with `getEvent`', () => { + const testPath = 'testPath'; + module.options.path = testPath; + const testExampleObject = {}; + module.serverless.utils.readFileSync = sinon.stub().returns(testExampleObject); + const res = module.getEvent(); + expect(res).to.equal(testExampleObject); + }); + + it('should return an context object with `getContext`', () => { + const testFunctionName = 'testFunctionName'; + const res = module.getContext(testFunctionName); + expect(res).to.eql({ + awsRequestId: 'testguid', + functionName: testFunctionName, + functionVersion: '$LATEST', + invokeid: 'testguid', + isDefaultFunctionVersion: true, + logGroupName: `/aws/lambda/${testFunctionName}`, + logStreamName: '2016/02/14/[HEAD]13370a84ca4ed8b77c427af260', + memoryLimitInMB: '1024', + }); + }); + }); + + describe('run', () => { + const testEvent = {}; + const testContext = {}; + const testStats = {}; + const testFunctionId = 'testFunctionId'; + const testFunctionResult = 'testFunctionResult'; + const testModule = { + [testFunctionId]: null, + }; + + beforeEach(() => { + module.options['function'] = testFunctionId; + module.loadHandler = sinon.stub().returns(testModule); + module.getEvent = sinon.stub().returns(testEvent); + module.getContext = sinon.stub().returns(testContext); + }); + + it('should execute the given function handler', () => { + const testFunction = sinon.spy((e, c, cb) => cb(null, testFunctionResult)); + testModule[testFunctionId] = testFunction; + return module + .run(testStats) + .then((res) => { + expect(res).to.equal(testFunctionResult); + expect(testFunction).to.have.been.calledWith( + testEvent, + testContext + ); + }); + }); + + it('should fail if the function handler returns an error', () => { + const testError = 'testError'; + const testFunction = sinon.spy((e, c, cb) => cb(testError)); + testModule[testFunctionId] = testFunction; + return module + .run(testStats) + .catch((res) => { + expect(res).to.equal(testError); + }); + }); + }); + + describe('watch', () => { + const testEvent = {}; + const testContext = {}; + const testStats = {}; + const testFunctionId = 'testFunctionId'; + const testFunctionResult = 'testFunctionResult'; + const testModule = { + [testFunctionId]: null, + }; + + beforeEach(() => { + module.options['function'] = testFunctionId; + module.loadHandler = sinon.stub().returns(testModule); + module.getEvent = sinon.stub().returns(testEvent); + module.getContext = sinon.stub().returns(testContext); + }); + + it('should throw if webpack watch fails', () => { + const testError = 'testError'; + webpackMock.compilerMock.watch = sinon.spy((opt, cb) => cb(testError)); + expect(module.watch.bind(module)).to.throw(testError); + }); + + it('should throw if function handler fails', () => { + const testError = 'testHandlerError'; + const testFunction = sinon.spy((e, c, cb) => cb(testError)); + testModule[testFunctionId] = testFunction; + let testCb; + webpackMock.compilerMock.watch = sinon.spy((opt, cb) => { + testCb = cb; + cb(null, webpackMock.statsMock); + }); + expect(module.watch.bind(module)).to.throw(testError); + }); + + it('should call the handler every time a compilation occurs', () => { + const testFunction = sinon.spy((e, c, cb) => cb(null, testFunctionResult)); + testModule[testFunctionId] = testFunction; + let testCb; + webpackMock.compilerMock.watch = sinon.spy((opt, cb) => { + testCb = cb; + cb(null, webpackMock.statsMock); + }); + module.watch(); + expect(testFunction).to.have.callCount(1); + testCb(null, webpackMock.statsMock); + expect(testFunction).to.have.callCount(2); + }); + }); +}); diff --git a/tests/utils.mock.js b/tests/utils.mock.js new file mode 100644 index 0000000..f38c37c --- /dev/null +++ b/tests/utils.mock.js @@ -0,0 +1,15 @@ +'use strict'; + +const sinon = require('sinon'); + +module.exports = () => ({ + _resetSpies() { + for (let p in this) { + if (this.hasOwnProperty(p) && p !== '_resetSpies') { + this[p].reset(); + } + } + }, + guid: sinon.stub().returns('testguid'), + purgeCache: sinon.spy(), +}); diff --git a/tests/webpack.mock.js b/tests/webpack.mock.js new file mode 100644 index 0000000..15fe4cc --- /dev/null +++ b/tests/webpack.mock.js @@ -0,0 +1,33 @@ +'use strict'; + +const sinon = require('sinon'); + +const statsMockBase = () => ({ + compilation: { + errors: [], + compiler: { + outputPath: 'statsMock-outputPath', + }, + }, + toString: () => 'testStats', +}); + +const statsMock = {}; + +Object.assign(statsMock, statsMockBase()); + +const compilerMock = { + run: sinon.spy((cb) => cb(null, statsMock)), + watch: sinon.spy((opt, cb) => cb(null, statsMock)), +}; + +const webpackMock = sinon.stub().returns(compilerMock); +webpackMock.statsMock = statsMock; +webpackMock.compilerMock = compilerMock; +webpackMock._resetSpies = () => { + webpackMock.reset(); + compilerMock.run.reset(); + Object.assign(statsMock, statsMockBase()); +}; + +module.exports = () => webpackMock; From 62f9cda2cc8ada1a7e7affdec2e979983b003671 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 12:55:28 +0200 Subject: [PATCH 5/8] Fix run tests for new multiple-entrypoints --- tests/run.test.js | 77 ++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/tests/run.test.js b/tests/run.test.js index 9a3fbf0..d4f5da9 100644 --- a/tests/run.test.js +++ b/tests/run.test.js @@ -48,10 +48,18 @@ describe('run', () => { expect(module.getContext).to.be.a('function'); }); - it('should require the output file with `loadHandler`', () => { + describe('loadHandler', () => { + const testFunctionId = 'testFunctionId'; + const testHandlerModuleName = 'testHandlerModule'; + const testHandlerFunctionName = 'testHandlerFunction'; + const testFunctionsConfig = { + [testFunctionId]: { + handler: `${testHandlerModuleName}.${testHandlerFunctionName}`, + } + }; const testPath = '/testpath'; - const testFilename = 'testfilename'; - const testModuleName = `${testPath}/${testFilename}`; + const testFilename = `${testHandlerModuleName}.js`; + const testModuleFileName = `${testPath}/${testFilename}`; const testStats = { compilation: { options: { @@ -62,12 +70,33 @@ describe('run', () => { }, }, }; - const testModule = {}; - mockery.registerMock(testModuleName, testModule); - const res = module.loadHandler(testStats); - mockery.deregisterMock(testModuleName); - expect(res).to.equal(testModule); - expect(utilsMock.purgeCache).to.have.callCount(1); + const testHandlerFunction = sinon.spy(); + const testModule = { + [testHandlerFunctionName]: testHandlerFunction, + }; + + before(() => { + mockery.registerMock(testModuleFileName, testModule); + }); + + after(() => { + mockery.deregisterMock(testModuleFileName); + }); + + beforeEach(() => { + serverless.service.functions = testFunctionsConfig; + }); + + it('should require the handler module', () => { + const res = module.loadHandler(testStats, testFunctionId); + expect(res).to.equal(testHandlerFunction); + expect(utilsMock.purgeCache).to.have.callCount(0); + }); + + it('should purge the modules cache if required', () => { + const res = module.loadHandler(testStats, testFunctionId, true); + expect(utilsMock.purgeCache).to.have.been.calledWith(testModuleFileName); + }); }); it('should return a default event with `getEvent` and no option path', () => { @@ -107,25 +136,21 @@ describe('run', () => { const testStats = {}; const testFunctionId = 'testFunctionId'; const testFunctionResult = 'testFunctionResult'; - const testModule = { - [testFunctionId]: null, - }; beforeEach(() => { module.options['function'] = testFunctionId; - module.loadHandler = sinon.stub().returns(testModule); module.getEvent = sinon.stub().returns(testEvent); module.getContext = sinon.stub().returns(testContext); }); it('should execute the given function handler', () => { - const testFunction = sinon.spy((e, c, cb) => cb(null, testFunctionResult)); - testModule[testFunctionId] = testFunction; + const testHandlerFunc = sinon.spy((e, c, cb) => cb(null, testFunctionResult)); + module.loadHandler = sinon.stub().returns(testHandlerFunc); return module .run(testStats) .then((res) => { expect(res).to.equal(testFunctionResult); - expect(testFunction).to.have.been.calledWith( + expect(testHandlerFunc).to.have.been.calledWith( testEvent, testContext ); @@ -134,8 +159,8 @@ describe('run', () => { it('should fail if the function handler returns an error', () => { const testError = 'testError'; - const testFunction = sinon.spy((e, c, cb) => cb(testError)); - testModule[testFunctionId] = testFunction; + const testHandlerFunc = sinon.spy((e, c, cb) => cb(testError)); + module.loadHandler = sinon.stub().returns(testHandlerFunc); return module .run(testStats) .catch((res) => { @@ -150,13 +175,9 @@ describe('run', () => { const testStats = {}; const testFunctionId = 'testFunctionId'; const testFunctionResult = 'testFunctionResult'; - const testModule = { - [testFunctionId]: null, - }; beforeEach(() => { module.options['function'] = testFunctionId; - module.loadHandler = sinon.stub().returns(testModule); module.getEvent = sinon.stub().returns(testEvent); module.getContext = sinon.stub().returns(testContext); }); @@ -169,8 +190,8 @@ describe('run', () => { it('should throw if function handler fails', () => { const testError = 'testHandlerError'; - const testFunction = sinon.spy((e, c, cb) => cb(testError)); - testModule[testFunctionId] = testFunction; + const testHandlerFunc = sinon.spy((e, c, cb) => cb(testError)); + module.loadHandler = sinon.stub().returns(testHandlerFunc); let testCb; webpackMock.compilerMock.watch = sinon.spy((opt, cb) => { testCb = cb; @@ -180,17 +201,17 @@ describe('run', () => { }); it('should call the handler every time a compilation occurs', () => { - const testFunction = sinon.spy((e, c, cb) => cb(null, testFunctionResult)); - testModule[testFunctionId] = testFunction; + const testHandlerFunc = sinon.spy((e, c, cb) => cb(null, testFunctionResult)); + module.loadHandler = sinon.stub().returns(testHandlerFunc); let testCb; webpackMock.compilerMock.watch = sinon.spy((opt, cb) => { testCb = cb; cb(null, webpackMock.statsMock); }); module.watch(); - expect(testFunction).to.have.callCount(1); + expect(testHandlerFunc).to.have.callCount(1); testCb(null, webpackMock.statsMock); - expect(testFunction).to.have.callCount(2); + expect(testHandlerFunc).to.have.callCount(2); }); }); }); From 6940c496bd49f729133c25e2491a9bb3bb7d8d79 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 14:21:32 +0200 Subject: [PATCH 6/8] Use serverless cli consoleLog --- index.js | 2 +- lib/compile.js | 2 +- lib/run.js | 2 +- lib/serve.js | 6 +++--- tests/compile.test.js | 5 ++++- tests/run.test.js | 5 ++++- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index be2fffd..eb68a22 100644 --- a/index.js +++ b/index.js @@ -104,7 +104,7 @@ class ServerlessWebpack { .then(this.validate) .then(this.compile) .then(this.run) - .then(out => console.log(out)), + .then(out => this.serverless.cli.consoleLog(out)), 'webpack:watch:watch': () => BbPromise.bind(this) .then(this.validate) diff --git a/lib/compile.js b/lib/compile.js index 0da04b6..ccee537 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -12,7 +12,7 @@ module.exports = { return BbPromise .fromCallback(cb => compiler.run(cb)) .then(stats => { - console.log(stats.toString({ + this.serverless.cli.consoleLog(stats.toString({ colors: true, hash: false, version: false, diff --git a/lib/run.js b/lib/run.js index 9ff9657..a417233 100644 --- a/lib/run.js +++ b/lib/run.js @@ -87,7 +87,7 @@ module.exports = { if (err) { throw err; } else { - console.log(res); + this.serverless.cli.consoleLog(res); } } ) diff --git a/lib/serve.js b/lib/serve.js index fe8fe9b..df1cda9 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -52,7 +52,7 @@ module.exports = { const method = httpEvent.method.toLowerCase(); const endpoint = `/${this.options.stage}/${httpEvent.path}`; const path = endpoint.replace(/\{(.+?)\}/g, ':$1'); - let handler = this._handerBase(funcConf); + let handler = this._handlerBase(funcConf); if (httpEvent.cors) { handler = this._handlerAddCors(handler); } @@ -60,7 +60,7 @@ module.exports = { path, handler ); - console.log(` ${method.toUpperCase()} - http://localhost:${this._getPort()}${endpoint}`); + this.serverless.cli.consoleLog(` ${method.toUpperCase()} - http://localhost:${this._getPort()}${endpoint}`); } } @@ -100,7 +100,7 @@ module.exports = { }; }, - _handerBase(funcConf) { + _handlerBase(funcConf) { return (req, res, next) => { const func = funcConf.handlerFunc; const event = { diff --git a/tests/compile.test.js b/tests/compile.test.js index 4d668df..99dd95f 100644 --- a/tests/compile.test.js +++ b/tests/compile.test.js @@ -28,7 +28,10 @@ describe('compile', () => { beforeEach(() => { serverless = new Serverless(); - serverless.cli = { log: sinon.spy() }; + serverless.cli = { + log: sinon.spy(), + consoleLog: sinon.spy(), + }; webpackMock._resetSpies(); module = Object.assign({ serverless, diff --git a/tests/run.test.js b/tests/run.test.js index d4f5da9..cc09abc 100644 --- a/tests/run.test.js +++ b/tests/run.test.js @@ -32,7 +32,10 @@ describe('run', () => { beforeEach(() => { serverless = new Serverless(); - serverless.cli = { log: sinon.spy() }; + serverless.cli = { + log: sinon.spy(), + consoleLog: sinon.spy(), + }; webpackMock._resetSpies(); utilsMock._resetSpies(); module = Object.assign({ From 8c10bee00cec9f9ff470883a5644cf436b16390b Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 15:25:48 +0200 Subject: [PATCH 7/8] Adding tests for part of serve module --- lib/serve.js | 9 ++- tests/all.js | 1 + tests/serve.test.js | 150 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 tests/serve.test.js diff --git a/lib/serve.js b/lib/serve.js index df1cda9..6fc3763 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -94,14 +94,14 @@ module.exports = { _handlerAddCors(handler) { return (req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); - res.header( 'Access-Control-Allow-Methods', 'GET,PUT,HEAD,PATCH,POST,DELETE,OPTIONS'); - res.header( 'Access-Control-Allow-Headers', 'Authorization,Content-Type,x-amz-date,x-amz-security-token'); + res.header('Access-Control-Allow-Methods', 'GET,PUT,HEAD,PATCH,POST,DELETE,OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Authorization,Content-Type,x-amz-date,x-amz-security-token'); handler(req, res, next); }; }, _handlerBase(funcConf) { - return (req, res, next) => { + return (req, res) => { const func = funcConf.handlerFunc; const event = { method: req.method, @@ -115,8 +115,7 @@ module.exports = { const context = this.getContext(funcConf.id); func(event, context, (err, resp) => { if (err) { - console.error(err); - res.sendStatus(500); + res.status(500).send(err); } else { res.status(200).send(resp); } diff --git a/tests/all.js b/tests/all.js index 4b1faaf..a2914a2 100644 --- a/tests/all.js +++ b/tests/all.js @@ -4,4 +4,5 @@ describe('serverless-webpack', () => { require('./validate.test'); require('./compile.test'); require('./run.test'); + require('./serve.test'); }); diff --git a/tests/serve.test.js b/tests/serve.test.js new file mode 100644 index 0000000..6c7556b --- /dev/null +++ b/tests/serve.test.js @@ -0,0 +1,150 @@ +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const mockery = require('mockery'); +const Serverless = require('serverless'); +const makeWebpackMock = require('./webpack.mock'); +const makeUtilsMock = require('./utils.mock'); +chai.use(require('sinon-chai')); +const expect = chai.expect; + +describe('serve', () => { + let webpackMock; + let baseModule; + let module; + let serverless; + + before(() => { + mockery.enable({ warnOnUnregistered: false }); + webpackMock = makeWebpackMock(); + mockery.registerMock('webpack', webpackMock); + baseModule = require('../lib/serve'); + }); + + after(() => { + mockery.disable(); + mockery.deregisterAll(); + }); + + beforeEach(() => { + serverless = new Serverless(); + serverless.cli = { log: sinon.spy() }; + webpackMock._resetSpies(); + module = Object.assign({ + serverless, + options: {}, + }, baseModule); + }); + + describe('_handlerBase', () => { + it('should return an express handler', () => { + const testFuncConf = { + id: 'testFuncId', + handerFunc: sinon.spy(), + }; + const handler = module._handlerBase(testFuncConf); + expect(handler).to.be.a('function'); + }); + + it('should send a 200 express response if successful', () => { + const testHandlerResp = 'testHandlerResp'; + const testHandlerFunc = sinon.spy((ev, ct, cb) => { + cb(null, testHandlerResp); + }); + const testFuncConf = { + id: 'testFuncId', + handlerFunc: testHandlerFunc, + }; + const testReq = { + method: 'testmethod', + headers: 'testheaders', + body: 'testbody', + params: 'testparams', + query: 'testquery', + }; + const testRes = { + send: sinon.spy(), + }; + testRes.status = sinon.stub().returns(testRes); + module.getContext = sinon.stub().returns('testContext'); + const handler = module._handlerBase(testFuncConf); + handler(testReq, testRes); + expect(testRes.status).to.have.been.calledWith(200); + expect(testRes.send).to.have.been.calledWith(testHandlerResp); + expect(testHandlerFunc).to.have.been.calledWith( + { + body: 'testbody', + headers: 'testheaders', + method: 'testmethod', + path: 'testparams', + query: 'testquery', + }, + 'testContext' + ); + }); + + it('should send a 500 express response if fails', () => { + const testHandlerErr = 'testHandlerErr'; + const testHandlerFunc = sinon.spy((ev, ct, cb) => { + cb(testHandlerErr); + }); + const testFuncConf = { + id: 'testFuncId', + handlerFunc: testHandlerFunc, + }; + const testRes = { + send: sinon.spy(), + }; + testRes.status = sinon.stub().returns(testRes); + module.getContext = sinon.stub().returns('testContext'); + const handler = module._handlerBase(testFuncConf); + handler({}, testRes); + expect(testRes.status).to.have.been.calledWith(500); + expect(testRes.send).to.have.been.calledWith(testHandlerErr); + }); + }); + + describe('_handlerAddCors', () => { + it('should retun an express handler', () => { + const res = module._handlerAddCors(); + expect(res).to.be.a('function'); + }); + + it('should call the given handler when called adding CORS headers', () => { + const testHandler = sinon.spy(); + const res = { + header: sinon.spy(), + }; + const req = {}; + const next = () => {}; + module._handlerAddCors(testHandler)(req, res, next); + expect(testHandler).to.have.been.calledWith(req, res, next); + expect(res.header).to.have.been.calledWith( + 'Access-Control-Allow-Origin', + '*' + ); + expect(res.header).to.have.been.calledWith( + 'Access-Control-Allow-Methods', + 'GET,PUT,HEAD,PATCH,POST,DELETE,OPTIONS' + ); + expect(res.header).to.have.been.calledWith( + 'Access-Control-Allow-Headers', + 'Authorization,Content-Type,x-amz-date,x-amz-security-token' + ); + }); + }); + + describe('_getPort', () => { + it('should return a default port', () => { + const port = module._getPort(); + expect(port).to.equal(8000); + }); + + it('should return the input option port if specified', () => { + module.options.port = 1234; + const port = module._getPort(); + expect(port).to.equal(1234); + }); + }); +}); From be51640b67a04d53c6d4defa78d8210d9d02d611 Mon Sep 17 00:00:00 2001 From: Nicola Peduzzi Date: Fri, 19 Aug 2016 17:59:31 +0200 Subject: [PATCH 8/8] Covered serve module --- lib/serve.js | 10 +- tests/compile.test.js | 1 + tests/express.mock.js | 20 +++ tests/run.test.js | 1 + tests/serve.test.js | 272 ++++++++++++++++++++++++++++++++++++++++- tests/validate.test.js | 1 + tests/webpack.mock.js | 1 + 7 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 tests/express.mock.js diff --git a/lib/serve.js b/lib/serve.js index 6fc3763..50e1814 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -14,7 +14,7 @@ module.exports = { const app = this._newExpressApp(funcConfs); const port = this._getPort(); - app.listen(port, () => { + app.listen(port, () => compiler.watch({}, (err, stats) => { if (err) { throw err; @@ -28,8 +28,8 @@ module.exports = { ); loadedModules.push(funcConf.moduleName); } - }); - }); + }) + ); return BbPromise.resolve(); }, @@ -39,11 +39,11 @@ module.exports = { app.use(bodyParser.json({ limit: '5mb' })); - app.use((req, res, next) => { + app.use(function optionsHandler(req, res, next) { if(req.method !== 'OPTIONS') { next(); } else { - res.status(200).end(); + res.sendStatus(200); } }); diff --git a/tests/compile.test.js b/tests/compile.test.js index 99dd95f..88067b9 100644 --- a/tests/compile.test.js +++ b/tests/compile.test.js @@ -19,6 +19,7 @@ describe('compile', () => { webpackMock = makeWebpackMock(); mockery.registerMock('webpack', webpackMock); baseModule = require('../lib/compile'); + Object.freeze(baseModule); }); after(() => { diff --git a/tests/express.mock.js b/tests/express.mock.js new file mode 100644 index 0000000..a0b213f --- /dev/null +++ b/tests/express.mock.js @@ -0,0 +1,20 @@ +const sinon = require('sinon'); + +const appMock = { + listen: sinon.spy(), + use: sinon.spy(), + get: sinon.spy(), + post: sinon.spy(), +}; + +const expressMock = sinon.stub().returns(appMock); +expressMock.appMock = appMock; +expressMock._resetSpies = () => { + expressMock.reset(); + appMock.listen.reset(); + appMock.use.reset(); + appMock.get.reset(); + appMock.post.reset(); +}; + +module.exports = () => expressMock; \ No newline at end of file diff --git a/tests/run.test.js b/tests/run.test.js index cc09abc..1832ccc 100644 --- a/tests/run.test.js +++ b/tests/run.test.js @@ -23,6 +23,7 @@ describe('run', () => { mockery.registerMock('webpack', webpackMock); mockery.registerMock('./utils', utilsMock); baseModule = require('../lib/run'); + Object.freeze(baseModule); }); after(() => { diff --git a/tests/serve.test.js b/tests/serve.test.js index 6c7556b..d04f7a0 100644 --- a/tests/serve.test.js +++ b/tests/serve.test.js @@ -5,12 +5,13 @@ const sinon = require('sinon'); const mockery = require('mockery'); const Serverless = require('serverless'); const makeWebpackMock = require('./webpack.mock'); -const makeUtilsMock = require('./utils.mock'); +const makeExpressMock = require('./express.mock'); chai.use(require('sinon-chai')); const expect = chai.expect; describe('serve', () => { let webpackMock; + let expressMock; let baseModule; let module; let serverless; @@ -18,8 +19,11 @@ describe('serve', () => { before(() => { mockery.enable({ warnOnUnregistered: false }); webpackMock = makeWebpackMock(); + expressMock = makeExpressMock(); mockery.registerMock('webpack', webpackMock); + mockery.registerMock('express', expressMock); baseModule = require('../lib/serve'); + Object.freeze(baseModule); }); after(() => { @@ -29,8 +33,12 @@ describe('serve', () => { beforeEach(() => { serverless = new Serverless(); - serverless.cli = { log: sinon.spy() }; + serverless.cli = { + log: sinon.spy(), + consoleLog: sinon.spy(), + }; webpackMock._resetSpies(); + expressMock._resetSpies(); module = Object.assign({ serverless, options: {}, @@ -147,4 +155,264 @@ describe('serve', () => { expect(port).to.equal(1234); }); }); + + describe('_getFuncConfig', () => { + const testFunctionsConfig = { + func1: { + handler: 'module1.func1handler', + events: [{ + http: { + method: 'get', + path: 'func1path', + }, + }], + }, + func2: { + handler: 'module2.func2handler', + events: [{ + http: { + method: 'POST', + path: 'func2path', + }, + }, { + nonhttp: 'non-http', + }], + }, + func3: { + handler: 'module2.func3handler', + events: [{ + nonhttp: 'non-http', + }], + }, + }; + + beforeEach(() => { + serverless.service.functions = testFunctionsConfig; + }); + + it('should return a list of normalized functions configurations', () => { + const res = module._getFuncConfigs(); + expect(res).to.eql([ + { + 'events': [ + { + 'method': 'get', + 'path': 'func1path', + } + ], + 'handler': 'module1.func1handler', + 'handlerFunc': null, + 'id': 'func1', + 'moduleName': 'module1', + }, + { + 'events': [ + { + 'method': 'POST', + 'path': 'func2path', + } + ], + 'handler': 'module2.func2handler', + 'handlerFunc': null, + 'id': 'func2', + 'moduleName': 'module2', + }, + ]); + }); + }); + + describe('_newExpressApp', () => { + it('should return an express app', () => { + const res = module._newExpressApp([]); + expect(res).to.equal(expressMock.appMock); + }); + + it('should add a body-parser to the app', () => { + const res = module._newExpressApp([]); + expect(res.use).to.have.been.calledWith(sinon.match(value => { + return typeof value === 'function' && value.name === 'jsonParser'; + })); + }); + + describe('OPTIONS handler', () => { + let optionsHandler; + + beforeEach(() => { + const res = module._newExpressApp([]); + const optionsHandlers = res.use.getCalls().filter(c => + typeof c.args[0] === 'function' && + c.args[0].name === 'optionsHandler' + ); + optionsHandler = optionsHandlers.length ? optionsHandlers[0].args[0] : null; + }); + + it('should add an OPTIONS request handler', () => { + expect(optionsHandler).to.exist; + }); + + it('should continue for non OPTIONS requests', () => { + const req = { method: 'GET' }; + const next = sinon.spy(); + optionsHandler(req, {}, next); + expect(next).to.have.callCount(1); + }); + + it('should send status 200 for OPTIONS requests', () => { + const req = { method: 'OPTIONS' }; + const res = { sendStatus: sinon.spy() }; + const next = sinon.spy(); + optionsHandler(req, res, next); + expect(res.sendStatus).to.have.been.calledWith(200); + expect(next).to.have.callCount(0); + }); + }); + + it('should create express handlers for all functions http event', () => { + const testFuncsConfs = [ + { + 'events': [ + { + 'method': 'get', + 'path': 'func1path', + 'cors': true, + } + ], + 'handler': 'module1.func1handler', + 'handlerFunc': null, + 'id': 'func1', + 'moduleName': 'module1', + }, + { + 'events': [ + { + 'method': 'POST', + 'path': 'func2path/{testParam}', + } + ], + 'handler': 'module2.func2handler', + 'handlerFunc': null, + 'id': 'func2', + 'moduleName': 'module2', + }, + ]; + const testStage = 'test'; + module.options.stage = testStage; + const testHandlerBase = 'testHandlerBase'; + const testHandlerCors = 'testHandlerCors'; + module._handlerBase = sinon.stub().returns(testHandlerBase); + module._handlerAddCors = sinon.stub().returns(testHandlerCors); + const app = module._newExpressApp(testFuncsConfs); + expect(app.get).to.have.callCount(1); + expect(app.get).to.have.been.calledWith( + '/test/func1path', + testHandlerCors + ); + expect(module.serverless.cli.consoleLog).to.have.been.calledWith( + ' GET - http://localhost:8000/test/func1path' + ); + expect(app.post).to.have.callCount(1); + expect(app.post).to.have.been.calledWith( + '/test/func2path/:testParam', + testHandlerBase + ); + expect(module.serverless.cli.consoleLog).to.have.been.calledWith( + ' POST - http://localhost:8000/test/func2path/{testParam}' + ); + }); + }); + + describe('serve method', () => { + let serve; + let listenerCb; + + beforeEach(() => { + serve = module.serve(); + listenerCb = expressMock.appMock.listen.firstCall.args[1]; + }); + + it('should start an express app listener', () => { + expect(expressMock.appMock.listen).to.have.callCount(1); + }); + + it('should start a webpack watcher', () => { + listenerCb.bind(module)(); + expect(webpackMock.compilerMock.watch).to.have.callCount(1); + }); + + it('should throw if compiler fails', () => { + listenerCb.bind(module)(); + const compileCb = webpackMock.compilerMock.watch.firstCall.args[1]; + const testError = 'testError'; + expect(compileCb.bind(module, testError)).to.throw(testError); + }); + + it('should reload all function handlers on compilation', () => { + const testFuncsConfs = [ + { + 'events': [ + { + 'method': 'get', + 'path': 'func1path', + 'cors': true, + } + ], + 'handler': 'module1.func1handler', + 'handlerFunc': null, + 'id': 'func1', + 'moduleName': 'module1', + }, + { + 'events': [ + { + 'method': 'POST', + 'path': 'func2path/{testParam}', + } + ], + 'handler': 'module2.func2handler', + 'handlerFunc': null, + 'id': 'func2', + 'moduleName': 'module2', + }, + { + 'events': [ + { + 'method': 'GET', + 'path': 'func3path', + } + ], + 'handler': 'module2.func2handler', + 'handlerFunc': null, + 'id': 'func3', + 'moduleName': 'module2', + }, + ]; + module._getFuncConfigs = sinon.stub().returns(testFuncsConfs); + module.loadHandler = sinon.spy(); + expressMock._resetSpies(); + webpackMock._resetSpies(); + serve = module.serve(); + listenerCb = expressMock.appMock.listen.firstCall.args[1]; + listenerCb.bind(module)(); + const compileCb = webpackMock.compilerMock.watch.firstCall.args[1]; + const testStats = {}; + module.loadHandler.reset(); + compileCb.bind(module)(null, testStats); + expect(module.loadHandler).to.have.callCount(3); + expect(module.loadHandler).to.have.been.calledWith( + testStats, + 'func1', + true + ); + expect(module.loadHandler).to.have.been.calledWith( + testStats, + 'func2', + true + ); + expect(module.loadHandler).to.have.been.calledWith( + testStats, + 'func3', + false + ); + }); + }); }); diff --git a/tests/validate.test.js b/tests/validate.test.js index 2a8a05d..e251f95 100644 --- a/tests/validate.test.js +++ b/tests/validate.test.js @@ -20,6 +20,7 @@ describe('validate', () => { fsExtraMock = makeFsExtraMock(); mockery.registerMock('fs-extra', fsExtraMock); baseModule = require('../lib/validate'); + Object.freeze(baseModule); }); after(() => { diff --git a/tests/webpack.mock.js b/tests/webpack.mock.js index 15fe4cc..2a2cf70 100644 --- a/tests/webpack.mock.js +++ b/tests/webpack.mock.js @@ -27,6 +27,7 @@ webpackMock.compilerMock = compilerMock; webpackMock._resetSpies = () => { webpackMock.reset(); compilerMock.run.reset(); + compilerMock.watch.reset(); Object.assign(statsMock, statsMockBase()); };