Repository: serverless/serverless-azure-functions
Pull request #148 (2019-05-24 20:21:40)

test: Unit tests for deploy, login, package, delete and APIM plugins
- Refactored Azure login logic into `AzureLoginService`
- Added helper util for tests (`invokeHook`)
- Added more functionality to Mock Factory
- All tested plugins have 100% test coverage


Patch:
diff --git a/package-lock.json b/package-lock.json
index 025338f..04d35d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6794,9 +6794,9 @@
       "dev": true
     },
     "nan": {
-      "version": "2.13.2",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
-      "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
+      "version": "2.14.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+      "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
       "optional": true
     },
     "nanomatch": {
diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts
index e2425e2..75072d3 100644
--- a/src/plugins/login/loginPlugin.ts
+++ b/src/plugins/login/loginPlugin.ts
@@ -1,7 +1,6 @@
-import open from 'open';
-import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse } from '@azure/ms-rest-nodeauth';
 import Serverless from 'serverless';
 import AzureProvider from '../../provider/azureProvider';
+import { AzureLoginService } from '../../services/loginService';
 
 export class AzureLoginPlugin {
   private provider: AzureProvider;
@@ -23,32 +22,16 @@ export class AzureLoginPlugin {
 
     this.serverless.cli.log('Logging into Azure');
 
-    let authResult = null;
-
-    const subscriptionId = process.env.azureSubId;
-    const clientId = process.env.azureServicePrincipalClientId;
-    const secret = process.env.azureServicePrincipalPassword;
-    const tenantId = process.env.azureServicePrincipalTenantId;
-
     try {
-      if (subscriptionId && clientId && secret && tenantId) {
-        authResult = await loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId);
-      }
-      else {
-        await open('https://microsoft.com/devicelogin');
-        authResult = await interactiveLoginWithAuthResponse();
-      }
-
-      // TODO: This is temporary until the azure provider goes away
-      this.provider.credentials = authResult.credentials;
-
-      this.serverless.variables['azureAccessToken'] = authResult.credentials.tokenCache._entries[0].accessToken;
+      const authResult = await AzureLoginService.login();
       this.serverless.variables['azureCredentials'] = authResult.credentials;
-      this.serverless.variables['subscriptionId'] = authResult.subscriptionId || subscriptionId;
+      // Use environment variable for sub ID or use the first subscription in the list (service principal can
+      // have access to more than one subscription)
+      this.serverless.variables['subscriptionId'] = process.env.azureSubId || authResult.subscriptions[0].id;
     }
     catch (e) {
       this.serverless.cli.log('Error logging into azure');
-      this.serverless.cli.log(e);
+      this.serverless.cli.log(`${e}`);
     }
   }
 }
\ No newline at end of file
diff --git a/src/plugins/package/azurePackage.ts b/src/plugins/package/azurePackage.ts
index 7d44762..1eb4446 100644
--- a/src/plugins/package/azurePackage.ts
+++ b/src/plugins/package/azurePackage.ts
@@ -1,8 +1,8 @@
 
 import Serverless from 'serverless';
 import AzureProvider from '../../provider/azureProvider';
-import { createEventsBindings } from '../../shared/bindings';
-import { getFunctionMetaData } from '../../shared/utils';
+import { BindingUtils } from '../../shared/bindings';
+import { Utils } from '../../shared/utils';
 
 export class AzurePackage {
   provider: AzureProvider
@@ -19,9 +19,9 @@ export class AzurePackage {
 
     const createEventsPromises = this.serverless.service.getAllFunctions()
       .map((functionName) => {
-        const metaData = getFunctionMetaData(functionName, this.serverless);
+        const metaData = Utils.getFunctionMetaData(functionName, this.serverless);
 
-        return createEventsBindings(this.serverless.config.servicePath, functionName, metaData);
+        return BindingUtils.createEventsBindings(this.serverless.config.servicePath, functionName, metaData);
       });
 
     return Promise.all(createEventsPromises);
diff --git a/src/services/loginService.ts b/src/services/loginService.ts
new file mode 100644
index 0000000..1faa24b
--- /dev/null
+++ b/src/services/loginService.ts
@@ -0,0 +1,26 @@
+import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse } from '@azure/ms-rest-nodeauth';
+
+export class AzureLoginService {
+
+  public static async login() {
+    const subscriptionId = process.env.azureSubId;
+    const clientId = process.env.azureServicePrincipalClientId;
+    const secret = process.env.azureServicePrincipalPassword;
+    const tenantId = process.env.azureServicePrincipalTenantId;
+
+    if (subscriptionId && clientId && secret && tenantId) {
+      return await AzureLoginService.servicePrincipalLogin(clientId, secret, tenantId);
+    } else {
+      return await AzureLoginService.interactiveLogin();
+    }
+  }
+
+  public static async interactiveLogin() {
+    await open('https://microsoft.com/devicelogin');
+    return await interactiveLoginWithAuthResponse();
+  }
+
+  public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string) {
+    return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId);
+  }
+}
\ No newline at end of file
diff --git a/src/shared/bindings.ts b/src/shared/bindings.ts
index a4c70c6..c108072 100644
--- a/src/shared/bindings.ts
+++ b/src/shared/bindings.ts
@@ -2,50 +2,128 @@ import { writeFileSync } from 'fs';
 import { join } from 'path';
 import Serverless from 'serverless';
 import { FunctionMetadata } from './utils';
-const bindingsJson = require('./bindings.json');
-
-const constants = {
-  bindings: 'bindings',
-  settings: 'settings',
-  name: 'name',
-  displayName: 'displayName',
-  type: 'type'
-};
+import { constants } from './constants';
 
-export function getBindingsMetaData(serverless: Serverless) {
-  const bindingDisplayNames = [];
-  const bindingTypes = [];
-  const bindingSettings = [];
-  const bindingSettingsNames = [];
-
-  serverless.cli.log('Parsing Azure Functions Bindings.json...');
+const bindingsJson = require('./bindings.json');
 
-  for (let bindingsIndex = 0; bindingsIndex < bindingsJson[constants.bindings].length; bindingsIndex++) {
-    const settingsNames = [];
+export class BindingUtils {
+  public static getBindingsMetaData(serverless: Serverless) {
+    const bindingDisplayNames = [];
+    const bindingTypes = [];
+    const bindingSettings = [];
+    const bindingSettingsNames = [];
+  
+    serverless.cli.log('Parsing Azure Functions Bindings.json...');
+  
+    for (let bindingsIndex = 0; bindingsIndex < bindingsJson[constants.bindings].length; bindingsIndex++) {
+      const settingsNames = [];
+  
+      bindingTypes.push(bindingsJson[constants.bindings][bindingsIndex][constants.type]);
+      bindingDisplayNames.push(bindingsJson[constants.bindings][bindingsIndex][constants.displayName].toLowerCase());
+      bindingSettings[bindingsIndex] = bindingsJson[constants.bindings][bindingsIndex][constants.settings];
+  
+      for (let bindingSettingsIndex = 0; bindingSettingsIndex < bindingSettings[bindingsIndex].length; bindingSettingsIndex++) {
+        settingsNames.push(bindingSettings[bindingsIndex][bindingSettingsIndex][constants.name]);
+      }
+  
+      bindingSettingsNames[bindingsIndex] = settingsNames;
+    }
+  
+    return {
+      bindingDisplayNames: bindingDisplayNames,
+      bindingTypes: bindingTypes,
+      bindingSettings: bindingSettings,
+      bindingSettingsNames: bindingSettingsNames
+    };
+  }
 
-    bindingTypes.push(bindingsJson[constants.bindings][bindingsIndex][constants.type]);
-    bindingDisplayNames.push(bindingsJson[constants.bindings][bindingsIndex][constants.displayName].toLowerCase());
-    bindingSettings[bindingsIndex] = bindingsJson[constants.bindings][bindingsIndex][constants.settings];
+  public static createEventsBindings(servicePath: string, functionName: string, functionMetadata: FunctionMetadata): Promise<any> {
+    const functionJSON = functionMetadata.params.functionsJson;
+    functionJSON.entryPoint = functionMetadata.entryPoint;
+    functionJSON.scriptFile = functionMetadata.handlerPath;
+    writeFileSync(join(servicePath, functionName, 'function.json'), JSON.stringify(functionJSON, null, 4));
+    return Promise.resolve();
+  }
 
-    for (let bindingSettingsIndex = 0; bindingSettingsIndex < bindingSettings[bindingsIndex].length; bindingSettingsIndex++) {
-      settingsNames.push(bindingSettings[bindingsIndex][bindingSettingsIndex][constants.name]);
+  public static getBindingUserSettingsMetaData(azureSettings, bindingType, bindingTypeIndex, bindingDisplayNames) {
+    let bindingDisplayNamesIndex = bindingTypeIndex;
+    const bindingUserSettings = {};
+  
+    if (azureSettings) {
+      const directionIndex = Object.keys(azureSettings).indexOf(constants.direction);
+  
+      if (directionIndex >= 0) {
+        const key = Object.keys(azureSettings)[directionIndex];
+        const displayName = `$${bindingType}${azureSettings[key]}_displayName`;
+  
+        bindingDisplayNamesIndex = bindingDisplayNames.indexOf(displayName.toLowerCase());
+        bindingUserSettings[constants.direction] = azureSettings[key];
+      }
     }
+    const bindingUserSettingsMetaData = {
+      index: bindingDisplayNamesIndex,
+      userSettings: bindingUserSettings
+    };
+  
+    return bindingUserSettingsMetaData;
+  }
 
-    bindingSettingsNames[bindingsIndex] = settingsNames;
+  public static getHttpOutBinding(bindingUserSettings) {
+    const binding = {};
+  
+    binding[constants.type] = 'http';
+    binding[constants.direction] = constants.outDirection;
+    binding[constants.name] = '$return';
+    if (bindingUserSettings[constants.webHookType]) {
+      binding[constants.name] = 'res';
+    }
+  
+    return binding;
   }
 
-  return {
-    bindingDisplayNames: bindingDisplayNames,
-    bindingTypes: bindingTypes,
-    bindingSettings: bindingSettings,
-    bindingSettingsNames: bindingSettingsNames
-  };
+  public static getBinding(bindingType, bindingSettings, bindingUserSettings) {
+    const binding = {};
+  
+    binding[constants.type] = bindingType;
+    if (bindingUserSettings && bindingUserSettings[constants.direction]) {
+      binding[constants.direction] = bindingUserSettings[constants.direction];
+    } else if (bindingType.includes(constants.trigger)) {
+      binding[constants.direction] = constants.inDirection;
+    } else {
+      binding[constants.direction] = constants.outDirection;
+    }
+  
+    for (let bindingSettingsIndex = 0; bindingSettingsIndex < bindingSettings.length; bindingSettingsIndex++) {
+      const name = bindingSettings[bindingSettingsIndex][constants.name];
+  
+      if (bindingUserSettings && bindingUserSettings[name] !== undefined && bindingUserSettings[name] !== null) {
+        binding[name] = bindingUserSettings[name];
+        continue;
+      }
+      const value = bindingSettings[bindingSettingsIndex][constants.value];
+      const required = bindingSettings[bindingSettingsIndex][constants.required];
+      const resource = bindingSettings[bindingSettingsIndex][constants.resource];
+  
+      if (required) {
+        const defaultValue = bindingSettings[bindingSettingsIndex][constants.defaultValue];
+  
+        if (defaultValue) {
+          binding[name] = defaultValue;
+        } else if (name === constants.connection && resource.toLowerCase() === constants.storage) {
+          binding[name] = 'AzureWebJobsStorage';
+        } else {
+          throw new Error(`Required property ${name} is missing for binding:${bindingType}`);
+        }
+      }
+  
+      if (value === constants.enum && name !== constants.webHookType) {
+        const enumValues = bindingSettings[bindingSettingsIndex][constants.enum];
+  
+        binding[name] = enumValues[0][constants.value];
+      }
+    }
+  
+    return binding;
+  }
+  
 }
-
-export async function createEventsBindings(servicePath: string, functionName: string, functionMetadata: FunctionMetadata): Promise<any> {
-  const functionJSON = functionMetadata.params.functionsJson;
-  functionJSON.entryPoint = functionMetadata.entryPoint;
-  functionJSON.scriptFile = functionMetadata.handlerPath;
-  writeFileSync(join(servicePath, functionName, 'function.json'), JSON.stringify(functionJSON, null, 4));
-  return Promise.resolve();
-}
\ No newline at end of file
diff --git a/src/shared/constants.ts b/src/shared/constants.ts
new file mode 100644
index 0000000..a9dd351
--- /dev/null
+++ b/src/shared/constants.ts
@@ -0,0 +1,24 @@
+export const constants = {
+  bindings: 'bindings',
+  settings: 'settings',
+  name: 'name',
+  displayName: 'displayName',
+  type: 'type',
+  direction: 'direction',
+  trigger: 'Trigger',
+  inDirection: 'in',
+  outDirection: 'out',
+  value: 'value',
+  resource: 'resource',
+  required: 'required',
+  storage: 'storage',
+  connection: 'connection',
+  enum: 'enum',
+  defaultValue: 'defaultValue',
+  webHookType: 'webHookType',
+  httpTrigger: 'httpTrigger',
+  queue: 'queue',
+  queueName: 'queueName',
+  xAzureSettings: 'x-azure-settings',
+  entryPoint: 'entryPoint'
+}
\ No newline at end of file
diff --git a/src/shared/utils.ts b/src/shared/utils.ts
index 05cd035..5523468 100644
--- a/src/shared/utils.ts
+++ b/src/shared/utils.ts
@@ -1,28 +1,5 @@
-import { getBindingsMetaData } from "./bindings";
-
-const constants = {
-  type: 'type',
-  direction: 'direction',
-  trigger: 'Trigger',
-  inDirection: 'in',
-  outDirection: 'out',
-  settings: 'settings',
-  name: 'name',
-  value: 'value',
-  resource: 'resource',
-  required: 'required',
-  storage: 'storage',
-  connection: 'connection',
-  enum: 'enum',
-  defaultValue: 'defaultValue',
-  webHookType: 'webHookType',
-  httpTrigger: 'httpTrigger',
-  queue: 'queue',
-  queueName: 'queueName',
-  displayName: 'displayName',
-  xAzureSettings: 'x-azure-settings',
-  entryPoint: 'entryPoint'
-};
+import { BindingUtils } from "./bindings";
+import { constants } from "./constants";
 
 export interface FunctionMetadata {
   entryPoint: any;
@@ -30,186 +7,107 @@ export interface FunctionMetadata {
   params: any;
 }
 
-export function getFunctionMetaData(functionName, serverless): FunctionMetadata {
-  const bindings = [];
-  let bindingSettingsNames = [];
-  let bindingSettings = [];
-  let bindingUserSettings = {};
-  let bindingType;
-  const functionsJson = { disabled: false, bindings: [] };
-  const functionObject = serverless.service.getFunction(functionName);
-  const handler = functionObject.handler;
-  const events = functionObject.events;
-  const params: any = {
-    functionJson: null
-  };
-
-  const parsedBindings = getBindingsMetaData(serverless);
-
-  const bindingTypes = parsedBindings.bindingTypes;
-  const bindingDisplayNames = parsedBindings.bindingDisplayNames;
-
-  for (let eventsIndex = 0; eventsIndex < events.length; eventsIndex++) {
-    bindingType = Object.keys(functionObject.events[eventsIndex])[0];
-
-    if (eventsIndex === 0) {
-      bindingType += constants.trigger;
-    }
-
-    const index = bindingTypes.indexOf(bindingType);
-
-    if (index < 0) {
-      throw new Error(`Binding  ${bindingType} not supported`);
-    }
-
-    serverless.cli.log(`Building binding for function: ${functionName} event: ${bindingType}`);
-
-    bindingUserSettings = {};
-    const azureSettings = events[eventsIndex][constants.xAzureSettings];
-    let bindingTypeIndex = bindingTypes.indexOf(bindingType);
-    const bindingUserSettingsMetaData = this.getBindingUserSettingsMetaData(azureSettings, bindingType, bindingTypeIndex, bindingDisplayNames);
-
-    bindingTypeIndex = bindingUserSettingsMetaData.index;
-    bindingUserSettings = bindingUserSettingsMetaData.userSettings;
-
-    if (bindingType.includes(constants.queue) && functionObject.events[eventsIndex].queue) {
-      bindingUserSettings[constants.queueName] = functionObject.events[eventsIndex].queue;
-    }
-
-    if (bindingTypeIndex < 0) {
-      throw new Error('Binding not supported');
-    }
-
-    bindingSettings = parsedBindings.bindingSettings[bindingTypeIndex];
-    bindingSettingsNames = parsedBindings.bindingSettingsNames[bindingTypeIndex];
-
-    if (azureSettings) {
-      for (let azureSettingKeyIndex = 0; azureSettingKeyIndex < Object.keys(azureSettings).length; azureSettingKeyIndex++) {
-        const key = Object.keys(azureSettings)[azureSettingKeyIndex];
-
-        if (bindingSettingsNames.indexOf(key) >= 0) {
-          bindingUserSettings[key] = azureSettings[key];
+export class Utils {
+  public static getFunctionMetaData(functionName: string, serverless): FunctionMetadata {
+    const bindings = [];
+    let bindingSettingsNames = [];
+    let bindingSettings = [];
+    let bindingUserSettings = {};
+    let bindingType;
+    const functionsJson = { disabled: false, bindings: [] };
+    const functionObject = serverless.service.getFunction(functionName);
+    const handler = functionObject.handler;
+    const events = functionObject.events;
+    const params: any = {
+      functionJson: null
+    };
+  
+    const parsedBindings = BindingUtils.getBindingsMetaData(serverless);
+  
+    const bindingTypes = parsedBindings.bindingTypes;
+    const bindingDisplayNames = parsedBindings.bindingDisplayNames;
+  
+    for (let eventsIndex = 0; eventsIndex < events.length; eventsIndex++) {
+      bindingType = Object.keys(functionObject.events[eventsIndex])[0];
+  
+      if (eventsIndex === 0) {
+        bindingType += constants.trigger;
+      }
+  
+      const index = bindingTypes.indexOf(bindingType);
+  
+      if (index < 0) {
+        throw new Error(`Binding  ${bindingType} not supported`);
+      }
+  
+      serverless.cli.log(`Building binding for function: ${functionName} event: ${bindingType}`);
+  
+      bindingUserSettings = {};
+      const azureSettings = events[eventsIndex][constants.xAzureSettings];
+      let bindingTypeIndex = bindingTypes.indexOf(bindingType);
+      const bindingUserSettingsMetaData = BindingUtils.getBindingUserSettingsMetaData(azureSettings, bindingType, bindingTypeIndex, bindingDisplayNames);
+  
+      bindingTypeIndex = bindingUserSettingsMetaData.index;
+      bindingUserSettings = bindingUserSettingsMetaData.userSettings;
+  
+      if (bindingType.includes(constants.queue) && functionObject.events[eventsIndex].queue) {
+        bindingUserSettings[constants.queueName] = functionObject.events[eventsIndex].queue;
+      }
+  
+      if (bindingTypeIndex < 0) {
+        throw new Error('Binding not supported');
+      }
+  
+      bindingSettings = parsedBindings.bindingSettings[bindingTypeIndex];
+      bindingSettingsNames = parsedBindings.bindingSettingsNames[bindingTypeIndex];
+  
+      if (azureSettings) {
+        for (let azureSettingKeyIndex = 0; azureSettingKeyIndex < Object.keys(azureSettings).length; azureSettingKeyIndex++) {
+          const key = Object.keys(azureSettings)[azureSettingKeyIndex];
+  
+          if (bindingSettingsNames.indexOf(key) >= 0) {
+            bindingUserSettings[key] = azureSettings[key];
+          }
         }
       }
+  
+      bindings.push(BindingUtils.getBinding(bindingType, bindingSettings, bindingUserSettings));
     }
-
-    bindings.push(this.getBinding(bindingType, bindingSettings, bindingUserSettings, serverless));
-  }
-
-  if (bindingType === constants.httpTrigger) {
-    bindings.push(this.getHttpOutBinding(bindingUserSettings));
-  }
-
-  functionsJson.bindings = bindings;
-  params.functionsJson = functionsJson;
-
-  const entryPointAndHandlerPath = this.getEntryPointAndHandlerPath(handler);
-  if (functionObject.scriptFile) {
-    entryPointAndHandlerPath.handlerPath = functionObject.scriptFile;
-  }
-  const metaData = {
-    entryPoint: entryPointAndHandlerPath[constants.entryPoint],
-    handlerPath: entryPointAndHandlerPath.handlerPath,
-    params: params
-  };
-
-  return metaData;
-};
-
-export function getBindingUserSettingsMetaData(azureSettings, bindingType, bindingTypeIndex, bindingDisplayNames) {
-  let bindingDisplayNamesIndex = bindingTypeIndex;
-  const bindingUserSettings = {};
-
-  if (azureSettings) {
-    const directionIndex = Object.keys(azureSettings).indexOf(constants.direction);
-
-    if (directionIndex >= 0) {
-      const key = Object.keys(azureSettings)[directionIndex];
-      const displayName = `$${bindingType}${azureSettings[key]}_displayName`;
-
-      bindingDisplayNamesIndex = bindingDisplayNames.indexOf(displayName.toLowerCase());
-      bindingUserSettings[constants.direction] = azureSettings[key];
+  
+    if (bindingType === constants.httpTrigger) {
+      bindings.push(BindingUtils.getHttpOutBinding(bindingUserSettings));
     }
-  }
-  const bindingUserSettingsMetaData = {
-    index: bindingDisplayNamesIndex,
-    userSettings: bindingUserSettings
-  };
-
-  return bindingUserSettingsMetaData;
-};
-
-export function getEntryPointAndHandlerPath(handler) {
-  let handlerPath = 'handler.js';
-  let entryPoint = handler;
-  const handlerSplit = handler.split('.');
-
-  if (handlerSplit.length > 1) {
-    entryPoint = handlerSplit[handlerSplit.length - 1];
-    handlerPath = `${handler.substring(0, handler.lastIndexOf('.'))}.js`;
-  }
-  const metaData = {
-    entryPoint: entryPoint,
-    handlerPath: handlerPath
-  };
-
-  return metaData;
-};
-
-export function getHttpOutBinding(bindingUserSettings) {
-  const binding = {};
-
-  binding[constants.type] = 'http';
-  binding[constants.direction] = constants.outDirection;
-  binding[constants.name] = '$return';
-  if (bindingUserSettings[constants.webHookType]) {
-    binding[constants.name] = 'res';
-  }
-
-  return binding;
-}
-
-export function getBinding(bindingType, bindingSettings, bindingUserSettings) {
-  const binding = {};
-
-  binding[constants.type] = bindingType;
-  if (bindingUserSettings && bindingUserSettings[constants.direction]) {
-    binding[constants.direction] = bindingUserSettings[constants.direction];
-  } else if (bindingType.includes(constants.trigger)) {
-    binding[constants.direction] = constants.inDirection;
-  } else {
-    binding[constants.direction] = constants.outDirection;
-  }
-
-  for (let bindingSettingsIndex = 0; bindingSettingsIndex < bindingSettings.length; bindingSettingsIndex++) {
-    const name = bindingSettings[bindingSettingsIndex][constants.name];
-
-    if (bindingUserSettings && bindingUserSettings[name] !== undefined && bindingUserSettings[name] !== null) {
-      binding[name] = bindingUserSettings[name];
-      continue;
-    }
-    const value = bindingSettings[bindingSettingsIndex][constants.value];
-    const required = bindingSettings[bindingSettingsIndex][constants.required];
-    const resource = bindingSettings[bindingSettingsIndex][constants.resource];
-
-    if (required) {
-      const defaultValue = bindingSettings[bindingSettingsIndex][constants.defaultValue];
-
-      if (defaultValue) {
-        binding[name] = defaultValue;
-      } else if (name === constants.connection && resource.toLowerCase() === constants.storage) {
-        binding[name] = 'AzureWebJobsStorage';
-      } else {
-        throw new Error(`Required property ${name} is missing for binding:${bindingType}`);
-      }
+  
+    functionsJson.bindings = bindings;
+    params.functionsJson = functionsJson;
+  
+    const entryPointAndHandlerPath = Utils.getEntryPointAndHandlerPath(handler);
+    if (functionObject.scriptFile) {
+      entryPointAndHandlerPath.handlerPath = functionObject.scriptFile;
     }
+    const metaData = {
+      entryPoint: entryPointAndHandlerPath[constants.entryPoint],
+      handlerPath: entryPointAndHandlerPath.handlerPath,
+      params: params
+    };
+  
+    return metaData;
+  }
 
-    if (value === constants.enum && name !== constants.webHookType) {
-      const enumValues = bindingSettings[bindingSettingsIndex][constants.enum];
-
-      binding[name] = enumValues[0][constants.value];
+  public static getEntryPointAndHandlerPath(handler) {
+    let handlerPath = 'handler.js';
+    let entryPoint = handler;
+    const handlerSplit = handler.split('.');
+  
+    if (handlerSplit.length > 1) {
+      entryPoint = handlerSplit[handlerSplit.length - 1];
+      handlerPath = `${handler.substring(0, handler.lastIndexOf('.'))}.js`;
     }
+    const metaData = {
+      entryPoint: entryPoint,
+      handlerPath: handlerPath
+    };
+  
+    return metaData;
   }
-
-  return binding;
-};
+}


Test patch:
diff --git a/src/index.test.ts b/src/index.test.ts
deleted file mode 100644
index 8b3dc44..0000000
--- a/src/index.test.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-
-describe("Index test", () => {
-  it("should run a test", () => {
-    expect(true).toBe(true);
-  });
-});
\ No newline at end of file
diff --git a/src/plugins/apim/apimFunctionPlugin.test.ts b/src/plugins/apim/apimFunctionPlugin.test.ts
new file mode 100644
index 0000000..d7a02af
--- /dev/null
+++ b/src/plugins/apim/apimFunctionPlugin.test.ts
@@ -0,0 +1,25 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import { AzureApimFunctionPlugin } from './apimFunctionPlugin';
+
+jest.mock('../../services/apimService');
+import { ApimService } from '../../services/apimService';
+
+describe('APIM Function Plugin', () => {
+  it('calls deploy function', async () => {
+    const deployFunction = jest.fn();
+
+    ApimService.prototype.deployFunction = deployFunction;
+
+    const sls = MockFactory.createTestServerless();
+    sls.service.provider['apim'] = 'apim config'
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureApimFunctionPlugin(sls, options);
+
+    await invokeHook(plugin, 'after:deploy:function:deploy');
+
+    expect(sls.cli.log).toBeCalledWith('Starting APIM function deployment')
+    expect(deployFunction).toBeCalled();
+    expect(sls.cli.log).lastCalledWith('Finished APIM function deployment')
+  });
+});
\ No newline at end of file
diff --git a/src/plugins/apim/apimServicePlugin.test.ts b/src/plugins/apim/apimServicePlugin.test.ts
index a407594..c17a705 100644
--- a/src/plugins/apim/apimServicePlugin.test.ts
+++ b/src/plugins/apim/apimServicePlugin.test.ts
@@ -1,6 +1,11 @@
 import Serverless from 'serverless';
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
 import { AzureApimServicePlugin } from './apimServicePlugin';
 
+jest.mock('../../services/apimService');
+import { ApimService } from '../../services/apimService';
+
 describe('APIM Service Plugin', () => {
   it('is defined', () => {
     expect(AzureApimServicePlugin).toBeDefined();
@@ -16,4 +21,42 @@ describe('APIM Service Plugin', () => {
 
     expect(plugin).not.toBeNull();
   });
+
+  it('calls deploy API and deploy functions', async () => {
+    const deployApi = jest.fn();
+    const deployFunctions = jest.fn();
+
+    ApimService.prototype.deployApi = deployApi;
+    ApimService.prototype.deployFunctions = deployFunctions;
+
+    const sls = MockFactory.createTestServerless();
+    sls.service.provider['apim'] = 'apim config'
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureApimServicePlugin(sls, options);
+
+    await invokeHook(plugin, 'after:deploy:deploy');
+
+    expect(sls.cli.log).toBeCalledWith('Starting APIM service deployment')
+    expect(deployApi).toBeCalled();
+    expect(deployFunctions).toBeCalled();
+    expect(sls.cli.log).lastCalledWith('Finished APIM service deployment')
+  });
+
+  it('does not call deploy API or deploy functions when "apim" not included in config', async () => {
+    const deployApi = jest.fn();
+    const deployFunctions = jest.fn();
+
+    ApimService.prototype.deployApi = deployApi;
+    ApimService.prototype.deployFunctions = deployFunctions;
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureApimServicePlugin(sls, options);
+
+    await invokeHook(plugin, 'after:deploy:deploy');
+
+    expect(sls.cli.log).not.toBeCalled()
+    expect(deployApi).not.toBeCalled();
+    expect(deployFunctions).not.toBeCalled();
+  });
 });
\ No newline at end of file
diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts
new file mode 100644
index 0000000..05d3d2f
--- /dev/null
+++ b/src/plugins/deploy/azureDeployPlugin.test.ts
@@ -0,0 +1,33 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import { AzureDeployPlugin } from "./azureDeployPlugin";
+
+jest.mock("../../services/functionAppService");
+import { FunctionAppService } from "../../services/functionAppService";
+
+jest.mock("../../services/resourceService");
+import { ResourceService } from "../../services/resourceService";
+
+describe('Deploy plugin', () => {
+  
+  it('calls deploy hook', async () => {
+    const deployResourceGroup = jest.fn();
+    const functionAppStub = "Function App Stub";
+    const deploy = jest.fn(() => Promise.resolve(functionAppStub));
+    const uploadFunctions = jest.fn();
+
+    ResourceService.prototype.deployResourceGroup = deployResourceGroup
+    FunctionAppService.prototype.deploy = deploy
+    FunctionAppService.prototype.uploadFunctions = uploadFunctions
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureDeployPlugin(sls, options);
+    
+    await invokeHook(plugin, 'deploy:deploy');
+    
+    expect(deployResourceGroup).toBeCalled();
+    expect(deploy).toBeCalled();
+    expect(uploadFunctions).toBeCalledWith(functionAppStub);
+  });
+});
\ No newline at end of file
diff --git a/src/plugins/login/loginPlugin.test.ts b/src/plugins/login/loginPlugin.test.ts
new file mode 100644
index 0000000..86f9eea
--- /dev/null
+++ b/src/plugins/login/loginPlugin.test.ts
@@ -0,0 +1,123 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import { AzureLoginPlugin } from "./loginPlugin";
+import { AzureLoginService } from "../../services/loginService";
+
+describe('Login Plugin', () => {
+
+  const authResponse = MockFactory.createTestAuthResponse();
+
+  it('returns if azure credentials are set', async () => {
+    const interactiveLogin = jest.fn(() => Promise.resolve(authResponse));
+    const servicePrincipalLogin = jest.fn(() => Promise.resolve(authResponse));
+
+    AzureLoginService.interactiveLogin = interactiveLogin;
+    AzureLoginService.servicePrincipalLogin = servicePrincipalLogin;
+
+    const sls = MockFactory.createTestServerless();
+    sls.variables['azureCredentials'] = 'credentials';
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureLoginPlugin(sls, options);
+
+    await invokeHook(plugin, 'before:package:initialize');
+
+    expect(interactiveLogin).not.toBeCalled();
+    expect(servicePrincipalLogin).not.toBeCalled();
+  });
+
+  it('calls login if azure credentials are not set', async () => {
+    const interactiveLogin = jest.fn(() => Promise.resolve(authResponse));
+    const servicePrincipalLogin = jest.fn(() => Promise.resolve(authResponse));
+
+    AzureLoginService.interactiveLogin = interactiveLogin;
+    AzureLoginService.servicePrincipalLogin = servicePrincipalLogin;
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureLoginPlugin(sls, options);
+
+    await invokeHook(plugin, 'before:package:initialize');
+
+    expect(interactiveLogin).toBeCalled();
+    expect(servicePrincipalLogin).not.toBeCalled();
+  });
+
+  it('calls service principal login if environment variables are set', async () => {
+
+    process.env.azureSubId = 'azureSubId';
+    process.env.azureServicePrincipalClientId = 'azureServicePrincipalClientId';
+    process.env.azureServicePrincipalPassword = 'azureServicePrincipalPassword';
+    process.env.azureServicePrincipalTenantId = 'azureServicePrincipalTenantId';
+
+    const interactiveLogin = jest.fn(() => Promise.resolve(authResponse));
+    const servicePrincipalLogin = jest.fn(() => Promise.resolve(authResponse));
+
+    AzureLoginService.interactiveLogin = interactiveLogin;
+    AzureLoginService.servicePrincipalLogin = servicePrincipalLogin;
+    
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureLoginPlugin(sls, options);
+    await invokeHook(plugin, 'before:package:initialize');
+    expect(servicePrincipalLogin).toBeCalledWith(
+      'azureServicePrincipalClientId',
+      'azureServicePrincipalPassword',
+      'azureServicePrincipalTenantId'
+    )
+    expect(interactiveLogin).not.toBeCalled();
+
+    expect(sls.variables['azureCredentials']).toEqual(authResponse.credentials);
+    expect(sls.variables['subscriptionId']).toEqual('azureSubId');
+  });
+
+  it('calls interactive login if environment variables are not set', async () => {
+    delete process.env.azureSubId;
+    delete process.env.azureServicePrincipalClientId;
+    delete process.env.azureServicePrincipalPassword;
+    delete process.env.azureServicePrincipalTenantId;
+
+    const interactiveLogin = jest.fn(() => Promise.resolve(authResponse));
+    const servicePrincipalLogin = jest.fn(() => Promise.resolve(authResponse));
+
+    AzureLoginService.interactiveLogin = interactiveLogin;
+    AzureLoginService.servicePrincipalLogin = servicePrincipalLogin;
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureLoginPlugin(sls, options);
+    await invokeHook(plugin, 'before:package:initialize');
+    expect(servicePrincipalLogin).not.toBeCalled();
+    expect(interactiveLogin).toBeCalled();
+
+    expect(sls.variables['azureCredentials']).toEqual(authResponse.credentials);
+    expect(sls.variables['subscriptionId']).toEqual('azureSubId');
+  });
+
+  it('logs an error from authentication', async () => {
+    process.env.azureSubId = 'azureSubId';
+    process.env.azureServicePrincipalClientId = 'azureServicePrincipalClientId';
+    process.env.azureServicePrincipalPassword = 'azureServicePrincipalPassword';
+    process.env.azureServicePrincipalTenantId = 'azureServicePrincipalTenantId';
+
+    const interactiveLogin = jest.fn(() => Promise.resolve(authResponse));
+    const errorMessage = 'This is my error message';
+    const servicePrincipalLogin = jest.fn(() => {
+      throw new Error(errorMessage);
+    });
+
+    AzureLoginService.interactiveLogin = interactiveLogin;
+    AzureLoginService.servicePrincipalLogin = servicePrincipalLogin;
+    
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureLoginPlugin(sls, options);
+    await invokeHook(plugin, 'before:package:initialize');
+    expect(servicePrincipalLogin).toBeCalledWith(
+      'azureServicePrincipalClientId',
+      'azureServicePrincipalPassword',
+      'azureServicePrincipalTenantId'
+    )
+    expect(interactiveLogin).not.toBeCalled();
+    expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`)
+  });
+})
\ No newline at end of file
diff --git a/src/plugins/package/azurePackage.test.ts b/src/plugins/package/azurePackage.test.ts
new file mode 100644
index 0000000..1c18d65
--- /dev/null
+++ b/src/plugins/package/azurePackage.test.ts
@@ -0,0 +1,31 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import { AzurePackage } from "./azurePackage"
+
+jest.mock('../../shared/bindings');
+import { BindingUtils } from '../../shared/bindings';
+jest.mock('../../shared/utils');
+import { Utils, FunctionMetadata } from '../../shared/utils';
+
+describe('Azure Package Plugin', () => {
+  it('sets up provider configuration', async () => {
+    const metadata = 'metadata';
+    const functionName = 'function1';
+
+    const getFunctionMetaDataFn = jest.fn(() => metadata as any as FunctionMetadata);
+    const createEventsBindingsFn = jest.fn();
+
+    Utils.getFunctionMetaData = getFunctionMetaDataFn
+    BindingUtils.createEventsBindings = createEventsBindingsFn
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzurePackage(sls, options);
+
+    await invokeHook(plugin, 'package:setupProviderConfiguration');
+
+    expect(sls.cli.log).toBeCalledWith('Building Azure Events Hooks');
+    expect(getFunctionMetaDataFn).toBeCalledWith(functionName, sls);
+    expect(createEventsBindingsFn).toBeCalledWith(sls.config.servicePath, functionName, metadata);
+  });
+});
\ No newline at end of file
diff --git a/src/plugins/remove/azureRemove.test.ts b/src/plugins/remove/azureRemove.test.ts
new file mode 100644
index 0000000..223531f
--- /dev/null
+++ b/src/plugins/remove/azureRemove.test.ts
@@ -0,0 +1,26 @@
+import { MockFactory } from "../../test/mockFactory";
+import { invokeHook } from "../../test/utils";
+import { AzureRemove } from "./azureRemove";
+
+jest.mock("../../services/resourceService");
+import { ResourceService } from "../../services/resourceService";
+
+describe('Remove Plugin', () => {
+  it('calls remove hook', async () => {
+    const deleteDeployment = jest.fn();
+    const deleteResourceGroup = jest.fn();
+
+    ResourceService.prototype.deleteDeployment = deleteDeployment;
+    ResourceService.prototype.deleteResourceGroup = deleteResourceGroup;
+
+    const sls = MockFactory.createTestServerless();
+    const options = MockFactory.createTestServerlessOptions();
+    const plugin = new AzureRemove(sls, options);
+
+    await invokeHook(plugin, 'remove:remove');
+
+    expect(deleteDeployment).toBeCalled();
+    expect(deleteResourceGroup).toBeCalled();
+    expect(sls.cli.log).toBeCalledWith('Service successfully removed');
+  });
+});
\ No newline at end of file
diff --git a/src/shared/binding.test.ts b/src/shared/binding.test.ts
index ea2c575..8ea864a 100644
--- a/src/shared/binding.test.ts
+++ b/src/shared/binding.test.ts
@@ -1,11 +1,11 @@
 import { MockFactory } from '../test/mockFactory';
-import { getBindingsMetaData, createEventsBindings } from './bindings'
+import { BindingUtils } from './bindings';
 
 describe('Bindings', () => {
   it('should get bindings metadata from serverless', () => {
     const sls = MockFactory.createTestServerless();
     expect(sls).not.toBeNull();
-    const bindingsMetadata = getBindingsMetaData(sls);
+    BindingUtils.getBindingsMetaData(sls);
     expect(sls.cli.log).toBeCalledWith('Parsing Azure Functions Bindings.json...');
   });
 });
\ No newline at end of file
diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts
index 9d87480..da96e62 100644
--- a/src/test/mockFactory.ts
+++ b/src/test/mockFactory.ts
@@ -1,3 +1,4 @@
+import { AuthResponse, LinkedSubscription, TokenCredentialsBase } from '@azure/ms-rest-nodeauth';
 import Serverless from 'serverless';
 import Service from 'serverless/classes/Service';
 import Utils = require('serverless/classes/Utils');
@@ -10,12 +11,35 @@ export class MockFactory {
     sls.utils = MockFactory.createTestUtils();
     sls.cli = MockFactory.createTestCli();
     sls.pluginManager = MockFactory.createTestPluginManager();
+    sls.variables = {};
     return sls;
   }
 
+  public static createTestServerlessOptions(): Serverless.Options {
+    return {
+      extraServicePath: null,
+      function: null,
+      noDeploy: null,
+      region: null,
+      stage: null,
+      watch: null
+    }
+  }
+
+  public static createTestAuthResponse(): AuthResponse {
+    return {
+      credentials: 'credentials' as any as TokenCredentialsBase,
+      subscriptions: [
+        {
+          id: 'azureSubId',
+        }
+      ] as any as LinkedSubscription[]
+    }
+  }
+
   private static createTestService(): Service {
     return {
-      getAllFunctions: jest.fn(),
+      getAllFunctions: jest.fn(() => ['function1']),
       getFunction: jest.fn(),
       getAllEventsInFunction: jest.fn(),
       getAllFunctionsNames: jest.fn(),
@@ -27,7 +51,7 @@ export class MockFactory {
       update: jest.fn(),
       validate: jest.fn(),
       custom: null,
-      provider: null
+      provider: {} as any,
     };
   }
 
diff --git a/src/test/utils.ts b/src/test/utils.ts
new file mode 100644
index 0000000..3210ab3
--- /dev/null
+++ b/src/test/utils.ts
@@ -0,0 +1,3 @@
+export async function invokeHook(plugin: { hooks: { [eventName: string]: Promise<any> }}, hook: string) {
+  return await (plugin.hooks[hook] as any)();
+}
\ No newline at end of file