Repository: serverless/serverless-azure-functions
Pull request #170 (2019-06-19 17:09:24)

docs: Design documents for rollback functionality

tbarlow12: ![image](https://user-images.githubusercontent.com/10962815/59798827-e15a0180-9297-11e9-9ab0-23b728486780.png)

tbarlow12: ![image](https://user-images.githubusercontent.com/10962815/59799307-e8cdda80-9298-11e9-95b4-e7c08ffe95cc.png)

pjlittle: I like this approach and it's about the closest we'll get to the AWS experience today.  That being said, I'm still hung up on leveraging source control and CI/CD to effectively control going forwards or backwards with changes. Still, I see the value and elegance in using blob effectively as an 'artifactory' of releases.

I almost want to suggest having a param in the serverless yaml that would toggle between zip deploy and run from blob, like @mydiemho suggested and perhaps default to zip deploy.  

*Open Question:* if an Azure function is running from blob and you update/change the zip file in blob, will the new file eventually be used/referenced by the function? Maybe run in mixed mode while containers are cycled in and out? Could be another argument for having another unique value in the zip name.
wbreza: Please update the docs based on decisions that we all discussed.
tbarlow12: > @tbarlow12 Please update the docs based on decisions that we all discussed.

@wbreza will do. Thanks for the reminder

Patch:
diff --git a/.travis.yml b/.travis.yml
index 536eeeb..ee489b9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,14 +27,15 @@ install:
 jobs:
   include:
     - stage: test
-      name: "Unit Tests on Node 8"
+      name: "Compile & Unit Tests on Node 8"
       node_js: "8"
       script:
+        - npm run compile
         - npm run test:ci
       after_success:
         - npm run test:coverage
         - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage
-    - name: "Unit Tests on Node 10"
+    - name: "Compile & Unit Tests on Node 10"
       node_js: "10"
 
     - stage: publish
diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md
index 45fbd25..675a79e 100644
--- a/docs/DEPLOY.md
+++ b/docs/DEPLOY.md
@@ -1,4 +1,4 @@
-# Overview
+# Deploy Overview
 
 Deploy usage guide and design decision.
 
@@ -56,3 +56,64 @@ then user try to deploy
 
 1. always using the right resource group
 1. restrictive for user who have already defined their resources
+
+## Deployment Methodologies
+
+#### 1. Deployment to Function App (rollback disabled)
+- 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)
+- 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)
+
+### Deployment configuration
+
+```yml
+service: my-app
+provider:
+  ...
+
+plugins:
+  - serverless-azure-functions
+
+package:
+  ...
+
+deploy:
+  # Rollback enabled, deploying to blob storage
+  # Default is true
+  # If false, deploys directly to function app
+  rollback: true
+  # Container in blob storage containing deployed packages
+  # Default is DEPLOYMENT_ARTIFACTS
+  container: MY_CONTAINER_NAME
+
+functions:
+  ...
+```
+
+If rollback is enabled, the name of the created package will include the UTC timestamp of its creation. This timestamp will also be included in the name of the Azure deployment so as to be able to link the two together. Both names will have `-t{timestamp}` appended to the end.
+
+##### Sequence diagram for deployment to blob storage
+
+```mermaid
+sequenceDiagram
+  participant s as Serverless CLI
+  participant r as Resource Group 
+  participant f as Function App
+  participant b as Blob Storage
+
+  note right of s: `sls deploy`
+  s ->> r: Create resource group
+  s ->> r: Deploy ARM template
+  r ->> f: Included in ARM template
+  r ->> b: Included in ARM template
+  note right of s: Zip code
+  s ->> b: Deploy zip code with name {appName}-v{version}.zip
+  s ->> f: Set package path in settings
+  note right of s: Log URLs 
+```
+
+##### Sub-Commands
+
+- `sls deploy list` - Logs list of deployments to configured resource group with relevant metadata (name, timestamp, etc.). Also logs versions of deployed function app code if available
diff --git a/docs/ROLLBACK.md b/docs/ROLLBACK.md
new file mode 100644
index 0000000..77697da
--- /dev/null
+++ b/docs/ROLLBACK.md
@@ -0,0 +1,55 @@
+# Rollback
+
+##### `sls rollback`
+- Description - Roll back deployment of Function App & Resource Group
+- Options:
+  - `-t` or `--timestamp` - Timestamp associated with version to target
+  - Use `sls deploy list` to discover timestamps
+  - Defaults to previous deployment
+- **Important to note that there is no option for rolling back an individual function. A function app is considered one unit and will be rolled back as such.**
+- In order to roll back your function app, make sure your `deploy.rollback` is either set to `true` or not specified (defaults to `true`). The container in Azure Blob Storage which contains the packaged code artifacts can also be specified, defaults to `DEPLOYMENT_ARTIFACTS` (see [deploy documentation](./DEPLOY.md))
+
+##### Example usage
+
+```bash
+# List all deployments to know the timestamp for rollback
+$ sls deploy list
+Serverless:
+-----------
+Name: myFunctionApp-t1561479533 
+Timestamp: 1561479533
+Datetime: 2019-06-25T16:18:53+00:00
+-----------
+Name: myFunctionApp-t1561479506
+Timestamp: 1561479506
+Datetime: 2019-06-25T16:18:26+00:00
+-----------
+Name: myFunctionApp-t1561479444
+Timestamp: 1561479444
+Datetime: 2019-06-25T16:17:24+00:00
+-----------
+
+$ sls rollback -t 1561479506
+```
+
+##### Sequence diagram for rollback
+
+```mermaid
+sequenceDiagram
+  participant s as Serverless CLI
+  participant r as Resource Group 
+  participant f as Function App
+  participant b as Blob Storage
+
+  note right of s: `sls rollback`
+  s ->> r: Request deployments
+  r ->> s: Return deployments
+  note right of s: Select deployment
+  s ->> r: Deploy ARM template
+  s ->> b: Request names of previously deployed artifacts
+  b ->> s: Return names
+  note right of s: Select artifact
+  s ->> r: Re-deploy template
+  s ->> f: Update RUN_FROM_PACKAGE path (could be done with above step)
+  note right of s: Log URLs
+```
diff --git a/package-lock.json b/package-lock.json
index b3cfd58..d8bbd4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-4",
+  "version": "1.0.0-5",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -73,6 +73,31 @@
         "adal-node": "^0.1.28"
       }
     },
+    "@azure/storage-blob": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-10.3.0.tgz",
+      "integrity": "sha512-KZbJ3q8RpAdeIB5Em1lgXkiq7Mll9bSHHbHavOFMepkkF7HQa3Sez9FdkAVIkVVWK5YoBlshBGZ+mtiSQiS9Fw==",
+      "requires": {
+        "@azure/ms-rest-js": "1.2.3",
+        "events": "3.0.0",
+        "tslib": "^1.9.3"
+      },
+      "dependencies": {
+        "@azure/ms-rest-js": {
+          "version": "1.2.3",
+          "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-1.2.3.tgz",
+          "integrity": "sha512-eROQ034b+9v0Hd3wETKi/EwF5pqS3VRAk1Lm8iKVPOP8v30f6Zfzsi420MRfBMsbNCx/mE2N0L65Px7tvcGfVg==",
+          "requires": {
+            "axios": "^0.18.0",
+            "form-data": "^2.3.2",
+            "tough-cookie": "^2.4.3",
+            "tslib": "^1.9.2",
+            "uuid": "^3.2.1",
+            "xml2js": "^0.4.19"
+          }
+        }
+      }
+    },
     "@babel/code-frame": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
@@ -1759,7 +1784,6 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
-      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1932,7 +1956,6 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2402,8 +2425,7 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
-      "optional": true
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2508,8 +2530,7 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
-      "optional": true
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2538,7 +2559,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
-      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2575,7 +2595,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
-      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2672,8 +2691,7 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
-      "optional": true
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2855,7 +2873,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3233,7 +3250,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3246,7 +3262,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3705,7 +3720,6 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3767,7 +3781,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
-      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4147,7 +4160,6 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
-      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4156,14 +4168,12 @@
     "events": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
-      "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==",
-      "optional": true
+      "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA=="
     },
     "evp_bytestokey": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
-      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -4693,8 +4703,7 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4712,13 +4721,11 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4731,18 +4738,15 @@
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4845,8 +4849,7 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4856,7 +4859,6 @@
         "is-fullwidth-code-point": {
           "version": "1.0.0",
           "bundled": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4869,20 +4871,17 @@
         "minimatch": {
           "version": "3.0.4",
           "bundled": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -4899,7 +4898,6 @@
         "mkdirp": {
           "version": "0.5.1",
           "bundled": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -4972,8 +4970,7 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -4983,7 +4980,6 @@
         "once": {
           "version": "1.4.0",
           "bundled": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5059,8 +5055,7 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5090,7 +5085,6 @@
         "string-width": {
           "version": "1.0.2",
           "bundled": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5108,7 +5102,6 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
-          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5147,13 +5140,11 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true,
-          "optional": true
+          "bundled": true
         }
       }
     },
@@ -5461,7 +5452,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5471,7 +5461,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5481,7 +5470,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
-      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5800,8 +5788,7 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "optional": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7441,8 +7428,7 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
-      "optional": true
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7533,7 +7519,6 @@
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7559,7 +7544,6 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
-      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7650,14 +7634,12 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
-      "optional": true
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
-      "optional": true
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8255,7 +8237,6 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
-      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8364,7 +8345,6 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
-      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8567,8 +8547,7 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
-      "optional": true
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8635,7 +8614,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9048,7 +9026,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9438,7 +9415,6 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9634,8 +9610,7 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
-      "optional": true
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
     },
     "source-map": {
       "version": "0.7.3",
@@ -10192,8 +10167,7 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
-      "optional": true
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11043,7 +11017,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
-      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11052,8 +11025,7 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "optional": true
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
         }
       }
     },
diff --git a/package.json b/package.json
index fa52a26..c95c386 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "serverless-azure-functions",
-  "version": "1.0.0-4",
+  "version": "1.0.0-5",
   "description": "Provider plugin for the Serverless Framework v1.x which adds support for Azure Functions.",
   "license": "MIT",
   "main": "./lib/index.js",
@@ -14,8 +14,9 @@
     "test": "jest",
     "test:ci": "npm run test -- --ci",
     "test:coverage": "npm run test -- --coverage",
+    "compile": "tsc",
     "prebuild": "shx rm -rf lib/ && npm run test",
-    "build": "tsc"
+    "build": "npm run compile"
   },
   "repository": {
     "git": "https://github.com/serverless/serverless-azure-functions"
@@ -39,6 +40,7 @@
     "@azure/arm-appservice": "^5.7.0",
     "@azure/arm-resources": "^1.0.1",
     "@azure/ms-rest-nodeauth": "^1.0.1",
+    "@azure/storage-blob": "^10.3.0",
     "axios": "^0.18.0",
     "js-yaml": "^3.13.1",
     "jsonpath": "^1.0.1",
diff --git a/src/armTemplates/resources/appInsights.ts b/src/armTemplates/resources/appInsights.ts
index 9e7df06..1223062 100644
--- a/src/armTemplates/resources/appInsights.ts
+++ b/src/armTemplates/resources/appInsights.ts
@@ -1,5 +1,5 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
+import { ServerlessAzureConfig } from "../../models/serverless";
 
 export class AppInsightsResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
diff --git a/src/armTemplates/resources/hostingEnvironment.ts b/src/armTemplates/resources/hostingEnvironment.ts
index f785485..0591002 100644
--- a/src/armTemplates/resources/hostingEnvironment.ts
+++ b/src/armTemplates/resources/hostingEnvironment.ts
@@ -1,5 +1,5 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
+import { ServerlessAzureConfig } from "../../models/serverless";
 
 export class HostingEnvironmentResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
diff --git a/src/armTemplates/resources/virtualNetwork.ts b/src/armTemplates/resources/virtualNetwork.ts
index b93448b..9c1feff 100644
--- a/src/armTemplates/resources/virtualNetwork.ts
+++ b/src/armTemplates/resources/virtualNetwork.ts
@@ -1,5 +1,5 @@
 import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates";
-import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless";
+import { ServerlessAzureConfig } from "../../models/serverless";
 
 export class VirtualNetworkResource implements ArmResourceTemplateGenerator {
   public static getResourceName(config: ServerlessAzureConfig) {
diff --git a/src/services/azureBlobStorageService.ts b/src/services/azureBlobStorageService.ts
new file mode 100644
index 0000000..9946cea
--- /dev/null
+++ b/src/services/azureBlobStorageService.ts
@@ -0,0 +1,152 @@
+import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, StorageURL, uploadFileToBlockBlob } from "@azure/storage-blob";
+import Serverless from "serverless";
+import { BaseService } from "./baseService";
+import { Guard } from "../shared/guard";
+
+/**
+ * Wrapper for operations on Azure Blob Storage account
+ */
+export class AzureBlobStorageService extends BaseService {
+
+  /**
+   * Account URL for Azure Blob Storage account. Depends on `storageAccountName` being set in baseService
+   */
+  private accountUrl: string;
+
+  public constructor(serverless: Serverless, options: Serverless.Options) {
+    super(serverless, options);
+    this.accountUrl = `https://${this.storageAccountName}.blob.core.windows.net`;
+  }
+
+  /**
+   * Upload a file to Azure Blob Storage
+   * @param path Path of file to upload
+   * @param containerName Name of container in Azure Blob storage for upload
+   * @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);
+    // Use specified blob name or replace `/` in path with `-`
+    const name = blobName || path.replace(/^.*[\\\/]/, "-");
+    uploadFileToBlockBlob(Aborter.none, path, this.getBlockBlobURL(containerName, name));
+  };
+  
+  /**
+   * Delete a blob from Azure Blob Storage
+   * @param containerName Name of container containing blob
+   * @param blobName Blob to delete
+   */
+  public async deleteFile(containerName: string, blobName: string): Promise<void> {
+    Guard.empty(containerName);
+    Guard.empty(blobName);
+    const blockBlobUrl = await this.getBlockBlobURL(containerName, blobName)
+    await blockBlobUrl.delete(Aborter.none);
+  }
+
+  /**
+   * Lists files in container
+   * @param ext - Extension of files to filter on when retrieving files
+   * from container
+   */
+  public async listFiles(containerName: string, ext?: string): Promise<string[]> {
+    Guard.empty(containerName, "containerName");
+    const result: string[] = [];
+    let marker;
+    const containerURL = this.getContainerURL(containerName);
+    do {
+      const listBlobsResponse = await containerURL.listBlobFlatSegment(
+        Aborter.none,
+        marker,
+      );
+      marker = listBlobsResponse.nextMarker;
+      for (const blob of listBlobsResponse.segment.blobItems) {
+        if ((ext && blob.name.endsWith(ext)) || !ext) {
+          result.push(blob.name);
+        }
+      }
+    } while (marker);
+
+    return result;
+  }
+
+  /**
+   * Lists the containers within the Azure Blob Storage account
+   */
+  public async listContainers() {
+    const result: string[] = [];
+    let marker;
+    do {
+      const listContainersResponse = await this.getServiceURL().listContainersSegment(
+        Aborter.none,
+        marker,
+      );
+      marker = listContainersResponse.nextMarker;
+      for (const container of listContainersResponse.containerItems) {
+        result.push(container.name);
+      }
+    } while (marker);
+
+    return result;
+  }
+
+  /**
+   * 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);
+  }
+
+  /**
+   * Delete a container from Azure Blob Storage Account
+   * @param containerName Name of container to delete
+   */
+  public async deleteContainer(containerName: string): Promise<void> {
+    Guard.empty(containerName);
+    const containerUrl = await this.getContainerURL(containerName)
+    await containerUrl.delete(Aborter.none);
+  }
+
+  /**
+   * Get ServiceURL object for Azure Blob Storage Account
+   */
+  private getServiceURL(): ServiceURL {
+    const pipeline = StorageURL.newPipeline(this.credentials);
+    const accountUrl = this.accountUrl;
+    const serviceUrl = new ServiceURL(
+      accountUrl,
+      pipeline,
+    );
+    return serviceUrl;
+  }
+
+  /**
+   * Get a ContainerURL object to perform operations on Azure Blob Storage container
+   * @param containerName Name of container
+   * @param serviceURL Previously created ServiceURL object (will create if undefined)
+   */
+  private getContainerURL(containerName: string): ContainerURL {
+    Guard.empty(containerName);
+    return ContainerURL.fromServiceURL(
+      this.getServiceURL(),
+      containerName
+    );
+  }
+
+  /**
+   * Get a BlockBlobURL object to perform operations on Azure Blob Storage Blob
+   * @param containerName Name of container containing blob
+   * @param blobName Name of blob
+   */
+  private getBlockBlobURL(containerName: string, blobName: string): BlockBlobURL {
+    Guard.empty(containerName);
+    Guard.empty(blobName);
+    return BlockBlobURL.fromContainerURL(
+      this.getContainerURL(containerName),
+      blobName,
+    );
+  }
+}
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 44b4231..c45a86a 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -12,6 +12,8 @@ export abstract class BaseService {
   protected subscriptionId: string;
   protected resourceGroup: string;
   protected deploymentName: string;
+  protected deploymentContainerName: string;
+  protected storageAccountName: string;
   protected config: ServerlessAzureConfig;
 
   protected constructor(
diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts
index b4d26f6..e83fd6d 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -241,7 +241,7 @@ export class FunctionAppService extends BaseService {
    */
   private getScmDomain(functionApp: Site) {
     return functionApp.enabledHostNames.find((hostName: string) => {
-      return hostName.endsWith("scm.azurewebsites.net");
+      return hostName.includes(".scm.") && hostName.endsWith(".azurewebsites.net");
     });
   }
 }


Test patch:
diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts
new file mode 100644
index 0000000..9302e14
--- /dev/null
+++ b/src/services/azureBlobStorageService.test.ts
@@ -0,0 +1,92 @@
+import { MockFactory } from "../test/mockFactory"
+import mockFs from "mock-fs";
+
+jest.mock("@azure/storage-blob");
+import { BlockBlobURL, ContainerURL, ServiceURL, Aborter, uploadFileToBlockBlob } from "@azure/storage-blob";
+import { AzureBlobStorageService } from "./azureBlobStorageService";
+
+describe("Azure Blob Storage Service", () => {
+
+  const filePath = "deployments/deployment.zip";
+  const fileName = "deployment.zip";
+  const fileContents = "contents";
+  const containerName = "DEPLOYMENTS";
+
+  const containers = MockFactory.createTestAzureContainers();
+  const sls = MockFactory.createTestServerless();
+  const options = MockFactory.createTestServerlessOptions();
+  const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath);
+
+  let service: AzureBlobStorageService;
+
+  beforeAll(() => {
+    BlockBlobURL.fromContainerURL = jest.fn(() => blockBlobUrl) as any;
+  });
+
+  beforeAll(() => {
+    mockFs({
+      "deployments/deployment.zip": fileContents
+    })
+  });
+
+  afterAll(() => {
+    mockFs.restore();
+  });
+
+  beforeEach(() => {
+    service = new AzureBlobStorageService(sls, options);
+  });
+
+  it("should upload a file", async () => {
+    uploadFileToBlockBlob.prototype = jest.fn();
+    ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any));
+    await service.uploadFile(filePath, containerName);
+    expect(uploadFileToBlockBlob).toBeCalledWith(
+      Aborter.none,
+      filePath,
+      blockBlobUrl
+    );
+  });
+
+  it("should delete a file", async () => {
+    ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any));
+    await service.deleteFile(containerName, fileName);
+    expect(blockBlobUrl.delete).toBeCalledWith(Aborter.none)
+  });
+
+  it("should list files of container", async () => {
+    const blobs = MockFactory.createTestAzureBlobItems();
+    ContainerURL.prototype.listBlobFlatSegment = jest.fn(() => Promise.resolve(blobs)) as any;
+    ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));
+    const files = await service.listFiles(containerName);
+    expect(files.length).toEqual(5);
+
+    const otherFiles = await service.listFiles(containerName, "jpeg");
+    expect(otherFiles.length).toEqual(0);
+  });
+
+  it("should list containers", async () => {
+    ServiceURL.prototype.listContainersSegment = jest.fn(() => Promise.resolve(containers));
+    const containerList = await service.listContainers();
+    expect(containerList).toEqual(
+      containers.containerItems.map((container) => container.name));
+  });
+
+  it("should create a container", async () => {
+    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);
+    expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), newContainerName);
+    expect(ContainerURL.prototype.create).toBeCalledWith(Aborter.none);
+  });
+  
+  it("should delete a container", async () => {
+    const containerToDelete = "delete container";
+    ContainerURL.fromServiceURL = jest.fn(() => new ContainerURL(null, null));
+    ContainerURL.prototype.delete = jest.fn(() => Promise.resolve({ statusCode: 204 })) as any;
+    await service.deleteContainer(containerToDelete);
+    expect(ContainerURL.fromServiceURL).toBeCalledWith(expect.anything(), containerToDelete);
+    expect(ContainerURL.prototype.delete).toBeCalledWith(Aborter.none);
+  });
+});
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index 51aa527..e6def2c 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -13,7 +13,6 @@ import { ArmDeployment, ArmTemplateType } from "../models/armTemplates";
 jest.mock("@azure/arm-resources")
 
 describe("Function App Service", () => {
-
   const app = MockFactory.createTestSite();
   const slsService = MockFactory.createTestService();
   const variables = MockFactory.createTestVariables();
@@ -31,12 +30,8 @@ describe("Function App Service", () => {
   const authKeyUrl = `${baseUrl}${app.id}/functions/admin/token?api-version=2016-08-01`;
   const syncTriggersUrl = `${baseUrl}${app.id}/syncfunctiontriggers?api-version=2016-08-01`;
   const listFunctionsUrl = `${baseUrl}${app.id}/functions?api-version=2016-08-01`;
-  const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net"));
-  const uploadUrl = `https://${scmDomain}/api/zipdeploy/`;
 
   beforeAll(() => {
-
-    // TODO: How to spy on default exported function?
     const axiosMock = new MockAdapter(axios);
 
     // Master Key
@@ -59,7 +54,6 @@ describe("Function App Service", () => {
   });
 
   beforeEach(() => {
-
     WebSiteManagementClient.prototype.webApps = {
       get: jest.fn(() => app),
       deleteFunction: jest.fn(),
@@ -107,8 +101,8 @@ describe("Function App Service", () => {
 
   it("gets master key", async () => {
     const service = createService();
-    const masterKey = await service.getMasterKey();
-    expect(masterKey).toEqual(masterKey);
+    const result = await service.getMasterKey();
+    expect(result).toEqual(masterKey);
 
   });
 
@@ -197,11 +191,41 @@ describe("Function App Service", () => {
   });
 
   it("uploads functions", async () => {
+    const scmDomain = app.enabledHostNames.find((hostname) => hostname.endsWith("scm.azurewebsites.net"));
+    const expectedUploadUrl = `https://${scmDomain}/api/zipdeploy/`;
+
     const service = createService();
     await service.uploadFunctions(app);
+
+    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",
+      }
+    }, slsService["artifact"])
+  });
+
+  it("uploads functions with custom SCM domain (aka App service environments)", async () => {
+    const customApp = {
+      ...MockFactory.createTestSite("CustomAppWithinASE"),
+      enabledHostNames: [
+        "myapi.customase.p.azurewebsites.net",
+        "myapi.scm.customase.p.azurewebsites.net"
+      ],
+    }
+
+    const expectedUploadUrl = `https://${customApp.enabledHostNames[1]}/api/zipdeploy/`;
+
+    const service = createService();
+    await service.uploadFunctions(customApp);
+
     expect((FunctionAppService.prototype as any).sendFile).toBeCalledWith({
       method: "POST",
-      uri: uploadUrl,
+      uri: expectedUploadUrl,
       json: true,
       headers: {
         Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`,
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index d6fa7a8..2b78b48 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -1,20 +1,21 @@
-import { ApiContract, ApiManagementServiceResource } from "@azure/arm-apimanagement/esm/models";
-import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models";
-import { HttpHeaders, HttpOperationResponse, HttpResponse, WebResource } from "@azure/ms-rest-js";
-import { AuthResponse, LinkedSubscription, TokenCredentialsBase } from "@azure/ms-rest-nodeauth";
-import { TokenClientCredentials, TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials";
-import { AxiosRequestConfig, AxiosResponse } from "axios";
 import yaml from "js-yaml";
 import Serverless from "serverless";
 import Service from "serverless/classes/Service";
 import Utils from "serverless/classes/Utils";
 import PluginManager from "serverless/lib/classes/PluginManager";
+import { HttpHeaders, HttpOperationResponse, HttpResponse, WebResource } from "@azure/ms-rest-js";
+import { LinkedSubscription, TokenCredentialsBase, AuthResponse } from "@azure/ms-rest-nodeauth";
+import { TokenResponse, TokenClientCredentials } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials";
+import { ServiceListContainersSegmentResponse } from "@azure/storage-blob/typings/lib/generated/lib/models";
+import { AxiosResponse, AxiosRequestConfig } from "axios";
 import { ServerlessAzureConfig } from "../models/serverless";
 import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider"
 import { Logger } from "../models/generic";
 import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement";
 import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models";
 import { ArmResourceTemplate } from "../models/armTemplates";
+import { FunctionEnvelope, Site } from "@azure/arm-appservice/esm/models";
+import { ApiManagementServiceResource, ApiContract } from "@azure/arm-apimanagement/esm/models";
 
 function getAttribute(object: any, prop: string, defaultValue: any): any {
   if (object && object[prop]) {
@@ -183,6 +184,39 @@ export class MockFactory {
     return Promise.resolve(response);
   }
 
+  public static createTestAzureContainers(count: number = 5): ServiceListContainersSegmentResponse {
+    const result = [];
+    for (let i = 0; i < count; i++) {
+      result.push({
+        name: `container${i}`,
+        blobs: MockFactory.createTestAzureBlobItems(i),
+      });
+    }
+    return { containerItems: result } as ServiceListContainersSegmentResponse;
+  }
+
+  public static createTestBlockBlobUrl(containerName: string, blobName: string) {
+    return {
+      containerName,
+      blobName,
+      delete: jest.fn(),
+    }
+  }
+
+  public static createTestAzureBlobItems(id: number = 1, count: number = 5) {
+    const result = [];
+    for (let i = 0; i < count; i++) {
+      result.push(MockFactory.createTestAzureBlobItem(id, i));
+    }
+    return { segment: { blobItems: result } };
+  }
+
+  public static createTestAzureBlobItem(id: number = 1, index: number = 1, ext: string = ".zip") {
+    return {
+      name: `blob-${id}-${index}${ext}`
+    }
+  }
+
   public static createTestAzureClientResponse<T>(responseJson: T, statusCode: number = 200): Promise<HttpOperationResponse> {
     const response: HttpOperationResponse = {
       request: new WebResource(),