├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── on-push.yaml │ └── on-release.yaml ├── .gitignore ├── .husky ├── .gitignore ├── pre-commit └── pre-push ├── .npmignore ├── LICENSE ├── README.md ├── install_husky.js ├── package-lock.json ├── package.json ├── src ├── index.js ├── javascript.js ├── schema.js ├── typescript.js └── utils.js └── test ├── backwardsCompatibility.test.js ├── cleanupHooks.test.js ├── processAllFunctionsHooks.node.test.js ├── processSingleFunctionHooks.node.test.js └── utils ├── configUtils.js ├── generatedFunctionTester.js └── jest.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb-base', 3 | parserOptions: { 4 | ecmaVersion: 10, 5 | }, 6 | env: { 7 | node: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/on-push.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] # macos-latest is too slow 12 | node-version: [18.x, 16.x, 14.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Cache Node.js modules on Linux 21 | uses: actions/cache@v2 22 | if: ${{ runner.OS != 'Windows' }} 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.OS }}-node- 28 | ${{ runner.OS }}- 29 | - name: Cache Node.js modules on Windows 30 | uses: actions/cache@v2 31 | if: ${{ runner.OS == 'Windows' }} 32 | with: 33 | path: ~\AppData\Roaming\npm-cache 34 | key: ${{ runner.os }}-node-${{ hashFiles('**\package-lock.json') }} 35 | restore-keys: | 36 | ${{ runner.OS }}-node- 37 | ${{ runner.OS }}- 38 | - run: npm ci 39 | - run: npm run lint 40 | - name: Unit tests 41 | run: npm run test-with-coverage 42 | - name: Coveralls Parallel 43 | uses: coverallsapp/github-action@master 44 | with: 45 | github-token: ${{ secrets.github_token }} 46 | flag-name: run-${{ matrix.node-version }} 47 | parallel: true 48 | finish: 49 | needs: test 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Coveralls Finished 53 | uses: coverallsapp/github-action@master 54 | with: 55 | github-token: ${{ secrets.github_token }} 56 | parallel-finished: true -------------------------------------------------------------------------------- /.github/workflows/on-release.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js Publish 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | runs-on: Ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v2 10 | # Setup .npmrc file to publish to npm 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 18.x 14 | # Needs to be explicitly specified for auth to work 15 | registry-url: https://registry.npmjs.org 16 | - run: npm ci 17 | - run: npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # So it cand find NPM installed by homebrew 5 | PATH="/usr/local/bin/:$PATH" 6 | 7 | npm run lint -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # So it cand find NPM installed by homebrew 5 | PATH="/usr/local/bin/:$PATH" 6 | 7 | npm test -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #tests 2 | test 3 | coverage 4 | 5 | #build tools 6 | .github 7 | .husky 8 | .eslintrc.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Juanjo Diaz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Serverless Middleware 2 | ===================== 3 | [![Serverless][serverless-badge]](serverless-badge-url) 4 | [![npm version][npm-version-badge]][npm-version-badge-url] 5 | [![npm monthly downloads][npm-downloads-badge]][npm-version-badge-url] 6 | [![Node.js CI](https://github.com/juanjoDiaz/serverless-middleware/actions/workflows/on-push.yaml/badge.svg)](https://github.com/juanjoDiaz/serverless-middleware/actions/workflows/on-push.yaml) 7 | [![Coverage Status][coveralls-badge]][coveralls-badge-url] 8 | [![license](https://img.shields.io/npm/l/serverless-middleware.svg)](https://raw.githubusercontent.com/juanjoDiaz/serverless-middleware/master/LICENSE) 9 | 10 | Serverless plugin to allow middleware handlers configured directly in serverless.yaml 11 | 12 | ## Requirements: 13 | * Serverless v3 14 | * AWS provider 15 | * Node.js 14+ 16 | 17 | ### Supported runtimes 18 | 19 | - [x] nodejs12.x (both Javascript and Typescript) 20 | - [x] nodejs14.x (both Javascript and Typescript) 21 | - [x] nodejs16.x (both Javascript and Typescript) 22 | - [x] nodejs18.x (both Javascript and Typescript) 23 | - [x] nodejs20.x (both Javascript and Typescript) 24 | - [x] nodejs22.x (both Javascript and Typescript) 25 | - [ ] dotnetcore2.1 26 | - [ ] java8 27 | - [ ] java11 28 | - [ ] go1.x 29 | - [ ] python2.7 30 | - [ ] python3.7 31 | - [ ] ruby2.5 32 | - [ ] provided 33 | 34 | ## Installation 35 | 36 | Install via npm in the root of your Serverless service: 37 | 38 | ```sh 39 | npm install serverless-middleware --save-dev 40 | ``` 41 | 42 | Add the plugin to the `plugins` array in your Serverless `serverless.yaml`: 43 | 44 | ```yaml 45 | plugins: 46 | - serverless-middleware 47 | ``` 48 | 49 | ## How it works 50 | 51 | Middleware allows you to set up multiple handlers to be executed sequentially including error handlers that will capture any exception in the chain. 52 | 53 | Middlewares are just standard AWS lambda handlers that return a promise (or are async). 54 | Handlers using `callback` will NOT work. 55 | ```js 56 | const myMiddleware = async (event, context) => { ... }; 57 | ``` 58 | 59 | Once `serverless-middleware` is installed you can set the `function.middleware` property to an array and skip the `function.handler` property. 60 | Each middleware handler can be a string (like a standard handler would be) or an object containing the properties `then` and/or `catch`. 61 | 62 | For example: 63 | 64 | ```yaml 65 | provider: 66 | name: aws 67 | runtime: nodejs22.x 68 | 69 | functions: 70 | myFunction: 71 | middleware: 72 | - auth.authenticate 73 | - auth.authorize 74 | - then: myFunction.handler # `then:` is unnecessary here. 75 | - catch: utils.handlerError 76 | - # or both can be combined 77 | then: logger.log 78 | catch: utils.handlerLoggerError 79 | ``` 80 | 81 | will result in an execution like: 82 | 83 | ```js 84 | Promise.resolve() 85 | .then(require('./auth').authenticate) 86 | .then(require('./auth').authorize) 87 | .then(require('./myFunction').handler) 88 | .catch(require('./utils').handlerError) 89 | .then(require('./logger').log) 90 | .catch(require('./utils').handlerLoggerError); 91 | ``` 92 | 93 | As with standard promises, catch handlers are only executed when there are exceptions. 94 | The resulting lambda will return the result returned by the last middleware handler executed. 95 | 96 | The `event` and `context` objects are passed from handler to handler so you can attach new properties to be accessed by subsequent handlers. 97 | `context` always contains the result of the previous handler in the `prev` property. 98 | The user can also stop at any point in the chain by calling the `end` method in the `context` argument. After `context.end()` is called, no more handlers will be executed. 99 | 100 | For example: 101 | 102 | ```js 103 | const myMiddleware = async (event, context) => { 104 | if (context.prev === undefined) { 105 | // Previous middleware handler didn't return. End execution. 106 | context.end(); 107 | return { 108 | statusCode: 200, 109 | body: 'No results', 110 | }; 111 | } 112 | 113 | ... 114 | }; 115 | ``` 116 | 117 | You can also add pre/pos- middleware handlers and maintain the `function.handler`. These middleware are just prepended/appended to the main handler. 118 | 119 | For example: 120 | 121 | ```yaml 122 | 123 | provider: 124 | name: aws 125 | runtime: nodejs22.x 126 | 127 | functions: 128 | myFunction: 129 | events: 130 | - http: 131 | path: my-function 132 | method: get 133 | handler: myFunction.handler 134 | middleware: 135 | pre: 136 | - auth.authenticate 137 | - auth.authorize 138 | pos: 139 | - catch: utils.handlerError 140 | ``` 141 | 142 | You can also add pre/pos- middleware handlers at the package level using the `custom.middleware` section of `serverless.yaml`. These middleware are just prepended/appended to all the function middleware handlers chain. 143 | 144 | For example: 145 | 146 | ```yaml 147 | 148 | provider: 149 | name: aws 150 | runtime: nodejs22.x 151 | 152 | custom: 153 | middleware: 154 | pre: 155 | - auth.authenticate 156 | pos: 157 | - catch: utils.handlerError 158 | 159 | functions: 160 | myAnonymousFunction: 161 | events: 162 | - http: 163 | path: my-anonymous-function 164 | method: get 165 | handler: myAnonymousFunction.handler 166 | myFunction: 167 | events: 168 | - http: 169 | path: my-function 170 | method: get 171 | handler: myFunction.handler 172 | middleware: 173 | pre: 174 | - auth.authorize 175 | ``` 176 | 177 | will result in a similar promise chain as above. 178 | 179 | ## Packaging 180 | 181 | In most cases, you shouldn't need to change the default packaging configuration. 182 | For edge cases, Middleware can be configured to use a specific intermediary folder and to not clear it after creating the serverless package. 183 | 184 | These settings are also set in the `custom.middleware` section of `serverless.yaml` 185 | 186 | ```yaml 187 | custom: 188 | middleware: 189 | folderName: my_custom_folder # defaults to '.middleware' 190 | cleanFolder: false # defaults to 'true' 191 | ``` 192 | 193 | This might be useful if you are using `sls package` and building your own artifacts. 194 | 195 | ## Migrations 196 | 197 | ### v1.0.0 to 2.0.0 198 | 199 | #### Use function.middleware instead fo function.custom.middleware 200 | 201 | ### v0.0.14 to v0.0.15 202 | 203 | #### Use function.custom.middleware instead fo function.handler 204 | Passing an array to the handler property is not allowed anymore since Serverless is getting stricter with it's types and it also causes issues with Typescript. 205 | 206 | So 207 | ```js 208 | functions: 209 | myFunction: 210 | handler: 211 | - auth.authenticate 212 | - auth.authorize 213 | - then: myFunction.handler # `then:` is unnecessary here. 214 | - catch: utils.handlerError 215 | - # or both can be combined 216 | then: logger.log 217 | catch: utils.handlerLoggerError 218 | ``` 219 | becomes 220 | ```js 221 | functions: 222 | myFunction: 223 | custom: 224 | middleware: 225 | - auth.authenticate 226 | - auth.authorize 227 | - then: myFunction.handler # `then:` is unnecessary here. 228 | - catch: utils.handlerError 229 | - # or both can be combined 230 | then: logger.log 231 | catch: utils.handlerLoggerError 232 | ``` 233 | 234 | ## Contribute 235 | 236 | Help us to make this plugin better. 237 | 238 | * Clone the code 239 | * Install the dependencies with `npm install` 240 | * Create a feature branch `git checkout -b new_feature` 241 | * Add your code and add tests if you implement a new feature 242 | * Validate your changes `npm run lint` and `npm test` (or `npm run test-with-coverage`) 243 | 244 | ## License 245 | 246 | This software is released under the MIT license. See [the license file](LICENSE) for more details. 247 | 248 | [serverless-badge]: http://public.serverless.com/badges/v3.svg 249 | [serverless-badge-url]: http://www.serverless.com 250 | [npm-version-badge]: https://badge.fury.io/js/serverless-middleware.svg 251 | [npm-version-badge-url]: https://www.npmjs.com/package/serverless-middleware 252 | [npm-downloads-badge]: https://img.shields.io/npm/dm/serverless-middleware.svg 253 | [coveralls-badge]: https://coveralls.io/repos/juanjoDiaz/serverless-middleware/badge.svg?branch=master 254 | [coveralls-badge-url]: https://coveralls.io/r/juanjoDiaz/serverless-middleware?branch=master -------------------------------------------------------------------------------- /install_husky.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | const util = require('util'); 4 | const exec = util.promisify(require('child_process').exec); 5 | 6 | (async () => { 7 | try { 8 | await fs.stat(path.join(__dirname, '.husky')); 9 | } catch { 10 | // We are not in dev. 11 | return; 12 | } 13 | 14 | const { stdout, stderr } = await exec('husky install'); 15 | if (stderr) { 16 | process.stderr.write(stderr); 17 | process.exit(1); 18 | } 19 | process.stdout.write(stdout); 20 | })(); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-middleware", 3 | "version": "3.4.0", 4 | "description": "Serverless plugin to allow middleware handlers configured directly in serverless.yaml", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "prepare": "husky install", 8 | "lint": "eslint .", 9 | "lint:fix": "eslint --fix .", 10 | "test": "jest test", 11 | "test-with-coverage": "jest --coverage --collectCoverageFrom=src/**/*.js test" 12 | }, 13 | "author": "Juanjo Diaz", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/juanjoDiaz/serverless-middleware.git" 18 | }, 19 | "keywords": [ 20 | "serverless", 21 | "plugin", 22 | "middleware" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/juanjoDiaz/serverless-middleware/issues" 26 | }, 27 | "homepage": "https://github.com/juanjoDiaz/serverless-middleware", 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "eslint": "^8.45.0", 31 | "eslint-config-airbnb-base": "^15.0.0", 32 | "eslint-plugin-import": "^2.27.5", 33 | "husky": "^8.0.3", 34 | "jest": "^29.6.1", 35 | "typescript": "^4.1.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module serverless-middleware 3 | * 4 | * @see {@link https://serverless.com/framework/docs/providers/aws/guide/plugins/} 5 | * 6 | * @requires 'fs' 7 | * @requires 'path' 8 | * */ 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const { extendServerlessSchema } = require('./schema'); 12 | const createJSMiddlewareHandler = require('./javascript'); 13 | const createTSMiddlewareHandler = require('./typescript'); 14 | const { parseHandler } = require('./utils'); 15 | 16 | const fsAsync = fs.promises; 17 | 18 | /** 19 | * @classdesc Easily use handlers as middleware. 20 | * @class Middleware 21 | * */ 22 | class Middleware { 23 | /** 24 | * @description Serverless Middleware 25 | * @constructor 26 | * 27 | * @param {!Object} serverless - Serverless object 28 | * */ 29 | constructor(serverless, cliOptions, { log }) { 30 | this.serverless = serverless; 31 | this.cliOptions = cliOptions; 32 | this.log = log; 33 | 34 | extendServerlessSchema(this.serverless); 35 | 36 | this.hooks = { 37 | 'before:package:createDeploymentArtifacts': this.processHandlers.bind(this), 38 | 'after:package:createDeploymentArtifacts': this.clearResources.bind(this), 39 | 'before:deploy:function:packageFunction': this.processHandlers.bind(this), 40 | 'after:deploy:function:deploy': this.clearResources.bind(this), 41 | 'before:invoke:local:invoke': this.processHandlers.bind(this), 42 | 'after:invoke:local:invoke': this.clearResources.bind(this), 43 | 'before:offline:start:init': this.processHandlers.bind(this), 44 | 'before:offline:start:end': this.clearResources.bind(this), 45 | }; 46 | 47 | this.middlewareBuilders = { 48 | js: createJSMiddlewareHandler, 49 | ts: createTSMiddlewareHandler, 50 | }; 51 | 52 | // Fix for issues in Serverles 53 | // https://github.com/serverless/serverless/pull/9307 54 | this.serviceDir = this.serverless.serviceDir || this.serverless.config.servicePath || ''; 55 | } 56 | 57 | /** 58 | * @description Configure the plugin based on the context of serverless.yml 59 | * 60 | * @return {Object} - Configuration cliOptions to be used by the plugin 61 | * */ 62 | configPlugin(service) { 63 | const defaultOpts = { 64 | folderName: '.middleware', 65 | cleanFolder: true, 66 | pre: [], 67 | pos: [], 68 | }; 69 | 70 | const config = (service.custom && service.custom.middleware) || {}; 71 | const folderName = (typeof config.folderName === 'string') ? config.folderName : defaultOpts.folderName; 72 | const pathFolder = path.join(this.serviceDir, folderName); 73 | const pathToRoot = path.relative(pathFolder, this.serviceDir); 74 | 75 | return { 76 | folderName, 77 | pathFolder, 78 | pathToRoot, 79 | cleanFolder: (typeof config.cleanFolder === 'boolean') ? config.cleanFolder : defaultOpts.cleanFolder, 80 | pre: Array.isArray(config.pre) ? config.pre : defaultOpts.pre, 81 | pos: Array.isArray(config.pos) ? config.pos : defaultOpts.pos, 82 | }; 83 | } 84 | 85 | /** 86 | * @description After package initialize hook. Create middleware functions and update the service. 87 | * 88 | * @fulfil {} — Middleware set 89 | * @reject {Error} Middleware error 90 | * 91 | * @return {Promise} 92 | * */ 93 | async processHandlers() { 94 | this.middlewareOpts = this.middlewareOpts || this.configPlugin(this.serverless.service); 95 | 96 | const fnNames = this.cliOptions.function 97 | ? [this.cliOptions.function] 98 | : this.serverless.service.getAllFunctions(); 99 | 100 | const fns = fnNames 101 | .map((name) => { 102 | const fn = this.serverless.service.getFunction(name); 103 | 104 | if (fn === undefined) { 105 | throw new this.serverless.classes.Error(`Unknown function: ${name}`); 106 | } 107 | 108 | return fn; 109 | }) 110 | .filter((fn) => this.middlewareOpts.pre.length 111 | || this.middlewareOpts.pos.length 112 | || fn.middleware) 113 | .map((fn) => { 114 | if (!fn.middleware) { 115 | return { 116 | fn, 117 | handlers: fn.handler ? [fn.handler] : [], 118 | }; 119 | } 120 | 121 | if (Array.isArray(fn.middleware)) { 122 | if (fn.handler) { 123 | throw new this.serverless.classes.Error(`Error in function ${fn.name}. When defining a handler, only the { pre: ..., pos: ...} configuration is allowed.`); 124 | } 125 | 126 | return { 127 | fn, 128 | handlers: fn.middleware, 129 | }; 130 | } 131 | 132 | return { 133 | fn, 134 | handlers: [ 135 | ...(fn.middleware.pre ? fn.middleware.pre : []), 136 | ...(fn.handler ? [fn.handler] : []), 137 | ...(fn.middleware.pos ? fn.middleware.pos : []), 138 | ], 139 | }; 140 | }) 141 | .map(({ fn, handlers: rawHandlers }) => { 142 | const handlers = this.preProcessFnHandlers(rawHandlers); 143 | const extension = this.getLanguageExtension(handlers); 144 | return { fn, handlers, extension }; 145 | }); 146 | 147 | if (fns.length === 0) return; 148 | 149 | await fsAsync.mkdir(this.middlewareOpts.pathFolder, { recursive: true }); 150 | 151 | await Promise.all(fns.map(async ({ fn, handlers, extension }) => { 152 | this.log.info(`Middleware: setting ${handlers.length} middlewares for function ${fn.name}`); 153 | 154 | const middlewareBuilder = this.middlewareBuilders[extension]; 155 | const handlerPath = path.join(this.middlewareOpts.pathFolder, `${fn.name}.${extension}`); 156 | const handler = middlewareBuilder(handlers, this.middlewareOpts.pathToRoot); 157 | await fsAsync.writeFile(handlerPath, handler); 158 | // eslint-disable-next-line no-param-reassign 159 | fn.handler = `${this.middlewareOpts.folderName}/${fn.name}.handler`; 160 | })); 161 | } 162 | 163 | /** 164 | * @description Generate the list of middlewares for a given function. 165 | * 166 | * @return {Object[]} List of middleware to include for the function 167 | * */ 168 | preProcessFnHandlers(handlers) { 169 | return [ 170 | ...this.middlewareOpts.pre, 171 | ...handlers, 172 | ...this.middlewareOpts.pos, 173 | ].map((handler) => { 174 | if (handler.then && handler.catch) { 175 | return { 176 | then: parseHandler(handler.then), 177 | catch: parseHandler(handler.catch), 178 | }; 179 | } 180 | if (handler.then) return { then: parseHandler(handler.then) }; 181 | if (handler.catch) return { catch: parseHandler(handler.catch) }; 182 | if (typeof handler === 'string') return { then: parseHandler(handler) }; 183 | 184 | throw new this.serverless.classes.Error(`Invalid handler: ${JSON.stringify(handler)}`); 185 | }); 186 | } 187 | 188 | /** 189 | * @description Determine the extension to use for the middleware handler. 190 | * 191 | * @return {string} Extension to use 192 | * */ 193 | getLanguageExtension(handlers) { 194 | switch (this.serverless.service.provider.runtime) { 195 | case 'nodejs12.x': 196 | case 'nodejs14.x': 197 | case 'nodejs16.x': 198 | case 'nodejs18.x': 199 | case 'nodejs20.x': 200 | case 'nodejs22.x': 201 | return this.getNodeExtension(handlers); 202 | // TODO add other runtimes 203 | default: 204 | throw new this.serverless.classes.Error(`Serverless Middleware doesn't support the "${this.serverless.service.provider.runtime}" runtime`); 205 | } 206 | } 207 | 208 | /** 209 | * @description Check the extension of the handlers to find determine 210 | * whether they are Javascript or TypeScript. 211 | * 212 | * @return {string} Extension to use 213 | * */ 214 | getNodeExtension(handlers) { 215 | const getNodeType = (handler) => { 216 | if (handler === undefined) return false; 217 | 218 | const { module } = handler; 219 | 220 | if (fs.existsSync(`${module}.js`) || fs.existsSync(`${module}.jsx`)) return 'js'; 221 | if (fs.existsSync(`${module}.ts`) || fs.existsSync(`${module}.tsx`)) return 'ts'; 222 | 223 | throw new this.serverless.classes.Error(`Unsupported handler extension for module ${module}. Only .js, .jsx, .ts and .tsx are supported.`); 224 | }; 225 | 226 | const isTS = handlers.some((handler) => getNodeType(handler.then) === 'ts' || getNodeType(handler.catch) === 'ts'); 227 | 228 | return isTS ? 'ts' : 'js'; 229 | } 230 | 231 | /** 232 | * @description After create deployment artifacts. Clean prefix folder. 233 | * 234 | * @fulfil {} — Optimization finished 235 | * @reject {Error} Optimization error 236 | * 237 | * @return {Promise} 238 | * */ 239 | async clearResources() { 240 | try { 241 | this.middlewareOpts = this.middlewareOpts || this.configPlugin(this.serverless.service); 242 | if (this.middlewareOpts.cleanFolder) { 243 | await fsAsync.rm(this.middlewareOpts.pathFolder, { recursive: true }); 244 | } 245 | } catch (err) { 246 | if (err.code !== 'ENOENT') { 247 | this.log.error(`Middleware: Couldn't clean up temporary folder ${this.middlewareOpts.cleanFolder}.`); 248 | } 249 | } 250 | } 251 | } 252 | 253 | module.exports = Middleware; 254 | -------------------------------------------------------------------------------- /src/javascript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Create Javascript middleware handler 3 | * 4 | * @param {Array} handlers - handlers to be run as middleware 5 | * 6 | * @return {string} Javascript middleware handler 7 | * */ 8 | function createJSMiddlewareHandler(handlers, pathToRoot) { 9 | const handlersInfo = handlers 10 | .reduce((modules, handler) => { 11 | if (handler.then && handler.catch) { 12 | const { name, module } = handler.then; 13 | const { name: name2, module: module2 } = handler.catch; 14 | return { ...modules, [module]: name, [module2]: name2 }; 15 | } 16 | if (handler.then) { 17 | const { name, module } = handler.then; 18 | return { ...modules, [module]: name }; 19 | } 20 | 21 | const { name, module } = handler.catch; 22 | return { ...modules, [module]: name }; 23 | }, {}); 24 | 25 | const imports = Object.keys(handlersInfo) 26 | .map((handler) => `const ${handlersInfo[handler]} = require('${pathToRoot}/${handler}.js');`).join('\n'); 27 | 28 | const promiseChain = handlers.map((handler) => { 29 | if (handler.then && handler.catch) { 30 | const { name, fn } = handler.then; 31 | const { name: name2, fn: fn2 } = handler.catch; 32 | return ` .then(wrappedHandler(${name}.${fn}.bind(${name}))) 33 | .catch(wrappedHandler(${name2}.${fn2}.bind(${name2})))`; 34 | } 35 | 36 | if (handler.then) { 37 | const { name, fn } = handler.then; 38 | return ` .then(wrappedHandler(${name}.${fn}.bind(${name})))`; 39 | } 40 | 41 | const { name, fn } = handler.catch; 42 | return ` .catch(wrappedHandler(${name}.${fn}.bind(${name})))`; 43 | }).join('\n'); 44 | 45 | return `'use strict'; 46 | 47 | ${imports} 48 | 49 | module.exports.handler = async (event, context) => { 50 | let end = false; 51 | context.end = () => end = true; 52 | 53 | const wrappedHandler = handler => prev => { 54 | if (end) return prev; 55 | context.prev = prev; 56 | return handler(event, context); 57 | }; 58 | 59 | return Promise.resolve() 60 | ${promiseChain}; 61 | };`; 62 | } 63 | 64 | module.exports = createJSMiddlewareHandler; 65 | -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Define the additions to the serverless schema by this plugin. 3 | * */ 4 | function extendServerlessSchema(serverless) { 5 | if (!serverless.configSchemaHandler) return; 6 | 7 | const middlewareSchema = { 8 | anyOf: [ 9 | { type: 'string' }, 10 | { 11 | type: 'object', 12 | properties: { 13 | then: { type: 'string' }, 14 | catch: { type: 'string' }, 15 | }, 16 | additionalProperties: false, 17 | }, 18 | ], 19 | }; 20 | 21 | if (typeof serverless.configSchemaHandler.defineCustomProperties === 'function') { 22 | serverless.configSchemaHandler.defineCustomProperties({ 23 | properties: { 24 | middleware: { 25 | type: 'object', 26 | properties: { 27 | folderName: { type: 'string' }, 28 | cleanFolder: { type: 'boolean' }, 29 | pre: { type: 'array', items: middlewareSchema }, 30 | pos: { type: 'array', items: middlewareSchema }, 31 | }, 32 | additionalProperties: false, 33 | }, 34 | }, 35 | }); 36 | } 37 | 38 | if (typeof serverless.configSchemaHandler.defineFunctionProperties === 'function') { 39 | serverless.configSchemaHandler.defineFunctionProperties('aws', { 40 | type: 'object', 41 | properties: { 42 | middleware: { 43 | anyOf: [ 44 | { type: 'array', items: middlewareSchema }, 45 | { 46 | type: 'object', 47 | properties: { 48 | pre: { type: 'array', items: middlewareSchema }, 49 | pos: { type: 'array', items: middlewareSchema }, 50 | }, 51 | additionalProperties: false, 52 | }, 53 | ], 54 | }, 55 | }, 56 | }); 57 | } 58 | } 59 | 60 | module.exports = { 61 | extendServerlessSchema, 62 | }; 63 | -------------------------------------------------------------------------------- /src/typescript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Create TypeScript middleware handler 3 | * 4 | * @param {Array} handlers - handlers to be run as middleware 5 | *np 6 | * @return {string} TypeScript Middleware handler 7 | * */ 8 | function createTSMiddlewareHandler(handlers, pathToRoot) { 9 | const handlersInfo = handlers 10 | .reduce((modules, handler) => { 11 | if (handler.then && handler.catch) { 12 | const { name, module } = handler.then; 13 | const { name: name2, module: module2 } = handler.catch; 14 | return { ...modules, [module]: name, [module2]: name2 }; 15 | } 16 | if (handler.then) { 17 | const { name, module } = handler.then; 18 | return { ...modules, [module]: name }; 19 | } 20 | 21 | const { name, module } = handler.catch; 22 | return { ...modules, [module]: name }; 23 | }, {}); 24 | 25 | const imports = Object.keys(handlersInfo) 26 | .map((handler) => `import * as ${handlersInfo[handler]} from '${pathToRoot}/${handler}.js';`).join('\n'); 27 | 28 | const promiseChain = handlers.map((handler) => { 29 | if (handler.then && handler.catch) { 30 | const { name, fn } = handler.then; 31 | const { name: name2, fn: fn2 } = handler.catch; 32 | return ` .then(wrappedHandler(${name}.${fn}.bind(${name}))) 33 | .catch(wrappedHandler(${name2}.${fn2}.bind(${name2})))`; 34 | } 35 | 36 | if (handler.then) { 37 | const { name, fn } = handler.then; 38 | return ` .then(wrappedHandler(${name}.${fn}.bind(${name})))`; 39 | } 40 | 41 | const { name, fn } = handler.catch; 42 | return ` .catch(wrappedHandler(${name}.${fn}.bind(${name})))`; 43 | }).join('\n'); 44 | 45 | return `'use strict'; 46 | 47 | import { Context } from 'aws-lambda'; 48 | 49 | type MiddlewareContext = Context & { end: () => void, prev: TResult }; 50 | type Handler = ( 51 | event: TEvent, 52 | context: MiddlewareContext, 53 | ) => Promise; 54 | 55 | ${imports} 56 | 57 | export const handler: Handler = (event, context) => { 58 | let end = false; 59 | context.end = () => end = true; 60 | 61 | const wrappedHandler = (handler: Handler) => (prev: any): Promise => { 62 | if (end) return prev; 63 | context.prev = prev; 64 | return handler(event, context); 65 | }; 66 | 67 | return Promise.resolve() 68 | ${promiseChain}; 69 | };`; 70 | } 71 | 72 | module.exports = createTSMiddlewareHandler; 73 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function parseHandler(handler) { 2 | const [module, fn] = handler.split(/\.(?=[^.]+$)/); 3 | return { 4 | name: module.replace(/^[^a-zA-Z_$]|[^\w_$]/g, '_'), 5 | module, 6 | fn, 7 | }; 8 | } 9 | 10 | module.exports = { 11 | parseHandler, 12 | }; 13 | -------------------------------------------------------------------------------- /test/backwardsCompatibility.test.js: -------------------------------------------------------------------------------- 1 | /* global jest beforeEach describe it expect */ 2 | 3 | jest.mock('fs', () => ({ 4 | existsSync: jest.fn(), 5 | promises: { 6 | mkdir: jest.fn(), 7 | unlink: jest.fn(), 8 | writeFile: jest.fn(), 9 | rm: jest.fn(), 10 | }, 11 | })); 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | 15 | const fsAsync = fs.promises; 16 | const Middleware = require('../src/index'); 17 | const { getServerlessConfig, getPluginUtils } = require('./utils/configUtils'); 18 | 19 | describe('Backward compatibility', () => { 20 | describe('configSchemaHandler', () => { 21 | it('should not set the schema if configSchemaHandler is undefined', async () => { 22 | const serverless = getServerlessConfig({ 23 | configSchemaHandler: null, 24 | }); 25 | const pluginUtils = getPluginUtils(); 26 | 27 | // eslint-disable-next-line no-new 28 | new Middleware(serverless, {}, pluginUtils); 29 | }); 30 | 31 | it('should not define custom properties if defineCustomProperties is undefined', async () => { 32 | const defineCustomProperties = null; 33 | const defineFunctionProperties = jest.fn(() => {}); 34 | const serverless = getServerlessConfig({ 35 | configSchemaHandler: { 36 | defineCustomProperties, 37 | defineFunctionProperties, 38 | }, 39 | }); 40 | const pluginUtils = getPluginUtils(); 41 | 42 | // eslint-disable-next-line no-new 43 | new Middleware(serverless, {}, pluginUtils); 44 | 45 | expect(defineFunctionProperties).toHaveBeenCalledTimes(1); 46 | }); 47 | 48 | it('should not define function properties if defineFunctionProperties is undefined', async () => { 49 | const defineCustomProperties = jest.fn(() => {}); 50 | const defineFunctionProperties = null; 51 | const serverless = getServerlessConfig({ 52 | configSchemaHandler: { 53 | defineCustomProperties, 54 | defineFunctionProperties, 55 | }, 56 | }); 57 | const pluginUtils = getPluginUtils(); 58 | 59 | // eslint-disable-next-line no-new 60 | new Middleware(serverless, {}, pluginUtils); 61 | 62 | expect(defineCustomProperties).toHaveBeenCalledTimes(1); 63 | }); 64 | }); 65 | 66 | describe('servicePath renamed to serviceDir', () => { 67 | beforeEach(() => { 68 | fs.existsSync.mockImplementation((filePath) => filePath.endsWith('.js')); 69 | fsAsync.mkdir.mockClear(); 70 | fsAsync.mkdir.mockResolvedValue(undefined); 71 | fsAsync.writeFile.mockClear(); 72 | fsAsync.writeFile.mockResolvedValue(undefined); 73 | }); 74 | 75 | it('should fallback to servicePath if serviceDir is not defined', async () => { 76 | const serverless = getServerlessConfig({ 77 | service: { 78 | custom: { 79 | warmup: { 80 | default: { 81 | enabled: true, 82 | }, 83 | }, 84 | }, 85 | functions: { 86 | someFunc1: { 87 | name: 'someFunc1', 88 | middleware: ['middleware1.handler', 'middleware2.handler', 'handler.handler'], 89 | }, 90 | someFunc2: { name: 'someFunc2' }, 91 | }, 92 | }, 93 | serviceDir: null, 94 | config: { 95 | servicePath: 'testPath', 96 | }, 97 | }); 98 | const pluginUtils = getPluginUtils(); 99 | 100 | const plugin = new Middleware(serverless, {}, pluginUtils); 101 | 102 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 103 | 104 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 105 | expect(fsAsync.mkdir).toHaveBeenCalledWith(path.join('testPath', '.middleware'), { recursive: true }); 106 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 107 | expect(fsAsync.writeFile).toHaveBeenCalledWith(path.join('testPath', '.middleware', 'someFunc1.js'), expect.anything()); 108 | }); 109 | 110 | it('should fallback to \'\' if serviceDir and servicePath are not defined', async () => { 111 | const serverless = getServerlessConfig({ 112 | service: { 113 | functions: { 114 | someFunc1: { 115 | name: 'someFunc1', 116 | middleware: ['middleware1.handler', 'middleware2.handler', 'handler.handler'], 117 | }, 118 | someFunc2: { name: 'someFunc2' }, 119 | }, 120 | }, 121 | serviceDir: null, 122 | }); 123 | const pluginUtils = getPluginUtils(); 124 | 125 | const plugin = new Middleware(serverless, {}, pluginUtils); 126 | 127 | await plugin.hooks['before:package:createDeploymentArtifacts'](); 128 | 129 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 130 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('', '.middleware'), { recursive: true }); 131 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 132 | expect(fsAsync.writeFile).toHaveBeenCalledWith(path.join('', '.middleware', 'someFunc1.js'), expect.anything()); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/cleanupHooks.test.js: -------------------------------------------------------------------------------- 1 | /* global jest beforeEach describe it expect */ 2 | 3 | jest.mock('fs', () => ({ 4 | existsSync: jest.mock(), 5 | promises: { 6 | mkdir: jest.fn(), 7 | write: jest.fn(), 8 | rm: jest.fn(), 9 | }, 10 | })); 11 | const fsAsync = require('fs').promises; 12 | const path = require('path'); 13 | const Middleware = require('../src/index'); 14 | const { getServerlessConfig, getPluginUtils } = require('./utils/configUtils'); 15 | 16 | describe.each([ 17 | 'after:package:createDeploymentArtifacts', 18 | 'after:deploy:function:deploy', 19 | 'after:invoke:local:invoke', 20 | 'before:offline:start:end', 21 | ])('Serverless middleware plugin %s hook', (hook) => { 22 | beforeEach(() => fsAsync.rm.mockClear()); 23 | 24 | it('Should clean the temporary folder by default', async () => { 25 | const mockProvider = { request: jest.fn(() => Promise.resolve()) }; 26 | const serverless = getServerlessConfig({ 27 | getProvider() { return mockProvider; }, 28 | service: { 29 | functions: { 30 | someFunc1: { 31 | name: 'someFunc1', 32 | middleware: ['middleware1.handler', 'middleware2.handler', 'handler.handler'], 33 | }, 34 | }, 35 | }, 36 | }); 37 | const pluginUtils = getPluginUtils({ 38 | log: { 39 | error: jest.fn(), 40 | }, 41 | }); 42 | 43 | const plugin = new Middleware(serverless, {}, pluginUtils); 44 | 45 | await plugin.hooks[hook](); 46 | 47 | expect(fsAsync.rm).toHaveBeenCalledTimes(1); 48 | expect(fsAsync.rm).toHaveBeenCalledWith(path.join('testPath', '.middleware'), { recursive: true }); 49 | expect(pluginUtils.log.error).not.toHaveBeenCalledWith(expect.stringMatching(/^Middleware: Couldn't clean up temporary folder .*/)); 50 | }); 51 | 52 | it('Should clean the temporary folder if cleanFolder is set to true', async () => { 53 | const mockProvider = { request: jest.fn(() => Promise.resolve()) }; 54 | const serverless = getServerlessConfig({ 55 | getProvider() { return mockProvider; }, 56 | service: { 57 | custom: { 58 | middleware: { 59 | cleanFolder: true, 60 | }, 61 | }, 62 | functions: { 63 | someFunc1: { 64 | name: 'someFunc1', 65 | middleware: ['middleware1.handler', 'middleware2.handler', 'handler.handler'], 66 | }, 67 | }, 68 | }, 69 | }); 70 | const pluginUtils = getPluginUtils({ 71 | log: { 72 | error: jest.fn(), 73 | }, 74 | }); 75 | 76 | const plugin = new Middleware(serverless, {}, pluginUtils); 77 | 78 | await plugin.hooks[hook](); 79 | 80 | expect(fsAsync.rm).toHaveBeenCalledTimes(1); 81 | expect(fsAsync.rm).toHaveBeenCalledWith(path.join('testPath', '.middleware'), { recursive: true }); 82 | expect(pluginUtils.log.error).not.toHaveBeenCalledWith(expect.stringMatching(/^Middleware: Couldn't clean up temporary folder .*/)); 83 | }); 84 | 85 | it('Should clean the custom temporary folder if cleanFolder is set to true', async () => { 86 | const mockProvider = { request: jest.fn(() => Promise.resolve()) }; 87 | const serverless = getServerlessConfig({ 88 | getProvider() { return mockProvider; }, 89 | service: { 90 | custom: { 91 | middleware: { 92 | folderName: 'test-folder', 93 | cleanFolder: true, 94 | }, 95 | }, 96 | functions: { 97 | someFunc1: { 98 | name: 'someFunc1', 99 | middleware: ['middleware1.handler', 'middleware2.handler', 'handler.handler'], 100 | }, 101 | }, 102 | }, 103 | }); 104 | const pluginUtils = getPluginUtils({ 105 | log: { 106 | error: jest.fn(), 107 | }, 108 | }); 109 | 110 | const plugin = new Middleware(serverless, {}, pluginUtils); 111 | 112 | await plugin.hooks[hook](); 113 | 114 | expect(fsAsync.rm).toHaveBeenCalledTimes(1); 115 | expect(fsAsync.rm).toHaveBeenCalledWith(path.join('testPath', 'test-folder'), { recursive: true }); 116 | expect(pluginUtils.log.error).not.toHaveBeenCalledWith(expect.stringMatching(/^Middleware: Couldn't clean up temporary folder .*/)); 117 | }); 118 | 119 | it('Should ignore cleaning the custom temporary folder if there was nothing to clean', async () => { 120 | const err = new Error('Folder doesn\'t exist'); 121 | err.code = 'ENOENT'; 122 | fsAsync.rm.mockRejectedValueOnce(err); 123 | const serverless = getServerlessConfig({ 124 | service: { 125 | functions: { someFunc1: { name: 'someFunc1' }, someFunc2: { name: 'someFunc2' } }, 126 | }, 127 | }); 128 | const pluginUtils = getPluginUtils({ 129 | log: { 130 | error: jest.fn(), 131 | }, 132 | }); 133 | 134 | const plugin = new Middleware(serverless, {}, pluginUtils); 135 | 136 | await plugin.hooks[hook](); 137 | 138 | expect(fsAsync.rm).toHaveBeenCalledTimes(1); 139 | expect(fsAsync.rm).toHaveBeenCalledWith(path.join('testPath', '.middleware'), { recursive: true }); 140 | expect(pluginUtils.log.error).not.toHaveBeenCalledWith(expect.stringMatching(/^Middleware: Couldn't clean up temporary folder .*/)); 141 | }); 142 | 143 | it('Should not error if couldn\'t clean up the custom temporary folder', async () => { 144 | fsAsync.rm.mockRejectedValueOnce(new Error('Folder couldn\'t be cleaned')); 145 | const serverless = getServerlessConfig({ 146 | service: { 147 | functions: { someFunc1: { name: 'someFunc1' }, someFunc2: { name: 'someFunc2' } }, 148 | }, 149 | }); 150 | const pluginUtils = getPluginUtils({ 151 | log: { 152 | error: jest.fn(), 153 | }, 154 | }); 155 | 156 | const plugin = new Middleware(serverless, {}, pluginUtils); 157 | 158 | await plugin.hooks[hook](); 159 | 160 | expect(fsAsync.rm).toHaveBeenCalledTimes(1); 161 | expect(fsAsync.rm).toHaveBeenCalledWith(path.join('testPath', '.middleware'), { recursive: true }); 162 | expect(pluginUtils.log.error).toHaveBeenCalledWith(expect.stringMatching(/^Middleware: Couldn't clean up temporary folder .*/)); 163 | }); 164 | 165 | it('Should not clean the temporary folder if cleanFolder is set to false', async () => { 166 | const mockProvider = { request: jest.fn(() => Promise.resolve()) }; 167 | const serverless = getServerlessConfig({ 168 | getProvider() { return mockProvider; }, 169 | service: { 170 | custom: { 171 | middleware: { 172 | cleanFolder: false, 173 | }, 174 | }, 175 | functions: { 176 | someFunc1: { 177 | name: 'someFunc1', 178 | middleware: ['middleware1.handler', 'middleware2.handler', 'handler.handler'], 179 | }, 180 | }, 181 | }, 182 | }); 183 | const pluginUtils = getPluginUtils({ 184 | log: { 185 | error: jest.fn(), 186 | }, 187 | }); 188 | 189 | const plugin = new Middleware(serverless, {}, pluginUtils); 190 | 191 | await plugin.hooks[hook](); 192 | 193 | expect(fsAsync.rm).not.toHaveBeenCalled(); 194 | expect(pluginUtils.log.error).not.toHaveBeenCalledWith(expect.stringMatching(/^Middleware: Couldn't clean up temporary folder .*/)); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /test/processAllFunctionsHooks.node.test.js: -------------------------------------------------------------------------------- 1 | /* global jest beforeEach describe it expect */ 2 | 3 | jest.mock('fs', () => ({ 4 | existsSync: jest.fn(), 5 | promises: { 6 | mkdir: jest.fn(), 7 | writeFile: jest.fn(), 8 | rm: jest.fn(), 9 | }, 10 | })); 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | 14 | const fsAsync = fs.promises; 15 | const Middleware = require('../src/index'); 16 | const { getServerlessConfig, getPluginUtils } = require('./utils/configUtils'); 17 | const { GeneratedFunctionTester } = require('./utils/generatedFunctionTester'); 18 | const { shouldHaveBeenCalledInOrder } = require('./utils/jest'); 19 | 20 | fsAsync.mkdir.mockReturnValue(Promise.resolve()); 21 | fsAsync.writeFile.mockReturnValue(Promise.resolve()); 22 | 23 | describe.each([ 24 | 'before:package:createDeploymentArtifacts', 25 | 'before:offline:start:init', 26 | ])('Serverless middleware %s hook', (hook) => { 27 | beforeEach(() => { 28 | fsAsync.mkdir.mockClear(); 29 | fsAsync.writeFile.mockClear(); 30 | }); 31 | 32 | describe('error cases', () => { 33 | beforeEach(() => fs.existsSync.mockImplementation((filePath) => filePath.endsWith('.js'))); 34 | 35 | it('should error on unsupported runtimes', async () => { 36 | const serverless = getServerlessConfig({ 37 | service: { 38 | provider: { stage: '', region: '', runtime: 'dotnet' }, 39 | functions: { 40 | someFunc1: { 41 | name: 'someFunc1', 42 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 43 | }, 44 | someFunc2: { 45 | name: 'someFunc2', 46 | handler: 'someFunc2.handler', 47 | }, 48 | }, 49 | }, 50 | }); 51 | const pluginUtils = getPluginUtils(); 52 | 53 | const plugin = new Middleware(serverless, {}, pluginUtils); 54 | 55 | await expect(plugin.hooks[hook]()).rejects.toThrow('Serverless Middleware doesn\'t support the "dotnet" runtime'); 56 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 57 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 58 | }); 59 | 60 | it('should error on unsupported node extensions', async () => { 61 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 62 | const serverless = getServerlessConfig({ 63 | service: { 64 | functions: { 65 | someFunc1: { 66 | name: 'someFunc1', 67 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 68 | }, 69 | someFunc2: { 70 | name: 'someFunc2', 71 | handler: 'someFunc2.handler', 72 | }, 73 | }, 74 | }, 75 | }); 76 | const pluginUtils = getPluginUtils(); 77 | 78 | const plugin = new Middleware(serverless, {}, pluginUtils); 79 | 80 | await expect(plugin.hooks[hook]()).rejects.toThrow('Unsupported handler extension for module middleware1. Only .js, .jsx, .ts and .tsx are supported.'); 81 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 82 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 83 | }); 84 | 85 | it('should error on invalid handler', async () => { 86 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 87 | const serverless = getServerlessConfig({ 88 | service: { 89 | functions: { 90 | someFunc1: { 91 | name: 'someFunc1', 92 | middleware: [{ wrong_field: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 93 | }, 94 | someFunc2: { 95 | name: 'someFunc2', 96 | handler: 'someFunc2.handler', 97 | }, 98 | }, 99 | }, 100 | }); 101 | const pluginUtils = getPluginUtils(); 102 | 103 | const plugin = new Middleware(serverless, {}, pluginUtils); 104 | 105 | await expect(plugin.hooks[hook]()).rejects.toThrow('Invalid handler: {"wrong_field":"middleware1.handler"}'); 106 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 107 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 108 | }); 109 | 110 | it('should error on function mixing handler and array middlewares', async () => { 111 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 112 | const serverless = getServerlessConfig({ 113 | service: { 114 | functions: { 115 | someFunc1: { 116 | name: 'someFunc1', 117 | handler: 'someFunc1.handler', 118 | middleware: ['middleware1.handler', 'middleware2.handler'], 119 | }, 120 | someFunc2: { 121 | name: 'someFunc2', 122 | handler: 'someFunc2.handler', 123 | }, 124 | }, 125 | }, 126 | }); 127 | const pluginUtils = getPluginUtils(); 128 | 129 | const plugin = new Middleware(serverless, {}, pluginUtils); 130 | 131 | await expect(plugin.hooks[hook]()).rejects.toThrow('Error in function someFunc1. When defining a handler, only the { pre: ..., pos: ...} configuration is allowed.'); 132 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 133 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 134 | }); 135 | }); 136 | 137 | describe('without functions', () => { 138 | beforeEach(() => fs.existsSync.mockImplementation((filePath) => filePath.endsWith('.js'))); 139 | it('should process handlers that contain arrays and do nothing with standard handlers', async () => { 140 | const serverless = getServerlessConfig({ 141 | service: { 142 | functions: {}, 143 | }, 144 | }); 145 | const pluginUtils = getPluginUtils(); 146 | 147 | const plugin = new Middleware(serverless, {}, pluginUtils); 148 | 149 | await plugin.hooks[hook](); 150 | 151 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 152 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 153 | }); 154 | }); 155 | 156 | describe.each([ 157 | ['js', GeneratedFunctionTester.fromJavaScript], 158 | ['ts', GeneratedFunctionTester.fromTypeScript], 159 | ])('Node.js extension: %s', (extension, functionTesterFrom) => { 160 | beforeEach(() => fs.existsSync.mockImplementation((filePath) => filePath.endsWith(`.${extension}`))); 161 | 162 | describe('without pre/pos', () => { 163 | it('should process handlers that contain arrays and do nothing with standard handlers', async () => { 164 | const serverless = getServerlessConfig({ 165 | service: { 166 | functions: { 167 | someFunc1: { 168 | name: 'someFunc1', 169 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 170 | }, 171 | someFunc2: { 172 | name: 'someFunc2', 173 | handler: 'someFunc2.handler', 174 | }, 175 | }, 176 | }, 177 | }); 178 | const pluginUtils = getPluginUtils(); 179 | 180 | const plugin = new Middleware(serverless, {}, pluginUtils); 181 | 182 | await plugin.hooks[hook](); 183 | 184 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 185 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('someFunc2.handler'); 186 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 187 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 188 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 189 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 190 | 191 | const event = {}; 192 | const context = {}; 193 | const middlewares = { 194 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 195 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 196 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 197 | }; 198 | 199 | const functionTester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 200 | await functionTester.executeMiddlewareFunction(event, context, middlewares); 201 | 202 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 203 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 204 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 205 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 206 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 207 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 208 | 209 | shouldHaveBeenCalledInOrder([ 210 | middlewares.middleware1.handler, 211 | middlewares.middleware2.handler, 212 | middlewares.someFunc1.handler, 213 | ]); 214 | }); 215 | 216 | it('should process handler with catch blocks', async () => { 217 | const serverless = getServerlessConfig({ 218 | service: { 219 | functions: { 220 | someFunc1: { 221 | name: 'someFunc1', 222 | middleware: [ 223 | { then: 'middleware1.handler' }, 224 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 225 | 'middleware3.handler', 226 | { catch: 'catchMiddleware2.handler' }, 227 | 'someFunc1.handler', 228 | ], 229 | }, 230 | someFunc2: { 231 | name: 'someFunc2', 232 | handler: 'someFunc2.handler', 233 | }, 234 | }, 235 | }, 236 | }); 237 | const pluginUtils = getPluginUtils(); 238 | 239 | const plugin = new Middleware(serverless, {}, pluginUtils); 240 | 241 | await plugin.hooks[hook](); 242 | 243 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 244 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('someFunc2.handler'); 245 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 246 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 247 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 248 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 249 | 250 | const event = {}; 251 | const context = {}; 252 | const err = new Error('Error.'); 253 | const middlewares = { 254 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 255 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 256 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err)) }, 257 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 258 | catchMiddleware2: { 259 | handler: jest.fn().mockImplementation(() => { 260 | expect(context.prev).toEqual(err); 261 | return Promise.resolve(); 262 | }), 263 | }, 264 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 265 | }; 266 | 267 | const functionTester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 268 | await functionTester.executeMiddlewareFunction(event, context, middlewares); 269 | 270 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 271 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 272 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 273 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 274 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 275 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 276 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 277 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 278 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 279 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 280 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 281 | 282 | shouldHaveBeenCalledInOrder([ 283 | middlewares.middleware1.handler, 284 | middlewares.middleware2.handler, 285 | middlewares.middleware3.handler, 286 | middlewares.catchMiddleware2.handler, 287 | middlewares.someFunc1.handler, 288 | ]); 289 | }); 290 | 291 | it('should end process if context.end is called', async () => { 292 | const serverless = getServerlessConfig({ 293 | service: { 294 | functions: { 295 | someFunc1: { 296 | name: 'someFunc1', 297 | middleware: [ 298 | { then: 'middleware1.handler' }, 299 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 300 | 'middleware3.handler', 301 | { catch: 'catchMiddleware2.handler' }, 302 | 'someFunc1.handler', 303 | ], 304 | }, 305 | someFunc2: { 306 | name: 'someFunc2', 307 | handler: 'someFunc2.handler', 308 | }, 309 | }, 310 | }, 311 | }); 312 | const pluginUtils = getPluginUtils(); 313 | 314 | const plugin = new Middleware(serverless, {}, pluginUtils); 315 | 316 | await plugin.hooks[hook](); 317 | 318 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 319 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('someFunc2.handler'); 320 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 321 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 322 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 323 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 324 | 325 | const event = {}; 326 | const context = {}; 327 | const err = new Error('Error.'); 328 | const middlewares = { 329 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 330 | middleware2: { 331 | handler: jest.fn().mockImplementation((_, ctx) => { 332 | ctx.end(); 333 | return Promise.resolve(); 334 | }), 335 | }, 336 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err)) }, 337 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 338 | catchMiddleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 339 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 340 | }; 341 | 342 | const functionTester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 343 | await functionTester.executeMiddlewareFunction(event, context, middlewares); 344 | 345 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 346 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 347 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 348 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 349 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 350 | expect(middlewares.middleware3.handler).not.toHaveBeenCalled(); 351 | expect(middlewares.catchMiddleware2.handler).not.toHaveBeenCalled(); 352 | expect(middlewares.someFunc1.handler).not.toHaveBeenCalled(); 353 | 354 | shouldHaveBeenCalledInOrder([ 355 | middlewares.middleware1.handler, 356 | middlewares.middleware2.handler, 357 | ]); 358 | }); 359 | }); 360 | 361 | describe('with pre-handlers', () => { 362 | it('should process standard handlers and array middlewares and add the global pre-handlers', async () => { 363 | const serverless = getServerlessConfig({ 364 | service: { 365 | custom: { 366 | middleware: { 367 | pre: ['preHandler1.handler', 'preHandler2.handler'], 368 | }, 369 | }, 370 | functions: { 371 | someFunc1: { 372 | name: 'someFunc1', 373 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 374 | }, 375 | someFunc2: { 376 | name: 'someFunc2', 377 | handler: 'someFunc2.handler', 378 | }, 379 | }, 380 | }, 381 | }); 382 | const pluginUtils = getPluginUtils(); 383 | 384 | const plugin = new Middleware(serverless, {}, pluginUtils); 385 | 386 | await plugin.hooks[hook](); 387 | 388 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 389 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('.middleware/someFunc2.handler'); 390 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 391 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 392 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(2); 393 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 394 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(2, path.join('testPath', '.middleware', `someFunc2.${extension}`), expect.any(String)); 395 | 396 | const event = {}; 397 | const context = {}; 398 | const middlewares = { 399 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 400 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 401 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 402 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 403 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 404 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 405 | }; 406 | 407 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 408 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 409 | 410 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 411 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 412 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 413 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 414 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 415 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 416 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 417 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 418 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 419 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 420 | 421 | shouldHaveBeenCalledInOrder([ 422 | middlewares.preHandler1.handler, 423 | middlewares.preHandler2.handler, 424 | middlewares.middleware1.handler, 425 | middlewares.middleware2.handler, 426 | middlewares.someFunc1.handler, 427 | ]); 428 | 429 | middlewares.preHandler1.handler.mockClear(); 430 | middlewares.preHandler2.handler.mockClear(); 431 | 432 | const someFunc2Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[1][1]); 433 | await someFunc2Tester.executeMiddlewareFunction(event, context, middlewares); 434 | 435 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 436 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 437 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 438 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 439 | expect(middlewares.someFunc2.handler).toHaveBeenCalledTimes(1); 440 | expect(middlewares.someFunc2.handler).toHaveBeenCalledWith(event, context); 441 | 442 | // Commented because jest doesn't clear invocationCallOrder 443 | // shouldHaveBeenCalledInOrder([ 444 | // middlewares.preHandler1.handler, 445 | // middlewares.preHandler2.handler, 446 | // middlewares.middleware1.handler, 447 | // middlewares.middleware2.handler, 448 | // middlewares.someFunc2.handler, 449 | // ]); 450 | }); 451 | 452 | it('should process middlewares and add pre-handlers with catch blocks', async () => { 453 | const serverless = getServerlessConfig({ 454 | service: { 455 | custom: { 456 | middleware: { 457 | pre: ['preHandler1.handler', { then: 'preHandler2.handler', catch: 'catchPreHandler1.handler' }], 458 | }, 459 | }, 460 | functions: { 461 | someFunc1: { 462 | name: 'someFunc1', 463 | middleware: [ 464 | { then: 'middleware1.handler' }, 465 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 466 | 'middleware3.handler', 467 | { catch: 'catchMiddleware2.handler' }, 468 | 'someFunc1.handler', 469 | ], 470 | }, 471 | someFunc2: { 472 | name: 'someFunc2', 473 | handler: 'someFunc2.handler', 474 | }, 475 | }, 476 | }, 477 | }); 478 | const pluginUtils = getPluginUtils(); 479 | 480 | const plugin = new Middleware(serverless, {}, pluginUtils); 481 | 482 | await plugin.hooks[hook](); 483 | 484 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 485 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('.middleware/someFunc2.handler'); 486 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 487 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 488 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(2); 489 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 490 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(2, path.join('testPath', '.middleware', `someFunc2.${extension}`), expect.any(String)); 491 | 492 | const event = {}; 493 | const context = {}; 494 | const err1 = new Error('Error 1.'); 495 | const err2 = new Error('Error 2.'); 496 | const middlewares = { 497 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 498 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 499 | catchPreHandler1: { 500 | handler: jest.fn().mockImplementation(() => { 501 | expect(context.prev).toEqual(err1); 502 | return Promise.resolve(); 503 | }), 504 | }, 505 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 506 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 507 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 508 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 509 | catchMiddleware2: { 510 | handler: jest.fn().mockImplementation(() => { 511 | expect(context.prev).toEqual(err2); 512 | return Promise.resolve(); 513 | }), 514 | }, 515 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 516 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 517 | }; 518 | 519 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 520 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 521 | 522 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 523 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 524 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 525 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 526 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledTimes(1); 527 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledWith(event, context); 528 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 529 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 530 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 531 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 532 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 533 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 534 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 535 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 536 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 537 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 538 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 539 | 540 | shouldHaveBeenCalledInOrder([ 541 | middlewares.preHandler1.handler, 542 | middlewares.preHandler2.handler, 543 | middlewares.catchPreHandler1.handler, 544 | middlewares.middleware1.handler, 545 | middlewares.middleware2.handler, 546 | middlewares.middleware3.handler, 547 | middlewares.catchMiddleware2.handler, 548 | middlewares.someFunc1.handler, 549 | ]); 550 | 551 | middlewares.preHandler1.handler.mockClear(); 552 | middlewares.preHandler2.handler.mockClear(); 553 | middlewares.catchPreHandler1.handler.mockClear(); 554 | const someFunc2Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[1][1]); 555 | await someFunc2Tester.executeMiddlewareFunction(event, context, middlewares); 556 | 557 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 558 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 559 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 560 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 561 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledTimes(1); 562 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledWith(event, context); 563 | expect(middlewares.someFunc2.handler).toHaveBeenCalledTimes(1); 564 | expect(middlewares.someFunc2.handler).toHaveBeenCalledWith(event, context); 565 | 566 | // Commented because jest doesn't clear invocationCallOrder 567 | // shouldHaveBeenCalledInOrder([ 568 | // middlewares.preHandler1.handler, 569 | // middlewares.preHandler2.handler, 570 | // middlewares.catchPreHandler1.handler, 571 | // middlewares.someFunc2.handler, 572 | // ]); 573 | }); 574 | 575 | it('should process standard handlers and array middlewares and add the function-specific pre-handlers', async () => { 576 | const serverless = getServerlessConfig({ 577 | service: { 578 | functions: { 579 | someFunc1: { 580 | name: 'someFunc1', 581 | handler: 'someFunc1.handler', 582 | middleware: { 583 | pre: ['preHandler1.handler', 'preHandler2.handler'], 584 | }, 585 | }, 586 | }, 587 | }, 588 | }); 589 | const pluginUtils = getPluginUtils(); 590 | 591 | const plugin = new Middleware(serverless, {}, pluginUtils); 592 | 593 | await plugin.hooks[hook](); 594 | 595 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 596 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 597 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 598 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 599 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 600 | 601 | const event = {}; 602 | const context = {}; 603 | const middlewares = { 604 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 605 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 606 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 607 | }; 608 | 609 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 610 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 611 | 612 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 613 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 614 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 615 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 616 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 617 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 618 | 619 | shouldHaveBeenCalledInOrder([ 620 | middlewares.preHandler1.handler, 621 | middlewares.preHandler2.handler, 622 | middlewares.someFunc1.handler, 623 | ]); 624 | 625 | middlewares.preHandler1.handler.mockClear(); 626 | middlewares.preHandler2.handler.mockClear(); 627 | 628 | // Commented because jest doesn't clear invocationCallOrder 629 | // shouldHaveBeenCalledInOrder([ 630 | // middlewares.preHandler1.handler, 631 | // middlewares.preHandler2.handler, 632 | // middlewares.middleware1.handler, 633 | // middlewares.middleware2.handler, 634 | // middlewares.someFunc2.handler, 635 | // ]); 636 | }); 637 | 638 | it('should end process if context.end is called', async () => { 639 | const serverless = getServerlessConfig({ 640 | service: { 641 | custom: { 642 | middleware: { 643 | pre: ['preHandler1.handler', { then: 'preHandler2.handler', catch: 'catchPreHandler1.handler' }], 644 | }, 645 | }, 646 | functions: { 647 | someFunc1: { 648 | name: 'someFunc1', 649 | middleware: [ 650 | { then: 'middleware1.handler' }, 651 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 652 | 'middleware3.handler', 653 | { catch: 'catchMiddleware2.handler' }, 654 | 'someFunc1.handler', 655 | ], 656 | }, 657 | someFunc2: { 658 | name: 'someFunc2', 659 | handler: 'someFunc2.handler', 660 | }, 661 | }, 662 | }, 663 | }); 664 | const pluginUtils = getPluginUtils(); 665 | 666 | const plugin = new Middleware(serverless, {}, pluginUtils); 667 | 668 | await plugin.hooks[hook](); 669 | 670 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 671 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('.middleware/someFunc2.handler'); 672 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 673 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 674 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(2); 675 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 676 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(2, path.join('testPath', '.middleware', `someFunc2.${extension}`), expect.any(String)); 677 | 678 | const event = {}; 679 | const context = {}; 680 | const err1 = new Error('Error 1.'); 681 | const err2 = new Error('Error 2.'); 682 | const middlewares = { 683 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 684 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 685 | catchPreHandler1: { 686 | handler: jest.fn().mockImplementation((_, ctx) => { 687 | expect(ctx.prev).toEqual(err1); 688 | ctx.end(); 689 | return Promise.resolve(); 690 | }), 691 | }, 692 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 693 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 694 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 695 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 696 | catchMiddleware2: { 697 | handler: jest.fn().mockImplementation(() => { 698 | expect(context.prev).toEqual(err2); 699 | return Promise.resolve(); 700 | }), 701 | }, 702 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 703 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 704 | }; 705 | 706 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 707 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 708 | 709 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 710 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 711 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 712 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 713 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledTimes(1); 714 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledWith(event, context); 715 | expect(middlewares.middleware1.handler).not.toHaveBeenCalled(); 716 | expect(middlewares.middleware2.handler).not.toHaveBeenCalled(); 717 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 718 | expect(middlewares.middleware3.handler).not.toHaveBeenCalled(); 719 | expect(middlewares.catchMiddleware2.handler).not.toHaveBeenCalled(); 720 | expect(middlewares.someFunc1.handler).not.toHaveBeenCalled(); 721 | 722 | shouldHaveBeenCalledInOrder([ 723 | middlewares.preHandler1.handler, 724 | middlewares.preHandler2.handler, 725 | middlewares.catchPreHandler1.handler, 726 | ]); 727 | 728 | middlewares.preHandler1.handler.mockClear(); 729 | middlewares.preHandler2.handler.mockClear(); 730 | middlewares.catchPreHandler1.handler.mockClear(); 731 | const someFunc2Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[1][1]); 732 | await someFunc2Tester.executeMiddlewareFunction(event, context, middlewares); 733 | 734 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 735 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 736 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 737 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 738 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledTimes(1); 739 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledWith(event, context); 740 | expect(middlewares.someFunc2.handler).not.toHaveBeenCalled(); 741 | 742 | // Commented because jest doesn't clear invocationCallOrder 743 | // shouldHaveBeenCalledInOrder([ 744 | // middlewares.preHandler1.handler, 745 | // middlewares.preHandler2.handler, 746 | // middlewares.catchPreHandler1.handler, 747 | // middlewares.someFunc2.handler, 748 | // ]); 749 | }); 750 | }); 751 | 752 | describe('with pos-handlers', () => { 753 | it('should process standard handlers and array middlewares and add the global pos-handlers', async () => { 754 | const serverless = getServerlessConfig({ 755 | service: { 756 | custom: { 757 | middleware: { 758 | pos: ['posHandler1.handler', 'posHandler2.handler'], 759 | }, 760 | }, 761 | functions: { 762 | someFunc1: { 763 | name: 'someFunc1', 764 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 765 | }, 766 | someFunc2: { 767 | name: 'someFunc2', 768 | handler: 'someFunc2.handler', 769 | }, 770 | }, 771 | }, 772 | }); 773 | const pluginUtils = getPluginUtils(); 774 | 775 | const plugin = new Middleware(serverless, {}, pluginUtils); 776 | 777 | await plugin.hooks[hook](); 778 | 779 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 780 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('.middleware/someFunc2.handler'); 781 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 782 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 783 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(2); 784 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 785 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(2, path.join('testPath', '.middleware', `someFunc2.${extension}`), expect.any(String)); 786 | 787 | const event = {}; 788 | const context = {}; 789 | const middlewares = { 790 | posHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 791 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 792 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 793 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 794 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 795 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 796 | }; 797 | 798 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 799 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 800 | 801 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 802 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 803 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 804 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 805 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 806 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 807 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 808 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 809 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 810 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 811 | 812 | shouldHaveBeenCalledInOrder([ 813 | middlewares.middleware1.handler, 814 | middlewares.middleware2.handler, 815 | middlewares.someFunc1.handler, 816 | middlewares.posHandler1.handler, 817 | middlewares.posHandler2.handler, 818 | ]); 819 | 820 | middlewares.posHandler1.handler.mockClear(); 821 | middlewares.posHandler2.handler.mockClear(); 822 | 823 | const someFunc2Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[1][1]); 824 | await someFunc2Tester.executeMiddlewareFunction(event, context, middlewares); 825 | 826 | expect(middlewares.someFunc2.handler).toHaveBeenCalledTimes(1); 827 | expect(middlewares.someFunc2.handler).toHaveBeenCalledWith(event, context); 828 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 829 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 830 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 831 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 832 | 833 | // Commented because jest doesn't clear invocationCallOrder 834 | // shouldHaveBeenCalledInOrder([ 835 | // middlewares.someFunc2.handler, 836 | // middlewares.posHandler1.handler, 837 | // middlewares.posHandler2.handler, 838 | // ]); 839 | }); 840 | 841 | it('should process middlewares and add pos-handlers with catch blocks', async () => { 842 | const serverless = getServerlessConfig({ 843 | service: { 844 | custom: { 845 | middleware: { 846 | pos: ['posHandler1.handler', { then: 'posHandler2.handler', catch: 'catchPosHandler1.handler' }], 847 | }, 848 | }, 849 | functions: { 850 | someFunc1: { 851 | name: 'someFunc1', 852 | middleware: [ 853 | { then: 'middleware1.handler' }, 854 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 855 | 'middleware3.handler', 856 | { catch: 'catchMiddleware2.handler' }, 857 | 'someFunc1.handler', 858 | ], 859 | }, 860 | someFunc2: { 861 | name: 'someFunc2', 862 | handler: 'someFunc2.handler', 863 | }, 864 | }, 865 | }, 866 | }); 867 | const pluginUtils = getPluginUtils(); 868 | 869 | const plugin = new Middleware(serverless, {}, pluginUtils); 870 | 871 | await plugin.hooks[hook](); 872 | 873 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 874 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('.middleware/someFunc2.handler'); 875 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 876 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 877 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(2); 878 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 879 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(2, path.join('testPath', '.middleware', `someFunc2.${extension}`), expect.any(String)); 880 | 881 | const event = {}; 882 | const context = {}; 883 | const err1 = new Error('Error 1.'); 884 | const err2 = new Error('Error 2.'); 885 | const middlewares = { 886 | posHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 887 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 888 | catchPosHandler1: { 889 | handler: jest.fn().mockImplementation(() => { 890 | expect(context.prev).toEqual(err1); 891 | return Promise.resolve(); 892 | }), 893 | }, 894 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 895 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 896 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 897 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 898 | catchMiddleware2: { 899 | handler: jest.fn().mockImplementation(() => { 900 | expect(context.prev).toEqual(err2); 901 | return Promise.resolve(); 902 | }), 903 | }, 904 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 905 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 906 | }; 907 | 908 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 909 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 910 | 911 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 912 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 913 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 914 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 915 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 916 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 917 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 918 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 919 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 920 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 921 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 922 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 923 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 924 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 925 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 926 | expect(middlewares.catchPosHandler1.handler).toHaveBeenCalledTimes(1); 927 | expect(middlewares.catchPosHandler1.handler).toHaveBeenCalledWith(event, context); 928 | 929 | shouldHaveBeenCalledInOrder([ 930 | middlewares.middleware1.handler, 931 | middlewares.middleware2.handler, 932 | middlewares.middleware3.handler, 933 | middlewares.catchMiddleware2.handler, 934 | middlewares.someFunc1.handler, 935 | middlewares.posHandler1.handler, 936 | middlewares.posHandler2.handler, 937 | ]); 938 | 939 | middlewares.posHandler1.handler.mockClear(); 940 | middlewares.posHandler2.handler.mockClear(); 941 | middlewares.catchPosHandler1.handler.mockClear(); 942 | const someFunc2Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[1][1]); 943 | await someFunc2Tester.executeMiddlewareFunction(event, context, middlewares); 944 | 945 | expect(middlewares.someFunc2.handler).toHaveBeenCalledTimes(1); 946 | expect(middlewares.someFunc2.handler).toHaveBeenCalledWith(event, context); 947 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 948 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 949 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 950 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 951 | expect(middlewares.catchPosHandler1.handler).toHaveBeenCalledTimes(1); 952 | expect(middlewares.catchPosHandler1.handler).toHaveBeenCalledWith(event, context); 953 | 954 | // Commented because jest doesn't clear invocationCallOrder 955 | // shouldHaveBeenCalledInOrder([ 956 | // middlewares.someFunc1.handler, 957 | // middlewares.posHandler1.handler, 958 | // middlewares.posHandler2.handler, 959 | // middlewares.catchPosHandler1.handler, 960 | // ]); 961 | }); 962 | 963 | it('should process standard handlers and array middlewares and add the function-specific pos-handlers', async () => { 964 | const serverless = getServerlessConfig({ 965 | service: { 966 | functions: { 967 | someFunc1: { 968 | name: 'someFunc1', 969 | handler: 'someFunc1.handler', 970 | middleware: { 971 | pos: ['posHandler1.handler', 'posHandler2.handler'], 972 | }, 973 | }, 974 | }, 975 | }, 976 | }); 977 | const pluginUtils = getPluginUtils(); 978 | 979 | const plugin = new Middleware(serverless, {}, pluginUtils); 980 | 981 | await plugin.hooks[hook](); 982 | 983 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 984 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 985 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 986 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 987 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 988 | 989 | const event = {}; 990 | const context = {}; 991 | const middlewares = { 992 | posHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 993 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 994 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 995 | }; 996 | 997 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 998 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 999 | 1000 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 1001 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 1002 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 1003 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 1004 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 1005 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 1006 | 1007 | shouldHaveBeenCalledInOrder([ 1008 | middlewares.someFunc1.handler, 1009 | middlewares.posHandler1.handler, 1010 | middlewares.posHandler2.handler, 1011 | ]); 1012 | 1013 | middlewares.posHandler1.handler.mockClear(); 1014 | middlewares.posHandler2.handler.mockClear(); 1015 | 1016 | // Commented because jest doesn't clear invocationCallOrder 1017 | // shouldHaveBeenCalledInOrder([ 1018 | // middlewares.preHandler1.handler, 1019 | // middlewares.preHandler2.handler, 1020 | // middlewares.middleware1.handler, 1021 | // middlewares.middleware2.handler, 1022 | // middlewares.someFunc2.handler, 1023 | // ]); 1024 | }); 1025 | 1026 | it('should end process if context.end is called', async () => { 1027 | const serverless = getServerlessConfig({ 1028 | service: { 1029 | custom: { 1030 | middleware: { 1031 | pos: ['posHandler1.handler', { then: 'posHandler2.handler', catch: 'catchPosHandler1.handler' }], 1032 | }, 1033 | }, 1034 | functions: { 1035 | someFunc1: { 1036 | name: 'someFunc1', 1037 | middleware: [ 1038 | { then: 'middleware1.handler' }, 1039 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 1040 | 'middleware3.handler', 1041 | { catch: 'catchMiddleware2.handler' }, 1042 | 'someFunc1.handler', 1043 | ], 1044 | }, 1045 | someFunc2: { 1046 | name: 'someFunc2', 1047 | handler: 'someFunc2.handler', 1048 | }, 1049 | }, 1050 | }, 1051 | }); 1052 | const pluginUtils = getPluginUtils(); 1053 | 1054 | const plugin = new Middleware(serverless, {}, pluginUtils); 1055 | 1056 | await plugin.hooks[hook](); 1057 | 1058 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 1059 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('.middleware/someFunc2.handler'); 1060 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 1061 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 1062 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(2); 1063 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 1064 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(2, path.join('testPath', '.middleware', `someFunc2.${extension}`), expect.any(String)); 1065 | 1066 | const event = {}; 1067 | const context = {}; 1068 | const err1 = new Error('Error 1.'); 1069 | const err2 = new Error('Error 2.'); 1070 | const middlewares = { 1071 | posHandler1: { 1072 | handler: jest.fn().mockImplementation((_, ctx) => { 1073 | ctx.end(); 1074 | return Promise.resolve(); 1075 | }), 1076 | }, 1077 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 1078 | catchPosHandler1: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 1079 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 1080 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 1081 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 1082 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 1083 | catchMiddleware2: { 1084 | handler: jest.fn().mockImplementation(() => { 1085 | expect(context.prev).toEqual(err2); 1086 | return Promise.resolve(); 1087 | }), 1088 | }, 1089 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 1090 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 1091 | }; 1092 | 1093 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 1094 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 1095 | 1096 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 1097 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 1098 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 1099 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 1100 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 1101 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 1102 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 1103 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 1104 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 1105 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 1106 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 1107 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 1108 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 1109 | expect(middlewares.posHandler2.handler).not.toHaveBeenCalled(); 1110 | expect(middlewares.catchPosHandler1.handler).not.toHaveBeenCalled(); 1111 | 1112 | shouldHaveBeenCalledInOrder([ 1113 | middlewares.middleware1.handler, 1114 | middlewares.middleware2.handler, 1115 | middlewares.middleware3.handler, 1116 | middlewares.catchMiddleware2.handler, 1117 | middlewares.someFunc1.handler, 1118 | middlewares.posHandler1.handler, 1119 | ]); 1120 | 1121 | middlewares.posHandler1.handler.mockClear(); 1122 | middlewares.posHandler2.handler.mockClear(); 1123 | middlewares.catchPosHandler1.handler.mockClear(); 1124 | const someFunc2Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[1][1]); 1125 | await someFunc2Tester.executeMiddlewareFunction(event, context, middlewares); 1126 | 1127 | expect(middlewares.someFunc2.handler).toHaveBeenCalledTimes(1); 1128 | expect(middlewares.someFunc2.handler).toHaveBeenCalledWith(event, context); 1129 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 1130 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 1131 | expect(middlewares.posHandler2.handler).not.toHaveBeenCalled(); 1132 | expect(middlewares.catchPosHandler1.handler).not.toHaveBeenCalled(); 1133 | 1134 | // Commented because jest doesn't clear invocationCallOrder 1135 | // shouldHaveBeenCalledInOrder([ 1136 | // middlewares.someFunc2.handler, 1137 | // middlewares.posHandler1.handler, 1138 | // ]); 1139 | }); 1140 | }); 1141 | }); 1142 | }); 1143 | -------------------------------------------------------------------------------- /test/processSingleFunctionHooks.node.test.js: -------------------------------------------------------------------------------- 1 | /* global jest beforeEach describe it expect */ 2 | 3 | jest.mock('fs', () => ({ 4 | existsSync: jest.fn(), 5 | promises: { 6 | mkdir: jest.fn(), 7 | writeFile: jest.fn(), 8 | rm: jest.fn(), 9 | }, 10 | })); 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | 14 | const fsAsync = fs.promises; 15 | const Middleware = require('../src/index'); 16 | const { getServerlessConfig, getPluginUtils } = require('./utils/configUtils'); 17 | const { GeneratedFunctionTester } = require('./utils/generatedFunctionTester'); 18 | const { shouldHaveBeenCalledInOrder } = require('./utils/jest'); 19 | 20 | fsAsync.mkdir.mockReturnValue(Promise.resolve()); 21 | fsAsync.writeFile.mockReturnValue(Promise.resolve()); 22 | 23 | describe.each(['before:deploy:function:packageFunction', 'before:invoke:local:invoke'])('Serverless middleware %s hook', (hook) => { 24 | beforeEach(() => { 25 | fsAsync.mkdir.mockClear(); 26 | fsAsync.writeFile.mockClear(); 27 | }); 28 | 29 | describe('error cases', () => { 30 | beforeEach(() => fs.existsSync.mockImplementation((filePath) => filePath.endsWith('.js'))); 31 | 32 | it('should error on unsupported runtimes', async () => { 33 | const serverless = getServerlessConfig({ 34 | service: { 35 | provider: { stage: '', region: '', runtime: 'dotnet' }, 36 | functions: { 37 | someFunc1: { 38 | name: 'someFunc1', 39 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 40 | }, 41 | someFunc2: { 42 | name: 'someFunc2', 43 | handler: 'someFunc2.handler', 44 | }, 45 | }, 46 | }, 47 | }); 48 | const pluginUtils = getPluginUtils(); 49 | 50 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 51 | 52 | await expect(plugin.hooks[hook]()).rejects.toThrow('Serverless Middleware doesn\'t support the "dotnet" runtime'); 53 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 54 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 55 | }); 56 | 57 | it('should error on unsupported node extensions', async () => { 58 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 59 | const serverless = getServerlessConfig({ 60 | service: { 61 | functions: { 62 | someFunc1: { 63 | name: 'someFunc1', 64 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 65 | }, 66 | someFunc2: { 67 | name: 'someFunc2', 68 | handler: 'someFunc2.handler', 69 | }, 70 | }, 71 | }, 72 | }); 73 | const pluginUtils = getPluginUtils(); 74 | 75 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 76 | 77 | await expect(plugin.hooks[hook]()).rejects.toThrow('Unsupported handler extension for module middleware1. Only .js, .jsx, .ts and .tsx are supported.'); 78 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 79 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 80 | }); 81 | 82 | it('should error on invalid handler', async () => { 83 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 84 | const serverless = getServerlessConfig({ 85 | service: { 86 | functions: { 87 | someFunc1: { 88 | name: 'someFunc1', 89 | middleware: [{ wrong_field: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 90 | }, 91 | someFunc2: { 92 | name: 'someFunc2', 93 | handler: 'someFunc2.handler', 94 | }, 95 | }, 96 | }, 97 | }); 98 | const pluginUtils = getPluginUtils(); 99 | 100 | const plugin = new Middleware(serverless, { function: 'unknownFunction' }, pluginUtils); 101 | 102 | await expect(plugin.hooks[hook]()).rejects.toThrow('Unknown function: unknownFunction'); 103 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 104 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 105 | }); 106 | 107 | it('should error on unknown function option', async () => { 108 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 109 | const serverless = getServerlessConfig({ 110 | service: { 111 | functions: { 112 | someFunc1: { 113 | name: 'someFunc1', 114 | middleware: [{ wrong_field: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 115 | }, 116 | someFunc2: { 117 | name: 'someFunc2', 118 | handler: 'someFunc2.handler', 119 | }, 120 | }, 121 | }, 122 | }); 123 | const pluginUtils = getPluginUtils(); 124 | 125 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 126 | 127 | await expect(plugin.hooks[hook]()).rejects.toThrow('Invalid handler: {"wrong_field":"middleware1.handler"}'); 128 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 129 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 130 | }); 131 | 132 | it('should error on function mixing handler and array middlewares', async () => { 133 | fs.existsSync.mockImplementation((filePath) => !filePath.startsWith('middleware1')); 134 | const serverless = getServerlessConfig({ 135 | service: { 136 | functions: { 137 | someFunc1: { 138 | name: 'someFunc1', 139 | handler: 'someFunc1.handler', 140 | middleware: ['middleware1.handler', 'middleware2.handler'], 141 | }, 142 | someFunc2: { 143 | name: 'someFunc2', 144 | handler: 'someFunc2.handler', 145 | }, 146 | }, 147 | }, 148 | }); 149 | const pluginUtils = getPluginUtils(); 150 | 151 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 152 | 153 | await expect(plugin.hooks[hook]()).rejects.toThrow('Error in function someFunc1. When defining a handler, only the { pre: ..., pos: ...} configuration is allowed.'); 154 | expect(fsAsync.mkdir).not.toHaveBeenCalled(); 155 | expect(fsAsync.writeFile).not.toHaveBeenCalled(); 156 | }); 157 | }); 158 | 159 | describe.each([ 160 | ['js', GeneratedFunctionTester.fromJavaScript], 161 | ['ts', GeneratedFunctionTester.fromTypeScript], 162 | ])('Node.js extension: %s', (extension, functionTesterFrom) => { 163 | beforeEach(() => fs.existsSync.mockImplementation((filePath) => filePath.endsWith(`.${extension}`))); 164 | 165 | describe('without pre/pos', () => { 166 | it('should process handlers that contain arrays and do nothing with standard handlers', async () => { 167 | const serverless = getServerlessConfig({ 168 | service: { 169 | functions: { 170 | someFunc1: { 171 | name: 'someFunc1', 172 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 173 | }, 174 | someFunc2: { 175 | name: 'someFunc2', 176 | handler: 'someFunc2.handler', 177 | }, 178 | }, 179 | }, 180 | }); 181 | const pluginUtils = getPluginUtils(); 182 | 183 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 184 | 185 | await plugin.hooks[hook](); 186 | 187 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 188 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('someFunc2.handler'); 189 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 190 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 191 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 192 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 193 | 194 | const event = {}; 195 | const context = {}; 196 | const middlewares = { 197 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 198 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 199 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 200 | }; 201 | 202 | const functionTester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 203 | await functionTester.executeMiddlewareFunction(event, context, middlewares); 204 | 205 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 206 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 207 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 208 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 209 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 210 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 211 | 212 | shouldHaveBeenCalledInOrder([ 213 | middlewares.middleware1.handler, 214 | middlewares.middleware2.handler, 215 | middlewares.someFunc1.handler, 216 | ]); 217 | }); 218 | 219 | it('should process handler with catch blocks', async () => { 220 | const serverless = getServerlessConfig({ 221 | service: { 222 | functions: { 223 | someFunc1: { 224 | name: 'someFunc1', 225 | middleware: [ 226 | { then: 'middleware1.handler' }, 227 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 228 | 'middleware3.handler', 229 | { catch: 'catchMiddleware2.handler' }, 230 | 'someFunc1.handler', 231 | ], 232 | }, 233 | someFunc2: { 234 | name: 'someFunc2', 235 | handler: 'someFunc2.handler', 236 | }, 237 | }, 238 | }, 239 | }); 240 | const pluginUtils = getPluginUtils(); 241 | 242 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 243 | 244 | await plugin.hooks[hook](); 245 | 246 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 247 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('someFunc2.handler'); 248 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 249 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 250 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 251 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 252 | 253 | const event = {}; 254 | const context = {}; 255 | const err = new Error('Error.'); 256 | const middlewares = { 257 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 258 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 259 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err)) }, 260 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 261 | catchMiddleware2: { 262 | handler: jest.fn().mockImplementation(() => { 263 | expect(context.prev).toEqual(err); 264 | return Promise.resolve(); 265 | }), 266 | }, 267 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 268 | }; 269 | 270 | const functionTester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 271 | await functionTester.executeMiddlewareFunction(event, context, middlewares); 272 | 273 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 274 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 275 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 276 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 277 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 278 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 279 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 280 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 281 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 282 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 283 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 284 | 285 | shouldHaveBeenCalledInOrder([ 286 | middlewares.middleware1.handler, 287 | middlewares.middleware2.handler, 288 | middlewares.middleware3.handler, 289 | middlewares.catchMiddleware2.handler, 290 | middlewares.someFunc1.handler, 291 | ]); 292 | }); 293 | 294 | it('should end process if context.end is called', async () => { 295 | const serverless = getServerlessConfig({ 296 | service: { 297 | functions: { 298 | someFunc1: { 299 | name: 'someFunc1', 300 | middleware: [ 301 | { then: 'middleware1.handler' }, 302 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 303 | 'middleware3.handler', 304 | { catch: 'catchMiddleware2.handler' }, 305 | 'someFunc1.handler', 306 | ], 307 | }, 308 | someFunc2: { 309 | name: 'someFunc2', 310 | handler: 'someFunc2.handler', 311 | }, 312 | }, 313 | }, 314 | }); 315 | const pluginUtils = getPluginUtils(); 316 | 317 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 318 | 319 | await plugin.hooks[hook](); 320 | 321 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 322 | expect(plugin.serverless.service.functions.someFunc2.handler).toEqual('someFunc2.handler'); 323 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 324 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 325 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 326 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 327 | 328 | const event = {}; 329 | const context = {}; 330 | const err = new Error('Error.'); 331 | const middlewares = { 332 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 333 | middleware2: { 334 | handler: jest.fn().mockImplementation((_, ctx) => { 335 | ctx.end(); 336 | return Promise.resolve(); 337 | }), 338 | }, 339 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err)) }, 340 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 341 | catchMiddleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 342 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 343 | }; 344 | 345 | const functionTester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 346 | await functionTester.executeMiddlewareFunction(event, context, middlewares); 347 | 348 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 349 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 350 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 351 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 352 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 353 | expect(middlewares.middleware3.handler).not.toHaveBeenCalled(); 354 | expect(middlewares.catchMiddleware2.handler).not.toHaveBeenCalled(); 355 | expect(middlewares.someFunc1.handler).not.toHaveBeenCalled(); 356 | 357 | shouldHaveBeenCalledInOrder([ 358 | middlewares.middleware1.handler, 359 | middlewares.middleware2.handler, 360 | ]); 361 | }); 362 | }); 363 | 364 | describe('with pre-handlers', () => { 365 | it('should process standard handlers and array middlewares and add the global pre-handlers', async () => { 366 | const serverless = getServerlessConfig({ 367 | service: { 368 | custom: { 369 | middleware: { 370 | pre: ['preHandler1.handler', 'preHandler2.handler'], 371 | }, 372 | }, 373 | functions: { 374 | someFunc1: { 375 | name: 'someFunc1', 376 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 377 | }, 378 | someFunc2: { 379 | name: 'someFunc2', 380 | handler: 'someFunc2.handler', 381 | }, 382 | }, 383 | }, 384 | }); 385 | const pluginUtils = getPluginUtils(); 386 | 387 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 388 | 389 | await plugin.hooks[hook](); 390 | 391 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 392 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 393 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 394 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 395 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 396 | 397 | const event = {}; 398 | const context = {}; 399 | const middlewares = { 400 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 401 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 402 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 403 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 404 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 405 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 406 | }; 407 | 408 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 409 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 410 | 411 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 412 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 413 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 414 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 415 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 416 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 417 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 418 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 419 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 420 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 421 | 422 | shouldHaveBeenCalledInOrder([ 423 | middlewares.preHandler1.handler, 424 | middlewares.preHandler2.handler, 425 | middlewares.middleware1.handler, 426 | middlewares.middleware2.handler, 427 | middlewares.someFunc1.handler, 428 | ]); 429 | 430 | // Commented because jest doesn't clear invocationCallOrder 431 | // shouldHaveBeenCalledInOrder([ 432 | // middlewares.preHandler1.handler, 433 | // middlewares.preHandler2.handler, 434 | // middlewares.middleware1.handler, 435 | // middlewares.middleware2.handler, 436 | // middlewares.someFunc2.handler, 437 | // ]); 438 | }); 439 | 440 | it('should process middlewares and add pre-handlers with catch blocks', async () => { 441 | const serverless = getServerlessConfig({ 442 | service: { 443 | custom: { 444 | middleware: { 445 | pre: ['preHandler1.handler', { then: 'preHandler2.handler', catch: 'catchPreHandler1.handler' }], 446 | }, 447 | }, 448 | functions: { 449 | someFunc1: { 450 | name: 'someFunc1', 451 | middleware: [ 452 | { then: 'middleware1.handler' }, 453 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 454 | 'middleware3.handler', 455 | { catch: 'catchMiddleware2.handler' }, 456 | 'someFunc1.handler', 457 | ], 458 | }, 459 | someFunc2: { 460 | name: 'someFunc2', 461 | handler: 'someFunc2.handler', 462 | }, 463 | }, 464 | }, 465 | }); 466 | const pluginUtils = getPluginUtils(); 467 | 468 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 469 | 470 | await plugin.hooks[hook](); 471 | 472 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 473 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 474 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 475 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 476 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 477 | 478 | const event = {}; 479 | const context = {}; 480 | const err1 = new Error('Error 1.'); 481 | const err2 = new Error('Error 2.'); 482 | const middlewares = { 483 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 484 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 485 | catchPreHandler1: { 486 | handler: jest.fn().mockImplementation(() => { 487 | expect(context.prev).toEqual(err1); 488 | return Promise.resolve(); 489 | }), 490 | }, 491 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 492 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 493 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 494 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 495 | catchMiddleware2: { 496 | handler: jest.fn().mockImplementation(() => { 497 | expect(context.prev).toEqual(err2); 498 | return Promise.resolve(); 499 | }), 500 | }, 501 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 502 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 503 | }; 504 | 505 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 506 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 507 | 508 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 509 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 510 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 511 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 512 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledTimes(1); 513 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledWith(event, context); 514 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 515 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 516 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 517 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 518 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 519 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 520 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 521 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 522 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 523 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 524 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 525 | 526 | shouldHaveBeenCalledInOrder([ 527 | middlewares.preHandler1.handler, 528 | middlewares.preHandler2.handler, 529 | middlewares.catchPreHandler1.handler, 530 | middlewares.middleware1.handler, 531 | middlewares.middleware2.handler, 532 | middlewares.middleware3.handler, 533 | middlewares.catchMiddleware2.handler, 534 | middlewares.someFunc1.handler, 535 | ]); 536 | }); 537 | 538 | it('should process standard handlers and array middlewares and add the function-specific pre-handlers', async () => { 539 | const serverless = getServerlessConfig({ 540 | service: { 541 | functions: { 542 | someFunc1: { 543 | name: 'someFunc1', 544 | handler: 'someFunc1.handler', 545 | middleware: { 546 | pre: ['preHandler1.handler', 'preHandler2.handler'], 547 | }, 548 | }, 549 | }, 550 | }, 551 | }); 552 | const pluginUtils = getPluginUtils(); 553 | 554 | const plugin = new Middleware(serverless, {}, pluginUtils); 555 | 556 | await plugin.hooks[hook](); 557 | 558 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 559 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 560 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 561 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 562 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 563 | 564 | const event = {}; 565 | const context = {}; 566 | const middlewares = { 567 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 568 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 569 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 570 | }; 571 | 572 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 573 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 574 | 575 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 576 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 577 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 578 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 579 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 580 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 581 | 582 | shouldHaveBeenCalledInOrder([ 583 | middlewares.preHandler1.handler, 584 | middlewares.preHandler2.handler, 585 | middlewares.someFunc1.handler, 586 | ]); 587 | 588 | middlewares.preHandler1.handler.mockClear(); 589 | middlewares.preHandler2.handler.mockClear(); 590 | 591 | // Commented because jest doesn't clear invocationCallOrder 592 | // shouldHaveBeenCalledInOrder([ 593 | // middlewares.preHandler1.handler, 594 | // middlewares.preHandler2.handler, 595 | // middlewares.middleware1.handler, 596 | // middlewares.middleware2.handler, 597 | // middlewares.someFunc2.handler, 598 | // ]); 599 | }); 600 | 601 | it('should end process if context.end is called', async () => { 602 | const serverless = getServerlessConfig({ 603 | service: { 604 | custom: { 605 | middleware: { 606 | pre: ['preHandler1.handler', { then: 'preHandler2.handler', catch: 'catchPreHandler1.handler' }], 607 | }, 608 | }, 609 | functions: { 610 | someFunc1: { 611 | name: 'someFunc1', 612 | middleware: [ 613 | { then: 'middleware1.handler' }, 614 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 615 | 'middleware3.handler', 616 | { catch: 'catchMiddleware2.handler' }, 617 | 'someFunc1.handler', 618 | ], 619 | }, 620 | someFunc2: { 621 | name: 'someFunc2', 622 | handler: 'someFunc2.handler', 623 | }, 624 | }, 625 | }, 626 | }); 627 | const pluginUtils = getPluginUtils(); 628 | 629 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 630 | 631 | await plugin.hooks[hook](); 632 | 633 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 634 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 635 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 636 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 637 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 638 | 639 | const event = {}; 640 | const context = {}; 641 | const err1 = new Error('Error 1.'); 642 | const err2 = new Error('Error 2.'); 643 | const middlewares = { 644 | preHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 645 | preHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 646 | catchPreHandler1: { 647 | handler: jest.fn().mockImplementation((_, ctx) => { 648 | expect(ctx.prev).toEqual(err1); 649 | ctx.end(); 650 | return Promise.resolve(); 651 | }), 652 | }, 653 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 654 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 655 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 656 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 657 | catchMiddleware2: { 658 | handler: jest.fn().mockImplementation(() => { 659 | expect(context.prev).toEqual(err2); 660 | return Promise.resolve(); 661 | }), 662 | }, 663 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 664 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 665 | }; 666 | 667 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 668 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 669 | 670 | expect(middlewares.preHandler1.handler).toHaveBeenCalledTimes(1); 671 | expect(middlewares.preHandler1.handler).toHaveBeenCalledWith(event, context); 672 | expect(middlewares.preHandler2.handler).toHaveBeenCalledTimes(1); 673 | expect(middlewares.preHandler2.handler).toHaveBeenCalledWith(event, context); 674 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledTimes(1); 675 | expect(middlewares.catchPreHandler1.handler).toHaveBeenCalledWith(event, context); 676 | expect(middlewares.middleware1.handler).not.toHaveBeenCalled(); 677 | expect(middlewares.middleware2.handler).not.toHaveBeenCalled(); 678 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 679 | expect(middlewares.middleware3.handler).not.toHaveBeenCalled(); 680 | expect(middlewares.catchMiddleware2.handler).not.toHaveBeenCalled(); 681 | expect(middlewares.someFunc1.handler).not.toHaveBeenCalled(); 682 | 683 | shouldHaveBeenCalledInOrder([ 684 | middlewares.preHandler1.handler, 685 | middlewares.preHandler2.handler, 686 | middlewares.catchPreHandler1.handler, 687 | ]); 688 | }); 689 | }); 690 | 691 | describe('with pos-handlers', () => { 692 | it('should process standard handlers and array middlewares and add the global pos-handlers', async () => { 693 | const serverless = getServerlessConfig({ 694 | service: { 695 | custom: { 696 | middleware: { 697 | pos: ['posHandler1.handler', 'posHandler2.handler'], 698 | }, 699 | }, 700 | functions: { 701 | someFunc1: { 702 | name: 'someFunc1', 703 | middleware: [{ then: 'middleware1.handler' }, 'middleware2.handler', 'someFunc1.handler'], 704 | }, 705 | someFunc2: { 706 | name: 'someFunc2', 707 | handler: 'someFunc2.handler', 708 | }, 709 | }, 710 | }, 711 | }); 712 | const pluginUtils = getPluginUtils(); 713 | 714 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 715 | 716 | await plugin.hooks[hook](); 717 | 718 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 719 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 720 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 721 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 722 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 723 | 724 | const event = {}; 725 | const context = {}; 726 | const middlewares = { 727 | posHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 728 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 729 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 730 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 731 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 732 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 733 | }; 734 | 735 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 736 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 737 | 738 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 739 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 740 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 741 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 742 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 743 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 744 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 745 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 746 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 747 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 748 | 749 | shouldHaveBeenCalledInOrder([ 750 | middlewares.middleware1.handler, 751 | middlewares.middleware2.handler, 752 | middlewares.someFunc1.handler, 753 | middlewares.posHandler1.handler, 754 | middlewares.posHandler2.handler, 755 | ]); 756 | }); 757 | 758 | it('should process middlewares and add pos-handlers with catch blocks', async () => { 759 | const serverless = getServerlessConfig({ 760 | service: { 761 | custom: { 762 | middleware: { 763 | pos: ['posHandler1.handler', { then: 'posHandler2.handler', catch: 'catchPosHandler1.handler' }], 764 | }, 765 | }, 766 | functions: { 767 | someFunc1: { 768 | name: 'someFunc1', 769 | middleware: [ 770 | { then: 'middleware1.handler' }, 771 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 772 | 'middleware3.handler', 773 | { catch: 'catchMiddleware2.handler' }, 774 | 'someFunc1.handler', 775 | ], 776 | }, 777 | someFunc2: { 778 | name: 'someFunc2', 779 | handler: 'someFunc2.handler', 780 | }, 781 | }, 782 | }, 783 | }); 784 | const pluginUtils = getPluginUtils(); 785 | 786 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 787 | 788 | await plugin.hooks[hook](); 789 | 790 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 791 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 792 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 793 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 794 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 795 | 796 | const event = {}; 797 | const context = {}; 798 | const err1 = new Error('Error 1.'); 799 | const err2 = new Error('Error 2.'); 800 | const middlewares = { 801 | posHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 802 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 803 | catchPosHandler1: { 804 | handler: jest.fn().mockImplementation(() => { 805 | expect(context.prev).toEqual(err1); 806 | return Promise.resolve(); 807 | }), 808 | }, 809 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 810 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 811 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 812 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 813 | catchMiddleware2: { 814 | handler: jest.fn().mockImplementation(() => { 815 | expect(context.prev).toEqual(err2); 816 | return Promise.resolve(); 817 | }), 818 | }, 819 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 820 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 821 | }; 822 | 823 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 824 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 825 | 826 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 827 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 828 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 829 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 830 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 831 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 832 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 833 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 834 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 835 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 836 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 837 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 838 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 839 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 840 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 841 | expect(middlewares.catchPosHandler1.handler).toHaveBeenCalledTimes(1); 842 | expect(middlewares.catchPosHandler1.handler).toHaveBeenCalledWith(event, context); 843 | 844 | shouldHaveBeenCalledInOrder([ 845 | middlewares.middleware1.handler, 846 | middlewares.middleware2.handler, 847 | middlewares.middleware3.handler, 848 | middlewares.catchMiddleware2.handler, 849 | middlewares.someFunc1.handler, 850 | middlewares.posHandler1.handler, 851 | middlewares.posHandler2.handler, 852 | ]); 853 | }); 854 | 855 | it('should process standard handlers and array middlewares and add the function-specific pos-handlers', async () => { 856 | const serverless = getServerlessConfig({ 857 | service: { 858 | functions: { 859 | someFunc1: { 860 | name: 'someFunc1', 861 | handler: 'someFunc1.handler', 862 | middleware: { 863 | pos: ['posHandler1.handler', 'posHandler2.handler'], 864 | }, 865 | }, 866 | }, 867 | }, 868 | }); 869 | const pluginUtils = getPluginUtils(); 870 | 871 | const plugin = new Middleware(serverless, {}, pluginUtils); 872 | 873 | await plugin.hooks[hook](); 874 | 875 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 876 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 877 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 878 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 879 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 880 | 881 | const event = {}; 882 | const context = {}; 883 | const middlewares = { 884 | posHandler1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 885 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 886 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 887 | }; 888 | 889 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 890 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 891 | 892 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 893 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 894 | expect(middlewares.posHandler2.handler).toHaveBeenCalledTimes(1); 895 | expect(middlewares.posHandler2.handler).toHaveBeenCalledWith(event, context); 896 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 897 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 898 | 899 | shouldHaveBeenCalledInOrder([ 900 | middlewares.someFunc1.handler, 901 | middlewares.posHandler1.handler, 902 | middlewares.posHandler2.handler, 903 | ]); 904 | 905 | middlewares.posHandler1.handler.mockClear(); 906 | middlewares.posHandler2.handler.mockClear(); 907 | 908 | // Commented because jest doesn't clear invocationCallOrder 909 | // shouldHaveBeenCalledInOrder([ 910 | // middlewares.preHandler1.handler, 911 | // middlewares.preHandler2.handler, 912 | // middlewares.middleware1.handler, 913 | // middlewares.middleware2.handler, 914 | // middlewares.someFunc2.handler, 915 | // ]); 916 | }); 917 | 918 | it('should end process if context.end is called', async () => { 919 | const serverless = getServerlessConfig({ 920 | service: { 921 | custom: { 922 | middleware: { 923 | pos: ['posHandler1.handler', { then: 'posHandler2.handler', catch: 'catchPosHandler1.handler' }], 924 | }, 925 | }, 926 | functions: { 927 | someFunc1: { 928 | name: 'someFunc1', 929 | middleware: [ 930 | { then: 'middleware1.handler' }, 931 | { then: 'middleware2.handler', catch: 'catchMiddleware1.handler' }, 932 | 'middleware3.handler', 933 | { catch: 'catchMiddleware2.handler' }, 934 | 'someFunc1.handler', 935 | ], 936 | }, 937 | someFunc2: { 938 | name: 'someFunc2', 939 | handler: 'someFunc2.handler', 940 | }, 941 | }, 942 | }, 943 | }); 944 | const pluginUtils = getPluginUtils(); 945 | 946 | const plugin = new Middleware(serverless, { function: 'someFunc1' }, pluginUtils); 947 | 948 | await plugin.hooks[hook](); 949 | 950 | expect(plugin.serverless.service.functions.someFunc1.handler).toEqual('.middleware/someFunc1.handler'); 951 | expect(fsAsync.mkdir).toHaveBeenCalledTimes(1); 952 | expect(fsAsync.mkdir).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware'), { recursive: true }); 953 | expect(fsAsync.writeFile).toHaveBeenCalledTimes(1); 954 | expect(fsAsync.writeFile).toHaveBeenNthCalledWith(1, path.join('testPath', '.middleware', `someFunc1.${extension}`), expect.any(String)); 955 | 956 | const event = {}; 957 | const context = {}; 958 | const err1 = new Error('Error 1.'); 959 | const err2 = new Error('Error 2.'); 960 | const middlewares = { 961 | posHandler1: { 962 | handler: jest.fn().mockImplementation((_, ctx) => { 963 | ctx.end(); 964 | return Promise.resolve(); 965 | }), 966 | }, 967 | posHandler2: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 968 | catchPosHandler1: { handler: jest.fn().mockImplementation(() => Promise.reject(err1)) }, 969 | middleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 970 | middleware2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 971 | middleware3: { handler: jest.fn().mockImplementation(() => Promise.reject(err2)) }, 972 | catchMiddleware1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 973 | catchMiddleware2: { 974 | handler: jest.fn().mockImplementation(() => { 975 | expect(context.prev).toEqual(err2); 976 | return Promise.resolve(); 977 | }), 978 | }, 979 | someFunc1: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 980 | someFunc2: { handler: jest.fn().mockImplementation(() => Promise.resolve()) }, 981 | }; 982 | 983 | const someFunc1Tester = functionTesterFrom(fsAsync.writeFile.mock.calls[0][1]); 984 | await someFunc1Tester.executeMiddlewareFunction(event, context, middlewares); 985 | 986 | expect(middlewares.middleware1.handler).toHaveBeenCalledTimes(1); 987 | expect(middlewares.middleware1.handler).toHaveBeenCalledWith(event, context); 988 | expect(middlewares.middleware2.handler).toHaveBeenCalledTimes(1); 989 | expect(middlewares.middleware2.handler).toHaveBeenCalledWith(event, context); 990 | expect(middlewares.catchMiddleware1.handler).not.toHaveBeenCalled(); 991 | expect(middlewares.middleware3.handler).toHaveBeenCalledTimes(1); 992 | expect(middlewares.middleware3.handler).toHaveBeenCalledWith(event, context); 993 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledTimes(1); 994 | expect(middlewares.catchMiddleware2.handler).toHaveBeenCalledWith(event, context); 995 | expect(middlewares.someFunc1.handler).toHaveBeenCalledTimes(1); 996 | expect(middlewares.someFunc1.handler).toHaveBeenCalledWith(event, context); 997 | expect(middlewares.posHandler1.handler).toHaveBeenCalledTimes(1); 998 | expect(middlewares.posHandler1.handler).toHaveBeenCalledWith(event, context); 999 | expect(middlewares.posHandler2.handler).not.toHaveBeenCalled(); 1000 | expect(middlewares.catchPosHandler1.handler).not.toHaveBeenCalled(); 1001 | 1002 | shouldHaveBeenCalledInOrder([ 1003 | middlewares.middleware1.handler, 1004 | middlewares.middleware2.handler, 1005 | middlewares.middleware3.handler, 1006 | middlewares.catchMiddleware2.handler, 1007 | middlewares.someFunc1.handler, 1008 | middlewares.posHandler1.handler, 1009 | ]); 1010 | }); 1011 | }); 1012 | }); 1013 | }); 1014 | -------------------------------------------------------------------------------- /test/utils/configUtils.js: -------------------------------------------------------------------------------- 1 | class FakeServerlessError extends Error {} 2 | 3 | function getServerlessConfig(serverlessOverrides = {}) { 4 | const serverless = { 5 | provider: {}, 6 | config: {}, 7 | service: {}, 8 | ...serverlessOverrides, 9 | }; 10 | 11 | return { 12 | getProvider: serverless.getProvider || (() => {}), 13 | configSchemaHandler: serverless.configSchemaHandler !== undefined 14 | ? serverless.configSchemaHandler 15 | : { 16 | defineCustomProperties() {}, 17 | defineFunctionProperties() {}, 18 | }, 19 | serviceDir: (serverless.serviceDir !== undefined) ? serverless.serviceDir : 'testPath', 20 | config: { 21 | servicePath: serverless.config.servicePath, 22 | }, 23 | service: { 24 | provider: serverless.service.provider || { stage: '', region: '', runtime: 'nodejs22.x' }, 25 | defaults: serverless.service.defaults || { stage: '', region: '' }, 26 | service: 'middleware-test', 27 | custom: serverless.service.custom, 28 | getAllFunctions() { return Object.keys(this.functions); }, 29 | getFunction(name) { return this.functions[name]; }, 30 | functions: serverless.service.functions || {}, 31 | }, 32 | classes: { 33 | Error: FakeServerlessError, 34 | }, 35 | }; 36 | } 37 | 38 | function getPluginUtils(options = {}) { 39 | return { 40 | log: { 41 | error: () => {}, 42 | warning: () => {}, 43 | notice: () => {}, 44 | info: () => {}, 45 | ...options.log, 46 | }, 47 | }; 48 | } 49 | 50 | module.exports = { 51 | getServerlessConfig, 52 | getPluginUtils, 53 | }; 54 | -------------------------------------------------------------------------------- /test/utils/generatedFunctionTester.js: -------------------------------------------------------------------------------- 1 | const { transpileModule, ModuleKind } = require('typescript'); 2 | 3 | class GeneratedFunctionTester { 4 | constructor(fn) { 5 | this.fn = fn; 6 | } 7 | 8 | static fromJavaScript(fn) { 9 | return new GeneratedFunctionTester(fn); 10 | } 11 | 12 | static fromTypeScript(fn) { 13 | return new GeneratedFunctionTester(transpileModule(fn, { 14 | compilerOptions: { module: ModuleKind.CommonJS }, 15 | }).outputText); 16 | } 17 | 18 | get middlewareFunction() { 19 | // eslint-disable-next-line no-new-func 20 | return new Function('event', 'context', 'dependencies', ` 21 | const require = (dependencyName) => { 22 | const dependency = dependencies[dependencyName.replace('../', '').replace(/^(.+).js$/, '$1')]; 23 | if (!dependency) { 24 | throw new Error(\`Unknow dependency (\${dependencyName})\`); 25 | } 26 | 27 | return dependency; 28 | }; 29 | const exports = {}; 30 | const module = { exports }; 31 | ${this.fn} 32 | return module.exports.handler(event, context); 33 | `); 34 | } 35 | 36 | async executeMiddlewareFunction(event, context, dependencies) { 37 | await this.middlewareFunction(event, context, dependencies); 38 | } 39 | } 40 | 41 | module.exports = { GeneratedFunctionTester }; 42 | -------------------------------------------------------------------------------- /test/utils/jest.js: -------------------------------------------------------------------------------- 1 | /* global expect */ 2 | 3 | function shouldHaveBeenCalledInOrder(mocks) { 4 | mocks.reduce((prevInvocation, mockFn) => { 5 | expect(mockFn.mock.invocationCallOrder[0]).toBeGreaterThan(prevInvocation); 6 | return mockFn.mock.invocationCallOrder[0]; 7 | }, -1); 8 | } 9 | 10 | module.exports = { 11 | shouldHaveBeenCalledInOrder, 12 | }; 13 | --------------------------------------------------------------------------------