0z2i6v3u5t 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/.devcontainer/devcontainer.json +4 -0
  2. package/.devcontainer/setup.sh +11 -0
  3. package/.dockerignore +2 -0
  4. package/.github/CONTRIBUTING.md +52 -0
  5. package/.github/FUNDING.yml +3 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +59 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +43 -0
  9. package/.github/dependabot.yml +17 -0
  10. package/.github/workflows/codeql.yml +76 -0
  11. package/.github/workflows/publish_docs.yml +25 -0
  12. package/.github/workflows/test.yml +78 -0
  13. package/.nvmrc +1 -0
  14. package/.prettierignore +1 -0
  15. package/.prettierrc +1 -0
  16. package/.vscode/launch.json +42 -0
  17. package/CODE_OF_CONDUCT.md +76 -0
  18. package/Dockerfile +17 -0
  19. package/LICENSE +21 -0
  20. package/README.md +3 -0
  21. package/SECURITY.md +5 -0
  22. package/__tests__/actions/cacheTest.ts +58 -0
  23. package/__tests__/actions/randomNumber.ts +26 -0
  24. package/__tests__/actions/recursiveAction.ts +16 -0
  25. package/__tests__/actions/sleepTest.ts +24 -0
  26. package/__tests__/actions/status.ts +17 -0
  27. package/__tests__/actions/swagger.ts +76 -0
  28. package/__tests__/actions/validationTest.ts +63 -0
  29. package/__tests__/cli/cli.ts +126 -0
  30. package/__tests__/core/api.ts +632 -0
  31. package/__tests__/core/cache.ts +400 -0
  32. package/__tests__/core/chatRoom.ts +589 -0
  33. package/__tests__/core/cli.ts +349 -0
  34. package/__tests__/core/cluster.ts +132 -0
  35. package/__tests__/core/config.ts +78 -0
  36. package/__tests__/core/errors.ts +112 -0
  37. package/__tests__/core/log.ts +23 -0
  38. package/__tests__/core/middleware.ts +427 -0
  39. package/__tests__/core/plugins/partialPlugin.ts +94 -0
  40. package/__tests__/core/plugins/withPlugin.ts +88 -0
  41. package/__tests__/core/plugins/withoutPlugin.ts +81 -0
  42. package/__tests__/core/process.ts +42 -0
  43. package/__tests__/core/specHelper.ts +330 -0
  44. package/__tests__/core/staticFile/compression.ts +99 -0
  45. package/__tests__/core/staticFile/staticFile.ts +180 -0
  46. package/__tests__/core/tasks/customQueueFunction.ts +67 -0
  47. package/__tests__/core/tasks/fullWorkerFlow.ts +199 -0
  48. package/__tests__/core/tasks/tasks.ts +605 -0
  49. package/__tests__/integration/browser.ts +133 -0
  50. package/__tests__/integration/ioredis-mock.ts +194 -0
  51. package/__tests__/integration/sendBuffer.ts +97 -0
  52. package/__tests__/integration/sendFile.ts +24 -0
  53. package/__tests__/integration/sharedFingerprint.ts +82 -0
  54. package/__tests__/integration/taskFlow.ts +110 -0
  55. package/__tests__/jest.ts +5 -0
  56. package/__tests__/modules/action.ts +103 -0
  57. package/__tests__/modules/config.ts +19 -0
  58. package/__tests__/modules/utils/ensureNoTsHeaderOrSpecFiles.ts +24 -0
  59. package/__tests__/servers/web/allowedRequestHosts.ts +88 -0
  60. package/__tests__/servers/web/enableMultiples.ts +83 -0
  61. package/__tests__/servers/web/fileUpload.ts +79 -0
  62. package/__tests__/servers/web/jsonp.ts +57 -0
  63. package/__tests__/servers/web/nonMultiples.ts +83 -0
  64. package/__tests__/servers/web/rawBody.ts +208 -0
  65. package/__tests__/servers/web/returnErrorCodes.ts +55 -0
  66. package/__tests__/servers/web/routes/deepRoutes.ts +96 -0
  67. package/__tests__/servers/web/routes/routes.ts +579 -0
  68. package/__tests__/servers/web/routes/veryDeepRoutes.ts +92 -0
  69. package/__tests__/servers/web/web.ts +1031 -0
  70. package/__tests__/servers/websocket.ts +795 -0
  71. package/__tests__/tasks/runAction.ts +37 -0
  72. package/__tests__/template.ts.example +20 -0
  73. package/__tests__/testCliCommands/hello.ts +44 -0
  74. package/__tests__/testPlugin/public/plugin.html +1 -0
  75. package/__tests__/testPlugin/src/actions/pluginAction.ts +14 -0
  76. package/__tests__/testPlugin/src/bin/hello.ts +22 -0
  77. package/__tests__/testPlugin/src/initializers/pluginInitializer.ts +17 -0
  78. package/__tests__/testPlugin/src/tasks/pluginTask.ts +15 -0
  79. package/__tests__/testPlugin/tsconfig.json +10 -0
  80. package/__tests__/utils/utils.ts +492 -0
  81. package/app.json +23 -0
  82. package/bin/deploy-docs +39 -0
  83. package/client/ActionheroWebsocketClient.js +277 -0
  84. package/docker-compose.yml +73 -0
  85. package/package.json +24 -0
  86. package/public/chat.html +194 -0
  87. package/public/css/cosmo.css +12 -0
  88. package/public/favicon.ico +0 -0
  89. package/public/index.html +115 -0
  90. package/public/javascript/.gitkeep +0 -0
  91. package/public/linkedSession.html +80 -0
  92. package/public/logo/actionhero-small.png +0 -0
  93. package/public/logo/actionhero.png +0 -0
  94. package/public/pixel.gif +0 -0
  95. package/public/simple.html +2 -0
  96. package/public/swagger.html +32 -0
  97. package/public/websocketLoadTest.html +322 -0
  98. package/src/actions/cacheTest.ts +58 -0
  99. package/src/actions/createChatRoom.ts +20 -0
  100. package/src/actions/randomNumber.ts +17 -0
  101. package/src/actions/recursiveAction.ts +13 -0
  102. package/src/actions/sendFile.ts +12 -0
  103. package/src/actions/sleepTest.ts +40 -0
  104. package/src/actions/status.ts +73 -0
  105. package/src/actions/swagger.ts +155 -0
  106. package/src/actions/validationTest.ts +36 -0
  107. package/src/bin/actionhero.ts +225 -0
  108. package/src/bin/methods/actions/list.ts +30 -0
  109. package/src/bin/methods/console.ts +26 -0
  110. package/src/bin/methods/generate/action.ts +58 -0
  111. package/src/bin/methods/generate/cli.ts +51 -0
  112. package/src/bin/methods/generate/initializer.ts +54 -0
  113. package/src/bin/methods/generate/plugin.ts +57 -0
  114. package/src/bin/methods/generate/server.ts +38 -0
  115. package/src/bin/methods/generate/task.ts +68 -0
  116. package/src/bin/methods/generate.ts +176 -0
  117. package/src/bin/methods/task/enqueue.ts +35 -0
  118. package/src/classes/action.ts +98 -0
  119. package/src/classes/actionProcessor.ts +463 -0
  120. package/src/classes/api.ts +51 -0
  121. package/src/classes/cli.ts +67 -0
  122. package/src/classes/config.ts +15 -0
  123. package/src/classes/connection.ts +321 -0
  124. package/src/classes/exceptionReporter.ts +9 -0
  125. package/src/classes/initializer.ts +59 -0
  126. package/src/classes/initializers.ts +5 -0
  127. package/src/classes/input.ts +9 -0
  128. package/src/classes/inputs.ts +34 -0
  129. package/src/classes/process/actionheroVersion.ts +15 -0
  130. package/src/classes/process/env.ts +16 -0
  131. package/src/classes/process/id.ts +34 -0
  132. package/src/classes/process/pid.ts +32 -0
  133. package/src/classes/process/projectRoot.ts +16 -0
  134. package/src/classes/process/typescript.ts +47 -0
  135. package/src/classes/process.ts +479 -0
  136. package/src/classes/server.ts +251 -0
  137. package/src/classes/task.ts +87 -0
  138. package/src/config/api.ts +107 -0
  139. package/src/config/errors.ts +162 -0
  140. package/src/config/logger.ts +113 -0
  141. package/src/config/plugins.ts +37 -0
  142. package/src/config/redis.ts +78 -0
  143. package/src/config/routes.ts +44 -0
  144. package/src/config/tasks.ts +84 -0
  145. package/src/config/web.ts +136 -0
  146. package/src/config/websocket.ts +62 -0
  147. package/src/index.ts +46 -0
  148. package/src/initializers/actions.ts +125 -0
  149. package/src/initializers/chatRoom.ts +214 -0
  150. package/src/initializers/connections.ts +124 -0
  151. package/src/initializers/exceptions.ts +155 -0
  152. package/src/initializers/params.ts +52 -0
  153. package/src/initializers/redis.ts +191 -0
  154. package/src/initializers/resque.ts +248 -0
  155. package/src/initializers/routes.ts +229 -0
  156. package/src/initializers/servers.ts +134 -0
  157. package/src/initializers/specHelper.ts +195 -0
  158. package/src/initializers/staticFile.ts +253 -0
  159. package/src/initializers/tasks.ts +188 -0
  160. package/src/modules/action.ts +89 -0
  161. package/src/modules/cache.ts +326 -0
  162. package/src/modules/chatRoom.ts +321 -0
  163. package/src/modules/config.ts +246 -0
  164. package/src/modules/log.ts +62 -0
  165. package/src/modules/redis.ts +93 -0
  166. package/src/modules/route.ts +59 -0
  167. package/src/modules/specHelper.ts +182 -0
  168. package/src/modules/task.ts +527 -0
  169. package/src/modules/utils/argv.ts +3 -0
  170. package/src/modules/utils/arrayStartingMatch.ts +21 -0
  171. package/src/modules/utils/arrayUnique.ts +15 -0
  172. package/src/modules/utils/collapseObjectToArray.ts +33 -0
  173. package/src/modules/utils/deepCopy.ts +3 -0
  174. package/src/modules/utils/ensureNoTsHeaderOrSpecFiles.ts +19 -0
  175. package/src/modules/utils/eventLoopDelay.ts +34 -0
  176. package/src/modules/utils/fileUtils.ts +119 -0
  177. package/src/modules/utils/filterObjectForLogging.ts +51 -0
  178. package/src/modules/utils/filterResponseForLogging.ts +53 -0
  179. package/src/modules/utils/getExternalIPAddress.ts +17 -0
  180. package/src/modules/utils/hashMerge.ts +63 -0
  181. package/src/modules/utils/isPlainObject.ts +45 -0
  182. package/src/modules/utils/isRunning.ts +7 -0
  183. package/src/modules/utils/parseCookies.ts +20 -0
  184. package/src/modules/utils/parseHeadersForClientAddress.ts +53 -0
  185. package/src/modules/utils/parseIPv6URI.ts +24 -0
  186. package/src/modules/utils/replaceDistWithSrc.ts +9 -0
  187. package/src/modules/utils/safeGlob.ts +6 -0
  188. package/src/modules/utils/sleep.ts +8 -0
  189. package/src/modules/utils/sortGlobalMiddleware.ts +17 -0
  190. package/src/modules/utils/sourceRelativeLinkPath.ts +29 -0
  191. package/src/modules/utils.ts +66 -0
  192. package/src/server.ts +20 -0
  193. package/src/servers/web.ts +894 -0
  194. package/src/servers/websocket.ts +304 -0
  195. package/src/tasks/runAction.ts +29 -0
  196. package/tea.yaml +9 -0
  197. package/templates/README.md.template +17 -0
  198. package/templates/action.ts.template +15 -0
  199. package/templates/boot.js.template +9 -0
  200. package/templates/cli.ts.template +15 -0
  201. package/templates/gitignore.template +23 -0
  202. package/templates/initializer.ts.template +17 -0
  203. package/templates/package-plugin.json.template +12 -0
  204. package/templates/package.json.template +45 -0
  205. package/templates/projectMap.txt +39 -0
  206. package/templates/projectServer.ts.template +20 -0
  207. package/templates/server.ts.template +37 -0
  208. package/templates/task.ts.template +16 -0
  209. package/templates/test/action.ts.template +13 -0
  210. package/templates/test/task.ts.template +20 -0
  211. package/tsconfig.json +11 -0
@@ -0,0 +1,1031 @@
1
+ process.env.AUTOMATIC_ROUTES = "head,get,post,put,delete";
2
+
3
+ import axios, { AxiosError } from "axios";
4
+ import * as FormData from "form-data";
5
+ import { wrapper } from "axios-cookiejar-support";
6
+ import { CookieJar } from "tough-cookie";
7
+ import * as fs from "fs";
8
+ import * as os from "os";
9
+ import * as path from "path";
10
+ import { api, Process, config, utils, route } from "./../../../src/index";
11
+
12
+ const actionhero = new Process();
13
+ let url: string;
14
+
15
+ describe("Server: Web", () => {
16
+ beforeAll(async () => {
17
+ await actionhero.start();
18
+ url = "http://localhost:" + config.web!.port;
19
+ });
20
+
21
+ afterAll(async () => await actionhero.stop());
22
+
23
+ test("should be up and return data", async () => {
24
+ await axios.get(url + "/api/randomNumber");
25
+ // should throw no errors
26
+ });
27
+
28
+ test("basic response should be JSON and have basic data", async () => {
29
+ const response = await axios.get(url + "/api/randomNumber");
30
+ expect(response).toBeInstanceOf(Object);
31
+ expect(response.data.requesterInformation).toBeInstanceOf(Object);
32
+ });
33
+
34
+ test("returns JSON with errors", async () => {
35
+ try {
36
+ await axios.get(url + "/api");
37
+ throw new Error("should not get here");
38
+ } catch (error) {
39
+ if (error instanceof AxiosError) {
40
+ expect(error.response?.status).toEqual(404);
41
+ expect(error.response?.data.requesterInformation).toBeInstanceOf(
42
+ Object,
43
+ );
44
+ } else throw error;
45
+ }
46
+ });
47
+
48
+ test("params work", async () => {
49
+ try {
50
+ await axios.get(url + "/api?key=value");
51
+ throw new Error("should not get here");
52
+ } catch (error) {
53
+ if (error instanceof AxiosError) {
54
+ expect(error.response?.status).toEqual(404);
55
+ expect(
56
+ error.response?.data.requesterInformation.receivedParams.key,
57
+ ).toEqual("value");
58
+ } else throw error;
59
+ }
60
+ });
61
+
62
+ test("params are ignored unless they are in the whitelist", async () => {
63
+ try {
64
+ await axios.get(url + "/api?crazyParam123=something");
65
+ throw new Error("should not get here");
66
+ } catch (error) {
67
+ if (error instanceof AxiosError) {
68
+ expect(error.response?.status).toEqual(404);
69
+ expect(
70
+ error.response?.data.requesterInformation.receivedParams
71
+ .crazyParam123,
72
+ ).toBeUndefined();
73
+ } else throw error;
74
+ }
75
+ });
76
+
77
+ describe("will properly destroy connections", () => {
78
+ beforeAll(() => {
79
+ config.web!.returnErrorCodes = true;
80
+ api.actions.versions.customRender = [1];
81
+ api.actions.actions.customRender = {
82
+ // @ts-ignore
83
+ 1: {
84
+ name: "customRender",
85
+ description: "I am a test",
86
+ version: 1,
87
+ outputExample: {},
88
+ run: async (data) => {
89
+ data.toRender = false;
90
+ data.connection!.rawConnection.res.writeHead(200, {
91
+ "Content-Type": "text/plain",
92
+ });
93
+ data.connection!.rawConnection.res.end(`${Math.random()}`);
94
+ },
95
+ },
96
+ };
97
+
98
+ api.routes.loadRoutes();
99
+ });
100
+
101
+ afterAll(() => {
102
+ delete api.actions.actions.customRender;
103
+ delete api.actions.versions.customRender;
104
+ });
105
+
106
+ test("works for the API", async () => {
107
+ expect(Object.keys(api.connections.connections)).toHaveLength(0);
108
+ axios.get(url + "/api/sleepTest"); // don't await
109
+
110
+ await utils.sleep(100);
111
+ expect(Object.keys(api.connections.connections)).toHaveLength(1);
112
+
113
+ await utils.sleep(1000);
114
+ expect(Object.keys(api.connections.connections)).toHaveLength(0);
115
+ });
116
+
117
+ test("works for files", async () => {
118
+ expect(Object.keys(api.connections.connections)).toHaveLength(0);
119
+ await axios.get(url + "/simple.html");
120
+ await utils.sleep(100);
121
+ expect(Object.keys(api.connections.connections)).toHaveLength(0);
122
+ });
123
+
124
+ test("works for actions with toRender: false", async () => {
125
+ expect(Object.keys(api.connections.connections)).toHaveLength(0);
126
+ const body = await axios.get(url + "/api/customRender");
127
+ expect(body).toBeTruthy();
128
+ await utils.sleep(200);
129
+ expect(Object.keys(api.connections.connections)).toHaveLength(0);
130
+ });
131
+ });
132
+
133
+ describe("errors", () => {
134
+ beforeAll(() => {
135
+ api.actions.versions.stringErrorTestAction = [1];
136
+ api.actions.actions.stringErrorTestAction = {
137
+ // @ts-ignore
138
+ 1: {
139
+ name: "stringErrorTestAction",
140
+ description: "stringErrorTestAction",
141
+ version: 1,
142
+ run: async (data) => {
143
+ data.response!.error = "broken";
144
+ },
145
+ },
146
+ };
147
+
148
+ api.actions.versions.errorErrorTestAction = [1];
149
+ api.actions.actions.errorErrorTestAction = {
150
+ // @ts-ignore
151
+ 1: {
152
+ name: "errorErrorTestAction",
153
+ description: "errorErrorTestAction",
154
+ version: 1,
155
+ run: async () => {
156
+ throw new Error("broken");
157
+ },
158
+ },
159
+ };
160
+
161
+ api.actions.versions.complexErrorTestAction = [1];
162
+ api.actions.actions.complexErrorTestAction = {
163
+ // @ts-ignore
164
+ 1: {
165
+ name: "complexErrorTestAction",
166
+ description: "complexErrorTestAction",
167
+ version: 1,
168
+ run: async (data) => {
169
+ data.response!.error = { error: "broken", reason: "stuff" };
170
+ },
171
+ },
172
+ };
173
+
174
+ api.routes.loadRoutes();
175
+ });
176
+
177
+ afterAll(() => {
178
+ delete api.actions.actions.stringErrorTestAction;
179
+ delete api.actions.versions.stringErrorTestAction;
180
+ delete api.actions.actions.errorErrorTestAction;
181
+ delete api.actions.versions.errorErrorTestAction;
182
+ delete api.actions.actions.complexErrorTestAction;
183
+ delete api.actions.versions.complexErrorTestAction;
184
+ });
185
+
186
+ test("errors can be error strings", async () => {
187
+ try {
188
+ await axios.get(url + "/api/stringErrorTestAction");
189
+ throw new Error("should not get here");
190
+ } catch (error) {
191
+ if (error instanceof AxiosError) {
192
+ expect(error.response?.status).toEqual(500);
193
+ expect(error.response?.data.error).toEqual("broken");
194
+ } else throw error;
195
+ }
196
+ });
197
+
198
+ test("errors can be error objects and returned plainly", async () => {
199
+ try {
200
+ await axios.get(url + "/api/errorErrorTestAction");
201
+ throw new Error("should not get here");
202
+ } catch (error) {
203
+ if (error instanceof AxiosError) {
204
+ expect(error.response?.status).toEqual(500);
205
+ expect(error.response?.data.error).toEqual("broken");
206
+ } else throw error;
207
+ }
208
+ });
209
+
210
+ test("errors can be complex JSON payloads", async () => {
211
+ try {
212
+ await axios.get(url + "/api/complexErrorTestAction");
213
+ throw new Error("should not get here");
214
+ } catch (error) {
215
+ if (error instanceof AxiosError) {
216
+ expect(error.response?.status).toEqual(500);
217
+ expect(error.response?.data.error).toEqual({
218
+ error: "broken",
219
+ reason: "stuff",
220
+ });
221
+ } else throw error;
222
+ }
223
+ });
224
+ });
225
+
226
+ describe("if disableParamScrubbing is set", () => {
227
+ let orig: boolean;
228
+ beforeAll(() => {
229
+ orig = config.general!.disableParamScrubbing as boolean;
230
+ config.general!.disableParamScrubbing = true;
231
+ });
232
+
233
+ afterAll(() => {
234
+ config.general!.disableParamScrubbing = orig;
235
+ });
236
+
237
+ test("params are not ignored", async () => {
238
+ try {
239
+ await axios.get(url + "/api/testAction/?crazyParam123=something");
240
+ throw new Error("should not get here");
241
+ } catch (error) {
242
+ if (error instanceof AxiosError) {
243
+ expect(error.response?.status).toEqual(404);
244
+ expect(
245
+ error.response?.data.requesterInformation.receivedParams
246
+ .crazyParam123,
247
+ ).toEqual("something");
248
+ } else throw error;
249
+ }
250
+ });
251
+ });
252
+
253
+ test("gibberish actions have the right response", async () => {
254
+ try {
255
+ await axios.get(url + "/api/IAMNOTANACTION");
256
+ throw new Error("should not get here");
257
+ } catch (error) {
258
+ if (error instanceof AxiosError) {
259
+ expect(error.response?.status).toEqual(404);
260
+ expect(error.response?.data.error).toEqual(
261
+ "unknown action or invalid apiVersion",
262
+ );
263
+ } else throw error;
264
+ }
265
+ });
266
+
267
+ test("real actions do not have an error response", async () => {
268
+ const response = await axios.get(url + "/api/status");
269
+ expect(response.data.error).toBeUndefined();
270
+ });
271
+
272
+ test("HTTP Verbs should work: GET", async () => {
273
+ const response = await axios.get(url + "/api/randomNumber");
274
+ expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
275
+ expect(response.data.randomNumber).toBeLessThan(1);
276
+ });
277
+
278
+ test("HTTP Verbs should work: PUT", async () => {
279
+ const response = await axios.put(url + "/api/randomNumber");
280
+ expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
281
+ expect(response.data.randomNumber).toBeLessThan(1);
282
+ });
283
+
284
+ test("HTTP Verbs should work: POST", async () => {
285
+ const response = await axios.post(url + "/api/randomNumber");
286
+ expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
287
+ expect(response.data.randomNumber).toBeLessThan(1);
288
+ });
289
+
290
+ test("HTTP Verbs should work: DELETE", async () => {
291
+ const response = await axios.delete(url + "/api/randomNumber");
292
+ expect(response.data.randomNumber).toBeGreaterThanOrEqual(0);
293
+ expect(response.data.randomNumber).toBeLessThan(1);
294
+ });
295
+
296
+ test("HTTP Verbs should work: Post with Form", async () => {
297
+ try {
298
+ const formDataA = new FormData();
299
+ formDataA.append("key", "key");
300
+ await axios.post(url + "/api/cacheTest", formDataA);
301
+ throw new Error("should not get here");
302
+ } catch (error) {
303
+ if (error instanceof AxiosError) {
304
+ expect(error.response?.status).toEqual(422);
305
+ expect(error.response?.data.error).toEqual(
306
+ "value is a required parameter for this action",
307
+ );
308
+ } else throw error;
309
+ }
310
+
311
+ const formDataB = new FormData();
312
+ formDataB.append("key", "key");
313
+ formDataB.append("value", "value");
314
+ const successResponse = await axios.post(url + "/api/cacheTest", formDataB);
315
+
316
+ expect(successResponse.data.cacheTestResults.saveResp).toEqual(true);
317
+ });
318
+
319
+ test("HTTP Verbs should work: Post with JSON Payload as body", async () => {
320
+ try {
321
+ await axios.post(url + "/api/cacheTest", { key: "key" });
322
+ throw new Error("should not get here");
323
+ } catch (error) {
324
+ if (error instanceof AxiosError) {
325
+ expect(error.response?.status).toEqual(422);
326
+ expect(error.response?.data.error).toEqual(
327
+ "value is a required parameter for this action",
328
+ );
329
+ } else throw error;
330
+ }
331
+
332
+ const successResponse = await axios.post(url + "/api/cacheTest", {
333
+ key: "key",
334
+ value: "value",
335
+ });
336
+
337
+ expect(successResponse.data.cacheTestResults.saveResp).toEqual(true);
338
+ });
339
+
340
+ describe("messageId", () => {
341
+ test("generates unique messageIds for each request", async () => {
342
+ const responseA = await axios.get(url + "/api/randomNumber");
343
+ const responseB = await axios.get(url + "/api/randomNumber");
344
+ expect(responseA.data.requesterInformation.messageId).not.toEqual(
345
+ responseB.data.requesterInformation.messageId,
346
+ );
347
+ });
348
+
349
+ test("messageIds can be provided by the client and returned by the server", async () => {
350
+ const response = await axios.get(url + "/api/randomNumber?messageId=aaa");
351
+ expect(response.data.requesterInformation.messageId).not.toEqual("aaa");
352
+ });
353
+
354
+ test("a connection id should be a combination of fingerprint and message id", async () => {
355
+ const response = await axios.get(url + "/api/randomNumber");
356
+ expect(response.data.requesterInformation.id).toEqual(
357
+ `${response.data.requesterInformation.fingerprint}-${response.data.requesterInformation.messageId}`,
358
+ );
359
+ });
360
+ });
361
+
362
+ describe("connection.rawConnection.params", () => {
363
+ beforeAll(() => {
364
+ api.actions.versions.paramTestAction = [1];
365
+ api.actions.actions.paramTestAction = {
366
+ // @ts-ignore
367
+ 1: {
368
+ name: "paramTestAction",
369
+ description: "I return connection.rawConnection.params",
370
+ version: 1,
371
+ run: async (data) => {
372
+ data.response = data.connection!.rawConnection.params;
373
+ if (data.connection!.rawConnection.params.rawBody) {
374
+ data.response!.rawBody =
375
+ data.connection!.rawConnection.params.rawBody.toString();
376
+ }
377
+ },
378
+ },
379
+ };
380
+
381
+ api.routes.loadRoutes();
382
+ });
383
+
384
+ afterAll(() => {
385
+ delete api.actions.actions.paramTestAction;
386
+ delete api.actions.versions.paramTestAction;
387
+ });
388
+
389
+ test(".query should contain unfiltered query params", async () => {
390
+ const response = await axios.get(
391
+ url + "/api/paramTestAction/?crazyParam123=something",
392
+ );
393
+ expect(response.data.query.crazyParam123).toEqual("something");
394
+ });
395
+
396
+ test(".body should contain unfiltered, parsed request body params", async () => {
397
+ const response = await axios.post(url + "/api/paramTestAction", {
398
+ key: "value",
399
+ });
400
+
401
+ expect(response.data.body.key).toEqual("value");
402
+ });
403
+
404
+ test(".rawBody can be disabled", async () => {
405
+ config.web!.saveRawBody = false;
406
+ const requestBody = '{"key": "value"}';
407
+ const response = await axios.post(
408
+ url + "/api/paramTestAction",
409
+ requestBody,
410
+ { headers: { "Content-type": "application/json" } },
411
+ );
412
+ expect(response.data.body.key).toEqual("value");
413
+ expect(response.data.rawBody).toEqual("");
414
+ });
415
+ });
416
+
417
+ test("returnErrorCodes can be opted to change http header codes", async () => {
418
+ try {
419
+ await axios.delete(url + "/api/");
420
+ } catch (error) {
421
+ if (error instanceof AxiosError) {
422
+ expect(error.response?.status).toEqual(404);
423
+ } else throw error;
424
+ }
425
+ });
426
+
427
+ describe("http header", () => {
428
+ beforeAll(() => {
429
+ api.actions.versions.headerTestAction = [1];
430
+ api.actions.actions.headerTestAction = {
431
+ // @ts-ignore
432
+ 1: {
433
+ name: "headerTestAction",
434
+ description: "I am a test",
435
+ version: 1,
436
+ outputExample: {},
437
+ run: async (data) => {
438
+ data.connection!.rawConnection.responseHeaders.push(["thing", "A"]);
439
+ data.connection!.rawConnection.responseHeaders.push(["thing", "B"]);
440
+ data.connection!.rawConnection.responseHeaders.push(["thing", "C"]);
441
+ data.connection!.rawConnection.responseHeaders.push([
442
+ "Set-Cookie",
443
+ "value_1=1",
444
+ ]);
445
+ data.connection!.rawConnection.responseHeaders.push([
446
+ "Set-Cookie",
447
+ "value_2=2",
448
+ ]);
449
+ },
450
+ },
451
+ };
452
+
453
+ api.routes.loadRoutes();
454
+ });
455
+
456
+ afterAll(() => {
457
+ delete api.actions.actions.headerTestAction;
458
+ delete api.actions.versions.headerTestAction;
459
+ });
460
+
461
+ test("duplicate headers should be removed (in favor of the last set)", async () => {
462
+ const response = await axios.get(url + "/api/headerTestAction");
463
+ expect(response.status).toEqual(200);
464
+ expect(response.headers.thing).toEqual("C");
465
+ });
466
+
467
+ test("but duplicate set-cookie requests should be allowed", async () => {
468
+ const response = await axios.get(url + "/api/headerTestAction");
469
+ expect(response.status).toEqual(200);
470
+ // this will convert node >= 10 header array to look like node <= 9 combined strings
471
+ const cookieString = (response.headers["set-cookie"] || [""]).join();
472
+ const parts = cookieString.split(",");
473
+ expect(parts[1]).toEqual("value_1=1");
474
+ expect(parts[0]).toEqual("value_2=2");
475
+ });
476
+
477
+ test("should respond to OPTIONS with only HTTP headers", async () => {
478
+ const response = await axios.options(url + "/api/cacheTest");
479
+ expect(response.status).toEqual(200);
480
+ expect(response.headers["access-control-allow-methods"]).toEqual(
481
+ "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE",
482
+ );
483
+ expect(response.headers["access-control-allow-origin"]).toEqual("*");
484
+ expect(response.headers["content-length"]).toEqual("0");
485
+ expect(response.data).toEqual("");
486
+ });
487
+
488
+ test("should respond to TRACE with parsed params received", async () => {
489
+ const response = await axios({
490
+ method: "trace",
491
+ url: url + "/api/x",
492
+ data: { key: "someKey", value: "someValue" },
493
+ });
494
+ expect(response.status).toEqual(200);
495
+ expect(response.data.receivedParams.key).toEqual("someKey");
496
+ expect(response.data.receivedParams.value).toEqual("someValue");
497
+ });
498
+
499
+ test("should respond to HEAD requests just like GET, but with no body", async () => {
500
+ const response = await axios.head(url + "/api/headerTestAction");
501
+ expect(response.status).toEqual(200);
502
+ expect(response.data).toEqual("");
503
+ });
504
+
505
+ test("keeps sessions with browser_fingerprint", async () => {
506
+ const jar = new CookieJar();
507
+ const client = wrapper(axios.create({ jar }));
508
+
509
+ const response1 = await client.post(url + "/api/randomNumber");
510
+ const response2 = await client.get(url + "/api/randomNumber");
511
+ const response3 = await client.put(url + "/api/randomNumber");
512
+ const response4 = await client.delete(url + "/api/randomNumber");
513
+ const response5 = await axios.delete(url + "/api/randomNumber");
514
+
515
+ expect(response1.headers["set-cookie"]).toBeTruthy();
516
+ expect(response2.headers["set-cookie"]).toBeUndefined();
517
+ expect(response3.headers["set-cookie"]).toBeUndefined();
518
+ expect(response4.headers["set-cookie"]).toBeUndefined();
519
+ expect(response5.headers["set-cookie"]).toBeTruthy();
520
+
521
+ const fingerprint1 = response1.data.requesterInformation.id.split("-")[0];
522
+ const fingerprint2 = response2.data.requesterInformation.id.split("-")[0];
523
+ const fingerprint3 = response3.data.requesterInformation.id.split("-")[0];
524
+ const fingerprint4 = response4.data.requesterInformation.id.split("-")[0];
525
+ const fingerprint5 = response5.data.requesterInformation.id.split("-")[0];
526
+
527
+ expect(fingerprint1).toEqual(fingerprint2);
528
+ expect(fingerprint1).toEqual(fingerprint3);
529
+ expect(fingerprint1).toEqual(fingerprint4);
530
+ expect(fingerprint1).not.toEqual(fingerprint5);
531
+
532
+ expect(fingerprint1).toEqual(
533
+ response1.data.requesterInformation.fingerprint,
534
+ );
535
+ expect(fingerprint2).toEqual(
536
+ response2.data.requesterInformation.fingerprint,
537
+ );
538
+ expect(fingerprint3).toEqual(
539
+ response3.data.requesterInformation.fingerprint,
540
+ );
541
+ expect(fingerprint4).toEqual(
542
+ response4.data.requesterInformation.fingerprint,
543
+ );
544
+ expect(fingerprint5).toEqual(
545
+ response5.data.requesterInformation.fingerprint,
546
+ );
547
+ });
548
+ });
549
+
550
+ describe("http returnErrorCodes true", () => {
551
+ class ErrorWithCode extends Error {
552
+ code: number;
553
+ }
554
+
555
+ beforeAll(() => {
556
+ api.actions.versions.statusTestAction = [1];
557
+ api.actions.actions.statusTestAction = {
558
+ // @ts-ignore
559
+ 1: {
560
+ name: "statusTestAction",
561
+ description: "I am a test",
562
+ inputs: {
563
+ key: { required: true },
564
+ query: { required: false },
565
+ randomKey: { required: false },
566
+ },
567
+ run: async (data) => {
568
+ if (data.params!.key !== "value") {
569
+ data.connection!.rawConnection.responseHttpCode = 402;
570
+ throw new ErrorWithCode("key != value");
571
+ }
572
+ const hasQueryParam = !!data.params!.query;
573
+ if (hasQueryParam) {
574
+ const validQueryFilters = ["test", "search"];
575
+ const validQueryParam =
576
+ validQueryFilters.indexOf(data.params!.query) > -1;
577
+ if (!validQueryParam) {
578
+ const notFoundError = new ErrorWithCode(
579
+ `404: Filter '${data.params!.query}' not found `,
580
+ );
581
+ notFoundError.code = 404;
582
+ throw notFoundError;
583
+ }
584
+ }
585
+ const hasRandomKey = !!data.params!.randomKey;
586
+ if (hasRandomKey) {
587
+ const validRandomKeys = ["key1", "key2", "key3"];
588
+ const validRandomKey =
589
+ validRandomKeys.indexOf(data.params!.randomKey) > -1;
590
+ if (!validRandomKey) {
591
+ if (data.params!.randomKey === "expired-key") {
592
+ const expiredError = new ErrorWithCode(
593
+ `999: Key '${data.params!.randomKey}' is expired`,
594
+ );
595
+ expiredError.code = 999;
596
+ throw expiredError;
597
+ }
598
+ const suspiciousError = new ErrorWithCode(
599
+ `402: Suspicious Activity detected with key ${
600
+ data.params!.randomKey
601
+ }`,
602
+ );
603
+ suspiciousError.code = 402;
604
+ throw suspiciousError;
605
+ }
606
+ }
607
+ data.response!.good = true;
608
+ },
609
+ },
610
+ };
611
+
612
+ api.routes.loadRoutes();
613
+ });
614
+
615
+ afterAll(() => {
616
+ delete api.actions.versions.statusTestAction;
617
+ delete api.actions.actions.statusTestAction;
618
+ });
619
+
620
+ test("actions that do not exists should return 404", async () => {
621
+ try {
622
+ await axios.post(url + "/api/aFakeAction");
623
+ throw new Error("should not get here");
624
+ } catch (error) {
625
+ if (error instanceof AxiosError) {
626
+ expect(error.response?.status).toEqual(404);
627
+ } else throw error;
628
+ }
629
+ });
630
+
631
+ test("missing params result in a 422", async () => {
632
+ try {
633
+ await axios.post(url + "/api/statusTestAction");
634
+ throw new Error("should not get here");
635
+ } catch (error) {
636
+ if (error instanceof AxiosError) {
637
+ expect(error.response?.status).toEqual(422);
638
+ } else throw error;
639
+ }
640
+ });
641
+
642
+ test("status codes can be set for errors", async () => {
643
+ try {
644
+ await axios.post(url + "/api/statusTestAction", { key: "bannana" });
645
+ throw new Error("should not get here");
646
+ } catch (error) {
647
+ if (error instanceof AxiosError) {
648
+ expect(error.response?.status).toEqual(402);
649
+ expect(error.response?.data.error).toEqual("key != value");
650
+ } else throw error;
651
+ }
652
+ });
653
+
654
+ test("status code should still be 200 if everything is OK", async () => {
655
+ const response = await axios.post(url + "/api/statusTestAction", {
656
+ key: "value",
657
+ });
658
+ expect(response.status).toEqual(200);
659
+ expect(response.data.good).toEqual(true);
660
+ });
661
+
662
+ describe("setting status code using custom errors", () => {
663
+ test("should work for 404 status code, set using custom error for invalid params", async () => {
664
+ try {
665
+ await axios.post(url + "/api/statusTestAction", {
666
+ key: "value",
667
+ query: "guess",
668
+ });
669
+ throw new Error("should not get here");
670
+ } catch (error) {
671
+ if (error instanceof AxiosError) {
672
+ expect(error.response?.status).toEqual(404);
673
+ expect(error.response?.data.error).toEqual(
674
+ "404: Filter 'guess' not found ",
675
+ );
676
+ } else throw error;
677
+ }
678
+ });
679
+
680
+ test("should work for 402 status code set using custom error for invalid params", async () => {
681
+ try {
682
+ await axios.post(url + "/api/statusTestAction", {
683
+ key: "value",
684
+ randomKey: "guessKey",
685
+ });
686
+ throw new Error("should not get here");
687
+ } catch (error) {
688
+ if (error instanceof AxiosError) {
689
+ expect(error.response?.status).toEqual(402);
690
+ expect(error.response?.data.error).toEqual(
691
+ "402: Suspicious Activity detected with key guessKey",
692
+ );
693
+ } else throw error;
694
+ }
695
+ });
696
+
697
+ test("should not throw custom error for valid params", async () => {
698
+ const responseWithQuery = await axios.post(
699
+ url + "/api/statusTestAction",
700
+ { key: "value", query: "test" },
701
+ );
702
+ expect(responseWithQuery.status).toEqual(200);
703
+ expect(responseWithQuery.data.good).toEqual(true);
704
+
705
+ const responseWithRandomKey = await axios.post(
706
+ url + "/api/statusTestAction",
707
+ { key: "value", randomKey: "key1" },
708
+ );
709
+ expect(responseWithRandomKey.status).toEqual(200);
710
+ expect(responseWithRandomKey.data.good).toEqual(true);
711
+
712
+ const responseWithKeyAndQuery = await axios.post(
713
+ url + "/api/statusTestAction",
714
+ {
715
+ key: "value",
716
+ query: "search",
717
+ randomKey: "key2",
718
+ },
719
+ );
720
+ expect(responseWithKeyAndQuery.status).toEqual(200);
721
+ expect(responseWithKeyAndQuery.data.good).toEqual(true);
722
+ });
723
+
724
+ test("should not work for 999 status code set using custom error and default error code, 400 is thrown", async () => {
725
+ try {
726
+ await axios.post(url + "/api/statusTestAction", {
727
+ key: "value",
728
+ randomKey: "expired-key",
729
+ });
730
+ throw new Error("should not get here");
731
+ } catch (error) {
732
+ if (error instanceof AxiosError) {
733
+ expect(error.response?.status).not.toEqual(999);
734
+ expect(error.response?.status).toEqual(500);
735
+ expect(error.response?.data.error).toEqual(
736
+ "999: Key 'expired-key' is expired",
737
+ );
738
+ } else throw error;
739
+ }
740
+ });
741
+ });
742
+ });
743
+
744
+ describe("documentation", () => {
745
+ test("documentation can be returned via a swagger action", async () => {
746
+ const response = await axios.get(url + "/api/swagger");
747
+ expect(response.data.paths).toBeInstanceOf(Object);
748
+ });
749
+ });
750
+
751
+ describe("files", () => {
752
+ test("an HTML file", async () => {
753
+ const response = await axios.get(url + "/public/simple.html");
754
+ expect(response.status).toEqual(200);
755
+ expect(response.data).toContain("<h1>Actionhero</h1>");
756
+ });
757
+
758
+ test("404 pages", async () => {
759
+ try {
760
+ await axios.get(url + "/public/notARealFile");
761
+ throw new Error("should not get here");
762
+ } catch (error) {
763
+ if (error instanceof AxiosError) {
764
+ expect(error.response?.status).toEqual(404);
765
+ } else throw error;
766
+ }
767
+ });
768
+
769
+ test("404 pages from POST with if-modified-since header", async () => {
770
+ const file = Math.random().toString(36);
771
+ try {
772
+ await axios.get(url + "/" + file, {
773
+ headers: { "if-modified-since": "Thu, 19 Apr 2012 09:51:20 GMT" },
774
+ });
775
+ throw new Error("should not get here");
776
+ } catch (error) {
777
+ if (error instanceof AxiosError) {
778
+ expect(error.response?.status).toEqual(404);
779
+ expect(error.response?.data).toEqual("that file is not found");
780
+ } else throw error;
781
+ }
782
+ });
783
+
784
+ test("should not see files outside of the public dir", async () => {
785
+ try {
786
+ await axios.get(url + "/public/../config.json");
787
+ throw new Error("should not get here");
788
+ } catch (error) {
789
+ if (error instanceof AxiosError) {
790
+ expect(error.response?.status).toEqual(404);
791
+ expect(error.response?.data).toEqual("that file is not found");
792
+ } else throw error;
793
+ }
794
+ });
795
+
796
+ test("index page should be served when requesting a path (trailing slash)", async () => {
797
+ const response = await axios.get(url + "/public/");
798
+ expect(response.status).toEqual(200);
799
+ expect(response.data).toMatch(
800
+ /Actionhero is a multi-transport API Server/,
801
+ );
802
+ });
803
+
804
+ test("index page should be served when requesting a path (no trailing slash)", async () => {
805
+ const response = await axios.get(url + "/public");
806
+ expect(response.status).toEqual(200);
807
+ expect(response.data).toMatch(
808
+ /Actionhero is a multi-transport API Server/,
809
+ );
810
+ });
811
+
812
+ describe("can serve files from a specific mapped route", () => {
813
+ beforeAll(() => {
814
+ const testFolderPublicPath = path.join(
815
+ __dirname,
816
+ "/../../../public/testFolder",
817
+ );
818
+ fs.mkdirSync(testFolderPublicPath);
819
+ fs.writeFileSync(
820
+ testFolderPublicPath + "/testFile.html",
821
+ "Actionhero Route Test File",
822
+ );
823
+
824
+ route.registerRoute(
825
+ "get",
826
+ "/my/public/route",
827
+ // @ts-ignore
828
+ null,
829
+ null,
830
+ true,
831
+ testFolderPublicPath,
832
+ );
833
+ });
834
+
835
+ afterAll(() => {
836
+ const testFolderPublicPath = path.join(
837
+ __dirname,
838
+ "/../../../public/testFolder",
839
+ );
840
+ fs.unlinkSync(testFolderPublicPath + path.sep + "testFile.html");
841
+ fs.rmdirSync(testFolderPublicPath);
842
+ });
843
+
844
+ test("works for routes mapped paths", async () => {
845
+ const response = await axios.get(
846
+ url + "/my/public/route/testFile.html",
847
+ );
848
+ expect(response.status).toEqual(200);
849
+ expect(response.data).toEqual("Actionhero Route Test File");
850
+ });
851
+
852
+ test("returns 404 for files not available in route mapped paths", async () => {
853
+ try {
854
+ await axios.get(url + "/my/public/route/fileNotFound.html");
855
+ } catch (error) {
856
+ if (error instanceof AxiosError) {
857
+ expect(error.response?.status).toEqual(404);
858
+ expect(error.response?.data).toEqual("that file is not found");
859
+ } else throw error;
860
+ }
861
+ });
862
+
863
+ test("should not see files outside of the mapped dir", async () => {
864
+ try {
865
+ await axios.get(url + "/my/public/route/../../config/servers/web.js");
866
+ } catch (error) {
867
+ if (error instanceof AxiosError) {
868
+ expect(error.response?.status).toEqual(404);
869
+ expect(error.response?.data).toEqual("that file is not found");
870
+ } else throw error;
871
+ }
872
+ });
873
+ });
874
+
875
+ describe("can serve files from more than one directory", () => {
876
+ const source = path.join(__dirname, "/../../../public/simple.html");
877
+
878
+ beforeAll(() => {
879
+ fs.createReadStream(source).pipe(
880
+ fs.createWriteStream(os.tmpdir() + path.sep + "tmpTestFile.html"),
881
+ );
882
+ api.staticFile.searchLocations.push(os.tmpdir());
883
+ });
884
+
885
+ afterAll(() => {
886
+ fs.unlinkSync(os.tmpdir() + path.sep + "tmpTestFile.html");
887
+ api.staticFile.searchLocations.pop();
888
+ });
889
+
890
+ test("works for secondary paths", async () => {
891
+ const response = await axios.get(url + "/public/tmpTestFile.html");
892
+ expect(response.status).toEqual(200);
893
+ expect(response.data).toContain("<h1>Actionhero</h1>");
894
+ });
895
+ });
896
+ });
897
+
898
+ describe("custom methods", () => {
899
+ let originalRoutes: typeof api.routes.routes;
900
+
901
+ beforeAll(() => {
902
+ originalRoutes = api.routes.routes;
903
+ api.actions.versions.proxyHeaders = [1];
904
+ api.actions.actions.proxyHeaders = {
905
+ // @ts-ignore
906
+ 1: {
907
+ name: "proxyHeaders",
908
+ description: "proxy header test",
909
+ inputs: {},
910
+ outputExample: {},
911
+ run: async (data) => {
912
+ data.connection!.setHeader!("X-Foo", "bar");
913
+ },
914
+ },
915
+ };
916
+
917
+ api.actions.versions.proxyStatusCode = [1];
918
+ api.actions.actions.proxyStatusCode = {
919
+ // @ts-ignore
920
+ 1: {
921
+ name: "proxyStatusCode",
922
+ description: "proxy status code test",
923
+ inputs: {
924
+ code: {
925
+ required: true,
926
+ default: 200,
927
+ formatter: (p: string) => {
928
+ return parseInt(p);
929
+ },
930
+ },
931
+ },
932
+ outputExample: {},
933
+ run: async (data) => {
934
+ data.connection!.setStatusCode!(data.params!.code);
935
+ },
936
+ },
937
+ };
938
+
939
+ api.actions.versions.pipe = [1];
940
+ api.actions.actions.pipe = {
941
+ // @ts-ignore
942
+ 1: {
943
+ name: "pipe",
944
+ description: "pipe response test",
945
+ inputs: {
946
+ mode: { required: true },
947
+ },
948
+ outputExample: {},
949
+ run: async (data) => {
950
+ data.toRender = false;
951
+ if (data.params!.mode === "string") {
952
+ data.connection!.pipe!("a string", { "custom-header": "cool" });
953
+ } else if (data.params!.mode === "buffer") {
954
+ data.connection!.pipe!(Buffer.from("a buffer"), {
955
+ "custom-header": "still-cool",
956
+ });
957
+ } else if (data.params!.mode === "contentType") {
958
+ data.connection!.pipe!("just some good, old-fashioned words", {
959
+ "Content-Type": "text/plain",
960
+ "custom-header": "words",
961
+ });
962
+ } else {
963
+ throw new Error("I Do not know this mode");
964
+ }
965
+ },
966
+ },
967
+ };
968
+
969
+ api.routes.loadRoutes({
970
+ get: [
971
+ { path: "/proxy", action: "proxyHeaders", apiVersion: 1 },
972
+ { path: "/code", action: "proxyStatusCode", apiVersion: 1 },
973
+ { path: "/pipe", action: "pipe", apiVersion: 1 },
974
+ ],
975
+ });
976
+ });
977
+
978
+ afterAll(() => {
979
+ api.routes.routes = originalRoutes;
980
+ delete api.actions.versions.proxyHeaders;
981
+ delete api.actions.versions.proxyStatusCode;
982
+ delete api.actions.versions.pipe;
983
+ delete api.actions.actions.proxyHeaders;
984
+ delete api.actions.actions.proxyStatusCode;
985
+ delete api.actions.actions.pipe;
986
+ });
987
+
988
+ test("actions handled by the web server support proxy for setHeaders", async () => {
989
+ const response = await axios.get(url + "/api/proxy");
990
+ expect(response.headers["x-foo"]).toEqual("bar");
991
+ });
992
+
993
+ test("actions handled by the web server support proxy for setting status code", async () => {
994
+ const responseDefault = await axios.get(url + "/api/proxyStatusCode", {});
995
+ expect(responseDefault.status).toEqual(200);
996
+
997
+ try {
998
+ await axios.get(url + "/api/proxyStatusCode?code=404");
999
+ throw new Error("should not get here");
1000
+ } catch (error) {
1001
+ if (error instanceof AxiosError) {
1002
+ expect(error.response?.status).toEqual(404);
1003
+ } else throw error;
1004
+ }
1005
+ });
1006
+
1007
+ test("can pipe string responses with custom headers to clients", async () => {
1008
+ const response = await axios.get(url + "/api/pipe?mode=string");
1009
+ expect(response.headers["custom-header"]).toEqual("cool");
1010
+ expect(response.headers["content-length"]).toEqual("8");
1011
+ expect(response.data).toEqual("a string");
1012
+ });
1013
+
1014
+ test("can pipe buffer responses with custom headers to clients", async () => {
1015
+ const response = await axios.get(url + "/api/pipe?mode=buffer");
1016
+ expect(response.headers["custom-header"]).toEqual("still-cool");
1017
+ expect(response.headers["content-length"]).toEqual("8");
1018
+ expect(response.data).toEqual("a buffer");
1019
+ });
1020
+
1021
+ test("can pipe buffer responses with custom content types to clients", async () => {
1022
+ const { headers, data } = await axios.get(
1023
+ url + "/api/pipe?mode=contentType",
1024
+ );
1025
+ expect(headers["content-type"]).toEqual("text/plain");
1026
+ expect(headers["content-length"]).toEqual("35");
1027
+ expect(headers["custom-header"]).toEqual("words");
1028
+ expect(data).toEqual("just some good, old-fashioned words");
1029
+ });
1030
+ });
1031
+ });