Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.
Merged
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
96 changes: 74 additions & 22 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions src/models/serverless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export interface ServerlessAzureConfig {
functions: any;
}

export interface ServerlessCommand {
usage: string;
lifecycleEvents: string[];
options?: {
[key: string]: {
usage: string;
shortcut?: string;
};
};
commands?: ServerlessCommandMap;
}

export interface ServerlessCommandMap {
[command: string]: ServerlessCommand;
}
export interface ServerlessAzureOptions extends Serverless.Options {
resourceGroup?: string;
}
1 change: 1 addition & 0 deletions src/plugins/deploy/azureDeployPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class AzureDeployPlugin {

private async deploy() {
const resourceService = new ResourceService(this.serverless, this.options);

await resourceService.deployResourceGroup();

const functionAppService = new FunctionAppService(this.serverless, this.options);
Expand Down
97 changes: 97 additions & 0 deletions src/plugins/invoke/azureInvoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { MockFactory } from "../../test/mockFactory";
import { invokeHook } from "../../test/utils";
import mockFs from "mock-fs";
import { AzureInvoke } from "./azureInvoke";
jest.mock("../../services/functionAppService");
jest.mock("../../services/resourceService");
jest.mock("../../services/invokeService");
import { InvokeService } from "../../services/invokeService";

describe("Azure Invoke Plugin", () => {
const fileContent = JSON.stringify({
name: "Azure-Test",
});
afterEach(() => {
jest.resetAllMocks();
})

beforeAll(() => {
mockFs({
"testFile.json": fileContent,
}, { createCwd: true, createTmp: true });
});
afterAll(() => {
mockFs.restore();
});

it("calls invoke hook", async () => {
const expectedResult = { data: "test" };
const invoke = jest.fn(() => expectedResult);
InvokeService.prototype.invoke = invoke as any;

const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["function"] = "testApp";
options["data"] = "{\"name\": \"AzureTest\"}";
options["method"] = "GET";

const plugin = new AzureInvoke(sls, options);
await invokeHook(plugin, "invoke:invoke");
expect(invoke).toBeCalledWith(options["method"], options["function"], options["data"]);
expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
});

it("calls the invoke hook with file path", async () => {
const expectedResult = { data: "test" };
const invoke = jest.fn(() => expectedResult);
InvokeService.prototype.invoke = invoke as any;
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["function"] = "testApp";
options["path"] = "testFile.json";
options["method"] = "GET";
const plugin = new AzureInvoke(sls, options);
await invokeHook(plugin, "invoke:invoke");
expect(invoke).toBeCalledWith(options["method"], options["function"], fileContent);
expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));

});

it("calls the invoke hook with file path", async () => {
const invoke = jest.fn();
InvokeService.prototype.invoke = invoke;
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["function"] = "testApp";
options["path"] = "notExist.json";
options["method"] = "GET";
expect(() => new AzureInvoke(sls, options)).toThrow();
});

it("Function invoked with no data", async () => {
const expectedResult = { data: "test" };
const invoke = jest.fn(() => expectedResult);
InvokeService.prototype.invoke = invoke as any;
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["function"] = "testApp";
options["method"] = "GET";
const plugin = new AzureInvoke(sls, options);
await invokeHook(plugin, "invoke:invoke");
expect(invoke).toBeCalledWith(options["method"], options["function"], undefined);
expect(sls.cli.log).toBeCalledWith(JSON.stringify(expectedResult.data));
});

it("The invoke function fails when no function name is passed", async () => {
const invoke = jest.fn();
InvokeService.prototype.invoke = invoke;
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
options["function"] = null;
options["data"] = "{\"name\": \"AzureTest\"}";
options["method"] = "GET";
const plugin = new AzureInvoke(sls, options);
await invokeHook(plugin, "invoke:invoke");
expect(invoke).not.toBeCalled();
});
});
67 changes: 49 additions & 18 deletions src/plugins/invoke/azureInvoke.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,72 @@
import { isAbsolute, join } from "path";
import Serverless from "serverless";
import { join, isAbsolute } from "path";
import AzureProvider from "../../provider/azureProvider";
import { InvokeService } from "../../services/invokeService";
import fs from "fs";
import { ServerlessCommandMap } from "../../models/serverless";

export class AzureInvoke {
public hooks: { [eventName: string]: Promise<any> };
private provider: AzureProvider;

private commands: ServerlessCommandMap;
private invokeService: InvokeService;
public constructor(private serverless: Serverless, private options: Serverless.Options) {
this.provider = (this.serverless.getProvider("azure") as any) as AzureProvider;
const path = this.options["path"];

if (path) {
const absolutePath = isAbsolute(path)
? path
: join(this.serverless.config.servicePath, path);
this.serverless.cli.log(this.serverless.config.servicePath);
this.serverless.cli.log(path);

if (!this.serverless.utils.fileExistsSync(absolutePath)) {
if (!fs.existsSync(absolutePath)) {
throw new Error("The file you provided does not exist.");
}
this.options["data"] = this.serverless.utils.readFileSync(absolutePath);
this.options["data"] = fs.readFileSync(absolutePath).toString();
}

this.commands = {
invoke: {
usage: "Invoke command",
lifecycleEvents: ["invoke"],
options: {
function: {
usage: "Function to call",
shortcut: "f",
},
path: {
usage: "Path to file to put in body",
shortcut: "p"
},
data: {
usage: "Data string for body of request",
shortcut: "d"
},
method: {
usage: "HTTP method (Default is GET)",
shortcut: "m"
}
}
}
}

this.hooks = {
"before:invoke:invoke": this.provider.getAdminKey.bind(this),
"invoke:invoke": this.invoke.bind(this)
};
}

private async invoke() {
const func = this.options.function;
const functionObject = this.serverless.service.getFunction(func);
const eventType = Object.keys(functionObject["events"][0])[0];

if (!this.options["data"]) {
this.options["data"] = {};
const functionName = this.options["function"];
const data = this.options["data"];
const method = this.options["method"] || "GET";
if (!functionName) {
this.serverless.cli.log("Need to provide a name of function to invoke");
return;
}

return this.provider.invoke(func, eventType, this.options["data"]);
this.invokeService = new InvokeService(this.serverless, this.options);
const response = await this.invokeService.invoke(method, functionName, data);
if(response){
this.serverless.cli.log(JSON.stringify(response.data));
}
}
}
}
1 change: 1 addition & 0 deletions src/plugins/login/loginPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class AzureLoginPlugin {
this.hooks = {
"before:package:initialize": this.login.bind(this),
"before:deploy:list:list": this.login.bind(this),
"before:invoke:invoke": this.login.bind(this),
"before:rollback:rollback": this.login.bind(this),
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/services/baseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export abstract class BaseService {
protected getAccessToken(): string {
return (this.credentials.tokenCache as any)._entries[0].accessToken;
}

/**
* Sends an API request using axios HTTP library
* @param method The HTTP method
Expand Down Expand Up @@ -231,4 +231,4 @@ export abstract class BaseService {
}
return timestamp;
}
}
}
5 changes: 4 additions & 1 deletion src/services/functionAppService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class FunctionAppService extends BaseService {
public async get(): Promise<Site> {
const response: any = await this.webClient.webApps.get(this.resourceGroup, FunctionAppResource.getResourceName(this.config));
if (response.error && (response.error.code === "ResourceNotFound" || response.error.code === "ResourceGroupNotFound")) {
this.serverless.cli.log(this.resourceGroup);
this.serverless.cli.log(FunctionAppResource.getResourceName(this.config));
this.serverless.cli.log(JSON.stringify(response));
return null;
}

Expand Down Expand Up @@ -214,7 +217,7 @@ export class FunctionAppService extends BaseService {
return `${deploymentName.replace("rg-deployment", "artifact")}.zip`;
}

private getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
public getFunctionHttpTriggerConfig(functionApp: Site, functionConfig: FunctionEnvelope): FunctionAppHttpTriggerConfig {
const httpTrigger = functionConfig.config.bindings.find((binding) => {
return binding.type === "httpTrigger";
});
Expand Down
73 changes: 73 additions & 0 deletions src/services/invokeService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import { MockFactory } from "../test/mockFactory";
import { InvokeService } from "./invokeService";
jest.mock("@azure/arm-appservice")
jest.mock("@azure/arm-resources")
jest.mock("./functionAppService")
import { FunctionAppService } from "./functionAppService";

describe("Invoke Service ", () => {
const app = MockFactory.createTestSite();
const expectedSite = MockFactory.createTestSite();
const testData = "test-data";
const testResult = "test-data";
const authKey = "authKey";
const baseUrl = "https://management.azure.com"
const masterKeyUrl = `https://${app.defaultHostName}/admin/host/systemkeys/_master`;
const authKeyUrl = `${baseUrl}${app.id}/functions/admin/token?api-version=2016-08-01`;
let urlPOST = `http://${app.defaultHostName}/api/hello`;
let urlGET = `http://${app.defaultHostName}/api/hello?name%3D${testData}`;
let masterKey: string;

beforeAll(() => {
const axiosMock = new MockAdapter(axios);
// Master Key
axiosMock.onGet(masterKeyUrl).reply(200, { value: masterKey });
// Auth Key
axiosMock.onGet(authKeyUrl).reply(200, authKey);
//Mock url for GET
axiosMock.onGet(urlGET).reply(200, testResult);
//Mock url for POST
axiosMock.onPost(urlPOST).reply(200, testResult);
});

beforeEach(() => {
FunctionAppService.prototype.getMasterKey = jest.fn();
FunctionAppService.prototype.get = jest.fn(() => Promise.resolve(expectedSite));
});

it("Invokes a function with GET request", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const expectedResult = {url: `${app.defaultHostName}/api/hello`};
const httpConfig = jest.fn(() => expectedResult);

FunctionAppService.prototype.getFunctionHttpTriggerConfig = httpConfig as any;

options["function"] = "hello";
options["data"] = `{"name": "${testData}"}`;
options["method"] = "GET";

const service = new InvokeService(sls, options);
const response = await service.invoke(options["method"], options["function"], options["data"]);
expect(JSON.stringify(response.data)).toEqual(JSON.stringify(testResult));
});

it("Invokes a function with POST request", async () => {
const sls = MockFactory.createTestServerless();
const options = MockFactory.createTestServerlessOptions();
const expectedResult = {url: `${app.defaultHostName}/api/hello`};
const httpConfig = jest.fn(() => expectedResult);
FunctionAppService.prototype.getFunctionHttpTriggerConfig = httpConfig as any;

options["function"] = "hello";
options["data"] = `{"name": "${testData}"}`;
options["method"] = "POST";

const service = new InvokeService(sls, options);
const response = await service.invoke(options["method"], options["function"], options["data"]);
expect(JSON.stringify(response.data)).toEqual(JSON.stringify(testResult));
});

});
Loading