UNPKG

actionhero

Version:

actionhero.js is a multi-transport API Server with integrated cluster capabilities and delayed tasks

1,067 lines (960 loc) 36.4 kB
import * as request from "request-promise-native"; 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; const toJson = async (string) => { try { return JSON.parse(string); } catch (error) { return error; } }; describe("Server: Web", () => { beforeAll(async () => { await actionhero.start(); url = "http://localhost:" + config.servers.web.port; }); afterAll(async () => { await actionhero.stop(); }); test("should be up and return data", async () => { await request.get(url + "/api/randomNumber").then(toJson); // should throw no errors }); test("basic response should be JSON and have basic data", async () => { const body = await request.get(url + "/api/randomNumber").then(toJson); expect(body).toBeInstanceOf(Object); expect(body.requesterInformation).toBeInstanceOf(Object); }); test("returns JSON with errors", async () => { try { await request.get(url + "/api").then(toJson); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); const body = await toJson(error.response.body); expect(body.requesterInformation).toBeInstanceOf(Object); } }); test("params work", async () => { try { await request.get(url + "/api?key=value").then(toJson); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); const body = await toJson(error.response.body); expect(body.requesterInformation.receivedParams.key).toEqual("value"); } }); test("params are ignored unless they are in the whitelist", async () => { try { await request.get(url + "/api?crazyParam123=something").then(toJson); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); const body = await toJson(error.response.body); expect( body.requesterInformation.receivedParams.crazyParam123 ).toBeUndefined(); } }); describe("will properly destroy connections", () => { beforeAll(() => { config.servers.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); request.get(url + "/api/sleepTest").then(toJson); // 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 request.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 request.get(url + "/api/customRender").then(toJson); expect(body).toBeTruthy(); await utils.sleep(100); 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 request.get(url + "/api/stringErrorTestAction"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(400); const body = await toJson(error.response.body); expect(body.error).toEqual("broken"); } }); test("errors can be error objects and returned plainly", async () => { try { await request.get(url + "/api/errorErrorTestAction"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(400); const body = await toJson(error.response.body); expect(body.error).toEqual("broken"); } }); test("errors can be complex JSON payloads", async () => { try { await request.get(url + "/api/complexErrorTestAction"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(400); const body = await toJson(error.response.body); expect(body.error).toEqual({ error: "broken", reason: "stuff" }); } }); }); describe("if disableParamScrubbing is set", () => { let orig; beforeAll(() => { orig = config.general.disableParamScrubbing; config.general.disableParamScrubbing = true; }); afterAll(() => { config.general.disableParamScrubbing = orig; }); test("params are not ignored", async () => { try { await request.get(url + "/api/testAction/?crazyParam123=something"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); const body = await toJson(error.response.body); expect(body.requesterInformation.receivedParams.crazyParam123).toEqual( "something" ); } }); }); test("gibberish actions have the right response", async () => { try { await request.get(url + "/api/IAMNOTANACTION"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); const body = await toJson(error.response.body); expect(body.error).toEqual("unknown action or invalid apiVersion"); } }); test("real actions do not have an error response", async () => { const body = await request.get(url + "/api/status").then(toJson); expect(body.error).toBeUndefined(); }); test("HTTP Verbs should work: GET", async () => { const body = await request.get(url + "/api/randomNumber").then(toJson); expect(body.randomNumber).toBeGreaterThanOrEqual(0); expect(body.randomNumber).toBeLessThan(1); }); test("HTTP Verbs should work: PUT", async () => { const body = await request.put(url + "/api/randomNumber").then(toJson); expect(body.randomNumber).toBeGreaterThanOrEqual(0); expect(body.randomNumber).toBeLessThan(1); }); test("HTTP Verbs should work: POST", async () => { const body = await request.post(url + "/api/randomNumber").then(toJson); expect(body.randomNumber).toBeGreaterThanOrEqual(0); expect(body.randomNumber).toBeLessThan(100); }); test("HTTP Verbs should work: DELETE", async () => { const body = await request.delete(url + "/api/randomNumber").then(toJson); expect(body.randomNumber).toBeGreaterThanOrEqual(0); expect(body.randomNumber).toBeLessThan(1000); }); test("HTTP Verbs should work: Post with Form", async () => { try { await request.post(url + "/api/cacheTest", { form: { key: "key" } }); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(422); expect(error.message).toMatch( /value is a required parameter for this action/ ); } const successBody = await request .post(url + "/api/cacheTest", { form: { key: "key", value: "value" } }) .then(toJson); expect(successBody.cacheTestResults.saveResp).toEqual(true); }); test("HTTP Verbs should work: Post with JSON Payload as body", async () => { let bodyPayload = JSON.stringify({ key: "key" }); try { await request.post(url + "/api/cacheTest", { body: bodyPayload, headers: { "Content-type": "application/json" }, }); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(422); expect(error.message).toMatch( /value is a required parameter for this action/ ); } bodyPayload = JSON.stringify({ key: "key", value: "value" }); const successBody = await request .post(url + "/api/cacheTest", { body: bodyPayload, headers: { "Content-type": "application/json" }, }) .then(toJson); expect(successBody.cacheTestResults.saveResp).toEqual(true); }); describe("messageId", () => { test("generates unique messageIds for each request", async () => { const requestA = await request .get(url + "/api/randomNumber") .then(toJson); const requestB = await request .get(url + "/api/randomNumber") .then(toJson); expect(requestA.requesterInformation.messageId).not.toEqual( requestB.requesterInformation.messageId ); }); test("messageIds can be provided by the client and returned by the server", async () => { const response = await request .get(url + "/api/randomNumber", { messageId: "aaa" }) .then(toJson); expect(response.requesterInformation.messageId).not.toEqual("aaa"); }); test("a connection id should be a combination of fingerprint and message id", async () => { const response = await request .get(url + "/api/randomNumber") .then(toJson); expect(response.requesterInformation.id).toEqual( `${response.requesterInformation.fingerprint}-${response.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 body = await request .get(url + "/api/paramTestAction/?crazyParam123=something") .then(toJson); expect(body.query.crazyParam123).toEqual("something"); }); test(".body should contain unfiltered, parsed request body params", async () => { const requestBody = JSON.stringify({ key: "value" }); const body = await request .post(url + "/api/paramTestAction", { body: requestBody, headers: { "Content-type": "application/json" }, }) .then(toJson); expect(body.body.key).toEqual("value"); }); test(".rawBody can be disabled", async () => { config.servers.web.saveRawBody = false; const requestBody = '{"key": "value"}'; const body = await request .post(url + "/api/paramTestAction", { body: requestBody, headers: { "Content-type": "application/json" }, }) .then(toJson); expect(body.body.key).toEqual("value"); expect(body.rawBody).toEqual(""); }); }); test("returnErrorCodes can be opted to change http header codes", async () => { try { await request.del(url + "/api/"); } catch (error) { expect(error.statusCode).toEqual(404); } }); 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 request.get(url + "/api/headerTestAction", { resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); expect(response.headers.thing).toEqual("C"); }); test("but duplicate set-cookie requests should be allowed", async () => { const response = await request.get(url + "/api/headerTestAction", { resolveWithFullResponse: true, }); expect(response.statusCode).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 request({ method: "options", url: url + "/api/cacheTest", resolveWithFullResponse: true, }); expect(response.statusCode).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.body).toEqual(""); }); test("should respond to TRACE with parsed params received", async () => { const response = await request({ method: "trace", url: url + "/api/x", form: { key: "someKey", value: "someValue" }, resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); const body = await toJson(response.body); expect(body.receivedParams.key).toEqual("someKey"); expect(body.receivedParams.value).toEqual("someValue"); }); test("should respond to HEAD requests just like GET, but with no body", async () => { const response = await request({ method: "head", url: url + "/api/headerTestAction", resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); expect(response.body).toEqual(""); }); test("keeps sessions with browser_fingerprint", async () => { const j = request.jar(); const response1 = await request.post({ url: url + "/api/randomNumber", jar: j, resolveWithFullResponse: true, }); const response2 = await request.get({ url: url + "/api/randomNumber", jar: j, resolveWithFullResponse: true, }); const response3 = await request.put({ url: url + "/api/randomNumber", jar: j, resolveWithFullResponse: true, }); const response4 = await request.del({ url: url + "/api/randomNumber", jar: j, resolveWithFullResponse: true, }); expect(response1.headers["set-cookie"]).toBeTruthy(); expect(response2.headers["set-cookie"]).toBeUndefined(); expect(response3.headers["set-cookie"]).toBeUndefined(); expect(response4.headers["set-cookie"]).toBeUndefined(); const body1 = await toJson(response1.body); const body2 = await toJson(response2.body); const body3 = await toJson(response3.body); const body4 = await toJson(response4.body); const fingerprint1 = body1.requesterInformation.id.split("-")[0]; const fingerprint2 = body2.requesterInformation.id.split("-")[0]; const fingerprint3 = body3.requesterInformation.id.split("-")[0]; const fingerprint4 = body4.requesterInformation.id.split("-")[0]; expect(fingerprint1).toEqual(fingerprint2); expect(fingerprint1).toEqual(fingerprint3); expect(fingerprint1).toEqual(fingerprint4); expect(fingerprint1).toEqual(body1.requesterInformation.fingerprint); expect(fingerprint2).toEqual(body2.requesterInformation.fingerprint); expect(fingerprint3).toEqual(body3.requesterInformation.fingerprint); expect(fingerprint4).toEqual(body4.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 request.post(url + "/api/aFakeAction"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); } }); test("missing params result in a 422", async () => { try { await request.post(url + "/api/statusTestAction"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(422); } }); test("status codes can be set for errors", async () => { try { await request.post(url + "/api/statusTestAction", { form: { key: "bannana" }, }); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(402); const body = await toJson(error.response.body); expect(body.error).toEqual("key != value"); } }); test("status code should still be 200 if everything is OK", async () => { const response = await request.post(url + "/api/statusTestAction", { form: { key: "value" }, resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); const body = await toJson(response.body); expect(body.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 request.post(url + "/api/statusTestAction", { form: { key: "value", query: "guess" }, }); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); const body = await toJson(error.response.body); expect(body.error).toEqual("404: Filter 'guess' not found "); } }); test("should work for 402 status code set using custom error for invalid params", async () => { try { await request.post(url + "/api/statusTestAction", { form: { key: "value", randomKey: "guessKey" }, }); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(402); const body = await toJson(error.response.body); expect(body.error).toEqual( "402: Suspicious Activity detected with key guessKey" ); } }); test("should not throw custom error for valid params", async () => { const responseWithQuery = await request.post( url + "/api/statusTestAction", { form: { key: "value", query: "test" }, resolveWithFullResponse: true, } ); expect(responseWithQuery.statusCode).toEqual(200); const responseBody = await toJson(responseWithQuery.body); expect(responseBody.good).toEqual(true); const responseWithRandomKey = await request.post( url + "/api/statusTestAction", { form: { key: "value", randomKey: "key1" }, resolveWithFullResponse: true, } ); expect(responseWithRandomKey.statusCode).toEqual(200); const body = await toJson(responseWithRandomKey.body); expect(body.good).toEqual(true); const responseWithKeyAndQuery = await request.post( url + "/api/statusTestAction", { form: { key: "value", query: "search", randomKey: "key2" }, resolveWithFullResponse: true, } ); expect(responseWithKeyAndQuery.statusCode).toEqual(200); const receivedBody = await toJson(responseWithKeyAndQuery.body); expect(receivedBody.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 request.post(url + "/api/statusTestAction", { form: { key: "value", randomKey: "expired-key" }, }); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).not.toEqual(999); expect(error.statusCode).toEqual(400); const body = await toJson(error.response.body); expect(body.error).toEqual("999: Key 'expired-key' is expired"); } }); }); }); describe("documentation", () => { test("documentation can be returned via a documentation action", async () => { const body = await request .get(url + "/api/showDocumentation") .then(toJson); expect(body.documentation).toBeInstanceOf(Object); }); test("should have actions with all the right parts", async () => { const body = await request .get(url + "/api/showDocumentation") .then(toJson); for (const actionName in body.documentation) { for (const version in body.documentation[actionName]) { const action = body.documentation[actionName][version]; expect(typeof action.name).toEqual("string"); expect(typeof action.description).toEqual("string"); expect(action.inputs).toBeInstanceOf(Object); } } }); }); describe("files", () => { test("an HTML file", async () => { const response = await request.get(url + "/public/simple.html", { resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); expect(response.body).toContain("<h1>Actionhero</h1>"); }); test("404 pages", async () => { try { await request.get(url + "/public/notARealFile"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); } }); test("404 pages from POST with if-modified-since header", async () => { const file = Math.random().toString(36); const options = { url: url + "/" + file, headers: { "if-modified-since": "Thu, 19 Apr 2012 09:51:20 GMT", }, }; try { await request.get(options); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.response.body).toEqual("That file is not found"); } }); test("should not see files outside of the public dir", async () => { try { await request.get(url + "/public/../config.json"); throw new Error("should not get here"); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.response.body).toEqual("That file is not found"); } }); test("index page should be served when requesting a path (trailing slash)", async () => { const response = await request.get(url + "/public/", { resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); expect(response.body).toMatch( /Actionhero.js is a multi-transport API Server/ ); }); test("index page should be served when requesting a path (no trailing slash)", async () => { const response = await request.get(url + "/public", { resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); expect(response.body).toMatch( /Actionhero.js 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", 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 request.get( url + "/my/public/route/testFile.html", { resolveWithFullResponse: true } ); expect(response.statusCode).toEqual(200); expect(response.body).toEqual("Actionhero Route Test File"); }); test("returns 404 for files not available in route mapped paths", async () => { try { await request.get(url + "/my/public/route/fileNotFound.html"); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.response.body).toEqual("That file is not found"); } }); test("should not see files outside of the mapped dir", async () => { try { await request.get( url + "/my/public/route/../../config/servers/web.js" ); } catch (error) { expect(error.statusCode).toEqual(404); expect(error.response.body).toEqual("That file is not found"); } }); }); 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 request.get(url + "/public/tmpTestFile.html", { resolveWithFullResponse: true, }); expect(response.statusCode).toEqual(200); expect(response.body).toContain("<h1>Actionhero</h1>"); }); }); }); describe("custom methods", () => { let originalRoutes; 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) => { 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 request.get(url + "/api/proxy", { resolveWithFullResponse: true, }); expect(response.headers["x-foo"]).toEqual("bar"); }); test("actions handled by the web server support proxy for setting status code", async () => { const responseDefault = await request.get(url + "/api/proxyStatusCode", { resolveWithFullResponse: true, }); expect(responseDefault.statusCode).toEqual(200); try { await request.get(url + "/api/proxyStatusCode?code=404", { resolveWithFullResponse: true, }); throw new Error("should not get here"); } catch (error) { expect(error.toString()).toMatch(/StatusCodeError: 404/); } }); test("can pipe string responses with custom headers to clients", async () => { const response = await request.get(url + "/api/pipe?mode=string", { resolveWithFullResponse: true, }); expect(response.headers["custom-header"]).toEqual("cool"); expect(response.headers["content-length"]).toEqual("8"); expect(response.body).toEqual("a string"); }); test("can pipe buffer responses with custom headers to clients", async () => { const response = await request.get(url + "/api/pipe?mode=buffer", { resolveWithFullResponse: true, }); expect(response.headers["custom-header"]).toEqual("still-cool"); expect(response.headers["content-length"]).toEqual("8"); expect(response.body).toEqual("a buffer"); }); test("can pipe buffer responses with custom content types to clients", async () => { const { headers, body } = await request.get( url + "/api/pipe?mode=contentType", { resolveWithFullResponse: true } ); expect(headers["content-type"]).toEqual("text/plain"); expect(headers["content-length"]).toEqual("35"); expect(headers["custom-header"]).toEqual("words"); expect(body).toEqual("just some good, old-fashioned words"); }); }); });