├── .appveyor.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── any.svg ├── delete.svg ├── function-context.png ├── function-demo.png ├── get.svg ├── post.svg ├── put.svg └── service-demo.png ├── package-lock.json ├── package.json ├── src ├── extension.ts └── lib │ ├── CommandBase.ts │ ├── CommandHandler.ts │ ├── Serverless.ts │ ├── ServerlessNode.ts │ ├── commands │ ├── Deploy.ts │ ├── DeployFunction.ts │ ├── InvokeLocal.ts │ ├── Logs.ts │ ├── OpenHandler.ts │ ├── Package.ts │ └── Resolve.ts │ └── serverlessOutline.ts ├── test ├── index.ts └── lib │ ├── CommandBase.test.ts │ ├── CommandHandler.test.ts │ ├── Serverless.test.ts │ ├── ServerlessNode.test.ts │ ├── TestContext.ts │ ├── commands │ ├── Deploy.test.ts │ ├── DeployFunction.test.ts │ ├── InvokeLocal.test.ts │ ├── Logs.test.ts │ ├── OpenHandler.test.ts │ ├── Package.test.ts │ └── Resolve.test.ts │ └── serverlessOutline.test.ts ├── tsconfig.json └── tslint.json /.appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - NODEJS_VERSION: "4" 4 | - NODEJS_VERSION: "6" 5 | 6 | matrix: 7 | fast_finish: true 8 | 9 | skip_branch_with_pr: true 10 | 11 | install: 12 | - ps: $env:package_version = (Get-Content -Raw -Path package.json | ConvertFrom-Json).version 13 | - ps: Update-AppveyorBuild -Version "$env:package_version-$env:APPVEYOR_BUILD_NUMBER" 14 | # Get the version of Node.js 15 | - ps: Install-Product node $Env:NODEJS_VERSION 16 | # install modules 17 | - npm install 18 | - node --version 19 | - npm --version 20 | 21 | test_script: 22 | - npm run lint 23 | - npm run vscode:prepublish 24 | - npm test 25 | 26 | # Don't actually build. 27 | build: off 28 | 29 | deploy: off 30 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true ; top-most EditorConfig file 2 | 3 | ; Unix-style newlines with a newline ending every file 4 | [*.yml] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*] 11 | charset = utf-8 12 | end_of_line = lf 13 | indent_style = tab 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # This is a (Bug Report / Feature Proposal) 9 | 10 | ## Description 11 | 12 | For bug reports: 13 | * What went wrong? 14 | * What did you expect should have happened? 15 | * What was the config you used? 16 | * What stacktrace or error message from your provider did you see? 17 | 18 | For feature proposals: 19 | * What is the use case that should be solved. The more detail you describe this in the easier it is to understand for us. 20 | * If there is additional config how would it look 21 | 22 | Similar or dependent issue(s): 23 | * #12345 24 | 25 | ## Additional Data 26 | 27 | * **Extension version**: 28 | * **Serverless Framework**: 29 | Version: 30 | - [ ] Serverless installed globally 31 | * ***Operating System***: 32 | * ***Stack Trace (if available)***: 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## What did you implement: 7 | 8 | Closes #XXXXX 9 | 10 | 16 | 17 | ## How did you implement it: 18 | 19 | 22 | 23 | ## How can we verify it: 24 | 25 | 35 | 36 | ## Todos: 37 | 38 | - [ ] Write tests 39 | - [ ] Write documentation 40 | - [ ] Fix linting errors 41 | - [ ] Make sure code coverage hasn't dropped 42 | - [ ] Provide verification config / commands / resources 43 | - [ ] Enable "Allow edits from maintainers" for this PR 44 | - [ ] Update the messages below 45 | 46 | ***Is this ready for review?:*** NO 47 | ***Is it a breaking change?:*** NO 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /node_modules 3 | /.vscode-test 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Launch Extension", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "stopOnEntry": false, 13 | "sourceMaps": true, 14 | "outFiles": [ 15 | "${workspaceFolder}/out/**/*.js" 16 | ], 17 | "preLaunchTask": "compile" 18 | }, 19 | { 20 | "name": "Launch Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "runtimeExecutable": "${execPath}", 24 | "args": [ "--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 25 | "stopOnEntry": false, 26 | "sourceMaps": true, 27 | "outFiles": [ 28 | "${workspaceFolder}/out/**/*.js" 29 | ], 30 | "preLaunchTask": "compile" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true 8 | }, 9 | "editor.tabSize": 4, 10 | "editor.insertSpaces": false 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "compile", 8 | "type": "npm", 9 | "script": "compile", 10 | "isBackground": true, 11 | "problemMatcher": ["$tsc-watch"] 12 | }, 13 | { 14 | "label": "lint", 15 | "type": "npm", 16 | "script": "lint", 17 | "presentation": { 18 | "echo": true, 19 | "reveal": "silent", 20 | "focus": false, 21 | "panel": "shared" 22 | }, 23 | "problemMatcher": { 24 | "owner": "tslint", 25 | "fileLocation": [ 26 | "relative", 27 | "${workspaceRoot}" 28 | ], 29 | "severity": "warning", 30 | "pattern": { 31 | "regexp": "^(ERROR|WARNING): (.*)\\[([1-9]+[0-9]*), ([1-9]+[0-9]*)]: (.*)$", 32 | "severity": 1, 33 | "file": 2, 34 | "line": 3, 35 | "column": 4, 36 | "message": 5 37 | } 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .github/** 3 | typings/** 4 | out/test/** 5 | test/** 6 | **/*.ts 7 | **/*.map 8 | .gitignore 9 | tsconfig.json 10 | .vscode-test/** 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | - Parallel execution of serverless commands should be blocked [#17](https://github.com/HyperBrain/serverless-vscode/issues/17) 3 | 4 | ## 0.0.4 5 | - Support package and deploy [#10](https://github.com/HyperBrain/serverless-vscode/issues/10) 6 | - Support region and default configuration [#11](https://github.com/HyperBrain/serverless-vscode/issues/11) 7 | 8 | ## 0.0.3 9 | - Added API hive 10 | 11 | ## 0.0.2 12 | - Initial alpha release 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Frank Schmid 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 Framework integration for VS Code 2 | 3 | [![Version](https://vsmarketplacebadge.apphb.com/version/frankschmid.serverless-vscode.svg)](https://marketplace.visualstudio.com/items?itemName=frankschmid.serverless-vscode) 4 | [![Installs](https://vsmarketplacebadge.apphb.com/installs/frankschmid.serverless-vscode.svg)](https://marketplace.visualstudio.com/items?itemName=frankschmid.serverless-vscode) 5 | [![Ratings](https://vsmarketplacebadge.apphb.com/rating/frankschmid.serverless-vscode.svg)](https://marketplace.visualstudio.com/items?itemName=frankschmid.serverless-vscode) 6 | 7 | This extension enables an integration of Serverless projects with VSCode. It eliminates the need 8 | to start Serverless commands from a separate command line. 9 | 10 | ## Installation 11 | 12 | In order to install an extension you need to open the extension palette and search for serverless-vscode. 13 | You can then install it. 14 | 15 | **Currently the extension only supports Serverless projects with Serverless installed locally!** 16 | 17 | That means, that Serverless must be a development dependency of the project itself. A subsequent 18 | version of the extension will also support the globally installed Serverless framework and a 19 | configuration for that. 20 | 21 | ## Configuration 22 | 23 | The extension supports user and workspace configuration. To access the configuration settings, 24 | open `File->Preferences->Settings` (workspace or user) and expand the `Serverless Configuration` node. 25 | 26 | The following configuration settings are available: 27 | 28 | ### serverless.aws.askForRegion 29 | 30 | When set to false (the default), the extension will not ask for the region to deploy to but use the 31 | one, set as `serverless.aws.defaultRegion`. This reduces the typing needed to execute a single 32 | command, as normally you'll not deploy cross-region that often. 33 | 34 | ### serverless.aws.defaultStage 35 | 36 | The defult stage that is assumed, if you just press ENTER in the stage input field when executing a command. 37 | 38 | ### serverless.aws.defaultRegion 39 | 40 | The defult region that is assumed, if you just press ENTER in the stage input field when executing a command. See also `serverless.aws.askForRegion`. 41 | 42 | ## Usage 43 | 44 | ### The Serverless outline 45 | 46 | As soon as you have added a Serverless project to your workspace, you can select the `serverless.yml` 47 | in the Explorer tree view. Then an outline is shown in the Explorer view, that shows the parsed 48 | structure of your Serverless service definition. 49 | The outline will contain a `functions` and an `API` hive, which contain the defined functions in the 50 | project and the defined API endpoint hierarchy. Each item in the outline has a context menu that allows 51 | access to context specific commands. Most of the command will ask you for the target stage when triggered. 52 | 53 | #### Top container objects 54 | 55 | Each of the top hives has a context menu that lets you invoke service/project related functions. 56 | 57 | ![Function](images/service-demo.png "Service") 58 | 59 | ##### Package 60 | 61 | Package will ask for the stage and optionally region and packages the service with `serverless package`. 62 | 63 | ##### Deploy 64 | 65 | Package will ask for the stage and optionally region and deploys the service with `serverless deploy`. 66 | 67 | ##### Variable resolution (Resolve) 68 | 69 | Resolve allows you to show a generated `resolved.yml`, i.e. your `serverless.yml` with all Serverless 70 | variables resolved to their values for a selected stage. 71 | 72 | #### Functions 73 | 74 | The functions hive lets you analyze your service function-wise and contains a node for each function. 75 | Each function then contains a list of all defined HTTP endpoints in the function definition. 76 | 77 | ![Function](images/function-demo.png "Function") 78 | 79 | All function related commands of the extension can be called via the context menu of the function. 80 | 81 | ![FunctionContext](images/function-context.png "Function context menu") 82 | 83 | ##### Deploy function 84 | 85 | Deploys the selected function with `serverless deploy function`. Attention: In general, single function 86 | deployment does not replace a service deployment. See the Serverless documentation for details. 87 | 88 | ##### Invoke local 89 | 90 | Invoke the selected function locally. The command lets you select an `event.json` that will be used 91 | for the local invocation. Setting a custom context is not yet possible. 92 | 93 | ##### Show logs 94 | 95 | Retrieve and show the online logs of the deployed function in the output pane. 96 | 97 | ##### Open handler 98 | 99 | Open the handler source file that is associated with the function. 100 | 101 | #### API 102 | 103 | The API hive shows the combined API that will eventually be deployed to API Gateway. 104 | 105 | ## Releases 106 | 107 | See the `CHANGELOG.MD` file. 108 | -------------------------------------------------------------------------------- /images/any.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | ANY 69 | 70 | 71 | -------------------------------------------------------------------------------- /images/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | DELETE 69 | 70 | 71 | -------------------------------------------------------------------------------- /images/function-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperBrain/serverless-vscode/1e3ef50007b1b50c7172578093c9e900053d4ee0/images/function-context.png -------------------------------------------------------------------------------- /images/function-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperBrain/serverless-vscode/1e3ef50007b1b50c7172578093c9e900053d4ee0/images/function-demo.png -------------------------------------------------------------------------------- /images/get.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | GET 69 | 70 | 71 | -------------------------------------------------------------------------------- /images/post.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | POST 69 | 70 | 71 | -------------------------------------------------------------------------------- /images/put.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | PUT 69 | 70 | 71 | -------------------------------------------------------------------------------- /images/service-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HyperBrain/serverless-vscode/1e3ef50007b1b50c7172578093c9e900053d4ee0/images/service-demo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-vscode", 3 | "description": "Serverless integration. Lets you manage your service from within VSCode.", 4 | "version": "1.0.0", 5 | "publisher": "frankschmid", 6 | "author": "Frank Schmid ", 7 | "engines": { 8 | "vscode": "^1.19.0" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/HyperBrain/serverless-vscode" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "onView:serverlessOutline" 20 | ], 21 | "main": "./out/src/extension.js", 22 | "contributes": { 23 | "configuration": { 24 | "type": "object", 25 | "title": "Serverless Configuration", 26 | "properties": { 27 | "serverless.useGlobal": { 28 | "type": "boolean", 29 | "default": false, 30 | "description": "Use global Serverless installation" 31 | }, 32 | "serverless.slsDebug": { 33 | "type": "boolean", 34 | "default": false, 35 | "description": "Set SLS_DEBUG for debug output" 36 | }, 37 | "serverless.aws.defaultStage": { 38 | "type": "string", 39 | "default": "dev", 40 | "description": "Default stage for builds and deployments" 41 | }, 42 | "serverless.aws.defaultRegion": { 43 | "type": "string", 44 | "default": "us-east-1", 45 | "description": "Default region for builds and deployments" 46 | }, 47 | "serverless.aws.askForRegion": { 48 | "type": "boolean", 49 | "default": false, 50 | "description": "Ask for region (false uses the given default region)" 51 | } 52 | } 53 | }, 54 | "views": { 55 | "explorer": [ 56 | { 57 | "id": "serverlessOutline", 58 | "name": "Serverless", 59 | "when": "resourceLangId == 'yaml'" 60 | } 61 | ] 62 | }, 63 | "commands": [ 64 | { 65 | "command": "serverless.package", 66 | "title": "Package service", 67 | "category": "Serverless" 68 | }, 69 | { 70 | "command": "serverless.deploy", 71 | "title": "Deploy service", 72 | "category": "Serverless" 73 | }, 74 | { 75 | "command": "serverless.openHandler", 76 | "title": "Open handler", 77 | "category": "Serverless" 78 | }, 79 | { 80 | "command": "serverless.invokeLocal", 81 | "title": "Invoke local", 82 | "category": "Serverless" 83 | }, 84 | { 85 | "command": "serverless.deployFunction", 86 | "title": "Deploy function", 87 | "category": "Serverless" 88 | }, 89 | { 90 | "command": "serverless.logs", 91 | "title": "Show logs", 92 | "category": "Serverless" 93 | }, 94 | { 95 | "command": "serverless.resolve", 96 | "title": "Resolve", 97 | "category": "Serverless" 98 | } 99 | ], 100 | "menus": { 101 | "view/item/context": [ 102 | { 103 | "command": "serverless.resolve", 104 | "when": "viewItem == container" 105 | }, 106 | { 107 | "command": "serverless.package", 108 | "when": "viewItem == container" 109 | }, 110 | { 111 | "command": "serverless.deploy", 112 | "when": "viewItem == container" 113 | }, 114 | { 115 | "command": "serverless.openHandler", 116 | "when": "viewItem == function" 117 | }, 118 | { 119 | "command": "serverless.invokeLocal", 120 | "when": "viewItem == function" 121 | }, 122 | { 123 | "command": "serverless.logs", 124 | "when": "viewItem == function" 125 | }, 126 | { 127 | "command": "serverless.deployFunction", 128 | "when": "viewItem == function" 129 | } 130 | ], 131 | "explorer/context": [ 132 | { 133 | "when": "filesExplorerFocus", 134 | "command": "serverless.package", 135 | "group": "Serverless@1" 136 | }, 137 | { 138 | "when": "filesExplorerFocus", 139 | "command": "serverless.deploy", 140 | "group": "Serverless@2" 141 | } 142 | ] 143 | } 144 | }, 145 | "scripts": { 146 | "vscode:prepublish": "tsc -p ./", 147 | "compile": "tsc -watch -p ./", 148 | "lint": "tslint \"src/**/*.ts\"", 149 | "postinstall": "node ./node_modules/vscode/bin/install", 150 | "test": "node ./node_modules/vscode/bin/test" 151 | }, 152 | "devDependencies": { 153 | "@types/chai": "^4.0.10", 154 | "@types/chai-as-promised": "^7.1.0", 155 | "@types/js-yaml": "^3.10.1", 156 | "@types/lodash": "^4.14.91", 157 | "@types/mocha": "^2.2.44", 158 | "@types/node": "^8.5.1", 159 | "@types/sinon": "^4.1.2", 160 | "@types/sinon-chai": "^2.7.29", 161 | "chai": "^4.1.2", 162 | "chai-as-promised": "^7.1.1", 163 | "mocha": "^4.0.1", 164 | "sinon": "^4.1.3", 165 | "sinon-chai": "^2.14.0", 166 | "tslint": "^5.8.0", 167 | "typescript": "^2.6.2", 168 | "vscode": "^1.1.10" 169 | }, 170 | "dependencies": { 171 | "js-yaml": "^3.10.0", 172 | "jsonc-parser": "^1.0.0", 173 | "lodash": "^4.17.4" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { commands, ExtensionContext, window } from "vscode"; 3 | 4 | import { CommandHandler } from "./lib/CommandHandler"; 5 | import { Deploy } from "./lib/commands/Deploy"; 6 | import { DeployFunction } from "./lib/commands/DeployFunction"; 7 | import { InvokeLocal } from "./lib/commands/InvokeLocal"; 8 | import { Logs } from "./lib/commands/Logs"; 9 | import { OpenHandler } from "./lib/commands/OpenHandler"; 10 | import { Package } from "./lib/commands/Package"; 11 | import { Resolve } from "./lib/commands/Resolve"; 12 | import { ServerlessOutlineProvider } from "./lib/serverlessOutline"; 13 | 14 | /** 15 | * Activation entry point for the extension 16 | * @param context VSCode context 17 | */ 18 | export function activate(context: ExtensionContext) { 19 | // tslint:disable-next-line:no-console 20 | console.log("Loading Serverless extension"); 21 | 22 | const serverlessOutlineProvider = new ServerlessOutlineProvider(context); 23 | context.subscriptions.push(window.registerTreeDataProvider("serverlessOutline", serverlessOutlineProvider)); 24 | 25 | CommandHandler.registerCommand(OpenHandler, "serverless.openHandler", context); 26 | CommandHandler.registerCommand(Resolve, "serverless.resolve", context); 27 | CommandHandler.registerCommand(Logs, "serverless.logs", context); 28 | CommandHandler.registerCommand(InvokeLocal, "serverless.invokeLocal", context); 29 | CommandHandler.registerCommand(DeployFunction, "serverless.deployFunction", context); 30 | CommandHandler.registerCommand(Package, "serverless.package", context); 31 | CommandHandler.registerCommand(Deploy, "serverless.deploy", context); 32 | 33 | return null; 34 | } 35 | 36 | export function deactivate() { 37 | return; 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/CommandBase.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { window, workspace } from "vscode"; 3 | import { ServerlessNode } from "./ServerlessNode"; 4 | 5 | /** 6 | * Base class for VSCode Serverless commands. 7 | */ 8 | export abstract class CommandBase { 9 | 10 | protected static askForStageAndRegion(): Thenable { 11 | const configuration = workspace.getConfiguration(); 12 | const defaultStage: string = configuration.get("serverless.aws.defaultStage") || "dev"; 13 | const defaultRegion: string = configuration.get("serverless.aws.defaultRegion") || "us-east-1"; 14 | const askForRegion: boolean = configuration.get("serverless.aws.askForRegion") || false; 15 | 16 | return window.showInputBox({ 17 | placeHolder: defaultStage, 18 | prompt: `Stage (defaults to ${defaultStage})`, 19 | }) 20 | .then(stage => { 21 | if (_.isNil(stage)) { 22 | throw new Error("Command cancelled"); 23 | } 24 | if (askForRegion) { 25 | return window.showInputBox({ 26 | placeHolder: defaultRegion, 27 | prompt: `Region (defaults to ${defaultRegion})`, 28 | }) 29 | .then(region => { 30 | return [ stage || defaultStage, region || defaultRegion ]; 31 | }); 32 | } 33 | return [ stage || defaultStage, defaultRegion ]; 34 | }); 35 | } 36 | 37 | constructor(public readonly isExclusive: boolean = false) { 38 | } 39 | 40 | public abstract invoke(node: ServerlessNode): Thenable; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/CommandHandler.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { commands, Disposable, ExtensionContext, window } from "vscode"; 3 | import { CommandBase } from "./CommandBase"; 4 | import { ServerlessNode } from "./ServerlessNode"; 5 | 6 | /** 7 | * Wrap commands that process ServerlessNode objects and 8 | * provide a common UX. 9 | */ 10 | 11 | export class CommandHandler { 12 | 13 | public static isCommandRunning: boolean = false; 14 | 15 | public static registerCommand( 16 | commandClass: { new (context: ExtensionContext): T; }, 17 | name: string, 18 | context: ExtensionContext, 19 | ) { 20 | const handler = new CommandHandler(context, commandClass); 21 | context.subscriptions.push(commands.registerCommand(name, handler.invoke)); 22 | } 23 | 24 | private handler: T; 25 | 26 | private constructor(private context: ExtensionContext, handlerClass: { new (context: ExtensionContext): T; }) { 27 | this.handler = new handlerClass(context); 28 | this.invoke = this.invoke.bind(this); 29 | } 30 | 31 | public invoke(node: ServerlessNode): Thenable { 32 | const isExclusive = this.handler.isExclusive; 33 | if (isExclusive) { 34 | if (CommandHandler.isCommandRunning) { 35 | return window.showErrorMessage("Serverless: Another command is still in progress.") 36 | .then(_.noop); 37 | } 38 | CommandHandler.isCommandRunning = true; 39 | } 40 | 41 | return this.handler.invoke(node) 42 | .then(() => { 43 | if (isExclusive) { 44 | CommandHandler.isCommandRunning = false; 45 | } 46 | }, err => { 47 | if (isExclusive) { 48 | CommandHandler.isCommandRunning = false; 49 | } 50 | return window.showErrorMessage(`Serverless: ${err.message}`); 51 | }); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/Serverless.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process"; 2 | import * as _ from "lodash"; 3 | import * as path from "path"; 4 | import { OutputChannel, Terminal, TerminalOptions, window, workspace } from "vscode"; 5 | 6 | export interface IServerlessInvokeOptions { 7 | stage?: string; 8 | cwd?: string; 9 | } 10 | 11 | const ProcessingOptions = [ 12 | "cwd", 13 | ]; 14 | 15 | export class Serverless { 16 | 17 | public static invoke(command: string, options?: IServerlessInvokeOptions): Thenable { 18 | const commandOptions = Serverless.formatOptions(options); 19 | const cwd: string = _.get(options, "cwd") || __dirname; 20 | 21 | const serverless = new Serverless(cwd); 22 | return serverless.invokeCommand(command, commandOptions); 23 | } 24 | 25 | public static invokeWithResult(command: string, options?: IServerlessInvokeOptions): Thenable { 26 | const commandOptions = Serverless.formatOptions(options); 27 | const cwd: string = _.get(options, "cwd") || __dirname; 28 | 29 | const serverless = new Serverless(cwd); 30 | return serverless.invokeCommandWithResult(command, commandOptions); 31 | } 32 | 33 | private static formatOptions(invokeOptions?: IServerlessInvokeOptions): string[] { 34 | const options = _.defaults({}, _.omitBy(invokeOptions, (value, key) => _.includes(ProcessingOptions, key)), { 35 | stage: "dev", 36 | }); 37 | const commandOptions = _.map(options, (value: any, key: string) => { 38 | if (value === false) { 39 | return `--${key}`; 40 | } 41 | return `--${key}=${value}`; 42 | }); 43 | 44 | return commandOptions; 45 | } 46 | 47 | private cwd: string; 48 | private channel: OutputChannel; 49 | 50 | private constructor(cwd: string) { 51 | this.cwd = cwd; 52 | } 53 | 54 | private invokeCommandWithResult(command: string, options: string[]): Thenable { 55 | this.channel = window.createOutputChannel("Serverless"); 56 | this.channel.show(true); 57 | 58 | const serverlessCommand = `Running "serverless ${command} ${_.join(options, " ")}"`; 59 | this.channel.appendLine(serverlessCommand); 60 | 61 | return new Promise((resolve, reject) => { 62 | let result = ""; 63 | const sls = spawn("node", _.concat( 64 | [ "node_modules/serverless/bin/serverless" ], 65 | _.split(command, " "), 66 | options, 67 | ), { 68 | cwd: this.cwd, 69 | }); 70 | 71 | sls.on("error", err => { 72 | reject(err); 73 | }); 74 | 75 | sls.stdout.on("data", data => { 76 | result += data.toString(); 77 | }); 78 | 79 | sls.stderr.on("data", data => { 80 | this.channel.append(data.toString()); 81 | }); 82 | 83 | sls.on("exit", code => { 84 | if (code !== 0) { 85 | this.channel.append(result); 86 | reject(new Error(`Command exited with ${code}`)); 87 | } 88 | this.channel.appendLine("\nCommand finished."); 89 | this.channel.show(true); 90 | resolve(result); 91 | }); 92 | }); 93 | } 94 | 95 | private invokeCommand(command: string, options: string[]): Thenable { 96 | this.channel = window.createOutputChannel(command); 97 | this.channel.show(); 98 | 99 | const serverlessCommand = `Running "serverless ${command} ${_.join(options, " ")}"`; 100 | this.channel.appendLine(serverlessCommand); 101 | 102 | return new Promise((resolve, reject) => { 103 | const sls = spawn("node", _.concat( 104 | [ "node_modules/serverless/bin/serverless" ], 105 | _.split(command, " "), 106 | options, 107 | ), { 108 | cwd: this.cwd, 109 | }); 110 | 111 | sls.on("error", err => { 112 | reject(err); 113 | }); 114 | 115 | sls.stdout.on("data", data => { 116 | this.channel.append(data.toString()); 117 | }); 118 | 119 | sls.stderr.on("data", data => { 120 | this.channel.append(data.toString()); 121 | }); 122 | 123 | sls.on("exit", code => { 124 | this.channel.appendLine("\nCommand finished."); 125 | this.channel.show(true); 126 | resolve(); 127 | }); 128 | }); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/lib/ServerlessNode.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { Command, ExtensionContext } from "vscode"; 3 | 4 | export const enum NodeKind { 5 | ROOT = "root", 6 | CONTAINER = "container", 7 | FUNCTION = "function", 8 | APIPATH = "apipath", 9 | APIMETHOD = "apimethod", 10 | } 11 | 12 | export class ServerlessNode { 13 | 14 | public children: ServerlessNode[]; 15 | public name: string; 16 | public kind: NodeKind; 17 | public documentRoot: string; 18 | public data?: any; 19 | 20 | public constructor(name: string, kind: NodeKind, data?: object) { 21 | this.children = []; 22 | this.name = name; 23 | this.kind = kind; 24 | this.documentRoot = ""; 25 | this.data = data; 26 | } 27 | 28 | public get hasChildren(): boolean { 29 | return !_.isEmpty(this.children); 30 | } 31 | 32 | public getCommand(): Command | null { 33 | switch (this.kind) { 34 | 35 | } 36 | return null; 37 | } 38 | 39 | public setDocumentRoot(documentRoot: string) { 40 | this.documentRoot = documentRoot; 41 | _.forEach(this.children, child => child.setDocumentRoot(documentRoot)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/commands/Deploy.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as path from "path"; 3 | import { ExtensionContext, Uri, window } from "vscode"; 4 | import { CommandBase } from "../CommandBase"; 5 | import { Serverless } from "../Serverless"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | /** 9 | * Wrapper for Serverless deploy. 10 | */ 11 | 12 | export class Deploy extends CommandBase { 13 | 14 | constructor(private context: ExtensionContext) { 15 | super(true); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | if (node.kind !== NodeKind.CONTAINER) { 20 | return Promise.reject(new Error("Target must be a container")); 21 | } 22 | 23 | return CommandBase.askForStageAndRegion() 24 | .then(result => { 25 | const options = { 26 | cwd: node.documentRoot, 27 | region: result[1], 28 | stage: result[0], 29 | }; 30 | return Serverless.invoke("deploy", options); 31 | }); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/commands/DeployFunction.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as path from "path"; 3 | import { ExtensionContext, Uri, window } from "vscode"; 4 | import { CommandBase } from "../CommandBase"; 5 | import { Serverless } from "../Serverless"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | /** 9 | * Wrapper for Serverless deploy function. 10 | */ 11 | 12 | export class DeployFunction extends CommandBase { 13 | 14 | constructor(private context: ExtensionContext) { 15 | super(true); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | if (node.kind !== NodeKind.FUNCTION) { 20 | return Promise.reject(new Error("Target must be a function")); 21 | } 22 | 23 | return CommandBase.askForStageAndRegion() 24 | .then(result => { 25 | const options = { 26 | cwd: node.documentRoot, 27 | function: node.name, 28 | region: result[1], 29 | stage: result[0], 30 | }; 31 | return Serverless.invoke("deploy function", options); 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/commands/InvokeLocal.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as path from "path"; 3 | import { ExtensionContext, Uri, window } from "vscode"; 4 | import { CommandBase } from "../CommandBase"; 5 | import { Serverless } from "../Serverless"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | /** 9 | * Wrapper for Serverless invoke local. 10 | */ 11 | 12 | export class InvokeLocal extends CommandBase { 13 | 14 | constructor(private context: ExtensionContext) { 15 | super(true); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | if (node.kind !== NodeKind.FUNCTION) { 20 | return Promise.reject(new Error("Target must be a function")); 21 | } 22 | 23 | return CommandBase.askForStageAndRegion() 24 | .then(result => { 25 | return window.showOpenDialog({ 26 | canSelectFiles: true, 27 | canSelectFolders: false, 28 | canSelectMany: false, 29 | filters: { 30 | "Event JSON": [ "json" ], 31 | }, 32 | openLabel: "Select event", 33 | }) 34 | .then((files: Uri[] | undefined) => { 35 | if (!files || _.isEmpty(files)) { 36 | return Promise.resolve(); 37 | } 38 | 39 | const filePath = path.relative(node.documentRoot, files[0].fsPath); 40 | const options = { 41 | cwd: node.documentRoot, 42 | function: node.name, 43 | path: filePath, 44 | region: result[1], 45 | stage: result[0], 46 | }; 47 | return Serverless.invoke("invoke local", options); 48 | }); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/commands/Logs.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as path from "path"; 3 | import { ExtensionContext, Uri, window } from "vscode"; 4 | import { CommandBase } from "../CommandBase"; 5 | import { Serverless } from "../Serverless"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | /** 9 | * Wrapper for Serverless logs. 10 | */ 11 | 12 | export class Logs extends CommandBase { 13 | 14 | constructor(private context: ExtensionContext) { 15 | super(true); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | if (node.kind !== NodeKind.FUNCTION) { 20 | return Promise.reject(new Error("Target must be a function")); 21 | } 22 | 23 | return CommandBase.askForStageAndRegion() 24 | .then(result => { 25 | const options = { 26 | cwd: node.documentRoot, 27 | function: node.name, 28 | region: result[1], 29 | stage: result[0], 30 | }; 31 | return Serverless.invoke("logs", options); 32 | }); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/commands/OpenHandler.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "fs"; 2 | import * as _ from "lodash"; 3 | import * as path from "path"; 4 | import { ExtensionContext, Uri, window } from "vscode"; 5 | import { CommandBase } from "../CommandBase"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | export class OpenHandler extends CommandBase { 9 | 10 | constructor(private context: ExtensionContext) { 11 | super(); 12 | } 13 | 14 | public invoke(node: ServerlessNode): Thenable { 15 | if (node.kind !== NodeKind.FUNCTION) { 16 | return Promise.reject(new Error("Cannot open handler for non function")); 17 | } 18 | 19 | const handler = _.get(node.data, "handler", null); 20 | if (!handler) { 21 | return Promise.reject(new Error("Your function does not declare a valid handler")); 22 | } 23 | 24 | const handlerBase = /^(.*)\..*?$/.exec(handler); 25 | if (!handlerBase) { 26 | return Promise.reject(new Error("Your function handler is not formatted correctly")); 27 | } 28 | 29 | const root = node.documentRoot; 30 | // TODO: Support other handler types that are not supported directly with SLS. 31 | const handlerFile = path.join(root, handlerBase[1] + ".js"); 32 | if (!existsSync(handlerFile)) { 33 | return Promise.reject(new Error("Could not load handler")); 34 | } 35 | 36 | return window.showTextDocument(Uri.file(handlerFile), { 37 | preview: true, 38 | }) 39 | .then(() => Promise.resolve()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/commands/Package.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as path from "path"; 3 | import { ExtensionContext, Uri, window } from "vscode"; 4 | import { CommandBase } from "../CommandBase"; 5 | import { Serverless } from "../Serverless"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | /** 9 | * Wrapper for Serverless package. 10 | */ 11 | 12 | export class Package extends CommandBase { 13 | 14 | constructor(private context: ExtensionContext) { 15 | super(true); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | if (node.kind !== NodeKind.CONTAINER) { 20 | return Promise.reject(new Error("Target must be a container")); 21 | } 22 | 23 | return CommandBase.askForStageAndRegion() 24 | .then(result => { 25 | const options = { 26 | cwd: node.documentRoot, 27 | region: result[1], 28 | stage: result[0], 29 | }; 30 | return Serverless.invoke("package", options); 31 | }); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/commands/Resolve.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as path from "path"; 3 | import { ExtensionContext, Position, TextDocument, TextEditor, Uri, window, workspace } from "vscode"; 4 | import { CommandBase } from "../CommandBase"; 5 | import { Serverless } from "../Serverless"; 6 | import { NodeKind, ServerlessNode } from "../ServerlessNode"; 7 | 8 | /** 9 | * Wrapper for Serverless logs. 10 | */ 11 | 12 | export class Resolve extends CommandBase { 13 | 14 | constructor(private context: ExtensionContext) { 15 | super(true); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | if (node.kind !== NodeKind.CONTAINER) { 20 | return Promise.reject(new Error("Target must be a container")); 21 | } 22 | 23 | return CommandBase.askForStageAndRegion() 24 | .then(result => { 25 | const options = { 26 | cwd: node.documentRoot, 27 | region: result[1], 28 | stage: result[0], 29 | }; 30 | return Serverless.invokeWithResult("print", options); 31 | }) 32 | .then((resolvedYaml: string) => { 33 | return workspace.openTextDocument(Uri.parse("untitled:" + path.join(node.documentRoot, "resolved.yml"))) 34 | .then((doc: TextDocument) => window.showTextDocument(doc)) 35 | .then((editor: TextEditor) => { 36 | return editor.edit(edit => edit.insert(new Position(0, 0), resolvedYaml)); 37 | }); 38 | }) 39 | .then(_.noop); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/serverlessOutline.ts: -------------------------------------------------------------------------------- 1 | import * as yaml from "js-yaml"; 2 | import * as json from "jsonc-parser"; 3 | import * as _ from "lodash"; 4 | import * as path from "path"; 5 | import { 6 | Command, 7 | Event, 8 | EventEmitter, 9 | ExtensionContext, 10 | TextDocument, 11 | TextEditor, 12 | TreeDataProvider, 13 | TreeItem, 14 | TreeItemCollapsibleState, 15 | Uri, 16 | window, 17 | } from "vscode"; 18 | 19 | import { NodeKind, ServerlessNode } from "./ServerlessNode"; 20 | 21 | export class ServerlessOutlineProvider implements TreeDataProvider { 22 | 23 | // tslint:disable-next-line:variable-name 24 | private _onDidChangeTreeData: EventEmitter = new EventEmitter(); 25 | // tslint:disable-next-line:member-ordering 26 | public readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; 27 | 28 | private service: any; 29 | private warnings: string[]; 30 | private nodes: ServerlessNode; 31 | 32 | public constructor(private context: ExtensionContext) { 33 | this.warnings = []; 34 | this.nodes = new ServerlessNode("Service", NodeKind.ROOT); 35 | 36 | window.onDidChangeActiveTextEditor(editor => { 37 | this.refresh(); 38 | }); 39 | 40 | this.parseYaml(); 41 | } 42 | 43 | public getTreeItem(element: ServerlessNode): TreeItem { 44 | const treeItem = new TreeItem(element.name); 45 | treeItem.contextValue = element.kind; 46 | if (element.hasChildren) { 47 | treeItem.collapsibleState = 48 | element.kind !== NodeKind.ROOT ? TreeItemCollapsibleState.Collapsed : TreeItemCollapsibleState.Expanded; 49 | } else { 50 | treeItem.collapsibleState = TreeItemCollapsibleState.None; 51 | } 52 | // For API Methods we set the method as icon 53 | if (element.kind === NodeKind.APIMETHOD && element.data) { 54 | treeItem.iconPath = this.context.asAbsolutePath(`images/${_.toLower(element.data.method)}.svg`); 55 | } 56 | 57 | return treeItem; 58 | } 59 | 60 | public getChildren(element?: ServerlessNode): ServerlessNode[] { 61 | if (!element) { 62 | return this.nodes.children; 63 | } 64 | 65 | return element.children; 66 | } 67 | 68 | private refresh(offset?: ServerlessNode): void { 69 | this.parseYaml(); 70 | if (offset) { 71 | this._onDidChangeTreeData.fire(offset); 72 | } else { 73 | this._onDidChangeTreeData.fire(); 74 | } 75 | } 76 | 77 | private parseYaml(): void { 78 | const editor: TextEditor | undefined = window.activeTextEditor; 79 | const document = _.get(editor, "document"); 80 | const file = _.get(document, "fileName"); 81 | 82 | if (document && file && _.endsWith(file, "serverless.yml")) { 83 | this.nodes.children = []; 84 | try { 85 | const service = yaml.safeLoad(document.getText(), {}); 86 | this.parseService(service, document); 87 | } catch (err) { 88 | // console.error(err.message); 89 | } 90 | } 91 | } 92 | 93 | private addAPINode(apiRoot: ServerlessNode, httpNode: ServerlessNode) { 94 | const http = httpNode.data; 95 | const httpPath = _.compact(_.split(http.path, "/")); 96 | const apiLeaf = _.reduce(_.initial(httpPath), (root, httpPathElement) => { 97 | let apiPath = _.find(root.children, child => child.name === httpPathElement); 98 | if (!apiPath) { 99 | apiPath = new ServerlessNode(httpPathElement, NodeKind.APIPATH); 100 | root.children.push(apiPath); 101 | } 102 | return apiPath; 103 | }, apiRoot); 104 | const method = _.last(httpPath); 105 | if (method) { 106 | apiLeaf.children.push(new ServerlessNode(method, NodeKind.APIMETHOD, http)); 107 | } 108 | } 109 | 110 | private parseService(service: any, document: TextDocument) { 111 | const apiRootNode = new ServerlessNode("API", NodeKind.CONTAINER); 112 | const functionRootNode = new ServerlessNode("Functions", NodeKind.CONTAINER); 113 | const documentRoot = path.dirname(document.fileName); 114 | 115 | // Parse functions 116 | _.forOwn(service.functions, (func, funcName) => { 117 | const functionNode = new ServerlessNode(funcName, NodeKind.FUNCTION, func); 118 | 119 | // Add nodes for the function events 120 | if (!_.isEmpty(func.events)) { 121 | const httpEvents = _.filter(func.events, funcEvent => funcEvent.http); 122 | if (!_.isEmpty(httpEvents)) { 123 | const httpNode = new ServerlessNode("HTTP", NodeKind.CONTAINER); 124 | _.forEach(httpEvents, ({ http }) => { 125 | const name = http.path; 126 | const httpMethodNode = new ServerlessNode(name, NodeKind.APIMETHOD, http); 127 | httpNode.children.push(httpMethodNode); 128 | this.addAPINode(apiRootNode, httpMethodNode); 129 | }); 130 | functionNode.children.push(httpNode); 131 | } 132 | } 133 | 134 | functionRootNode.children.push(functionNode); 135 | }); 136 | 137 | functionRootNode.setDocumentRoot(documentRoot); 138 | 139 | this.nodes.children.push(functionRootNode); 140 | this.nodes.children.push(apiRootNode); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | import * as testRunner from "vscode/lib/testrunner"; 13 | 14 | // You can directly control Mocha options by uncommenting the following lines 15 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 16 | testRunner.configure({ 17 | ui: "bdd", // the BDD UI is being used in extension.test.ts (describe, it, etc.) 18 | useColors: true, // colored output from test results 19 | }); 20 | 21 | module.exports = testRunner; 22 | -------------------------------------------------------------------------------- /test/lib/CommandBase.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as sinon from "sinon"; 3 | import * as sinon_chai from "sinon-chai"; 4 | import { window, workspace, WorkspaceConfiguration } from "vscode"; 5 | import { CommandBase } from "../../src/lib/CommandBase"; 6 | import { NodeKind, ServerlessNode } from "../../src/lib/ServerlessNode"; 7 | 8 | // tslint:disable:no-unused-expression 9 | 10 | chai.use(sinon_chai); 11 | const expect = chai.expect; 12 | 13 | class CommandBaseTester extends CommandBase { 14 | public static askForStageAndRegion() { 15 | return CommandBase.askForStageAndRegion(); 16 | } 17 | 18 | public invoke(node: ServerlessNode): Thenable { 19 | return Promise.resolve(); 20 | } 21 | } 22 | 23 | describe("CommandBase", () => { 24 | let sandbox: sinon.SinonSandbox; 25 | let workspaceGetConfigurationStub: sinon.SinonStub; 26 | let configurationMock: any; 27 | let windowShowInputBoxStub: sinon.SinonStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | configurationMock = { 32 | get: sandbox.stub(), 33 | }; 34 | }); 35 | 36 | beforeEach(() => { 37 | windowShowInputBoxStub = sandbox.stub(window, "showInputBox"); 38 | workspaceGetConfigurationStub = sandbox.stub(workspace, "getConfiguration"); 39 | workspaceGetConfigurationStub.returns(configurationMock); 40 | }); 41 | 42 | afterEach(() => { 43 | sandbox.restore(); 44 | }); 45 | 46 | describe("askForStage", () => { 47 | describe("without asking for region", () => { 48 | it("should set prompt and placeholder", async () => { 49 | windowShowInputBoxStub.resolves(""); 50 | configurationMock.get.withArgs("serverless.aws.askForRegion").returns(false); 51 | configurationMock.get.withArgs("serverless.aws.defaultStage").returns("dev"); 52 | configurationMock.get.withArgs("serverless.aws.defaultRegion").returns("us-east-1"); 53 | const result = await CommandBaseTester.askForStageAndRegion(); 54 | expect(windowShowInputBoxStub).to.have.been.calledOnce; 55 | expect(windowShowInputBoxStub).to.have.been.calledWithExactly({ 56 | placeHolder: "dev", 57 | prompt: "Stage (defaults to dev)", 58 | }); 59 | }); 60 | 61 | it("should use configured defaults", async () => { 62 | windowShowInputBoxStub.resolves(""); 63 | configurationMock.get.withArgs("serverless.aws.askForRegion").returns(false); 64 | configurationMock.get.withArgs("serverless.aws.defaultStage").returns("myStage"); 65 | configurationMock.get.withArgs("serverless.aws.defaultRegion").returns("us-east-1"); 66 | const result = await CommandBaseTester.askForStageAndRegion(); 67 | expect(result).to.deep.equal(["myStage", "us-east-1"]); 68 | }); 69 | 70 | it("should set stage to user input", async () => { 71 | windowShowInputBoxStub.resolves("myStage"); 72 | configurationMock.get.withArgs("serverless.aws.askForRegion").returns(false); 73 | configurationMock.get.withArgs("serverless.aws.defaultStage").returns("dev"); 74 | configurationMock.get.withArgs("serverless.aws.defaultRegion").returns("us-east-1"); 75 | const result = await CommandBaseTester.askForStageAndRegion(); 76 | expect(result).to.deep.equal(["myStage", "us-east-1"]); 77 | }); 78 | }); 79 | 80 | describe("with asking for region", () => { 81 | it("should set prompt and placeholder", async () => { 82 | windowShowInputBoxStub.resolves(""); 83 | configurationMock.get.withArgs("serverless.aws.askForRegion").returns(true); 84 | configurationMock.get.withArgs("serverless.aws.defaultStage").returns("dev"); 85 | configurationMock.get.withArgs("serverless.aws.defaultRegion").returns("us-east-1"); 86 | const result = await CommandBaseTester.askForStageAndRegion(); 87 | expect(windowShowInputBoxStub).to.have.been.calledTwice; 88 | expect(windowShowInputBoxStub.firstCall).to.have.been.calledWithExactly({ 89 | placeHolder: "dev", 90 | prompt: "Stage (defaults to dev)", 91 | }); 92 | expect(windowShowInputBoxStub.secondCall).to.have.been.calledWithExactly({ 93 | placeHolder: "us-east-1", 94 | prompt: "Region (defaults to us-east-1)", 95 | }); 96 | }); 97 | 98 | it("should use configured defaults", async () => { 99 | windowShowInputBoxStub.resolves(""); 100 | configurationMock.get.withArgs("serverless.aws.askForRegion").returns(true); 101 | configurationMock.get.withArgs("serverless.aws.defaultStage").returns("myStage"); 102 | configurationMock.get.withArgs("serverless.aws.defaultRegion").returns("us-west-1"); 103 | const result = await CommandBaseTester.askForStageAndRegion(); 104 | expect(result).to.deep.equal(["myStage", "us-west-1"]); 105 | }); 106 | 107 | it("should set stage and region to user input", async () => { 108 | windowShowInputBoxStub.onFirstCall().resolves("myStage"); 109 | windowShowInputBoxStub.onSecondCall().resolves("myRegion"); 110 | configurationMock.get.withArgs("serverless.aws.askForRegion").returns(true); 111 | configurationMock.get.withArgs("serverless.aws.defaultStage").returns("dev"); 112 | configurationMock.get.withArgs("serverless.aws.defaultRegion").returns("us-east-1"); 113 | const result = await CommandBaseTester.askForStageAndRegion(); 114 | expect(result).to.deep.equal(["myStage", "myRegion"]); 115 | }); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/lib/CommandHandler.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as _ from "lodash"; 3 | import * as sinon from "sinon"; 4 | import * as sinon_chai from "sinon-chai"; 5 | import { commands, ExtensionContext, Memento, window } from "vscode"; 6 | import { CommandBase } from "../../src/lib/CommandBase"; 7 | import { CommandHandler } from "../../src/lib/CommandHandler"; 8 | import { NodeKind, ServerlessNode } from "../../src/lib/ServerlessNode"; 9 | import { TestContext } from "./TestContext"; 10 | 11 | // tslint:disable:no-unused-expression 12 | // tslint:disable:max-classes-per-file 13 | 14 | chai.use(sinon_chai); 15 | const expect = chai.expect; 16 | 17 | class TestCommand extends CommandBase { 18 | constructor(public context: ExtensionContext) { 19 | super(); 20 | this.invoke = sinon.stub(); 21 | } 22 | 23 | public invoke(node: ServerlessNode): Thenable { 24 | throw new Error("TestCommand: Method not implemented."); 25 | } 26 | } 27 | 28 | class TestCommandExclusive extends CommandBase { 29 | constructor(public context: ExtensionContext) { 30 | super(true); 31 | } 32 | 33 | public invoke(node: ServerlessNode): Thenable { 34 | return new Promise(resolve => { 35 | setTimeout(() => { 36 | resolve(); 37 | }, 200); 38 | }); 39 | } 40 | } 41 | 42 | describe("CommandHandler", () => { 43 | let sandbox: sinon.SinonSandbox; 44 | let windowShowInputBoxStub: sinon.SinonStub; 45 | let windowShowErrorMessageStub: sinon.SinonStub; 46 | 47 | before(() => { 48 | sandbox = sinon.createSandbox(); 49 | }); 50 | 51 | beforeEach(() => { 52 | windowShowInputBoxStub = sandbox.stub(window, "showInputBox"); 53 | windowShowErrorMessageStub = sandbox.stub(window, "showErrorMessage"); 54 | }); 55 | 56 | afterEach(() => { 57 | sandbox.restore(); 58 | }); 59 | 60 | describe("registerCommand", () => { 61 | let testContext: ExtensionContext; 62 | let commandsRegisterCommandSpy: sinon.SinonSpy; 63 | 64 | beforeEach(() => { 65 | testContext = new TestContext(); 66 | commandsRegisterCommandSpy = sandbox.spy(commands, "registerCommand"); 67 | }); 68 | 69 | it("should register command and keep subscription", async () => { 70 | CommandHandler.registerCommand( 71 | TestCommand, 72 | "serverless.test", 73 | testContext, 74 | ); 75 | 76 | expect(testContext.subscriptions).to.have.length(1); 77 | expect(commandsRegisterCommandSpy).to.have.been.calledOnce; 78 | const registeredCommands = await commands.getCommands(); 79 | expect(registeredCommands).to.include("serverless.test"); 80 | }); 81 | 82 | it("should invoke registered command", async () => { 83 | const registeredCommands = await commands.getCommands(); 84 | if (!_.includes(registeredCommands, "serverless.test")) { 85 | CommandHandler.registerCommand( 86 | TestCommand, 87 | "serverless.test", 88 | testContext, 89 | ); 90 | } 91 | 92 | return commands.executeCommand("serverless.test") 93 | .then( 94 | () => expect(true).to.be.false, 95 | _.noop, 96 | ); 97 | }); 98 | }); 99 | 100 | describe("Commands", () => { 101 | let testContext: ExtensionContext; 102 | 103 | before(() => { 104 | testContext = new TestContext(); 105 | CommandHandler.registerCommand( 106 | TestCommandExclusive, 107 | "serverless.testexclusive", 108 | testContext, 109 | ); 110 | }); 111 | 112 | it("should execute exclusively alone", async () => { 113 | return expect(commands.executeCommand("serverless.testexclusive")) 114 | .to.been.fulfilled 115 | .then(() => { 116 | expect(windowShowErrorMessageStub).to.not.have.been.called; 117 | }); 118 | }); 119 | 120 | it("should execute only one exclusive command", async () => { 121 | CommandHandler.isCommandRunning = true; 122 | return expect(commands.executeCommand("serverless.testexclusive")) 123 | .to.been.rejected 124 | .then(() => { 125 | expect(windowShowErrorMessageStub).to.have.been.called; 126 | }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/lib/Serverless.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chai_as_promised from "chai-as-promised"; 3 | import * as child_process from "child_process"; 4 | import * as _ from "lodash"; 5 | import * as sinon from "sinon"; 6 | import * as sinon_chai from "sinon-chai"; 7 | import { commands, ExtensionContext, Memento, OutputChannel, window } from "vscode"; 8 | import { CommandHandler } from "../../src/lib/CommandHandler"; 9 | import { IServerlessInvokeOptions, Serverless } from "../../src/lib/Serverless"; 10 | import { NodeKind, ServerlessNode } from "../../src/lib/ServerlessNode"; 11 | 12 | // tslint:disable:max-classes-per-file 13 | // tslint:disable:no-unused-expression 14 | 15 | chai.use(chai_as_promised); 16 | chai.use(sinon_chai); 17 | const expect = chai.expect; 18 | 19 | class TestOutputChannel implements OutputChannel { 20 | public static create(sandbox: sinon.SinonSandbox) { 21 | return new TestOutputChannel(sandbox); 22 | } 23 | 24 | public name: string; 25 | public append: sinon.SinonStub; 26 | public appendLine: sinon.SinonStub; 27 | public clear: sinon.SinonStub; 28 | public show: sinon.SinonStub; 29 | public hide: sinon.SinonStub; 30 | public dispose: sinon.SinonStub; 31 | 32 | private constructor(sandbox: sinon.SinonSandbox) { 33 | this.name = ""; 34 | this.append = sandbox.stub(); 35 | this.appendLine = sandbox.stub(); 36 | this.clear = sandbox.stub(); 37 | this.show = sandbox.stub(); 38 | this.hide = sandbox.stub(); 39 | this.dispose = sandbox.stub(); 40 | } 41 | } 42 | 43 | describe("Serverless", () => { 44 | let sandbox: sinon.SinonSandbox; 45 | let testOutputChannel: TestOutputChannel; 46 | let testChildProcess: any; 47 | let windowCreateOutputChannelStub: sinon.SinonStub; 48 | let spawnStub: sinon.SinonStub; 49 | 50 | before(() => { 51 | sandbox = sinon.createSandbox(); 52 | }); 53 | 54 | beforeEach(() => { 55 | windowCreateOutputChannelStub = sandbox.stub(window, "createOutputChannel"); 56 | testOutputChannel = TestOutputChannel.create(sandbox); 57 | windowCreateOutputChannelStub.returns(testOutputChannel); 58 | spawnStub = sandbox.stub(child_process, "spawn"); 59 | testChildProcess = { 60 | on: sandbox.stub(), 61 | stderr: { 62 | on: sandbox.stub(), 63 | }, 64 | stdout: { 65 | on: sandbox.stub(), 66 | }, 67 | }; 68 | spawnStub.returns(testChildProcess); 69 | }); 70 | 71 | afterEach(() => { 72 | sandbox.resetHistory(); 73 | sandbox.restore(); 74 | }); 75 | 76 | describe("invoke", () => { 77 | it("should spawn serverless", () => { 78 | testChildProcess.on.withArgs("exit").yields(0); 79 | return expect(Serverless.invoke("my command")).to.be.fulfilled 80 | .then(() => { 81 | expect(spawnStub).to.have.been.calledOnce; 82 | expect(spawnStub.firstCall.args[0]).to.equal("node"); 83 | expect(spawnStub.firstCall.args[1]).to.deep.equal([ 84 | "node_modules/serverless/bin/serverless", 85 | "my", 86 | "command", 87 | "--stage=dev", 88 | ]); 89 | expect(spawnStub.firstCall.args[2]).to.be.an("object") 90 | .that.has.property("cwd"); 91 | }); 92 | }); 93 | 94 | it("should use custom options", () => { 95 | const options = { 96 | cwd: "myCwd", 97 | myOpt: "myOption", 98 | stage: "myStage", 99 | }; 100 | testChildProcess.on.withArgs("exit").yields(0); 101 | return expect(Serverless.invoke("my command", options)).to.be.fulfilled 102 | .then(() => { 103 | expect(spawnStub).to.have.been.calledOnce; 104 | expect(spawnStub.firstCall.args[0]).to.equal("node"); 105 | expect(spawnStub.firstCall.args[1]).to.deep.equal([ 106 | "node_modules/serverless/bin/serverless", 107 | "my", 108 | "command", 109 | "--myOpt=myOption", 110 | "--stage=myStage", 111 | ]); 112 | expect(spawnStub.firstCall.args[2]).to.be.an("object") 113 | .that.has.property("cwd", "myCwd"); 114 | }); 115 | }); 116 | 117 | it("should reject if spawn fails", () => { 118 | const options = { 119 | cwd: "myCwd", 120 | myOpt: "myOption", 121 | stage: "myStage", 122 | }; 123 | testChildProcess.on.withArgs("error").yields(new Error("SPAWN FAILED")); 124 | return expect(Serverless.invoke("my command", options)) 125 | .to.be.rejectedWith("SPAWN FAILED"); 126 | }); 127 | 128 | it("should log stdout to channel", () => { 129 | const testOutput = "My command output"; 130 | const options = { 131 | cwd: "myCwd", 132 | myOpt: "myOption", 133 | stage: "myStage", 134 | }; 135 | testChildProcess.stdout.on.withArgs("data").yields(testOutput); 136 | testChildProcess.on.withArgs("exit").yields(0); 137 | return expect(Serverless.invoke("my command", options)) 138 | .to.be.fulfilled 139 | .then(() => { 140 | expect(testOutputChannel.append).to.have.been.calledWithExactly(testOutput); 141 | }); 142 | }); 143 | 144 | it("should log stderr to channel", () => { 145 | const testOutput = "My error output"; 146 | const options = { 147 | cwd: "myCwd", 148 | myOpt: "myOption", 149 | stage: "myStage", 150 | }; 151 | testChildProcess.stderr.on.withArgs("data").yields(testOutput); 152 | testChildProcess.on.withArgs("exit").yields(0); 153 | return expect(Serverless.invoke("my command", options)) 154 | .to.be.fulfilled 155 | .then(() => { 156 | expect(testOutputChannel.append).to.have.been.calledWithExactly(testOutput); 157 | }); 158 | }); 159 | }); 160 | 161 | describe("invokeWithResult", () => { 162 | it("should spawn serverless", () => { 163 | testChildProcess.on.withArgs("exit").yields(0); 164 | return expect(Serverless.invokeWithResult("my command")).to.be.fulfilled 165 | .then(() => { 166 | expect(spawnStub).to.have.been.calledOnce; 167 | expect(spawnStub.firstCall.args[0]).to.equal("node"); 168 | expect(spawnStub.firstCall.args[1]).to.deep.equal([ 169 | "node_modules/serverless/bin/serverless", 170 | "my", 171 | "command", 172 | "--stage=dev", 173 | ]); 174 | expect(spawnStub.firstCall.args[2]).to.be.an("object") 175 | .that.has.property("cwd"); 176 | }); 177 | }); 178 | 179 | it("should use custom options", () => { 180 | const options = { 181 | cwd: "myCwd", 182 | myOpt: "myOption", 183 | stage: "myStage", 184 | }; 185 | testChildProcess.on.withArgs("exit").yields(0); 186 | return expect(Serverless.invokeWithResult("my command", options)).to.be.fulfilled 187 | .then(() => { 188 | expect(spawnStub).to.have.been.calledOnce; 189 | expect(spawnStub.firstCall.args[0]).to.equal("node"); 190 | expect(spawnStub.firstCall.args[1]).to.deep.equal([ 191 | "node_modules/serverless/bin/serverless", 192 | "my", 193 | "command", 194 | "--myOpt=myOption", 195 | "--stage=myStage", 196 | ]); 197 | expect(spawnStub.firstCall.args[2]).to.be.an("object") 198 | .that.has.property("cwd", "myCwd"); 199 | }); 200 | }); 201 | 202 | it("should reject if spawn fails", () => { 203 | const options = { 204 | cwd: "myCwd", 205 | myOpt: "myOption", 206 | stage: "myStage", 207 | }; 208 | testChildProcess.on.withArgs("error").yields(new Error("SPAWN FAILED")); 209 | return expect(Serverless.invokeWithResult("my command", options)) 210 | .to.be.rejectedWith("SPAWN FAILED"); 211 | }); 212 | 213 | it("should capture stdout and not log to channel", () => { 214 | const testOutput = "My command output"; 215 | const options = { 216 | cwd: "myCwd", 217 | myOpt: "myOption", 218 | stage: "myStage", 219 | }; 220 | testChildProcess.stdout.on.withArgs("data").yields(testOutput); 221 | testChildProcess.on.withArgs("exit").yields(0); 222 | return expect(Serverless.invokeWithResult("my command", options)) 223 | .to.be.fulfilled 224 | .then(() => { 225 | expect(testOutputChannel.append).to.not.have.been.called; 226 | }); 227 | }); 228 | 229 | it("should log stderr to channel", () => { 230 | const testOutput = "My error output"; 231 | const options = { 232 | cwd: "myCwd", 233 | myOpt: "myOption", 234 | stage: "myStage", 235 | }; 236 | testChildProcess.stderr.on.withArgs("data").yields(testOutput); 237 | testChildProcess.on.withArgs("exit").yields(0); 238 | return expect(Serverless.invoke("my command", options)) 239 | .to.be.fulfilled 240 | .then(() => { 241 | expect(testOutputChannel.append).to.have.been.calledWithExactly(testOutput); 242 | }); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /test/lib/ServerlessNode.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as _ from "lodash"; 3 | import * as sinon from "sinon"; 4 | import { NodeKind, ServerlessNode } from "../../src/lib/ServerlessNode"; 5 | 6 | // tslint:disable:no-unused-expression 7 | 8 | const expect = chai.expect; 9 | 10 | describe("ServerlessNode", () => { 11 | it("should provide hasChildren property", () => { 12 | const root = new ServerlessNode("root", NodeKind.ROOT); 13 | const noChildren = new ServerlessNode("root", NodeKind.ROOT); 14 | 15 | root.children.push(new ServerlessNode("Child", NodeKind.CONTAINER)); 16 | 17 | expect(root.hasChildren).to.be.true; 18 | expect(noChildren.hasChildren).to.be.false; 19 | }); 20 | 21 | it("should set document root resursively", () => { 22 | const docRoot = "myRoot"; 23 | const root = new ServerlessNode("root", NodeKind.ROOT); 24 | 25 | // Add some child nodes 26 | for (let n = 0; n < 5; n++) { 27 | const childNode = new ServerlessNode(`child ${n}`, NodeKind.CONTAINER); 28 | for (let m = 0; m < 2; m++) { 29 | childNode.children.push(new ServerlessNode(`func ${m}`, NodeKind.FUNCTION)); 30 | } 31 | root.children.push(childNode); 32 | } 33 | 34 | expect(root.setDocumentRoot(docRoot)).to.not.throw; 35 | 36 | // Check all child nodes 37 | const allChildren = _.flatMap(root.children, child => { 38 | const result = [ child.documentRoot ]; 39 | if (child.hasChildren) { 40 | const childDocRoots = _.flatMap(child.children, subChild => subChild.documentRoot); 41 | Array.prototype.push.apply(result, childDocRoots); 42 | } 43 | return result; 44 | }); 45 | expect(allChildren).to.been.of.length(15); 46 | expect(_.every(allChildren, (childDocRoot: string) => _.isEqual(childDocRoot, docRoot))) 47 | .to.been.true; 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/lib/TestContext.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { commands, ExtensionContext, Memento } from "vscode"; 3 | 4 | export class TestContext implements ExtensionContext { 5 | public subscriptions: Array<{ dispose(): any; }> = []; 6 | public workspaceState: Memento; 7 | public globalState: Memento; 8 | public extensionPath: string = "myExtensionPath"; 9 | public asAbsolutePath: sinon.SinonStub = sinon.stub(); 10 | public storagePath: string = "myStoragePath"; 11 | } 12 | -------------------------------------------------------------------------------- /test/lib/commands/Deploy.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as _ from "lodash"; 4 | import * as sinon from "sinon"; 5 | import { CommandBase } from "../../../src/lib/CommandBase"; 6 | import { Deploy } from "../../../src/lib/commands/Deploy"; 7 | import { Serverless } from "../../../src/lib/Serverless"; 8 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 9 | import { TestContext } from "../TestContext"; 10 | 11 | // tslint:disable:no-unused-expression 12 | 13 | // tslint:disable-next-line:no-var-requires 14 | chai.use(chaiAsPromised); 15 | const expect = chai.expect; 16 | 17 | /** 18 | * Unit tests for the Deploy command 19 | */ 20 | 21 | describe("Deploy", () => { 22 | let sandbox: sinon.SinonSandbox; 23 | let deployCommand: Deploy; 24 | let commandBaseAskForStageStub: sinon.SinonStub; 25 | let serverlessInvokeStub: sinon.SinonStub; 26 | 27 | before(() => { 28 | sandbox = sinon.createSandbox(); 29 | }); 30 | 31 | beforeEach(() => { 32 | deployCommand = new Deploy(new TestContext()); 33 | commandBaseAskForStageStub = sandbox.stub(CommandBase, "askForStageAndRegion" as any); 34 | serverlessInvokeStub = sandbox.stub(Serverless, "invoke"); 35 | }); 36 | 37 | afterEach(() => { 38 | sandbox.restore(); 39 | }); 40 | 41 | describe("with different node types", () => { 42 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 43 | { 44 | node: new ServerlessNode("function node", NodeKind.FUNCTION), 45 | shouldSucceed: false, 46 | }, 47 | { 48 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 49 | shouldSucceed: true, 50 | }, 51 | { 52 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 53 | shouldSucceed: false, 54 | }, 55 | { 56 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 57 | shouldSucceed: false, 58 | }, 59 | { 60 | node: new ServerlessNode("root node", NodeKind.ROOT), 61 | shouldSucceed: false, 62 | }, 63 | ]; 64 | 65 | _.forEach(testNodes, testNode => { 66 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 67 | commandBaseAskForStageStub.resolves(["stage", "region"]); 68 | const expectation = expect(deployCommand.invoke(testNode.node)); 69 | if (testNode.shouldSucceed) { 70 | return expectation.to.be.fulfilled; 71 | } 72 | return expectation.to.be.rejected; 73 | }); 74 | }); 75 | }); 76 | 77 | it("should ask for the stage", () => { 78 | commandBaseAskForStageStub.resolves(["stage", "region"]); 79 | return expect(deployCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 80 | .to.be.fulfilled 81 | .then(() => { 82 | expect(commandBaseAskForStageStub).to.have.been.calledOnce; 83 | }); 84 | }); 85 | 86 | it("should invoke Serverless", () => { 87 | commandBaseAskForStageStub.resolves(["stage", "region"]); 88 | serverlessInvokeStub.resolves(); 89 | return expect(deployCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 90 | .to.be.fulfilled 91 | .then(() => { 92 | expect(serverlessInvokeStub).to.have.been.calledOnce; 93 | expect(serverlessInvokeStub).to.have.been.calledWithExactly("deploy", { 94 | cwd: "", 95 | region: "region", 96 | stage: "stage", 97 | }); 98 | }); 99 | }); 100 | 101 | it("should propagate Serverless error", () => { 102 | commandBaseAskForStageStub.resolves(["stage", "region"]); 103 | serverlessInvokeStub.rejects(new Error("Serverless error")); 104 | return expect(deployCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 105 | .to.be.rejectedWith("Serverless error"); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/lib/commands/DeployFunction.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as _ from "lodash"; 4 | import * as sinon from "sinon"; 5 | import { CommandBase } from "../../../src/lib/CommandBase"; 6 | import { DeployFunction } from "../../../src/lib/commands/DeployFunction"; 7 | import { Serverless } from "../../../src/lib/Serverless"; 8 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 9 | import { TestContext } from "../TestContext"; 10 | 11 | // tslint:disable:no-unused-expression 12 | 13 | // tslint:disable-next-line:no-var-requires 14 | chai.use(chaiAsPromised); 15 | const expect = chai.expect; 16 | 17 | /** 18 | * Unit tests for the DeployFunction command 19 | */ 20 | 21 | describe("DeployFunction", () => { 22 | let sandbox: sinon.SinonSandbox; 23 | let deployFunctionCommand: DeployFunction; 24 | let commandBaseAskForStageStub: sinon.SinonStub; 25 | let serverlessInvokeStub: sinon.SinonStub; 26 | 27 | before(() => { 28 | sandbox = sinon.createSandbox(); 29 | }); 30 | 31 | beforeEach(() => { 32 | deployFunctionCommand = new DeployFunction(new TestContext()); 33 | commandBaseAskForStageStub = sandbox.stub(CommandBase, "askForStageAndRegion" as any); 34 | serverlessInvokeStub = sandbox.stub(Serverless, "invoke"); 35 | }); 36 | 37 | afterEach(() => { 38 | sandbox.restore(); 39 | }); 40 | 41 | describe("with different node types", () => { 42 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 43 | { 44 | node: new ServerlessNode("function node", NodeKind.FUNCTION), 45 | shouldSucceed: true, 46 | }, 47 | { 48 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 49 | shouldSucceed: false, 50 | }, 51 | { 52 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 53 | shouldSucceed: false, 54 | }, 55 | { 56 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 57 | shouldSucceed: false, 58 | }, 59 | { 60 | node: new ServerlessNode("root node", NodeKind.ROOT), 61 | shouldSucceed: false, 62 | }, 63 | ]; 64 | 65 | _.forEach(testNodes, testNode => { 66 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 67 | commandBaseAskForStageStub.resolves(["stage", "region"]); 68 | const expectation = expect(deployFunctionCommand.invoke(testNode.node)); 69 | if (testNode.shouldSucceed) { 70 | return expectation.to.be.fulfilled; 71 | } 72 | return expectation.to.be.rejected; 73 | }); 74 | }); 75 | }); 76 | 77 | it("should ask for the stage", () => { 78 | commandBaseAskForStageStub.resolves(["stage", "region"]); 79 | return expect(deployFunctionCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 80 | .to.be.fulfilled 81 | .then(() => { 82 | expect(commandBaseAskForStageStub).to.have.been.calledOnce; 83 | }); 84 | }); 85 | 86 | it("should invoke Serverless", () => { 87 | commandBaseAskForStageStub.resolves(["stage", "region"]); 88 | serverlessInvokeStub.resolves(); 89 | return expect(deployFunctionCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 90 | .to.be.fulfilled 91 | .then(() => { 92 | expect(serverlessInvokeStub).to.have.been.calledOnce; 93 | expect(serverlessInvokeStub).to.have.been.calledWithExactly("deploy function", { 94 | cwd: "", 95 | function: "testNode", 96 | region: "region", 97 | stage: "stage", 98 | }); 99 | }); 100 | }); 101 | 102 | it("should propagate Serverless error", () => { 103 | commandBaseAskForStageStub.resolves(["stage", "region"]); 104 | serverlessInvokeStub.rejects(new Error("Serverless error")); 105 | return expect(deployFunctionCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 106 | .to.be.rejectedWith("Serverless error"); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/lib/commands/InvokeLocal.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as _ from "lodash"; 4 | import * as path from "path"; 5 | import * as sinon from "sinon"; 6 | import { Uri, window } from "vscode"; 7 | import { CommandBase } from "../../../src/lib/CommandBase"; 8 | import { InvokeLocal } from "../../../src/lib/commands/InvokeLocal"; 9 | import { Serverless } from "../../../src/lib/Serverless"; 10 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 11 | import { TestContext } from "../TestContext"; 12 | 13 | // tslint:disable:no-unused-expression 14 | 15 | // tslint:disable-next-line:no-var-requires 16 | chai.use(chaiAsPromised); 17 | const expect = chai.expect; 18 | 19 | /** 20 | * Unit tests for the InvokeLocal command 21 | */ 22 | 23 | describe("InvokeLocal", () => { 24 | let sandbox: sinon.SinonSandbox; 25 | let InvokeLocalCommand: InvokeLocal; 26 | let commandBaseAskForStageStub: sinon.SinonStub; 27 | let windowShowOpenDialogStub: sinon.SinonStub; 28 | let serverlessInvokeStub: sinon.SinonStub; 29 | 30 | before(() => { 31 | sandbox = sinon.createSandbox(); 32 | }); 33 | 34 | beforeEach(() => { 35 | InvokeLocalCommand = new InvokeLocal(new TestContext()); 36 | commandBaseAskForStageStub = sandbox.stub(CommandBase, "askForStageAndRegion" as any); 37 | windowShowOpenDialogStub = sandbox.stub(window, "showOpenDialog"); 38 | serverlessInvokeStub = sandbox.stub(Serverless, "invoke"); 39 | }); 40 | 41 | afterEach(() => { 42 | sandbox.restore(); 43 | }); 44 | 45 | describe("with different node types", () => { 46 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 47 | { 48 | node: new ServerlessNode("function node", NodeKind.FUNCTION), 49 | shouldSucceed: true, 50 | }, 51 | { 52 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 53 | shouldSucceed: false, 54 | }, 55 | { 56 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 57 | shouldSucceed: false, 58 | }, 59 | { 60 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 61 | shouldSucceed: false, 62 | }, 63 | { 64 | node: new ServerlessNode("root node", NodeKind.ROOT), 65 | shouldSucceed: false, 66 | }, 67 | ]; 68 | 69 | _.forEach(testNodes, testNode => { 70 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 71 | commandBaseAskForStageStub.resolves(["stage", "region"]); 72 | windowShowOpenDialogStub.resolves([ 73 | Uri.file("/my/test/event.json"), 74 | ]); 75 | const expectation = expect(InvokeLocalCommand.invoke(testNode.node)); 76 | if (testNode.shouldSucceed) { 77 | return expectation.to.be.fulfilled; 78 | } 79 | return expectation.to.be.rejected; 80 | }); 81 | }); 82 | }); 83 | 84 | it("should ask for the stage", () => { 85 | commandBaseAskForStageStub.resolves(["stage", "region"]); 86 | windowShowOpenDialogStub.resolves([ 87 | Uri.file("/my/test/event.json"), 88 | ]); 89 | return expect(InvokeLocalCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 90 | .to.be.fulfilled 91 | .then(() => { 92 | expect(commandBaseAskForStageStub).to.have.been.calledOnce; 93 | }); 94 | }); 95 | 96 | it("should ask for an event json", () => { 97 | commandBaseAskForStageStub.resolves(["stage", "region"]); 98 | windowShowOpenDialogStub.resolves([ 99 | Uri.file("/my/test/event.json"), 100 | ]); 101 | return expect(InvokeLocalCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 102 | .to.be.fulfilled 103 | .then(() => { 104 | expect(windowShowOpenDialogStub).to.have.been.calledOnce; 105 | expect(windowShowOpenDialogStub).to.have.been.calledWithExactly({ 106 | canSelectFiles: true, 107 | canSelectFolders: false, 108 | canSelectMany: false, 109 | filters: { 110 | "Event JSON": [ "json" ], 111 | }, 112 | openLabel: "Select event", 113 | }); 114 | }); 115 | }); 116 | 117 | describe("when event selection is cancelled", () => { 118 | it("should do nothing for empty array", () => { 119 | commandBaseAskForStageStub.resolves(["stage", "region"]); 120 | windowShowOpenDialogStub.resolves([]); 121 | return expect(InvokeLocalCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 122 | .to.be.fulfilled 123 | .then(() => { 124 | expect(windowShowOpenDialogStub).to.have.been.calledOnce; 125 | expect(serverlessInvokeStub).to.not.have.been.called; 126 | }); 127 | }); 128 | 129 | it("should do nothing for undefined", () => { 130 | commandBaseAskForStageStub.resolves(["stage", "region"]); 131 | windowShowOpenDialogStub.resolves(); 132 | return expect(InvokeLocalCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 133 | .to.be.fulfilled 134 | .then(() => { 135 | expect(windowShowOpenDialogStub).to.have.been.calledOnce; 136 | expect(serverlessInvokeStub).to.not.have.been.called; 137 | }); 138 | }); 139 | }); 140 | 141 | it("should invoke Serverless", () => { 142 | const testFilePath = "/my/test/event.json"; 143 | commandBaseAskForStageStub.resolves(["stage", "region"]); 144 | windowShowOpenDialogStub.resolves([ 145 | Uri.file(testFilePath), 146 | ]); 147 | serverlessInvokeStub.resolves(); 148 | return expect(InvokeLocalCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 149 | .to.be.fulfilled 150 | .then(() => { 151 | expect(serverlessInvokeStub).to.have.been.calledOnce; 152 | expect(serverlessInvokeStub).to.have.been.calledWithExactly("invoke local", { 153 | cwd: "", 154 | function: "testNode", 155 | path: path.relative("", testFilePath), 156 | region: "region", 157 | stage: "stage", 158 | }); 159 | }); 160 | }); 161 | 162 | it("should propagate Serverless error", () => { 163 | commandBaseAskForStageStub.resolves(["stage", "region"]); 164 | windowShowOpenDialogStub.resolves([ 165 | Uri.file("/my/test/event.json"), 166 | ]); 167 | serverlessInvokeStub.rejects(new Error("Serverless error")); 168 | return expect(InvokeLocalCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 169 | .to.be.rejectedWith("Serverless error"); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/lib/commands/Logs.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as _ from "lodash"; 4 | import * as sinon from "sinon"; 5 | import { CommandBase } from "../../../src/lib/CommandBase"; 6 | import { Logs } from "../../../src/lib/commands/Logs"; 7 | import { Serverless } from "../../../src/lib/Serverless"; 8 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 9 | import { TestContext } from "../TestContext"; 10 | 11 | // tslint:disable:no-unused-expression 12 | 13 | // tslint:disable-next-line:no-var-requires 14 | chai.use(chaiAsPromised); 15 | const expect = chai.expect; 16 | 17 | /** 18 | * Unit tests for the Logs command 19 | */ 20 | 21 | describe("Logs", () => { 22 | let sandbox: sinon.SinonSandbox; 23 | let logsCommand: Logs; 24 | let commandBaseAskForStageStub: sinon.SinonStub; 25 | let serverlessInvokeStub: sinon.SinonStub; 26 | 27 | before(() => { 28 | sandbox = sinon.createSandbox(); 29 | }); 30 | 31 | beforeEach(() => { 32 | logsCommand = new Logs(new TestContext()); 33 | commandBaseAskForStageStub = sandbox.stub(CommandBase, "askForStageAndRegion" as any); 34 | serverlessInvokeStub = sandbox.stub(Serverless, "invoke"); 35 | }); 36 | 37 | afterEach(() => { 38 | sandbox.restore(); 39 | }); 40 | 41 | describe("with different node types", () => { 42 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 43 | { 44 | node: new ServerlessNode("function node", NodeKind.FUNCTION), 45 | shouldSucceed: true, 46 | }, 47 | { 48 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 49 | shouldSucceed: false, 50 | }, 51 | { 52 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 53 | shouldSucceed: false, 54 | }, 55 | { 56 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 57 | shouldSucceed: false, 58 | }, 59 | { 60 | node: new ServerlessNode("root node", NodeKind.ROOT), 61 | shouldSucceed: false, 62 | }, 63 | ]; 64 | 65 | _.forEach(testNodes, testNode => { 66 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 67 | commandBaseAskForStageStub.resolves(["stage", "region"]); 68 | const expectation = expect(logsCommand.invoke(testNode.node)); 69 | if (testNode.shouldSucceed) { 70 | return expectation.to.be.fulfilled; 71 | } 72 | return expectation.to.be.rejected; 73 | }); 74 | }); 75 | }); 76 | 77 | it("should ask for the stage", () => { 78 | commandBaseAskForStageStub.resolves(["stage", "region"]); 79 | return expect(logsCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 80 | .to.be.fulfilled 81 | .then(() => { 82 | expect(commandBaseAskForStageStub).to.have.been.calledOnce; 83 | }); 84 | }); 85 | 86 | it("should invoke Serverless", () => { 87 | commandBaseAskForStageStub.resolves(["stage", "region"]); 88 | serverlessInvokeStub.resolves(); 89 | return expect(logsCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 90 | .to.be.fulfilled 91 | .then(() => { 92 | expect(serverlessInvokeStub).to.have.been.calledOnce; 93 | expect(serverlessInvokeStub).to.have.been.calledWithExactly("logs", { 94 | cwd: "", 95 | function: "testNode", 96 | region: "region", 97 | stage: "stage", 98 | }); 99 | }); 100 | }); 101 | 102 | it("should propagate Serverless error", () => { 103 | commandBaseAskForStageStub.resolves(["stage", "region"]); 104 | serverlessInvokeStub.rejects(new Error("Serverless error")); 105 | return expect(logsCommand.invoke(new ServerlessNode("testNode", NodeKind.FUNCTION))) 106 | .to.be.rejectedWith("Serverless error"); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/lib/commands/OpenHandler.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as fs from "fs"; 4 | import * as _ from "lodash"; 5 | import * as sinon from "sinon"; 6 | import { window } from "vscode"; 7 | import { CommandBase } from "../../../src/lib/CommandBase"; 8 | import { OpenHandler } from "../../../src/lib/commands/OpenHandler"; 9 | import { Serverless } from "../../../src/lib/Serverless"; 10 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 11 | import { TestContext } from "../TestContext"; 12 | 13 | // tslint:disable:no-unused-expression 14 | 15 | // tslint:disable-next-line:no-var-requires 16 | chai.use(chaiAsPromised); 17 | const expect = chai.expect; 18 | 19 | /** 20 | * Unit tests for the OpenHandler command 21 | */ 22 | 23 | describe("OpenHandler", () => { 24 | let sandbox: sinon.SinonSandbox; 25 | let openHandlerCommand: OpenHandler; 26 | let windowShowTextDocumentStub: sinon.SinonStub; 27 | let existsSyncStub: sinon.SinonStub; 28 | 29 | before(() => { 30 | sandbox = sinon.createSandbox(); 31 | }); 32 | 33 | beforeEach(() => { 34 | openHandlerCommand = new OpenHandler(new TestContext()); 35 | windowShowTextDocumentStub = sandbox.stub(window, "showTextDocument"); 36 | existsSyncStub = sandbox.stub(fs, "existsSync"); 37 | }); 38 | 39 | afterEach(() => { 40 | sandbox.restore(); 41 | }); 42 | 43 | describe("with different node types", () => { 44 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 45 | { 46 | node: new ServerlessNode("function node", NodeKind.FUNCTION, { 47 | handler: "myHandler.handle", 48 | }), 49 | shouldSucceed: true, 50 | }, 51 | { 52 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 53 | shouldSucceed: false, 54 | }, 55 | { 56 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 57 | shouldSucceed: false, 58 | }, 59 | { 60 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 61 | shouldSucceed: false, 62 | }, 63 | { 64 | node: new ServerlessNode("root node", NodeKind.ROOT), 65 | shouldSucceed: false, 66 | }, 67 | ]; 68 | 69 | _.forEach(testNodes, testNode => { 70 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 71 | windowShowTextDocumentStub.resolves(); 72 | existsSyncStub.returns(true); 73 | const expectation = expect(openHandlerCommand.invoke(testNode.node)); 74 | if (testNode.shouldSucceed) { 75 | return expectation.to.be.fulfilled; 76 | } 77 | return expectation.to.be.rejected; 78 | }); 79 | }); 80 | }); 81 | 82 | it("should reject if handler is not declared", () => { 83 | const functionDefinition = {}; 84 | windowShowTextDocumentStub.resolves(); 85 | existsSyncStub.returns(true); 86 | return expect(openHandlerCommand.invoke(new ServerlessNode("myFunc", NodeKind.FUNCTION, functionDefinition))) 87 | .to.been.rejectedWith(/does not declare a valid handler/) 88 | .then(() => { 89 | expect(existsSyncStub).to.not.have.been.called; 90 | expect(windowShowTextDocumentStub).to.not.have.been.called; 91 | }); 92 | }); 93 | 94 | it("should reject if handler is malformed", () => { 95 | const functionDefinition = { 96 | handler: "myInvalidHandler", 97 | }; 98 | windowShowTextDocumentStub.resolves(); 99 | existsSyncStub.returns(true); 100 | return expect(openHandlerCommand.invoke(new ServerlessNode("myFunc", NodeKind.FUNCTION, functionDefinition))) 101 | .to.been.rejectedWith(/is not formatted correctly/) 102 | .then(() => { 103 | expect(existsSyncStub).to.not.have.been.called; 104 | expect(windowShowTextDocumentStub).to.not.have.been.called; 105 | }); 106 | }); 107 | 108 | it("should reject if handler source is not found", () => { 109 | const functionDefinition = { 110 | handler: "myHandler.handle", 111 | }; 112 | windowShowTextDocumentStub.resolves(); 113 | existsSyncStub.returns(false); 114 | return expect(openHandlerCommand.invoke(new ServerlessNode("myFunc", NodeKind.FUNCTION, functionDefinition))) 115 | .to.been.rejectedWith(/Could not load handler/) 116 | .then(() => { 117 | expect(existsSyncStub).to.have.been.calledOnce; 118 | expect(windowShowTextDocumentStub).to.not.have.been.called; 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/lib/commands/Package.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as _ from "lodash"; 4 | import * as sinon from "sinon"; 5 | import { CommandBase } from "../../../src/lib/CommandBase"; 6 | import { Package } from "../../../src/lib/commands/Package"; 7 | import { Serverless } from "../../../src/lib/Serverless"; 8 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 9 | import { TestContext } from "../TestContext"; 10 | 11 | // tslint:disable:no-unused-expression 12 | 13 | // tslint:disable-next-line:no-var-requires 14 | chai.use(chaiAsPromised); 15 | const expect = chai.expect; 16 | 17 | /** 18 | * Unit tests for the DeployFunction command 19 | */ 20 | 21 | describe("DeployFunction", () => { 22 | let sandbox: sinon.SinonSandbox; 23 | let packageCommand: Package; 24 | let commandBaseAskForStageStub: sinon.SinonStub; 25 | let serverlessInvokeStub: sinon.SinonStub; 26 | 27 | before(() => { 28 | sandbox = sinon.createSandbox(); 29 | }); 30 | 31 | beforeEach(() => { 32 | packageCommand = new Package(new TestContext()); 33 | commandBaseAskForStageStub = sandbox.stub(CommandBase, "askForStageAndRegion" as any); 34 | serverlessInvokeStub = sandbox.stub(Serverless, "invoke"); 35 | }); 36 | 37 | afterEach(() => { 38 | sandbox.restore(); 39 | }); 40 | 41 | describe("with different node types", () => { 42 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 43 | { 44 | node: new ServerlessNode("function node", NodeKind.FUNCTION), 45 | shouldSucceed: false, 46 | }, 47 | { 48 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 49 | shouldSucceed: true, 50 | }, 51 | { 52 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 53 | shouldSucceed: false, 54 | }, 55 | { 56 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 57 | shouldSucceed: false, 58 | }, 59 | { 60 | node: new ServerlessNode("root node", NodeKind.ROOT), 61 | shouldSucceed: false, 62 | }, 63 | ]; 64 | 65 | _.forEach(testNodes, testNode => { 66 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 67 | commandBaseAskForStageStub.resolves(["stage", "region"]); 68 | const expectation = expect(packageCommand.invoke(testNode.node)); 69 | if (testNode.shouldSucceed) { 70 | return expectation.to.be.fulfilled; 71 | } 72 | return expectation.to.be.rejected; 73 | }); 74 | }); 75 | }); 76 | 77 | it("should ask for the stage", () => { 78 | commandBaseAskForStageStub.resolves(["stage", "region"]); 79 | return expect(packageCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 80 | .to.be.fulfilled 81 | .then(() => { 82 | expect(commandBaseAskForStageStub).to.have.been.calledOnce; 83 | }); 84 | }); 85 | 86 | it("should invoke Serverless", () => { 87 | commandBaseAskForStageStub.resolves(["stage", "region"]); 88 | serverlessInvokeStub.resolves(); 89 | return expect(packageCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 90 | .to.be.fulfilled 91 | .then(() => { 92 | expect(serverlessInvokeStub).to.have.been.calledOnce; 93 | expect(serverlessInvokeStub).to.have.been.calledWithExactly("package", { 94 | cwd: "", 95 | region: "region", 96 | stage: "stage", 97 | }); 98 | }); 99 | }); 100 | 101 | it("should propagate Serverless error", () => { 102 | commandBaseAskForStageStub.resolves(["stage", "region"]); 103 | serverlessInvokeStub.rejects(new Error("Serverless error")); 104 | return expect(packageCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 105 | .to.be.rejectedWith("Serverless error"); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/lib/commands/Resolve.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as chaiAsPromised from "chai-as-promised"; 3 | import * as _ from "lodash"; 4 | import * as sinon from "sinon"; 5 | import { 6 | Position, 7 | Range, 8 | Selection, 9 | SnippetString, 10 | TextDocument, 11 | TextEditor, 12 | TextEditorDecorationType, 13 | TextEditorEdit, 14 | TextEditorOptions, 15 | TextEditorRevealType, 16 | ViewColumn, 17 | window, 18 | workspace, 19 | } from "vscode"; 20 | import { CommandBase } from "../../../src/lib/CommandBase"; 21 | import { Resolve } from "../../../src/lib/commands/Resolve"; 22 | import { Serverless } from "../../../src/lib/Serverless"; 23 | import { NodeKind, ServerlessNode } from "../../../src/lib/ServerlessNode"; 24 | import { TestContext } from "../TestContext"; 25 | 26 | // tslint:disable:no-unused-expression 27 | 28 | // tslint:disable-next-line:no-var-requires 29 | chai.use(chaiAsPromised); 30 | const expect = chai.expect; 31 | 32 | /** 33 | * Stubbed TextEditor. 34 | */ 35 | class TestEditor implements TextEditor { 36 | public document: TextDocument; 37 | public selection: Selection; 38 | public selections: Selection[]; 39 | public options: TextEditorOptions; 40 | public viewColumn?: ViewColumn | undefined; 41 | 42 | public edit: sinon.SinonStub; 43 | public insertSnippet: sinon.SinonStub; 44 | public setDecorations: sinon.SinonStub; 45 | public revealRange: sinon.SinonStub; 46 | public show: sinon.SinonStub; 47 | public hide: sinon.SinonStub; 48 | 49 | constructor(sandbox: sinon.SinonSandbox) { 50 | this.edit = sandbox.stub(); 51 | this.insertSnippet = sandbox.stub(); 52 | this.setDecorations = sandbox.stub(); 53 | this.revealRange = sandbox.stub(); 54 | this.show = sandbox.stub(); 55 | this.hide = sandbox.stub(); 56 | } 57 | } 58 | 59 | const testDocument = _.join([ 60 | "# Test serverless.yml (resolved)", 61 | "service: my-service", 62 | "provider:", 63 | " name: aws", 64 | "# Nothing more here ;-)", 65 | ], "\n"); 66 | 67 | /** 68 | * Unit tests for the Resolve command 69 | */ 70 | 71 | describe("Resolve", () => { 72 | let sandbox: sinon.SinonSandbox; 73 | let resolveCommand: Resolve; 74 | let testEditor: TestEditor; 75 | let windowShowTextDocumentStub: sinon.SinonStub; 76 | let workspaceOpenTextDocumentStub: sinon.SinonStub; 77 | let commandBaseAskForStageStub: sinon.SinonStub; 78 | let serverlessInvokeWithResultStub: sinon.SinonStub; 79 | 80 | before(() => { 81 | sandbox = sinon.createSandbox(); 82 | }); 83 | 84 | beforeEach(() => { 85 | resolveCommand = new Resolve(new TestContext()); 86 | windowShowTextDocumentStub = sandbox.stub(window, "showTextDocument"); 87 | workspaceOpenTextDocumentStub = sandbox.stub(workspace, "openTextDocument"); 88 | commandBaseAskForStageStub = sandbox.stub(CommandBase, "askForStageAndRegion" as any); 89 | serverlessInvokeWithResultStub = sandbox.stub(Serverless, "invokeWithResult"); 90 | 91 | testEditor = new TestEditor(sandbox); 92 | testEditor.edit.resolves(); 93 | windowShowTextDocumentStub.resolves(testEditor); 94 | }); 95 | 96 | afterEach(() => { 97 | sandbox.restore(); 98 | }); 99 | 100 | describe("with different node types", () => { 101 | const testNodes: Array<{ node: ServerlessNode, shouldSucceed: boolean }> = [ 102 | { 103 | node: new ServerlessNode("function node", NodeKind.FUNCTION), 104 | shouldSucceed: false, 105 | }, 106 | { 107 | node: new ServerlessNode("container node", NodeKind.CONTAINER), 108 | shouldSucceed: true, 109 | }, 110 | { 111 | node: new ServerlessNode("api method node", NodeKind.APIMETHOD), 112 | shouldSucceed: false, 113 | }, 114 | { 115 | node: new ServerlessNode("api path node", NodeKind.APIPATH), 116 | shouldSucceed: false, 117 | }, 118 | { 119 | node: new ServerlessNode("root node", NodeKind.ROOT), 120 | shouldSucceed: false, 121 | }, 122 | ]; 123 | 124 | _.forEach(testNodes, testNode => { 125 | it(`should ${testNode.shouldSucceed ? "succeed" : "fail"} for ${testNode.node.name}`, () => { 126 | commandBaseAskForStageStub.resolves(["stage", "region"]); 127 | serverlessInvokeWithResultStub.resolves(testDocument); 128 | workspaceOpenTextDocumentStub.resolves(); 129 | const expectation = expect(resolveCommand.invoke(testNode.node)); 130 | if (testNode.shouldSucceed) { 131 | return expectation.to.be.fulfilled; 132 | } 133 | return expectation.to.be.rejected; 134 | }); 135 | }); 136 | }); 137 | 138 | it("should ask for the stage", () => { 139 | commandBaseAskForStageStub.resolves(["stage", "region"]); 140 | serverlessInvokeWithResultStub.resolves(testDocument); 141 | workspaceOpenTextDocumentStub.resolves(); 142 | return expect(resolveCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 143 | .to.be.fulfilled 144 | .then(() => { 145 | expect(commandBaseAskForStageStub).to.have.been.calledOnce; 146 | }); 147 | }); 148 | 149 | it("should invoke Serverless", () => { 150 | commandBaseAskForStageStub.resolves(["stage", "region"]); 151 | serverlessInvokeWithResultStub.resolves(); 152 | serverlessInvokeWithResultStub.resolves(testDocument); 153 | workspaceOpenTextDocumentStub.resolves(); 154 | return expect(resolveCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 155 | .to.be.fulfilled 156 | .then(() => { 157 | expect(serverlessInvokeWithResultStub).to.have.been.calledOnce; 158 | expect(serverlessInvokeWithResultStub).to.have.been.calledWithExactly("print", { 159 | cwd: "", 160 | region: "region", 161 | stage: "stage", 162 | }); 163 | expect(workspaceOpenTextDocumentStub).to.have.been.calledOnce; 164 | expect(windowShowTextDocumentStub).to.have.been.calledOnce; 165 | expect(testEditor.edit).to.have.been.calledOnce; 166 | }); 167 | }); 168 | 169 | it("should propagate Serverless error", () => { 170 | commandBaseAskForStageStub.resolves(["stage", "region"]); 171 | serverlessInvokeWithResultStub.rejects(new Error("Serverless error")); 172 | return expect(resolveCommand.invoke(new ServerlessNode("testNode", NodeKind.CONTAINER))) 173 | .to.be.rejectedWith("Serverless error"); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/lib/serverlessOutline.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from "chai"; 2 | import * as yaml from "js-yaml"; 3 | import * as _ from "lodash"; 4 | import * as sinon from "sinon"; 5 | import * as sinon_chai from "sinon-chai"; 6 | import { window } from "vscode"; 7 | import { ServerlessOutlineProvider } from "../../src/lib/serverlessOutline"; 8 | import { TestContext } from "./TestContext"; 9 | 10 | // tslint:disable:no-unused-expression 11 | 12 | chai.use(sinon_chai); 13 | const expect = chai.expect; 14 | 15 | const sampleYaml = ` 16 | # serverless.yml 17 | provider: 18 | name: aws 19 | functions: 20 | hello: 21 | handler: hello.handle 22 | wow: 23 | handler: wow.handle 24 | events: 25 | - http: 26 | path: api/v1/wow 27 | method: get 28 | `; 29 | 30 | const TextEditorMock = { 31 | document: { 32 | fileName: "serverless.yml", 33 | getText: sinon.stub().returns(sampleYaml), 34 | }, 35 | }; 36 | 37 | describe("ServerlessOutlineProvider", () => { 38 | let sandbox: sinon.SinonSandbox; 39 | let yamlSafeLoadStub: sinon.SinonStub; 40 | 41 | before(() => { 42 | sandbox = sinon.createSandbox(); 43 | }); 44 | 45 | beforeEach(() => { 46 | // Set active editor mock 47 | sandbox.stub(window, "activeTextEditor").value(TextEditorMock); 48 | yamlSafeLoadStub = sandbox.stub(yaml, "safeLoad"); 49 | }); 50 | 51 | afterEach(() => { 52 | sandbox.restore(); 53 | }); 54 | 55 | describe("constructor", () => { 56 | it("should parse service from active text editor", () => { 57 | yamlSafeLoadStub.returns({ 58 | functions: { 59 | hello: { 60 | handler: "hello.handle", 61 | }, 62 | wow: { 63 | events: [ 64 | { 65 | http: { 66 | method: "get", 67 | path: "/api/v1/wow", 68 | }, 69 | }, 70 | ], 71 | handler: "wow.handle", 72 | }, 73 | }, 74 | provider: { 75 | name: "aws", 76 | }, 77 | }); 78 | 79 | const provider = new ServerlessOutlineProvider(new TestContext()); 80 | 81 | expect(yamlSafeLoadStub).to.have.been.calledOnce; 82 | expect(yamlSafeLoadStub).to.have.been.calledWithExactly( 83 | sampleYaml, 84 | {}, 85 | ); 86 | 87 | const children = provider.getChildren(); 88 | expect(children).to.been.of.length(2); 89 | const functionsNode = _.find(children, [ "name", "Functions" ]); 90 | expect(functionsNode).to.have.property("children") 91 | .that.is.an("array").that.has.lengthOf(2); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES5", 5 | "outDir": "out", 6 | "sourceMap": true, 7 | "strict": true, 8 | "types" : ["node","lodash","mocha"], 9 | "lib": [ 10 | "es2015", 11 | "es2015.promise" 12 | ] 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "arrow-parens": [true, "ban-single-arg-parens"], 9 | "indent": [true, "tabs"], 10 | "no-trailing-whitespace": [true, "ignore-jsdoc"] 11 | }, 12 | "rulesDirectory": [] 13 | } 14 | --------------------------------------------------------------------------------