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;