actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
1,032 lines (923 loc) • 35.7 kB
text/typescript
process.env.AUTOMATIC_ROUTES = "head,get,post,put,delete";
import axios, { AxiosError } from "axios";
import * as FormData from "form-data";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { api, Process, config, utils, route } from "./../../../src/index";
const actionhero = new Process();
let url: string;
describe("Server: Web", () => {
beforeAll(async () => {
await actionhero.start();
url = "http://localhost:" + config.web!.port;
});
afterAll(async () => await actionhero.stop());
test("should be up and return data", async () => {
await axios.get(url + "/api/randomNumber");
// should throw no errors
});
test("basic response should be JSON and have basic data", async () => {
const response = await axios.get(url + "/api/randomNumber");
expect(response).toBeInstanceOf(Object);
expect(response.data.requesterInformation).toBeInstanceOf(Object);
});
test("returns JSON with errors", async () => {
try {
await axios.get(url + "/api");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data.requesterInformation).toBeInstanceOf(
Object,
);
} else throw error;
}
});
test("params work", async () => {
try {
await axios.get(url + "/api?key=value");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(
error.response?.data.requesterInformation.receivedParams.key,
).toEqual("value");
} else throw error;
}
});
test("params are ignored unless they are in the whitelist", async () => {
try {
await axios.get(url + "/api?crazyParam123=something");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(
error.response?.data.requesterInformation.receivedParams
.crazyParam123,
).toBeUndefined();
} else throw error;
}
});
describe("will properly destroy connections", () => {
beforeAll(() => {
config.web!.returnErrorCodes = true;
api.actions.versions.customRender = [1];
api.actions.actions.customRender = {
// @ts-ignore
1: {
name: "customRender",
description: "I am a test",
version: 1,
outputExample: {},
run: async (data) => {
data.toRender = false;
data.connection!.rawConnection.res.writeHead(200, {
"Content-Type": "text/plain",
});
data.connection!.rawConnection.res.end(`${Math.random()}`);
},
},
};
api.routes.loadRoutes();
});
afterAll(() => {
delete api.actions.actions.customRender;
delete api.actions.versions.customRender;
});
test("works for the API", async () => {
expect(Object.keys(api.connections.connections)).toHaveLength(0);
axios.get(url + "/api/sleepTest"); // don't await
await utils.sleep(100);
expect(Object.keys(api.connections.connections)).toHaveLength(1);
await utils.sleep(1000);
expect(Object.keys(api.connections.connections)).toHaveLength(0);
});
test("works for files", async () => {
expect(Object.keys(api.connections.connections)).toHaveLength(0);
await axios.get(url + "/simple.html");
await utils.sleep(100);
expect(Object.keys(api.connections.connections)).toHaveLength(0);
});
test("works for actions with toRender: false", async () => {
expect(Object.keys(api.connections.connections)).toHaveLength(0);
const body = await axios.get(url + "/api/customRender");
expect(body).toBeTruthy();
await utils.sleep(200);
expect(Object.keys(api.connections.connections)).toHaveLength(0);
});
});
describe("errors", () => {
beforeAll(() => {
api.actions.versions.stringErrorTestAction = [1];
api.actions.actions.stringErrorTestAction = {
// @ts-ignore
1: {
name: "stringErrorTestAction",
description: "stringErrorTestAction",
version: 1,
run: async (data) => {
data.response!.error = "broken";
},
},
};
api.actions.versions.errorErrorTestAction = [1];
api.actions.actions.errorErrorTestAction = {
// @ts-ignore
1: {
name: "errorErrorTestAction",
description: "errorErrorTestAction",
version: 1,
run: async () => {
throw new Error("broken");
},
},
};
api.actions.versions.complexErrorTestAction = [1];
api.actions.actions.complexErrorTestAction = {
// @ts-ignore
1: {
name: "complexErrorTestAction",
description: "complexErrorTestAction",
version: 1,
run: async (data) => {
data.response!.error = { error: "broken", reason: "stuff" };
},
},
};
api.routes.loadRoutes();
});
afterAll(() => {
delete api.actions.actions.stringErrorTestAction;
delete api.actions.versions.stringErrorTestAction;
delete api.actions.actions.errorErrorTestAction;
delete api.actions.versions.errorErrorTestAction;
delete api.actions.actions.complexErrorTestAction;
delete api.actions.versions.complexErrorTestAction;
});
test("errors can be error strings", async () => {
try {
await axios.get(url + "/api/stringErrorTestAction");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(500);
expect(error.response?.data.error).toEqual("broken");
} else throw error;
}
});
test("errors can be error objects and returned plainly", async () => {
try {
await axios.get(url + "/api/errorErrorTestAction");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(500);
expect(error.response?.data.error).toEqual("broken");
} else throw error;
}
});
test("errors can be complex JSON payloads", async () => {
try {
await axios.get(url + "/api/complexErrorTestAction");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(500);
expect(error.response?.data.error).toEqual({
error: "broken",
reason: "stuff",
});
} else throw error;
}
});
});
describe("if disableParamScrubbing is set", () => {
let orig: boolean;
beforeAll(() => {
orig = config.general!.disableParamScrubbing as boolean;
config.general!.disableParamScrubbing = true;
});
afterAll(() => {
config.general!.disableParamScrubbing = orig;
});
test("params are not ignored", async () => {
try {
await axios.get(url + "/api/testAction/?crazyParam123=something");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(
error.response?.data.requesterInformation.receivedParams
.crazyParam123,
).toEqual("something");
} else throw error;
}
});
});
test("gibberish actions have the right response", async () => {
try {
await axios.get(url + "/api/IAMNOTANACTION");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data.error).toEqual(
"unknown action or invalid apiVersion",
);
} else throw error;
}
});
test("real actions do not have an error response", async () => {
const response = await axios.get(url + "/api/status");
expect(response.data.error).toBeUndefined();
});
test("HTTP Verbs should work: GET", async () => {
const response = await axios.get(url + "/api/randomNumber");
expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
expect(response.data.randomNumber).toBeLessThan(1);
});
test("HTTP Verbs should work: PUT", async () => {
const response = await axios.put(url + "/api/randomNumber");
expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
expect(response.data.randomNumber).toBeLessThan(1);
});
test("HTTP Verbs should work: POST", async () => {
const response = await axios.post(url + "/api/randomNumber");
expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
expect(response.data.randomNumber).toBeLessThan(1);
});
test("HTTP Verbs should work: DELETE", async () => {
const response = await axios.delete(url + "/api/randomNumber");
expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
expect(response.data.randomNumber).toBeLessThan(1);
});
test("HTTP Verbs should work: Post with Form", async () => {
try {
const formDataA = new FormData();
formDataA.append("key", "key");
await axios.post(url + "/api/cacheTest", formDataA);
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(422);
expect(error.response?.data.error).toEqual(
"value is a required parameter for this action",
);
} else throw error;
}
const formDataB = new FormData();
formDataB.append("key", "key");
formDataB.append("value", "value");
const successResponse = await axios.post(url + "/api/cacheTest", formDataB);
expect(successResponse.data.cacheTestResults.saveResp).toEqual(true);
});
test("HTTP Verbs should work: Post with JSON Payload as body", async () => {
try {
await axios.post(url + "/api/cacheTest", { key: "key" });
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(422);
expect(error.response?.data.error).toEqual(
"value is a required parameter for this action",
);
} else throw error;
}
const successResponse = await axios.post(url + "/api/cacheTest", {
key: "key",
value: "value",
});
expect(successResponse.data.cacheTestResults.saveResp).toEqual(true);
});
describe("messageId", () => {
test("generates unique messageIds for each request", async () => {
const responseA = await axios.get(url + "/api/randomNumber");
const responseB = await axios.get(url + "/api/randomNumber");
expect(responseA.data.requesterInformation.messageId).not.toEqual(
responseB.data.requesterInformation.messageId,
);
});
test("messageIds can be provided by the client and returned by the server", async () => {
const response = await axios.get(url + "/api/randomNumber?messageId=aaa");
expect(response.data.requesterInformation.messageId).not.toEqual("aaa");
});
test("a connection id should be a combination of fingerprint and message id", async () => {
const response = await axios.get(url + "/api/randomNumber");
expect(response.data.requesterInformation.id).toEqual(
`${response.data.requesterInformation.fingerprint}-${response.data.requesterInformation.messageId}`,
);
});
});
describe("connection.rawConnection.params", () => {
beforeAll(() => {
api.actions.versions.paramTestAction = [1];
api.actions.actions.paramTestAction = {
// @ts-ignore
1: {
name: "paramTestAction",
description: "I return connection.rawConnection.params",
version: 1,
run: async (data) => {
data.response = data.connection!.rawConnection.params;
if (data.connection!.rawConnection.params.rawBody) {
data.response!.rawBody =
data.connection!.rawConnection.params.rawBody.toString();
}
},
},
};
api.routes.loadRoutes();
});
afterAll(() => {
delete api.actions.actions.paramTestAction;
delete api.actions.versions.paramTestAction;
});
test(".query should contain unfiltered query params", async () => {
const response = await axios.get(
url + "/api/paramTestAction/?crazyParam123=something",
);
expect(response.data.query.crazyParam123).toEqual("something");
});
test(".body should contain unfiltered, parsed request body params", async () => {
const response = await axios.post(url + "/api/paramTestAction", {
key: "value",
});
expect(response.data.body.key).toEqual("value");
});
test(".rawBody can be disabled", async () => {
config.web!.saveRawBody = false;
const requestBody = '{"key": "value"}';
const response = await axios.post(
url + "/api/paramTestAction",
requestBody,
{ headers: { "Content-type": "application/json" } },
);
expect(response.data.body.key).toEqual("value");
expect(response.data.rawBody).toEqual("");
});
});
test("returnErrorCodes can be opted to change http header codes", async () => {
try {
await axios.delete(url + "/api/");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
} else throw error;
}
});
describe("http header", () => {
beforeAll(() => {
api.actions.versions.headerTestAction = [1];
api.actions.actions.headerTestAction = {
// @ts-ignore
1: {
name: "headerTestAction",
description: "I am a test",
version: 1,
outputExample: {},
run: async (data) => {
data.connection!.rawConnection.responseHeaders.push(["thing", "A"]);
data.connection!.rawConnection.responseHeaders.push(["thing", "B"]);
data.connection!.rawConnection.responseHeaders.push(["thing", "C"]);
data.connection!.rawConnection.responseHeaders.push([
"Set-Cookie",
"value_1=1",
]);
data.connection!.rawConnection.responseHeaders.push([
"Set-Cookie",
"value_2=2",
]);
},
},
};
api.routes.loadRoutes();
});
afterAll(() => {
delete api.actions.actions.headerTestAction;
delete api.actions.versions.headerTestAction;
});
test("duplicate headers should be removed (in favor of the last set)", async () => {
const response = await axios.get(url + "/api/headerTestAction");
expect(response.status).toEqual(200);
expect(response.headers.thing).toEqual("C");
});
test("but duplicate set-cookie requests should be allowed", async () => {
const response = await axios.get(url + "/api/headerTestAction");
expect(response.status).toEqual(200);
// this will convert node >= 10 header array to look like node <= 9 combined strings
const cookieString = (response.headers["set-cookie"] || [""]).join();
const parts = cookieString.split(",");
expect(parts[1]).toEqual("value_1=1");
expect(parts[0]).toEqual("value_2=2");
});
test("should respond to OPTIONS with only HTTP headers", async () => {
const response = await axios.options(url + "/api/cacheTest");
expect(response.status).toEqual(200);
expect(response.headers["access-control-allow-methods"]).toEqual(
"HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE",
);
expect(response.headers["access-control-allow-origin"]).toEqual("*");
expect(response.headers["content-length"]).toEqual("0");
expect(response.data).toEqual("");
});
test("should respond to TRACE with parsed params received", async () => {
const response = await axios({
method: "trace",
url: url + "/api/x",
data: { key: "someKey", value: "someValue" },
});
expect(response.status).toEqual(200);
expect(response.data.receivedParams.key).toEqual("someKey");
expect(response.data.receivedParams.value).toEqual("someValue");
});
test("should respond to HEAD requests just like GET, but with no body", async () => {
const response = await axios.head(url + "/api/headerTestAction");
expect(response.status).toEqual(200);
expect(response.data).toEqual("");
});
test("keeps sessions with browser_fingerprint", async () => {
const jar = new CookieJar();
const client = wrapper(axios.create({ jar }));
const response1 = await client.post(url + "/api/randomNumber");
const response2 = await client.get(url + "/api/randomNumber");
const response3 = await client.put(url + "/api/randomNumber");
const response4 = await client.delete(url + "/api/randomNumber");
const response5 = await axios.delete(url + "/api/randomNumber");
expect(response1.headers["set-cookie"]).toBeTruthy();
expect(response2.headers["set-cookie"]).toBeUndefined();
expect(response3.headers["set-cookie"]).toBeUndefined();
expect(response4.headers["set-cookie"]).toBeUndefined();
expect(response5.headers["set-cookie"]).toBeTruthy();
const fingerprint1 = response1.data.requesterInformation.id.split("-")[0];
const fingerprint2 = response2.data.requesterInformation.id.split("-")[0];
const fingerprint3 = response3.data.requesterInformation.id.split("-")[0];
const fingerprint4 = response4.data.requesterInformation.id.split("-")[0];
const fingerprint5 = response5.data.requesterInformation.id.split("-")[0];
expect(fingerprint1).toEqual(fingerprint2);
expect(fingerprint1).toEqual(fingerprint3);
expect(fingerprint1).toEqual(fingerprint4);
expect(fingerprint1).not.toEqual(fingerprint5);
expect(fingerprint1).toEqual(
response1.data.requesterInformation.fingerprint,
);
expect(fingerprint2).toEqual(
response2.data.requesterInformation.fingerprint,
);
expect(fingerprint3).toEqual(
response3.data.requesterInformation.fingerprint,
);
expect(fingerprint4).toEqual(
response4.data.requesterInformation.fingerprint,
);
expect(fingerprint5).toEqual(
response5.data.requesterInformation.fingerprint,
);
});
});
describe("http returnErrorCodes true", () => {
class ErrorWithCode extends Error {
code: number;
}
beforeAll(() => {
api.actions.versions.statusTestAction = [1];
api.actions.actions.statusTestAction = {
// @ts-ignore
1: {
name: "statusTestAction",
description: "I am a test",
inputs: {
key: { required: true },
query: { required: false },
randomKey: { required: false },
},
run: async (data) => {
if (data.params!.key !== "value") {
data.connection!.rawConnection.responseHttpCode = 402;
throw new ErrorWithCode("key != value");
}
const hasQueryParam = !!data.params!.query;
if (hasQueryParam) {
const validQueryFilters = ["test", "search"];
const validQueryParam =
validQueryFilters.indexOf(data.params!.query) > -1;
if (!validQueryParam) {
const notFoundError = new ErrorWithCode(
`404: Filter '${data.params!.query}' not found `,
);
notFoundError.code = 404;
throw notFoundError;
}
}
const hasRandomKey = !!data.params!.randomKey;
if (hasRandomKey) {
const validRandomKeys = ["key1", "key2", "key3"];
const validRandomKey =
validRandomKeys.indexOf(data.params!.randomKey) > -1;
if (!validRandomKey) {
if (data.params!.randomKey === "expired-key") {
const expiredError = new ErrorWithCode(
`999: Key '${data.params!.randomKey}' is expired`,
);
expiredError.code = 999;
throw expiredError;
}
const suspiciousError = new ErrorWithCode(
`402: Suspicious Activity detected with key ${
data.params!.randomKey
}`,
);
suspiciousError.code = 402;
throw suspiciousError;
}
}
data.response!.good = true;
},
},
};
api.routes.loadRoutes();
});
afterAll(() => {
delete api.actions.versions.statusTestAction;
delete api.actions.actions.statusTestAction;
});
test("actions that do not exists should return 404", async () => {
try {
await axios.post(url + "/api/aFakeAction");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
} else throw error;
}
});
test("missing params result in a 422", async () => {
try {
await axios.post(url + "/api/statusTestAction");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(422);
} else throw error;
}
});
test("status codes can be set for errors", async () => {
try {
await axios.post(url + "/api/statusTestAction", { key: "bannana" });
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(402);
expect(error.response?.data.error).toEqual("key != value");
} else throw error;
}
});
test("status code should still be 200 if everything is OK", async () => {
const response = await axios.post(url + "/api/statusTestAction", {
key: "value",
});
expect(response.status).toEqual(200);
expect(response.data.good).toEqual(true);
});
describe("setting status code using custom errors", () => {
test("should work for 404 status code, set using custom error for invalid params", async () => {
try {
await axios.post(url + "/api/statusTestAction", {
key: "value",
query: "guess",
});
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data.error).toEqual(
"404: Filter 'guess' not found ",
);
} else throw error;
}
});
test("should work for 402 status code set using custom error for invalid params", async () => {
try {
await axios.post(url + "/api/statusTestAction", {
key: "value",
randomKey: "guessKey",
});
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(402);
expect(error.response?.data.error).toEqual(
"402: Suspicious Activity detected with key guessKey",
);
} else throw error;
}
});
test("should not throw custom error for valid params", async () => {
const responseWithQuery = await axios.post(
url + "/api/statusTestAction",
{ key: "value", query: "test" },
);
expect(responseWithQuery.status).toEqual(200);
expect(responseWithQuery.data.good).toEqual(true);
const responseWithRandomKey = await axios.post(
url + "/api/statusTestAction",
{ key: "value", randomKey: "key1" },
);
expect(responseWithRandomKey.status).toEqual(200);
expect(responseWithRandomKey.data.good).toEqual(true);
const responseWithKeyAndQuery = await axios.post(
url + "/api/statusTestAction",
{
key: "value",
query: "search",
randomKey: "key2",
},
);
expect(responseWithKeyAndQuery.status).toEqual(200);
expect(responseWithKeyAndQuery.data.good).toEqual(true);
});
test("should not work for 999 status code set using custom error and default error code, 400 is thrown", async () => {
try {
await axios.post(url + "/api/statusTestAction", {
key: "value",
randomKey: "expired-key",
});
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).not.toEqual(999);
expect(error.response?.status).toEqual(500);
expect(error.response?.data.error).toEqual(
"999: Key 'expired-key' is expired",
);
} else throw error;
}
});
});
});
describe("documentation", () => {
test("documentation can be returned via a swagger action", async () => {
const response = await axios.get(url + "/api/swagger");
expect(response.data.paths).toBeInstanceOf(Object);
});
});
describe("files", () => {
test("an HTML file", async () => {
const response = await axios.get(url + "/public/simple.html");
expect(response.status).toEqual(200);
expect(response.data).toContain("<h1>Actionhero</h1>");
});
test("404 pages", async () => {
try {
await axios.get(url + "/public/notARealFile");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
} else throw error;
}
});
test("404 pages from POST with if-modified-since header", async () => {
const file = Math.random().toString(36);
try {
await axios.get(url + "/" + file, {
headers: { "if-modified-since": "Thu, 19 Apr 2012 09:51:20 GMT" },
});
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data).toEqual("that file is not found");
} else throw error;
}
});
test("should not see files outside of the public dir", async () => {
try {
await axios.get(url + "/public/../config.json");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data).toEqual("that file is not found");
} else throw error;
}
});
test("index page should be served when requesting a path (trailing slash)", async () => {
const response = await axios.get(url + "/public/");
expect(response.status).toEqual(200);
expect(response.data).toMatch(
/Actionhero is a multi-transport API Server/,
);
});
test("index page should be served when requesting a path (no trailing slash)", async () => {
const response = await axios.get(url + "/public");
expect(response.status).toEqual(200);
expect(response.data).toMatch(
/Actionhero is a multi-transport API Server/,
);
});
describe("can serve files from a specific mapped route", () => {
beforeAll(() => {
const testFolderPublicPath = path.join(
__dirname,
"/../../../public/testFolder",
);
fs.mkdirSync(testFolderPublicPath);
fs.writeFileSync(
testFolderPublicPath + "/testFile.html",
"Actionhero Route Test File",
);
route.registerRoute(
"get",
"/my/public/route",
// @ts-ignore
null,
null,
true,
testFolderPublicPath,
);
});
afterAll(() => {
const testFolderPublicPath = path.join(
__dirname,
"/../../../public/testFolder",
);
fs.unlinkSync(testFolderPublicPath + path.sep + "testFile.html");
fs.rmdirSync(testFolderPublicPath);
});
test("works for routes mapped paths", async () => {
const response = await axios.get(
url + "/my/public/route/testFile.html",
);
expect(response.status).toEqual(200);
expect(response.data).toEqual("Actionhero Route Test File");
});
test("returns 404 for files not available in route mapped paths", async () => {
try {
await axios.get(url + "/my/public/route/fileNotFound.html");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data).toEqual("that file is not found");
} else throw error;
}
});
test("should not see files outside of the mapped dir", async () => {
try {
await axios.get(url + "/my/public/route/../../config/servers/web.js");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
expect(error.response?.data).toEqual("that file is not found");
} else throw error;
}
});
});
describe("can serve files from more than one directory", () => {
const source = path.join(__dirname, "/../../../public/simple.html");
beforeAll(() => {
fs.createReadStream(source).pipe(
fs.createWriteStream(os.tmpdir() + path.sep + "tmpTestFile.html"),
);
api.staticFile.searchLocations.push(os.tmpdir());
});
afterAll(() => {
fs.unlinkSync(os.tmpdir() + path.sep + "tmpTestFile.html");
api.staticFile.searchLocations.pop();
});
test("works for secondary paths", async () => {
const response = await axios.get(url + "/public/tmpTestFile.html");
expect(response.status).toEqual(200);
expect(response.data).toContain("<h1>Actionhero</h1>");
});
});
});
describe("custom methods", () => {
let originalRoutes: typeof api.routes.routes;
beforeAll(() => {
originalRoutes = api.routes.routes;
api.actions.versions.proxyHeaders = [1];
api.actions.actions.proxyHeaders = {
// @ts-ignore
1: {
name: "proxyHeaders",
description: "proxy header test",
inputs: {},
outputExample: {},
run: async (data) => {
data.connection!.setHeader!("X-Foo", "bar");
},
},
};
api.actions.versions.proxyStatusCode = [1];
api.actions.actions.proxyStatusCode = {
// @ts-ignore
1: {
name: "proxyStatusCode",
description: "proxy status code test",
inputs: {
code: {
required: true,
default: 200,
formatter: (p: string) => {
return parseInt(p);
},
},
},
outputExample: {},
run: async (data) => {
data.connection!.setStatusCode!(data.params!.code);
},
},
};
api.actions.versions.pipe = [1];
api.actions.actions.pipe = {
// @ts-ignore
1: {
name: "pipe",
description: "pipe response test",
inputs: {
mode: { required: true },
},
outputExample: {},
run: async (data) => {
data.toRender = false;
if (data.params!.mode === "string") {
data.connection!.pipe!("a string", { "custom-header": "cool" });
} else if (data.params!.mode === "buffer") {
data.connection!.pipe!(Buffer.from("a buffer"), {
"custom-header": "still-cool",
});
} else if (data.params!.mode === "contentType") {
data.connection!.pipe!("just some good, old-fashioned words", {
"Content-Type": "text/plain",
"custom-header": "words",
});
} else {
throw new Error("I Do not know this mode");
}
},
},
};
api.routes.loadRoutes({
get: [
{ path: "/proxy", action: "proxyHeaders", apiVersion: 1 },
{ path: "/code", action: "proxyStatusCode", apiVersion: 1 },
{ path: "/pipe", action: "pipe", apiVersion: 1 },
],
});
});
afterAll(() => {
api.routes.routes = originalRoutes;
delete api.actions.versions.proxyHeaders;
delete api.actions.versions.proxyStatusCode;
delete api.actions.versions.pipe;
delete api.actions.actions.proxyHeaders;
delete api.actions.actions.proxyStatusCode;
delete api.actions.actions.pipe;
});
test("actions handled by the web server support proxy for setHeaders", async () => {
const response = await axios.get(url + "/api/proxy");
expect(response.headers["x-foo"]).toEqual("bar");
});
test("actions handled by the web server support proxy for setting status code", async () => {
const responseDefault = await axios.get(url + "/api/proxyStatusCode", {});
expect(responseDefault.status).toEqual(200);
try {
await axios.get(url + "/api/proxyStatusCode?code=404");
throw new Error("should not get here");
} catch (error) {
if (error instanceof AxiosError) {
expect(error.response?.status).toEqual(404);
} else throw error;
}
});
test("can pipe string responses with custom headers to clients", async () => {
const response = await axios.get(url + "/api/pipe?mode=string");
expect(response.headers["custom-header"]).toEqual("cool");
expect(response.headers["content-length"]).toEqual("8");
expect(response.data).toEqual("a string");
});
test("can pipe buffer responses with custom headers to clients", async () => {
const response = await axios.get(url + "/api/pipe?mode=buffer");
expect(response.headers["custom-header"]).toEqual("still-cool");
expect(response.headers["content-length"]).toEqual("8");
expect(response.data).toEqual("a buffer");
});
test("can pipe buffer responses with custom content types to clients", async () => {
const { headers, data } = await axios.get(
url + "/api/pipe?mode=contentType",
);
expect(headers["content-type"]).toEqual("text/plain");
expect(headers["content-length"]).toEqual("35");
expect(headers["custom-header"]).toEqual("words");
expect(data).toEqual("just some good, old-fashioned words");
});
});
});