Repository: serverless/serverless-azure-functions
Pull request #195 (2019-07-09 00:11:56)

test: Added test cases for missing areas of coverage
- [x] Added missing `funcService.test.ts`
- [x] Added missing test cases from `offlineService`
- [x] Removed unused & untested code from `utils`


Patch:
diff --git a/package-lock.json b/package-lock.json
index f756d84..b52ea54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1794,6 +1794,7 @@
       "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",
@@ -1966,6 +1967,7 @@
       "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",
@@ -2435,7 +2437,8 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
+      "optional": true
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2540,7 +2543,8 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
+      "optional": true
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2569,6 +2573,7 @@
       "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",
@@ -2605,6 +2610,7 @@
       "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"
@@ -2701,7 +2707,8 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
+      "optional": true
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2888,6 +2895,7 @@
       "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"
@@ -3265,6 +3273,7 @@
       "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",
@@ -3277,6 +3286,7 @@
       "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",
@@ -3740,6 +3750,7 @@
       "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",
@@ -3801,6 +3812,7 @@
       "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"
       }
@@ -4180,6 +4192,7 @@
       "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"
@@ -4194,6 +4207,7 @@
       "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"
@@ -4723,7 +4737,8 @@
         },
         "ansi-regex": {
           "version": "2.1.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4741,11 +4756,13 @@
         },
         "balanced-match": {
           "version": "1.0.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4758,15 +4775,18 @@
         },
         "code-point-at": {
           "version": "1.1.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4869,7 +4889,8 @@
         },
         "inherits": {
           "version": "2.0.3",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4879,6 +4900,7 @@
         "is-fullwidth-code-point": {
           "version": "1.0.0",
           "bundled": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4891,17 +4913,20 @@
         "minimatch": {
           "version": "3.0.4",
           "bundled": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
         },
         "minimist": {
           "version": "0.0.8",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -4918,6 +4943,7 @@
         "mkdirp": {
           "version": "0.5.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -4990,7 +5016,8 @@
         },
         "number-is-nan": {
           "version": "1.0.1",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5000,6 +5027,7 @@
         "once": {
           "version": "1.4.0",
           "bundled": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5075,7 +5103,8 @@
         },
         "safe-buffer": {
           "version": "5.1.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5105,6 +5134,7 @@
         "string-width": {
           "version": "1.0.2",
           "bundled": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5122,6 +5152,7 @@
         "strip-ansi": {
           "version": "3.0.1",
           "bundled": true,
+          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5160,11 +5191,13 @@
         },
         "wrappy": {
           "version": "1.0.2",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         },
         "yallist": {
           "version": "3.0.3",
-          "bundled": true
+          "bundled": true,
+          "optional": true
         }
       }
     },
@@ -5472,6 +5505,7 @@
       "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"
@@ -5481,6 +5515,7 @@
       "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"
@@ -5490,6 +5525,7 @@
       "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",
@@ -5808,7 +5844,8 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "optional": true
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7448,7 +7485,8 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
+      "optional": true
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7549,6 +7587,7 @@
       "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",
@@ -7574,6 +7613,7 @@
       "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"
@@ -7664,12 +7704,14 @@
     "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=="
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "optional": true
     },
     "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="
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
+      "optional": true
     },
     "minimatch": {
       "version": "3.0.4",
@@ -8267,6 +8309,7 @@
       "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",
@@ -8375,6 +8418,7 @@
       "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",
@@ -8577,7 +8621,8 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
+      "optional": true
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8644,6 +8689,7 @@
       "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"
       }
@@ -9056,6 +9102,7 @@
       "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"
@@ -9445,6 +9492,7 @@
       "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"
@@ -9640,7 +9688,8 @@
     "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=="
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
+      "optional": true
     },
     "source-map": {
       "version": "0.7.3",
@@ -10197,7 +10246,8 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
+      "optional": true
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11047,6 +11097,7 @@
       "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"
@@ -11055,7 +11106,8 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "optional": true
         }
       }
     },
diff --git a/src/models/serverless.ts b/src/models/serverless.ts
index 2f009ab..727d8ff 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -58,6 +58,21 @@ export interface ServerlessAzureConfig {
   functions: any;
 }
 
+export interface ServerlessCommand {
+  usage: string;
+  lifecycleEvents: string[];
+  options?: {
+    [key: string]: {
+      usage: string;
+      shortcut?: string;
+    };
+  };
+  commands?: ServerlessCommandMap;
+}
+
+export interface ServerlessCommandMap {
+  [command: string]: ServerlessCommand;
+}
 export interface ServerlessAzureOptions extends Serverless.Options {
   resourceGroup?: string;
 }
diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts
index a17f23c..02ff649 100644
--- a/src/plugins/deploy/azureDeployPlugin.ts
+++ b/src/plugins/deploy/azureDeployPlugin.ts
@@ -61,6 +61,7 @@ export class AzureDeployPlugin {
 
   private async deploy() {
     const resourceService = new ResourceService(this.serverless, this.options);
+
     await resourceService.deployResourceGroup();
 
     const functionAppService = new FunctionAppService(this.serverless, this.options);
diff --git a/src/plugins/invoke/azureInvoke.ts b/src/plugins/invoke/azureInvoke.ts
index f8da7e2..f5fa576 100644
--- a/src/plugins/invoke/azureInvoke.ts
+++ b/src/plugins/invoke/azureInvoke.ts
@@ -1,41 +1,72 @@
+import { isAbsolute, join } from "path";
 import Serverless from "serverless";
-import { join, isAbsolute } from "path";
-import AzureProvider from "../../provider/azureProvider";
+import { InvokeService } from "../../services/invokeService";
+import fs from "fs";
+import { ServerlessCommandMap } from "../../models/serverless";
 
 export class AzureInvoke {
   public hooks: { [eventName: string]: Promise<any> };
-  private provider: AzureProvider;
-
+  private commands: ServerlessCommandMap;
+  private invokeService: InvokeService;
   public constructor(private serverless: Serverless, private options: Serverless.Options) {
-    this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
     const path = this.options["path"];
-
+    
     if (path) {
       const absolutePath = isAbsolute(path)
         ? path
         : join(this.serverless.config.servicePath, path);
+      this.serverless.cli.log(this.serverless.config.servicePath);
+      this.serverless.cli.log(path);
 
-      if (!this.serverless.utils.fileExistsSync(absolutePath)) {
+      if (!fs.existsSync(absolutePath)) {
         throw new Error("The file you provided does not exist.");
       }
-      this.options["data"] = this.serverless.utils.readFileSync(absolutePath);
+      this.options["data"] = fs.readFileSync(absolutePath).toString();
+    }
+
+    this.commands = {
+      invoke: {
+        usage: "Invoke command",
+        lifecycleEvents: ["invoke"],
+        options: {
+          function: {
+            usage: "Function to call",
+            shortcut: "f",
+          },
+          path: {
+            usage: "Path to file to put in body",
+            shortcut: "p"
+          },
+          data: {
+            usage: "Data string for body of request",
+            shortcut: "d"
+          },
+          method: {
+            usage: "HTTP method (Default is GET)",
+            shortcut: "m"
+          }
+        }
+      }
     }
 
     this.hooks = {
-      "before:invoke:invoke": this.provider.getAdminKey.bind(this),
       "invoke:invoke": this.invoke.bind(this)
     };
   }
-
+  
   private async invoke() {
-    const func = this.options.function;
-    const functionObject = this.serverless.service.getFunction(func);
-    const eventType = Object.keys(functionObject["events"][0])[0];
-
-    if (!this.options["data"]) {
-      this.options["data"] = {};
+    const functionName = this.options["function"];
+    const data = this.options["data"];
+    const method = this.options["method"] || "GET";
+    if (!functionName) {
+      this.serverless.cli.log("Need to provide a name of function to invoke");
+      return;
     }
 
-    return this.provider.invoke(func, eventType, this.options["data"]);
+    this.invokeService = new InvokeService(this.serverless, this.options);
+    const response =  await this.invokeService.invoke(method, functionName, data);
+    if(response){
+      this.serverless.cli.log(JSON.stringify(response.data));
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts
index 1b36eb6..599c3f5 100644
--- a/src/plugins/login/loginPlugin.ts
+++ b/src/plugins/login/loginPlugin.ts
@@ -12,6 +12,7 @@ export class AzureLoginPlugin {
     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),
     };
   }
diff --git a/src/plugins/offline/azureOfflinePlugin.ts b/src/plugins/offline/azureOfflinePlugin.ts
index 2a96789..0615edb 100644
--- a/src/plugins/offline/azureOfflinePlugin.ts
+++ b/src/plugins/offline/azureOfflinePlugin.ts
@@ -51,4 +51,4 @@ export class AzureOfflinePlugin {
   private async azureOfflineCleanup(){
     this.offlineService.cleanup();
   }
-}
\ No newline at end of file
+}
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index aee0061..f7f1e79 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
@@ -231,4 +231,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 0072afe..834239e 100644
--- a/src/services/functionAppService.ts
+++ b/src/services/functionAppService.ts
@@ -25,6 +25,9 @@ export class FunctionAppService extends BaseService {
   public async get(): Promise<Site> {
     const response: any = await this.webClient.webApps.get(this.resourceGroup, FunctionAppResource.getResourceName(this.config));
     if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) {
+      this.serverless.cli.log(this.resourceGroup);
+      this.serverless.cli.log(FunctionAppResource.getResourceName(this.config));
+      this.serverless.cli.log(JSON.stringify(response));
       return null;
     }
 
@@ -214,7 +217,7 @@ export class FunctionAppService extends BaseService {
     return `${deploymentName.replace("rg-deployment", "artifact")}.zip`;
   }
 
-  private getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
+  public getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
     const httpTrigger = functionConfig.config.bindings.find((binding) => {
       return binding.type === "httpTrigger";
     });
diff --git a/src/services/invokeService.ts b/src/services/invokeService.ts
new file mode 100644
index 0000000..0b14fbc
--- /dev/null
+++ b/src/services/invokeService.ts
@@ -0,0 +1,87 @@
+import { BaseService } from "./baseService"
+import Serverless from "serverless";
+import axios from "axios";
+import { FunctionAppService } from "./functionAppService";
+
+export class InvokeService extends BaseService {
+  public functionAppService: FunctionAppService;
+
+  public constructor(serverless: Serverless, options: Serverless.Options) {
+    super(serverless, options);
+    this.functionAppService = new FunctionAppService(serverless, options);
+  }
+
+  /**
+   * Invoke an Azure Function
+   * @param method HTTP method
+   * @param functionName Name of function to invoke
+   * @param data Data to use as body or query params
+   */
+  public async invoke(method: string, functionName: string, data?: any){
+
+    const functionObject = this.slsFunctions()[functionName];
+    /* accesses the admin key */
+    if (!functionObject) {
+      this.serverless.cli.log(`Function ${functionName} does not exist`);
+      return;
+    }
+    
+    const eventType = Object.keys(functionObject["events"][0])[0];
+
+    if (eventType !== "http") {
+      this.log("Needs to be an http function");
+      return;
+    }
+    
+    const functionApp = await this.functionAppService.get();
+    const functionConfig = await this.functionAppService.getFunction(functionApp, functionName);
+    const httpConfig = this.functionAppService.getFunctionHttpTriggerConfig(functionApp, functionConfig);
+    let url  = "http://" + httpConfig.url;
+
+    if (method === "GET" && data) {
+      const queryString = this.getQueryString(data);
+      url += `?${queryString}`
+    }   
+
+    this.log(url);
+    const options = await this.getOptions(method, data);
+    this.log(`Invoking function ${functionName} with ${method} request`);
+    return await axios(url, options);
+  }
+
+  private getQueryString(eventData: any) {
+    if (typeof eventData === "string") {
+      try {
+        eventData = JSON.parse(eventData);
+      }
+      catch (error) {
+        return Promise.reject("The specified input data isn't a valid JSON string. " +
+        "Please correct it and try invoking the function again.");
+      }
+    }
+    return encodeURIComponent(Object.keys(eventData)
+      .map((key) => `${key}=${eventData[key]}`)
+      .join("&"));
+  }
+
+  /**
+ * Get options object  
+ * @param method The method used (POST or GET)
+ * @param data Data to use as body or query params
+ */
+  private async getOptions(method: string, data?: any) {
+  
+    const functionsAdminKey = await this.functionAppService.getMasterKey();
+    const functionApp = await this.functionAppService.get();
+    const options: any = {
+      host: functionApp.defaultHostName,
+      headers: {
+        "x-functions-key": functionsAdminKey
+      },
+      method,
+      data,
+    };
+    
+    return options;
+  }
+} 
\ No newline at end of file
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 8bc5e70..0fa71d8 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -118,13 +118,6 @@ export class Utils {
     return JSON.stringify(json, null, 2);
   }
 
-  public static interpolateFile(sls: Serverless, path: string, params: Map<string, string>) {
-    const template = sls.utils.readFileSync(path);
-    const names = params.keys();
-    const vals = params.values();
-    return new Function(...names, `return \`${template}\`;`)(...vals);
-  }
-
   /**
    * Take the first `substringSize` characters from each string and return as one string
    * @param substringSize Size of substring to take from beginning of each string


Test patch:
diff --git a/src/plugins/invoke/azureInvoke.test.ts b/src/plugins/invoke/azureInvoke.test.ts
new file mode 100644
index 0000000..982f562
--- /dev/null
+++ b/src/plugins/invoke/azureInvoke.test.ts
@@ -0,0 +1,97 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import mockFs from "mock-fs";
+import { AzureInvoke } from "./azureInvoke";
+jest.mock("../../services/functionAppService");
+jest.mock("../../services/resourceService");
+jest.mock("../../services/invokeService");
+import { InvokeService } from "../../services/invokeService";
+
+describe("Azure Invoke Plugin", () => {
+  const fileContent =  JSON.stringify({
+    name: "Azure-Test",
+  });
+  afterEach(() => {
+    jest.resetAllMocks();
+  })
+
+  beforeAll(() => {
+    mockFs({
+      "testFile.json": fileContent,
+    }, { createCwd: true, createTmp: true });
+  });
+  afterAll(() => {
+    mockFs.restore();
+  });
+
+  it("calls invoke hook", async () => {
+    const expectedResult = { data: "test" };
+    const invoke = jest.fn(() => expectedResult);
+    InvokeService.prototype.invoke = invoke as any;
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["data"] = "{\"name\": \"AzureTest\"}";
+    options["method"] = "GET";
+
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke");
+    expect(invoke).toBeCalledWith(options["method"], options["function"], options["data"]);
+    expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
+  });
+
+  it("calls the invoke hook with file path", async () => {
+    const expectedResult = { data: "test" };
+    const invoke = jest.fn(() => expectedResult);
+    InvokeService.prototype.invoke = invoke as any;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["path"] = "testFile.json";
+    options["method"] = "GET";
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke");
+    expect(invoke).toBeCalledWith(options["method"], options["function"], fileContent);
+    expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
+  
+  });
+
+  it("calls the invoke hook with file path", async () => {
+    const invoke = jest.fn();
+    InvokeService.prototype.invoke = invoke;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["path"] = "notExist.json";
+    options["method"] = "GET";
+    expect(() => new AzureInvoke(sls, options)).toThrow();
+  });
+  
+  it("Function invoked with no data", async () => {
+    const expectedResult = { data: "test" };
+    const invoke = jest.fn(() => expectedResult);
+    InvokeService.prototype.invoke = invoke as any;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = "testApp";
+    options["method"] = "GET";
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke"); 
+    expect(invoke).toBeCalledWith(options["method"], options["function"], undefined);
+    expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
+  });
+
+  it("The invoke function fails when no function name is passed", async () => {
+    const invoke = jest.fn();
+    InvokeService.prototype.invoke = invoke;
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    options["function"] = null;
+    options["data"] = "{\"name\": \"AzureTest\"}";
+    options["method"] = "GET";
+    const plugin = new AzureInvoke(sls, options);
+    await invokeHook(plugin, "invoke:invoke"); 
+    expect(invoke).not.toBeCalled();
+  });
+});
diff --git a/src/plugins/offline/azureOfflinePlugin.test.ts b/src/plugins/offline/azureOfflinePlugin.test.ts
index f786b78..d8a6cda 100644
--- a/src/plugins/offline/azureOfflinePlugin.test.ts
+++ b/src/plugins/offline/azureOfflinePlugin.test.ts
@@ -6,6 +6,9 @@ import { MockFactory } from "../../test/mockFactory";
 import { invokeHook } from "../../test/utils";
 import { AzureOfflinePlugin } from "./azureOfflinePlugin";
 
+jest.mock("../../services/offlineService")
+import { OfflineService } from "../../services/offlineService"
+
 describe("Azure Offline Plugin", () => {
 
   function createPlugin(sls?: Serverless, options?: Serverless.Options) {
@@ -15,68 +18,24 @@ describe("Azure Offline Plugin", () => {
     )
   }
 
-  beforeEach(() => {
-    mockFs({})
-  });
-
-  afterEach(() => {
-    mockFs.restore();
-  })
-
   it("invokes build hook", async () => {
     const sls = MockFactory.createTestServerless();
     const plugin = createPlugin(sls);
-    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
     await invokeHook(plugin, "offline:build:build");
-    const calls = writeFileSpy.mock.calls;
-    const functionNames = sls.service.getAllFunctions();
-    expect(calls).toHaveLength(functionNames.length + 1);
-    for (let i = 0; i < functionNames.length; i++) {
-      const name = functionNames[i];
-      expect(calls[i][0]).toEqual(`${name}${path.sep}function.json`)
-      expect(
-        JSON.parse(calls[i][1])
-      ).toEqual(
-        MockFactory.createTestBindingsObject(`..${path.sep}${name}.js`)
-      );
-    }
-    expect(calls[calls.length - 1][0]).toEqual("local.settings.json");
-    writeFileSpy.mockRestore();
+    expect(OfflineService.prototype.build).toBeCalled();
   });
 
   it("invokes offline hook", async () => {
     const sls = MockFactory.createTestServerless();
     const plugin = createPlugin(sls);
     await invokeHook(plugin, "offline:offline");
-    // Trivial test for now. In the future, this process
-    // may spawn the start process itself rather than telling
-    // the user how to do it.
-    expect(sls.cli.log).toBeCalledTimes(3);
+    expect(OfflineService.prototype.start).toBeCalled();
   });
 
   it("invokes cleanup hook", async () => {
-    mockFs({
-      hello: {
-        "function.json": "contents"
-      },
-      goodbye: {
-        "function.json": "contents"
-      },
-      "local.settings.json": "contents",
-    });
-    const unlinkSpy = jest.spyOn(fs, "unlinkSync");
-    const rmdirSpy = jest.spyOn(fs, "rmdirSync")
     const sls = MockFactory.createTestServerless();
     const plugin = createPlugin(sls);
     await invokeHook(plugin, "offline:cleanup:cleanup");
-    const unlinkCalls = unlinkSpy.mock.calls;
-    expect(unlinkCalls[0][0]).toBe(`hello${path.sep}function.json`);
-    expect(unlinkCalls[1][0]).toBe(`goodbye${path.sep}function.json`);
-    expect(unlinkCalls[2][0]).toBe("local.settings.json");
-    const rmdirCalls = rmdirSpy.mock.calls;
-    expect(rmdirCalls[0][0]).toBe("hello");
-    expect(rmdirCalls[1][0]).toBe("goodbye");
-    unlinkSpy.mockRestore();
-    rmdirSpy.mockRestore();
+    expect(OfflineService.prototype.cleanup).toBeCalled();
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/funcService.test.ts b/src/services/funcService.test.ts
new file mode 100644
index 0000000..b42bb75
--- /dev/null
+++ b/src/services/funcService.test.ts
@@ -0,0 +1,152 @@
+import fs from "fs";
+import mockFs from "mock-fs";
+import rimraf from "rimraf";
+import { MockFactory } from "../test/mockFactory";
+import Serverless from "serverless";
+import { FuncService } from "./funcService";
+
+describe("Azure Func Service", () => {
+
+  function createService(sls?: Serverless, options?: Serverless.Options) {
+    return new FuncService(
+      sls || MockFactory.createTestServerless(),
+      options || MockFactory.createTestServerlessOptions(),
+    )
+  }
+
+  describe("Add command", () => {
+
+    beforeAll(() => {
+      mockFs({
+        "myExistingFunction": {
+          "index.js": "contents",
+          "function.json": "contents",
+        },
+        "serverless.yml": MockFactory.createTestServerlessYml(true) as any,
+      });
+    });
+
+    afterAll(() => {
+      mockFs.restore();
+    });
+
+    afterEach(() => {
+      jest.clearAllMocks();
+    })
+
+    it("returns with missing name", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+
+      const service = createService(sls, options);
+      await service.add();
+
+      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add")
+    });
+
+    it("returns with pre-existing function", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      options["name"] = "hello";
+
+      const service = createService(sls, options);
+      await service.add();
+
+      expect(sls.cli.log).toBeCalledWith("Function hello already exists");
+    });
+
+    it("creates function handler and updates serverless.yml", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const functionName = "myFunction";
+      options["name"] = functionName;
+      const expectedFunctionsYml = MockFactory.createTestSlsFunctionConfig();
+      expectedFunctionsYml[functionName] = MockFactory.createTestFunctionMetadata(functionName);
+
+      const service = createService(sls, options);
+      await service.add();
+
+      const writeFileCalls = (sls.utils.writeFileSync as any).mock.calls;
+      expect(writeFileCalls[0][0]).toBe(`./${functionName}.js`);
+
+      expect(writeFileCalls[1][0]).toBe("serverless.yml");
+      expect(writeFileCalls[1][1]).toBe(MockFactory.createTestServerlessYml(true, expectedFunctionsYml));
+    });
+  });
+
+  describe("Remove command", () => {
+
+    beforeAll(() => {
+      mockFs({
+        "index.js": "contents",
+        "function1": {
+          "function.json": "contents",
+        },
+      });
+    });
+
+    afterAll(() => {
+      mockFs.restore();
+    });
+
+    it("returns with missing name", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const service = createService(sls, options);
+      await service.remove();
+      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove")
+    });
+
+    it("returns with non-existing function", async () => {
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      options["name"] = "myNonExistingFunction";
+      const service = createService(sls, options);
+      await service.remove();
+      expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist");
+    });
+
+    it("deletes directory and updates serverless.yml", async () => {
+      mockFs({
+        "hello.js": "contents",
+        hello: {
+          "function.json": "contents",
+        }
+      });
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const functionName = "hello";
+      options["name"] = functionName;
+      const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+      const rimrafSpy = jest.spyOn(rimraf, "sync");
+      const service = createService(sls, options);
+      await service.remove();
+      expect(unlinkSpy).toBeCalledWith(`${functionName}.js`)
+      expect(rimrafSpy).toBeCalledWith(functionName);
+      unlinkSpy.mockRestore();
+      rimrafSpy.mockRestore();
+      const expectedFunctionsYml = MockFactory.createTestSlsFunctionConfig();
+      delete expectedFunctionsYml[functionName];
+      expect(sls.utils.writeFileSync).toBeCalledWith("serverless.yml", MockFactory.createTestServerlessYml(true, expectedFunctionsYml))
+    });
+
+    it("does not try to delete file or directory if they don't exist", async () => {
+      mockFs({})
+      const sls = MockFactory.createTestServerless();
+      const options = MockFactory.createTestServerlessOptions();
+      const functionName = "hello";
+      options["name"] = functionName;
+      const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+      const rimrafSpy = jest.spyOn(rimraf, "sync");
+      const service = createService(sls, options);
+      await service.remove();
+      expect(unlinkSpy).not.toBeCalled();
+      expect(rimrafSpy).not.toBeCalled();
+      unlinkSpy.mockRestore();
+      rimrafSpy.mockRestore();
+      const expectedFunctionsYml = MockFactory.createTestSlsFunctionConfig();
+      delete expectedFunctionsYml[functionName];
+      expect(sls.utils.writeFileSync).toBeCalledWith("serverless.yml", MockFactory.createTestServerlessYml(true, expectedFunctionsYml))
+    });
+  });
+});
diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts
index eebc61a..e81cef8 100644
--- a/src/services/functionAppService.test.ts
+++ b/src/services/functionAppService.test.ts
@@ -254,4 +254,4 @@ describe("Function App Service", () => {
     const service = createService(sls);
     await expect(service.uploadFunctions(app)).rejects.not.toBeNull()
   });
-});
\ No newline at end of file
+});
diff --git a/src/services/invokeService.test.ts b/src/services/invokeService.test.ts
new file mode 100644
index 0000000..0dd751a
--- /dev/null
+++ b/src/services/invokeService.test.ts
@@ -0,0 +1,73 @@
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { MockFactory } from "../test/mockFactory";
+import { InvokeService } from "./invokeService";
+jest.mock("@azure/arm-appservice")
+jest.mock("@azure/arm-resources")
+jest.mock("./functionAppService")
+import { FunctionAppService } from "./functionAppService";
+
+describe("Invoke Service ", () => {
+  const app = MockFactory.createTestSite();
+  const expectedSite = MockFactory.createTestSite();
+  const testData = "test-data";
+  const testResult = "test-data";
+  const authKey = "authKey";
+  const baseUrl = "https://management.azure.com"
+  const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`;
+  const authKeyUrl = `${baseUrl}${app.id}/functions/admin/token?api-version=2016-08-01`;
+  let urlPOST = `http://${app.defaultHostName}/api/hello`;
+  let urlGET = `http://${app.defaultHostName}/api/hello?name%3D${testData}`;
+  let masterKey: string;
+
+  beforeAll(() => {
+    const axiosMock = new MockAdapter(axios);
+    // Master Key
+    axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey });
+    // Auth Key
+    axiosMock.onGet(authKeyUrl).reply(200, authKey);
+    //Mock url for GET
+    axiosMock.onGet(urlGET).reply(200, testResult);
+    //Mock url for POST
+    axiosMock.onPost(urlPOST).reply(200, testResult);
+  });
+  
+  beforeEach(() => {
+    FunctionAppService.prototype.getMasterKey = jest.fn();
+    FunctionAppService.prototype.get = jest.fn(() => Promise.resolve(expectedSite));
+  });
+
+  it("Invokes a function with GET request", async () => {
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const expectedResult = {url: `${app.defaultHostName}/api/hello`};
+    const httpConfig = jest.fn(() => expectedResult);
+
+    FunctionAppService.prototype.getFunctionHttpTriggerConfig = httpConfig as any;
+
+    options["function"] = "hello";
+    options["data"] = `{"name": "${testData}"}`;
+    options["method"] = "GET";
+
+    const service = new InvokeService(sls, options);
+    const response = await service.invoke(options["method"], options["function"], options["data"]);
+    expect(JSON.stringify(response.data)).toEqual(JSON.stringify(testResult));
+  });
+
+  it("Invokes a function with POST request", async () => {
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const expectedResult = {url: `${app.defaultHostName}/api/hello`};
+    const httpConfig = jest.fn(() => expectedResult);
+    FunctionAppService.prototype.getFunctionHttpTriggerConfig = httpConfig as any;
+
+    options["function"] = "hello";
+    options["data"] = `{"name": "${testData}"}`;
+    options["method"] = "POST";
+
+    const service = new InvokeService(sls, options);
+    const response = await service.invoke(options["method"], options["function"], options["data"]);
+    expect(JSON.stringify(response.data)).toEqual(JSON.stringify(testResult));
+  });
+  
+});
\ No newline at end of file
diff --git a/src/services/offlineService.test.ts b/src/services/offlineService.test.ts
index c416800..979dc0c 100644
--- a/src/services/offlineService.test.ts
+++ b/src/services/offlineService.test.ts
@@ -44,6 +44,19 @@ describe("Offline Service", () => {
     writeFileSpy.mockRestore();
   });
 
+  it("does not write files if they already exist", async () => {
+    const sls = MockFactory.createTestServerless();
+    const service = createService(sls);
+    const functionNames = sls.service.getAllFunctions();
+    const writeFileSpy = jest.spyOn(fs, "writeFileSync");
+    mockFs({
+      "local.settings.json": "contents",
+    });
+    await service.build();
+    const calls = writeFileSpy.mock.calls;
+    expect(calls).toHaveLength(functionNames.length);
+  });
+
   it("cleans up functions files", async () => {
     mockFs({
       hello: {
@@ -70,6 +83,31 @@ describe("Offline Service", () => {
     rmdirSpy.mockRestore();
   });
 
+  it("does not try to remove files if they don't exist", async () => {
+    mockFs({
+      hello: {
+        "function.json": "contents"
+      },
+      goodbye: {
+        "function.json": "contents"
+      },
+    })
+    const sls = MockFactory.createTestServerless();
+    const service = createService(sls);
+    const unlinkSpy = jest.spyOn(fs, "unlinkSync");
+    const rmdirSpy = jest.spyOn(fs, "rmdirSync")
+    await service.cleanup();
+    const unlinkCalls = unlinkSpy.mock.calls;
+    expect(unlinkCalls).toHaveLength(2);
+    expect(unlinkCalls[0][0]).toBe(`hello${path.sep}function.json`);
+    expect(unlinkCalls[1][0]).toBe(`goodbye${path.sep}function.json`);
+    const rmdirCalls = rmdirSpy.mock.calls;
+    expect(rmdirCalls[0][0]).toBe("hello");
+    expect(rmdirCalls[1][0]).toBe("goodbye");
+    unlinkSpy.mockRestore();
+    rmdirSpy.mockRestore();
+  });
+
   it("instructs users how to run locally", async () => {
     const sls = MockFactory.createTestServerless();
     const service = createService(sls);
@@ -79,4 +117,4 @@ describe("Offline Service", () => {
     // the user how to do it.
     expect(sls.cli.log).toBeCalledTimes(3);
   });
-});
\ No newline at end of file
+});