@app-connect/core 1.7.35 → 1.7.37
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/connector/mock.js +9 -4
- package/connector/proxy/engine.js +3 -2
- package/connector/proxy/index.js +2 -2
- package/docs/architecture.md +1 -0
- package/docs/handlers.md +1 -1
- package/docs/models.md +2 -0
- package/docs/routes.md +1 -1
- package/handlers/disposition.js +3 -2
- package/handlers/log.js +16 -6
- package/handlers/plugin.js +13 -6
- package/index.js +19 -4
- package/lib/callLogLookup.js +82 -9
- package/lib/migrateCallLogsSchema.js +315 -7
- package/models/callLogModel.js +6 -0
- package/package.json +1 -1
- package/releaseNotes.json +20 -0
- package/test/connector/developerPortal.test.js +166 -0
- package/test/connector/mock.test.js +131 -0
- package/test/connector/proxy/engine.test.js +85 -0
- package/test/connector/proxy/index.test.js +246 -0
- package/test/connector/proxy/sample.json +1 -0
- package/test/handlers/admin.test.js +344 -0
- package/test/handlers/appointment.test.js +260 -0
- package/test/handlers/calldown.test.js +310 -0
- package/test/handlers/disposition.test.js +396 -0
- package/test/handlers/log.test.js +324 -1
- package/test/handlers/managedOAuth.test.js +262 -0
- package/test/handlers/plugin.test.js +305 -0
- package/test/handlers/user.test.js +381 -0
- package/test/index.test.js +102 -0
- package/test/lib/analytics.test.js +146 -0
- package/test/lib/authSession.test.js +173 -0
- package/test/lib/encode.test.js +59 -0
- package/test/lib/errorHandler.test.js +246 -0
- package/test/lib/generalErrorMessage.test.js +82 -0
- package/test/lib/s3ErrorLogReport.test.js +187 -0
- package/test/mcp/mcpHandlerMore.test.js +384 -0
- package/test/mcp/tools/appointmentTools.test.js +362 -0
- package/test/models/callDownListModel.test.js +125 -0
- package/test/models/dynamo/lockSchema.test.js +37 -0
- package/test/models/dynamo/noteCacheSchema.test.js +45 -0
- package/test/models/llmSessionModel.test.js +91 -0
- package/test/models/models.test.js +92 -0
- package/test/routes/calldownRoutes.test.js +224 -0
- package/test/routes/coreRouterBroadRoutes.test.js +855 -0
- package/test/routes/dispositionRoutes.test.js +192 -0
- package/test/routes/managedAuthRoutes.test.js +151 -0
- package/test/routes/pluginRoutes.test.js +262 -0
package/test/index.test.js
CHANGED
|
@@ -20,6 +20,7 @@ const jwt = require('../lib/jwt');
|
|
|
20
20
|
const authCore = require('../handlers/auth');
|
|
21
21
|
const logCore = require('../handlers/log');
|
|
22
22
|
const { createCoreRouter, createCoreMiddleware } = require('../index');
|
|
23
|
+
const connectorRegistry = require('../connector/registry');
|
|
23
24
|
|
|
24
25
|
function buildApp() {
|
|
25
26
|
const app = express();
|
|
@@ -196,4 +197,105 @@ describe('Core Router JWT normalization', () => {
|
|
|
196
197
|
});
|
|
197
198
|
});
|
|
198
199
|
});
|
|
200
|
+
describe('Core Router implementedInterfaces contract', () => {
|
|
201
|
+
const originalUseCache = process.env.USE_CACHE;
|
|
199
202
|
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
connectorRegistry.connectors.clear();
|
|
205
|
+
connectorRegistry.manifests.clear();
|
|
206
|
+
connectorRegistry.platformInterfaces.clear();
|
|
207
|
+
delete process.env.USE_CACHE;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterEach(() => {
|
|
211
|
+
connectorRegistry.connectors.clear();
|
|
212
|
+
connectorRegistry.manifests.clear();
|
|
213
|
+
connectorRegistry.platformInterfaces.clear();
|
|
214
|
+
if (originalUseCache === undefined) {
|
|
215
|
+
delete process.env.USE_CACHE;
|
|
216
|
+
} else {
|
|
217
|
+
process.env.USE_CACHE = originalUseCache;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('returns client capability flags for an oauth platform', async () => {
|
|
222
|
+
connectorRegistry.registerConnector('contractCRM', {
|
|
223
|
+
getAuthType: jest.fn().mockReturnValue('oauth'),
|
|
224
|
+
getOauthInfo: jest.fn(),
|
|
225
|
+
getUserInfo: jest.fn(),
|
|
226
|
+
createCallLog: jest.fn(),
|
|
227
|
+
updateCallLog: jest.fn(),
|
|
228
|
+
createMessageLog: jest.fn(),
|
|
229
|
+
findContactWithName: jest.fn(),
|
|
230
|
+
getLicenseStatus: jest.fn(),
|
|
231
|
+
});
|
|
232
|
+
const app = buildApp();
|
|
233
|
+
|
|
234
|
+
const response = await request(app).get('/implementedInterfaces?platform=contractCRM');
|
|
235
|
+
|
|
236
|
+
expect(response.status).toBe(200);
|
|
237
|
+
expect(response.body).toEqual({
|
|
238
|
+
getAuthType: true,
|
|
239
|
+
getOauthInfo: true,
|
|
240
|
+
getUserInfo: true,
|
|
241
|
+
createCallLog: true,
|
|
242
|
+
updateCallLog: true,
|
|
243
|
+
getCallLog: false,
|
|
244
|
+
createMessageLog: true,
|
|
245
|
+
updateMessageLog: false,
|
|
246
|
+
createContact: false,
|
|
247
|
+
findContact: false,
|
|
248
|
+
listAppointments: false,
|
|
249
|
+
createAppointment: false,
|
|
250
|
+
updateAppointment: false,
|
|
251
|
+
refreshAppointment: false,
|
|
252
|
+
confirmAppointment: false,
|
|
253
|
+
cancelAppointment: false,
|
|
254
|
+
unAuthorize: false,
|
|
255
|
+
upsertCallDisposition: false,
|
|
256
|
+
findContactWithName: true,
|
|
257
|
+
getUserList: false,
|
|
258
|
+
getLicenseStatus: true,
|
|
259
|
+
getLogFormatType: false,
|
|
260
|
+
refreshUserInfo: false,
|
|
261
|
+
cacheCallNote: false,
|
|
262
|
+
});
|
|
263
|
+
expect(response.body).not.toHaveProperty('getBasicAuth');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('returns basic auth and cache flags for an apiKey platform', async () => {
|
|
267
|
+
process.env.USE_CACHE = 'true';
|
|
268
|
+
connectorRegistry.registerConnector('apiKeyCRM', {
|
|
269
|
+
getAuthType: jest.fn().mockReturnValue('apiKey'),
|
|
270
|
+
getBasicAuth: jest.fn(),
|
|
271
|
+
createCallLog: jest.fn(),
|
|
272
|
+
updateCallLog: jest.fn(),
|
|
273
|
+
updateMessageLog: jest.fn(),
|
|
274
|
+
refreshUserInfo: jest.fn(),
|
|
275
|
+
});
|
|
276
|
+
const app = buildApp();
|
|
277
|
+
|
|
278
|
+
const response = await request(app).get('/implementedInterfaces?platform=apiKeyCRM');
|
|
279
|
+
|
|
280
|
+
expect(response.status).toBe(200);
|
|
281
|
+
expect(response.body).toEqual(expect.objectContaining({
|
|
282
|
+
getAuthType: true,
|
|
283
|
+
getBasicAuth: true,
|
|
284
|
+
createCallLog: true,
|
|
285
|
+
updateCallLog: true,
|
|
286
|
+
updateMessageLog: true,
|
|
287
|
+
refreshUserInfo: true,
|
|
288
|
+
cacheCallNote: true,
|
|
289
|
+
}));
|
|
290
|
+
expect(response.body).not.toHaveProperty('getOauthInfo');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('rejects requests without a platform parameter', async () => {
|
|
294
|
+
const app = buildApp();
|
|
295
|
+
|
|
296
|
+
const response = await request(app).get('/implementedInterfaces');
|
|
297
|
+
|
|
298
|
+
expect(response.status).toBe(400);
|
|
299
|
+
expect(response.text).toBe('Please provide platform.');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
describe('analytics', () => {
|
|
2
|
+
let mixpanelInit;
|
|
3
|
+
let peopleSetOnce;
|
|
4
|
+
let track;
|
|
5
|
+
let parseUserAgent;
|
|
6
|
+
|
|
7
|
+
function loadAnalytics({ token } = {}) {
|
|
8
|
+
jest.resetModules();
|
|
9
|
+
peopleSetOnce = jest.fn();
|
|
10
|
+
track = jest.fn();
|
|
11
|
+
mixpanelInit = jest.fn(() => ({
|
|
12
|
+
people: {
|
|
13
|
+
set_once: peopleSetOnce
|
|
14
|
+
},
|
|
15
|
+
track
|
|
16
|
+
}));
|
|
17
|
+
parseUserAgent = jest.fn(() => ({
|
|
18
|
+
browser: { name: 'Chrome' },
|
|
19
|
+
os: { name: 'Windows' },
|
|
20
|
+
device: { type: 'desktop' }
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
if (token) {
|
|
24
|
+
process.env.MIXPANEL_TOKEN = token;
|
|
25
|
+
} else {
|
|
26
|
+
delete process.env.MIXPANEL_TOKEN;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
jest.doMock('mixpanel', () => ({
|
|
30
|
+
init: mixpanelInit
|
|
31
|
+
}));
|
|
32
|
+
jest.doMock('ua-parser-js', () => parseUserAgent);
|
|
33
|
+
jest.doMock('../../lib/logger', () => ({
|
|
34
|
+
info: jest.fn(),
|
|
35
|
+
warn: jest.fn(),
|
|
36
|
+
error: jest.fn()
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
analytics: require('../../lib/analytics'),
|
|
41
|
+
logger: require('../../lib/logger')
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
delete process.env.MIXPANEL_TOKEN;
|
|
47
|
+
jest.dontMock('mixpanel');
|
|
48
|
+
jest.dontMock('ua-parser-js');
|
|
49
|
+
jest.dontMock('../../lib/logger');
|
|
50
|
+
jest.resetModules();
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('does not initialize Mixpanel when token is missing and skips tracking without an initialized client', () => {
|
|
55
|
+
const { analytics } = loadAnalytics();
|
|
56
|
+
|
|
57
|
+
analytics.init();
|
|
58
|
+
analytics.track({
|
|
59
|
+
eventName: 'Call Log Created',
|
|
60
|
+
connectorName: 'salesforce',
|
|
61
|
+
extensionId: 'ext-1'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(mixpanelInit).not.toHaveBeenCalled();
|
|
65
|
+
expect(peopleSetOnce).not.toHaveBeenCalled();
|
|
66
|
+
expect(track).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('skips tracking when extension id is missing', () => {
|
|
70
|
+
const { analytics } = loadAnalytics({ token: 'mixpanel-token' });
|
|
71
|
+
|
|
72
|
+
analytics.init();
|
|
73
|
+
analytics.track({
|
|
74
|
+
eventName: 'Call Log Created',
|
|
75
|
+
connectorName: 'salesforce',
|
|
76
|
+
accountId: 'account-1'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(mixpanelInit).toHaveBeenCalledWith('mixpanel-token');
|
|
80
|
+
expect(peopleSetOnce).not.toHaveBeenCalled();
|
|
81
|
+
expect(track).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('tracks event properties, parsed user agent fields, extras, and default source', () => {
|
|
85
|
+
const { analytics, logger } = loadAnalytics({ token: 'mixpanel-token' });
|
|
86
|
+
|
|
87
|
+
analytics.init();
|
|
88
|
+
analytics.track({
|
|
89
|
+
eventName: 'Call Log Created',
|
|
90
|
+
interfaceName: 'desktop',
|
|
91
|
+
connectorName: 'salesforce',
|
|
92
|
+
accountId: 'account-1',
|
|
93
|
+
extensionId: 'ext-1',
|
|
94
|
+
success: true,
|
|
95
|
+
requestDuration: 123,
|
|
96
|
+
userAgent: 'Mozilla/5.0',
|
|
97
|
+
ip: '127.0.0.1',
|
|
98
|
+
author: 'unit-test',
|
|
99
|
+
extras: {
|
|
100
|
+
extraKey: 'extra-value'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(peopleSetOnce).toHaveBeenCalledWith('ext-1', {
|
|
105
|
+
version: expect.any(String),
|
|
106
|
+
appName: 'App Connect',
|
|
107
|
+
crmPlatform: 'salesforce'
|
|
108
|
+
});
|
|
109
|
+
expect(parseUserAgent).toHaveBeenCalledWith('Mozilla/5.0');
|
|
110
|
+
expect(track).toHaveBeenCalledWith('Call Log Created', expect.objectContaining({
|
|
111
|
+
distinct_id: 'ext-1',
|
|
112
|
+
interfaceName: 'desktop',
|
|
113
|
+
adapterName: 'salesforce',
|
|
114
|
+
rcAccountId: 'account-1',
|
|
115
|
+
extensionId: 'ext-1',
|
|
116
|
+
success: true,
|
|
117
|
+
requestDuration: 123,
|
|
118
|
+
collectedFrom: 'server',
|
|
119
|
+
appName: 'App Connect',
|
|
120
|
+
eventAddedVia: 'server',
|
|
121
|
+
$browser: 'Chrome',
|
|
122
|
+
$os: 'Windows',
|
|
123
|
+
$device: 'desktop',
|
|
124
|
+
ip: '127.0.0.1',
|
|
125
|
+
author: 'unit-test',
|
|
126
|
+
extraKey: 'extra-value'
|
|
127
|
+
}));
|
|
128
|
+
expect(logger.info).toHaveBeenCalledWith('Event: Call Log Created');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('uses explicit eventAddedVia when provided', () => {
|
|
132
|
+
const { analytics } = loadAnalytics({ token: 'mixpanel-token' });
|
|
133
|
+
|
|
134
|
+
analytics.init();
|
|
135
|
+
analytics.track({
|
|
136
|
+
eventName: 'Disposition Saved',
|
|
137
|
+
connectorName: 'hubspot',
|
|
138
|
+
extensionId: 'ext-2',
|
|
139
|
+
eventAddedVia: 'browser'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(track).toHaveBeenCalledWith('Disposition Saved', expect.objectContaining({
|
|
143
|
+
eventAddedVia: 'browser'
|
|
144
|
+
}));
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const { CacheModel } = require('../../models/cacheModel');
|
|
2
|
+
const {
|
|
3
|
+
createAuthSession,
|
|
4
|
+
getAuthSession,
|
|
5
|
+
updateAuthSession,
|
|
6
|
+
} = require('../../lib/authSession');
|
|
7
|
+
|
|
8
|
+
describe('authSession', () => {
|
|
9
|
+
const baseTime = new Date(Date.now() + (60 * 60 * 1000));
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
await CacheModel.destroy({ where: {} });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('creates a pending auth session record with generated id and expiry', async () => {
|
|
20
|
+
jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime());
|
|
21
|
+
|
|
22
|
+
await createAuthSession('session-1', {
|
|
23
|
+
platform: 'clio',
|
|
24
|
+
hostname: 'https://example.clio.com',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const record = await CacheModel.findByPk('auth-session-session-1');
|
|
28
|
+
expect(record).not.toBeNull();
|
|
29
|
+
expect(record.userId).toBe('session-1');
|
|
30
|
+
expect(record.cacheKey).toBe('auth-session');
|
|
31
|
+
expect(record.status).toBe('pending');
|
|
32
|
+
expect(record.expiry.getTime()).toBe(baseTime.getTime() + (5 * 60 * 1000));
|
|
33
|
+
expect(record.data).toMatchObject({
|
|
34
|
+
platform: 'clio',
|
|
35
|
+
hostname: 'https://example.clio.com',
|
|
36
|
+
});
|
|
37
|
+
expect(new Date(record.data.createdAt).toString()).not.toBe('Invalid Date');
|
|
38
|
+
|
|
39
|
+
await expect(getAuthSession('session-1')).resolves.toMatchObject({
|
|
40
|
+
sessionId: 'session-1',
|
|
41
|
+
status: 'pending',
|
|
42
|
+
platform: 'clio',
|
|
43
|
+
hostname: 'https://example.clio.com',
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('creates with an empty hostname when hostname is missing', async () => {
|
|
48
|
+
await createAuthSession('session-no-hostname', {
|
|
49
|
+
platform: 'googleSheets',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const session = await getAuthSession('session-no-hostname');
|
|
53
|
+
|
|
54
|
+
expect(session).toMatchObject({
|
|
55
|
+
sessionId: 'session-no-hostname',
|
|
56
|
+
status: 'pending',
|
|
57
|
+
platform: 'googleSheets',
|
|
58
|
+
hostname: '',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('resets an existing session record to pending with new session data', async () => {
|
|
63
|
+
await createAuthSession('retry-session', {
|
|
64
|
+
platform: 'clio',
|
|
65
|
+
hostname: 'old-host',
|
|
66
|
+
});
|
|
67
|
+
const existing = await CacheModel.findByPk('auth-session-retry-session');
|
|
68
|
+
await existing.update({
|
|
69
|
+
status: 'completed',
|
|
70
|
+
data: {
|
|
71
|
+
platform: 'clio',
|
|
72
|
+
hostname: 'old-host',
|
|
73
|
+
jwtToken: 'old-token',
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await createAuthSession('retry-session', {
|
|
78
|
+
platform: 'bullhorn',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const record = await CacheModel.findByPk('auth-session-retry-session');
|
|
82
|
+
expect(record.status).toBe('pending');
|
|
83
|
+
expect(record.data).toMatchObject({
|
|
84
|
+
platform: 'bullhorn',
|
|
85
|
+
hostname: '',
|
|
86
|
+
});
|
|
87
|
+
expect(record.data.jwtToken).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns null for missing sessions', async () => {
|
|
91
|
+
await expect(getAuthSession('missing-session')).resolves.toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('marks expired sessions as expired and returns current session data', async () => {
|
|
95
|
+
await CacheModel.create({
|
|
96
|
+
id: 'auth-session-expired-session',
|
|
97
|
+
cacheKey: 'auth-session',
|
|
98
|
+
userId: 'expired-session',
|
|
99
|
+
status: 'pending',
|
|
100
|
+
data: {
|
|
101
|
+
platform: 'pipedrive',
|
|
102
|
+
hostname: 'pipedrive-host',
|
|
103
|
+
},
|
|
104
|
+
expiry: new Date(Date.now() - 1000),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const session = await getAuthSession('expired-session');
|
|
108
|
+
|
|
109
|
+
expect(session).toEqual({
|
|
110
|
+
sessionId: 'expired-session',
|
|
111
|
+
status: 'expired',
|
|
112
|
+
platform: 'pipedrive',
|
|
113
|
+
hostname: 'pipedrive-host',
|
|
114
|
+
});
|
|
115
|
+
const record = await CacheModel.findByPk('auth-session-expired-session');
|
|
116
|
+
expect(record.status).toBe('expired');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('updates an existing session by merging data and extending settled expiry', async () => {
|
|
120
|
+
jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime());
|
|
121
|
+
await createAuthSession('update-session', {
|
|
122
|
+
platform: 'netsuite',
|
|
123
|
+
hostname: 'netsuite-host',
|
|
124
|
+
});
|
|
125
|
+
jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime() + 1000);
|
|
126
|
+
|
|
127
|
+
await updateAuthSession('update-session', {
|
|
128
|
+
status: 'completed',
|
|
129
|
+
jwtToken: 'jwt-token',
|
|
130
|
+
rcExtensionId: 'extension-1',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const record = await CacheModel.findByPk('auth-session-update-session');
|
|
134
|
+
expect(record.status).toBe('completed');
|
|
135
|
+
expect(record.expiry.getTime()).toBe(baseTime.getTime() + 1000 + (15 * 60 * 1000));
|
|
136
|
+
expect(record.data).toMatchObject({
|
|
137
|
+
platform: 'netsuite',
|
|
138
|
+
hostname: 'netsuite-host',
|
|
139
|
+
status: 'completed',
|
|
140
|
+
jwtToken: 'jwt-token',
|
|
141
|
+
rcExtensionId: 'extension-1',
|
|
142
|
+
});
|
|
143
|
+
expect(new Date(record.data.updatedAt).toString()).not.toBe('Invalid Date');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('keeps pending expiry when update does not settle the session', async () => {
|
|
147
|
+
jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime());
|
|
148
|
+
await createAuthSession('pending-session', {
|
|
149
|
+
platform: 'redtail',
|
|
150
|
+
});
|
|
151
|
+
jest.spyOn(Date, 'now').mockReturnValue(baseTime.getTime() + 2000);
|
|
152
|
+
|
|
153
|
+
await updateAuthSession('pending-session', {
|
|
154
|
+
hostname: 'new-host',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const record = await CacheModel.findByPk('auth-session-pending-session');
|
|
158
|
+
expect(record.status).toBe('pending');
|
|
159
|
+
expect(record.expiry.getTime()).toBe(baseTime.getTime() + 2000 + (5 * 60 * 1000));
|
|
160
|
+
expect(record.data).toMatchObject({
|
|
161
|
+
platform: 'redtail',
|
|
162
|
+
hostname: 'new-host',
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('does nothing when updating a missing session', async () => {
|
|
167
|
+
await expect(updateAuthSession('missing-session', {
|
|
168
|
+
status: 'completed',
|
|
169
|
+
})).resolves.toBeUndefined();
|
|
170
|
+
|
|
171
|
+
await expect(CacheModel.findByPk('auth-session-missing-session')).resolves.toBeNull();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { decoded, encode } = require('../../lib/encode');
|
|
2
|
+
|
|
3
|
+
describe('encode', () => {
|
|
4
|
+
const originalSecret = process.env.APP_SERVER_SECRET_KEY;
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
if (originalSecret === undefined) {
|
|
8
|
+
delete process.env.APP_SERVER_SECRET_KEY;
|
|
9
|
+
} else {
|
|
10
|
+
process.env.APP_SERVER_SECRET_KEY = originalSecret;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('encodes and decodes a string with a 32-character secret', () => {
|
|
15
|
+
process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012';
|
|
16
|
+
|
|
17
|
+
const encrypted = encode('sensitive token value');
|
|
18
|
+
|
|
19
|
+
expect(encrypted).toMatch(/^[0-9a-f]+$/);
|
|
20
|
+
expect(encrypted).not.toBe('sensitive token value');
|
|
21
|
+
expect(decoded(encrypted)).toBe('sensitive token value');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('throws a deterministic error when secret is missing', () => {
|
|
25
|
+
delete process.env.APP_SERVER_SECRET_KEY;
|
|
26
|
+
|
|
27
|
+
expect(() => encode('data')).toThrow('APP_SERVER_SECRET_KEY is not defined');
|
|
28
|
+
expect(() => decoded('abcd')).toThrow('APP_SERVER_SECRET_KEY is not defined');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('pads short secrets with spaces to 32 bytes', () => {
|
|
32
|
+
process.env.APP_SERVER_SECRET_KEY = 'short-secret';
|
|
33
|
+
const encryptedWithShortSecret = encode('payload');
|
|
34
|
+
|
|
35
|
+
process.env.APP_SERVER_SECRET_KEY = 'short-secret'.padEnd(32, ' ');
|
|
36
|
+
const encryptedWithPaddedSecret = encode('payload');
|
|
37
|
+
|
|
38
|
+
expect(encryptedWithShortSecret).toBe(encryptedWithPaddedSecret);
|
|
39
|
+
expect(decoded(encryptedWithShortSecret)).toBe('payload');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('truncates long secrets to 32 bytes', () => {
|
|
43
|
+
process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012-first-suffix';
|
|
44
|
+
const encryptedWithLongSecret = encode('payload');
|
|
45
|
+
|
|
46
|
+
process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012-second-suffix';
|
|
47
|
+
const encryptedWithSamePrefix = encode('payload');
|
|
48
|
+
|
|
49
|
+
expect(encryptedWithLongSecret).toBe(encryptedWithSamePrefix);
|
|
50
|
+
expect(decoded(encryptedWithLongSecret)).toBe('payload');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('throws when encrypted input is not valid hex ciphertext', () => {
|
|
54
|
+
process.env.APP_SERVER_SECRET_KEY = '12345678901234567890123456789012';
|
|
55
|
+
|
|
56
|
+
expect(() => decoded('not-valid-ciphertext')).toThrow();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|