Repository: serverless/serverless-azure-functions
Pull request #204 (2019-07-17 12:25:44)

docs: Deploy doc updates
- [x] Put paragraphs into one line within markdown
- [x] Remove `rollback enabled/disabled` for different deployment options since it is available for either deployment option

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24621176/badge)](https://coveralls.io/builds/24621176)

Coverage remained the same at 84.169% when pulling **d648bdc019be65c39238b5a66e349f01b8482e95 on tabarlow/doc-updates** into **b1feafbcc7c1815968e46041212b7904821daf90 on dev**.


Patch:
diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md
index ad19fd8..51cd26a 100644
--- a/docs/DEPLOY.md
+++ b/docs/DEPLOY.md
@@ -4,9 +4,7 @@ Deploy usage guide and design decision.
 
 ## Multi-stage, multi-region deploy
 
-By default, all functions are deploy to a stage (`dev`) and a region.  The
-autogenerated resource group name will reflect this information.  The name 
-follow the Microsoft recommended [naming convention](https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions#general).
+By default, all functions are deploy to a stage (`dev`) and a region. The autogenerated resource group name will reflect this information.  The name follow the Microsoft recommended [naming convention](https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions#general).
 
 ### Using exitsing resource group
 
@@ -28,7 +26,7 @@ follow the Microsoft recommended [naming convention](https://docs.microsoft.com/
 
 #### Specifying resourceGroup in serverless.yml
 
-- it is not clear if a resourceGroup has already been associated with a given stage and region.  
+- it is not clear if a resourceGroup has already been associated with a given stage and region.
 
 For example, we have the following default config
 
@@ -49,7 +47,7 @@ then user try to deploy
 
 `sls deploy -s prod -r westus -g prodResourceGroup`
 
-1. With this format, it's more clear which stage+region combo is associated with a resourceGroup.  
+1. With this format, it's more clear which stage+region combo is associated with a resourceGroup.
 1. User can still make mistake, however, and use the `wrong` resource group for a specific stage.
 
 #### Don't support user defined resource group
@@ -59,10 +57,10 @@ then user try to deploy
 
 ## Deployment Methodologies
 
-#### 1. Deployment to Function App (rollback disabled)
+#### 1. Deployment to Function App
 - Deploy resource group, upload packaged artifact directly to function app. Sets function app `RUN_FROM_PACKAGE` setting to `1`.
 
-#### 2. Deployment to Blob Storage (rollback enabled)
+#### 2. Deployment to Blob Storage
 - Deploy resource group, upload packaged function app to blob storage with version name. Sets function app `RUN_FROM_PACKAGE` setting to path of zipped artifact in blob storage
 - Default container name - `DEPLOYMENT_ARTIFACTS` (configurable in `serverless.yml`, see below)
 
diff --git a/package-lock.json b/package-lock.json
index 8f27d8f..0fb1b9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-6",
+  "version": "1.0.0-7",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index f8d38ce..7f84b17 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-6",
+  "version": "1.0.0-7",
   "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.",
   "license": "MIT",
   "main": "./lib/index.js",
diff --git a/src/plugins/azureBasePlugin.ts b/src/plugins/azureBasePlugin.ts
index e90b584..3aa7834 100644
--- a/src/plugins/azureBasePlugin.ts
+++ b/src/plugins/azureBasePlugin.ts
@@ -2,10 +2,10 @@ import { Guard } from "../shared/guard";
 import Serverless from "serverless";
 import { Utils } from "../shared/utils";
 
-export abstract class AzureBasePlugin {
+export abstract class AzureBasePlugin<TOptions=Serverless.Options> {
   public constructor(
     protected serverless: Serverless,
-    protected options: Serverless.Options,
+    protected options: TOptions,
   ) {
     Guard.null(serverless);
   }
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 945b672..8e38560 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -1,15 +1,15 @@
+import fs from "fs";
 import Serverless from "serverless";
 import { FunctionAppService } from "../../services/functionAppService";
+import { AzureLoginOptions } from "../../services/loginService";
 import { ResourceService } from "../../services/resourceService";
-import { Utils } from "../../shared/utils";
 import { AzureBasePlugin } from "../azureBasePlugin";
 
-
-export class AzureDeployPlugin extends AzureBasePlugin {
+export class AzureDeployPlugin extends AzureBasePlugin<AzureLoginOptions> {
   public hooks: { [eventName: string]: Promise<any> };
   public commands: any;
 
-  public constructor(serverless: Serverless, options: Serverless.Options) {
+  public constructor(serverless: Serverless, options: AzureLoginOptions) {
     super(serverless, options);
 
     this.hooks = {
@@ -28,9 +28,17 @@ export class AzureDeployPlugin extends AzureBasePlugin {
           }
         },
         options: {
-          "resourceGroup": {
+          resourceGroup: {
             usage: "Resource group for the service",
             shortcut: "g",
+          },
+          subscriptionId: {
+            usage: "Sets the Azure subscription ID",
+            shortcut: "i",
+          },
+          package: {
+            usage: "Package to deploy",
+            shortcut: "p",
           }
         }
       }
@@ -40,35 +48,19 @@ export class AzureDeployPlugin extends AzureBasePlugin {
   private async list() {
     this.log("Listing deployments");
     const resourceService = new ResourceService(this.serverless, this.options);
-    const deployments = await resourceService.getDeployments();
-    if (!deployments || deployments.length === 0) {
-      this.log(`No deployments found for resource group '${resourceService.getResourceGroupName()}'`);
-      return;
-    }
-    let stringDeployments = "\n\nDeployments";
-
-    for (const dep of deployments) {
-      stringDeployments += "\n-----------\n"
-      stringDeployments += `Name: ${dep.name}\n`
-      const timestampFromName = Utils.getTimestampFromName(dep.name);
-      stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;
-
-      const dateTime = timestampFromName ? new Date(+timestampFromName).toISOString() : "None";
-      stringDeployments += `Datetime: ${dateTime}\n`
-    }
-
-    stringDeployments += "-----------\n"
-    this.log(stringDeployments);
+    this.log(await resourceService.listDeployments());
   }
 
   private async deploy() {
     const resourceService = new ResourceService(this.serverless, this.options);
-
-    await resourceService.deployResourceGroup();
-
     const functionAppService = new FunctionAppService(this.serverless, this.options);
+    const zipFile = functionAppService.getFunctionZipFile();
+    if (!fs.existsSync(zipFile)) {
+      this.log(`Function app zip file '${zipFile}' does not exist`);
+      return Promise.resolve();
+    }
+    await resourceService.deployResourceGroup();
     const functionApp = await functionAppService.deploy();
-
     await functionAppService.uploadFunctions(functionApp);
   }
 }
diff --git a/src/plugins/login/azureLoginPlugin.ts b/src/plugins/login/azureLoginPlugin.ts
index 188086f..14e12a1 100644
--- a/src/plugins/login/azureLoginPlugin.ts
+++ b/src/plugins/login/azureLoginPlugin.ts
@@ -1,22 +1,18 @@
 import Serverless from "serverless";
-import AzureProvider from "../../provider/azureProvider";
-import { AzureLoginService } from "../../services/loginService";
+import { AzureLoginOptions, AzureLoginService } from "../../services/loginService";
 import { AzureBasePlugin } from "../azureBasePlugin";
+import { loginHooks } from "./loginHooks";
 
-export class AzureLoginPlugin extends AzureBasePlugin {
-  private provider: AzureProvider;
+export class AzureLoginPlugin extends AzureBasePlugin<AzureLoginOptions> {
   public hooks: { [eventName: string]: Promise<any> };
 
-  public constructor(serverless: Serverless, options: Serverless.Options) {
+  public constructor(serverless: Serverless, options: AzureLoginOptions) {
     super(serverless, options);
-    this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
 
-    this.hooks = {
-      "before:package:initialize": this.login.bind(this),
-      "before:deploy:list:list": this.login.bind(this),
-      "before:invoke:invoke": this.login.bind(this),
-      "before:rollback:rollback": this.login.bind(this),
-    };
+    this.hooks = {};
+    for (const h of loginHooks) {
+      this.hooks[`before:${h}`] = this.login.bind(this);
+    }
   }
 
   private async login() {
@@ -32,7 +28,8 @@ export class AzureLoginPlugin extends AzureBasePlugin {
       this.serverless.variables["azureCredentials"] = authResult.credentials;
       // Use environment variable for sub ID or use the first subscription in the list (service principal can
       // have access to more than one subscription)
-      this.serverless.variables["subscriptionId"] = process.env.azureSubId || authResult.subscriptions[0].id;
+      this.serverless.variables["subscriptionId"] = this.options.subscriptionId || process.env.azureSubId || this.serverless.service.provider["subscriptionId"] || authResult.subscriptions[0].id;
+      this.serverless.cli.log(`Using subscription ID: ${this.serverless.variables["subscriptionId"]}`);
     }
     catch (e) {
       this.log("Error logging into azure");
diff --git a/src/plugins/login/loginHooks.ts b/src/plugins/login/loginHooks.ts
new file mode 100644
index 0000000..4840de9
--- /dev/null
+++ b/src/plugins/login/loginHooks.ts
@@ -0,0 +1,11 @@
+/**
+ * Hooks that require authentication before execution
+ */
+export const loginHooks = [
+  "package:initialize",
+  "deploy:list:list",
+  "deploy:deploy",
+  "invoke:invoke",
+  "rollback:rollback",
+  "remove:remove",
+]
diff --git a/src/plugins/package/azurePackagePlugin.ts b/src/plugins/package/azurePackagePlugin.ts
index abfb7b9..9fe86c3 100644
--- a/src/plugins/package/azurePackagePlugin.ts
+++ b/src/plugins/package/azurePackagePlugin.ts
@@ -22,6 +22,10 @@ export class AzurePackagePlugin extends AzureBasePlugin {
   }
 
   private async setupProviderConfiguration(): Promise<void> {
+    if (this.getOption("package")) {
+      this.log("No need to create bindings. Using pre-existing package");
+      return Promise.resolve();
+    }
     await this.packageService.createBindings();
     this.bindingsCreated = true;
 
@@ -29,6 +33,10 @@ export class AzurePackagePlugin extends AzureBasePlugin {
   }
 
   private async webpack(): Promise<void> {
+    if (this.getOption("package")) {
+      this.log("No need to perform webpack. Using pre-existing package");
+      return Promise.resolve();
+    }
     if (!this.bindingsCreated) {
       await this.setupProviderConfiguration();
     }
@@ -40,7 +48,10 @@ export class AzurePackagePlugin extends AzureBasePlugin {
    * Cleans up generated folders & files after packaging is complete
    */
   private async finalize(): Promise<void> {
+    if (this.getOption("package")) {
+      this.log("No need to clean up generated folders & files. Using pre-existing package");
+      return Promise.resolve();
+    }
     await this.packageService.cleanUp();
   }
 }
-
diff --git a/src/services/apimService.ts b/src/services/apimService.ts
index 6126a70..9257b13 100644
--- a/src/services/apimService.ts
+++ b/src/services/apimService.ts
@@ -10,6 +10,7 @@ import {
 } from "@azure/arm-apimanagement/esm/models";
 import { Site } from "@azure/arm-appservice/esm/models";
 import { Guard } from "../shared/guard";
+import { ApimResource } from "../armTemplates/resources/apim";
 
 /**
  * APIM Service handles deployment and integration with Azure API Management
@@ -28,7 +29,7 @@ export class ApimService extends BaseService {
     }
 
     if (!this.apimConfig.name) {
-      this.apimConfig.name = `${this.config.provider.prefix}-${this.config.provider.region}-${this.config.provider.stage}-apim`
+      this.apimConfig.name = ApimResource.getResourceName(this.config);
     }
 
     if (!this.apimConfig.backend) {
@@ -325,4 +326,4 @@ export class ApimService extends BaseService {
 
     return xml(policy, { indent: "\t" });
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index f7f1e79..58d0d50 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -121,7 +121,7 @@ export abstract class BaseService {
   protected getAccessToken(): string {
     return (this.credentials.tokenCache as any)._entries[0].accessToken;
   }
-  
+
   /**
    * Sends an API request using axios HTTP library
    * @param method The HTTP method
@@ -189,6 +189,10 @@ export abstract class BaseService {
     return "config" in this.options ? this.options["config"] : "serverless.yml";
   }
 
+  protected getOption(key: string, defaultValue?: any) {
+    return Utils.get(this.options, key, defaultValue);
+  }
+
   private setDefaultValues(): void {
     // TODO: Right now the serverless core will always default to AWS default region if the
     // region has not been set in the serverless.yml or CLI options
@@ -231,4 +235,4 @@ export abstract class BaseService {
     }
     return timestamp;
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index 7f8f820..b0eb5cd 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -185,6 +185,17 @@ export class FunctionAppService extends BaseService {
     });
   }
 
+  /**
+   * Gets local path of packaged function app
+   */
+  public getFunctionZipFile(): string {
+    let functionZipFile = this.getOption("package") || this.serverless.service["artifact"];
+    if (!functionZipFile) {
+      functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`);
+    }
+    return functionZipFile;
+  }
+
   /**
    * Uploads artifact file to blob storage container
    */
@@ -198,17 +209,6 @@ export class FunctionAppService extends BaseService {
     );
   }
 
-  /**
-   * Gets local path of packaged function app
-   */
-  private getFunctionZipFile(): string {
-    let functionZipFile = this.serverless.service["artifact"];
-    if (!functionZipFile) {
-      functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`);
-    }
-    return functionZipFile;
-  }
-
   /**
    * Get rollback-configured artifact name. Contains `-t{timestamp}`
    * if rollback is configured
diff --git a/src/services/loginService.ts b/src/services/loginService.ts
index 736671a..94d00b9 100644
--- a/src/services/loginService.ts
+++ b/src/services/loginService.ts
@@ -1,4 +1,5 @@
 import open from "open";
+import Serverless from "serverless";
 import {
   interactiveLoginWithAuthResponse,
   loginWithServicePrincipalSecretWithAuthResponse,
@@ -7,6 +8,10 @@ import {
   InteractiveLoginOptions,  
 } from "@azure/ms-rest-nodeauth";
 
+export interface AzureLoginOptions extends Serverless.Options {
+  subscriptionId?: string;
+}
+
 export class AzureLoginService {
 
   /**
diff --git a/src/services/packageService.ts b/src/services/packageService.ts
index 89127a4..86a8d2d 100644
--- a/src/services/packageService.ts
+++ b/src/services/packageService.ts
@@ -92,4 +92,4 @@ export class PackageService {
 
     return Promise.resolve();
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts
index 4b7dca0..2d75181 100644
--- a/src/services/resourceService.ts
+++ b/src/services/resourceService.ts
@@ -20,6 +20,31 @@ export class ResourceService extends BaseService {
     return await this.resourceClient.deployments.listByResourceGroup(this.resourceGroup);
   }
 
+  /**
+   * Returns stringified list of deployments with timestamps
+   */
+  public async listDeployments(): Promise<string> {
+    const deployments = await this.getDeployments()
+    if (!deployments || deployments.length === 0) {
+      this.log(`No deployments found for resource group '${this.getResourceGroupName()}'`);
+      return;
+    }
+    let stringDeployments = "\n\nDeployments";
+
+    for (const dep of deployments) {
+      stringDeployments += "\n-----------\n"
+      stringDeployments += `Name: ${dep.name}\n`
+      const timestampFromName = Utils.getTimestampFromName(dep.name);
+      stringDeployments += `Timestamp: ${(timestampFromName) ? timestampFromName : "None"}\n`;
+
+      const dateTime = timestampFromName ? new Date(+timestampFromName).toISOString() : "None";
+      stringDeployments += `Datetime: ${dateTime}\n`
+    }
+
+    stringDeployments += "-----------\n"
+    return stringDeployments
+  }
+
   /**
    * Get ARM template for previous deployment
    * @param deploymentName Name of deployment
@@ -45,4 +70,4 @@ export class ResourceService extends BaseService {
     this.log(`Deleting resource group: ${this.resourceGroup}`);
     return await this.resourceClient.resourceGroups.deleteMethod(this.resourceGroup);
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/rollbackService.ts b/src/services/rollbackService.ts
index 0adb0bd..a857693 100644
--- a/src/services/rollbackService.ts
+++ b/src/services/rollbackService.ts
@@ -102,12 +102,14 @@ export class RollbackService extends BaseService {
 
   /**
    * Get deployment specified by timestamp in Serverless options
+   * Lists deployments if no timestamp is provided
    */
   private async getDeployment(): Promise<DeploymentExtended> {
     let timestamp = Utils.get(this.options, "timestamp");
     if (!timestamp) {
-      this.log("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
+      this.log("Need to specify a timestamp for rollback.");
       this.log("Example usage:\n\nsls rollback -t 1562014362");
+      this.log(await this.resourceService.listDeployments());
       return null;
     }
     const deployments = await this.getArmDeploymentsByTimestamp();


Test patch:
diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts
index f54ed11..7b1f3e0 100644
--- a/src/plugins/deploy/azureDeployPlugin.test.ts
+++ b/src/plugins/deploy/azureDeployPlugin.test.ts
@@ -1,23 +1,31 @@
+import { Site } from "@azure/arm-appservice/esm/models";
+import Serverless from "serverless";
+import { ServerlessAzureOptions } from "../../models/serverless";
 import { MockFactory } from "../../test/mockFactory";
 import { invokeHook } from "../../test/utils";
 import { AzureDeployPlugin } from "./azureDeployPlugin";
+import mockFs  from "mock-fs"
 
 jest.mock("../../services/functionAppService");
 import { FunctionAppService } from "../../services/functionAppService";
 
 jest.mock("../../services/resourceService");
 import { ResourceService } from "../../services/resourceService";
-import { Site } from "@azure/arm-appservice/esm/models";
-import { ServerlessAzureOptions } from "../../models/serverless";
-import Serverless from "serverless";
 
 describe("Deploy plugin", () => {
   let sls: Serverless;
   let options: ServerlessAzureOptions;
   let plugin: AzureDeployPlugin;
 
+  beforeAll(() => {
+    mockFs({
+      "serviceName.zip": "contents",
+    }, { createCwd: true, createTmp: true });
+  });
+
   beforeEach(() => {
-    jest.resetAllMocks();
+    FunctionAppService.prototype.getFunctionZipFile = jest.fn(() => "serviceName.zip");
+
     sls = MockFactory.createTestServerless();
     options = MockFactory.createTestServerlessOptions();
 
@@ -26,9 +34,13 @@ describe("Deploy plugin", () => {
 
   afterEach(() => {
     jest.resetAllMocks();
+  });
+
+  afterAll(() => {
+    mockFs.restore();
   })
 
-  it("calls deploy hook", async () => {
+  it("calls deploy", async () => {
     const deployResourceGroup = jest.fn();
     const functionAppStub: Site = MockFactory.createTestSite();
     const deploy = jest.fn(() => Promise.resolve(functionAppStub));
@@ -45,54 +57,11 @@ describe("Deploy plugin", () => {
     expect(uploadFunctions).toBeCalledWith(functionAppStub);
   });
 
-  it("lists deployments with timestamps", async () => {
-    const deployments = MockFactory.createTestDeployments(5, true);
-    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
-
-    await invokeHook(plugin, "deploy:list:list");
-    expect(ResourceService.prototype.getDeployments).toBeCalled();
-
-    let expectedLogStatement = "\n\nDeployments";
-    const originalTimestamp = +MockFactory.createTestTimestamp();
-    let i = 0
-    for (const dep of deployments) {
-      const timestamp = originalTimestamp + i
-      expectedLogStatement += "\n-----------\n"
-      expectedLogStatement += `Name: ${dep.name}\n`
-      expectedLogStatement += `Timestamp: ${timestamp}\n`;
-      expectedLogStatement += `Datetime: ${new Date(timestamp).toISOString()}\n`
-      i++
-    }
-    expectedLogStatement += "-----------\n"
-    expect(sls.cli.log).lastCalledWith(expectedLogStatement);
-  });
-
-  it("lists deployments without timestamps", async () => {
-    const deployments = MockFactory.createTestDeployments();
-    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve(deployments));
-
+  it("lists deployments", async () => {
+    const deploymentString = "deployments";
+    ResourceService.prototype.listDeployments = jest.fn(() => Promise.resolve(deploymentString));
     await invokeHook(plugin, "deploy:list:list");
-    expect(ResourceService.prototype.getDeployments).toBeCalled();
-
-    let expectedLogStatement = "\n\nDeployments";
-    for (const dep of deployments) {
-      expectedLogStatement += "\n-----------\n"
-      expectedLogStatement += `Name: ${dep.name}\n`
-      expectedLogStatement += "Timestamp: None\n";
-      expectedLogStatement += "Datetime: None\n"
-    }
-    expectedLogStatement += "-----------\n"
-    expect(sls.cli.log).lastCalledWith(expectedLogStatement);
-  });
-
-  it("logs empty deployment list", async () => {
-    const resourceGroup = "rg1";
-    ResourceService.prototype.getDeployments = jest.fn(() => Promise.resolve([])) as any;
-    ResourceService.prototype.getResourceGroupName = jest.fn(() => resourceGroup);
-
-    await invokeHook(plugin, "deploy:list:list");
-    expect(ResourceService.prototype.getDeployments).toBeCalled();
-
-    expect(sls.cli.log).lastCalledWith(`No deployments found for resource group '${resourceGroup}'`);
+    expect(ResourceService.prototype.listDeployments).toBeCalled();
+    expect(sls.cli.log).lastCalledWith(deploymentString);
   });
 });
diff --git a/src/plugins/login/azureLoginPlugin.test.ts b/src/plugins/login/azureLoginPlugin.test.ts
index aef2f2d..9e71595 100644
--- a/src/plugins/login/azureLoginPlugin.test.ts
+++ b/src/plugins/login/azureLoginPlugin.test.ts
@@ -3,6 +3,7 @@ import { AzureLoginService } from "../../services/loginService";
 import { MockFactory } from "../../test/mockFactory";
 import { invokeHook, setEnvVariables, unsetEnvVariables } from "../../test/utils";
 import { AzureLoginPlugin } from "./azureLoginPlugin";
+import { loginHooks } from "./loginHooks";
 
 describe("Login Plugin", () => {
 
@@ -10,13 +11,12 @@ describe("Login Plugin", () => {
   const envVariables = MockFactory.createTestServicePrincipalEnvVariables()
   const credentials = MockFactory.createTestVariables().azureCredentials;
 
-  function createPlugin(hasCreds = false, serverless?: Serverless): AzureLoginPlugin {
+  function createPlugin(hasCreds = false, serverless?: Serverless, options?: Serverless.Options): AzureLoginPlugin {
     const sls = serverless || MockFactory.createTestServerless();
     if (!hasCreds) {
       delete sls.variables["azureCredentials"];
     }
-    const options = MockFactory.createTestServerlessOptions();
-    return new AzureLoginPlugin(sls, options);
+    return new AzureLoginPlugin(sls, options || MockFactory.createTestServerlessOptions());
   }
 
   function createMockLoginFunction() {
@@ -31,9 +31,9 @@ describe("Login Plugin", () => {
     unsetEnvVariables(envVariables);
   }
 
-  async function invokeLoginHook(hasCreds = false, serverless?: Serverless) {
-    const plugin = createPlugin(hasCreds, serverless);
-    await invokeHook(plugin, "before:package:initialize");
+  async function invokeLoginHook(hasCreds = false, serverless?: Serverless, options?: Serverless.Options) {
+    const plugin = createPlugin(hasCreds, serverless, options);
+    await invokeHook(plugin, `before:${loginHooks[0]}`);
   }
 
   beforeEach(() => {
@@ -41,6 +41,10 @@ describe("Login Plugin", () => {
     AzureLoginService.servicePrincipalLogin = createMockLoginFunction();
   });
 
+  it("contains the hooks as contained in loginHooks", () => {
+    expect(Object.keys(createPlugin().hooks)).toEqual(loginHooks.map((hook) => `before:${hook}`));
+  });
+
   it("returns if azure credentials are set", async () => {
     await invokeLoginHook(true);
     expect(AzureLoginService.interactiveLogin).not.toBeCalled();
@@ -92,4 +96,33 @@ describe("Login Plugin", () => {
     expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`);
     expect(process.exit).toBeCalledWith(0);
   });
+
+  it("Uses the user specified subscription ID", async () => {
+    const sls = MockFactory.createTestServerless();
+    const opt = MockFactory.createTestServerlessOptions();
+    opt["subscriptionId"] = "test-subs-id";
+    await invokeLoginHook(false, sls, opt);
+    expect(AzureLoginService.interactiveLogin).toBeCalled()
+    expect(sls.variables["subscriptionId"]).toEqual("test-subs-id");
+    expect(sls.cli.log).toBeCalledWith("Using subscription ID: test-subs-id");
+  });
+
+  it("Uses the default subscription ID" , async () => {
+    const sls = MockFactory.createTestServerless();
+    const opt = MockFactory.createTestServerlessOptions();
+    await invokeLoginHook(false, sls, opt);
+    expect(AzureLoginService.interactiveLogin).toBeCalled()
+    expect(sls.variables["subscriptionId"]).toEqual("azureSubId");
+    expect(sls.cli.log).toBeCalledWith("Using subscription ID: azureSubId");
+  });
+  
+  it("Uses the subscription ID specified in serverless yaml", async () => {
+    const sls = MockFactory.createTestServerless();
+    const opt = MockFactory.createTestServerlessOptions();
+    sls.service.provider["subscriptionId"] = "test-subs-id";
+    await invokeLoginHook(false, sls, opt);
+    expect(AzureLoginService.interactiveLogin).toBeCalled()
+    expect(sls.variables["subscriptionId"]).toEqual("test-subs-id");
+    expect(sls.cli.log).toBeCalledWith("Using subscription ID: test-subs-id");
+  });
 });
diff --git a/src/plugins/package/azurePackagePlugin.test.ts b/src/plugins/package/azurePackagePlugin.test.ts
index f40c307..d3d3867 100644
--- a/src/plugins/package/azurePackagePlugin.test.ts
+++ b/src/plugins/package/azurePackagePlugin.test.ts
@@ -39,5 +39,33 @@ describe("Azure Package Plugin", () => {
   it("cleans up package after package:finalize", async () => {
     await invokeHook(plugin, "after:package:finalize");
     expect(PackageService.prototype.cleanUp).toBeCalled();
+  });
+
+  describe("Package specified in options", () => {
+
+    beforeEach(() => {
+      plugin = new AzurePackagePlugin(sls, MockFactory.createTestServerlessOptions({
+        package: "fake.zip",
+      }));
+    });
+
+    it("does not call create bindings if package specified in options", async () => {
+      await invokeHook(plugin, "before:package:setupProviderConfiguration");
+      expect(PackageService.prototype.createBindings).not.toBeCalled();
+      expect(sls.cli.log).lastCalledWith("No need to create bindings. Using pre-existing package");
+    });
+
+    it("does not call webpack if package specified in options", async () => {
+      await invokeHook(plugin, "before:webpack:package:packageModules");
+      expect(PackageService.prototype.createBindings).not.toBeCalled();
+      expect(PackageService.prototype.prepareWebpack).not.toBeCalled();
+      expect(sls.cli.log).lastCalledWith("No need to perform webpack. Using pre-existing package");
+    });
+
+    it("does not call finalize if package specified in options", async () => {
+      await invokeHook(plugin, "after:package:finalize");
+      expect(PackageService.prototype.cleanUp).not.toBeCalled();
+      expect(sls.cli.log).lastCalledWith("No need to clean up generated folders & files. Using pre-existing package");
+    });
   })
 });
diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts
index 028b8ec..50131f2 100644
--- a/src/services/apimService.test.ts
+++ b/src/services/apimService.test.ts
@@ -19,6 +19,7 @@ import {
   OperationContract,
   ApiPolicyCreateOrUpdateResponse,
 } from "@azure/arm-apimanagement/esm/models";
+import { Utils } from "../shared/utils";
 
 describe("APIM Service", () => {
   const apimConfig = MockFactory.createTestApimConfig();
@@ -46,7 +47,6 @@ describe("APIM Service", () => {
       subscriptionId: "ABC123",
     };
   });
-
   it("is defined", () => {
     expect(ApimService).toBeDefined();
   });
@@ -56,6 +56,16 @@ describe("APIM Service", () => {
     expect(service).not.toBeNull();
   });
 
+  it("when generated, has a shorted region name in the resource name", () => {
+    // if APIM name is ommited, it should auto-generate one that contains a shortened region name
+    const apimConfigName = MockFactory.createTestApimConfig(true);
+    (serverless.service.provider as any).apim = apimConfigName;
+
+    const service = new ApimService(serverless);
+    const expectedRegionName = Utils.createShortAzureRegionName(service.getRegion());
+    expect(apimConfigName.name.includes(expectedRegionName)).toBeTruthy();
+  });
+
   describe("Get service reference", () => {
     it("returns null when service doesn't exist", async () => {
       axios.request = jest.fn((requestConfig) => MockFactory.createTestAxiosResponse(requestConfig, apimGetService404, 404));
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index e81cef8..9de1cf7 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -1,6 +1,7 @@
 import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 import mockFs from "mock-fs";
+import path from "path";
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
 import { FunctionAppService } from "./functionAppService";
@@ -55,6 +56,9 @@ describe("Function App Service", () => {
 
     mockFs({
       "app.zip": "contents",
+      ".serverless": {
+        "serviceName.zip": "contents",
+      }
     }, { createCwd: true, createTmp: true });
   });
 
@@ -222,6 +226,37 @@ describe("Function App Service", () => {
     expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/)
   });
 
+  it("uploads functions to function app and blob storage with default naming convention", async () => {
+    const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net"));
+    const expectedUploadUrl = `https://${scmDomain}/api/zipdeploy/`;
+
+    const sls = MockFactory.createTestServerless();
+    delete sls.service["artifact"]
+    const service = createService(sls);
+    await service.uploadFunctions(app);
+
+    const defaultArtifact = path.join(".serverless", `${sls.service.getServiceName()}.zip`);
+
+    expect((FunctionAppService.prototype as any).sendFile).toBeCalledWith({
+      method: "POST",
+      uri: expectedUploadUrl,
+      json: true,
+      headers: {
+        Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
+        Accept: "*/*",
+        ContentType: "application/octet-stream",
+      }
+    }, defaultArtifact);
+    const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact");
+    expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith(
+      defaultArtifact,
+      configConstants.deploymentConfig.container,
+      `${expectedArtifactName}.zip`,
+    )
+    const uploadCall = ((AzureBlobStorageService.prototype as any).uploadFile).mock.calls[0];
+    expect(uploadCall[2]).toMatch(/.*-t([0-9]+)/)
+  });
+
   it("uploads functions with custom SCM domain (aka App service environments)", async () => {
     const customApp = {
       ...MockFactory.createTestSite("CustomAppWithinASE"),
@@ -248,10 +283,19 @@ describe("Function App Service", () => {
     }, slsService["artifact"])
   });
 
-  it("throws an error with no zip file", async () => {
+  it("uses default name when no artifact provided", async () => {
     const sls = MockFactory.createTestServerless();
     delete sls.service["artifact"];
     const service = createService(sls);
-    await expect(service.uploadFunctions(app)).rejects.not.toBeNull()
+    expect(service.getFunctionZipFile()).toEqual(path.join(".serverless", `${sls.service.getServiceName()}.zip`))
+  });
+
+  it("uses package param from options if provided", async () => {
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions({
+      package: "fake.zip",
+    });
+    const service = createService(sls, options);
+    expect(service.getFunctionZipFile()).toEqual("fake.zip")
   });
 });
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index bd22314..c78df52 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -1,16 +1,17 @@
+import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
+import { Utils } from "../shared/utils";
 import { MockFactory } from "../test/mockFactory";
 import { ResourceService } from "./resourceService";
 
-
 jest.mock("@azure/arm-resources")
 import { ResourceManagementClient } from "@azure/arm-resources";
-import { Utils } from "../shared/utils";
 
 describe("Resource Service", () => {
-  const deployments = MockFactory.createTestDeployments();
+  let deployments: DeploymentsListByResourceGroupResponse;
   const template = "myTemplate";
 
-  beforeAll(() => {
+  beforeEach(() => {
+    deployments = MockFactory.createTestDeployments(5, true);
     ResourceManagementClient.prototype.resourceGroups = {
       createOrUpdate: jest.fn(),
       deleteMethod: jest.fn(),
@@ -80,7 +81,7 @@ describe("Resource Service", () => {
       .toBeCalledWith(resourceGroup);
   });
 
-  it("lists deployments", async () => {
+  it("gets deployments", async () => {
     const sls = MockFactory.createTestServerless();
     const resourceGroup = "myResourceGroup";
     sls.service.provider["resourceGroup"] = resourceGroup
@@ -91,6 +92,53 @@ describe("Resource Service", () => {
     expect(deps).toEqual(deployments);
   });
 
+  it("lists deployments as string with timestamps", async () => {
+    const sls = MockFactory.createTestServerless();
+    const resourceGroup = "myResourceGroup";
+    sls.service.provider["resourceGroup"] = resourceGroup
+    sls.variables["azureCredentials"] = "fake credentials"
+    const options = MockFactory.createTestServerlessOptions();
+    const service = new ResourceService(sls, options);
+    const deploymentString = await service.listDeployments();
+    let expectedDeploymentString = "\n\nDeployments";
+    const originalTimestamp = +MockFactory.createTestTimestamp();
+    let i = 0
+    for (const dep of deployments) {
+      const timestamp = originalTimestamp + i
+      expectedDeploymentString += "\n-----------\n"
+      expectedDeploymentString += `Name: ${dep.name}\n`
+      expectedDeploymentString += `Timestamp: ${timestamp}\n`;
+      expectedDeploymentString += `Datetime: ${new Date(timestamp).toISOString()}\n`
+      i++
+    }
+    expectedDeploymentString += "-----------\n"
+    expect(deploymentString).toEqual(expectedDeploymentString);
+  });
+
+  it("lists deployments as string without timestamps", async () => {
+    deployments = MockFactory.createTestDeployments();
+    ResourceManagementClient.prototype.deployments = {
+      listByResourceGroup: jest.fn(() => Promise.resolve(deployments)),
+    } as any;
+
+    const sls = MockFactory.createTestServerless();
+    const resourceGroup = "myResourceGroup";
+    sls.service.provider["resourceGroup"] = resourceGroup
+    sls.variables["azureCredentials"] = "fake credentials"
+    const options = MockFactory.createTestServerlessOptions();
+    const service = new ResourceService(sls, options);
+    const deploymentString = await service.listDeployments();
+    let expectedDeploymentString = "\n\nDeployments";
+    for (const dep of deployments) {
+      expectedDeploymentString += "\n-----------\n"
+      expectedDeploymentString += `Name: ${dep.name}\n`
+      expectedDeploymentString += "Timestamp: None\n";
+      expectedDeploymentString += "Datetime: None\n"
+    }
+    expectedDeploymentString += "-----------\n"
+    expect(deploymentString).toEqual(expectedDeploymentString);
+  });
+
   it("gets deployment template",async () => {
     const sls = MockFactory.createTestServerless();
     const resourceGroup = "myResourceGroup";
@@ -107,4 +155,4 @@ describe("Resource Service", () => {
       );
     expect(result).toEqual(template);
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts
index 2b0e51d..547bbe2 100644
--- a/src/services/rollbackService.test.ts
+++ b/src/services/rollbackService.test.ts
@@ -31,6 +31,7 @@ describe("Rollback Service", () => {
   const artifactName = MockFactory.createTestDeployment().name.replace("deployment", "artifact") + ".zip";
   const artifactPath = `.serverless${path.sep}${artifactName}`
   const armDeployment: ArmDeployment = { template, parameters };
+  const deploymentString = "deployments";
 
   function createOptions(timestamp?: string): Serverless.Options {
     return {
@@ -58,6 +59,7 @@ describe("Rollback Service", () => {
     ResourceService.prototype.getDeploymentTemplate = jest.fn(
       () => Promise.resolve({ template })
     ) as any;
+    ResourceService.prototype.listDeployments = jest.fn(() => Promise.resolve(deploymentString))
     AzureBlobStorageService.prototype.generateBlobSasTokenUrl = jest.fn(() => sasURL) as any;
     FunctionAppService.prototype.get = jest.fn(() => appStub) as any;
   });
@@ -72,8 +74,7 @@ describe("Rollback Service", () => {
     const options = {} as any;
     const service = createService(sls, options);
     await service.rollback();
-    const calls = (sls.cli.log as any).mock.calls;
-    expect(calls[0][0]).toEqual("Need to specify a timestamp for rollback. Run `sls deploy list` to see timestamps of deployments");
+    expect(sls.cli.log).lastCalledWith(deploymentString);
   });
 
   it("should return early with invalid timestamp", async () => {
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 485b689..2d48e2f 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -65,7 +65,7 @@ export class MockFactory {
     sls.service = MockFactory.createTestService(sls.service["functions"]);
   }
 
-  public static createTestServerlessOptions(): Serverless.Options {
+  public static createTestServerlessOptions(options?: any): Serverless.Options {
     return {
       extraServicePath: null,
       function: null,
@@ -73,6 +73,7 @@ export class MockFactory {
       region: null,
       stage: null,
       watch: null,
+      ...options
     };
   }
 
@@ -266,9 +267,9 @@ export class MockFactory {
     return (asYaml) ? yaml.dump(data) : data;
   }
 
-  public static createTestApimConfig(): ApiManagementConfig {
+  public static createTestApimConfig(generateName: boolean = false): ApiManagementConfig {
     return {
-      name: "test-apim-resource",
+      name: generateName ? null : "test-apim-resource",
       api: {
         name: "test-apim-api1",
         subscriptionRequired: false,