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,579 @@
1
+ import axios, { AxiosError } from "axios";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { api, Process, config, route } from "../../../../src/index";
6
+ import { routerMethods } from "../../../../src/modules/route";
7
+
8
+ let url: string;
9
+ let actionhero: Process;
10
+
11
+ describe("Server: Web", () => {
12
+ beforeAll(async () => {
13
+ actionhero = new Process();
14
+ await actionhero.start();
15
+ url = "http://localhost:" + config.web!.port;
16
+ });
17
+
18
+ afterAll(async () => await actionhero.stop());
19
+
20
+ describe("routes", () => {
21
+ let originalRoutes: typeof api.routes.routes;
22
+
23
+ beforeAll(() => {
24
+ originalRoutes = api.routes.routes;
25
+ api.actions.versions.mimeTestAction = [1];
26
+ api.actions.actions.mimeTestAction = {
27
+ // @ts-ignore
28
+ 1: {
29
+ name: "mimeTestAction",
30
+ description: "I am a test",
31
+ matchExtensionMimeType: true,
32
+ inputs: {
33
+ key: { required: true },
34
+ path: { required: false },
35
+ },
36
+ outputExample: {},
37
+ run: async (data) => {
38
+ if (data.params!.key === "fail") {
39
+ throw new Error("failed");
40
+ }
41
+
42
+ data.response!.matchedRoute = data.connection!.matchedRoute;
43
+ },
44
+ },
45
+ };
46
+
47
+ api.actions.versions.login = [1, 2];
48
+ api.actions.actions.login = {
49
+ // @ts-ignore
50
+ 1: {
51
+ name: "login",
52
+ description: "login",
53
+ version: 1,
54
+ matchExtensionMimeType: true,
55
+ inputs: {
56
+ user_id: { required: true },
57
+ },
58
+ outputExample: {},
59
+ run: async (data) => {
60
+ data.response!.user_id = data.params!.user_id;
61
+ data.response!.version = 1;
62
+ },
63
+ },
64
+
65
+ // @ts-ignore
66
+ 2: {
67
+ name: "login",
68
+ description: "login",
69
+ version: 2,
70
+ matchExtensionMimeType: true,
71
+ inputs: {
72
+ userID: { required: true },
73
+ },
74
+ outputExample: {},
75
+ run: async (data) => {
76
+ data.response!.userID = data.params!.userID;
77
+ data.response!.version = 2;
78
+ },
79
+ },
80
+
81
+ // @ts-ignore
82
+ three: {
83
+ name: "login",
84
+ description: "login",
85
+ version: "three",
86
+ matchExtensionMimeType: true,
87
+ inputs: {
88
+ userID: { required: true },
89
+ },
90
+ outputExample: {},
91
+ run: async (data) => {
92
+ data.response!.userID = data.params!.userID;
93
+ data.response!.version = "three";
94
+ },
95
+ },
96
+ };
97
+
98
+ api.params.buildPostVariables();
99
+ api.routes.loadRoutes({
100
+ all: [{ path: "/user/:userID", action: "user" }],
101
+ get: [
102
+ { path: "/bogus/:bogusID", action: "bogusAction" },
103
+ { path: "/users", action: "usersList" },
104
+ { path: "/c/:key/:value", action: "cacheTest" },
105
+ { path: "/mimeTestAction/:key", action: "mimeTestAction" },
106
+ { path: "/thing", action: "thing" },
107
+ { path: "/thing/stuff", action: "thingStuff" },
108
+ { path: "/v:apiVersion/login", action: "login" },
109
+ { path: "/login/v:apiVersion/stuff", action: "login" },
110
+ { path: "/login", action: "login" },
111
+ { path: "/old_login", action: "login", apiVersion: "1" },
112
+ {
113
+ path: "/a/wild/:key/:path(^.*$)",
114
+ action: "mimeTestAction",
115
+ apiVersion: "1",
116
+ matchTrailingPathParts: true,
117
+ },
118
+ {
119
+ path: "/a/complex/:key/__:path(^.*$)",
120
+ action: "mimeTestAction",
121
+ apiVersion: "1",
122
+ matchTrailingPathParts: true,
123
+ },
124
+ ],
125
+ post: [{ path: "/login/:userID(^(\\d{3}|admin)$)", action: "login" }],
126
+ });
127
+ });
128
+
129
+ afterAll(() => {
130
+ api.routes.routes = originalRoutes;
131
+ delete api.actions.versions.mimeTestAction;
132
+ delete api.actions.actions.mimeTestAction;
133
+ delete api.actions.versions.login;
134
+ delete api.actions.actions.login;
135
+ });
136
+
137
+ test("new params will not be allowed in route definitions (an action should do it)", () => {
138
+ expect(api.params.postVariables).not.toContain("bogusID");
139
+ });
140
+
141
+ test("'all' routes are duplicated properly", () => {
142
+ // @ts-ignore
143
+ route.registerRoute("all", "/other-login", "login", null);
144
+ const loaded: Partial<Record<(typeof routerMethods)[number], boolean>> =
145
+ {};
146
+ const registered: Partial<
147
+ Record<(typeof routerMethods)[number], boolean>
148
+ > = {};
149
+ routerMethods.forEach((verb) => {
150
+ api.routes.routes[verb].forEach((route) => {
151
+ if (!loaded[verb]) {
152
+ loaded[verb] =
153
+ route.action === "user" && route.path === "/user/:userID";
154
+ }
155
+ if (!registered[verb]) {
156
+ registered[verb] =
157
+ route.action === "login" && route.path === "/other-login";
158
+ }
159
+ });
160
+ });
161
+ expect(Object.keys(loaded).length).toEqual(routerMethods.length);
162
+ expect(Object.keys(registered).length).toEqual(routerMethods.length);
163
+ });
164
+
165
+ test("unknown actions are still unknown", async () => {
166
+ try {
167
+ await axios.get(url + "/api/a_crazy_action");
168
+ throw new Error("should not get here");
169
+ } catch (error) {
170
+ if (error instanceof AxiosError) {
171
+ expect(error.response?.status).toEqual(404);
172
+ expect(error.response?.data.error).toEqual(
173
+ "unknown action or invalid apiVersion",
174
+ );
175
+ } else throw error;
176
+ }
177
+ });
178
+
179
+ test("route actions will override explicit actions, if the defined action is null", async () => {
180
+ try {
181
+ await axios.get(url + "/api/user/123?action=someFakeAction");
182
+ throw new Error("should not get here");
183
+ } catch (error) {
184
+ if (error instanceof AxiosError) {
185
+ expect(error.response?.status).toEqual(404);
186
+ expect(
187
+ error.response?.data.requesterInformation.receivedParams.action,
188
+ ).toEqual("user");
189
+ } else throw error;
190
+ }
191
+ });
192
+
193
+ test("returns application/json when the mime type cannot be determined for an action", async () => {
194
+ const response = await axios.get(url + "/api/mimeTestAction/thing.bogus");
195
+ expect(response.headers["content-type"]).toMatch(/json/);
196
+ expect(response.data.matchedRoute.path).toEqual("/mimeTestAction/:key");
197
+ expect(response.data.matchedRoute.action).toEqual("mimeTestAction");
198
+ });
199
+
200
+ test("route actions have the matched route available to the action", async () => {
201
+ const body = await axios.get(url + "/api/mimeTestAction/thing.json");
202
+ expect(body.data.matchedRoute.path).toEqual("/mimeTestAction/:key");
203
+ expect(body.data.matchedRoute.action).toEqual("mimeTestAction");
204
+ });
205
+
206
+ test("Routes should recognize apiVersion as default param", async () => {
207
+ const body = await axios.get(url + "/api/old_login?user_id=7");
208
+ expect(body.data.user_id).toEqual("7");
209
+ expect(body.data.requesterInformation.receivedParams.action).toEqual(
210
+ "login",
211
+ );
212
+ });
213
+
214
+ test("Routes should be mapped for GET (simple)", async () => {
215
+ try {
216
+ await axios.get(url + "/api/users");
217
+ throw new Error("should not get here");
218
+ } catch (error) {
219
+ if (error instanceof AxiosError) {
220
+ expect(error.response?.status).toEqual(404);
221
+ expect(
222
+ error.response?.data.requesterInformation.receivedParams.action,
223
+ ).toEqual("usersList");
224
+ } else throw error;
225
+ }
226
+ });
227
+
228
+ test("Routes should be mapped for GET (complex)", async () => {
229
+ try {
230
+ await axios.get(url + "/api/user/1234");
231
+ throw new Error("should not get here");
232
+ } catch (error) {
233
+ if (error instanceof AxiosError) {
234
+ expect(error.response?.status).toEqual(404);
235
+ expect(
236
+ error.response?.data.requesterInformation.receivedParams.action,
237
+ ).toEqual("user");
238
+ expect(
239
+ error.response?.data.requesterInformation.receivedParams.userID,
240
+ ).toEqual("1234");
241
+ } else throw error;
242
+ }
243
+ });
244
+
245
+ test("Routes should be mapped for POST", async () => {
246
+ try {
247
+ await axios.post(url + "/api/user/1234?key=value");
248
+ throw new Error("should not get here");
249
+ } catch (error) {
250
+ if (error instanceof AxiosError) {
251
+ expect(error.response?.status).toEqual(404);
252
+ expect(
253
+ error.response?.data.requesterInformation.receivedParams.action,
254
+ ).toEqual("user");
255
+ expect(
256
+ error.response?.data.requesterInformation.receivedParams.userID,
257
+ ).toEqual("1234");
258
+ expect(
259
+ error.response?.data.requesterInformation.receivedParams.key,
260
+ ).toEqual("value");
261
+ } else throw error;
262
+ }
263
+ });
264
+
265
+ test("Routes should be mapped for PUT", async () => {
266
+ try {
267
+ await axios.put(url + "/api/user/1234?key=value");
268
+ throw new Error("should not get here");
269
+ } catch (error) {
270
+ if (error instanceof AxiosError) {
271
+ expect(error.response?.status).toEqual(404);
272
+ expect(
273
+ error.response?.data.requesterInformation.receivedParams.action,
274
+ ).toEqual("user");
275
+ expect(
276
+ error.response?.data.requesterInformation.receivedParams.userID,
277
+ ).toEqual("1234");
278
+ expect(
279
+ error.response?.data.requesterInformation.receivedParams.key,
280
+ ).toEqual("value");
281
+ } else throw error;
282
+ }
283
+ });
284
+
285
+ test("Routes should be mapped for DELETE", async () => {
286
+ try {
287
+ await axios.delete(url + "/api/user/1234?key=value");
288
+ throw new Error("should not get here");
289
+ } catch (error) {
290
+ if (error instanceof AxiosError) {
291
+ expect(error.response?.status).toEqual(404);
292
+ expect(
293
+ error.response?.data.requesterInformation.receivedParams.action,
294
+ ).toEqual("user");
295
+ expect(
296
+ error.response?.data.requesterInformation.receivedParams.userID,
297
+ ).toEqual("1234");
298
+ expect(
299
+ error.response?.data.requesterInformation.receivedParams.key,
300
+ ).toEqual("value");
301
+ } else throw error;
302
+ }
303
+ });
304
+
305
+ test("route params trump explicit params", async () => {
306
+ try {
307
+ await axios.get(url + "/api/user/1?userID=2");
308
+ throw new Error("should not get here");
309
+ } catch (error) {
310
+ if (error instanceof AxiosError) {
311
+ expect(error.response?.status).toEqual(404);
312
+ expect(
313
+ error.response?.data.requesterInformation.receivedParams.action,
314
+ ).toEqual("user");
315
+ expect(
316
+ error.response?.data.requesterInformation.receivedParams.userID,
317
+ ).toEqual("1");
318
+ } else throw error;
319
+ }
320
+ });
321
+
322
+ test("to match, a route much match all parts of the URL", async () => {
323
+ try {
324
+ await axios.get(url + "/api/thing");
325
+ throw new Error("should not get here");
326
+ } catch (error) {
327
+ if (error instanceof AxiosError) {
328
+ expect(error.response?.status).toEqual(404);
329
+ expect(
330
+ error.response?.data.requesterInformation.receivedParams.action,
331
+ ).toEqual("thing");
332
+ } else throw error;
333
+ }
334
+
335
+ try {
336
+ await axios.get(url + "/api/thing/stuff");
337
+ throw new Error("should not get here");
338
+ } catch (error) {
339
+ if (error instanceof AxiosError) {
340
+ expect(error.response?.status).toEqual(404);
341
+ expect(
342
+ error.response?.data.requesterInformation.receivedParams.action,
343
+ ).toEqual("thingStuff");
344
+ } else throw error;
345
+ }
346
+ });
347
+
348
+ test("regexp matches will provide proper variables", async () => {
349
+ const response = await axios.post(url + "/api/login/123");
350
+ expect(response.data.requesterInformation.receivedParams.action).toEqual(
351
+ "login",
352
+ );
353
+ expect(response.data.requesterInformation.receivedParams.userID).toEqual(
354
+ "123",
355
+ );
356
+
357
+ const responseAgain = await axios.post(url + "/api/login/admin");
358
+ expect(
359
+ responseAgain.data.requesterInformation.receivedParams.action,
360
+ ).toEqual("login");
361
+ expect(
362
+ responseAgain.data.requesterInformation.receivedParams.userID,
363
+ ).toEqual("admin");
364
+ });
365
+
366
+ test("regexp matches will still work with params with periods and other wacky chars", async () => {
367
+ const response = await axios.get(url + "/api/c/key/log_me-in.com$123.");
368
+ expect(response.data.requesterInformation.receivedParams.action).toEqual(
369
+ "cacheTest",
370
+ );
371
+ expect(response.data.requesterInformation.receivedParams.value).toEqual(
372
+ "log_me-in.com$123.",
373
+ );
374
+ });
375
+
376
+ test("regexp match failures will be rejected", async () => {
377
+ try {
378
+ await axios.get(url + "/api/login/1234");
379
+ throw new Error("should not get here");
380
+ } catch (error) {
381
+ if (error instanceof AxiosError) {
382
+ expect(error.response?.status).toEqual(404);
383
+ expect(
384
+ error.response?.data.requesterInformation.receivedParams.userID,
385
+ ).toBeUndefined();
386
+ } else throw error;
387
+ }
388
+ });
389
+
390
+ describe("file extensions + routes", () => {
391
+ test("will change header information based on extension (when active)", async () => {
392
+ const response = await axios.get(url + "/api/mimeTestAction/val.png");
393
+ expect(response.headers["content-type"]).toEqual("image/png");
394
+ });
395
+
396
+ test("will not change header information if there is a connection.error", async () => {
397
+ try {
398
+ await axios.get(url + "/api/mimeTestAction/fail");
399
+ throw new Error("should not get here");
400
+ } catch (error) {
401
+ if (error instanceof AxiosError) {
402
+ expect(error.response?.status).toEqual(500);
403
+ expect(error.response?.headers["content-type"]).toEqual(
404
+ "application/json; charset=utf-8",
405
+ );
406
+ expect(error.response?.data.error).toEqual("failed");
407
+ } else throw error;
408
+ }
409
+ });
410
+
411
+ test("works with with matchTrailingPathParts", async () => {
412
+ const response = await axios.get(
413
+ url + "/api/a/wild/theKey/and/some/more/path",
414
+ );
415
+ expect(
416
+ response.data.requesterInformation.receivedParams.action,
417
+ ).toEqual("mimeTestAction");
418
+ expect(response.data.requesterInformation.receivedParams.path).toEqual(
419
+ "and/some/more/path",
420
+ );
421
+ expect(response.data.requesterInformation.receivedParams.key).toEqual(
422
+ "theKey",
423
+ );
424
+ });
425
+
426
+ test("works with with matchTrailingPathParts and ignored variable prefixes", async () => {
427
+ const response = await axios.get(
428
+ url + "/api/a/complex/theKey/__path-stuff",
429
+ );
430
+ expect(
431
+ response.data.requesterInformation.receivedParams.action,
432
+ ).toEqual("mimeTestAction");
433
+ expect(response.data.requesterInformation.receivedParams.path).toEqual(
434
+ "path-stuff",
435
+ );
436
+ expect(response.data.requesterInformation.receivedParams.key).toEqual(
437
+ "theKey",
438
+ );
439
+ });
440
+ });
441
+
442
+ describe("spaces in URL with public files", () => {
443
+ const source = path.join(
444
+ __dirname,
445
+ "/../../../../public/logo/actionhero.png",
446
+ );
447
+
448
+ beforeAll(async () => {
449
+ const tmpDir = os.tmpdir();
450
+ const readStream = fs.createReadStream(source);
451
+ api.staticFile.searchLocations.push(tmpDir);
452
+
453
+ await new Promise((resolve) => {
454
+ readStream.pipe(
455
+ fs.createWriteStream(
456
+ tmpDir + path.sep + "actionhero with space.png",
457
+ ),
458
+ );
459
+ readStream.on("close", resolve);
460
+ });
461
+ });
462
+
463
+ afterAll(() => {
464
+ fs.unlinkSync(os.tmpdir() + path.sep + "actionhero with space.png");
465
+ api.staticFile.searchLocations.pop();
466
+ });
467
+
468
+ test("will decode %20 or plus sign to a space so that file system can read", async () => {
469
+ const response = await axios.get(
470
+ url + "/actionhero%20with%20space.png",
471
+ );
472
+ expect(response.status).toEqual(200);
473
+ expect(response.data).toMatch(/PNG/);
474
+ expect(response.headers["content-type"]).toEqual("image/png");
475
+ });
476
+
477
+ test("will capture bad encoding in URL and return NOT FOUND", async () => {
478
+ try {
479
+ await axios.get(url + "/actionhero%20%%%%%%%%%%with+space.png");
480
+ throw new Error("should not get here");
481
+ } catch (error) {
482
+ if (error instanceof AxiosError) {
483
+ expect(error.response?.status).toEqual(404);
484
+ expect(error.response?.data).toEqual("that file is not found");
485
+ } else throw error;
486
+ }
487
+ });
488
+ });
489
+
490
+ describe("versions", () => {
491
+ test("versions can be numbers", async () => {
492
+ const response = await axios.get(url + "/api/v1/login?user_id=123");
493
+ expect(response.data.version).toEqual(1);
494
+ expect(response.data.user_id).toEqual("123");
495
+ });
496
+
497
+ test("versions can be strings", async () => {
498
+ const response = await axios.get(url + "/api/vthree/login?userID=123");
499
+ expect(response.data.version).toEqual("three");
500
+ expect(response.data.userID).toEqual("123");
501
+ });
502
+
503
+ test("versions have an ignored prefix", async () => {
504
+ const response = await axios.get(url + "/api/v1/login?user_id=123");
505
+ expect(response.data.version).toEqual(1);
506
+ expect(response.data.user_id).toEqual("123");
507
+ expect(
508
+ response.data.requesterInformation.receivedParams.apiVersion,
509
+ ).toBe("1");
510
+ expect(response.data.requesterInformation.receivedParams.action).toBe(
511
+ "login",
512
+ );
513
+ });
514
+
515
+ [
516
+ [false, "/api/v0/login"], // there is no version 0
517
+ [true, "/api/v1/login"], // ✅
518
+ [true, "/api/v2/login"], // ✅
519
+ [true, "/api/vthree/login"], // ✅
520
+ [false, "/api/v9999/login"], // there is no version 99
521
+ [false, "/api/1/login"], // "1" is not "v1"
522
+ [false, "/api/three/login"], // "1" is not "v3"
523
+ [true, "/api/login"], // ✅
524
+ [false, "/api/foo/login"], // foo is not a matching prefix
525
+ [false, "/api/vv/login"], // "v" is not a version
526
+ [false, "/api/login/v1"], // "stuff" is needed at the end
527
+ [true, "/api/login/v1/stuff"], // ✅
528
+ [true, "/api/login/v2/stuff"], // ✅
529
+ [true, "/api/login/vthree/stuff"], // ✅
530
+ [false, "/api/login/v99/stuff"], // there is no version 99
531
+ ].forEach((group) => {
532
+ test(`routes match (${group[1]} - ${group[0]})`, async () => {
533
+ const [match, path] = group;
534
+ try {
535
+ await axios.get(url + path);
536
+ throw new Error("should not get here");
537
+ } catch (error) {
538
+ if (error instanceof AxiosError) {
539
+ expect(error.response?.data.error).toMatch(
540
+ match
541
+ ? "is a required parameter for this action"
542
+ : "unknown action or invalid apiVersion",
543
+ );
544
+ } else throw error;
545
+ }
546
+ });
547
+ });
548
+
549
+ test("routes with no version will default to the highest version number", async () => {
550
+ // sorting numerically, 2 > 'three'
551
+ const response = await axios.get(url + "/api/login?userID=123");
552
+ expect(response.data.version).toEqual(2);
553
+ expect(response.data.userID).toEqual("123");
554
+ });
555
+ });
556
+ });
557
+
558
+ describe("manually set routes persist a reload", () => {
559
+ let originalRoutes: typeof api.routes.routes;
560
+ beforeAll(() => {
561
+ originalRoutes = api.routes.routes;
562
+ });
563
+ afterAll(() => {
564
+ api.routes.routes = originalRoutes;
565
+ });
566
+
567
+ test("it remembers manually loaded routes", async () => {
568
+ // @ts-ignore
569
+ route.registerRoute("get", "/a-custom-route", "randomNumber", null);
570
+ const response = await axios.get(url + "/api/a-custom-route");
571
+ expect(response.status).toEqual(200);
572
+
573
+ api.routes.loadRoutes();
574
+
575
+ const responseAgain = await axios.get(url + "/api/a-custom-route");
576
+ expect(responseAgain.status).toEqual(200);
577
+ });
578
+ });
579
+ });
@@ -0,0 +1,92 @@
1
+ import axios, { AxiosError } from "axios";
2
+ import { Process, config } from "./../../../../src/index";
3
+
4
+ const actionhero = new Process();
5
+ let url: string;
6
+
7
+ jest.mock("./../../../../src/config/web.ts", () => ({
8
+ __esModule: true,
9
+ test: {
10
+ web: () => {
11
+ return {
12
+ enabled: true,
13
+ secure: false,
14
+ urlPathForActions: "/craz/y/action/path",
15
+ urlPathForFiles: "/a/b/c",
16
+ rootEndpointType: "file",
17
+ port: 18080 + parseInt(process.env.JEST_WORKER_ID || "0"),
18
+ matchExtensionMime: true,
19
+ metadataOptions: {
20
+ serverInformation: true,
21
+ requesterInformation: false,
22
+ },
23
+ fingerprintOptions: {
24
+ cookieKey: "sessionID",
25
+ },
26
+ };
27
+ },
28
+ },
29
+ }));
30
+
31
+ describe("Server: Web", () => {
32
+ describe("Routes", () => {
33
+ beforeAll(async () => {
34
+ await actionhero.start();
35
+ url = "http://localhost:" + config.web!.port;
36
+ });
37
+
38
+ afterAll(async () => {
39
+ await actionhero.stop();
40
+ });
41
+
42
+ describe("simple routing", () => {
43
+ describe("very deep routes", () => {
44
+ test("old action routes stop working", async () => {
45
+ try {
46
+ await axios.get(url + "/api/randomNumber");
47
+ throw new Error("should not get here");
48
+ } catch (error) {
49
+ if (error instanceof AxiosError) {
50
+ expect(error.response?.status).toEqual(404);
51
+ } else throw error;
52
+ }
53
+ });
54
+
55
+ test("can ask for nested URL actions", async () => {
56
+ const response = await axios.get(
57
+ url + "/craz/y/action/path/randomNumber",
58
+ );
59
+ expect(response.status).toEqual(200);
60
+ });
61
+
62
+ test("old file routes stop working", async () => {
63
+ try {
64
+ await axios.get(url + "/public/simple.html");
65
+ throw new Error("should not get here");
66
+ } catch (error) {
67
+ if (error instanceof AxiosError) {
68
+ expect(error.response?.status).toEqual(404);
69
+ } else throw error;
70
+ }
71
+ });
72
+
73
+ test("can ask for nested URL files", async () => {
74
+ const response = await axios.get(url + "/a/b/c/simple.html");
75
+ expect(response.status).toEqual(200);
76
+ expect(response.data).toContain("<h1>Actionhero</h1>");
77
+ });
78
+
79
+ test("can ask for nested URL files with depth", async () => {
80
+ const response = await axios.get(url + "/a/b/c/css/cosmo.css");
81
+ expect(response.status).toEqual(200);
82
+ });
83
+
84
+ test("root route files still work", async () => {
85
+ const response = await axios.get(url + "/simple.html");
86
+ expect(response.status).toEqual(200);
87
+ expect(response.data).toContain("<h1>Actionhero</h1>");
88
+ });
89
+ });
90
+ });
91
+ });
92
+ });