@hubspot/ui-extensions-dev-server 1.1.6 → 1.1.7
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.
- package/dist/lib/DevServerState.d.ts +1 -1
- package/dist/lib/ExtensionsWebSocket.js +26 -2
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +42 -8
- package/dist/lib/__tests__/app-functions/context.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/context.spec.js +101 -0
- package/dist/lib/__tests__/app-functions/errorReporter.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/errorReporter.spec.js +102 -0
- package/dist/lib/__tests__/app-functions/executor_v20231.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/executor_v20231.spec.js +168 -0
- package/dist/lib/__tests__/app-functions/executor_v20232.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/executor_v20232.spec.js +190 -0
- package/dist/lib/__tests__/app-functions/fixtures/constants.d.ts +18 -0
- package/dist/lib/__tests__/app-functions/fixtures/constants.js +139 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-fails.cjs +8 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-fails.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-succeeds.cjs +8 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-async-succeeds.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-rejected.cjs +8 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-rejected.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-resolved.cjs +8 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-callback-on-promise-resolved.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-does-not-export-main.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-does-not-export-main.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-echos-input.cjs +8 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-echos-input.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-logs.cjs +10 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-logs.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-function.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-function.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-rejected.cjs +7 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-rejected.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-resolved.cjs +7 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-promise-resolved.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-text.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-text.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-undefined.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-returns-undefined.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-throws-error.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-throws-error.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-times-out.cjs +10 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-times-out.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-undeclared.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-undeclared.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-uses-secret.cjs +14 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.1/app.functions/func-uses-secret.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-fails.cjs +5 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-fails.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-succeeds.cjs +5 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-async-succeeds.d.cts +3 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-calls-callback.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-calls-callback.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-does-not-export-main.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-does-not-export-main.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-echos-input.cjs +8 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-echos-input.d.cts +5 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-logs.cjs +10 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-logs.d.cts +3 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-function.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-function.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-implicitly.cjs +7 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-implicitly.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-rejected.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-rejected.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-resolved.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-promise-resolved.d.cts +3 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-text.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-text.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-undefined.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-returns-undefined.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-throws-error.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-throws-error.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-times-out.cjs +12 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-times-out.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-undeclared.cjs +4 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-undeclared.d.cts +1 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-uses-secret.cjs +14 -0
- package/dist/lib/__tests__/app-functions/fixtures/v2023.2/app.functions/func-uses-secret.d.cts +4 -0
- package/dist/lib/__tests__/app-functions/secrets.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/secrets.spec.js +278 -0
- package/dist/lib/__tests__/app-functions/services/AppProxyService.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/services/AppProxyService.spec.js +667 -0
- package/dist/lib/__tests__/app-functions/services/PrivateAppUserTokenManager.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/services/PrivateAppUserTokenManager.spec.js +243 -0
- package/dist/lib/__tests__/app-functions/services/services_v20231.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/services/services_v20231.spec.js +319 -0
- package/dist/lib/__tests__/app-functions/services/services_v20232.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/services/services_v20232.spec.js +302 -0
- package/dist/lib/__tests__/app-functions/setup.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/setup.js +7 -0
- package/dist/lib/__tests__/app-functions/signing.spec.d.ts +1 -0
- package/dist/lib/__tests__/app-functions/signing.spec.js +460 -0
- package/dist/lib/__tests__/server.spec.js +24 -2
- package/dist/lib/app-functions/api/privateAppUserToken.d.ts +16 -0
- package/dist/lib/app-functions/api/privateAppUserToken.js +28 -0
- package/dist/lib/app-functions/config.d.ts +4 -0
- package/dist/lib/app-functions/config.js +48 -0
- package/dist/lib/app-functions/constants.d.ts +26 -0
- package/dist/lib/app-functions/constants.js +63 -0
- package/dist/lib/app-functions/context.d.ts +3 -0
- package/dist/lib/app-functions/context.js +65 -0
- package/dist/lib/app-functions/errorReporter.d.ts +22 -0
- package/dist/lib/app-functions/errorReporter.js +42 -0
- package/dist/lib/app-functions/errors.d.ts +44 -0
- package/dist/lib/app-functions/errors.js +82 -0
- package/dist/lib/app-functions/executor.d.ts +3 -0
- package/dist/lib/app-functions/executor.js +131 -0
- package/dist/lib/app-functions/index.d.ts +4 -0
- package/dist/lib/app-functions/index.js +4 -0
- package/dist/lib/app-functions/secrets.d.ts +5 -0
- package/dist/lib/app-functions/secrets.js +55 -0
- package/dist/lib/app-functions/services/AppFunctionExecutionService.d.ts +2 -0
- package/dist/lib/app-functions/services/AppFunctionExecutionService.js +55 -0
- package/dist/lib/app-functions/services/AppProxyService.d.ts +5 -0
- package/dist/lib/app-functions/services/AppProxyService.js +196 -0
- package/dist/lib/app-functions/services/PrivateAppUserTokenManager.d.ts +22 -0
- package/dist/lib/app-functions/services/PrivateAppUserTokenManager.js +185 -0
- package/dist/lib/app-functions/services/constants.d.ts +4 -0
- package/dist/lib/app-functions/services/constants.js +4 -0
- package/dist/lib/app-functions/services/index.d.ts +3 -0
- package/dist/lib/app-functions/services/index.js +3 -0
- package/dist/lib/app-functions/services/messages.d.ts +14 -0
- package/dist/lib/app-functions/services/messages.js +36 -0
- package/dist/lib/app-functions/signing.d.ts +29 -0
- package/dist/lib/app-functions/signing.js +51 -0
- package/dist/lib/app-functions/types.d.ts +172 -0
- package/dist/lib/app-functions/types.js +6 -0
- package/dist/lib/app-functions/utils.d.ts +15 -0
- package/dist/lib/app-functions/utils.js +28 -0
- package/dist/lib/server.js +15 -4
- package/dist/lib/types.d.ts +1 -1
- package/package.json +9 -6
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, afterEach, it, expect, } from 'vitest';
|
|
2
|
+
import { scopesOnAccessToken as __scopesOnAccessToken } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
3
|
+
import { PrivateAppUserTokenManager } from "../../../app-functions/services/PrivateAppUserTokenManager.js";
|
|
4
|
+
import { fetchPrivateAppUserToken as __fetchPrivateAppUserToken, createPrivateAppUserToken as __createPrivateAppUserToken, updatePrivateAppUserToken as __updatePrivateAppUserToken, } from "../../../app-functions/api/privateAppUserToken.js";
|
|
5
|
+
import { HubSpotHttpError } from '@hubspot/local-dev-lib/models/HubSpotHttpError';
|
|
6
|
+
import { AxiosError, AxiosHeaders } from 'axios';
|
|
7
|
+
import { generateTokensEnableMessage, generateMissingScopesMessage, generateMissingScopesMoreContextMessage, } from "../../../app-functions/services/messages.js";
|
|
8
|
+
vi.mock('@hubspot/local-dev-lib/personalAccessKey');
|
|
9
|
+
vi.mock('../../../app-functions/api/privateAppUserToken.ts');
|
|
10
|
+
const scopesOnAccessToken = __scopesOnAccessToken;
|
|
11
|
+
const fetchPrivateAppUserToken = __fetchPrivateAppUserToken;
|
|
12
|
+
const createPrivateAppUserToken = __createPrivateAppUserToken;
|
|
13
|
+
const updatePrivateAppUserToken = __updatePrivateAppUserToken;
|
|
14
|
+
function mockAxiosResponse(data, status = 200, statusText = 'OK', headers = new AxiosHeaders(), config = {}) {
|
|
15
|
+
// Type cast needed due to AxiosHeaders type conflict between local axios@1.12.0 and @hubspot/local-dev-lib's axios dependency
|
|
16
|
+
// The runtime behavior is correct, just incompatible AxiosHeaders types between different axios versions
|
|
17
|
+
return {
|
|
18
|
+
data,
|
|
19
|
+
status,
|
|
20
|
+
statusText,
|
|
21
|
+
headers,
|
|
22
|
+
config: { ...config, headers },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
describe('lib/PrivateAppUserTokenManager', () => {
|
|
26
|
+
const accountId = 123;
|
|
27
|
+
const pakScopesTokenEnabled = [
|
|
28
|
+
'developer.private_app.temporary_token.read',
|
|
29
|
+
'developer.private_app.temporary_token.write',
|
|
30
|
+
];
|
|
31
|
+
const pakScopesTokenDisabled = ['developer.projects.read'];
|
|
32
|
+
let manager;
|
|
33
|
+
let logger;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
logger = {
|
|
36
|
+
error: vi.fn(),
|
|
37
|
+
debug: vi.fn(),
|
|
38
|
+
info: vi.fn(),
|
|
39
|
+
warn: vi.fn(),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
describe('init()', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
manager = new PrivateAppUserTokenManager(accountId, logger);
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
manager.cleanup();
|
|
48
|
+
});
|
|
49
|
+
it('should enable PrivateAppUserTokenManager when personal access key has all scopes', async () => {
|
|
50
|
+
scopesOnAccessToken.mockResolvedValue(pakScopesTokenEnabled);
|
|
51
|
+
await manager.init();
|
|
52
|
+
expect(manager.isEnabled()).toBe(true);
|
|
53
|
+
// logs the proper messages info. there's no warn/devbug
|
|
54
|
+
expect(logger.info).toHaveBeenCalledWith(generateTokensEnableMessage(accountId));
|
|
55
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
56
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
it('should not enable PrivateAppUserTokenManager if the personal access key does not contain scopes', async () => {
|
|
59
|
+
scopesOnAccessToken.mockResolvedValue(pakScopesTokenDisabled);
|
|
60
|
+
await manager.init();
|
|
61
|
+
expect(manager.isEnabled()).toBe(false);
|
|
62
|
+
// logs the proper info and debug messages. there is no warn
|
|
63
|
+
expect(logger.info).toHaveBeenCalledWith(generateMissingScopesMessage(accountId));
|
|
64
|
+
expect(logger.debug).toHaveBeenCalledWith(generateMissingScopesMoreContextMessage(accountId));
|
|
65
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('getPrivateAppUserToken()', () => {
|
|
69
|
+
const systemTime = new Date(Date.UTC(2024, 5, 1, 0, 0, 0));
|
|
70
|
+
const token = {
|
|
71
|
+
userId: 111,
|
|
72
|
+
portalId: 123,
|
|
73
|
+
appId: 345,
|
|
74
|
+
scopeGroups: ['crm.objects.contacts.read'],
|
|
75
|
+
userTokenKey: 'pat-na1-u-FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF',
|
|
76
|
+
clientId: 'my-client-id',
|
|
77
|
+
expiresAt: '2024-06-01T01:00:00Z',
|
|
78
|
+
};
|
|
79
|
+
const tokenWithMoreScopes = {
|
|
80
|
+
...token,
|
|
81
|
+
scopeGroups: ['crm.objects.contacts.read', 'crm.objects.contacts.write'],
|
|
82
|
+
};
|
|
83
|
+
const refreshedToken = { ...token, expiresAt: '2024-06-01T02:00:00Z' };
|
|
84
|
+
const expiredToken = { ...token, expiresAt: '2024-05-30T00:00:00Z' };
|
|
85
|
+
const headers = new AxiosHeaders();
|
|
86
|
+
const config = { url: 'https://api.google.com', headers };
|
|
87
|
+
const notFoundError = new HubSpotHttpError('Not Found', {
|
|
88
|
+
cause: new AxiosError('msg', 'code', config, undefined, {
|
|
89
|
+
status: 404,
|
|
90
|
+
data: { message: 'Not Found' },
|
|
91
|
+
statusText: 'Not Found',
|
|
92
|
+
config,
|
|
93
|
+
headers,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vi.resetAllMocks();
|
|
98
|
+
scopesOnAccessToken.mockResolvedValue(pakScopesTokenEnabled);
|
|
99
|
+
vi.spyOn(global, 'setInterval');
|
|
100
|
+
vi.useFakeTimers();
|
|
101
|
+
vi.setSystemTime(systemTime);
|
|
102
|
+
manager = new PrivateAppUserTokenManager(accountId, logger);
|
|
103
|
+
manager.init();
|
|
104
|
+
});
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
manager.cleanup();
|
|
107
|
+
vi.clearAllTimers();
|
|
108
|
+
});
|
|
109
|
+
it('should return undefined if class is not initilized', async () => {
|
|
110
|
+
manager = new PrivateAppUserTokenManager(accountId, logger); // set to new instance not initialized
|
|
111
|
+
const result = await manager.getPrivateAppUserToken(accountId);
|
|
112
|
+
expect(result).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
it('should return undefined if user does not have the scopes', async () => {
|
|
115
|
+
scopesOnAccessToken.mockResolvedValue(pakScopesTokenDisabled);
|
|
116
|
+
manager = new PrivateAppUserTokenManager(accountId, logger); // set to new instance not initialized
|
|
117
|
+
await manager.init();
|
|
118
|
+
const result = await manager.getPrivateAppUserToken(accountId);
|
|
119
|
+
expect(result).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
it('should fetch a existing valid Private App User Token', async () => {
|
|
122
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
123
|
+
const result = await manager.getPrivateAppUserToken(token.appId);
|
|
124
|
+
expect(result?.userTokenKey).toEqual(token.userTokenKey);
|
|
125
|
+
});
|
|
126
|
+
it('should used cached Private App User Token', async () => {
|
|
127
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
128
|
+
await manager.getPrivateAppUserToken(token.appId);
|
|
129
|
+
const result = await manager.getPrivateAppUserToken(token.appId);
|
|
130
|
+
expect(result?.userTokenKey).toEqual(token.userTokenKey);
|
|
131
|
+
expect(fetchPrivateAppUserToken).toHaveBeenCalledTimes(1);
|
|
132
|
+
});
|
|
133
|
+
it('should refresh existing expired Private App User Token', async () => {
|
|
134
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(expiredToken));
|
|
135
|
+
updatePrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
136
|
+
const result = await manager.getPrivateAppUserToken(token.appId);
|
|
137
|
+
expect(result?.userTokenKey).toEqual(token.userTokenKey);
|
|
138
|
+
expect(updatePrivateAppUserToken).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(updatePrivateAppUserToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
140
|
+
expiresAt: '2024-06-01T00:15:00.000Z',
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
143
|
+
it('should create a new Private App User Token if none exist', async () => {
|
|
144
|
+
fetchPrivateAppUserToken.mockRejectedValue(notFoundError);
|
|
145
|
+
createPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token, 200));
|
|
146
|
+
const result = await manager.getPrivateAppUserToken(token.appId);
|
|
147
|
+
expect(result?.userTokenKey).toEqual(token.userTokenKey);
|
|
148
|
+
expect(createPrivateAppUserToken).toHaveBeenCalledTimes(1);
|
|
149
|
+
expect(createPrivateAppUserToken).toHaveBeenCalledWith(expect.objectContaining({
|
|
150
|
+
expiresAt: '2024-06-01T00:15:00.000Z',
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
it('should refresh cached token if it is within the threashold and user request a new token', async () => {
|
|
154
|
+
// token expires in 1 hour. try 56 minutes to test threadhold
|
|
155
|
+
const fiftyMinutesMilli = 56 * 60 * 1000;
|
|
156
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
157
|
+
updatePrivateAppUserToken.mockResolvedValue(mockAxiosResponse(refreshedToken));
|
|
158
|
+
const tokenResp = await manager.getPrivateAppUserToken(token.appId);
|
|
159
|
+
expect(tokenResp?.userTokenKey).toEqual(token.userTokenKey);
|
|
160
|
+
vi.advanceTimersByTime(fiftyMinutesMilli);
|
|
161
|
+
await manager.getPrivateAppUserToken(token.appId);
|
|
162
|
+
expect(updatePrivateAppUserToken).toHaveBeenCalledTimes(1);
|
|
163
|
+
});
|
|
164
|
+
it('should not refresh token is still valid', async () => {
|
|
165
|
+
// token expires in 1 hour. try 10 minutes to test threadhold
|
|
166
|
+
const tenMinutesMilli = 10 * 60 * 1000;
|
|
167
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
168
|
+
updatePrivateAppUserToken.mockResolvedValue(mockAxiosResponse(refreshedToken));
|
|
169
|
+
const tokenResp = await manager.getPrivateAppUserToken(token.appId);
|
|
170
|
+
expect(tokenResp?.userTokenKey).toEqual(token.userTokenKey);
|
|
171
|
+
vi.advanceTimersByTime(tenMinutesMilli);
|
|
172
|
+
await manager.getPrivateAppUserToken(token.appId);
|
|
173
|
+
expect(updatePrivateAppUserToken).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
it('should update token is missing scopes', async () => {
|
|
176
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
177
|
+
updatePrivateAppUserToken.mockResolvedValue(mockAxiosResponse(tokenWithMoreScopes));
|
|
178
|
+
await manager.getPrivateAppUserToken(token.appId);
|
|
179
|
+
const tokenResp = await manager.getPrivateAppUserToken(token.appId, {
|
|
180
|
+
scopeGroups: tokenWithMoreScopes.scopeGroups,
|
|
181
|
+
});
|
|
182
|
+
expect(tokenResp?.userTokenKey).toEqual(token.userTokenKey);
|
|
183
|
+
expect(fetchPrivateAppUserToken).toHaveBeenCalledTimes(2); // initial get, and then fetches again when scopes don't match in cached token
|
|
184
|
+
expect(updatePrivateAppUserToken).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
it('should clear token refresh if cleanup is called', async () => {
|
|
187
|
+
fetchPrivateAppUserToken.mockResolvedValue(mockAxiosResponse(token));
|
|
188
|
+
await manager.getPrivateAppUserToken(token.appId);
|
|
189
|
+
manager.cleanup();
|
|
190
|
+
vi.advanceTimersByTime(60 * 60 * 1000);
|
|
191
|
+
expect(updatePrivateAppUserToken).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('doesUserTokenContainAppTokenScopes', () => {
|
|
195
|
+
const tokenWithMatchingScopes = {
|
|
196
|
+
userId: 111,
|
|
197
|
+
portalId: 123,
|
|
198
|
+
appId: 345,
|
|
199
|
+
scopeGroups: ['crm.objects.contacts.read', 'crm.objects.contacts.write'],
|
|
200
|
+
userTokenKey: 'pat-na1-u-FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF',
|
|
201
|
+
clientId: 'my-client-id',
|
|
202
|
+
expiresAt: '2024-06-01T01:00:00Z',
|
|
203
|
+
privateAppTokenInfo: {
|
|
204
|
+
userId: 111,
|
|
205
|
+
portalId: 123,
|
|
206
|
+
appId: 345,
|
|
207
|
+
scopeGroups: [
|
|
208
|
+
'crm.objects.contacts.read',
|
|
209
|
+
'crm.objects.contacts.write',
|
|
210
|
+
],
|
|
211
|
+
expiresAt: '2024-06-01T01:00:00Z',
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
it('should return true if the scopes match', () => {
|
|
215
|
+
expect(PrivateAppUserTokenManager.doesUserTokenContainAppTokenScopes(tokenWithMatchingScopes)).toBeTruthy();
|
|
216
|
+
});
|
|
217
|
+
it('should return false there is no PrivateAppToken', () => {
|
|
218
|
+
const localToken = {
|
|
219
|
+
...tokenWithMatchingScopes,
|
|
220
|
+
privateAppTokenInfo: undefined,
|
|
221
|
+
};
|
|
222
|
+
expect(PrivateAppUserTokenManager.doesUserTokenContainAppTokenScopes(localToken)).toBeFalsy();
|
|
223
|
+
});
|
|
224
|
+
it('should return false if PrivateAppUserToken has extra scopes that are not in PrivateAppToken', () => {
|
|
225
|
+
const localToken = {
|
|
226
|
+
...tokenWithMatchingScopes,
|
|
227
|
+
scopeGroups: [
|
|
228
|
+
'crm.objects.contacts.read',
|
|
229
|
+
'crm.objects.contacts.write',
|
|
230
|
+
'crm.objects.deals.write',
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
expect(PrivateAppUserTokenManager.doesUserTokenContainAppTokenScopes(localToken)).toBeTruthy();
|
|
234
|
+
});
|
|
235
|
+
it('should return true if PrivateAppUserToken is missing scopes', () => {
|
|
236
|
+
const localToken = {
|
|
237
|
+
...tokenWithMatchingScopes,
|
|
238
|
+
scopeGroups: ['crm.objects.contacts.write'],
|
|
239
|
+
};
|
|
240
|
+
expect(PrivateAppUserTokenManager.doesUserTokenContainAppTokenScopes(localToken)).toBeFalsy();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach, } from 'vitest';
|
|
2
|
+
import httpMocks from 'node-mocks-http';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { AppFunctionExecutionService } from "../../../app-functions/services/index.js";
|
|
5
|
+
import { TEST_CONFIG_V20231 as TEST_CONFIG } from "../fixtures/constants.js";
|
|
6
|
+
import * as executor from "../../../app-functions/executor.js";
|
|
7
|
+
import { scopesOnAccessToken as __scopesOnAccessToken } from '@hubspot/local-dev-lib/personalAccessKey';
|
|
8
|
+
import { USER_TOKEN_READ, USER_TOKEN_WRITE, } from "../../../app-functions/services/constants.js";
|
|
9
|
+
import { PrivateAppUserTokenManager } from "../../../app-functions/services/PrivateAppUserTokenManager.js";
|
|
10
|
+
vi.mock('../../../app-functions/services/PrivateAppUserTokenManager.ts');
|
|
11
|
+
vi.mock('@hubspot/local-dev-lib/personalAccessKey');
|
|
12
|
+
const scopesOnAccessToken = __scopesOnAccessToken;
|
|
13
|
+
const callAppFunction = async (functionName, parameters) => {
|
|
14
|
+
const request = httpMocks.createRequest({
|
|
15
|
+
method: 'POST',
|
|
16
|
+
url: '/action/function/100',
|
|
17
|
+
params: {
|
|
18
|
+
id: 100,
|
|
19
|
+
},
|
|
20
|
+
body: {
|
|
21
|
+
serverlessFunction: functionName,
|
|
22
|
+
parameters,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const response = httpMocks.createResponse({
|
|
26
|
+
req: request,
|
|
27
|
+
eventEmitter: EventEmitter,
|
|
28
|
+
});
|
|
29
|
+
const handler = AppFunctionExecutionService(TEST_CONFIG);
|
|
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
|
+
/**
|
|
41
|
+
* Validate the service for executing app functions
|
|
42
|
+
*/
|
|
43
|
+
describe('app function dev server', () => {
|
|
44
|
+
const initialEnvJson = JSON.stringify(process.env);
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
scopesOnAccessToken.mockResolvedValue([USER_TOKEN_READ, USER_TOKEN_WRITE]);
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
// restore process.env
|
|
50
|
+
process.env = JSON.parse(initialEnvJson);
|
|
51
|
+
vi.resetAllMocks();
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
it('returns "200 OK" response if function execution succeeded', async () => {
|
|
55
|
+
const getTokenspy = vi.spyOn(PrivateAppUserTokenManager.prototype, 'getPrivateAppUserToken');
|
|
56
|
+
const response = await callAppFunction('returns-text');
|
|
57
|
+
// Validate response status and body
|
|
58
|
+
expect(response.statusCode).toEqual(200);
|
|
59
|
+
expect(response._getJSONData()).toEqual({
|
|
60
|
+
logId: 'n/a',
|
|
61
|
+
response: 'result',
|
|
62
|
+
});
|
|
63
|
+
// Validate log output
|
|
64
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledWith(expect.stringContaining('App function "returns-text" execution succeeded'));
|
|
65
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
66
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
67
|
+
// Validate process.env is put back to what it was before the call
|
|
68
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
69
|
+
// get the token
|
|
70
|
+
expect(getTokenspy).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
it('returns "400 Bad Request" response if function execution failed', async () => {
|
|
73
|
+
const response = await callAppFunction('throws-error');
|
|
74
|
+
// Validate response status and body
|
|
75
|
+
expect(response.statusCode).toEqual(400);
|
|
76
|
+
const body = response._getJSONData();
|
|
77
|
+
expect(body).toHaveProperty('status', 'error');
|
|
78
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
79
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
80
|
+
expect(exception).toEqual('Error: Oops');
|
|
81
|
+
// Validate log output
|
|
82
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "throws-error" execution failed'));
|
|
83
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith('App function encountered an uncaught error.', expect.anything());
|
|
84
|
+
// Validate process.env is put back to what it was before the call
|
|
85
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
86
|
+
});
|
|
87
|
+
describe('handles asynchronous functions (promise chaining)', () => {
|
|
88
|
+
it('returns "200 OK" response if function returns a promise that resolves', async () => {
|
|
89
|
+
const response = await callAppFunction('returns-promise-resolved');
|
|
90
|
+
// Validate response status and body
|
|
91
|
+
expect(response.statusCode).toEqual(200);
|
|
92
|
+
expect(response._getJSONData()).toEqual({
|
|
93
|
+
logId: 'n/a',
|
|
94
|
+
response: { result: 'simulated' },
|
|
95
|
+
});
|
|
96
|
+
// Validate log output
|
|
97
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledWith(expect.stringContaining('App function "returns-promise-resolved" execution succeeded'));
|
|
98
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
99
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
100
|
+
// Validate process.env is put back to what it was before the call
|
|
101
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
102
|
+
});
|
|
103
|
+
it('returns "400 Bad Request" response if function returns a promise that rejects', async () => {
|
|
104
|
+
const response = await callAppFunction('returns-promise-rejected');
|
|
105
|
+
// Validate response status and body
|
|
106
|
+
expect(response.statusCode).toEqual(400);
|
|
107
|
+
const body = response._getJSONData();
|
|
108
|
+
expect(body).toHaveProperty('status', 'error');
|
|
109
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
110
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
111
|
+
expect(exception).toEqual('Error: fail');
|
|
112
|
+
// Validate log output
|
|
113
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "returns-promise-rejected" execution failed'));
|
|
114
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith('App function encountered an uncaught error.', expect.anything());
|
|
115
|
+
// Validate process.env is put back to what it was before the call
|
|
116
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('handles asynchronous functions (async/await)', () => {
|
|
120
|
+
it('returns "200 OK" response if an async function execution succeeded', async () => {
|
|
121
|
+
const response = await callAppFunction('async-succeeds');
|
|
122
|
+
// Validate response status and body
|
|
123
|
+
expect(response.statusCode).toEqual(200);
|
|
124
|
+
expect(response._getJSONData()).toEqual({
|
|
125
|
+
logId: 'n/a',
|
|
126
|
+
response: { result: 'simulated' },
|
|
127
|
+
});
|
|
128
|
+
// Validate log output
|
|
129
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledWith(expect.stringContaining('App function "async-succeeds" execution succeeded'));
|
|
130
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
131
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
132
|
+
// Validate process.env is put back to what it was before the call
|
|
133
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
134
|
+
});
|
|
135
|
+
it('returns "400 Bad Request" response if an async function execution failed', async () => {
|
|
136
|
+
const response = await callAppFunction('async-fails');
|
|
137
|
+
// Validate response status and body
|
|
138
|
+
expect(response.statusCode).toEqual(400);
|
|
139
|
+
const body = response._getJSONData();
|
|
140
|
+
expect(body).toHaveProperty('status', 'error');
|
|
141
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
142
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
143
|
+
expect(exception).toEqual('Error: fail');
|
|
144
|
+
// Validate log output
|
|
145
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "async-fails" execution failed'));
|
|
146
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith('App function encountered an uncaught error.', expect.anything());
|
|
147
|
+
// Validate process.env is put back to what it was before the call
|
|
148
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('handles functions with floating promises (only 2023.1)', () => {
|
|
152
|
+
it('returns "200 OK" response if function makes a promise that resolves and calls callback', async () => {
|
|
153
|
+
const response = await callAppFunction('callback-on-promise-resolved');
|
|
154
|
+
// Validate response status and body
|
|
155
|
+
expect(response.statusCode).toEqual(200);
|
|
156
|
+
expect(response._getJSONData()).toEqual({
|
|
157
|
+
logId: 'n/a',
|
|
158
|
+
response: { result: 'simulated' },
|
|
159
|
+
});
|
|
160
|
+
// Validate log output
|
|
161
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledWith(expect.stringContaining('App function "callback-on-promise-resolved" execution succeeded'));
|
|
162
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
163
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
164
|
+
// Validate process.env is put back to what it was before the call
|
|
165
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
166
|
+
});
|
|
167
|
+
it('returns "200 OK" response if function makes a promise that rejects and calls callback', async () => {
|
|
168
|
+
const response = await callAppFunction('callback-on-promise-rejected');
|
|
169
|
+
// Validate response status and body
|
|
170
|
+
expect(response.statusCode).toEqual(200);
|
|
171
|
+
expect(response._getJSONData()).toEqual({
|
|
172
|
+
logId: 'n/a',
|
|
173
|
+
response: { error: 'Error: fail' },
|
|
174
|
+
});
|
|
175
|
+
// Validate log output
|
|
176
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledWith(expect.stringContaining('App function "callback-on-promise-rejected" execution succeeded'));
|
|
177
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
178
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
179
|
+
// Validate process.env is put back to what it was before the call
|
|
180
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
it('returns "400 Bad Request" response if function not found', async () => {
|
|
184
|
+
const response = await callAppFunction('does-not-exist');
|
|
185
|
+
// Validate response status and body
|
|
186
|
+
expect(response.statusCode).toEqual(400);
|
|
187
|
+
const body = response._getJSONData();
|
|
188
|
+
expect(body).toHaveProperty('status', 'error');
|
|
189
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
190
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
191
|
+
expect(exception).toMatch(/doesn't exist in this project/);
|
|
192
|
+
// Validate log output
|
|
193
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "does-not-exist" execution failed'));
|
|
194
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith(expect.stringContaining('Could not find file'));
|
|
195
|
+
// Validate process.env is put back to what it was before the call
|
|
196
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
197
|
+
});
|
|
198
|
+
it('returns "400 Bad Request" response if function does not export main', async () => {
|
|
199
|
+
// TEST_CONFIG.functionTimeoutMs = 250
|
|
200
|
+
const response = await callAppFunction('does-not-export-main');
|
|
201
|
+
// Validate response status and body
|
|
202
|
+
expect(response.statusCode).toEqual(400);
|
|
203
|
+
const body = response._getJSONData();
|
|
204
|
+
expect(body).toHaveProperty('status', 'error');
|
|
205
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
206
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
207
|
+
expect(exception).toMatch(/customerPayload\.main is not a function/);
|
|
208
|
+
// Validate log output
|
|
209
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "does-not-export-main" execution failed'));
|
|
210
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith(expect.stringContaining('Could not find "main" export in'));
|
|
211
|
+
// Validate process.env is put back to what it was before the call
|
|
212
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
213
|
+
});
|
|
214
|
+
it('returns "400 Bad Request" response if function returns invalid json', async () => {
|
|
215
|
+
// TEST_CONFIG.functionTimeoutMs = 250
|
|
216
|
+
const response = await callAppFunction('returns-function');
|
|
217
|
+
// Validate response status and body
|
|
218
|
+
expect(response.statusCode).toEqual(400);
|
|
219
|
+
const body = response._getJSONData();
|
|
220
|
+
expect(body).toHaveProperty('status', 'error');
|
|
221
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
222
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
223
|
+
expect(exception).toMatch(/Wrong arguments/);
|
|
224
|
+
// Validate log output
|
|
225
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "returns-function" execution failed'));
|
|
226
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith('App function reponse is not valid JSON.');
|
|
227
|
+
// Validate process.env is put back to what it was before the call
|
|
228
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
229
|
+
});
|
|
230
|
+
it('returns "200 OK" response if function returns before timeout', async () => {
|
|
231
|
+
// TEST_CONFIG.functionTimeoutMs = 250
|
|
232
|
+
const response = await callAppFunction('times-out', { delayMs: 200 });
|
|
233
|
+
const variance = 5; // milliseconds
|
|
234
|
+
// Validate response status and body
|
|
235
|
+
expect(response.statusCode).toEqual(200);
|
|
236
|
+
const body = response._getJSONData();
|
|
237
|
+
expect(body).toEqual({
|
|
238
|
+
logId: 'n/a',
|
|
239
|
+
response: { status: 'success', elapsedMs: expect.anything() },
|
|
240
|
+
});
|
|
241
|
+
expect(body.response.elapsedMs).toBeGreaterThanOrEqual(200 - variance);
|
|
242
|
+
expect(body.response.elapsedMs).toBeLessThanOrEqual(200 + variance);
|
|
243
|
+
// Validate log output
|
|
244
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledWith(expect.stringContaining('App function "times-out" execution succeeded'));
|
|
245
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
246
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
247
|
+
// Validate process.env is put back to what it was before the call
|
|
248
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
249
|
+
});
|
|
250
|
+
it('returns "400 Bad Request" response if function returns after timeout', async () => {
|
|
251
|
+
// TEST_CONFIG.functionTimeoutMs = 250
|
|
252
|
+
const response = await callAppFunction('times-out', { delayMs: 300 });
|
|
253
|
+
// Validate response status and body
|
|
254
|
+
expect(response.statusCode).toEqual(400);
|
|
255
|
+
const body = response._getJSONData();
|
|
256
|
+
expect(body).toHaveProperty('status', 'error');
|
|
257
|
+
expect(body).toHaveProperty('category', 'VALIDATION_ERROR');
|
|
258
|
+
const exception = body.errors?.[0]?.context?.exception?.[0];
|
|
259
|
+
expect(exception).toMatch(/Task timed out after/);
|
|
260
|
+
// Validate log output
|
|
261
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "times-out" execution failed'));
|
|
262
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith('App function failed to callback within 0.25 second.');
|
|
263
|
+
// Validate process.env is put back to what it was before the call
|
|
264
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
265
|
+
});
|
|
266
|
+
it('returns "500 Internal Server Error" response if something went wrong', async () => {
|
|
267
|
+
const error = new Error('Something went terribly wrong!');
|
|
268
|
+
vi.spyOn(executor, 'executeFunction').mockImplementation(() => {
|
|
269
|
+
throw error;
|
|
270
|
+
});
|
|
271
|
+
// Act
|
|
272
|
+
const response = await callAppFunction('returns-text');
|
|
273
|
+
// Validate response status and body
|
|
274
|
+
expect(response.statusCode).toEqual(500);
|
|
275
|
+
const body = response._getJSONData();
|
|
276
|
+
expect(body).toHaveProperty('status', 'error');
|
|
277
|
+
expect(body).not.toHaveProperty('category', 'INTERNAL_ERROR');
|
|
278
|
+
expect(body).toHaveProperty('message', 'internal error');
|
|
279
|
+
// Validate log output
|
|
280
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledWith(expect.stringContaining('App function "returns-text" execution failed due to server internal error'));
|
|
281
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledWith(error);
|
|
282
|
+
// Validate process.env is put back to what it was before the call
|
|
283
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
284
|
+
});
|
|
285
|
+
it('logs to console from inside the function', async () => {
|
|
286
|
+
const log = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
287
|
+
const debug = vi.spyOn(console, 'debug').mockImplementation(() => { });
|
|
288
|
+
const info = vi.spyOn(console, 'info').mockImplementation(() => { });
|
|
289
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
290
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
291
|
+
// Act
|
|
292
|
+
const response = await callAppFunction('logs');
|
|
293
|
+
// Validate response status and body
|
|
294
|
+
expect(response.statusCode).toEqual(200);
|
|
295
|
+
expect(response._getJSONData()).toEqual({
|
|
296
|
+
logId: 'n/a',
|
|
297
|
+
response: { status: 'success' },
|
|
298
|
+
});
|
|
299
|
+
// Validate function logging
|
|
300
|
+
expect(log).toHaveBeenCalledTimes(1);
|
|
301
|
+
expect(log).toHaveBeenCalledWith('log line');
|
|
302
|
+
expect(debug).toHaveBeenCalledTimes(1);
|
|
303
|
+
expect(debug).toHaveBeenCalledWith('debug line');
|
|
304
|
+
expect(info).toHaveBeenCalledTimes(2);
|
|
305
|
+
expect(info).toHaveBeenNthCalledWith(1, 'info line');
|
|
306
|
+
expect(info).toHaveBeenNthCalledWith(2, 'print data:', [1, 2]);
|
|
307
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
308
|
+
expect(warn).toHaveBeenCalledWith('warn line');
|
|
309
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
310
|
+
expect(error).toHaveBeenCalledWith('error line');
|
|
311
|
+
// Validate service logging, which is separate from function logging
|
|
312
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenCalledTimes(1);
|
|
313
|
+
expect(TEST_CONFIG.logger.debug).toHaveBeenNthCalledWith(1, expect.stringContaining('App function "logs" execution succeeded'));
|
|
314
|
+
expect(TEST_CONFIG.logger.warn).toHaveBeenCalledTimes(0);
|
|
315
|
+
expect(TEST_CONFIG.logger.error).toHaveBeenCalledTimes(0);
|
|
316
|
+
// Validate process.env is put back to what it was before the call
|
|
317
|
+
expect(process.env).toEqual(JSON.parse(initialEnvJson));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|