@app-connect/core 1.7.20 → 1.7.22
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/README.md +8 -1
- package/connector/developerPortal.js +4 -4
- package/docs/README.md +50 -0
- package/docs/architecture.md +93 -0
- package/docs/connectors.md +117 -0
- package/docs/handlers.md +125 -0
- package/docs/libraries.md +101 -0
- package/docs/models.md +144 -0
- package/docs/routes.md +115 -0
- package/docs/tests.md +73 -0
- package/handlers/admin.js +22 -2
- package/handlers/auth.js +51 -10
- package/handlers/contact.js +7 -2
- package/handlers/log.js +4 -4
- package/handlers/managedAuth.js +446 -0
- package/index.js +263 -38
- package/lib/jwt.js +1 -1
- package/mcp/tools/createCallLog.js +5 -1
- package/mcp/tools/createContact.js +5 -1
- package/mcp/tools/createMessageLog.js +5 -1
- package/mcp/tools/findContactByName.js +5 -1
- package/mcp/tools/findContactByPhone.js +6 -2
- package/mcp/tools/getCallLog.js +5 -1
- package/mcp/tools/rcGetCallLogs.js +6 -2
- package/mcp/tools/updateCallLog.js +5 -1
- package/mcp/ui/App/lib/developerPortal.ts +1 -1
- package/package.json +1 -1
- package/releaseNotes.json +20 -0
- package/test/handlers/admin.test.js +34 -0
- package/test/handlers/auth.test.js +402 -6
- package/test/handlers/contact.test.js +162 -0
- package/test/handlers/managedAuth.test.js +458 -0
- package/test/index.test.js +105 -0
- package/test/lib/jwt.test.js +15 -0
- package/test/mcp/tools/createCallLog.test.js +11 -0
- package/test/mcp/tools/createContact.test.js +58 -0
- package/test/mcp/tools/createMessageLog.test.js +15 -0
- package/test/mcp/tools/findContactByName.test.js +12 -0
- package/test/mcp/tools/findContactByPhone.test.js +12 -0
- package/test/mcp/tools/getCallLog.test.js +12 -0
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -0
- package/test/mcp/tools/updateCallLog.test.js +14 -0
- package/test/routes/managedAuthRoutes.test.js +132 -0
- package/test/setup.js +2 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const request = require('supertest');
|
|
3
|
+
|
|
4
|
+
jest.mock('../lib/jwt', () => ({
|
|
5
|
+
decodeJwt: jest.fn(),
|
|
6
|
+
generateJwt: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
jest.mock('../handlers/auth', () => ({
|
|
9
|
+
authValidation: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
jest.mock('../lib/analytics', () => ({
|
|
12
|
+
init: jest.fn(),
|
|
13
|
+
track: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const jwt = require('../lib/jwt');
|
|
17
|
+
const authCore = require('../handlers/auth');
|
|
18
|
+
const { createCoreRouter, createCoreMiddleware } = require('../index');
|
|
19
|
+
|
|
20
|
+
function buildApp() {
|
|
21
|
+
const app = express();
|
|
22
|
+
createCoreMiddleware().forEach((m) => app.use(m));
|
|
23
|
+
app.use('/', createCoreRouter());
|
|
24
|
+
return app;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('Core Router JWT normalization', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should accept query jwtToken without refreshing it', async () => {
|
|
33
|
+
jwt.decodeJwt.mockReturnValue({ id: 'user-1', platform: 'testCRM' });
|
|
34
|
+
authCore.authValidation.mockResolvedValue({
|
|
35
|
+
successful: true,
|
|
36
|
+
returnMessage: { message: 'ok' },
|
|
37
|
+
failReason: null,
|
|
38
|
+
status: 200,
|
|
39
|
+
});
|
|
40
|
+
const app = buildApp();
|
|
41
|
+
|
|
42
|
+
const response = await request(app).get('/authValidation?jwtToken=query-token');
|
|
43
|
+
|
|
44
|
+
expect(response.status).toBe(200);
|
|
45
|
+
expect(response.headers['x-refreshed-jwt-token']).toBeUndefined();
|
|
46
|
+
expect(authCore.authValidation).toHaveBeenCalledWith({
|
|
47
|
+
platform: 'testCRM',
|
|
48
|
+
userId: 'user-1',
|
|
49
|
+
});
|
|
50
|
+
expect(jwt.generateJwt).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should refresh near-expiry bearer token and expose header', async () => {
|
|
54
|
+
const nowMs = 1700000000000;
|
|
55
|
+
const nowSeconds = Math.floor(nowMs / 1000);
|
|
56
|
+
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(nowMs);
|
|
57
|
+
jwt.decodeJwt.mockImplementation((token) => {
|
|
58
|
+
if (token === 'old-token') {
|
|
59
|
+
return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + 60 };
|
|
60
|
+
}
|
|
61
|
+
if (token === 'new-token') {
|
|
62
|
+
return { id: 'user-1', platform: 'testCRM', exp: nowSeconds + (14 * 24 * 60 * 60) };
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
});
|
|
66
|
+
jwt.generateJwt.mockReturnValue('new-token');
|
|
67
|
+
const app = buildApp();
|
|
68
|
+
|
|
69
|
+
const response = await request(app)
|
|
70
|
+
.get('/isAlive')
|
|
71
|
+
.set('Authorization', 'Bearer old-token')
|
|
72
|
+
.set('Origin', 'https://example.com');
|
|
73
|
+
|
|
74
|
+
expect(response.status).toBe(200);
|
|
75
|
+
expect(response.headers['x-refreshed-jwt-token']).toBe('new-token');
|
|
76
|
+
expect(response.headers['access-control-expose-headers']).toContain('x-refreshed-jwt-token');
|
|
77
|
+
expect(jwt.generateJwt).toHaveBeenCalledWith({ id: 'user-1', platform: 'testCRM' });
|
|
78
|
+
nowSpy.mockRestore();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('should treat invalid bearer token as unauthenticated for authValidation route', async () => {
|
|
82
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
83
|
+
const app = buildApp();
|
|
84
|
+
|
|
85
|
+
const response = await request(app)
|
|
86
|
+
.get('/authValidation?jwtToken=query-token')
|
|
87
|
+
.set('Authorization', 'Bearer invalid-token');
|
|
88
|
+
|
|
89
|
+
expect(response.status).toBe(400);
|
|
90
|
+
expect(response.text).toContain('authorize CRM platform');
|
|
91
|
+
expect(authCore.authValidation).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should bypass normalization for /mcp routes', async () => {
|
|
95
|
+
const app = buildApp();
|
|
96
|
+
|
|
97
|
+
const response = await request(app)
|
|
98
|
+
.get('/mcp')
|
|
99
|
+
.set('Authorization', 'Bearer maybe-token');
|
|
100
|
+
|
|
101
|
+
expect(response.status).toBe(404);
|
|
102
|
+
expect(jwt.decodeJwt).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
package/test/lib/jwt.test.js
CHANGED
|
@@ -35,6 +35,21 @@ describe('JWT Utility', () => {
|
|
|
35
35
|
// Assert
|
|
36
36
|
expect(token1).not.toBe(token2);
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
test('should generate token with about 2 weeks lifetime', () => {
|
|
40
|
+
// Arrange
|
|
41
|
+
const payload = { id: 'user-ttl', platform: 'testCRM' };
|
|
42
|
+
|
|
43
|
+
// Act
|
|
44
|
+
const token = jwt.generateJwt(payload);
|
|
45
|
+
const decoded = jwt.decodeJwt(token);
|
|
46
|
+
const lifetimeSeconds = decoded.exp - decoded.iat;
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
// Keep a tiny tolerance to avoid timing flakiness.
|
|
50
|
+
expect(lifetimeSeconds).toBeGreaterThanOrEqual((14 * 24 * 60 * 60) - 2);
|
|
51
|
+
expect(lifetimeSeconds).toBeLessThanOrEqual((14 * 24 * 60 * 60) + 2);
|
|
52
|
+
});
|
|
38
53
|
});
|
|
39
54
|
|
|
40
55
|
describe('decodeJwt', () => {
|
|
@@ -292,6 +292,17 @@ describe('MCP Tool: createCallLog', () => {
|
|
|
292
292
|
expect(result.error).toContain('Invalid JWT token');
|
|
293
293
|
});
|
|
294
294
|
|
|
295
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
296
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
297
|
+
const result = await createCallLog.execute({
|
|
298
|
+
jwtToken: 'invalid-token',
|
|
299
|
+
incomingData: { logInfo: { sessionId: 'session-123' } }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(result.success).toBe(false);
|
|
303
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
304
|
+
});
|
|
305
|
+
|
|
295
306
|
test('should return error when platform connector not found', async () => {
|
|
296
307
|
// Arrange
|
|
297
308
|
const mockIncomingData = {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const createContact = require('../../../mcp/tools/createContact');
|
|
2
|
+
const jwt = require('../../../lib/jwt');
|
|
3
|
+
const connectorRegistry = require('../../../connector/registry');
|
|
4
|
+
const contactCore = require('../../../handlers/contact');
|
|
5
|
+
|
|
6
|
+
jest.mock('../../../lib/jwt');
|
|
7
|
+
jest.mock('../../../connector/registry');
|
|
8
|
+
jest.mock('../../../handlers/contact');
|
|
9
|
+
|
|
10
|
+
describe('MCP Tool: createContact', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should have correct tool definition', () => {
|
|
16
|
+
expect(createContact.definition).toBeDefined();
|
|
17
|
+
expect(createContact.definition.name).toBe('createContact');
|
|
18
|
+
expect(createContact.definition.inputSchema.required).toContain('phoneNumber');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should create contact successfully', async () => {
|
|
22
|
+
jwt.decodeJwt.mockReturnValue({ id: 'user-123', platform: 'testCRM' });
|
|
23
|
+
connectorRegistry.getConnector.mockReturnValue({ createContact: jest.fn() });
|
|
24
|
+
contactCore.createContact.mockResolvedValue({
|
|
25
|
+
successful: true,
|
|
26
|
+
returnMessage: { message: 'Created' },
|
|
27
|
+
contact: { id: 'contact-1' }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = await createContact.execute({
|
|
31
|
+
jwtToken: 'mock-jwt-token',
|
|
32
|
+
phoneNumber: '+14155551234',
|
|
33
|
+
newContactName: 'John Doe'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({
|
|
37
|
+
success: true,
|
|
38
|
+
data: {
|
|
39
|
+
contact: { id: 'contact-1' },
|
|
40
|
+
message: 'Created'
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
46
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
47
|
+
|
|
48
|
+
const result = await createContact.execute({
|
|
49
|
+
jwtToken: 'invalid-jwt',
|
|
50
|
+
phoneNumber: '+14155551234',
|
|
51
|
+
newContactName: 'John Doe'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.success).toBe(false);
|
|
55
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
@@ -427,6 +427,21 @@ describe('MCP Tool: createMessageLog', () => {
|
|
|
427
427
|
expect(result.error).toContain('Invalid JWT token');
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
431
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
432
|
+
|
|
433
|
+
const result = await createMessageLog.execute({
|
|
434
|
+
jwtToken: 'invalid-token',
|
|
435
|
+
incomingData: {
|
|
436
|
+
sessionId: 'session-123',
|
|
437
|
+
messageInfo: { from: { phoneNumber: '+1234567890' }, to: [{ phoneNumber: '+1098765432' }] }
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
expect(result.success).toBe(false);
|
|
442
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
443
|
+
});
|
|
444
|
+
|
|
430
445
|
test('should return error when platform connector not found', async () => {
|
|
431
446
|
// Arrange
|
|
432
447
|
const mockIncomingData = {
|
|
@@ -162,6 +162,18 @@ describe('MCP Tool: findContactByName', () => {
|
|
|
162
162
|
expect(result.error).toContain('Invalid JWT token');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
166
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
167
|
+
|
|
168
|
+
const result = await findContactByName.execute({
|
|
169
|
+
jwtToken: 'invalid-token',
|
|
170
|
+
name: 'John Doe'
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(result.success).toBe(false);
|
|
174
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
175
|
+
});
|
|
176
|
+
|
|
165
177
|
test('should return error when platform connector not found', async () => {
|
|
166
178
|
// Arrange
|
|
167
179
|
jwt.decodeJwt.mockReturnValue({
|
|
@@ -211,6 +211,18 @@ describe('MCP Tool: findContactByPhone', () => {
|
|
|
211
211
|
expect(result.error).toContain('Invalid JWT token');
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
215
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
216
|
+
|
|
217
|
+
const result = await findContactByPhone.execute({
|
|
218
|
+
jwtToken: 'invalid-token',
|
|
219
|
+
phoneNumber: '+1234567890'
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.success).toBe(false);
|
|
223
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
224
|
+
});
|
|
225
|
+
|
|
214
226
|
test('should return error when platform connector not found', async () => {
|
|
215
227
|
// Arrange
|
|
216
228
|
jwt.decodeJwt.mockReturnValue({
|
|
@@ -213,6 +213,18 @@ describe('MCP Tool: getCallLog', () => {
|
|
|
213
213
|
expect(result.error).toContain('Invalid JWT token');
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
217
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
218
|
+
|
|
219
|
+
const result = await getCallLog.execute({
|
|
220
|
+
jwtToken: 'invalid-token',
|
|
221
|
+
sessionIds: ['session-123']
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(result.success).toBe(false);
|
|
225
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
226
|
+
});
|
|
227
|
+
|
|
216
228
|
test('should return error when platform connector not found', async () => {
|
|
217
229
|
// Arrange
|
|
218
230
|
jwt.decodeJwt.mockReturnValue({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const rcGetCallLogs = require('../../../mcp/tools/rcGetCallLogs');
|
|
2
|
+
const jwt = require('../../../lib/jwt');
|
|
3
|
+
const { RingCentral } = require('../../../lib/ringcentral');
|
|
4
|
+
|
|
5
|
+
jest.mock('../../../lib/jwt');
|
|
6
|
+
jest.mock('../../../lib/ringcentral', () => ({
|
|
7
|
+
RingCentral: jest.fn()
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('MCP Tool: rcGetCallLogs', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
process.env.RINGCENTRAL_SERVER = 'https://platform.ringcentral.com';
|
|
14
|
+
process.env.RINGCENTRAL_CLIENT_ID = 'client-id';
|
|
15
|
+
process.env.RINGCENTRAL_CLIENT_SECRET = 'client-secret';
|
|
16
|
+
process.env.APP_SERVER = 'https://app.example.com';
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should have correct tool definition', () => {
|
|
20
|
+
expect(rcGetCallLogs.definition).toBeDefined();
|
|
21
|
+
expect(rcGetCallLogs.definition.name).toBe('rcGetCallLogs');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should return call logs successfully', async () => {
|
|
25
|
+
jwt.decodeJwt.mockReturnValue({ id: 'user-123' });
|
|
26
|
+
const getCallLogData = jest.fn().mockResolvedValue({ records: [{ id: '1' }] });
|
|
27
|
+
RingCentral.mockImplementation(() => ({ getCallLogData }));
|
|
28
|
+
|
|
29
|
+
const result = await rcGetCallLogs.execute({
|
|
30
|
+
jwtToken: 'mock-jwt',
|
|
31
|
+
rcAccessToken: 'rc-token',
|
|
32
|
+
timeFrom: '2026-04-01T00:00:00.000Z',
|
|
33
|
+
timeTo: '2026-04-02T00:00:00.000Z'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({ records: [{ id: '1' }] });
|
|
37
|
+
expect(getCallLogData).toHaveBeenCalledWith({
|
|
38
|
+
token: { access_token: 'rc-token', token_type: 'Bearer' },
|
|
39
|
+
timeFrom: '2026-04-01T00:00:00.000Z',
|
|
40
|
+
timeTo: '2026-04-02T00:00:00.000Z'
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
45
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
46
|
+
|
|
47
|
+
const result = await rcGetCallLogs.execute({
|
|
48
|
+
jwtToken: 'bad-jwt',
|
|
49
|
+
rcAccessToken: 'rc-token'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.success).toBe(false);
|
|
53
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
@@ -191,6 +191,20 @@ describe('MCP Tool: updateCallLog', () => {
|
|
|
191
191
|
expect(result.error).toContain('Invalid JWT token');
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
test('should return error when decodeJwt returns null', async () => {
|
|
195
|
+
jwt.decodeJwt.mockReturnValue(null);
|
|
196
|
+
|
|
197
|
+
const result = await updateCallLog.execute({
|
|
198
|
+
jwtToken: 'invalid-token',
|
|
199
|
+
incomingData: {
|
|
200
|
+
logData: { sessionId: 'session-123' }
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result.success).toBe(false);
|
|
205
|
+
expect(result.error).toContain('Invalid JWT token');
|
|
206
|
+
});
|
|
207
|
+
|
|
194
208
|
test('should return error when platform connector not found', async () => {
|
|
195
209
|
// Arrange
|
|
196
210
|
const mockIncomingData = {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const request = require('supertest');
|
|
3
|
+
|
|
4
|
+
jest.mock('../../handlers/admin', () => ({
|
|
5
|
+
validateRcUserToken: jest.fn(),
|
|
6
|
+
validateAdminRole: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
jest.mock('../../handlers/managedAuth', () => ({
|
|
9
|
+
getManagedAuthState: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
jest.mock('../../handlers/auth', () => ({
|
|
12
|
+
onApiKeyLogin: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
jest.mock('../../lib/jwt', () => ({
|
|
15
|
+
generateJwt: jest.fn().mockReturnValue('jwt-token'),
|
|
16
|
+
decodeJwt: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const adminCore = require('../../handlers/admin');
|
|
20
|
+
const managedAuthCore = require('../../handlers/managedAuth');
|
|
21
|
+
const authCore = require('../../handlers/auth');
|
|
22
|
+
const { createCoreRouter } = require('../../index');
|
|
23
|
+
|
|
24
|
+
describe('Managed Auth Routes', () => {
|
|
25
|
+
let app;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
app = express();
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
app.use('/', createCoreRouter());
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('GET /apiKeyManagedAuthState', () => {
|
|
35
|
+
test('should require rcAccessToken', async () => {
|
|
36
|
+
const response = await request(app)
|
|
37
|
+
.get('/apiKeyManagedAuthState')
|
|
38
|
+
.query({ platform: 'testCRM' });
|
|
39
|
+
|
|
40
|
+
expect(response.status).toBe(400);
|
|
41
|
+
expect(response.text).toContain('Missing RingCentral access token');
|
|
42
|
+
expect(adminCore.validateRcUserToken).not.toHaveBeenCalled();
|
|
43
|
+
expect(managedAuthCore.getManagedAuthState).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should validate rcAccessToken and use validated identity', async () => {
|
|
47
|
+
adminCore.validateRcUserToken.mockResolvedValue({
|
|
48
|
+
rcAccountId: 'validated-account-id',
|
|
49
|
+
rcExtensionId: 'validated-extension-id',
|
|
50
|
+
rcUserName: 'Validated User',
|
|
51
|
+
});
|
|
52
|
+
managedAuthCore.getManagedAuthState.mockResolvedValue({
|
|
53
|
+
hasManagedAuth: true,
|
|
54
|
+
allRequiredFieldsSatisfied: true,
|
|
55
|
+
visibleFieldConsts: [],
|
|
56
|
+
missingRequiredFieldConsts: [],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const response = await request(app)
|
|
60
|
+
.get('/apiKeyManagedAuthState')
|
|
61
|
+
.query({
|
|
62
|
+
platform: 'testCRM',
|
|
63
|
+
rcAccessToken: 'valid-rc-token',
|
|
64
|
+
rcAccountId: 'spoofed-account-id',
|
|
65
|
+
rcExtensionId: 'spoofed-extension-id',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(response.status).toBe(200);
|
|
69
|
+
expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
|
|
70
|
+
expect(managedAuthCore.getManagedAuthState).toHaveBeenCalledWith(expect.objectContaining({
|
|
71
|
+
platform: 'testCRM',
|
|
72
|
+
rcAccountId: 'validated-account-id',
|
|
73
|
+
rcExtensionId: 'validated-extension-id',
|
|
74
|
+
}));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('POST /apiKeyLogin', () => {
|
|
79
|
+
test('should require rcAccessToken', async () => {
|
|
80
|
+
const response = await request(app)
|
|
81
|
+
.post('/apiKeyLogin')
|
|
82
|
+
.send({
|
|
83
|
+
platform: 'testCRM',
|
|
84
|
+
apiKey: 'api-key',
|
|
85
|
+
hostname: 'test.example.com',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(response.status).toBe(400);
|
|
89
|
+
expect(response.text).toContain('Missing RingCentral access token');
|
|
90
|
+
expect(adminCore.validateRcUserToken).not.toHaveBeenCalled();
|
|
91
|
+
expect(authCore.onApiKeyLogin).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should validate rcAccessToken and ignore spoofed rc ids in body', async () => {
|
|
95
|
+
adminCore.validateRcUserToken.mockResolvedValue({
|
|
96
|
+
rcAccountId: 'validated-account-id',
|
|
97
|
+
rcExtensionId: 'validated-extension-id',
|
|
98
|
+
rcUserName: 'Validated User',
|
|
99
|
+
});
|
|
100
|
+
authCore.onApiKeyLogin.mockResolvedValue({
|
|
101
|
+
userInfo: {
|
|
102
|
+
id: 'crm-user-id',
|
|
103
|
+
name: 'CRM User',
|
|
104
|
+
},
|
|
105
|
+
returnMessage: {
|
|
106
|
+
messageType: 'success',
|
|
107
|
+
message: 'ok',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const response = await request(app)
|
|
112
|
+
.post('/apiKeyLogin')
|
|
113
|
+
.send({
|
|
114
|
+
platform: 'testCRM',
|
|
115
|
+
apiKey: 'api-key',
|
|
116
|
+
hostname: 'test.example.com',
|
|
117
|
+
rcAccessToken: 'valid-rc-token',
|
|
118
|
+
rcAccountId: 'spoofed-account-id',
|
|
119
|
+
rcExtensionId: 'spoofed-extension-id',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(200);
|
|
123
|
+
expect(adminCore.validateRcUserToken).toHaveBeenCalledWith({ rcAccessToken: 'valid-rc-token' });
|
|
124
|
+
expect(authCore.onApiKeyLogin).toHaveBeenCalledWith(expect.objectContaining({
|
|
125
|
+
platform: 'testCRM',
|
|
126
|
+
rcAccountId: 'validated-account-id',
|
|
127
|
+
rcExtensionId: 'validated-extension-id',
|
|
128
|
+
rcUserName: 'Validated User',
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
package/test/setup.js
CHANGED
|
@@ -29,6 +29,7 @@ beforeAll(async () => {
|
|
|
29
29
|
const { UserModel } = require('../models/userModel');
|
|
30
30
|
const { CacheModel } = require('../models/cacheModel');
|
|
31
31
|
const { AdminConfigModel } = require('../models/adminConfigModel');
|
|
32
|
+
const { AccountDataModel } = require('../models/accountDataModel');
|
|
32
33
|
|
|
33
34
|
// Sync database models
|
|
34
35
|
await CallLogModel.sync({ force: true });
|
|
@@ -36,6 +37,7 @@ beforeAll(async () => {
|
|
|
36
37
|
await UserModel.sync({ force: true });
|
|
37
38
|
await CacheModel.sync({ force: true });
|
|
38
39
|
await AdminConfigModel.sync({ force: true });
|
|
40
|
+
await AccountDataModel.sync({ force: true });
|
|
39
41
|
|
|
40
42
|
console.log('Database models synced for testing');
|
|
41
43
|
} catch (error) {
|