@hubspot/ui-extensions-dev-server 1.1.6 → 1.1.8

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 (131) hide show
  1. package/dist/lib/DevServerState.d.ts +1 -1
  2. package/dist/lib/ExtensionsWebSocket.js +26 -2
  3. package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +42 -8
  4. package/dist/lib/__tests__/app-functions/context.spec.d.ts +1 -0
  5. package/dist/lib/__tests__/app-functions/context.spec.js +101 -0
  6. package/dist/lib/__tests__/app-functions/errorReporter.spec.d.ts +1 -0
  7. package/dist/lib/__tests__/app-functions/errorReporter.spec.js +102 -0
  8. package/dist/lib/__tests__/app-functions/executor_v20231.spec.d.ts +1 -0
  9. package/dist/lib/__tests__/app-functions/executor_v20231.spec.js +168 -0
  10. package/dist/lib/__tests__/app-functions/executor_v20232.spec.d.ts +1 -0
  11. package/dist/lib/__tests__/app-functions/executor_v20232.spec.js +190 -0
  12. package/dist/lib/__tests__/app-functions/fixtures/constants.d.ts +18 -0
  13. package/dist/lib/__tests__/app-functions/fixtures/constants.js +139 -0
  14. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-fails.cjs +8 -0
  15. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-fails.d.cts +1 -0
  16. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-succeeds.cjs +8 -0
  17. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-succeeds.d.cts +1 -0
  18. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-rejected.cjs +8 -0
  19. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-rejected.d.cts +1 -0
  20. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-resolved.cjs +8 -0
  21. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-resolved.d.cts +1 -0
  22. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-does-not-export-main.cjs +4 -0
  23. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-does-not-export-main.d.cts +1 -0
  24. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-echos-input.cjs +8 -0
  25. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-echos-input.d.cts +1 -0
  26. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-logs.cjs +10 -0
  27. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-logs.d.cts +1 -0
  28. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-function.cjs +4 -0
  29. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-function.d.cts +1 -0
  30. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-rejected.cjs +7 -0
  31. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-rejected.d.cts +1 -0
  32. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-resolved.cjs +7 -0
  33. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-resolved.d.cts +1 -0
  34. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-text.cjs +4 -0
  35. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-text.d.cts +1 -0
  36. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-undefined.cjs +4 -0
  37. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-undefined.d.cts +1 -0
  38. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-throws-error.cjs +4 -0
  39. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-throws-error.d.cts +1 -0
  40. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-times-out.cjs +10 -0
  41. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-times-out.d.cts +1 -0
  42. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-undeclared.cjs +4 -0
  43. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-undeclared.d.cts +1 -0
  44. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-uses-secret.cjs +14 -0
  45. package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-uses-secret.d.cts +1 -0
  46. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-fails.cjs +5 -0
  47. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-fails.d.cts +1 -0
  48. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-succeeds.cjs +5 -0
  49. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-succeeds.d.cts +3 -0
  50. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-calls-callback.cjs +4 -0
  51. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-calls-callback.d.cts +1 -0
  52. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-does-not-export-main.cjs +4 -0
  53. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-does-not-export-main.d.cts +1 -0
  54. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-echos-input.cjs +8 -0
  55. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-echos-input.d.cts +5 -0
  56. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-logs.cjs +10 -0
  57. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-logs.d.cts +3 -0
  58. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-function.cjs +4 -0
  59. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-function.d.cts +1 -0
  60. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-implicitly.cjs +7 -0
  61. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-implicitly.d.cts +1 -0
  62. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-rejected.cjs +4 -0
  63. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-rejected.d.cts +1 -0
  64. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-resolved.cjs +4 -0
  65. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-resolved.d.cts +3 -0
  66. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-text.cjs +4 -0
  67. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-text.d.cts +1 -0
  68. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-undefined.cjs +4 -0
  69. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-undefined.d.cts +1 -0
  70. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-throws-error.cjs +4 -0
  71. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-throws-error.d.cts +1 -0
  72. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-times-out.cjs +12 -0
  73. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-times-out.d.cts +1 -0
  74. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-undeclared.cjs +4 -0
  75. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-undeclared.d.cts +1 -0
  76. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-uses-secret.cjs +14 -0
  77. package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-uses-secret.d.cts +4 -0
  78. package/dist/lib/__tests__/app-functions/secrets.spec.d.ts +1 -0
  79. package/dist/lib/__tests__/app-functions/secrets.spec.js +278 -0
  80. package/dist/lib/__tests__/app-functions/services/AppProxyService.spec.d.ts +1 -0
  81. package/dist/lib/__tests__/app-functions/services/AppProxyService.spec.js +667 -0
  82. package/dist/lib/__tests__/app-functions/services/PrivateAppUserTokenManager.spec.d.ts +1 -0
  83. package/dist/lib/__tests__/app-functions/services/PrivateAppUserTokenManager.spec.js +243 -0
  84. package/dist/lib/__tests__/app-functions/services/services_v20231.spec.d.ts +1 -0
  85. package/dist/lib/__tests__/app-functions/services/services_v20231.spec.js +319 -0
  86. package/dist/lib/__tests__/app-functions/services/services_v20232.spec.d.ts +1 -0
  87. package/dist/lib/__tests__/app-functions/services/services_v20232.spec.js +302 -0
  88. package/dist/lib/__tests__/app-functions/setup.d.ts +1 -0
  89. package/dist/lib/__tests__/app-functions/setup.js +7 -0
  90. package/dist/lib/__tests__/app-functions/signing.spec.d.ts +1 -0
  91. package/dist/lib/__tests__/app-functions/signing.spec.js +460 -0
  92. package/dist/lib/__tests__/server.spec.js +24 -2
  93. package/dist/lib/app-functions/api/privateAppUserToken.d.ts +16 -0
  94. package/dist/lib/app-functions/api/privateAppUserToken.js +28 -0
  95. package/dist/lib/app-functions/config.d.ts +4 -0
  96. package/dist/lib/app-functions/config.js +48 -0
  97. package/dist/lib/app-functions/constants.d.ts +26 -0
  98. package/dist/lib/app-functions/constants.js +63 -0
  99. package/dist/lib/app-functions/context.d.ts +3 -0
  100. package/dist/lib/app-functions/context.js +65 -0
  101. package/dist/lib/app-functions/errorReporter.d.ts +22 -0
  102. package/dist/lib/app-functions/errorReporter.js +42 -0
  103. package/dist/lib/app-functions/errors.d.ts +44 -0
  104. package/dist/lib/app-functions/errors.js +82 -0
  105. package/dist/lib/app-functions/executor.d.ts +3 -0
  106. package/dist/lib/app-functions/executor.js +131 -0
  107. package/dist/lib/app-functions/index.d.ts +4 -0
  108. package/dist/lib/app-functions/index.js +4 -0
  109. package/dist/lib/app-functions/secrets.d.ts +5 -0
  110. package/dist/lib/app-functions/secrets.js +56 -0
  111. package/dist/lib/app-functions/services/AppFunctionExecutionService.d.ts +2 -0
  112. package/dist/lib/app-functions/services/AppFunctionExecutionService.js +55 -0
  113. package/dist/lib/app-functions/services/AppProxyService.d.ts +5 -0
  114. package/dist/lib/app-functions/services/AppProxyService.js +196 -0
  115. package/dist/lib/app-functions/services/PrivateAppUserTokenManager.d.ts +22 -0
  116. package/dist/lib/app-functions/services/PrivateAppUserTokenManager.js +185 -0
  117. package/dist/lib/app-functions/services/constants.d.ts +4 -0
  118. package/dist/lib/app-functions/services/constants.js +4 -0
  119. package/dist/lib/app-functions/services/index.d.ts +3 -0
  120. package/dist/lib/app-functions/services/index.js +3 -0
  121. package/dist/lib/app-functions/services/messages.d.ts +14 -0
  122. package/dist/lib/app-functions/services/messages.js +36 -0
  123. package/dist/lib/app-functions/signing.d.ts +29 -0
  124. package/dist/lib/app-functions/signing.js +51 -0
  125. package/dist/lib/app-functions/types.d.ts +172 -0
  126. package/dist/lib/app-functions/types.js +6 -0
  127. package/dist/lib/app-functions/utils.d.ts +15 -0
  128. package/dist/lib/app-functions/utils.js +28 -0
  129. package/dist/lib/server.js +15 -4
  130. package/dist/lib/types.d.ts +1 -1
  131. package/package.json +11 -7
@@ -0,0 +1,667 @@
1
+ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
2
+ import { extractErrorData, mapToLocalUrl, AppProxyService, } from "../../../app-functions/services/AppProxyService.js";
3
+ import { axiosErrorMappings, defaultServerError, validationError, } from "../../../app-functions/constants.js";
4
+ import axios, { AxiosError, HttpStatusCode } from 'axios';
5
+ import httpMocks from 'node-mocks-http';
6
+ import { EventEmitter } from 'node:events';
7
+ import * as signing from "../../../app-functions/signing.js";
8
+ const callAppProxy = async ({ requestUri, method, requestTimeoutMillis, requestBody, requestHeaders, accountId, localDevUrlMapping = {}, logger, }) => {
9
+ const request = httpMocks.createRequest({
10
+ method: 'POST',
11
+ url: '/proxy',
12
+ body: {
13
+ requestUri,
14
+ method,
15
+ requestTimeoutMillis,
16
+ requestBody,
17
+ requestHeaders,
18
+ },
19
+ });
20
+ const response = httpMocks.createResponse({
21
+ req: request,
22
+ eventEmitter: EventEmitter,
23
+ });
24
+ const handler = AppProxyService({
25
+ localDevUrlMapping,
26
+ logger,
27
+ accountId,
28
+ allowedUrls: [],
29
+ });
30
+ // Hold response until the handler finishes writing to it. THis must
31
+ // be set up before calling the handler or it may miss the `end` event.
32
+ const responsePromised = new Promise((resolve) => {
33
+ response.on('end', () => {
34
+ resolve(response);
35
+ });
36
+ });
37
+ await handler(request, response);
38
+ return await responsePromised;
39
+ };
40
+ describe('AppProxyService', () => {
41
+ let logger;
42
+ beforeEach(() => {
43
+ logger = {
44
+ error: vi.fn(),
45
+ debug: vi.fn(),
46
+ info: vi.fn(),
47
+ warn: vi.fn(),
48
+ };
49
+ });
50
+ describe('mapToLocalUrl', () => {
51
+ const validUrl = 'https://valid.com';
52
+ const invalidUrl = 'this-is-not-a-url';
53
+ it('should throw an error if provided an invalid uri', () => {
54
+ expect(() => {
55
+ mapToLocalUrl({}, invalidUrl, [], logger);
56
+ }).toThrowError('Invalid URL');
57
+ });
58
+ it('should return the provided requestUri if no mapping is provided', () => {
59
+ expect(mapToLocalUrl({}, validUrl, [], logger)).toBe(validUrl);
60
+ });
61
+ it('should throw an error if the url in the mapping is invalid', () => {
62
+ expect(() => {
63
+ mapToLocalUrl({
64
+ [validUrl]: invalidUrl,
65
+ }, validUrl, [], logger);
66
+ }).toThrowError('Invalid URL');
67
+ });
68
+ it('should update the url correctly', () => {
69
+ const newValidUrl = 'https://newvalidurl.com';
70
+ expect(mapToLocalUrl({
71
+ [validUrl]: newValidUrl,
72
+ }, validUrl, [], logger)).toBe(newValidUrl);
73
+ });
74
+ it('should update the url correctly when there is a base path', () => {
75
+ const basePath = '/some/base/path';
76
+ const validUrlWithBasePath = `${validUrl}${basePath}`;
77
+ const newValidUrl = 'https://newvalidurl.com';
78
+ expect(mapToLocalUrl({
79
+ [validUrl]: newValidUrl,
80
+ }, validUrlWithBasePath, [], logger)).toBe(`${newValidUrl}${basePath}`);
81
+ });
82
+ it('should honor urls with ports', () => {
83
+ const newValidUrl = 'https://localhost:5173';
84
+ expect(mapToLocalUrl({
85
+ [validUrl]: newValidUrl,
86
+ }, validUrl, [], logger)).toBe(newValidUrl);
87
+ });
88
+ it('should return a trailing slash if the user specifies it', () => {
89
+ const newValidUrl = 'https://localhost:5173/';
90
+ expect(mapToLocalUrl({
91
+ [validUrl]: newValidUrl,
92
+ }, validUrl, [], logger)).toBe(newValidUrl);
93
+ });
94
+ it('should allow trailing slashes in the mapping file', () => {
95
+ const newValidUrl = 'https://localhost:5173/';
96
+ expect(mapToLocalUrl({
97
+ [`${validUrl}/`]: newValidUrl,
98
+ }, validUrl, [], logger)).toBe(newValidUrl);
99
+ });
100
+ it('should log a warning if the url provided is not in the allowedUrls', () => {
101
+ mapToLocalUrl({}, validUrl, [], logger);
102
+ expect(logger.warn).toHaveBeenCalledTimes(1);
103
+ expect(logger.warn).toHaveBeenCalledWith(`'${validUrl}' is not in the 'allowedUrls' list in the application configuration file. Uploading the project in it's current state may result in request failures.`);
104
+ });
105
+ it('should NOT log a warning if the url provided is in the allowedUrls', () => {
106
+ const newValidUrl = 'https://localhost:5173/';
107
+ mapToLocalUrl({
108
+ [validUrl]: newValidUrl,
109
+ }, validUrl, [validUrl], logger);
110
+ expect(logger.warn).not.toHaveBeenCalled();
111
+ });
112
+ });
113
+ describe('extractErrorData', () => {
114
+ it('should return the default error if the provided error is not an AxiosError', () => {
115
+ expect(extractErrorData({})).toBe(defaultServerError);
116
+ });
117
+ it('should return the default error if the provided error is an unmapped AxiosError', () => {
118
+ expect(extractErrorData(new AxiosError())).toBe(defaultServerError);
119
+ });
120
+ it('should return the correct error data', () => {
121
+ expect(extractErrorData(new axios.AxiosError('YOOOOOO', AxiosError.ERR_FR_TOO_MANY_REDIRECTS))).toBe(axiosErrorMappings[AxiosError.ERR_FR_TOO_MANY_REDIRECTS]);
122
+ });
123
+ it('should return the status and data from the error.response if it exists', () => {
124
+ const error = new AxiosError('YOOOOOO', AxiosError.ERR_FR_TOO_MANY_REDIRECTS);
125
+ // @ts-expect-error excluding properties we don't care about
126
+ error.response = {
127
+ data: {
128
+ message: 'RUH ROH',
129
+ },
130
+ status: HttpStatusCode.ImATeapot,
131
+ };
132
+ expect(extractErrorData(error)).toStrictEqual({
133
+ ...axiosErrorMappings[AxiosError.ERR_FR_TOO_MANY_REDIRECTS],
134
+ status: error.response.status,
135
+ data: error.response.data,
136
+ });
137
+ });
138
+ });
139
+ describe('Request handler', () => {
140
+ // Changing this secret will change all the signatures
141
+ const clientSecret = '12345678-aaaa-bbbb-cccc-ddddeeeeffff';
142
+ // Changing this timestamp will change all the signatures
143
+ const fakeTime = 1111111111111;
144
+ const expectedSignatureHeaders = (includeContentType) => ({
145
+ 'X-HubSpot-Request-Timestamp': fakeTime,
146
+ 'X-HubSpot-Signature': expect.any(String),
147
+ 'X-HubSpot-Signature-Version': 'v2',
148
+ 'X-HubSpot-Signature-v3': expect.any(String),
149
+ ...(includeContentType && { 'content-type': 'application/json' }),
150
+ });
151
+ let savedEnv;
152
+ beforeEach(() => {
153
+ savedEnv = JSON.stringify(process.env);
154
+ process.env.CLIENT_SECRET = clientSecret;
155
+ vi.spyOn(axios, 'request');
156
+ });
157
+ afterEach(() => {
158
+ process.env = JSON.parse(savedEnv);
159
+ vi.restoreAllMocks();
160
+ });
161
+ it('should call the third party endpoint correctly for GET requests without a body', async () => {
162
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(fakeTime);
163
+ const method = 'GET';
164
+ const requestUri = 'http://localhost:1234';
165
+ const requestTimeoutMillis = 1000;
166
+ const accountId = 1;
167
+ // @ts-expect-error Typescript doesn't know this is a spy
168
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: {}, status: 200 }));
169
+ await callAppProxy({
170
+ requestUri,
171
+ method,
172
+ requestTimeoutMillis,
173
+ accountId,
174
+ logger,
175
+ });
176
+ expect(axios.request).toBeCalledWith({
177
+ url: requestUri,
178
+ method,
179
+ timeout: requestTimeoutMillis,
180
+ headers: expectedSignatureHeaders(false),
181
+ maxRedirects: 0,
182
+ });
183
+ });
184
+ it('should call the third party endpoint correctly for POST requests with a body', async () => {
185
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(fakeTime);
186
+ const method = 'POST';
187
+ const requestUri = 'http://localhost:1234';
188
+ const requestBody = { message: 'YOOOOOO' };
189
+ const requestTimeoutMillis = 1000;
190
+ const accountId = 1;
191
+ // @ts-expect-error Typescript doesn't know this is a spy
192
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: {}, status: 200 }));
193
+ await callAppProxy({
194
+ requestUri,
195
+ method,
196
+ requestTimeoutMillis,
197
+ requestBody,
198
+ accountId,
199
+ logger,
200
+ });
201
+ expect(axios.request).toBeCalledWith({
202
+ url: requestUri,
203
+ method,
204
+ timeout: requestTimeoutMillis,
205
+ data: JSON.stringify(requestBody),
206
+ headers: expectedSignatureHeaders(true),
207
+ maxRedirects: 0,
208
+ });
209
+ });
210
+ it('should not include body for GET requests even if requestBody is provided', async () => {
211
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(fakeTime);
212
+ const method = 'GET';
213
+ const requestUri = 'http://localhost:1234';
214
+ const requestBody = { message: 'YOOOOOO' };
215
+ const requestTimeoutMillis = 1000;
216
+ const accountId = 1;
217
+ // @ts-expect-error Typescript doesn't know this is a spy
218
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: {}, status: 200 }));
219
+ await callAppProxy({
220
+ requestUri,
221
+ method,
222
+ requestTimeoutMillis,
223
+ requestBody,
224
+ accountId,
225
+ logger,
226
+ });
227
+ expect(axios.request).toBeCalledWith({
228
+ url: requestUri,
229
+ method,
230
+ timeout: requestTimeoutMillis,
231
+ headers: expectedSignatureHeaders(false),
232
+ maxRedirects: 0,
233
+ });
234
+ });
235
+ it('should call getSignatureHeaders without requestBody for GET requests', async () => {
236
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(fakeTime);
237
+ const getSignatureHeadersSpy = vi.spyOn(signing, 'getSignatureHeaders');
238
+ const method = 'GET';
239
+ const requestUri = 'http://localhost:1234';
240
+ const requestBody = { message: 'YOOOOOO' };
241
+ const requestTimeoutMillis = 1000;
242
+ const accountId = 1;
243
+ // @ts-expect-error Typescript doesn't know this is a spy
244
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: {}, status: 200 }));
245
+ await callAppProxy({
246
+ requestUri,
247
+ method,
248
+ requestTimeoutMillis,
249
+ requestBody,
250
+ accountId,
251
+ logger,
252
+ });
253
+ expect(getSignatureHeadersSpy).toBeCalledWith({
254
+ method,
255
+ url: requestUri,
256
+ });
257
+ });
258
+ it('should call getSignatureHeaders with requestBody for POST requests', async () => {
259
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(fakeTime);
260
+ const getSignatureHeadersSpy = vi.spyOn(signing, 'getSignatureHeaders');
261
+ const method = 'POST';
262
+ const requestUri = 'http://localhost:1234';
263
+ const requestBody = { message: 'YOOOOOO' };
264
+ const requestTimeoutMillis = 1000;
265
+ const accountId = 1;
266
+ // @ts-expect-error Typescript doesn't know this is a spy
267
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: {}, status: 200 }));
268
+ await callAppProxy({
269
+ requestUri,
270
+ method,
271
+ requestTimeoutMillis,
272
+ requestBody,
273
+ accountId,
274
+ logger,
275
+ });
276
+ expect(getSignatureHeadersSpy).toBeCalledWith({
277
+ method,
278
+ url: requestUri,
279
+ requestBody,
280
+ });
281
+ });
282
+ it('returns success responses if the proxy endpoint is reachable', async () => {
283
+ const responseBody = { message: 'Yooooooo' };
284
+ const statusCode = 200;
285
+ const method = 'GET';
286
+ const requestUri = 'http://localhost:1234';
287
+ const accountId = 123456;
288
+ const requestTimeoutMillis = 1000;
289
+ // @ts-expect-error Typescript doesn't know this is a spy
290
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: responseBody, status: statusCode }));
291
+ const res = await callAppProxy({
292
+ requestUri,
293
+ method,
294
+ requestTimeoutMillis,
295
+ accountId,
296
+ logger,
297
+ });
298
+ expect(res.statusCode).toBe(200);
299
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
300
+ status: 'success',
301
+ responseBody,
302
+ context: expect.objectContaining({
303
+ httpMethod: [method],
304
+ appServerStatusCode: [`${statusCode}`],
305
+ portalId: [`${accountId}`],
306
+ requestUri: [requestUri],
307
+ }),
308
+ }));
309
+ });
310
+ it('should not follow 3xx redirects, mimicking the backend proxy behavior', async () => {
311
+ const responseBody = { message: 'redirecting' };
312
+ const statusCode = 302;
313
+ const method = 'GET';
314
+ const requestUri = 'http://localhost:1234';
315
+ const accountId = 123456;
316
+ const requestTimeoutMillis = 1000;
317
+ // @ts-expect-error Typescript doesn't know this is a spy
318
+ axios.request.mockImplementationOnce(() => Promise.reject(new AxiosError("this message doesn't matter", 'THIS_ERROR_DOESNT_MATTER',
319
+ // @ts-expect-error excluding properties we don't care about
320
+ {}, {}, { data: responseBody, status: statusCode })));
321
+ const res = await callAppProxy({
322
+ requestUri,
323
+ method,
324
+ requestTimeoutMillis,
325
+ accountId,
326
+ logger,
327
+ });
328
+ expect(res.statusCode).toBe(200);
329
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
330
+ category: 'BAD_GATEWAY',
331
+ message: 'Error returned from Request Uri',
332
+ status: 'error',
333
+ errors: [
334
+ {
335
+ message: `Unacceptable HTTP status ${statusCode} calling ${requestUri}`,
336
+ },
337
+ ],
338
+ context: expect.objectContaining({
339
+ correlationId: expect.any(Array),
340
+ httpMethod: [method],
341
+ appServerStatusCode: [`${statusCode}`],
342
+ portalId: [`${accountId}`],
343
+ requestUri: [requestUri],
344
+ }),
345
+ }));
346
+ });
347
+ it('returns success responses with error info in the body if the proxy returns 4xx, mimicking the backend proxy behavior', async () => {
348
+ const responseBody = { message: 'this is 404z' };
349
+ const statusCode = 404;
350
+ const method = 'GET';
351
+ const requestUri = 'http://localhost:1234';
352
+ const accountId = 494999;
353
+ const requestTimeoutMillis = 1000;
354
+ // @ts-expect-error Typescript doesn't know this is a spy
355
+ axios.request.mockImplementationOnce(() => Promise.reject(new AxiosError("this message doesn't matter", 'THIS_ERROR_DOESNT_MATTER',
356
+ // @ts-expect-error excluding properties we don't care about
357
+ {}, {}, { data: responseBody, status: statusCode })));
358
+ const res = await callAppProxy({
359
+ requestUri,
360
+ method,
361
+ requestTimeoutMillis,
362
+ accountId,
363
+ logger,
364
+ });
365
+ expect(res.statusCode).toBe(200);
366
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
367
+ category: 'BAD_GATEWAY',
368
+ message: 'Error returned from Request Uri',
369
+ status: 'error',
370
+ errors: [
371
+ {
372
+ message: `Unacceptable HTTP status ${statusCode} calling ${requestUri}`,
373
+ },
374
+ ],
375
+ context: expect.objectContaining({
376
+ correlationId: expect.any(Array),
377
+ httpMethod: [method],
378
+ appServerStatusCode: [`${statusCode}`],
379
+ portalId: [`${accountId}`],
380
+ requestUri: [requestUri],
381
+ }),
382
+ }));
383
+ });
384
+ it('returns success responses with error info in the body if the proxy returns 5xx, mimicking the backend proxy behavior', async () => {
385
+ const responseBody = { message: 'there is 500' };
386
+ const statusCode = 500;
387
+ const method = 'GET';
388
+ const requestUri = 'http://localhost:1234';
389
+ const accountId = 334545;
390
+ const requestTimeoutMillis = 1000;
391
+ // @ts-expect-error Typescript doesn't know this is a spy
392
+ axios.request.mockImplementationOnce(() => Promise.reject(new AxiosError("this message doesn't matter", 'THIS_ERROR_DOESNT_MATTER',
393
+ // @ts-expect-error excluding properties we don't care about
394
+ {}, {}, { data: responseBody, status: statusCode })));
395
+ const res = await callAppProxy({
396
+ requestUri,
397
+ method,
398
+ requestTimeoutMillis,
399
+ accountId,
400
+ logger,
401
+ });
402
+ expect(res.statusCode).toBe(200);
403
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
404
+ category: 'BAD_GATEWAY',
405
+ message: 'Error returned from Request Uri',
406
+ status: 'error',
407
+ errors: [
408
+ {
409
+ message: `Unacceptable HTTP status ${statusCode} calling ${requestUri}`,
410
+ },
411
+ ],
412
+ context: expect.objectContaining({
413
+ correlationId: expect.any(Array),
414
+ httpMethod: [method],
415
+ appServerStatusCode: [`${statusCode}`],
416
+ portalId: [`${accountId}`],
417
+ requestUri: [requestUri],
418
+ }),
419
+ }));
420
+ });
421
+ describe('with local.json mapping', () => {
422
+ const mappedUrl = 'http://localhost:1234';
423
+ const requestUri = 'https://example.com';
424
+ const requestTimeoutMillis = 1000;
425
+ const accountId = 999888777;
426
+ const requestBody = { message: 'YOOOOOO' };
427
+ const localDevUrlMapping = {
428
+ [requestUri]: mappedUrl,
429
+ };
430
+ beforeEach(() => {
431
+ vi.spyOn(Date.prototype, 'getTime').mockReturnValue(fakeTime);
432
+ // @ts-expect-error TS doesn't know this is a spy
433
+ axios.request.mockImplementationOnce(() => Promise.resolve({ data: {}, status: 200 }));
434
+ });
435
+ it('should call axios with the mapped url for GET requests without body', async () => {
436
+ const method = 'GET';
437
+ await callAppProxy({
438
+ requestUri,
439
+ method,
440
+ requestTimeoutMillis,
441
+ accountId,
442
+ localDevUrlMapping,
443
+ logger,
444
+ });
445
+ expect(axios.request).toBeCalledWith({
446
+ url: mappedUrl,
447
+ method,
448
+ timeout: requestTimeoutMillis,
449
+ headers: expectedSignatureHeaders(false),
450
+ maxRedirects: 0,
451
+ });
452
+ });
453
+ it('should call axios with body for POST requests', async () => {
454
+ const method = 'POST';
455
+ await callAppProxy({
456
+ requestUri,
457
+ method,
458
+ requestTimeoutMillis,
459
+ requestBody,
460
+ accountId,
461
+ localDevUrlMapping,
462
+ logger,
463
+ });
464
+ expect(axios.request).toBeCalledWith({
465
+ url: mappedUrl,
466
+ method,
467
+ timeout: requestTimeoutMillis,
468
+ data: JSON.stringify(requestBody),
469
+ headers: expectedSignatureHeaders(true),
470
+ maxRedirects: 0,
471
+ });
472
+ });
473
+ it('should pass the Authorization header, no matter the letter casing', async () => {
474
+ const method = 'POST';
475
+ const requestHeaders = {
476
+ auTHoriZaTion: 'a very nice bearer token',
477
+ };
478
+ await callAppProxy({
479
+ requestUri,
480
+ method,
481
+ requestTimeoutMillis,
482
+ requestBody,
483
+ requestHeaders,
484
+ accountId,
485
+ localDevUrlMapping,
486
+ logger,
487
+ });
488
+ expect(axios.request).toBeCalledWith({
489
+ url: mappedUrl,
490
+ method,
491
+ timeout: requestTimeoutMillis,
492
+ data: JSON.stringify(requestBody),
493
+ headers: {
494
+ ...requestHeaders,
495
+ ...expectedSignatureHeaders(true),
496
+ },
497
+ maxRedirects: 0,
498
+ });
499
+ });
500
+ it('should return error if more than one header is present in the request', async () => {
501
+ const method = 'POST';
502
+ const requestHeaders = {
503
+ AUTHORIZAtion: 'a very nice bearer token',
504
+ something: 'else',
505
+ };
506
+ const res = await callAppProxy({
507
+ requestUri,
508
+ method,
509
+ requestTimeoutMillis,
510
+ requestBody,
511
+ requestHeaders,
512
+ accountId,
513
+ localDevUrlMapping,
514
+ logger,
515
+ });
516
+ expect(res.statusCode).toBe(validationError.status);
517
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
518
+ category: validationError.category,
519
+ context: expect.objectContaining({
520
+ httpMethod: [method],
521
+ portalId: [`${accountId}`],
522
+ requestUri: [requestUri],
523
+ }),
524
+ message: "Only 'Authorization' header is allowed",
525
+ status: 'error',
526
+ }));
527
+ expect(axios.request).not.toBeCalled();
528
+ });
529
+ it('should return error if any header other than Authorization is present in the request', async () => {
530
+ const method = 'POST';
531
+ const requestHeaders = {
532
+ something: 'else',
533
+ };
534
+ const res = await callAppProxy({
535
+ requestUri,
536
+ method,
537
+ requestTimeoutMillis,
538
+ requestBody,
539
+ requestHeaders,
540
+ accountId,
541
+ localDevUrlMapping,
542
+ logger,
543
+ });
544
+ expect(res.statusCode).toBe(validationError.status);
545
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
546
+ category: validationError.category,
547
+ context: expect.objectContaining({
548
+ httpMethod: [method],
549
+ portalId: [`${accountId}`],
550
+ requestUri: [requestUri],
551
+ }),
552
+ message: "Invalid Request Headers provided. Only 'Authorization' header is allowed from hubspot.fetch()",
553
+ status: 'error',
554
+ }));
555
+ expect(axios.request).not.toBeCalled();
556
+ });
557
+ it('should return error if headers is not object', async () => {
558
+ const method = 'POST';
559
+ const res = await callAppProxy({
560
+ requestUri,
561
+ method,
562
+ requestTimeoutMillis,
563
+ requestBody,
564
+ // @ts-expect-error passing incorrect value on purpose
565
+ requestHeaders: 'hello',
566
+ accountId,
567
+ localDevUrlMapping,
568
+ logger,
569
+ });
570
+ expect(res.statusCode).toBe(validationError.status);
571
+ expect(res._getData()).toStrictEqual(expect.objectContaining({
572
+ category: validationError.category,
573
+ context: expect.objectContaining({
574
+ httpMethod: [method],
575
+ portalId: [`${accountId}`],
576
+ requestUri: [requestUri],
577
+ }),
578
+ message: "Only 'Authorization' header is allowed",
579
+ status: 'error',
580
+ }));
581
+ expect(axios.request).not.toBeCalled();
582
+ });
583
+ it('should work when the headers are missing for POST with body', async () => {
584
+ const method = 'POST';
585
+ await callAppProxy({
586
+ requestUri,
587
+ method,
588
+ requestTimeoutMillis,
589
+ requestBody,
590
+ accountId,
591
+ localDevUrlMapping,
592
+ logger,
593
+ });
594
+ expect(axios.request).toBeCalledWith({
595
+ url: mappedUrl,
596
+ method,
597
+ timeout: requestTimeoutMillis,
598
+ data: JSON.stringify(requestBody),
599
+ headers: expectedSignatureHeaders(true),
600
+ maxRedirects: 0,
601
+ });
602
+ });
603
+ it('should work when the headers are undefined for POST with body', async () => {
604
+ const method = 'POST';
605
+ await callAppProxy({
606
+ requestUri,
607
+ method,
608
+ requestTimeoutMillis,
609
+ requestBody,
610
+ accountId,
611
+ requestHeaders: undefined,
612
+ localDevUrlMapping,
613
+ logger,
614
+ });
615
+ expect(axios.request).toBeCalledWith({
616
+ url: mappedUrl,
617
+ method,
618
+ timeout: requestTimeoutMillis,
619
+ data: JSON.stringify(requestBody),
620
+ headers: expectedSignatureHeaders(true),
621
+ maxRedirects: 0,
622
+ });
623
+ });
624
+ it('should work when the headers are null for POST with body', async () => {
625
+ const method = 'POST';
626
+ await callAppProxy({
627
+ requestUri,
628
+ method,
629
+ requestTimeoutMillis,
630
+ requestBody,
631
+ accountId,
632
+ // @ts-expect-error passing incorrect value on purpose
633
+ requestHeaders: null,
634
+ localDevUrlMapping,
635
+ logger,
636
+ });
637
+ expect(axios.request).toBeCalledWith({
638
+ url: mappedUrl,
639
+ method,
640
+ timeout: requestTimeoutMillis,
641
+ data: JSON.stringify(requestBody),
642
+ headers: expectedSignatureHeaders(true),
643
+ maxRedirects: 0,
644
+ });
645
+ });
646
+ it('should not include body or content-type for GET requests even with requestBody provided', async () => {
647
+ const method = 'GET';
648
+ await callAppProxy({
649
+ requestUri,
650
+ method,
651
+ requestTimeoutMillis,
652
+ requestBody,
653
+ accountId,
654
+ localDevUrlMapping,
655
+ logger,
656
+ });
657
+ expect(axios.request).toBeCalledWith({
658
+ url: mappedUrl,
659
+ method,
660
+ timeout: requestTimeoutMillis,
661
+ headers: expectedSignatureHeaders(false),
662
+ maxRedirects: 0,
663
+ });
664
+ });
665
+ });
666
+ });
667
+ });