UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

633 lines (569 loc) 20.2 kB
import { AsyncReturnType } from "type-fest"; import { config, api, Process, Action, specHelper } from "./../../src"; const actionhero = new Process(); describe("Core", () => { describe("api", () => { beforeAll(async () => { await actionhero.start(); }); afterAll(async () => { await actionhero.stop(); }); test("should have an api object with proper parts", () => { [ api.actions.actions, api.actions.versions, api.actions.actions.cacheTest["1"], api.actions.actions.randomNumber["1"], api.actions.actions.status["1"], ].forEach((item) => { expect(item).toBeInstanceOf(Object); }); [ api.actions.actions.cacheTest["1"].run, api.actions.actions.randomNumber["1"].run, api.actions.actions.status["1"].run, ].forEach((item) => { expect(item).toBeInstanceOf(Function); }); [ api.actions.actions.randomNumber["1"].name, api.actions.actions.randomNumber["1"].description, ].forEach((item) => { expect(typeof item).toEqual("string"); }); expect(config).toBeInstanceOf(Object); }); test("should have loaded postVariables properly", () => { [ "file", "callback", "action", "apiVersion", "key", // from cacheTest action "value", // from cacheTest action ].forEach((item) => { expect(api.params.postVariables.indexOf(item) >= 0).toEqual(true); }); }); describe("api versions", () => { beforeAll(() => { api.actions.versions.versionedAction = [1, 2, 3]; api.actions.actions.versionedAction = { //@ts-ignore 1: { name: "versionedAction", description: "I am a test", version: 1, outputExample: {}, run: async () => { return { version: 1 }; }, }, //@ts-ignore 2: { name: "versionedAction", description: "I am a test", version: 2, outputExample: {}, run: async () => { return { version: 2 }; }, }, //@ts-ignore 3: { name: "versionedAction", description: "I am a test", version: 3, outputExample: {}, run: async (data) => { data.response!.version = 3; data.response!.error = { a: { complex: "error" }, }; }, }, }; }); afterAll(() => { delete api.actions.actions.versionedAction; delete api.actions.versions.versionedAction; }); test("will default actions to version 1 when no version is provided by the definition", async () => { const response = await specHelper.runAction("randomNumber"); expect( response.requesterInformation!.receivedParams.apiVersion, ).toEqual(1); }); test("can specify an apiVersion", async () => { let response; response = await specHelper.runAction("versionedAction", { apiVersion: 1, }); expect(response.requesterInformation.receivedParams.apiVersion).toEqual( 1, ); response = await specHelper.runAction("versionedAction", { apiVersion: 2, }); expect(response.requesterInformation.receivedParams.apiVersion).toEqual( 2, ); }); test("will default clients to the latest version of the action", async () => { const response = await specHelper.runAction("versionedAction"); expect( response.requesterInformation!.receivedParams.apiVersion, ).toEqual(3); }); test("will fail on a missing action + version", async () => { const response = await specHelper.runAction("versionedAction", { apiVersion: 10, }); expect(response.error).toEqual( "Error: unknown action or invalid apiVersion", ); }); test("can return complex error responses", async () => { const response = await specHelper.runAction("versionedAction", { apiVersion: 3, }); expect(response.error.a.complex).toEqual("error"); }); }); describe("action constructor", () => { test("validates actions", () => { class GoodAction extends Action { constructor() { super(); this.name = "good"; this.description = "good"; this.outputExample = {}; } async run() {} } class BadAction extends Action { constructor() { super(); // this.name = 'bad' this.description = "bad"; this.outputExample = {}; } async run() {} } const goodAction = new GoodAction(); const badAction = new BadAction(); goodAction.validate(); try { badAction.validate(); throw new Error("should not get here"); } catch (error) { expect(error.toString()).toMatch(/name is required for this action/); } }); test("actions cannot use reserved params as inputs", () => { class BadAction extends Action { constructor() { super(); this.name = "bad"; this.description = "bad"; this.outputExample = {}; this.inputs = { apiVersion: { required: true }, }; } async run() {} } const badAction = new BadAction(); expect(() => badAction.validate()).toThrow( "input `apiVersion` in action `bad` is a reserved param", ); }); test("the return types of actions can be imported", async () => { const { RandomNumber } = await import("../../src/actions/randomNumber"); type ResponseType = AsyncReturnType<typeof RandomNumber.prototype.run>; // now that we know the types, we can enforce that new objects match the type const responsePayload: ResponseType = { randomNumber: 1, stringRandomNumber: "some string", }; const responsePartial: ResponseType["randomNumber"] = 2; expect(responsePartial).toBe(2); }); }); describe("Action Params", () => { beforeAll(() => { api.actions.versions.testAction = [1]; api.actions.actions.testAction = { //@ts-ignore 1: { name: "testAction", description: "this action has some required params", version: 1, inputs: { requiredParam: { required: true }, optionalParam: { required: false }, fancyParam: { required: false, default: () => { return "abc123"; }, validator: function (s: unknown) { if (s !== "abc123") { return 'fancyParam should be "abc123". so says ' + this.id; } }, formatter: function (s: unknown) { return String(s); }, }, }, run: async (data) => { data.response!.params = data.params; }, }, }; }); afterAll(() => { delete api.actions.actions.testAction; delete api.actions.versions.testAction; config.general!.missingParamChecks = [null, "", undefined]; }); test("correct params that are falsey (false, []) should be allowed", async () => { let response; response = await specHelper.runAction("testAction", { requiredParam: false, }); expect(response.params.requiredParam).toEqual(false); response = await specHelper.runAction("testAction", { requiredParam: [], }); expect(response.params.requiredParam).toHaveLength(0); }); test("will fail for missing or empty string params", async () => { let response = await specHelper.runAction("testAction", { requiredParam: "", }); expect(response.error).toContain("required parameter for this action"); response = await specHelper.runAction("testAction", {}); expect(response.error).toMatch( /requiredParam is a required parameter for this action/, ); }); test("correct params respect config options", async () => { let response; config.general!.missingParamChecks = [undefined]; response = await specHelper.runAction("testAction", { requiredParam: "", }); expect(response.params.requiredParam).toEqual(""); response = await specHelper.runAction("testAction", { requiredParam: null, }); expect(response.params.requiredParam).toBeNull(); }); test("will set a default when params are not provided", async () => { const response = await specHelper.runAction<any>("testAction", { requiredParam: true, }); expect(response.params.fancyParam).toEqual("abc123"); }); test("will use validator if provided", async () => { const response = await specHelper.runAction("testAction", { requiredParam: true, fancyParam: 123, }); expect(response.error).toMatch(/Error: fancyParam should be "abc123"/); }); test("validator will have the API object in scope as this", async () => { const response = await specHelper.runAction("testAction", { requiredParam: true, fancyParam: 123, }); expect(response.error).toMatch(new RegExp(api.id)); }); test("will use formatter if provided (and still use validator)", async () => { const response = await specHelper.runAction("testAction", { requiredParam: true, fancyParam: 123, }); expect( response.requesterInformation!.receivedParams.fancyParam, ).toEqual("123"); }); test("succeeds a validator which returns no response", async () => { const response = await specHelper.runAction("testAction", { requiredParam: true, fancyParam: "abc123", }); expect(response.error).toBeUndefined(); }); test("will filter params not set in the target action or global safelist", async () => { const response = await specHelper.runAction("testAction", { requiredParam: true, sleepDuration: true, }); expect( response.requesterInformation!.receivedParams.requiredParam, ).toBeTruthy(); expect( response.requesterInformation!.receivedParams.sleepDuration, ).toBeUndefined(); }); }); describe("Action Params schema type", () => { beforeAll(() => { api.actions.versions.testAction = [1]; api.actions.actions.testAction = { //@ts-ignore 1: { name: "testAction", description: "this action has some required params", version: 1, inputs: { schemaParam: { schema: { requiredParam: { required: true }, optionalParam: { required: false }, fancyParam: { required: false, default: () => { return "abc123"; }, validator: function (s: unknown) { if (s === "abc123") { return true; } else { return ( 'fancyParam should be "abc123". so says ' + this.id ); } }, formatter: (s: unknown) => { return String(s); }, }, }, }, }, run: async (data) => { data.response!.params = data.params; }, }, }; }); afterAll(() => { delete api.actions.actions.testAction; delete api.actions.versions.testAction; config.general!.missingParamChecks = [null, "", undefined]; }); test("correct params that are falsey (false, []) should be allowed", async () => { let response; response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: false }, }); expect(response.params.schemaParam.requiredParam).toEqual(false); response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: [] }, }); expect(response.params.schemaParam.requiredParam).toHaveLength(0); }); test("will fail for missing or empty string params", async () => { let response; response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: "" }, }); expect(response.error).toContain( "schemaParam.requiredParam is a required parameter for this action", ); response = await specHelper.runAction("testAction", { schemaParam: {}, }); expect(response.error).toContain( "schemaParam.requiredParam is a required parameter for this action", ); }); test("correct params respect config options", async () => { let response: Record<string, any>; config.general!.missingParamChecks = [undefined]; response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: "" }, }); expect(response.params.schemaParam.requiredParam).toEqual(""); response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: null }, }); expect(response.params.schemaParam.requiredParam).toBeNull(); }); test("will set a default when params are not provided", async () => { const response = await specHelper.runAction<any>("testAction", { schemaParam: { requiredParam: true }, }); expect(response.params.schemaParam.fancyParam).toEqual("abc123"); }); test("will use validator if provided", async () => { const response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: true, fancyParam: 123 }, }); expect(response.error).toMatch(/Error: fancyParam should be "abc123"/); }); test("validator will have the API object in scope as this", async () => { const response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: true, fancyParam: 123 }, }); expect(response.error).toMatch(new RegExp(api.id)); }); test("will use formatter if provided (and still use validator)", async () => { const response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: true, fancyParam: 123 }, }); expect( response.requesterInformation!.receivedParams.schemaParam.fancyParam, ).toEqual("123"); }); test("will filter params not set in the target action or global safelist", async () => { const response = await specHelper.runAction("testAction", { schemaParam: { requiredParam: true, sleepDuration: true }, }); expect( response.requesterInformation!.receivedParams.schemaParam .requiredParam, ).toBeTruthy(); expect( response.requesterInformation!.receivedParams.schemaParam .sleepDuration, ).toBeUndefined(); }); }); describe("named action validations", () => { beforeAll(() => { api.validators = { validator1: (param: unknown) => { if (typeof param !== "string") { throw new Error("only strings"); } return true; }, validator2: (param: unknown) => { if (param !== "correct") { throw new Error("that is not correct"); } return true; }, }; api.actions.versions.testAction = [1]; api.actions.actions.testAction = { //@ts-ignore 1: { name: "testAction", description: "I am a test", inputs: { a: { validator: [ "api.validators.validator1", "api.validators.validator2", ], }, }, run: async () => {}, }, }; }); afterAll(() => { delete api.actions.versions.testAction; delete api.actions.actions.testAction; delete api.validators; }); test("runs validator arrays in the proper order", async () => { const response = await specHelper.runAction("testAction", { a: 6 }); expect(response.error).toEqual("Error: only strings"); }); test("runs more than 1 validator", async () => { const response = await specHelper.runAction("testAction", { a: "hello", }); expect(response.error).toEqual("Error: that is not correct"); }); test("succeeds multiple validators", async () => { const response = await specHelper.runAction("testAction", { a: "correct", }); expect(response.error).toBeUndefined(); }); }); describe("named action formatters", () => { beforeAll(() => { api._formatters = { formatter1: (param: unknown) => { return "*" + param + "*"; }, formatter2: (param: unknown) => { return "~" + param + "~"; }, }; api.actions.versions.testAction = [1]; api.actions.actions.testAction = { // @ts-ignore 1: { name: "testAction", description: "I am a test", inputs: { a: { formatter: [ "api._formatters.formatter1", "api._formatters.formatter2", ], }, }, run: async (data) => { data.response!.a = data.params!.a; }, }, }; }); afterAll(() => { delete api.actions.versions.testAction; delete api.actions.actions.testAction; delete api._formatters; }); test("runs formatter arrays in the proper order", async () => { const response = await specHelper.runAction<any>("testAction", { a: 6, }); expect(response.a).toEqual("~*6*~"); }); }); describe("immutability of data.params", () => { beforeAll(() => { api.actions.versions.testAction = [1]; api.actions.actions.testAction = { // @ts-ignore 1: { name: "testAction", description: "I am a test", inputs: { a: { required: true }, }, run: async ({ params, response }) => { params!.a = "changed!"; response!.a = params!.a; }, }, }; }); afterAll(() => { delete api.actions.actions.testAction; delete api.actions.versions.testAction; }); test("prevents data.params from being modified", async () => { const response = await specHelper.runAction<any>("testAction", { a: "original", }); expect(response.a).toBeUndefined(); expect(response.error).toMatch( /Cannot assign to read only property 'a' of object/, ); }); }); }); });