Repository: serverless/serverless-azure-functions
Pull request #185 (2019-06-28 20:24:49)

feat: Updates to deployment process to enable rollback (Part 2)
## High Level Overview
- Uploads a timestamped archive of code to Azure Blob Storage
- Adds same timestamp to ARM deployments to enable linking
- **Does not set `WEBSITE_RUN_FROM_PACKAGE` to use package directly from blob storage**
  - Currently just used as an archive. Initially, thought to update the `WEBSITE_RUN_FROM_PACKAGE` setting with the URL for the blob (hence the implementation of the SAS URL generator), but discovered [issues with cold start when using that methodology to deploy to Windows](https://docs.microsoft.com/en-us/azure/azure-functions/run-functions-from-deployment-package#enabling-functions-to-run-from-a-package).
  - Future `rollback` implementation will download the appropriate archive and re-deploy it to the Function App.
  - This may (and probably will) change as the Azure Functions Premium plan is released on Linux or cold start issues are mitigated

## Details

- Adds dependency on the `@azure/arm-storage` package
- Sets `rollbackEnabled` as default
- Updates broken tests because of timestamped deployment name
- Add tests for blob storage operations
- Allows user to still specify deployment name with rollback

Resolves [AB#388], [AB#358], [AB#414] and [AB#415]


Patch:
diff --git a/package-lock.json b/package-lock.json
index d8bbd4d..e4fb69a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,6 +34,16 @@
         "tslib": "^1.9.3"
       }
     },
+    "@azure/arm-storage": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/@azure/arm-storage/-/arm-storage-9.0.1.tgz",
+      "integrity": "sha512-cMswGdhbxrct87+lFDqzlezQDXzLGBj79aMEyF1sjJ2HnuwJtEEFA8Zfjg/KbHiT7vkFAJYDQgtB4Fu1joEkrg==",
+      "requires": {
+        "@azure/ms-rest-azure-js": "^1.3.2",
+        "@azure/ms-rest-js": "^1.8.1",
+        "tslib": "^1.9.3"
+      }
+    },
     "@azure/ms-rest-azure-env": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-1.1.2.tgz",
diff --git a/package.json b/package.json
index c95c386..2d65b3f 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
     "@azure/arm-apimanagement": "^5.1.0",
     "@azure/arm-appservice": "^5.7.0",
     "@azure/arm-resources": "^1.0.1",
+    "@azure/arm-storage": "^9.0.1",
     "@azure/ms-rest-nodeauth": "^1.0.1",
     "@azure/storage-blob": "^10.3.0",
     "axios": "^0.18.0",
diff --git a/src/config.ts b/src/config.ts
index 2f08214..f24db2d 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -11,7 +11,7 @@ export const configConstants = {
   logStreamApiPath: "/api/logstream/application/functions/function/",
   masterKeyApiPath: "/api/functions/admin/masterkey",
   providerName: "azure",
-  rollbackEnabled: false,
+  rollbackEnabled: true,
   scmCommandApiPath: "/api/command",
   scmDomain: ".scm.azurewebsites.net",
   scmVfsPath: "/api/vfs/site/wwwroot/",
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index 00b1941..40362ed 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -52,7 +52,6 @@ export class AzureDeployPlugin {
     await resourceService.deployResourceGroup();
 
     const functionAppService = new FunctionAppService(this.serverless, this.options);
-    await functionAppService.initialize();
 
     const functionApp = await functionAppService.deploy();
     await functionAppService.uploadFunctions(functionApp);
diff --git a/src/services/azureBlobStorageService.ts b/src/services/azureBlobStorageService.ts
index 8726dc0..6764d2b 100644
--- a/src/services/azureBlobStorageService.ts
+++ b/src/services/azureBlobStorageService.ts
@@ -1,9 +1,16 @@
-import { Aborter, BlockBlobURL, ContainerURL, Credential, ServiceURL, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob";
+import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage";
+import { Aborter, BlobSASPermissions, BlockBlobURL, ContainerURL, generateBlobSASQueryParameters,SASProtocol,
+  ServiceURL, SharedKeyCredential, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob";
 import Serverless from "serverless";
 import { Guard } from "../shared/guard";
 import { BaseService } from "./baseService";
 import { AzureLoginService } from "./loginService";
 
+export enum AzureStorageAuthType {
+  SharedKey,
+  Token
+}
+
 /**
  * Wrapper for operations on Azure Blob Storage account
  */
@@ -13,15 +20,26 @@ export class AzureBlobStorageService extends BaseService {
    * Account URL for Azure Blob Storage account. Depends on `storageAccountName` being set in baseService
    */
   private accountUrl: string;
-  private storageCredential: Credential;
+  private authType: AzureStorageAuthType;
+  private storageCredential: SharedKeyCredential|TokenCredential;
 
-  public constructor(serverless: Serverless, options: Serverless.Options) {
+  public constructor(serverless: Serverless, options: Serverless.Options,
+    authType: AzureStorageAuthType = AzureStorageAuthType.SharedKey) {
     super(serverless, options);
     this.accountUrl = `https://${this.storageAccountName}.blob.core.windows.net`;
+    this.authType = authType;
   }
 
+  /**
+   * Initialize Blob Storage service. This creates the credentials required
+   * to perform any operation with the service
+   */
   public async initialize() {
-    this.storageCredential = new TokenCredential(await this.getToken());
+    this.storageCredential = (this.authType === AzureStorageAuthType.SharedKey) 
+      ?
+      new SharedKeyCredential(this.storageAccountName, await this.getKey())
+      :
+      new TokenCredential(await this.getToken());
   }
 
   /**
@@ -31,11 +49,15 @@ export class AzureBlobStorageService extends BaseService {
    * @param blobName Name of blob file created as a result of upload
    */
   public async uploadFile(path: string, containerName: string, blobName?: string) {
-    Guard.empty(path);
-    Guard.empty(containerName);
+    Guard.empty(path, "path");
+    Guard.empty(containerName, "containerName");
+    this.checkInitialization();
+
     // Use specified blob name or replace `/` in path with `-`
     const name = blobName || path.replace(/^.*[\\\/]/, "-");
-    uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
+    this.log(`Uploading file at '${path}' to container '${containerName}' with name '${name}'`)
+    await uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
+    this.log("Finished uploading blob");
   };
   
   /**
@@ -44,8 +66,10 @@ export class AzureBlobStorageService extends BaseService {
    * @param blobName Blob to delete
    */
   public async deleteFile(containerName: string, blobName: string): Promise<void> {
-    Guard.empty(containerName);
-    Guard.empty(blobName);
+    Guard.empty(containerName, "containerName");
+    Guard.empty(blobName, "blobName");
+    this.checkInitialization();
+
     const blockBlobUrl = await this.getBlockBlobURL(containerName, blobName)
     await blockBlobUrl.delete(Aborter.none);
   }
@@ -57,6 +81,8 @@ export class AzureBlobStorageService extends BaseService {
    */
   public async listFiles(containerName: string, ext?: string): Promise<string[]> {
     Guard.empty(containerName, "containerName");
+    this.checkInitialization();
+
     const result: string[] = [];
     let marker;
     const containerURL = this.getContainerURL(containerName);
@@ -80,6 +106,8 @@ export class AzureBlobStorageService extends BaseService {
    * Lists the containers within the Azure Blob Storage account
    */
   public async listContainers() {
+    this.checkInitialization();
+
     const result: string[] = [];
     let marker;
     do {
@@ -100,10 +128,15 @@ export class AzureBlobStorageService extends BaseService {
    * Creates container specified in Azure Cloud Storage options
    * @param containerName - Name of container to create
    */
-  public async createContainer(containerName: string): Promise<void> {
-    Guard.empty(containerName);
-    const containerURL = this.getContainerURL(containerName);
-    await containerURL.create(Aborter.none);
+  public async createContainerIfNotExists(containerName: string): Promise<void> {
+    Guard.empty(containerName, "containerName");
+    this.checkInitialization();
+
+    const containers = await this.listContainers();
+    if (!containers.find((name) => name === containerName)) {
+      const containerURL = this.getContainerURL(containerName);
+      await containerURL.create(Aborter.none);
+    }
   }
 
   /**
@@ -111,15 +144,57 @@ export class AzureBlobStorageService extends BaseService {
    * @param containerName Name of container to delete
    */
   public async deleteContainer(containerName: string): Promise<void> {
-    Guard.empty(containerName);
+    Guard.empty(containerName, "containerName");
+    this.checkInitialization();
+
     const containerUrl = await this.getContainerURL(containerName)
     await containerUrl.delete(Aborter.none);
   }
 
+  /**
+   * Generate URL with SAS token for a specific blob
+   * @param containerName Name of container containing blob
+   * @param blobName Name of blob to generate SAS token for
+   * @param days Number of days from current date until expiry of SAS token. Defaults to 1 year
+   */
+  public async generateBlobSasTokenUrl(containerName: string, blobName: string, days: number = 365): Promise<string> {
+    this.checkInitialization();
+    if (this.authType !== AzureStorageAuthType.SharedKey) {
+      throw new Error("Need to authenticate with shared key in order to generate SAS tokens. " + 
+      "Initialize Blob Service with SharedKey auth type");
+    }
+
+    const now = new Date();
+    const endDate = new Date(now);
+    endDate.setDate(endDate.getDate() + days);
+
+    const blobSas = generateBlobSASQueryParameters({
+      blobName,
+      cacheControl: "cache-control-override",
+      containerName,
+      contentDisposition: "content-disposition-override",
+      contentEncoding: "content-encoding-override",
+      contentLanguage: "content-language-override",
+      contentType: "content-type-override",
+      expiryTime: endDate,
+      ipRange: { start: "0.0.0.0", end: "255.255.255.255" },
+      permissions: BlobSASPermissions.parse("racwd").toString(),
+      protocol: SASProtocol.HTTPSandHTTP,
+      startTime: now,
+      version: "2016-05-31"
+    },
+    this.storageCredential as SharedKeyCredential);
+    
+    const blobUrl = this.getBlockBlobURL(containerName, blobName);
+    return `${blobUrl.url}?${blobSas}`
+  }
+
   /**
    * Get ServiceURL object for Azure Blob Storage Account
    */
   private getServiceURL(): ServiceURL {
+    this.checkInitialization();
+
     const pipeline = StorageURL.newPipeline(this.storageCredential);
     const accountUrl = this.accountUrl;
     const serviceUrl = new ServiceURL(
@@ -135,7 +210,9 @@ export class AzureBlobStorageService extends BaseService {
    * @param serviceURL Previously created ServiceURL object (will create if undefined)
    */
   private getContainerURL(containerName: string): ContainerURL {
-    Guard.empty(containerName);
+    Guard.empty(containerName, "containerName");
+    this.checkInitialization();
+
     return ContainerURL.fromServiceURL(
       this.getServiceURL(),
       containerName
@@ -148,14 +225,19 @@ export class AzureBlobStorageService extends BaseService {
    * @param blobName Name of blob
    */
   private getBlockBlobURL(containerName: string, blobName: string): BlockBlobURL {
-    Guard.empty(containerName);
-    Guard.empty(blobName);
+    Guard.empty(containerName, "containerName");
+    Guard.empty(blobName, "blobName");
+    this.checkInitialization();
+
     return BlockBlobURL.fromContainerURL(
       this.getContainerURL(containerName),
       blobName,
     );
   }
 
+  /**
+   * Get access token by logging in (again) with a storage-specific context
+   */
   private async getToken(): Promise<string> {
     const authResponse = await AzureLoginService.login({
       tokenAudience: "https://storage.azure.com/"
@@ -163,4 +245,24 @@ export class AzureBlobStorageService extends BaseService {
     const token = await authResponse.credentials.getToken();
     return token.accessToken;
   }
+
+  /**
+   * Get access key for storage account
+   */
+  private async getKey(): Promise<string> {
+    const context = new StorageManagementClientContext(this.credentials, this.subscriptionId)
+    const storageAccounts = new StorageAccounts(context);
+    const keys = await storageAccounts.listKeys(this.resourceGroup, this.storageAccountName);
+    return keys.keys[0].value;
+  }
+
+  /**
+   * Ensure that the blob storage service has been initialized. If not initialized,
+   * the credentials will not be available for any operation
+   */
+  private checkInitialization() {
+    Guard.null(this.storageCredential, "storageCredential", 
+      "Azure Blob Storage Service has not been initialized. Make sure .initialize() has been called " + 
+      "before performing any operation");
+  }
 }
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 642da6b..b99a5ce 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -16,7 +16,6 @@ export abstract class BaseService {
   protected resourceGroup: string;
   protected deploymentName: string;
   protected deploymentConfig: DeploymentConfig
-  protected deploymentContainerName: string;
   protected storageAccountName: string;
   protected config: ServerlessAzureConfig;
 
@@ -60,12 +59,6 @@ export abstract class BaseService {
 
   public getDeploymentConfig(): DeploymentConfig {
     const providedConfig = this.serverless["deploy"] as DeploymentConfig;
-    const providedDepName = this.serverless.service.provider["deploymentName"];
-    if (providedConfig && providedDepName && providedConfig.rollback) {
-      throw new Error("Cannot both specify a deployment name and enable rollback. " +
-      "In order for rollback to work, the name of deployment must follow the generated " +
-      "naming convention")
-    }
     const config = providedConfig || {
       rollback: configConstants.rollbackEnabled,
       container: configConstants.deploymentArtifactContainer,
@@ -82,14 +75,6 @@ export abstract class BaseService {
     return this.serverless.service["service"];
   }
 
-  /**
-   * Get rollback-configured artifact name. Contains `-t{timestamp}`
-   * if rollback is configured
-   */
-  public getArtifactName(): string {
-    return this.rollbackConfiguredName(this.getServiceName());
-  }
-
   /**
    * Get the access token from credentials token cache
    */
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index eb1a717..ab5e69b 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -13,10 +13,14 @@ import { BaseService } from "./baseService";
 
 export class FunctionAppService extends BaseService {
   private webClient: WebSiteManagementClient;
+  private blobService: AzureBlobStorageService;
+  private functionZipFile: string;
 
   public constructor(serverless: Serverless, options: Serverless.Options) {
     super(serverless, options);
     this.webClient = new WebSiteManagementClient(this.credentials, this.subscriptionId);
+    this.blobService = new AzureBlobStorageService(serverless, options);
+    this.functionZipFile = this.getFunctionZipFile();
   }
 
   public async get(): Promise<Site> {
@@ -43,22 +47,11 @@ export class FunctionAppService extends BaseService {
     return response.data.value;
   }
 
-  /**
-   * Initialize deployment artifact container if rollback is specified
-   */
-  public async initialize() {
-    if (this.deploymentConfig.rollback) {
-      const blobService = new AzureBlobStorageService(this.serverless, this.options);
-      await blobService.initialize();
-      blobService.createContainer(this.deploymentConfig.container);
-    }
-  }
-
   public async deleteFunction(functionApp: Site, functionName: string) {
     Guard.null(functionApp);
     Guard.empty(functionName);
 
-    this.serverless.cli.log(`-> Deleting function: ${functionName}`);
+    this.log(`-> Deleting function: ${functionName}`);
     const deleteFunctionUrl = `${this.baseUrl}${functionApp.id}/functions/${functionName}?api-version=2016-08-01`;
 
     return await this.sendApiRequest("DELETE", deleteFunctionUrl);
@@ -67,7 +60,7 @@ export class FunctionAppService extends BaseService {
   public async syncTriggers(functionApp: Site) {
     Guard.null(functionApp);
 
-    this.serverless.cli.log("Syncing function triggers");
+    this.log("Syncing function triggers");
 
     const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`;
     return await this.sendApiRequest("POST", syncTriggersUrl);
@@ -76,7 +69,7 @@ export class FunctionAppService extends BaseService {
   public async cleanUp(functionApp: Site) {
     Guard.null(functionApp);
 
-    this.serverless.cli.log("Cleaning up existing functions");
+    this.log("Cleaning up existing functions");
     const deleteTasks = [];
 
     const serviceFunctions = this.serverless.service.getAllFunctions();
@@ -123,7 +116,8 @@ export class FunctionAppService extends BaseService {
 
     this.log("Deploying serverless functions...");    
 
-    await this.zipDeploy(functionApp);
+    await this.uploadZippedArfifactToFunctionApp(functionApp);
+    await this.uploadZippedArtifactToBlobStorage();
   }
 
   /**
@@ -144,22 +138,16 @@ export class FunctionAppService extends BaseService {
     return await this.get();
   }
 
-  private async zipDeploy(functionApp) {
+  private async uploadZippedArfifactToFunctionApp(functionApp) {
     const scmDomain = this.getScmDomain(functionApp);
 
-    this.serverless.cli.log(`Deploying zip file to function app: ${functionApp.name}`);
+    this.log(`Deploying zip file to function app: ${functionApp.name}`);
 
-    // Upload function artifact if it exists, otherwise the full service is handled in 'uploadFunctions' method
-    let functionZipFile = this.serverless.service["artifact"];
-    if (!functionZipFile) {
-      functionZipFile = path.join(this.serverless.config.servicePath, ".serverless", `${this.serverless.service.getServiceName()}.zip`);
-    }
-
-    if (!(functionZipFile && fs.existsSync(functionZipFile))) {
+    if (!(this.functionZipFile && fs.existsSync(this.functionZipFile))) {
       throw new Error("No zip file found for function app");
     }
 
-    this.serverless.cli.log(`-> Deploying service package @ ${functionZipFile}`);
+    this.log(`-> Deploying service package @ ${this.functionZipFile}`);
 
     // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url
     const requestOptions = {
@@ -173,12 +161,13 @@ export class FunctionAppService extends BaseService {
       }
     };
 
-    await this.sendFile(requestOptions, functionZipFile);
-    this.serverless.cli.log("-> Function package uploaded successfully");
+    await this.sendFile(requestOptions, this.functionZipFile);
+    
+    this.log("-> Function package uploaded successfully");
     const serverlessFunctions = this.serverless.service.getAllFunctions();
     const deployedFunctions = await this.listFunctions(functionApp);
 
-    this.serverless.cli.log("Deployed serverless functions:")
+    this.log("Deployed serverless functions:")
     deployedFunctions.forEach((functionConfig) => {
       // List functions that are part of the serverless yaml config
       if (serverlessFunctions.includes(functionConfig.name)) {
@@ -186,12 +175,44 @@ export class FunctionAppService extends BaseService {
 
         if (httpConfig) {
           const method = httpConfig.methods[0].toUpperCase();
-          this.serverless.cli.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
+          this.log(`-> ${functionConfig.name}: ${method} ${httpConfig.url}`);
         }
       }
     });
   }
 
+  /**
+   * Uploads artifact file to blob storage container 
+   */
+  private async uploadZippedArtifactToBlobStorage() {
+    await this.blobService.initialize();
+    await this.blobService.createContainerIfNotExists(this.deploymentConfig.container);
+    await this.blobService.uploadFile(
+      this.functionZipFile,
+      this.deploymentConfig.container,
+      this.getArtifactName(this.deploymentName),
+    );
+  }
+
+  /**
+   * 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
+   */
+  public getArtifactName(deploymentName: string): string {
+    return `${deploymentName.replace("rg-deployment", "artifact")}.zip`;
+  }
+
   private getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
     const httpTrigger = functionConfig.config.bindings.find((binding) => {
       return binding.type === "httpTrigger";


Test patch:
diff --git a/src/services/armService.test.ts b/src/services/armService.test.ts
index b61c48c..5ff5cac 100644
--- a/src/services/armService.test.ts
+++ b/src/services/armService.test.ts
@@ -182,6 +182,7 @@ describe("Arm Service", () => {
 
       const expectedResourceGroup = sls.service.provider["resourceGroup"];
       const expectedDeploymentName = sls.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`;
+      const expectedDeploymentNameRegex = new RegExp(expectedDeploymentName + "-t([0-9]+)")
       const expectedDeployment: Deployment = {
         properties: {
           mode: "Incremental",
@@ -190,7 +191,10 @@ describe("Arm Service", () => {
         },
       };
 
-      expect(Deployments.prototype.createOrUpdate).toBeCalledWith(expectedResourceGroup, expectedDeploymentName, expectedDeployment);
+      const call = (Deployments.prototype.createOrUpdate as any).mock.calls[0];
+      expect(call[0]).toEqual(expectedResourceGroup);
+      expect(call[1]).toMatch(expectedDeploymentNameRegex);
+      expect(call[2]).toEqual(expectedDeployment);
     });
   });
 });
\ No newline at end of file
diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts
index e8d10a2..d4d1a10 100644
--- a/src/services/azureBlobStorageService.test.ts
+++ b/src/services/azureBlobStorageService.test.ts
@@ -1,10 +1,14 @@
 import mockFs from "mock-fs";
 import { MockFactory } from "../test/mockFactory";
-import { AzureBlobStorageService } from "./azureBlobStorageService";
+import { AzureBlobStorageService, AzureStorageAuthType } from "./azureBlobStorageService";
 
 jest.mock("@azure/storage-blob");
 jest.genMockFromModule("@azure/storage-blob")
-import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential } from "@azure/storage-blob";
+import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential, SharedKeyCredential } from "@azure/storage-blob";
+
+jest.mock("@azure/arm-storage")
+jest.genMockFromModule("@azure/arm-storage");
+import { StorageAccounts, StorageManagementClientContext } from "@azure/arm-storage"
 
 jest.mock("./loginService");
 import { AzureLoginService } from "./loginService"
@@ -18,16 +22,27 @@ describe("Azure Blob Storage Service", () => {
 
   const containers = MockFactory.createTestAzureContainers();
   const sls = MockFactory.createTestServerless();
+  const accountName = "slswesdevservicenamesa";
   const options = MockFactory.createTestServerlessOptions();
   const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath);
 
   let service: AzureBlobStorageService;
   const token = "myToken";
+  const keyValue = "keyValue";
 
   beforeAll(() => {
-    (TokenCredential as any).mockImplementation((token: string) => {
-      token
-    });
+    (SharedKeyCredential as any).mockImplementation();
+    (TokenCredential as any).mockImplementation();
+
+    StorageAccounts.prototype.listKeys = jest.fn(() => {
+      return {
+        keys: [
+          {
+            value: keyValue
+          }
+        ]
+      }
+    }) as any;
 
     BlockBlobURL.fromContainerURL = jest.fn(() => blockBlobUrl) as any;
     AzureLoginService.login = jest.fn(() => Promise.resolve({
@@ -51,8 +66,20 @@ describe("Azure Blob Storage Service", () => {
     mockFs.restore();
   });
 
-  beforeEach(() => {
+  beforeEach( async () => {
     service = new AzureBlobStorageService(sls, options);
+    await service.initialize();
+  });
+
+  it("should initialize authentication", async () => {
+    // Note: initialize called in beforeEach
+    expect(SharedKeyCredential).toBeCalledWith(accountName, keyValue);
+    expect(StorageManagementClientContext).toBeCalled();
+    expect(StorageAccounts).toBeCalled();
+
+    const tokenService = new AzureBlobStorageService(sls, options, AzureStorageAuthType.Token);
+    await tokenService.initialize();
+    expect(TokenCredential).toBeCalled();
   });
 
   it("should initialize authentication", async () => {
@@ -61,7 +88,7 @@ describe("Azure Blob Storage Service", () => {
   });
 
   it("should upload a file", async () => {
-    uploadFileToBlockBlob.prototype = jest.fn();
+    uploadFileToBlockBlob.prototype = jest.fn(() => Promise.resolve());
     ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any));
     await service.uploadFile(filePath, containerName);
     expect(uploadFileToBlockBlob).toBeCalledWith(
@@ -99,7 +126,7 @@ describe("Azure Blob Storage Service", () => {
     ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));
     ContainerURL.prototype.create = jest.fn(() => Promise.resolve({ statusCode: 201 })) as any;
     const newContainerName = "newContainer";
-    await service.createContainer(newContainerName);
+    await service.createContainerIfNotExists(newContainerName);
     expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), newContainerName);
     expect(ContainerURL.prototype.create).toBeCalledWith(Aborter.none);
   });
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index f4c83da..8f637e1 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -69,7 +69,8 @@ describe("Base Service", () => {
     expect(props.subscriptionId).toEqual(sls.variables["subscriptionId"]);
     expect(props.serviceName).toEqual(slsConfig.service);
     expect(props.resourceGroup).toEqual(slsConfig.provider.resourceGroup);
-    expect(props.deploymentName).toEqual(slsConfig.provider.deploymentName);
+    const expectedDeploymentNameRegex = new RegExp(slsConfig.provider.deploymentName + "-t([0-9]+)")
+    expect(props.deploymentName).toMatch(expectedDeploymentNameRegex);
   });
 
   it("Sets default region and stage values if not defined", () => {
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index e6def2c..f923c6d 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -10,7 +10,12 @@ import { FunctionAppResource } from "../armTemplates/resources/functionApp";
 jest.mock("@azure/arm-appservice")
 import { WebSiteManagementClient } from "@azure/arm-appservice";
 import { ArmDeployment, ArmTemplateType } from "../models/armTemplates";
-jest.mock("@azure/arm-resources")
+jest.mock("@azure/arm-resources");
+
+jest.mock("./azureBlobStorageService");
+import { AzureBlobStorageService } from "./azureBlobStorageService"
+import configConstants from "../config";
+
 
 describe("Function App Service", () => {
   const app = MockFactory.createTestSite();
@@ -190,7 +195,7 @@ describe("Function App Service", () => {
     });
   });
 
-  it("uploads functions", async () => {
+  it("uploads functions to function app and blob storage", async () => {
     const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net"));
     const expectedUploadUrl = `https://${scmDomain}/api/zipdeploy/`;
 
@@ -206,7 +211,15 @@ describe("Function App Service", () => {
         Accept: "*/*",
         ContentType: "application/octet-stream",
       }
-    }, slsService["artifact"])
+    }, slsService["artifact"]);
+    const expectedArtifactName = service.getDeploymentName().replace("rg-deployment", "artifact");
+    expect((AzureBlobStorageService.prototype as any).uploadFile).toBeCalledWith(
+      slsService["artifact"],
+      configConstants.deploymentArtifactContainer,
+      `${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 () => {
diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts
index 22ac5ad..f374306 100644
--- a/src/services/resourceService.test.ts
+++ b/src/services/resourceService.test.ts
@@ -57,8 +57,10 @@ describe("Resource Service", () => {
     const options = MockFactory.createTestServerlessOptions();
     const service = new ResourceService(sls, options);
     service.deleteDeployment();
-    expect(ResourceManagementClient.prototype.deployments.deleteMethod)
-      .toBeCalledWith(resourceGroup, deploymentName);
+    const call = (ResourceManagementClient.prototype.deployments.deleteMethod as any).mock.calls[0];
+    expect(call[0]).toEqual(resourceGroup);
+    const expectedDeploymentNameRegex = new RegExp(deploymentName + "-t([0-9]+)")
+    expect(call[1]).toMatch(expectedDeploymentNameRegex)
   });
 
   it("deletes a resource group", () => {