From 44a4ee79d2f4ce152983c11ab0ec5f676dd8d663 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 31 May 2019 14:58:24 -0700 Subject: [PATCH 01/13] test: Function App Service tests --- package-lock.json | 21 +++ package.json | 2 + src/plugins/login/loginPlugin.test.ts | 4 + src/services/baseService.ts | 6 +- src/services/functionAppService.test.ts | 169 ++++++++++++++++++++++++ src/services/functionAppService.ts | 25 ++-- src/services/loginService.test.ts | 42 ++++++ src/services/resourceService.test.ts | 73 ++++++++++ src/test/mockFactory.ts | 74 ++++++++--- 9 files changed, 386 insertions(+), 30 deletions(-) create mode 100644 src/services/functionAppService.test.ts create mode 100644 src/services/loginService.test.ts create mode 100644 src/services/resourceService.test.ts diff --git a/package-lock.json b/package-lock.json index 04d35d85..58a9be3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1224,6 +1224,15 @@ "is-buffer": "^1.1.5" } }, + "axios-mock-adapter": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.16.0.tgz", + "integrity": "sha512-m2D8ngMTQ5p4zZNBsPKoENgwz5rDfd0pZmXI/spdE2eeeKIcR3jquk+NRiBVFtb9UJlciBYplNzSUmgQ6X385Q==", + "dev": true, + "requires": { + "deep-equal": "^1.0.1" + } + }, "babel-jest": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.8.0.tgz", @@ -2381,6 +2390,12 @@ } } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6776,6 +6791,12 @@ } } }, + "mock-fs": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.10.0.tgz", + "integrity": "sha512-eBpLEjI6tK4RKK44BbUBQu89lrNh+5WeX3wf2U6Uwo6RtRGAQ77qvKeuuQh3lVXHF1aPndVww9VcjqmLThIdtA==", + "dev": true + }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", diff --git a/package.json b/package.json index 30eb685d..f6819c15 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,12 @@ "@types/serverless": "^1.18.2", "@typescript-eslint/eslint-plugin": "^1.9.0", "@typescript-eslint/parser": "^1.9.0", + "axios-mock-adapter": "^1.16.0", "coveralls": "^3.0.3", "eslint": "^5.16.0", "jest": "^24.8.0", "jest-cli": "^24.8.0", + "mock-fs": "^4.10.0", "serverless": "^1.43.0", "ts-jest": "^24.0.2", "typescript": "^3.4.5" diff --git a/src/plugins/login/loginPlugin.test.ts b/src/plugins/login/loginPlugin.test.ts index 86f9eea4..5a5cd231 100644 --- a/src/plugins/login/loginPlugin.test.ts +++ b/src/plugins/login/loginPlugin.test.ts @@ -33,6 +33,7 @@ describe('Login Plugin', () => { AzureLoginService.servicePrincipalLogin = servicePrincipalLogin; const sls = MockFactory.createTestServerless(); + delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); @@ -56,6 +57,7 @@ describe('Login Plugin', () => { AzureLoginService.servicePrincipalLogin = servicePrincipalLogin; const sls = MockFactory.createTestServerless(); + delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); await invokeHook(plugin, 'before:package:initialize'); @@ -83,6 +85,7 @@ describe('Login Plugin', () => { AzureLoginService.servicePrincipalLogin = servicePrincipalLogin; const sls = MockFactory.createTestServerless(); + delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); await invokeHook(plugin, 'before:package:initialize'); @@ -109,6 +112,7 @@ describe('Login Plugin', () => { AzureLoginService.servicePrincipalLogin = servicePrincipalLogin; const sls = MockFactory.createTestServerless(); + delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); await invokeHook(plugin, 'before:package:initialize'); diff --git a/src/services/baseService.ts b/src/services/baseService.ts index cb7d9761..6ef84e70 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -24,7 +24,11 @@ export abstract class BaseService { } } - async sendApiRequest(method: string, relativeUrl: string, options: any = {}) { + protected log(message: string) { + this.serverless.cli.log(message); + } + + protected async sendApiRequest(method: string, relativeUrl: string, options: any = {}) { const defaultHeaders = { 'Authorization': `Bearer ${this.credentials.tokenCache._entries[0].accessToken}` }; diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts new file mode 100644 index 00000000..caa55058 --- /dev/null +++ b/src/services/functionAppService.test.ts @@ -0,0 +1,169 @@ +import mockFs from "mock-fs" +import { MockFactory } from "../test/mockFactory"; +import Serverless from "serverless"; +import { FunctionAppService } from "./functionAppService"; +import { constants } from "../config" + +jest.mock("@azure/arm-resources") + +jest.mock("@azure/arm-appservice") +import { WebSiteManagementClient } from "@azure/arm-appservice"; + +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; + +describe("Function App Service", () => { + + const app = MockFactory.createTestFunctionApp(); + const slsService = MockFactory.createTestService(); + const variables = MockFactory.createTestVariables(); + const provider = MockFactory.createTestAzureServiceProvider(); + + let sendFile; + let webAppDelete; + + const masterKey = "masterKey"; + const authKey = "authKey"; + const syncTriggersMessage = "sync triggers success"; + const functions = MockFactory.createTestFunctions(); + + const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`; + const authKeyUrl = `https://management.azure.com${app.id}/functions/admin/token?api-version=2016-08-01`; + const syncTriggersUrl = `https://management.azure.com${app.id}/syncfunctiontriggers?api-version=2016-08-01`; + const listFunctionsUrl = `https://management.azure.com${app.id}/functions?api-version=2016-08-01`; + const uploadUrl = `https://${app.name}${constants.scmDomain}${constants.scmZipDeployApiPath}` + + beforeAll(() => { + + // TODO: How to spy on defaul exported function? + const axiosMock = new MockAdapter(axios); + + // Master Key + axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey }); + // Auth Key + axiosMock.onGet(authKeyUrl).reply(200, authKey); + // Sync Triggers + axiosMock.onPost(syncTriggersUrl).reply(200, syncTriggersMessage); + // List Functions + axiosMock.onGet(listFunctionsUrl).reply(200, { value: functions }); + + mockFs({ + "app.zip": "contents", + }, {createCwd: true, createTmp: true}); + }); + + beforeEach(() => { + webAppDelete = jest.fn(); + sendFile = jest.fn((options, zipFile) => { + if (options.headers.Authorization === null) { + throw new Error(); + } + }); + + WebSiteManagementClient.prototype.webApps = { + get: jest.fn(() => app), + deleteFunction: webAppDelete, + } as any; + (FunctionAppService.prototype as any).sendFile = sendFile; + }); + + afterEach(() => { + jest.clearAllMocks(); + }) + + afterAll(() => { + mockFs.restore(); + }); + + function createService(sls?: Serverless, options?: Serverless.Options) { + return new FunctionAppService( + sls || MockFactory.createTestServerless({ + service: slsService, + variables: variables, + }), + options || MockFactory.createTestServerlessOptions() + ) + } + + it("get returns function app", async () => { + const service = createService(); + const result = await service.get(); + expect(WebSiteManagementClient.prototype.webApps.get) + .toBeCalledWith(provider.resourceGroup, slsService["service"]); + expect(result).toEqual(app) + }); + + it("get returns null if error occurred", async () => { + const service = createService(); + WebSiteManagementClient.prototype.webApps = { + get: jest.fn(() => { return { error: { code: "ResourceNotFound"}}}), + deleteFunction: webAppDelete, + } as any; + const result = await service.get(); + expect(WebSiteManagementClient.prototype.webApps.get) + .toBeCalledWith(provider.resourceGroup, slsService["service"]); + expect(result).toBeNull(); + }); + + it("gets master key", async () => { + const service = createService(); + const masterKey = await service.getMasterKey(); + expect(masterKey).toEqual(masterKey); + + }); + + it("deletes function", async () => { + const service = createService(); + await service.deleteFunction(app.name); + expect(WebSiteManagementClient.prototype.webApps.deleteFunction).toBeCalledWith( + provider.resourceGroup, + slsService["service"], + app.name + ); + }); + + it("syncs triggers", async () => { + const service = createService(); + const result = await service.syncTriggers(app); + expect(result.data).toEqual(syncTriggersMessage) + }); + + it("cleans up", async () => { + const sls = MockFactory.createTestServerless(); + const service = createService(sls); + const result = await service.cleanUp(app); + expect(result).toHaveLength(functions.length); + const logCalls = (sls.cli.log as any).mock.calls as any[]; + for (let i = 0; i < functions.length; i++) { + const functionName = `function${i+1}` + expect(logCalls[i + 1][0]).toEqual(`-> Deleting function: '${functionName}'`); + } + }); + + it("lists functions", async () => { + const service = createService(); + expect(await service.listFunctions(app)).toEqual(functions); + }); + + it("uploads functions", async () => { + const service = createService(); + await service.uploadFunctions(app); + expect(sendFile).toBeCalledWith({ + method: "POST", + uri: uploadUrl, + json: true, + headers: { + Authorization: `Bearer ${variables["azureCredentials"].tokenCache._entries[0].accessToken}`, + Accept: "*/*", + ContentType: "application/octet-stream", + } + }, slsService["artifact"]) + }); + + it("throws an error with no zip file", async () => { + const sls = MockFactory.createTestServerless(); + delete sls.service["artifact"]; + const service = createService(sls); + await expect(service.uploadFunctions(app)).rejects.not.toBeNull() + }); +}); \ No newline at end of file diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index f40e6204..f2e47250 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -29,7 +29,7 @@ export class FunctionAppService extends BaseService { return response; } - public async getMasterKey(functionApp) { + public async getMasterKey(functionApp?) { functionApp = functionApp || await this.get(); const adminToken = await this.getAuthKey(functionApp); const keyUrl = `https://${functionApp.defaultHostName}/admin/host/systemkeys/_master`; @@ -45,19 +45,19 @@ export class FunctionAppService extends BaseService { } public async deleteFunction(functionName) { - this.serverless.cli.log(`-> Deleting function: ${functionName}`); + this.log(`-> Deleting function: '${functionName}'`); return await this.webClient.webApps.deleteFunction(this.resourceGroup, this.serviceName, functionName); } public async syncTriggers(functionApp) { - this.serverless.cli.log('Syncing function triggers'); + this.log('Syncing function triggers'); const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`; - await this.sendApiRequest('POST', syncTriggersUrl); + return await this.sendApiRequest('POST', syncTriggersUrl); } public async cleanUp(functionApp) { - this.serverless.cli.log('Cleaning up existing functions'); + this.log('Cleaning up existing functions'); const deleteTasks = []; const serviceFunctions = this.serverless.service.getAllFunctions(); @@ -65,7 +65,6 @@ export class FunctionAppService extends BaseService { deployedFunctions.forEach((func) => { if (serviceFunctions.includes(func.name)) { - this.serverless.cli.log(`-> Deleting function '${func.name}'`); deleteTasks.push(this.deleteFunction(func.name)); } }); @@ -86,7 +85,7 @@ export class FunctionAppService extends BaseService { private async zipDeploy(functionApp) { const functionAppName = functionApp.name; - this.serverless.cli.log(`Deploying zip file to function app: ${functionAppName}`); + this.log(`Deploying zip file to function app: ${functionAppName}`); // Upload function artifact if it exists, otherwise the full service is handled in 'uploadFunctions' method const functionZipFile = this.serverless.service['artifact']; @@ -94,10 +93,10 @@ export class FunctionAppService extends BaseService { throw new Error('No zip file found for function app'); } - this.serverless.cli.log(`-> Uploading ${functionZipFile}`); + this.log(`-> Uploading ${functionZipFile}`); const uploadUrl = `https://${functionAppName}${constants.scmDomain}${constants.scmZipDeployApiPath}`; - this.serverless.cli.log(`-> Upload url: ${uploadUrl}`); + this.log(`-> Upload url: ${uploadUrl}`); // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url const requestOptions = { @@ -113,7 +112,7 @@ export class FunctionAppService extends BaseService { try { await this.sendFile(requestOptions, functionZipFile); - this.serverless.cli.log('-> Function package uploaded successfully'); + this.log('-> Function package uploaded successfully'); } catch (e) { throw new Error(`Error uploading zip file:\n --> ${e}`); } @@ -124,7 +123,7 @@ export class FunctionAppService extends BaseService { * resource-group, storage account, app service plan, and app service at the minimum */ public async deploy() { - this.serverless.cli.log(`Creating function app: ${this.serviceName}`); + this.log(`Creating function app: ${this.serviceName}`); let parameters: any = { functionAppName: { value: this.serviceName } }; const gitUrl = this.serverless.service.provider['gitUrl']; @@ -143,7 +142,7 @@ export class FunctionAppService extends BaseService { } if (this.serverless.service.provider['armTemplate']) { - this.serverless.cli.log(`-> Deploying custom ARM template: ${this.serverless.service.provider['armTemplate'].file}`); + this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider['armTemplate'].file}`); templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider['armTemplate'].file); const userParameters = this.serverless.service.provider['armTemplate'].parameters; const userParametersKeys = Object.keys(userParameters); @@ -192,7 +191,7 @@ export class FunctionAppService extends BaseService { } private async runKuduCommand(functionApp, command) { - this.serverless.cli.log(`-> Running Kudu command ${command}...`); + this.log(`-> Running Kudu command ${command}...`); const scmDomain = functionApp.enabledHostNames[0]; const requestUrl = `https://${scmDomain}/api/command`; diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts new file mode 100644 index 00000000..76147c50 --- /dev/null +++ b/src/services/loginService.test.ts @@ -0,0 +1,42 @@ +import { AzureLoginService } from "./loginService"; + +describe('Login Service', () => { + beforeAll(() => { + // Because the functions we use for authentication are exported + // as functions from their respective modules + // (open, interactiveLoginWithAuthResponse and + // loginWithServicePrincipalSecretWithAuthResponse), + // it is extremely difficult to mock their functionality. + // As a workaround, they have been placed in the thinnest possible + // functions within the Azure Login service, which we will + // use to make assertions on the functionality of the login service itself + AzureLoginService.interactiveLogin = jest.fn(); + AzureLoginService.servicePrincipalLogin = jest.fn(); + }); + + it('logs in interactively', async () => { + // Ensure env variables are not set + delete process.env.azureSubId; + delete process.env.azureServicePrincipalClientId; + delete process.env.azureServicePrincipalPassword; + delete process.env.azureServicePrincipalTenantId; + + await AzureLoginService.login(); + expect(AzureLoginService.interactiveLogin).toBeCalled(); + }); + + it('logs in with a service principal', async () => { + // Set environment variables + process.env.azureSubId = 'azureSubId'; + process.env.azureServicePrincipalClientId = 'azureServicePrincipalClientId'; + process.env.azureServicePrincipalPassword = 'azureServicePrincipalPassword'; + process.env.azureServicePrincipalTenantId = 'azureServicePrincipalTenantId'; + + await AzureLoginService.login(); + expect(AzureLoginService.servicePrincipalLogin).toBeCalledWith( + 'azureServicePrincipalClientId', + 'azureServicePrincipalPassword', + 'azureServicePrincipalTenantId' + ); + }); +}); \ No newline at end of file diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts new file mode 100644 index 00000000..fc8777a4 --- /dev/null +++ b/src/services/resourceService.test.ts @@ -0,0 +1,73 @@ +import { MockFactory } from '../test/mockFactory'; +import { ResourceService } from './resourceService'; + + +jest.mock('@azure/arm-resources') +import { ResourceManagementClient } from '@azure/arm-resources'; + +describe('Resource Service', () => { + + beforeAll(() => { + ResourceManagementClient.prototype.resourceGroups = { + createOrUpdate: jest.fn(), + deleteMethod: jest.fn(), + } as any; + + ResourceManagementClient.prototype.deployments = { + deleteMethod: jest.fn() + } as any; + }); + + it('throws error with empty credentials', () => { + const sls = MockFactory.createTestServerless(); + delete sls.variables['azureCredentials'] + const options = MockFactory.createTestServerlessOptions(); + expect(() => new ResourceService(sls, options)).toThrowError('Azure Credentials has not been set in ResourceService') + }); + + it('initializes a resource service', () => { + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + expect(() => new ResourceService(sls, options)).not.toThrowError(); + }); + + it('deploys a resource group', () => { + const sls = MockFactory.createTestServerless(); + const resourceGroup = 'myResourceGroup' + const location = 'West Us'; + sls.service.provider['resourceGroup'] = resourceGroup + sls.service.provider['location'] = location; + sls.variables['azureCredentials'] = 'fake credentials' + const options = MockFactory.createTestServerlessOptions(); + const service = new ResourceService(sls, options); + service.deployResourceGroup(); + expect(ResourceManagementClient.prototype.resourceGroups.createOrUpdate) + .toBeCalledWith(resourceGroup, { location }); + }); + + it('deletes a deployment', () => { + const sls = MockFactory.createTestServerless(); + const resourceGroup = 'myResourceGroup'; + const deploymentName = 'myDeployment'; + sls.service.provider['resourceGroup'] = resourceGroup + sls.service.provider['deploymentName'] = deploymentName; + sls.variables['azureCredentials'] = 'fake credentials' + const options = MockFactory.createTestServerlessOptions(); + const service = new ResourceService(sls, options); + service.deleteDeployment(); + expect(ResourceManagementClient.prototype.deployments.deleteMethod) + .toBeCalledWith(resourceGroup, deploymentName); + }); + + it('deletes a resource group', () => { + const sls = MockFactory.createTestServerless(); + const resourceGroup = 'myResourceGroup'; + sls.service.provider['resourceGroup'] = resourceGroup + sls.variables['azureCredentials'] = 'fake credentials' + const options = MockFactory.createTestServerlessOptions(); + const service = new ResourceService(sls, options); + service.deleteResourceGroup(); + expect(ResourceManagementClient.prototype.resourceGroups.deleteMethod) + .toBeCalledWith(resourceGroup); + }); +}); \ No newline at end of file diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index da96e62f..6bf8f921 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -1,17 +1,17 @@ -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'); -import PluginManager = require('serverless/classes/PluginManager'); +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"); +import PluginManager = require("serverless/classes/PluginManager"); export class MockFactory { public static createTestServerless(config?: any): Serverless { const sls = new Serverless(config); - sls.service = MockFactory.createTestService(); - sls.utils = MockFactory.createTestUtils(); - sls.cli = MockFactory.createTestCli(); - sls.pluginManager = MockFactory.createTestPluginManager(); - sls.variables = {}; + sls.service = config && config.service || MockFactory.createTestService(); + sls.utils = config && config.utils || MockFactory.createTestUtils(); + sls.cli = config && config.cli || MockFactory.createTestCli(); + sls.pluginManager = config && config.pluginManager || MockFactory.createTestPluginManager(); + sls.variables = config && config.variables || MockFactory.createTestVariables(); return sls; } @@ -28,18 +28,26 @@ export class MockFactory { public static createTestAuthResponse(): AuthResponse { return { - credentials: 'credentials' as any as TokenCredentialsBase, + credentials: "credentials" as any as TokenCredentialsBase, subscriptions: [ { - id: 'azureSubId', + id: "azureSubId", } ] as any as LinkedSubscription[] } } - private static createTestService(): Service { + public static createTestFunctionApp() { return { - getAllFunctions: jest.fn(() => ['function1']), + id: "/APP_ID", + name: "APP_NAME", + defaultHostName: "HOST_NAME" + } + } + + public static createTestService(): Service { + return { + getAllFunctions: jest.fn(() => MockFactory.createTestFunctions().map((f) => f.name)), getFunction: jest.fn(), getAllEventsInFunction: jest.fn(), getAllFunctionsNames: jest.fn(), @@ -51,8 +59,42 @@ export class MockFactory { update: jest.fn(), validate: jest.fn(), custom: null, - provider: {} as any, - }; + provider: MockFactory.createTestAzureServiceProvider(), + service: "serviceName", + artifact: "app.zip", + } as any as Service; + } + + public static createTestFunctions(functionCount = 3) { + const functions = [] + for (let i = 0; i < functionCount; i++) { + functions.push({ + name: `function${i + 1}` + }) + } + return functions; + } + + public static createTestAzureServiceProvider() { + return { + resourceGroup: "myResourceGroup", + deploymentName: "myDeploymentName", + } + } + + public static createTestVariables() { + return { + azureCredentials: { + tokenCache: { + _entries: [ + { + accessToken: "token" + } + ] + } + }, + subscriptionId: "subId", + } } private static createTestUtils(): Utils { From 9c298743f65b3f258741b13240489d357af2eb14 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 31 May 2019 15:02:21 -0700 Subject: [PATCH 02/13] style: Update to use double quotes --- .eslintrc.json | 5 +- src/config.ts | 30 +++---- src/index.ts | 22 +++--- src/plugins/apim/apimFunctionPlugin.test.ts | 18 ++--- src/plugins/apim/apimFunctionPlugin.ts | 12 +-- src/plugins/apim/apimServicePlugin.test.ts | 32 ++++---- src/plugins/apim/apimServicePlugin.ts | 14 ++-- src/plugins/deploy/azureDeployPlugin.test.ts | 6 +- src/plugins/deploy/azureDeployPlugin.ts | 10 +-- src/plugins/invoke/azureInvoke.ts | 28 +++---- src/plugins/login/loginPlugin.test.ts | 62 +++++++-------- src/plugins/login/loginPlugin.ts | 22 +++--- src/plugins/logs/azureLogs.ts | 10 +-- src/plugins/package/azurePackage.test.ts | 20 ++--- src/plugins/package/azurePackage.ts | 16 ++-- src/plugins/remove/azureRemove.test.ts | 8 +- src/plugins/remove/azureRemove.ts | 10 +-- src/provider/azureProvider.ts | 82 ++++++++++---------- src/services/apimService.ts | 36 ++++----- src/services/baseService.ts | 26 +++---- src/services/functionAppService.ts | 80 +++++++++---------- src/services/loginService.ts | 4 +- src/services/resourceService.ts | 10 +-- src/shared/binding.test.ts | 10 +-- src/shared/bindings.ts | 24 +++--- src/shared/constants.ts | 44 +++++------ src/shared/utils.ts | 8 +- 27 files changed, 323 insertions(+), 326 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 8a96041d..eb1be605 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,9 +9,6 @@ ], "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/no-parameter-properties": 0, - "@typescript-eslint/explicit-member-accessibility": 0, - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/no-use-before-define": 0 + "@typescript-eslint/no-parameter-properties": 0 } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 2d8a8bf5..f2c36435 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,19 +1,19 @@ export const constants = { - bearer: 'Bearer ', - functionAppApiPath: '/api/', - functionAppDomain: '.azurewebsites.net', - functionsAdminApiPath: '/admin/functions/', - functionsApiPath: '/api/functions', - jsonContentType: 'application/json', - logInvocationsApiPath: '/azurejobs/api/functions/definitions/', - logOutputApiPath: '/azurejobs/api/log/output/', - logStreamApiPath: '/api/logstream/application/functions/function/', - masterKeyApiPath: '/api/functions/admin/masterkey', - providerName: 'azure', - scmCommandApiPath: '/api/command', - scmDomain: '.scm.azurewebsites.net', - scmVfsPath: '/api/vfs/site/wwwroot/', - scmZipDeployApiPath: '/api/zipdeploy' + bearer: "Bearer ", + functionAppApiPath: "/api/", + functionAppDomain: ".azurewebsites.net", + functionsAdminApiPath: "/admin/functions/", + functionsApiPath: "/api/functions", + jsonContentType: "application/json", + logInvocationsApiPath: "/azurejobs/api/functions/definitions/", + logOutputApiPath: "/azurejobs/api/log/output/", + logStreamApiPath: "/api/logstream/application/functions/function/", + masterKeyApiPath: "/api/functions/admin/masterkey", + providerName: "azure", + scmCommandApiPath: "/api/command", + scmDomain: ".scm.azurewebsites.net", + scmVfsPath: "/api/vfs/site/wwwroot/", + scmZipDeployApiPath: "/api/zipdeploy" }; export default constants; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 538f2c1c..1dcbbbc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,19 +4,19 @@ This way only one plugin needs to be added to the service in order to get access whole provider implementation. */ -import Serverless from 'serverless'; -import AzureProvider from './provider/azureProvider'; -import { AzureInvoke } from './plugins/invoke/azureInvoke'; -import { AzureLogs } from './plugins/logs/azureLogs'; -import { AzureRemove } from './plugins/remove/azureRemove'; -import { AzurePackage } from './plugins/package/azurePackage'; -import { AzureDeployPlugin } from './plugins/deploy/azureDeployPlugin'; -import { AzureLoginPlugin } from './plugins/login/loginPlugin'; -import { AzureApimServicePlugin } from './plugins/apim/apimServicePlugin'; -import { AzureApimFunctionPlugin } from './plugins/apim/apimFunctionPlugin'; +import Serverless from "serverless"; +import AzureProvider from "./provider/azureProvider"; +import { AzureInvoke } from "./plugins/invoke/azureInvoke"; +import { AzureLogs } from "./plugins/logs/azureLogs"; +import { AzureRemove } from "./plugins/remove/azureRemove"; +import { AzurePackage } from "./plugins/package/azurePackage"; +import { AzureDeployPlugin } from "./plugins/deploy/azureDeployPlugin"; +import { AzureLoginPlugin } from "./plugins/login/loginPlugin"; +import { AzureApimServicePlugin } from "./plugins/apim/apimServicePlugin"; +import { AzureApimFunctionPlugin } from "./plugins/apim/apimFunctionPlugin"; export class AzureIndex { - constructor(private serverless: Serverless, private options) { + public constructor(private serverless: Serverless, private options) { this.serverless.setProvider(AzureProvider.getProviderName(), new AzureProvider(serverless) as any); // To be refactored diff --git a/src/plugins/apim/apimFunctionPlugin.test.ts b/src/plugins/apim/apimFunctionPlugin.test.ts index d7a02afd..a1c01b71 100644 --- a/src/plugins/apim/apimFunctionPlugin.test.ts +++ b/src/plugins/apim/apimFunctionPlugin.test.ts @@ -1,25 +1,25 @@ import { MockFactory } from "../../test/mockFactory"; import { invokeHook } from "../../test/utils"; -import { AzureApimFunctionPlugin } from './apimFunctionPlugin'; +import { AzureApimFunctionPlugin } from "./apimFunctionPlugin"; -jest.mock('../../services/apimService'); -import { ApimService } from '../../services/apimService'; +jest.mock("../../services/apimService"); +import { ApimService } from "../../services/apimService"; -describe('APIM Function Plugin', () => { - it('calls deploy function', async () => { +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' + sls.service.provider["apim"] = "apim config" const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureApimFunctionPlugin(sls, options); - await invokeHook(plugin, 'after:deploy:function:deploy'); + await invokeHook(plugin, "after:deploy:function:deploy"); - expect(sls.cli.log).toBeCalledWith('Starting APIM function deployment') + expect(sls.cli.log).toBeCalledWith("Starting APIM function deployment") expect(deployFunction).toBeCalled(); - expect(sls.cli.log).lastCalledWith('Finished APIM function deployment') + expect(sls.cli.log).lastCalledWith("Finished APIM function deployment") }); }); \ No newline at end of file diff --git a/src/plugins/apim/apimFunctionPlugin.ts b/src/plugins/apim/apimFunctionPlugin.ts index c4dcd6f0..8c5c9ee5 100644 --- a/src/plugins/apim/apimFunctionPlugin.ts +++ b/src/plugins/apim/apimFunctionPlugin.ts @@ -1,21 +1,21 @@ -import Serverless from 'serverless'; -import { ApimService } from '../../services/apimService'; +import Serverless from "serverless"; +import { ApimService } from "../../services/apimService"; export class AzureApimFunctionPlugin { public hooks: { [eventName: string]: Promise }; - constructor(private serverless: Serverless, private options: Serverless.Options) { + public constructor(private serverless: Serverless, private options: Serverless.Options) { this.hooks = { - 'after:deploy:function:deploy': this.deploy.bind(this) + "after:deploy:function:deploy": this.deploy.bind(this) }; } private async deploy() { - this.serverless.cli.log('Starting APIM function deployment'); + this.serverless.cli.log("Starting APIM function deployment"); const apimService = new ApimService(this.serverless, this.options); await apimService.deployFunction(this.options); - this.serverless.cli.log('Finished APIM function deployment'); + this.serverless.cli.log("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 c17a705d..e3434d38 100644 --- a/src/plugins/apim/apimServicePlugin.test.ts +++ b/src/plugins/apim/apimServicePlugin.test.ts @@ -1,28 +1,28 @@ -import Serverless from 'serverless'; +import Serverless from "serverless"; import { MockFactory } from "../../test/mockFactory"; import { invokeHook } from "../../test/utils"; -import { AzureApimServicePlugin } from './apimServicePlugin'; +import { AzureApimServicePlugin } from "./apimServicePlugin"; -jest.mock('../../services/apimService'); -import { ApimService } from '../../services/apimService'; +jest.mock("../../services/apimService"); +import { ApimService } from "../../services/apimService"; -describe('APIM Service Plugin', () => { - it('is defined', () => { +describe("APIM Service Plugin", () => { + it("is defined", () => { expect(AzureApimServicePlugin).toBeDefined(); }); - it('can be instantiated', () => { + it("can be instantiated", () => { const serverless = new Serverless(); const options: Serverless.Options = { - stage: '', - region: '', + stage: "", + region: "", } const plugin = new AzureApimServicePlugin(serverless, options); expect(plugin).not.toBeNull(); }); - it('calls deploy API and deploy functions', async () => { + it("calls deploy API and deploy functions", async () => { const deployApi = jest.fn(); const deployFunctions = jest.fn(); @@ -30,19 +30,19 @@ describe('APIM Service Plugin', () => { ApimService.prototype.deployFunctions = deployFunctions; const sls = MockFactory.createTestServerless(); - sls.service.provider['apim'] = 'apim config' + sls.service.provider["apim"] = "apim config" const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureApimServicePlugin(sls, options); - await invokeHook(plugin, 'after:deploy:deploy'); + await invokeHook(plugin, "after:deploy:deploy"); - expect(sls.cli.log).toBeCalledWith('Starting APIM service deployment') + expect(sls.cli.log).toBeCalledWith("Starting APIM service deployment") expect(deployApi).toBeCalled(); expect(deployFunctions).toBeCalled(); - expect(sls.cli.log).lastCalledWith('Finished APIM service deployment') + 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 () => { + it("does not call deploy API or deploy functions when \"apim\" not included in config", async () => { const deployApi = jest.fn(); const deployFunctions = jest.fn(); @@ -53,7 +53,7 @@ describe('APIM Service Plugin', () => { const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureApimServicePlugin(sls, options); - await invokeHook(plugin, 'after:deploy:deploy'); + await invokeHook(plugin, "after:deploy:deploy"); expect(sls.cli.log).not.toBeCalled() expect(deployApi).not.toBeCalled(); diff --git a/src/plugins/apim/apimServicePlugin.ts b/src/plugins/apim/apimServicePlugin.ts index a5255f65..2f13aef4 100644 --- a/src/plugins/apim/apimServicePlugin.ts +++ b/src/plugins/apim/apimServicePlugin.ts @@ -1,27 +1,27 @@ -import Serverless from 'serverless'; -import { ApimService } from '../../services/apimService'; +import Serverless from "serverless"; +import { ApimService } from "../../services/apimService"; export class AzureApimServicePlugin { public hooks: { [eventName: string]: Promise }; - constructor(private serverless: Serverless, private options: Serverless.Options) { + public constructor(private serverless: Serverless, private options: Serverless.Options) { this.hooks = { - 'after:deploy:deploy': this.deploy.bind(this) + "after:deploy:deploy": this.deploy.bind(this) }; } private async deploy() { - const apimConfig = this.serverless.service.provider['apim']; + const apimConfig = this.serverless.service.provider["apim"]; if (!apimConfig) { return Promise.resolve(); } - this.serverless.cli.log('Starting APIM service deployment'); + this.serverless.cli.log("Starting APIM service deployment"); const apimService = new ApimService(this.serverless, this.options); await apimService.deployApi(); await apimService.deployFunctions(); - this.serverless.cli.log('Finished APIM service deployment'); + this.serverless.cli.log("Finished APIM service deployment"); } } \ No newline at end of file diff --git a/src/plugins/deploy/azureDeployPlugin.test.ts b/src/plugins/deploy/azureDeployPlugin.test.ts index 05d3d2f5..468947dc 100644 --- a/src/plugins/deploy/azureDeployPlugin.test.ts +++ b/src/plugins/deploy/azureDeployPlugin.test.ts @@ -8,9 +8,9 @@ import { FunctionAppService } from "../../services/functionAppService"; jest.mock("../../services/resourceService"); import { ResourceService } from "../../services/resourceService"; -describe('Deploy plugin', () => { +describe("Deploy plugin", () => { - it('calls deploy hook', async () => { + it("calls deploy hook", async () => { const deployResourceGroup = jest.fn(); const functionAppStub = "Function App Stub"; const deploy = jest.fn(() => Promise.resolve(functionAppStub)); @@ -24,7 +24,7 @@ describe('Deploy plugin', () => { const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureDeployPlugin(sls, options); - await invokeHook(plugin, 'deploy:deploy'); + await invokeHook(plugin, "deploy:deploy"); expect(deployResourceGroup).toBeCalled(); expect(deploy).toBeCalled(); diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 56d5803a..48f09674 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -1,13 +1,13 @@ -import Serverless from 'serverless'; -import { ResourceService } from '../../services/resourceService'; -import { FunctionAppService } from '../../services/functionAppService'; +import Serverless from "serverless"; +import { ResourceService } from "../../services/resourceService"; +import { FunctionAppService } from "../../services/functionAppService"; export class AzureDeployPlugin { public hooks: { [eventName: string]: Promise }; - constructor(private serverless: Serverless, private options: Serverless.Options) { + public constructor(private serverless: Serverless, private options: Serverless.Options) { this.hooks = { - 'deploy:deploy': this.deploy.bind(this) + "deploy:deploy": this.deploy.bind(this) }; } diff --git a/src/plugins/invoke/azureInvoke.ts b/src/plugins/invoke/azureInvoke.ts index a672e345..f8da7e26 100644 --- a/src/plugins/invoke/azureInvoke.ts +++ b/src/plugins/invoke/azureInvoke.ts @@ -1,14 +1,14 @@ -import Serverless from 'serverless'; -import { join, isAbsolute } from 'path'; -import AzureProvider from '../../provider/azureProvider'; +import Serverless from "serverless"; +import { join, isAbsolute } from "path"; +import AzureProvider from "../../provider/azureProvider"; export class AzureInvoke { public hooks: { [eventName: string]: Promise }; private provider: AzureProvider; - constructor(private serverless: Serverless, private options: Serverless.Options) { - this.provider = (this.serverless.getProvider('azure') as any) as AzureProvider; - const path = this.options['path']; + 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) @@ -16,26 +16,26 @@ export class AzureInvoke { : join(this.serverless.config.servicePath, path); if (!this.serverless.utils.fileExistsSync(absolutePath)) { - throw new Error('The file you provided does not exist.'); + throw new Error("The file you provided does not exist."); } - this.options['data'] = this.serverless.utils.readFileSync(absolutePath); + this.options["data"] = this.serverless.utils.readFileSync(absolutePath); } this.hooks = { - 'before:invoke:invoke': this.provider.getAdminKey.bind(this), - 'invoke:invoke': this.invoke.bind(this) + "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]; + const eventType = Object.keys(functionObject["events"][0])[0]; - if (!this.options['data']) { - this.options['data'] = {}; + if (!this.options["data"]) { + this.options["data"] = {}; } - return this.provider.invoke(func, eventType, this.options['data']); + return this.provider.invoke(func, eventType, this.options["data"]); } } diff --git a/src/plugins/login/loginPlugin.test.ts b/src/plugins/login/loginPlugin.test.ts index 5a5cd231..14445512 100644 --- a/src/plugins/login/loginPlugin.test.ts +++ b/src/plugins/login/loginPlugin.test.ts @@ -3,11 +3,11 @@ import { invokeHook } from "../../test/utils"; import { AzureLoginPlugin } from "./loginPlugin"; import { AzureLoginService } from "../../services/loginService"; -describe('Login Plugin', () => { +describe("Login Plugin", () => { const authResponse = MockFactory.createTestAuthResponse(); - it('returns if azure credentials are set', async () => { + it("returns if azure credentials are set", async () => { const interactiveLogin = jest.fn(() => Promise.resolve(authResponse)); const servicePrincipalLogin = jest.fn(() => Promise.resolve(authResponse)); @@ -15,17 +15,17 @@ describe('Login Plugin', () => { AzureLoginService.servicePrincipalLogin = servicePrincipalLogin; const sls = MockFactory.createTestServerless(); - sls.variables['azureCredentials'] = 'credentials'; + sls.variables["azureCredentials"] = "credentials"; const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); - await invokeHook(plugin, 'before:package:initialize'); + await invokeHook(plugin, "before:package:initialize"); expect(interactiveLogin).not.toBeCalled(); expect(servicePrincipalLogin).not.toBeCalled(); }); - it('calls login if azure credentials are not set', async () => { + it("calls login if azure credentials are not set", async () => { const interactiveLogin = jest.fn(() => Promise.resolve(authResponse)); const servicePrincipalLogin = jest.fn(() => Promise.resolve(authResponse)); @@ -37,18 +37,18 @@ describe('Login Plugin', () => { const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); - await invokeHook(plugin, 'before:package:initialize'); + await invokeHook(plugin, "before:package:initialize"); expect(interactiveLogin).toBeCalled(); expect(servicePrincipalLogin).not.toBeCalled(); }); - it('calls service principal login if environment variables are set', async () => { + 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'; + 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)); @@ -60,19 +60,19 @@ describe('Login Plugin', () => { delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); - await invokeHook(plugin, 'before:package:initialize'); + await invokeHook(plugin, "before:package:initialize"); expect(servicePrincipalLogin).toBeCalledWith( - 'azureServicePrincipalClientId', - 'azureServicePrincipalPassword', - 'azureServicePrincipalTenantId' + "azureServicePrincipalClientId", + "azureServicePrincipalPassword", + "azureServicePrincipalTenantId" ) expect(interactiveLogin).not.toBeCalled(); - expect(sls.variables['azureCredentials']).toEqual(authResponse.credentials); - expect(sls.variables['subscriptionId']).toEqual('azureSubId'); + expect(sls.variables["azureCredentials"]).toEqual(authResponse.credentials); + expect(sls.variables["subscriptionId"]).toEqual("azureSubId"); }); - it('calls interactive login if environment variables are not set', async () => { + it("calls interactive login if environment variables are not set", async () => { delete process.env.azureSubId; delete process.env.azureServicePrincipalClientId; delete process.env.azureServicePrincipalPassword; @@ -88,22 +88,22 @@ describe('Login Plugin', () => { delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); - await invokeHook(plugin, 'before:package:initialize'); + 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'); + 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'; + 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 errorMessage = "This is my error message"; const servicePrincipalLogin = jest.fn(() => { throw new Error(errorMessage); }); @@ -115,11 +115,11 @@ describe('Login Plugin', () => { delete sls.variables['azureCredentials'] const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureLoginPlugin(sls, options); - await invokeHook(plugin, 'before:package:initialize'); + await invokeHook(plugin, "before:package:initialize"); expect(servicePrincipalLogin).toBeCalledWith( - 'azureServicePrincipalClientId', - 'azureServicePrincipalPassword', - 'azureServicePrincipalTenantId' + "azureServicePrincipalClientId", + "azureServicePrincipalPassword", + "azureServicePrincipalTenantId" ) expect(interactiveLogin).not.toBeCalled(); expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`) diff --git a/src/plugins/login/loginPlugin.ts b/src/plugins/login/loginPlugin.ts index 75072d35..d64635e6 100644 --- a/src/plugins/login/loginPlugin.ts +++ b/src/plugins/login/loginPlugin.ts @@ -1,36 +1,36 @@ -import Serverless from 'serverless'; -import AzureProvider from '../../provider/azureProvider'; -import { AzureLoginService } from '../../services/loginService'; +import Serverless from "serverless"; +import AzureProvider from "../../provider/azureProvider"; +import { AzureLoginService } from "../../services/loginService"; export class AzureLoginPlugin { private provider: AzureProvider; public hooks: { [eventName: string]: Promise }; - constructor(private serverless: Serverless, private options: Serverless.Options) { - this.provider = (this.serverless.getProvider('azure') as any) as AzureProvider; + public constructor(private serverless: Serverless, private options: Serverless.Options) { + this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider; this.hooks = { - 'before:package:initialize': this.login.bind(this) + "before:package:initialize": this.login.bind(this) }; } private async login() { // If credentials have already been set then short circuit - if (this.serverless.variables['azureCredentials']) { + if (this.serverless.variables["azureCredentials"]) { return; } - this.serverless.cli.log('Logging into Azure'); + this.serverless.cli.log("Logging into Azure"); try { const authResult = await AzureLoginService.login(); - this.serverless.variables['azureCredentials'] = authResult.credentials; + this.serverless.variables["azureCredentials"] = authResult.credentials; // Use environment variable for sub ID or use the first subscription in the list (service principal can // have access to more than one subscription) - this.serverless.variables['subscriptionId'] = process.env.azureSubId || authResult.subscriptions[0].id; + this.serverless.variables["subscriptionId"] = process.env.azureSubId || authResult.subscriptions[0].id; } catch (e) { - this.serverless.cli.log('Error logging into azure'); + this.serverless.cli.log("Error logging into azure"); this.serverless.cli.log(`${e}`); } } diff --git a/src/plugins/logs/azureLogs.ts b/src/plugins/logs/azureLogs.ts index 86c522ec..1a50f074 100644 --- a/src/plugins/logs/azureLogs.ts +++ b/src/plugins/logs/azureLogs.ts @@ -1,16 +1,16 @@ -import Serverless from 'serverless'; -import AzureProvider from '../../provider/azureProvider'; +import Serverless from "serverless"; +import AzureProvider from "../../provider/azureProvider"; export class AzureLogs { public hooks: { [eventName: string]: Promise }; private provider: AzureProvider; - constructor(private serverless: Serverless, private options: Serverless.Options) { - this.provider = (this.serverless.getProvider('azure') as any) as AzureProvider; + public constructor(private serverless: Serverless, private options: Serverless.Options) { + this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider; this.hooks = { - 'logs:logs': this.retrieveLogs.bind(this) + "logs:logs": this.retrieveLogs.bind(this) }; } diff --git a/src/plugins/package/azurePackage.test.ts b/src/plugins/package/azurePackage.test.ts index 1c18d650..bf8acca2 100644 --- a/src/plugins/package/azurePackage.test.ts +++ b/src/plugins/package/azurePackage.test.ts @@ -2,15 +2,15 @@ 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'; +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'; +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(); @@ -22,9 +22,9 @@ describe('Azure Package Plugin', () => { const options = MockFactory.createTestServerlessOptions(); const plugin = new AzurePackage(sls, options); - await invokeHook(plugin, 'package:setupProviderConfiguration'); + await invokeHook(plugin, "package:setupProviderConfiguration"); - expect(sls.cli.log).toBeCalledWith('Building Azure Events Hooks'); + expect(sls.cli.log).toBeCalledWith("Building Azure Events Hooks"); expect(getFunctionMetaDataFn).toBeCalledWith(functionName, sls); expect(createEventsBindingsFn).toBeCalledWith(sls.config.servicePath, functionName, metadata); }); diff --git a/src/plugins/package/azurePackage.ts b/src/plugins/package/azurePackage.ts index 1eb4446f..903effad 100644 --- a/src/plugins/package/azurePackage.ts +++ b/src/plugins/package/azurePackage.ts @@ -1,21 +1,21 @@ -import Serverless from 'serverless'; -import AzureProvider from '../../provider/azureProvider'; -import { BindingUtils } from '../../shared/bindings'; -import { Utils } from '../../shared/utils'; +import Serverless from "serverless"; +import AzureProvider from "../../provider/azureProvider"; +import { BindingUtils } from "../../shared/bindings"; +import { Utils } from "../../shared/utils"; export class AzurePackage { - provider: AzureProvider + public provider: AzureProvider public hooks: { [eventName: string]: Promise }; - constructor(private serverless: Serverless, private options: Serverless.Options) { + public constructor(private serverless: Serverless, private options: Serverless.Options) { this.hooks = { - 'package:setupProviderConfiguration': this.setupProviderConfiguration.bind(this), + "package:setupProviderConfiguration": this.setupProviderConfiguration.bind(this), }; } private async setupProviderConfiguration() { - this.serverless.cli.log('Building Azure Events Hooks'); + this.serverless.cli.log("Building Azure Events Hooks"); const createEventsPromises = this.serverless.service.getAllFunctions() .map((functionName) => { diff --git a/src/plugins/remove/azureRemove.test.ts b/src/plugins/remove/azureRemove.test.ts index 223531f8..23d5eb31 100644 --- a/src/plugins/remove/azureRemove.test.ts +++ b/src/plugins/remove/azureRemove.test.ts @@ -5,8 +5,8 @@ import { AzureRemove } from "./azureRemove"; jest.mock("../../services/resourceService"); import { ResourceService } from "../../services/resourceService"; -describe('Remove Plugin', () => { - it('calls remove hook', async () => { +describe("Remove Plugin", () => { + it("calls remove hook", async () => { const deleteDeployment = jest.fn(); const deleteResourceGroup = jest.fn(); @@ -17,10 +17,10 @@ describe('Remove Plugin', () => { const options = MockFactory.createTestServerlessOptions(); const plugin = new AzureRemove(sls, options); - await invokeHook(plugin, 'remove:remove'); + await invokeHook(plugin, "remove:remove"); expect(deleteDeployment).toBeCalled(); expect(deleteResourceGroup).toBeCalled(); - expect(sls.cli.log).toBeCalledWith('Service successfully removed'); + expect(sls.cli.log).toBeCalledWith("Service successfully removed"); }); }); \ No newline at end of file diff --git a/src/plugins/remove/azureRemove.ts b/src/plugins/remove/azureRemove.ts index f9bf78d6..0ad85b22 100644 --- a/src/plugins/remove/azureRemove.ts +++ b/src/plugins/remove/azureRemove.ts @@ -1,12 +1,12 @@ -import Serverless from 'serverless'; -import { ResourceService } from '../../services/resourceService'; +import Serverless from "serverless"; +import { ResourceService } from "../../services/resourceService"; export class AzureRemove { public hooks: { [eventName: string]: Promise }; - constructor(private serverless: Serverless, private options: Serverless.Options) { + public constructor(private serverless: Serverless, private options: Serverless.Options) { this.hooks = { - 'remove:remove': this.remove.bind(this) + "remove:remove": this.remove.bind(this) }; } @@ -15,6 +15,6 @@ export class AzureRemove { await resourceClient.deleteDeployment(); await resourceClient.deleteResourceGroup(); - this.serverless.cli.log('Service successfully removed'); + this.serverless.cli.log("Service successfully removed"); } } diff --git a/src/provider/azureProvider.ts b/src/provider/azureProvider.ts index a3d752a3..3d31d02b 100644 --- a/src/provider/azureProvider.ts +++ b/src/provider/azureProvider.ts @@ -1,8 +1,8 @@ -import fs from 'fs'; -import { join } from 'path'; -import request from 'request'; -import Serverless from 'serverless'; -import config from '../config'; +import fs from "fs"; +import { join } from "path"; +import request from "request"; +import Serverless from "serverless"; +import config from "../config"; let functionAppName; let functionsAdminKey; @@ -12,16 +12,16 @@ export default class AzureProvider { public credentials: any; private serverless: any; - static getProviderName() { + public static getProviderName() { return config.providerName; } - constructor(serverless: Serverless) { + public constructor(serverless: Serverless) { this.serverless = serverless; this.serverless.setProvider(config.providerName, this); } - getAdminKey(): Promise { + public getAdminKey(): Promise { const options = { url: `https://${functionAppName}${config.scmDomain}${config.masterKeyApiPath}`, json: true, @@ -42,21 +42,21 @@ export default class AzureProvider { }); } - pingHostStatus(functionName): Promise { + public pingHostStatus(functionName): Promise { const requestUrl = `https://${functionAppName}${config.functionAppDomain}/admin/functions/${functionName}/status`; const options = { host: functionAppName + config.functionAppDomain, - method: 'get', + method: "get", url: requestUrl, json: true, headers: { - 'x-functions-key': functionsAdminKey, - Accept: 'application/json,*/*' + "x-functions-key": functionsAdminKey, + Accept: "application/json,*/*" } }; return new Promise((resolve, reject) => { - this.serverless.cli.log('Pinging host status...'); + this.serverless.cli.log("Pinging host status..."); request(options, (err, res, body) => { if (err) return reject(err); if (body && body.Error) return reject(body.Error); @@ -70,28 +70,28 @@ export default class AzureProvider { }); } - getLogsStream(functionName) { + public getLogsStream(functionName) { const logOptions = { url: `https://${functionAppName}${config.scmDomain}${config.logStreamApiPath}${functionName}`, headers: { Authorization: config.bearer + this.credentials.tokenCache._entries[0].accessToken, - Accept: '*/*' + Accept: "*/*" } }; request .get(logOptions) - .on('error', () => { - console.error('Disconnected from log streaming.'); + .on("error", () => { + console.error("Disconnected from log streaming."); }) - .on('end', () => this.getLogsStream(functionName)) + .on("end", () => this.getLogsStream(functionName)) .pipe(process.stdout); } - getInvocationId(functionName): Promise { + public getInvocationId(functionName): Promise { const options = { url: `https://${functionAppName}${config.scmDomain}${config.logInvocationsApiPath + functionAppName}-${functionName}/invocations?limit=5`, - method: 'GET', + method: "GET", json: true, headers: { Authorization: config.bearer + this.credentials.tokenCache._entries[0].accessToken @@ -110,11 +110,11 @@ export default class AzureProvider { }); } - getLogsForInvocationId(): Promise { + public getLogsForInvocationId(): Promise { this.serverless.cli.log(`Logs for InvocationId: ${invocationId}`); const options = { url: `https://${functionAppName}${config.scmDomain}${config.logOutputApiPath}${invocationId}`, - method: 'GET', + method: "GET", json: true, headers: { Authorization: config.bearer + this.credentials.tokenCache._entries[0].accessToken @@ -131,33 +131,33 @@ export default class AzureProvider { }); } - invoke(functionName, eventType, eventData): Promise { - if (eventType === 'http') { - let queryString = ''; + public invoke(functionName, eventType, eventData): Promise { + if (eventType === "http") { + let queryString = ""; if (eventData) { - if (typeof eventData === 'string') { + 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 Promise.reject("The specified input data isn't a valid JSON string. " + + "Please correct it and try invoking the function again."); } } queryString = Object.keys(eventData) .map((key) => `${key}=${eventData[key]}`) - .join('&'); + .join("&"); } return new Promise((resolve, reject) => { const options = { headers: { - 'x-functions-key': functionsAdminKey + "x-functions-key": functionsAdminKey }, url: `http://${functionAppName}${config.functionAppDomain}${config.functionAppApiPath + functionName}?${queryString}`, - method: 'GET', + method: "GET", json: true, }; @@ -177,13 +177,13 @@ export default class AzureProvider { const options = { host: config.functionAppDomain, - method: 'post', + method: "post", body: eventData, url: requestUrl, json: true, headers: { - 'x-functions-key': functionsAdminKey, - Accept: 'application/json,*/*' + "x-functions-key": functionsAdminKey, + Accept: "application/json,*/*" } }; @@ -198,19 +198,19 @@ export default class AzureProvider { }); } - uploadPackageJson(): Promise { - const packageJsonFilePath = join(this.serverless.config.servicePath, 'package.json'); - this.serverless.cli.log('Uploading package.json...'); + public uploadPackageJson(): Promise { + const packageJsonFilePath = join(this.serverless.config.servicePath, "package.json"); + this.serverless.cli.log("Uploading package.json..."); const requestUrl = `https://${functionAppName}${config.scmDomain}${config.scmVfsPath}package.json`; const options = { host: functionAppName + config.scmDomain, - method: 'put', + method: "put", url: requestUrl, json: true, headers: { Authorization: config.bearer + this.credentials.tokenCache._entries[0].accessToken, - Accept: '*/*' + Accept: "*/*" } }; @@ -221,12 +221,12 @@ export default class AzureProvider { if (err) { reject(err); } else { - resolve('Package.json file uploaded'); + resolve("Package.json file uploaded"); } })); } else { - resolve('Package.json file does not exist'); + resolve("Package.json file does not exist"); } }); } diff --git a/src/services/apimService.ts b/src/services/apimService.ts index 1e0bc364..7516cf34 100644 --- a/src/services/apimService.ts +++ b/src/services/apimService.ts @@ -1,7 +1,7 @@ -import Serverless from 'serverless'; -import { ApiManagementClient } from '@azure/arm-apimanagement'; -import { FunctionAppService } from './functionAppService'; -import { BaseService } from './baseService'; +import Serverless from "serverless"; +import { ApiManagementClient } from "@azure/arm-apimanagement"; +import { FunctionAppService } from "./functionAppService"; +import { BaseService } from "./baseService"; /** * APIM Service handles deployment and integration with Azure API Management @@ -11,10 +11,10 @@ export class ApimService extends BaseService { private functionAppService: FunctionAppService; private config: any; - constructor(serverless: Serverless, options: Serverless.Options) { + public constructor(serverless: Serverless, options: Serverless.Options) { super(serverless, options); - this.config = this.serverless.service.provider['apim']; + this.config = this.serverless.service.provider["apim"]; this.apimClient = new ApiManagementClient(this.credentials, this.subscriptionId); this.functionAppService = new FunctionAppService(serverless, options); } @@ -34,7 +34,7 @@ export class ApimService extends BaseService { * Deploys all the functions of the serverless service to APIM */ public async deployFunctions() { - this.serverless.cli.log('-> Deploying API Operations'); + this.serverless.cli.log("-> Deploying API Operations"); const deployApiTasks = this.serverless.service .getAllFunctions() @@ -48,7 +48,7 @@ export class ApimService extends BaseService { * @param options */ public async deployFunction(options) { - const functionConfig = this.serverless.service['functions'][options.function]; + const functionConfig = this.serverless.service["functions"][options.function]; if (!functionConfig.apim) { return; @@ -68,7 +68,7 @@ export class ApimService extends BaseService { * Deploys the APIM API referenced by the serverless service */ private async ensureApi() { - this.serverless.cli.log('-> Deploying API') + this.serverless.cli.log("-> Deploying API") try { await this.apimClient.api.createOrUpdate(this.resourceGroup, this.config.resourceId, this.config.name, { @@ -81,7 +81,7 @@ export class ApimService extends BaseService { ] }); } catch (e) { - this.serverless.cli.log('Error creating APIM API'); + this.serverless.cli.log("Error creating APIM API"); this.serverless.cli.log(JSON.stringify(e.body, null, 4)); } } @@ -91,23 +91,23 @@ export class ApimService extends BaseService { * @param functionAppUrl The host name for the deployed function app */ private async ensureBackend(functionApp) { - this.serverless.cli.log('-> Deploying API Backend') + this.serverless.cli.log("-> Deploying API Backend") try { const functionAppResourceId = `https://management.azure.com${functionApp.id}`; await this.apimClient.backend.createOrUpdate(this.resourceGroup, this.config.resourceId, this.serviceName, { credentials: { header: { - 'x-functions-key': [`{{${this.serviceName}-key}}`], + "x-functions-key": [`{{${this.serviceName}-key}}`], } }, description: this.serviceName, - protocol: 'http', + protocol: "http", resourceId: functionAppResourceId, url: `https://${functionApp.defaultHostName}/api` }); } catch (e) { - this.serverless.cli.log('Error creating APIM Backend'); + this.serverless.cli.log("Error creating APIM Backend"); this.serverless.cli.log(JSON.stringify(e.body, null, 4)); } } @@ -125,7 +125,7 @@ export class ApimService extends BaseService { const operationConfig = { displayName: options.operation.displayName || options.function, - description: options.operation.description || '', + description: options.operation.description || "", urlTemplate: options.operation.path, method: options.operation.method, templateParameters: options.operation.templateParameters || [], @@ -134,7 +134,7 @@ export class ApimService extends BaseService { await client.apiOperation.createOrUpdate(this.resourceGroup, this.config.resourceId, this.config.name, options.function, operationConfig); await client.apiOperationPolicy.createOrUpdate(this.resourceGroup, this.config.resourceId, this.config.name, options.function, { - format: 'rawxml', + format: "rawxml", value: ` @@ -163,7 +163,7 @@ export class ApimService extends BaseService { * @param functionAppUrl The host name for the Azure function app */ private async ensureFunctionAppKeys(functionApp) { - this.serverless.cli.log('-> Deploying API keys') + this.serverless.cli.log("-> Deploying API keys") try { const masterKey = await this.functionAppService.getMasterKey(functionApp); const keyName = `${this.serviceName}-key`; @@ -174,7 +174,7 @@ export class ApimService extends BaseService { value: masterKey }); } catch (e) { - this.serverless.cli.log('Error creating APIM Property'); + this.serverless.cli.log("Error creating APIM Property"); this.serverless.cli.log(JSON.stringify(e, null, 4)); } } diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 6ef84e70..f94e0e80 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -1,7 +1,7 @@ -import Serverless from 'serverless'; -import axios from 'axios'; -import request from 'request' -import fs from 'fs'; +import Serverless from "serverless"; +import axios from "axios"; +import request from "request" +import fs from "fs"; export abstract class BaseService { protected baseUrl: string; @@ -11,13 +11,13 @@ export abstract class BaseService { protected resourceGroup: string; protected deploymentName: string; - constructor(protected serverless: Serverless, protected options: Serverless.Options) { - this.baseUrl = 'https://management.azure.com'; - this.serviceName = serverless.service['service']; - this.credentials = serverless.variables['azureCredentials']; - this.subscriptionId = serverless.variables['subscriptionId']; - this.resourceGroup = serverless.service.provider['resourceGroup'] || `${this.serviceName}-rg`; - this.deploymentName = serverless.service.provider['deploymentName'] || `${this.resourceGroup}-deployment`; + public constructor(protected serverless: Serverless, protected options: Serverless.Options) { + this.baseUrl = "https://management.azure.com"; + this.serviceName = serverless.service["service"]; + this.credentials = serverless.variables["azureCredentials"]; + this.subscriptionId = serverless.variables["subscriptionId"]; + this.resourceGroup = serverless.service.provider["resourceGroup"] || `${this.serviceName}-rg`; + this.deploymentName = serverless.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`; if (!this.credentials) { throw new Error(`Azure Credentials has not been set in ${this.constructor.name}`); @@ -30,7 +30,7 @@ export abstract class BaseService { protected async sendApiRequest(method: string, relativeUrl: string, options: any = {}) { const defaultHeaders = { - 'Authorization': `Bearer ${this.credentials.tokenCache._entries[0].accessToken}` + "Authorization": `Bearer ${this.credentials.tokenCache._entries[0].accessToken}` }; const allHeaders = { @@ -57,7 +57,7 @@ export abstract class BaseService { const id = setInterval(async () => { if (retries >= 20) { clearInterval(id); - return reject('Failed conditional check 20 times'); + return reject("Failed conditional check 20 times"); } retries++; diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index f2e47250..00043b14 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -1,19 +1,19 @@ -import fs from 'fs'; -import path from 'path'; -import { WebSiteManagementClient } from '@azure/arm-appservice'; -import { ResourceManagementClient } from '@azure/arm-resources'; -import { Deployment } from '@azure/arm-resources/esm/models'; -import jsonpath from 'jsonpath'; -import _ from 'lodash'; -import Serverless from 'serverless'; -import { BaseService } from './baseService'; -import { constants } from '../config'; +import fs from "fs"; +import path from "path"; +import { WebSiteManagementClient } from "@azure/arm-appservice"; +import { ResourceManagementClient } from "@azure/arm-resources"; +import { Deployment } from "@azure/arm-resources/esm/models"; +import jsonpath from "jsonpath"; +import _ from "lodash"; +import Serverless from "serverless"; +import { BaseService } from "./baseService"; +import { constants } from "../config"; export class FunctionAppService extends BaseService { private resourceClient: ResourceManagementClient; private webClient: WebSiteManagementClient; - constructor(serverless: Serverless, options: Serverless.Options) { + public constructor(serverless: Serverless, options: Serverless.Options) { super(serverless, options); this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId); @@ -22,7 +22,7 @@ export class FunctionAppService extends BaseService { public async get() { const response: any = await this.webClient.webApps.get(this.resourceGroup, this.serviceName); - if (response.error && (response.error.code === 'ResourceNotFound' || response.error.code === 'ResourceGroupNotFound')) { + if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) { return null; } @@ -34,10 +34,10 @@ export class FunctionAppService extends BaseService { const adminToken = await this.getAuthKey(functionApp); const keyUrl = `https://${functionApp.defaultHostName}/admin/host/systemkeys/_master`; - const response = await this.sendApiRequest('GET', keyUrl, { + const response = await this.sendApiRequest("GET", keyUrl, { json: true, headers: { - 'Authorization': `Bearer ${adminToken}` + "Authorization": `Bearer ${adminToken}` } }); @@ -50,14 +50,14 @@ export class FunctionAppService extends BaseService { } public async syncTriggers(functionApp) { - this.log('Syncing function triggers'); + this.log("Syncing function triggers"); const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`; - return await this.sendApiRequest('POST', syncTriggersUrl); + await this.sendApiRequest("POST", syncTriggersUrl); } public async cleanUp(functionApp) { - this.log('Cleaning up existing functions'); + this.log("Cleaning up existing functions"); const deleteTasks = []; const serviceFunctions = this.serverless.service.getAllFunctions(); @@ -74,7 +74,7 @@ export class FunctionAppService extends BaseService { public async listFunctions(functionApp) { const getTokenUrl = `${this.baseUrl}${functionApp.id}/functions?api-version=2016-08-01`; - const response = await this.sendApiRequest('GET', getTokenUrl); + const response = await this.sendApiRequest("GET", getTokenUrl); return response.data.value || []; } @@ -88,9 +88,9 @@ export class FunctionAppService extends BaseService { this.log(`Deploying zip file to function app: ${functionAppName}`); // Upload function artifact if it exists, otherwise the full service is handled in 'uploadFunctions' method - const functionZipFile = this.serverless.service['artifact']; + const functionZipFile = this.serverless.service["artifact"]; if (!functionZipFile) { - throw new Error('No zip file found for function app'); + throw new Error("No zip file found for function app"); } this.log(`-> Uploading ${functionZipFile}`); @@ -100,19 +100,19 @@ export class FunctionAppService extends BaseService { // https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url const requestOptions = { - method: 'POST', + method: "POST", uri: uploadUrl, json: true, headers: { Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, - Accept: '*/*', - ContentType: 'application/octet-stream', + Accept: "*/*", + ContentType: "application/octet-stream", } }; try { await this.sendFile(requestOptions, functionZipFile); - this.log('-> Function package uploaded successfully'); + this.log("-> Function package uploaded successfully"); } catch (e) { throw new Error(`Error uploading zip file:\n --> ${e}`); } @@ -126,7 +126,7 @@ export class FunctionAppService extends BaseService { this.log(`Creating function app: ${this.serviceName}`); let parameters: any = { functionAppName: { value: this.serviceName } }; - const gitUrl = this.serverless.service.provider['gitUrl']; + const gitUrl = this.serverless.service.provider["gitUrl"]; if (gitUrl) { parameters = { @@ -135,33 +135,33 @@ export class FunctionAppService extends BaseService { }; } - let templateFilePath = path.join(__dirname, '..', 'provider', 'armTemplates', 'azuredeploy.json'); + let templateFilePath = path.join(__dirname, "..", "provider", "armTemplates", "azuredeploy.json"); if (gitUrl) { - templateFilePath = path.join(__dirname, 'armTemplates', 'azuredeployWithGit.json'); + templateFilePath = path.join(__dirname, "armTemplates", "azuredeployWithGit.json"); } - if (this.serverless.service.provider['armTemplate']) { - this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider['armTemplate'].file}`); - templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider['armTemplate'].file); - const userParameters = this.serverless.service.provider['armTemplate'].parameters; + if (this.serverless.service.provider["armTemplate"]) { + this.serverless.cli.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); + templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); + const userParameters = this.serverless.service.provider["armTemplate"].parameters; const userParametersKeys = Object.keys(userParameters); for (let paramIndex = 0; paramIndex < userParametersKeys.length; paramIndex++) { const item = {}; - item[userParametersKeys[paramIndex]] = { 'value': userParameters[userParametersKeys[paramIndex]] }; + item[userParametersKeys[paramIndex]] = { "value": userParameters[userParametersKeys[paramIndex]] }; parameters = _.merge(parameters, item); } } - let template = JSON.parse(fs.readFileSync(templateFilePath, 'utf8')); + let template = JSON.parse(fs.readFileSync(templateFilePath, "utf8")); // Check if there are custom environment variables defined that need to be // added to the ARM template used in the deployment. - const environmentVariables = this.serverless.service.provider['environment']; + const environmentVariables = this.serverless.service.provider["environment"]; if (environmentVariables) { - const appSettingsPath = '$.resources[?(@.kind=="functionapp")].properties.siteConfig.appSettings'; + const appSettingsPath = "$.resources[?(@.kind==\"functionapp\")].properties.siteConfig.appSettings"; jsonpath.apply(template, appSettingsPath, function (appSettingsList) { Object.keys(environmentVariables).forEach(function (key) { @@ -177,7 +177,7 @@ export class FunctionAppService extends BaseService { const deploymentParameters: Deployment = { properties: { - mode: 'Incremental', + mode: "Incremental", parameters, template } @@ -198,10 +198,10 @@ export class FunctionAppService extends BaseService { // TODO: There is a case where the body will contain an error, but it's // not actually an error. These are warnings from npm install. - const response = await this.sendApiRequest('POST', requestUrl, { + const response = await this.sendApiRequest("POST", requestUrl, { data: { command: command, - dir: 'site\\wwwroot' + dir: "site\\wwwroot" } }); @@ -218,8 +218,8 @@ export class FunctionAppService extends BaseService { */ private async getAuthKey(functionApp) { const adminTokenUrl = `${this.baseUrl}${functionApp.id}/functions/admin/token?api-version=2016-08-01`; - const response = await this.sendApiRequest('GET', adminTokenUrl); + const response = await this.sendApiRequest("GET", adminTokenUrl); - return response.data.replace(/"/g, ''); + return response.data.replace(/"/g, ""); } } diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 1faa24b4..05abd722 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -1,4 +1,4 @@ -import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse } from '@azure/ms-rest-nodeauth'; +import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse } from "@azure/ms-rest-nodeauth"; export class AzureLoginService { @@ -16,7 +16,7 @@ export class AzureLoginService { } public static async interactiveLogin() { - await open('https://microsoft.com/devicelogin'); + await open("https://microsoft.com/devicelogin"); return await interactiveLoginWithAuthResponse(); } diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index fa53d3f3..7ef589c1 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -1,11 +1,11 @@ -import Serverless from 'serverless'; -import { ResourceManagementClient } from '@azure/arm-resources'; -import { BaseService } from './baseService'; +import Serverless from "serverless"; +import { ResourceManagementClient } from "@azure/arm-resources"; +import { BaseService } from "./baseService"; export class ResourceService extends BaseService { private resourceClient: ResourceManagementClient; - constructor(serverless: Serverless, options: Serverless.Options) { + public constructor(serverless: Serverless, options: Serverless.Options) { super(serverless, options); this.resourceClient = new ResourceManagementClient(this.credentials, this.subscriptionId); @@ -15,7 +15,7 @@ export class ResourceService extends BaseService { this.serverless.cli.log(`Creating resource group: ${this.resourceGroup}`); const groupParameters = { - location: this.serverless.service.provider['location'] + location: this.serverless.service.provider["location"] }; return await this.resourceClient.resourceGroups.createOrUpdate(this.resourceGroup, groupParameters); diff --git a/src/shared/binding.test.ts b/src/shared/binding.test.ts index 8ea864a1..1e8fcea5 100644 --- a/src/shared/binding.test.ts +++ b/src/shared/binding.test.ts @@ -1,11 +1,11 @@ -import { MockFactory } from '../test/mockFactory'; -import { BindingUtils } from './bindings'; +import { MockFactory } from "../test/mockFactory"; +import { BindingUtils } from "./bindings"; -describe('Bindings', () => { - it('should get bindings metadata from serverless', () => { +describe("Bindings", () => { + it("should get bindings metadata from serverless", () => { const sls = MockFactory.createTestServerless(); expect(sls).not.toBeNull(); BindingUtils.getBindingsMetaData(sls); - expect(sls.cli.log).toBeCalledWith('Parsing Azure Functions Bindings.json...'); + expect(sls.cli.log).toBeCalledWith("Parsing Azure Functions Bindings.json..."); }); }); \ No newline at end of file diff --git a/src/shared/bindings.ts b/src/shared/bindings.ts index c108072f..e53314b6 100644 --- a/src/shared/bindings.ts +++ b/src/shared/bindings.ts @@ -1,10 +1,10 @@ -import { writeFileSync } from 'fs'; -import { join } from 'path'; -import Serverless from 'serverless'; -import { FunctionMetadata } from './utils'; -import { constants } from './constants'; +import { writeFileSync } from "fs"; +import { join } from "path"; +import Serverless from "serverless"; +import { FunctionMetadata } from "./utils"; +import { constants } from "./constants"; -const bindingsJson = require('./bindings.json'); +const bindingsJson = require("./bindings.json"); // eslint-disable-line @typescript-eslint/no-var-requires export class BindingUtils { public static getBindingsMetaData(serverless: Serverless) { @@ -13,7 +13,7 @@ export class BindingUtils { const bindingSettings = []; const bindingSettingsNames = []; - serverless.cli.log('Parsing Azure Functions Bindings.json...'); + serverless.cli.log("Parsing Azure Functions Bindings.json..."); for (let bindingsIndex = 0; bindingsIndex < bindingsJson[constants.bindings].length; bindingsIndex++) { const settingsNames = []; @@ -41,7 +41,7 @@ export class BindingUtils { const functionJSON = functionMetadata.params.functionsJson; functionJSON.entryPoint = functionMetadata.entryPoint; functionJSON.scriptFile = functionMetadata.handlerPath; - writeFileSync(join(servicePath, functionName, 'function.json'), JSON.stringify(functionJSON, null, 4)); + writeFileSync(join(servicePath, functionName, "function.json"), JSON.stringify(functionJSON, null, 4)); return Promise.resolve(); } @@ -71,11 +71,11 @@ export class BindingUtils { public static getHttpOutBinding(bindingUserSettings) { const binding = {}; - binding[constants.type] = 'http'; + binding[constants.type] = "http"; binding[constants.direction] = constants.outDirection; - binding[constants.name] = '$return'; + binding[constants.name] = "$return"; if (bindingUserSettings[constants.webHookType]) { - binding[constants.name] = 'res'; + binding[constants.name] = "res"; } return binding; @@ -110,7 +110,7 @@ export class BindingUtils { if (defaultValue) { binding[name] = defaultValue; } else if (name === constants.connection && resource.toLowerCase() === constants.storage) { - binding[name] = 'AzureWebJobsStorage'; + binding[name] = "AzureWebJobsStorage"; } else { throw new Error(`Required property ${name} is missing for binding:${bindingType}`); } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index a9dd3512..979bb9a7 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,24 +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' + 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 55234686..96ab9f06 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -55,7 +55,7 @@ export class Utils { } if (bindingTypeIndex < 0) { - throw new Error('Binding not supported'); + throw new Error("Binding not supported"); } bindingSettings = parsedBindings.bindingSettings[bindingTypeIndex]; @@ -95,13 +95,13 @@ export class Utils { } public static getEntryPointAndHandlerPath(handler) { - let handlerPath = 'handler.js'; + let handlerPath = "handler.js"; let entryPoint = handler; - const handlerSplit = handler.split('.'); + const handlerSplit = handler.split("."); if (handlerSplit.length > 1) { entryPoint = handlerSplit[handlerSplit.length - 1]; - handlerPath = `${handler.substring(0, handler.lastIndexOf('.'))}.js`; + handlerPath = `${handler.substring(0, handler.lastIndexOf("."))}.js`; } const metaData = { entryPoint: entryPoint, From 48f78826c32dd2fc95b390033ee4a43143a79e56 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 31 May 2019 15:05:46 -0700 Subject: [PATCH 03/13] fix: Remove extra files from different PR --- src/services/loginService.test.ts | 42 ---------------- src/services/resourceService.test.ts | 73 ---------------------------- 2 files changed, 115 deletions(-) delete mode 100644 src/services/loginService.test.ts delete mode 100644 src/services/resourceService.test.ts diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts deleted file mode 100644 index 76147c50..00000000 --- a/src/services/loginService.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { AzureLoginService } from "./loginService"; - -describe('Login Service', () => { - beforeAll(() => { - // Because the functions we use for authentication are exported - // as functions from their respective modules - // (open, interactiveLoginWithAuthResponse and - // loginWithServicePrincipalSecretWithAuthResponse), - // it is extremely difficult to mock their functionality. - // As a workaround, they have been placed in the thinnest possible - // functions within the Azure Login service, which we will - // use to make assertions on the functionality of the login service itself - AzureLoginService.interactiveLogin = jest.fn(); - AzureLoginService.servicePrincipalLogin = jest.fn(); - }); - - it('logs in interactively', async () => { - // Ensure env variables are not set - delete process.env.azureSubId; - delete process.env.azureServicePrincipalClientId; - delete process.env.azureServicePrincipalPassword; - delete process.env.azureServicePrincipalTenantId; - - await AzureLoginService.login(); - expect(AzureLoginService.interactiveLogin).toBeCalled(); - }); - - it('logs in with a service principal', async () => { - // Set environment variables - process.env.azureSubId = 'azureSubId'; - process.env.azureServicePrincipalClientId = 'azureServicePrincipalClientId'; - process.env.azureServicePrincipalPassword = 'azureServicePrincipalPassword'; - process.env.azureServicePrincipalTenantId = 'azureServicePrincipalTenantId'; - - await AzureLoginService.login(); - expect(AzureLoginService.servicePrincipalLogin).toBeCalledWith( - 'azureServicePrincipalClientId', - 'azureServicePrincipalPassword', - 'azureServicePrincipalTenantId' - ); - }); -}); \ No newline at end of file diff --git a/src/services/resourceService.test.ts b/src/services/resourceService.test.ts deleted file mode 100644 index fc8777a4..00000000 --- a/src/services/resourceService.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { MockFactory } from '../test/mockFactory'; -import { ResourceService } from './resourceService'; - - -jest.mock('@azure/arm-resources') -import { ResourceManagementClient } from '@azure/arm-resources'; - -describe('Resource Service', () => { - - beforeAll(() => { - ResourceManagementClient.prototype.resourceGroups = { - createOrUpdate: jest.fn(), - deleteMethod: jest.fn(), - } as any; - - ResourceManagementClient.prototype.deployments = { - deleteMethod: jest.fn() - } as any; - }); - - it('throws error with empty credentials', () => { - const sls = MockFactory.createTestServerless(); - delete sls.variables['azureCredentials'] - const options = MockFactory.createTestServerlessOptions(); - expect(() => new ResourceService(sls, options)).toThrowError('Azure Credentials has not been set in ResourceService') - }); - - it('initializes a resource service', () => { - const sls = MockFactory.createTestServerless(); - const options = MockFactory.createTestServerlessOptions(); - expect(() => new ResourceService(sls, options)).not.toThrowError(); - }); - - it('deploys a resource group', () => { - const sls = MockFactory.createTestServerless(); - const resourceGroup = 'myResourceGroup' - const location = 'West Us'; - sls.service.provider['resourceGroup'] = resourceGroup - sls.service.provider['location'] = location; - sls.variables['azureCredentials'] = 'fake credentials' - const options = MockFactory.createTestServerlessOptions(); - const service = new ResourceService(sls, options); - service.deployResourceGroup(); - expect(ResourceManagementClient.prototype.resourceGroups.createOrUpdate) - .toBeCalledWith(resourceGroup, { location }); - }); - - it('deletes a deployment', () => { - const sls = MockFactory.createTestServerless(); - const resourceGroup = 'myResourceGroup'; - const deploymentName = 'myDeployment'; - sls.service.provider['resourceGroup'] = resourceGroup - sls.service.provider['deploymentName'] = deploymentName; - sls.variables['azureCredentials'] = 'fake credentials' - const options = MockFactory.createTestServerlessOptions(); - const service = new ResourceService(sls, options); - service.deleteDeployment(); - expect(ResourceManagementClient.prototype.deployments.deleteMethod) - .toBeCalledWith(resourceGroup, deploymentName); - }); - - it('deletes a resource group', () => { - const sls = MockFactory.createTestServerless(); - const resourceGroup = 'myResourceGroup'; - sls.service.provider['resourceGroup'] = resourceGroup - sls.variables['azureCredentials'] = 'fake credentials' - const options = MockFactory.createTestServerlessOptions(); - const service = new ResourceService(sls, options); - service.deleteResourceGroup(); - expect(ResourceManagementClient.prototype.resourceGroups.deleteMethod) - .toBeCalledWith(resourceGroup); - }); -}); \ No newline at end of file From 4176705f6adfe76d91aeeb795833a87a71aabc51 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 31 May 2019 15:17:26 -0700 Subject: [PATCH 04/13] Fix: return value from sync triggers --- src/services/functionAppService.test.ts | 6 +----- src/services/functionAppService.ts | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index caa55058..bf302a8d 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -54,11 +54,7 @@ describe("Function App Service", () => { beforeEach(() => { webAppDelete = jest.fn(); - sendFile = jest.fn((options, zipFile) => { - if (options.headers.Authorization === null) { - throw new Error(); - } - }); + sendFile = jest.fn(); WebSiteManagementClient.prototype.webApps = { get: jest.fn(() => app), diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 00043b14..42e86c40 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -53,7 +53,7 @@ export class FunctionAppService extends BaseService { this.log("Syncing function triggers"); const syncTriggersUrl = `${this.baseUrl}${functionApp.id}/syncfunctiontriggers?api-version=2016-08-01`; - await this.sendApiRequest("POST", syncTriggersUrl); + return await this.sendApiRequest("POST", syncTriggersUrl); } public async cleanUp(functionApp) { From 7eee80a2b3842f4113e4e08db7716683b9363691 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Mon, 3 Jun 2019 07:14:25 -0700 Subject: [PATCH 05/13] Invoke login hook helper function --- src/plugins/login/loginPlugin.test.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/plugins/login/loginPlugin.test.ts b/src/plugins/login/loginPlugin.test.ts index c52d3bef..e7699765 100644 --- a/src/plugins/login/loginPlugin.test.ts +++ b/src/plugins/login/loginPlugin.test.ts @@ -10,7 +10,7 @@ describe("Login Plugin", () => { const envVariables = MockFactory.createTestServicePrincipalEnvVariables() const credentials = MockFactory.createTestVariables().azureCredentials; - function createPlugin(hasCreds: boolean = false, serverless?: Serverless): AzureLoginPlugin { + function createPlugin(hasCreds = false, serverless?: Serverless): AzureLoginPlugin { const sls = serverless || MockFactory.createTestServerless(); if (!hasCreds) { delete sls.variables['azureCredentials']; @@ -31,7 +31,8 @@ describe("Login Plugin", () => { unsetEnvVariables(envVariables); } - async function invokeLoginHook(plugin: AzureLoginPlugin) { + async function invokeLoginHook(hasCreds = false, serverless?: Serverless) { + const plugin = createPlugin(hasCreds, serverless); await invokeHook(plugin, "before:package:initialize"); } @@ -40,18 +41,14 @@ describe("Login Plugin", () => { AzureLoginService.servicePrincipalLogin = createMockLoginFunction(); }); - afterEach(() => { - jest.clearAllMocks(); - }); - it("returns if azure credentials are set", async () => { - await invokeLoginHook(createPlugin(true)); + await invokeLoginHook(true); expect(AzureLoginService.interactiveLogin).not.toBeCalled(); expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled(); }); it("calls login if azure credentials are not set", async () => { - await invokeLoginHook(createPlugin()); + await invokeLoginHook(); expect(AzureLoginService.interactiveLogin).toBeCalled(); expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled(); }); @@ -59,7 +56,7 @@ describe("Login Plugin", () => { it("calls service principal login if environment variables are set", async () => { setServicePrincipalEnvVariables(); const sls = MockFactory.createTestServerless(); - await invokeLoginHook(createPlugin(false, sls)); + await invokeLoginHook(false, sls); expect(AzureLoginService.servicePrincipalLogin).toBeCalledWith( "azureServicePrincipalClientId", "azureServicePrincipalPassword", @@ -73,7 +70,7 @@ describe("Login Plugin", () => { it("calls interactive login if environment variables are not set", async () => { unsetServicePrincipalEnvVariables(); const sls = MockFactory.createTestServerless(); - await invokeLoginHook(createPlugin(false, sls)); + await invokeLoginHook(false, sls); expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled(); expect(AzureLoginService.interactiveLogin).toBeCalled(); expect(sls.variables["azureCredentials"]).toEqual(credentials); @@ -87,7 +84,7 @@ describe("Login Plugin", () => { throw new Error(errorMessage); }); const sls = MockFactory.createTestServerless(); - await invokeLoginHook(createPlugin(false, sls)); + await invokeLoginHook(false, sls); expect(AzureLoginService.interactiveLogin).toBeCalled() expect(AzureLoginService.servicePrincipalLogin).not.toBeCalled(); expect(sls.cli.log).lastCalledWith(`Error: ${errorMessage}`) From 5ef08e43e44c0defeef686136891637ffacc6fa3 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Mon, 3 Jun 2019 07:48:21 -0700 Subject: [PATCH 06/13] Add minimal models and set as return types in mock factory --- src/models.ts | 45 +++++++++++++++++++++++++++++++++++++++++ src/test/mockFactory.ts | 33 +++++++++++++++--------------- 2 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 src/models.ts diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 00000000..f4a478cb --- /dev/null +++ b/src/models.ts @@ -0,0 +1,45 @@ +export interface ServerlessYml { + provider: { + name: string; + location: string; + }; + plugins: string[]; + functions: any; +} + +export interface FunctionMetadata { + handler: string; + events: FunctionEvent[]; +} + +export interface FunctionEvent { + http?: boolean; + "x-azure-settings"?: { + authLevel?: string; + direction?: string; + name?: string; + }; +} + +export interface FunctionApp { + id: string; + name: string; + defaultHostName: string; +} + +export interface AzureServiceProvider { + resourceGroup: string; + deploymentName: string; +} + +export interface Logger { + log: (message: string) => void; +} + +export interface ServicePrincipalEnvVariables { + azureSubId: string; + azureServicePrincipalClientId: string; + azureServicePrincipalPassword: string; + azureServicePrincipalTenantId: string; +} + diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index fa7dbc12..71b056a1 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -4,6 +4,9 @@ import Serverless from "serverless"; import Service from "serverless/classes/Service"; import Utils = require("serverless/classes/Utils"); import PluginManager = require("serverless/classes/PluginManager"); +import { ServerlessYml, FunctionMetadata, FunctionEvent, ServicePrincipalEnvVariables, + FunctionApp, AzureServiceProvider, Logger } from "../models"; +import { stringify } from "querystring"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { @@ -46,7 +49,7 @@ export class MockFactory { } } - public static createTestServerlessYml(asYaml = false, functionMetadata?) { + public static createTestServerlessYml(asYaml = false, functionMetadata?): ServerlessYml { const data = { "provider": { "name": "azure", @@ -55,21 +58,21 @@ export class MockFactory { "plugins": [ "serverless-azure-functions" ], - "functions": functionMetadata || MockFactory.createTestFunctionsMetadata(2, false), + "functions": functionMetadata || MockFactory.createTestFunctionsMetadata(2), } return (asYaml) ? yaml.dump(data) : data; } - public static createTestFunctionsMetadata(functionCount = 2, wrap = false) { - const data = {}; + public static createTestFunctionsMetadata(functionCount = 2): any { + const data = {} for (let i = 0; i < functionCount; i++) { const functionName = `function${i+1}`; data[functionName] = MockFactory.createTestFunctionMetadata() } - return (wrap) ? {"functions": data } : data; + return data; } - public static createTestFunctionMetadata() { + public static createTestFunctionMetadata(): FunctionMetadata { return { "handler": "index.handler", "events": [ @@ -90,10 +93,10 @@ export class MockFactory { } } - public static createTestFunctionApp() { + public static createTestFunctionApp(name?: string): FunctionApp { return { id: "App Id", - name: "App Name", + name: name || "App Name", defaultHostName: "My Host Name" } } @@ -118,26 +121,22 @@ export class MockFactory { } as any as Service; } - public static createTestFunctions(functionCount = 3) { + public static createTestFunctions(functionCount = 3): FunctionApp[] { const functions = [] for (let i = 0; i < functionCount; i++) { - functions.push({ - name: `function${i + 1}` - }) + functions.push(MockFactory.createTestFunctionApp(`function${i + 1}`)); } return functions; } - public static createTestAzureServiceProvider() { + public static createTestAzureServiceProvider(): AzureServiceProvider { return { resourceGroup: "myResourceGroup", deploymentName: "myDeploymentName", } } - - - public static createTestServicePrincipalEnvVariables() { + public static createTestServicePrincipalEnvVariables(): ServicePrincipalEnvVariables { return { azureSubId: "azureSubId", azureServicePrincipalClientId: "azureServicePrincipalClientId", @@ -184,7 +183,7 @@ export class MockFactory { } } - private static createTestCli(){ + private static createTestCli(): Logger { return { log: jest.fn() } From ede21619d8d72526fed079040d14f6131e8a0659 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Tue, 4 Jun 2019 10:37:49 -0600 Subject: [PATCH 07/13] Add models --- src/index.test.ts | 31 ++++++++++++++++++ src/index.ts | 2 +- src/{models.ts => models/azureProvider.ts} | 38 ++++++---------------- src/models/functionApp.ts | 7 ++++ src/models/generic.ts | 4 +++ src/models/index.ts | 4 +++ src/models/serverless.ts | 8 +++++ src/services/apimService.ts | 26 +++++++-------- src/services/baseService.ts | 2 +- src/services/functionAppService.test.ts | 13 +++----- src/services/functionAppService.ts | 2 +- src/services/resourceService.ts | 6 ++-- src/test/mockFactory.ts | 5 ++- 13 files changed, 89 insertions(+), 59 deletions(-) create mode 100644 src/index.test.ts rename src/{models.ts => models/azureProvider.ts} (64%) create mode 100644 src/models/functionApp.ts create mode 100644 src/models/generic.ts create mode 100644 src/models/index.ts create mode 100644 src/models/serverless.ts diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 00000000..0547521a --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,31 @@ +import AzureIndex from "./index"; +import { MockFactory } from "./test/mockFactory" +import { AzureInvoke } from "./plugins/invoke/azureInvoke"; +import { AzureLogs } from "./plugins/logs/azureLogs"; +import { AzureRemove } from "./plugins/remove/azureRemove"; +import { AzurePackage } from "./plugins/package/azurePackage"; +import { AzureDeployPlugin } from "./plugins/deploy/azureDeployPlugin"; +import { AzureLoginPlugin } from "./plugins/login/loginPlugin"; +import { AzureApimServicePlugin } from "./plugins/apim/apimServicePlugin"; +import { AzureApimFunctionPlugin } from "./plugins/apim/apimFunctionPlugin"; +import AzureProvider from "./provider/azureProvider"; + +describe("Azure Index", () => { + it("contains all registered plugins", () => { + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + const index = new AzureIndex(sls, options); + sls.setProvider = jest.fn(); + + expect(sls.setProvider).toBeCalledWith("azure", new AzureProvider(sls)); + + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzurePackage); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureInvoke); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureLogs); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureRemove); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureLoginPlugin); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureDeployPlugin); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureApimServicePlugin); + expect(sls.pluginManager.addPlugin).toBeCalledWith(AzureApimFunctionPlugin); + }); +}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 05e9c210..d4550730 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ import { AzureApimFunctionPlugin } from "./plugins/apim/apimFunctionPlugin"; import { AzureFuncPlugin } from "./plugins/func/azureFunc"; -export class AzureIndex { +export default class AzureIndex { public constructor(private serverless: Serverless, private options) { this.serverless.setProvider(AzureProvider.getProviderName(), new AzureProvider(serverless) as any); diff --git a/src/models.ts b/src/models/azureProvider.ts similarity index 64% rename from src/models.ts rename to src/models/azureProvider.ts index f4a478cb..555fc64a 100644 --- a/src/models.ts +++ b/src/models/azureProvider.ts @@ -1,17 +1,3 @@ -export interface ServerlessYml { - provider: { - name: string; - location: string; - }; - plugins: string[]; - functions: any; -} - -export interface FunctionMetadata { - handler: string; - events: FunctionEvent[]; -} - export interface FunctionEvent { http?: boolean; "x-azure-settings"?: { @@ -21,10 +7,16 @@ export interface FunctionEvent { }; } -export interface FunctionApp { - id: string; - name: string; - defaultHostName: string; +export interface ServicePrincipalEnvVariables { + azureSubId: string; + azureServicePrincipalClientId: string; + azureServicePrincipalPassword: string; + azureServicePrincipalTenantId: string; +} + +export interface FunctionMetadata { + handler: string; + events: FunctionEvent[]; } export interface AzureServiceProvider { @@ -32,14 +24,4 @@ export interface AzureServiceProvider { deploymentName: string; } -export interface Logger { - log: (message: string) => void; -} - -export interface ServicePrincipalEnvVariables { - azureSubId: string; - azureServicePrincipalClientId: string; - azureServicePrincipalPassword: string; - azureServicePrincipalTenantId: string; -} diff --git a/src/models/functionApp.ts b/src/models/functionApp.ts new file mode 100644 index 00000000..8bbbebf7 --- /dev/null +++ b/src/models/functionApp.ts @@ -0,0 +1,7 @@ + + +export interface FunctionApp { + id: string; + name: string; + defaultHostName: string; +} diff --git a/src/models/generic.ts b/src/models/generic.ts new file mode 100644 index 00000000..cc7c3c24 --- /dev/null +++ b/src/models/generic.ts @@ -0,0 +1,4 @@ + +export interface Logger { + log: (message: string) => void; +} \ No newline at end of file diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 00000000..573bd832 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,4 @@ +export * from "./azureProvider"; +export * from "./functionApp"; +export * from "./generic"; +export * from "./serverless"; \ No newline at end of file diff --git a/src/models/serverless.ts b/src/models/serverless.ts new file mode 100644 index 00000000..d3ccc4b2 --- /dev/null +++ b/src/models/serverless.ts @@ -0,0 +1,8 @@ +export interface ServerlessYml { + provider: { + name: string; + location: string; + }; + plugins: string[]; + functions: any; +} diff --git a/src/services/apimService.ts b/src/services/apimService.ts index 7516cf34..424dd20a 100644 --- a/src/services/apimService.ts +++ b/src/services/apimService.ts @@ -34,7 +34,7 @@ export class ApimService extends BaseService { * Deploys all the functions of the serverless service to APIM */ public async deployFunctions() { - this.serverless.cli.log("-> Deploying API Operations"); + this.log("-> Deploying API Operations"); const deployApiTasks = this.serverless.service .getAllFunctions() @@ -68,7 +68,7 @@ export class ApimService extends BaseService { * Deploys the APIM API referenced by the serverless service */ private async ensureApi() { - this.serverless.cli.log("-> Deploying API") + this.log("-> Deploying API") try { await this.apimClient.api.createOrUpdate(this.resourceGroup, this.config.resourceId, this.config.name, { @@ -81,8 +81,8 @@ export class ApimService extends BaseService { ] }); } catch (e) { - this.serverless.cli.log("Error creating APIM API"); - this.serverless.cli.log(JSON.stringify(e.body, null, 4)); + this.log("Error creating APIM API"); + this.log(JSON.stringify(e.body, null, 4)); } } @@ -91,7 +91,7 @@ export class ApimService extends BaseService { * @param functionAppUrl The host name for the deployed function app */ private async ensureBackend(functionApp) { - this.serverless.cli.log("-> Deploying API Backend") + this.log("-> Deploying API Backend") try { const functionAppResourceId = `https://management.azure.com${functionApp.id}`; @@ -107,8 +107,8 @@ export class ApimService extends BaseService { url: `https://${functionApp.defaultHostName}/api` }); } catch (e) { - this.serverless.cli.log("Error creating APIM Backend"); - this.serverless.cli.log(JSON.stringify(e.body, null, 4)); + this.log("Error creating APIM Backend"); + this.log(JSON.stringify(e.body, null, 4)); } } @@ -118,7 +118,7 @@ export class ApimService extends BaseService { * @param options The plugin options */ private async deployOperation(options) { - this.serverless.cli.log(`--> Deploying API operation ${options.function}`); + this.log(`--> Deploying API operation ${options.function}`); try { const client = new ApiManagementClient(this.credentials, this.subscriptionId); @@ -153,8 +153,8 @@ export class ApimService extends BaseService { ` }); } catch (e) { - this.serverless.cli.log(`Error deploying API operation ${options.function}`); - this.serverless.cli.log(JSON.stringify(e, null, 4)); + this.log(`Error deploying API operation ${options.function}`); + this.log(JSON.stringify(e, null, 4)); } } @@ -163,7 +163,7 @@ export class ApimService extends BaseService { * @param functionAppUrl The host name for the Azure function app */ private async ensureFunctionAppKeys(functionApp) { - this.serverless.cli.log("-> Deploying API keys") + this.log("-> Deploying API keys") try { const masterKey = await this.functionAppService.getMasterKey(functionApp); const keyName = `${this.serviceName}-key`; @@ -174,8 +174,8 @@ export class ApimService extends BaseService { value: masterKey }); } catch (e) { - this.serverless.cli.log("Error creating APIM Property"); - this.serverless.cli.log(JSON.stringify(e, null, 4)); + this.log("Error creating APIM Property"); + this.log(JSON.stringify(e, null, 4)); } } } diff --git a/src/services/baseService.ts b/src/services/baseService.ts index f94e0e80..54320d89 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -80,7 +80,7 @@ export abstract class BaseService { fs.createReadStream(filePath) .pipe(request(requestOptions, (err, response) => { if (err) { - this.serverless.cli.log(JSON.stringify(err, null, 4)); + this.log(JSON.stringify(err, null, 4)); return reject(err); } resolve(response); diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index bf302a8d..cd052723 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -19,9 +19,6 @@ describe("Function App Service", () => { const variables = MockFactory.createTestVariables(); const provider = MockFactory.createTestAzureServiceProvider(); - let sendFile; - let webAppDelete; - const masterKey = "masterKey"; const authKey = "authKey"; const syncTriggersMessage = "sync triggers success"; @@ -53,14 +50,12 @@ describe("Function App Service", () => { }); beforeEach(() => { - webAppDelete = jest.fn(); - sendFile = jest.fn(); WebSiteManagementClient.prototype.webApps = { get: jest.fn(() => app), - deleteFunction: webAppDelete, + deleteFunction: jest.fn(), } as any; - (FunctionAppService.prototype as any).sendFile = sendFile; + (FunctionAppService.prototype as any).sendFile = jest.fn(); }); afterEach(() => { @@ -93,7 +88,7 @@ describe("Function App Service", () => { const service = createService(); WebSiteManagementClient.prototype.webApps = { get: jest.fn(() => { return { error: { code: "ResourceNotFound"}}}), - deleteFunction: webAppDelete, + deleteFunction: jest.fn(), } as any; const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) @@ -144,7 +139,7 @@ describe("Function App Service", () => { it("uploads functions", async () => { const service = createService(); await service.uploadFunctions(app); - expect(sendFile).toBeCalledWith({ + expect((FunctionAppService.prototype as any).sendFile).toBeCalledWith({ method: "POST", uri: uploadUrl, json: true, diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 42e86c40..d7b6b012 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -142,7 +142,7 @@ export class FunctionAppService extends BaseService { } if (this.serverless.service.provider["armTemplate"]) { - this.serverless.cli.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); + this.log(`-> Deploying custom ARM template: ${this.serverless.service.provider["armTemplate"].file}`); templateFilePath = path.join(this.serverless.config.servicePath, this.serverless.service.provider["armTemplate"].file); const userParameters = this.serverless.service.provider["armTemplate"].parameters; const userParametersKeys = Object.keys(userParameters); diff --git a/src/services/resourceService.ts b/src/services/resourceService.ts index 7ef589c1..7bd46530 100644 --- a/src/services/resourceService.ts +++ b/src/services/resourceService.ts @@ -12,7 +12,7 @@ export class ResourceService extends BaseService { } public async deployResourceGroup() { - this.serverless.cli.log(`Creating resource group: ${this.resourceGroup}`); + this.log(`Creating resource group: ${this.resourceGroup}`); const groupParameters = { location: this.serverless.service.provider["location"] @@ -22,12 +22,12 @@ export class ResourceService extends BaseService { } public async deleteDeployment() { - this.serverless.cli.log(`Deleting deployment: ${this.deploymentName}`); + this.log(`Deleting deployment: ${this.deploymentName}`); return await this.resourceClient.deployments.deleteMethod(this.resourceGroup, this.deploymentName); } public async deleteResourceGroup() { - this.serverless.cli.log(`Deleting resource group: ${this.resourceGroup}`); + this.log(`Deleting resource group: ${this.resourceGroup}`); return await this.resourceClient.resourceGroups.deleteMethod(this.resourceGroup); } } \ No newline at end of file diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 71b056a1..0ec63ca1 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -2,11 +2,10 @@ import { AuthResponse, LinkedSubscription, TokenCredentialsBase } from "@azure/m import yaml from "js-yaml"; import Serverless from "serverless"; import Service from "serverless/classes/Service"; +import { AzureServiceProvider, FunctionApp, FunctionMetadata, Logger, + ServerlessYml, ServicePrincipalEnvVariables } from "../models"; import Utils = require("serverless/classes/Utils"); import PluginManager = require("serverless/classes/PluginManager"); -import { ServerlessYml, FunctionMetadata, FunctionEvent, ServicePrincipalEnvVariables, - FunctionApp, AzureServiceProvider, Logger } from "../models"; -import { stringify } from "querystring"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { From cc2c04b276f86c055ad3c7728ee210d1a1a567a3 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Tue, 4 Jun 2019 10:40:40 -0600 Subject: [PATCH 08/13] Cleanup --- src/models/azureProvider.ts | 2 -- src/models/functionApp.ts | 2 -- src/models/generic.ts | 1 - 3 files changed, 5 deletions(-) diff --git a/src/models/azureProvider.ts b/src/models/azureProvider.ts index 555fc64a..9b293ae6 100644 --- a/src/models/azureProvider.ts +++ b/src/models/azureProvider.ts @@ -23,5 +23,3 @@ export interface AzureServiceProvider { resourceGroup: string; deploymentName: string; } - - diff --git a/src/models/functionApp.ts b/src/models/functionApp.ts index 8bbbebf7..f150843a 100644 --- a/src/models/functionApp.ts +++ b/src/models/functionApp.ts @@ -1,5 +1,3 @@ - - export interface FunctionApp { id: string; name: string; diff --git a/src/models/generic.ts b/src/models/generic.ts index cc7c3c24..d187d7c9 100644 --- a/src/models/generic.ts +++ b/src/models/generic.ts @@ -1,4 +1,3 @@ - export interface Logger { log: (message: string) => void; } \ No newline at end of file From 3df65bfab3240057b8fae3b6e96f3104b733256a Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Thu, 6 Jun 2019 18:14:40 -0600 Subject: [PATCH 09/13] Fix all tests --- src/plugins/package/azurePackage.test.ts | 4 +- src/services/apimService.test.ts | 8 ++- src/services/baseService.ts | 4 ++ src/services/functionAppService.test.ts | 44 ++++++++------ src/test/mockFactory.ts | 77 +++++++++++++++++------- 5 files changed, 93 insertions(+), 44 deletions(-) diff --git a/src/plugins/package/azurePackage.test.ts b/src/plugins/package/azurePackage.test.ts index 385e815b..20921943 100644 --- a/src/plugins/package/azurePackage.test.ts +++ b/src/plugins/package/azurePackage.test.ts @@ -11,9 +11,7 @@ describe("Azure Package Plugin", () => { it("sets up provider configuration", async () => { const slsFunctionConfig = MockFactory.createTestSlsFunctionConfig(); const sls = MockFactory.createTestServerless(); - Object.assign(sls.service, { - functions: slsFunctionConfig - }); + Object.assign(sls.service, MockFactory.createTestService(slsFunctionConfig)); const functionConfig = Object.keys(slsFunctionConfig).map((funcName) => { return { diff --git a/src/services/apimService.test.ts b/src/services/apimService.test.ts index e8ae9957..4d6634e2 100644 --- a/src/services/apimService.test.ts +++ b/src/services/apimService.test.ts @@ -1,6 +1,7 @@ import Serverless from "serverless"; import _ from "lodash"; import { MockFactory } from "../test/mockFactory"; +import Service from "serverless/classes/Service"; import { ApiManagementConfig } from "../models/apiManagement"; import { ApimService } from "./apimService"; import { interpolateJson } from "../test/utils"; @@ -37,6 +38,7 @@ describe("APIM Service", () => { beforeEach(() => { const slsConfig: any = { + ...MockFactory.createTestService(MockFactory.createTestSlsFunctionConfig()), service: "test-sls", provider: { name: "azure", @@ -44,11 +46,11 @@ describe("APIM Service", () => { location: "West US", apim: apimConfig, }, - functions: MockFactory.createTestSlsFunctionConfig(), }; - serverless = MockFactory.createTestServerless(); - Object.assign(serverless.service, slsConfig); + serverless = MockFactory.createTestServerless({ + service: slsConfig + }); serverless.variables = { ...serverless.variables, diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 0db2419c..4596b7be 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -26,6 +26,10 @@ export abstract class BaseService { throw new Error(`Azure Credentials has not been set in ${this.constructor.name}`); } } + + protected log(message: string) { + this.serverless.cli.log(message); + } protected async sendApiRequest(method: string, relativeUrl: string, options: any = {}) { const defaultHeaders = { diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index a1394d87..9649d2ea 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -14,21 +14,24 @@ import MockAdapter from "axios-mock-adapter"; describe("Function App Service", () => { - const app = MockFactory.createTestFunctionApp(); + const app = MockFactory.createTestSite(); const slsService = MockFactory.createTestService(); const variables = MockFactory.createTestVariables(); const provider = MockFactory.createTestAzureServiceProvider(); + const functionName = "function1"; const masterKey = "masterKey"; const authKey = "authKey"; const syncTriggersMessage = "sync triggers success"; + const deleteFunctionMessage = "delete function success"; const functions = MockFactory.createTestFunctions(); + const baseUrl = "https://management.azure.com" const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`; - const authKeyUrl = `https://management.azure.com${app.id}/functions/admin/token?api-version=2016-08-01`; - const syncTriggersUrl = `https://management.azure.com${app.id}/syncfunctiontriggers?api-version=2016-08-01`; - const listFunctionsUrl = `https://management.azure.com${app.id}/functions?api-version=2016-08-01`; - const uploadUrl = `https://${app.name}${constants.scmDomain}${constants.scmZipDeployApiPath}` + 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 uploadUrl = `https://${app.enabledHostNames[0]}${constants.scmZipDeployApiPath}/` beforeAll(() => { @@ -43,6 +46,12 @@ describe("Function App Service", () => { axiosMock.onPost(syncTriggersUrl).reply(200, syncTriggersMessage); // List Functions axiosMock.onGet(listFunctionsUrl).reply(200, { value: functions }); + // Delete Function + for (const funcName of Object.keys(functions)) { + const func = functions[funcName]; + axiosMock.onDelete(`${baseUrl}${app.id}/functions/${func.properties.name}?api-version=2016-08-01`) + .reply(200, deleteFunctionMessage); + } mockFs({ "app.zip": "contents", @@ -76,7 +85,7 @@ describe("Function App Service", () => { ) } - fit("get returns function app", async () => { + it("get returns function app", async () => { const service = createService(); const result = await service.get(); expect(WebSiteManagementClient.prototype.webApps.get) @@ -105,12 +114,9 @@ describe("Function App Service", () => { it("deletes function", async () => { const service = createService(); - await service.deleteFunction(app.name); - expect(WebSiteManagementClient.prototype.webApps.deleteFunction).toBeCalledWith( - provider.resourceGroup, - slsService["service"], - app.name - ); + const response = await service.deleteFunction(app, functionName); + expect(response.data).toEqual(deleteFunctionMessage); + }); it("syncs triggers", async () => { @@ -120,20 +126,24 @@ describe("Function App Service", () => { }); it("cleans up", async () => { - const sls = MockFactory.createTestServerless(); + const sls = MockFactory.createTestServerless({ + service: slsService, + variables: variables, + }); const service = createService(sls); const result = await service.cleanUp(app); - expect(result).toHaveLength(functions.length); + const functionNames = Object.keys(functions); + expect(result).toHaveLength(functionNames.length); const logCalls = (sls.cli.log as any).mock.calls as any[]; - for (let i = 0; i < functions.length; i++) { + for (let i = 0; i < functionNames.length; i++) { const functionName = `function${i+1}` - expect(logCalls[i + 1][0]).toEqual(`-> Deleting function: '${functionName}'`); + expect(logCalls[i + 1][0]).toEqual(`-> Deleting function: ${functionName}`); } }); it("lists functions", async () => { const service = createService(); - expect(await service.listFunctions(app)).toEqual(functions); + expect(await service.listFunctions(app)).toEqual(functions.map((f) => f.properties)); }); it("uploads functions", async () => { diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 5c11222f..97977d60 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -26,13 +26,7 @@ export class MockFactory { sls.cli = getAttribute(config, "cli", MockFactory.createTestCli()); sls.pluginManager = getAttribute(config, "pluginManager", MockFactory.createTestPluginManager()); sls.variables = getAttribute(config, "variables", MockFactory.createTestVariables()); - - sls.service.getAllFunctions = jest.fn(() => { - return Object.keys(sls.service["functions"]); - }) - sls.service.getAllFunctionsNames = sls.service.getAllFunctions; - sls.service.getServiceName = jest.fn(() => sls.service["service"]); - + sls.service = getAttribute(config, "service", MockFactory.createTestService()); return sls; } @@ -179,22 +173,21 @@ export class MockFactory { } } - public static createTestFunctionApp(name?: string): FunctionApp { - return { - id: "App Id", - name: name || "App Name", - defaultHostName: "My Host Name" + public static createTestService(functions?): Service { + if (!functions) { + functions = {}; + for (const func of MockFactory.createTestFunctions()) { + functions[func.properties.name] = func + } } - } - - public static createTestService(): Service { + const serviceName = "serviceName"; return { - getAllFunctions: jest.fn(() => MockFactory.createTestFunctions().map((f) => f.name)), + getAllFunctions: jest.fn(() => Object.keys(functions)), getFunction: jest.fn(), getAllEventsInFunction: jest.fn(), - getAllFunctionsNames: jest.fn(), + getAllFunctionsNames: jest.fn(() => Object.keys(functions)), getEventInFunction: jest.fn(), - getServiceName: jest.fn(), + getServiceName: jest.fn(() => serviceName), load: jest.fn(), mergeResourceArrays: jest.fn(), setFunctionNames: jest.fn(), @@ -202,15 +195,16 @@ export class MockFactory { validate: jest.fn(), custom: null, provider: MockFactory.createTestAzureServiceProvider(), - service: "serviceName", + service: serviceName, artifact: "app.zip", + functions } as any as Service; } - public static createTestFunctions(functionCount = 3): FunctionApp[] { + public static createTestFunctions(functionCount = 3) { const functions = [] for (let i = 0; i < functionCount; i++) { - functions.push(MockFactory.createTestFunctionApp(`function${i + 1}`)); + functions.push(MockFactory.createTestFunction(`function${i + 1}`)); } return functions; } @@ -248,11 +242,52 @@ export class MockFactory { public static createTestSite(name: string = "Test"): Site { return { + id: "appId", name: name, location: "West US", + defaultHostName: "myHostName", + enabledHostNames: [ + "myHostName" + ] }; } + public static createTestFunction(name: string = "TestFunction") { + return { + properties: { + name, + config: { + bindings: MockFactory.createTestBindings() + } + } + } + } + + public static createTestBindings(bindingCount = 3) { + const bindings = []; + for (let i = 0; i < bindingCount; i++) { + bindings.push(MockFactory.createTestBinding()); + } + return bindings; + } + + public static createTestBinding() { + // Only supporting HTTP for now, could support others + return MockFactory.createTestHttpBinding(); + } + + public static createTestHttpBinding() { + return { + type: "httpTrigger", + authLevel: "anonymous", + direction: "in", + methods: [ + "get", + "post" + ] + } + } + public static createTestSlsFunctionConfig() { return { hello: { From 0b1a8c57feaa5d1e76f62b73d9259b98a0f30eef Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Thu, 6 Jun 2019 18:22:55 -0600 Subject: [PATCH 10/13] Clean up --- src/models/functionApp.ts | 6 ------ src/services/functionAppService.test.ts | 14 ++++++-------- src/test/mockFactory.ts | 13 ++++++------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/models/functionApp.ts b/src/models/functionApp.ts index 53b4137c..712f65f4 100644 --- a/src/models/functionApp.ts +++ b/src/models/functionApp.ts @@ -1,9 +1,3 @@ -export interface FunctionApp { - id: string; - name: string; - defaultHostName: string; -} - export interface FunctionAppHttpTriggerConfig { authLevel: string; methods: string[]; diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index 9649d2ea..8b27290d 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -1,16 +1,14 @@ -import mockFs from "mock-fs" -import { MockFactory } from "../test/mockFactory"; +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import mockFs from "mock-fs"; import Serverless from "serverless"; +import { constants } from "../config"; +import { MockFactory } from "../test/mockFactory"; import { FunctionAppService } from "./functionAppService"; -import { constants } from "../config" - -jest.mock("@azure/arm-resources") jest.mock("@azure/arm-appservice") import { WebSiteManagementClient } from "@azure/arm-appservice"; - -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; +jest.mock("@azure/arm-resources") describe("Function App Service", () => { diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 97977d60..bac3eda2 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -1,16 +1,15 @@ +import { ApiContract, ApiManagementServiceResource } from "@azure/arm-apimanagement/esm/models"; +import { Site } 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 { AzureServiceProvider, FunctionApp, FunctionMetadata, Logger, - ServerlessYml, ServicePrincipalEnvVariables } from "../models"; import Utils from "serverless/classes/Utils"; import PluginManager from "serverless/lib/classes/PluginManager"; -import { HttpHeaders, WebResource, HttpOperationResponse, HttpResponse } from "@azure/ms-rest-js"; -import { AxiosResponse, AxiosRequestConfig } from "axios"; -import { TokenClientCredentials, TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials"; -import { Site } from "@azure/arm-appservice/esm/models"; -import { ApiManagementServiceResource, ApiContract } from "@azure/arm-apimanagement/esm/models"; +import { AzureServiceProvider, FunctionMetadata, Logger, ServerlessYml, ServicePrincipalEnvVariables } from "../models"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { From f992883f61084f50e5d57fb449faa60988e3064c Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 7 Jun 2019 10:34:12 -0600 Subject: [PATCH 11/13] Fixed mock factory to use same function structure --- src/services/functionAppService.test.ts | 19 +++++++---------- src/test/mockFactory.ts | 28 +++++++++++-------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/src/services/functionAppService.test.ts b/src/services/functionAppService.test.ts index 8b27290d..7897ac48 100644 --- a/src/services/functionAppService.test.ts +++ b/src/services/functionAppService.test.ts @@ -22,7 +22,8 @@ describe("Function App Service", () => { const authKey = "authKey"; const syncTriggersMessage = "sync triggers success"; const deleteFunctionMessage = "delete function success"; - const functions = MockFactory.createTestFunctions(); + const functions = MockFactory.createTestSlsFunctionConfig(); + const functionsResponse = MockFactory.createTestFunctionsResponse(functions); const baseUrl = "https://management.azure.com" const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`; @@ -43,11 +44,10 @@ describe("Function App Service", () => { // Sync Triggers axiosMock.onPost(syncTriggersUrl).reply(200, syncTriggersMessage); // List Functions - axiosMock.onGet(listFunctionsUrl).reply(200, { value: functions }); + axiosMock.onGet(listFunctionsUrl).reply(200, { value: functionsResponse }); // Delete Function for (const funcName of Object.keys(functions)) { - const func = functions[funcName]; - axiosMock.onDelete(`${baseUrl}${app.id}/functions/${func.properties.name}?api-version=2016-08-01`) + axiosMock.onDelete(`${baseUrl}${app.id}/functions/${funcName}?api-version=2016-08-01`) .reply(200, deleteFunctionMessage); } @@ -112,7 +112,7 @@ describe("Function App Service", () => { it("deletes function", async () => { const service = createService(); - const response = await service.deleteFunction(app, functionName); + const response = await service.deleteFunction(app, Object.keys(functions)[0]); expect(response.data).toEqual(deleteFunctionMessage); }); @@ -124,24 +124,21 @@ describe("Function App Service", () => { }); it("cleans up", async () => { - const sls = MockFactory.createTestServerless({ - service: slsService, - variables: variables, - }); + const sls = MockFactory.createTestServerless(); const service = createService(sls); const result = await service.cleanUp(app); const functionNames = Object.keys(functions); expect(result).toHaveLength(functionNames.length); const logCalls = (sls.cli.log as any).mock.calls as any[]; for (let i = 0; i < functionNames.length; i++) { - const functionName = `function${i+1}` + const functionName = functionNames[i]; expect(logCalls[i + 1][0]).toEqual(`-> Deleting function: ${functionName}`); } }); it("lists functions", async () => { const service = createService(); - expect(await service.listFunctions(app)).toEqual(functions.map((f) => f.properties)); + expect(await service.listFunctions(app)).toEqual(functionsResponse.map((f) => f.properties)); }); it("uploads functions", async () => { diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index bac3eda2..6736a90c 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -1,5 +1,5 @@ import { ApiContract, ApiManagementServiceResource } from "@azure/arm-apimanagement/esm/models"; -import { Site } from "@azure/arm-appservice/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"; @@ -174,10 +174,7 @@ export class MockFactory { public static createTestService(functions?): Service { if (!functions) { - functions = {}; - for (const func of MockFactory.createTestFunctions()) { - functions[func.properties.name] = func - } + functions = MockFactory.createTestSlsFunctionConfig() } const serviceName = "serviceName"; return { @@ -200,12 +197,13 @@ export class MockFactory { } as any as Service; } - public static createTestFunctions(functionCount = 3) { - const functions = [] - for (let i = 0; i < functionCount; i++) { - functions.push(MockFactory.createTestFunction(`function${i + 1}`)); + public static createTestFunctionsResponse(functions?) { + const result = [] + functions = functions || MockFactory.createTestSlsFunctionConfig(); + for (const name of Object.keys(functions)) { + result.push({ properties: MockFactory.createTestFunctionEnvelope(name)}); } - return functions; + return result; } public static createTestAzureServiceProvider(): AzureServiceProvider { @@ -251,13 +249,11 @@ export class MockFactory { }; } - public static createTestFunction(name: string = "TestFunction") { + public static createTestFunctionEnvelope(name: string = "TestFunction"): FunctionEnvelope { return { - properties: { - name, - config: { - bindings: MockFactory.createTestBindings() - } + name, + config: { + bindings: MockFactory.createTestBindings() } } } From a30c1f258c259d62dd542e6ff3d8f84f16eb84a9 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 7 Jun 2019 10:39:25 -0600 Subject: [PATCH 12/13] Remove unnecessary service assignment --- src/plugins/package/azurePackage.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/package/azurePackage.test.ts b/src/plugins/package/azurePackage.test.ts index 20921943..8113e18f 100644 --- a/src/plugins/package/azurePackage.test.ts +++ b/src/plugins/package/azurePackage.test.ts @@ -11,7 +11,6 @@ describe("Azure Package Plugin", () => { it("sets up provider configuration", async () => { const slsFunctionConfig = MockFactory.createTestSlsFunctionConfig(); const sls = MockFactory.createTestServerless(); - Object.assign(sls.service, MockFactory.createTestService(slsFunctionConfig)); const functionConfig = Object.keys(slsFunctionConfig).map((funcName) => { return { From 87aebf39951bb121b161036ddefca7eb1d937ada Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 7 Jun 2019 12:10:17 -0600 Subject: [PATCH 13/13] Respond to PR feedback --- src/models/index.ts | 4 ---- src/models/serverless.ts | 2 +- src/services/functionAppService.ts | 2 +- src/test/mockFactory.ts | 6 ++++-- 4 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 src/models/index.ts diff --git a/src/models/index.ts b/src/models/index.ts deleted file mode 100644 index 573bd832..00000000 --- a/src/models/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./azureProvider"; -export * from "./functionApp"; -export * from "./generic"; -export * from "./serverless"; \ No newline at end of file diff --git a/src/models/serverless.ts b/src/models/serverless.ts index d3ccc4b2..debd6df5 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -1,4 +1,4 @@ -export interface ServerlessYml { +export interface ServerlessAzureConfig { provider: { name: string; location: string; diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index 32c94fa2..8c71fa14 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -30,7 +30,7 @@ export class FunctionAppService extends BaseService { return response; } - public async getMasterKey(functionApp?) { + public async getMasterKey(functionApp?: Site) { functionApp = functionApp || await this.get(); const adminToken = await this.getAuthKey(functionApp); const keyUrl = `https://${functionApp.defaultHostName}/admin/host/systemkeys/_master`; diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 6736a90c..3a1ef76a 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -9,7 +9,9 @@ import Serverless from "serverless"; import Service from "serverless/classes/Service"; import Utils from "serverless/classes/Utils"; import PluginManager from "serverless/lib/classes/PluginManager"; -import { AzureServiceProvider, FunctionMetadata, Logger, ServerlessYml, ServicePrincipalEnvVariables } from "../models"; +import { ServerlessAzureConfig } from "../models/serverless"; +import { FunctionMetadata, AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider" +import { Logger } from "../models/generic"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { @@ -128,7 +130,7 @@ export class MockFactory { return Promise.resolve(response); } - public static createTestServerlessYml(asYaml = false, functionMetadata?): ServerlessYml { + public static createTestServerlessYml(asYaml = false, functionMetadata?): ServerlessAzureConfig { const data = { "provider": { "name": "azure",