From 96f4568d726f1f8527495f1143d916f4f5e92adf Mon Sep 17 00:00:00 2001 From: Neeraj Mandal Date: Tue, 2 Jul 2019 11:16:36 -0700 Subject: [PATCH 1/3] Test files added Signed-off-by: Neeraj Mandal --- src/plugins/invoke/azureInvoke.test.ts | 98 ++++++++++++++++++++++++++ src/plugins/invoke/azureInvoke.ts | 70 +++++++++++++----- src/services/invokeService.test.ts | 46 ++++++++++++ src/services/invokeService.ts | 84 ++++++++++++++++++++++ 4 files changed, 282 insertions(+), 16 deletions(-) create mode 100644 src/plugins/invoke/azureInvoke.test.ts create mode 100644 src/services/invokeService.test.ts create mode 100644 src/services/invokeService.ts diff --git a/src/plugins/invoke/azureInvoke.test.ts b/src/plugins/invoke/azureInvoke.test.ts new file mode 100644 index 00000000..7f45eb47 --- /dev/null +++ b/src/plugins/invoke/azureInvoke.test.ts @@ -0,0 +1,98 @@ +import { MockFactory } from "../../test/mockFactory"; +import { invokeHook } from "../../test/utils"; +import mockFs from "mock-fs"; +import { AzureInvoke } from "./azureInvoke"; +import { InvokeService } from "../../services/invokeService"; + +jest.mock("../../services/functionAppService"); +jest.mock("../../services/resourceService"); +jest.mock("../../services/invokeService"); + +describe("Azure Invoke Plugin", () => { + const fileContent = JSON.stringify({ + name: "Azure-Test", + }); + afterEach(() => { + jest.resetAllMocks(); + }) + + beforeAll(() => { + mockFs({ + "testFile.json": fileContent, + }, { createCwd: true, createTmp: true }); + }); + afterAll(() => { + mockFs.restore(); + }); + + it("calls invoke hook", async () => { + const expectedResult = { data: "test" }; + const invoke = jest.fn(() => expectedResult); + InvokeService.prototype.invoke = invoke as any; + + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + options["function"] = "testApp"; + options["data"] = "{\"name\": \"AzureTest\"}"; + options["method"] = "GET"; + + const plugin = new AzureInvoke(sls, options); + await invokeHook(plugin, "invoke:invoke"); + expect(invoke).toBeCalledWith(options["function"], options["data"], options["method"]); + expect(sls.cli.log).toBeCalledWith(expectedResult.data); + }); + + it("calls the invoke hook with file path", async () => { + const expectedResult = { data: "test" }; + const invoke = jest.fn(() => expectedResult); + InvokeService.prototype.invoke = invoke as any; + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + options["function"] = "testApp"; + options["path"] = "testFile.json"; + options["method"] = "GET"; + + const plugin = new AzureInvoke(sls, options); + await invokeHook(plugin, "invoke:invoke"); + expect(invoke).toBeCalledWith(options["function"], fileContent, options["method"]); + expect(sls.cli.log).toBeCalledWith(expectedResult.data); + + }); + + it("calls the invooke hook with file path", async () => { + const invoke = jest.fn(); + InvokeService.prototype.invoke = invoke; + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + options["function"] = "testApp"; + options["path"] = "garbage.json"; + options["method"] = "GET"; + expect(() => new AzureInvoke(sls, options)).toThrow(); + }); + + it("The invoke function fails when no data is passsed", async () => { + const invoke = jest.fn(); + InvokeService.prototype.invoke = invoke; + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + options["function"] = "testApp"; + options["data"] = null; + options["method"] = "GET"; + const plugin = new AzureInvoke(sls, options); + await invokeHook(plugin, "invoke:invoke"); + expect(invoke).not.toBeCalled(); + }); + + it("The invoke function fails when no function name is passed", async () => { + const invoke = jest.fn(); + InvokeService.prototype.invoke = invoke; + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + options["function"] = null; + options["data"] = "{\"name\": \"AzureTest\"}"; + options["method"] = "GET"; + const plugin = new AzureInvoke(sls, options); + await invokeHook(plugin, "invoke:invoke"); + expect(invoke).not.toBeCalled(); + }); +}); diff --git a/src/plugins/invoke/azureInvoke.ts b/src/plugins/invoke/azureInvoke.ts index f8da7e26..804f686b 100644 --- a/src/plugins/invoke/azureInvoke.ts +++ b/src/plugins/invoke/azureInvoke.ts @@ -1,41 +1,79 @@ +import { isAbsolute, join } from "path"; import Serverless from "serverless"; -import { join, isAbsolute } from "path"; import AzureProvider from "../../provider/azureProvider"; +import { InvokeService } from "../../services/invokeService"; +import { AzureLoginPlugin } from "../login/loginPlugin"; +import fs from "fs"; export class AzureInvoke { public hooks: { [eventName: string]: Promise }; + private commands: any; private provider: AzureProvider; - + private login: AzureLoginPlugin + private invokeService: InvokeService; public constructor(private serverless: Serverless, private options: Serverless.Options) { this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider; const path = this.options["path"]; - + this.login = new AzureLoginPlugin(this.serverless, this.options); + if (path) { const absolutePath = isAbsolute(path) ? path : join(this.serverless.config.servicePath, path); + this.serverless.cli.log(this.serverless.config.servicePath); + this.serverless.cli.log(path); - if (!this.serverless.utils.fileExistsSync(absolutePath)) { + if (!fs.existsSync(absolutePath)) { throw new Error("The file you provided does not exist."); } - this.options["data"] = this.serverless.utils.readFileSync(absolutePath); + this.options["data"] = fs.readFileSync(absolutePath).toString(); + } + this.commands = { + invoke: { + usage: "Invoke command", + lifecycleEvents: [ + "invoke" + ], + options: { + function: { + usage: "Function to call", + shortcut: "f", + }, + path: { + usage: "Path to file to put in body", + shortcut: "p" + }, + data: { + usage: "Data string for body of request", + shortcut: "d" + }, + method: { + usage: "GET or POST request (Default is GET)", + shortcut: "m" + } + } + } } - this.hooks = { - "before:invoke:invoke": this.provider.getAdminKey.bind(this), "invoke:invoke": this.invoke.bind(this) }; + } private async invoke() { - const func = this.options.function; - const functionObject = this.serverless.service.getFunction(func); - const eventType = Object.keys(functionObject["events"][0])[0]; - - if (!this.options["data"]) { - this.options["data"] = {}; + const functionName = this.options["function"]; + const data = this.options["data"]; + const method = this.options["method"] || "GET"; + if (!functionName) { + this.serverless.cli.log("Need to provide a name of function to invoke"); + return; } - - return this.provider.invoke(func, eventType, this.options["data"]); + if (!data) { + this.serverless.cli.log("Need to provide data or path"); + return; + } + this.invokeService = new InvokeService(this.serverless, this.options); + const response = await this.invokeService.invoke(functionName, data, method); + this.serverless.cli.log(response.data); } -} +} \ No newline at end of file diff --git a/src/services/invokeService.test.ts b/src/services/invokeService.test.ts new file mode 100644 index 00000000..b166052f --- /dev/null +++ b/src/services/invokeService.test.ts @@ -0,0 +1,46 @@ +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import { MockFactory } from "../test/mockFactory"; +import { InvokeService } from "./invokeService"; +import { FunctionAppService } from "./functionAppService"; +jest.mock("@azure/arm-appservice") +jest.mock("@azure/arm-resources") + +describe("Invoke Service ", () => { + const app = MockFactory.createTestSite(); + const slsService = MockFactory.createTestService(); + const testData = "test-data"; + const result = "result"; + const authKey = "authKey"; + const baseUrl = "https://management.azure.com" + const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`; + const authKeyUrl = `${baseUrl}${app.id}/functions/admin/token?api-version=2016-08-01`; + const functionUrl = `http://${slsService.getServiceName()}.azurewebsites.net/api/hello?name=${testData}`; + let masterKey: string; + + beforeAll(() => { + + const axiosMock = new MockAdapter(axios); + + // Master Key + axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey }); + // Auth Key + axiosMock.onGet(authKeyUrl).reply(200, authKey); + axiosMock.onGet(functionUrl).reply(200, result); + }); + beforeEach(() => { + FunctionAppService.prototype.getMasterKey = jest.fn(); + }); + + + it("Invokes a function", async () => { + const sls = MockFactory.createTestServerless(); + const options = MockFactory.createTestServerlessOptions(); + options["function"] = "hello"; + options["data"] = `{"name": "${testData}"}`; + options["method"] = "GET"; + const service = new InvokeService(sls, options); + const response = await service.invoke(options["function"], options["data"], options["method"]); + expect(response.data).toEqual(result); + }); +}); \ No newline at end of file diff --git a/src/services/invokeService.ts b/src/services/invokeService.ts new file mode 100644 index 00000000..e836fc47 --- /dev/null +++ b/src/services/invokeService.ts @@ -0,0 +1,84 @@ +import { BaseService } from "./baseService" +import Serverless from "serverless"; +import config from "../config"; +import axios from "axios"; +import { FunctionAppService } from "./functionAppService"; + +export class InvokeService extends BaseService { + public serverless: Serverless; + public options: Serverless.Options; + public constructor(serverless: Serverless, options: Serverless.Options) { + super(serverless, options); + this.serverless = serverless; + this.options = options; + } + + /** + * Invoke an Azure Function + * @param functionName Name of function to invoke + * @param data Data to use as body or query params + * @param method GET or POST + */ + public async invoke(functionName: string, data: any, method: string){ + /* accesses the admin key */ + if (!(functionName in this.slsFunctions())) { + this.serverless.cli.log(`Function ${functionName} does not exist`); + return; + } + const functionObject = this.slsFunctions()[functionName]; + const eventType = Object.keys(functionObject["events"][0])[0]; + if (eventType !== "http") { + this.log("Needs to be an http function"); + return; + } + let url = `http://${this.serviceName}${config.functionAppDomain}${config.functionAppApiPath + functionName}`; + + if (method === "GET") { + const queryString = this.getQueryString(data); + url += `?${queryString}` + } + + this.log(url); + const options = await this.getOptions(method, data); + this.log(`Invoking function ${functionName} with ${method} request`); + return await axios(url, options); + } + + private getQueryString(eventData: any) { + if (typeof eventData === "string") { + try { + eventData = JSON.parse(eventData); + } + catch (error) { + return Promise.reject("The specified input data isn't a valid JSON string. " + + "Please correct it and try invoking the function again."); + } + } + return Object.keys(eventData) + .map((key) => `${key}=${eventData[key]}`) + .join("&"); + } + + /** + * Get options object + * @param method The method used (POST or GET) + * @param data Data to use as body or query params + */ + private async getOptions(method: string, data?: any) { + + const functionAppService = new FunctionAppService(this.serverless, this.options); + const functionsAdminKey = await functionAppService.getMasterKey(); + this.log(functionsAdminKey); + const options: any = { + host: config.functionAppDomain, + headers: { + "x-functions-key": functionsAdminKey + }, + method, + }; + if (method === "POST" && data) { + options.body = data; + } + return options; + } +} \ No newline at end of file From c99cba33b832c3f2eef90c6939072c63379350b2 Mon Sep 17 00:00:00 2001 From: Neeraj Mandal Date: Tue, 2 Jul 2019 11:28:17 -0700 Subject: [PATCH 2/3] test files added Signed-off-by: Neeraj Mandal --- adduid | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 adduid diff --git a/adduid b/adduid new file mode 100644 index 00000000..e69de29b From 85301c9102f0fb15a90adf0022fbc422648f3d6c Mon Sep 17 00:00:00 2001 From: Neeraj Mandal Date: Tue, 2 Jul 2019 11:51:17 -0700 Subject: [PATCH 3/3] final test added --- src/services/invokeService.test.ts | 1 - src/services/invokeService.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/invokeService.test.ts b/src/services/invokeService.test.ts index b166052f..eea6eee8 100644 --- a/src/services/invokeService.test.ts +++ b/src/services/invokeService.test.ts @@ -21,7 +21,6 @@ describe("Invoke Service ", () => { beforeAll(() => { const axiosMock = new MockAdapter(axios); - // Master Key axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey }); // Auth Key diff --git a/src/services/invokeService.ts b/src/services/invokeService.ts index e836fc47..0c009062 100644 --- a/src/services/invokeService.ts +++ b/src/services/invokeService.ts @@ -13,6 +13,7 @@ export class InvokeService extends BaseService { this.options = options; } + /** * Invoke an Azure Function * @param functionName Name of function to invoke