├── .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 | [](https://marketplace.visualstudio.com/items?itemName=frankschmid.serverless-vscode)
4 | [](https://marketplace.visualstudio.com/items?itemName=frankschmid.serverless-vscode)
5 | [](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 | 
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 | 
78 |
79 | All function related commands of the extension can be called via the context menu of the function.
80 |
81 | 
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 |
71 |
--------------------------------------------------------------------------------
/images/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
71 |
--------------------------------------------------------------------------------
/images/post.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
71 |
--------------------------------------------------------------------------------
/images/put.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------