Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions src/plugins/invoke/azureInvoke.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
70 changes: 54 additions & 16 deletions src/plugins/invoke/azureInvoke.ts
Original file line number Diff line number Diff line change
@@ -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<any> };
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);
}
}
}
47 changes: 47 additions & 0 deletions src/services/invokeService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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);
});
});
84 changes: 84 additions & 0 deletions src/services/invokeService.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}