@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
|
@@ -29,6 +29,7 @@ const oauth = require('../../lib/oauth');
|
|
|
29
29
|
const { RingCentral } = require('../../lib/ringcentral');
|
|
30
30
|
const { Connector } = require('../../models/dynamo/connectorSchema');
|
|
31
31
|
const { sequelize } = require('../../models/sequelize');
|
|
32
|
+
const { getHashValue } = require('../../lib/util');
|
|
32
33
|
|
|
33
34
|
describe('Admin Handler', () => {
|
|
34
35
|
beforeAll(async () => {
|
|
@@ -612,5 +613,348 @@ describe('Admin Handler', () => {
|
|
|
612
613
|
expect(result[0].rcUser[0].extensionId).toBe('ext-existing');
|
|
613
614
|
});
|
|
614
615
|
});
|
|
616
|
+
|
|
617
|
+
describe('reports and mapping branch coverage', () => {
|
|
618
|
+
beforeEach(() => {
|
|
619
|
+
process.env.RINGCENTRAL_SERVER = 'https://platform.example.com';
|
|
620
|
+
process.env.RINGCENTRAL_CLIENT_ID = 'client-id';
|
|
621
|
+
process.env.RINGCENTRAL_CLIENT_SECRET = 'client-secret';
|
|
622
|
+
process.env.APP_SERVER = 'https://app.example.com';
|
|
623
|
+
process.env.HASH_KEY = 'hash-key';
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
afterEach(() => {
|
|
627
|
+
delete process.env.RINGCENTRAL_SERVER;
|
|
628
|
+
delete process.env.RINGCENTRAL_CLIENT_ID;
|
|
629
|
+
delete process.env.RINGCENTRAL_CLIENT_SECRET;
|
|
630
|
+
delete process.env.APP_SERVER;
|
|
631
|
+
delete process.env.HASH_KEY;
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('getAdminReport builds aggregation rows and skips unnamed records', async () => {
|
|
635
|
+
const hashedAccountId = getHashValue('account', process.env.HASH_KEY);
|
|
636
|
+
await AdminConfigModel.create({
|
|
637
|
+
id: hashedAccountId,
|
|
638
|
+
adminAccessToken: 'admin-token',
|
|
639
|
+
adminRefreshToken: 'refresh-token',
|
|
640
|
+
adminTokenExpiry: new Date(Date.now() + 60 * 60 * 1000)
|
|
641
|
+
});
|
|
642
|
+
const getCallsAggregationData = jest.fn().mockResolvedValue({
|
|
643
|
+
data: {
|
|
644
|
+
groupedBy: 'Users',
|
|
645
|
+
records: [
|
|
646
|
+
{},
|
|
647
|
+
{
|
|
648
|
+
info: { name: 'Agent 1' },
|
|
649
|
+
counters: {
|
|
650
|
+
callsByDirection: { values: { inbound: 2, outbound: 1 } },
|
|
651
|
+
callsByResponse: { values: { answered: 1 } }
|
|
652
|
+
},
|
|
653
|
+
timers: {
|
|
654
|
+
allCalls: { values: 90 }
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
info: { name: 'Agent 2' },
|
|
659
|
+
counters: {
|
|
660
|
+
callsByDirection: { values: { inbound: 0, outbound: 0 } },
|
|
661
|
+
callsByResponse: { values: { answered: 0 } }
|
|
662
|
+
},
|
|
663
|
+
timers: {
|
|
664
|
+
allCalls: { values: 0 }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
RingCentral.mockImplementation(() => ({
|
|
671
|
+
getCallsAggregationData
|
|
672
|
+
}));
|
|
673
|
+
|
|
674
|
+
const result = await adminHandler.getAdminReport({
|
|
675
|
+
rcAccountId: 'account',
|
|
676
|
+
timezone: 'UTC',
|
|
677
|
+
timeFrom: '2026-07-01T00:00:00Z',
|
|
678
|
+
timeTo: '2026-07-31T23:59:59Z',
|
|
679
|
+
groupBy: 'undefined'
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
expect(result.groupedBy).toBe('Users');
|
|
683
|
+
expect(result.itemKeys).toEqual(['Agent 1', 'Agent 2']);
|
|
684
|
+
expect(result.callLogStats[0]).toMatchObject({
|
|
685
|
+
name: 'Agent 1',
|
|
686
|
+
inboundCallCount: 2,
|
|
687
|
+
outboundCallCount: 1,
|
|
688
|
+
answeredCallCount: 1,
|
|
689
|
+
answeredCallPercentage: '50.00%',
|
|
690
|
+
totalTalkTime: '90.00',
|
|
691
|
+
averageTalkTime: '30.00'
|
|
692
|
+
});
|
|
693
|
+
expect(result.callLogStats[1]).toMatchObject({
|
|
694
|
+
name: 'Agent 2',
|
|
695
|
+
answeredCallPercentage: '0%',
|
|
696
|
+
totalTalkTime: 0,
|
|
697
|
+
averageTalkTime: 0
|
|
698
|
+
});
|
|
699
|
+
expect(getCallsAggregationData).toHaveBeenCalledWith(expect.objectContaining({
|
|
700
|
+
groupBy: 'Company'
|
|
701
|
+
}));
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('getUserReport builds call and SMS stats', async () => {
|
|
705
|
+
const hashedAccountId = getHashValue('account', process.env.HASH_KEY);
|
|
706
|
+
await AdminConfigModel.create({
|
|
707
|
+
id: hashedAccountId,
|
|
708
|
+
adminAccessToken: 'admin-token',
|
|
709
|
+
adminRefreshToken: 'refresh-token',
|
|
710
|
+
adminTokenExpiry: new Date(Date.now() + 60 * 60 * 1000)
|
|
711
|
+
});
|
|
712
|
+
RingCentral.mockImplementation(() => ({
|
|
713
|
+
getCallLogData: jest.fn().mockResolvedValue({
|
|
714
|
+
records: [
|
|
715
|
+
{ direction: 'Inbound', result: 'Call connected', duration: 120 },
|
|
716
|
+
{ direction: 'Inbound', result: 'Missed', duration: 0 },
|
|
717
|
+
{ direction: 'Outbound', result: 'Accepted', duration: 60 }
|
|
718
|
+
]
|
|
719
|
+
}),
|
|
720
|
+
getSMSData: jest.fn().mockResolvedValue({
|
|
721
|
+
records: [
|
|
722
|
+
{ direction: 'Outbound' },
|
|
723
|
+
{ direction: 'Inbound' },
|
|
724
|
+
{ direction: 'Inbound' }
|
|
725
|
+
]
|
|
726
|
+
})
|
|
727
|
+
}));
|
|
728
|
+
|
|
729
|
+
const result = await adminHandler.getUserReport({
|
|
730
|
+
rcAccountId: 'account',
|
|
731
|
+
rcExtensionId: '101',
|
|
732
|
+
timezone: 'UTC',
|
|
733
|
+
timeFrom: '2026-07-01T00:00:00Z',
|
|
734
|
+
timeTo: '2026-07-31T23:59:59Z'
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
expect(result).toEqual({
|
|
738
|
+
callLogStats: {
|
|
739
|
+
inboundCallCount: 2,
|
|
740
|
+
outboundCallCount: 1,
|
|
741
|
+
answeredCallCount: 1,
|
|
742
|
+
answeredCallPercentage: '50.00%',
|
|
743
|
+
totalTalkTime: 3,
|
|
744
|
+
averageTalkTime: 1
|
|
745
|
+
},
|
|
746
|
+
smsLogStats: {
|
|
747
|
+
smsSentCount: 1,
|
|
748
|
+
smsReceivedCount: 2
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test('report helpers return empty data on missing env or provider errors', async () => {
|
|
754
|
+
delete process.env.RINGCENTRAL_SERVER;
|
|
755
|
+
await expect(adminHandler.getAdminReport({ rcAccountId: 'account' })).resolves.toEqual({
|
|
756
|
+
callLogStats: {}
|
|
757
|
+
});
|
|
758
|
+
await expect(adminHandler.getUserReport({ rcAccountId: 'account' })).resolves.toEqual({
|
|
759
|
+
callLogStats: {}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
process.env.RINGCENTRAL_SERVER = 'https://platform.example.com';
|
|
763
|
+
const hashedAccountId = getHashValue('account', process.env.HASH_KEY);
|
|
764
|
+
await AdminConfigModel.create({
|
|
765
|
+
id: hashedAccountId,
|
|
766
|
+
adminAccessToken: 'admin-token',
|
|
767
|
+
adminRefreshToken: 'refresh-token',
|
|
768
|
+
adminTokenExpiry: new Date(Date.now() + 60 * 60 * 1000)
|
|
769
|
+
});
|
|
770
|
+
RingCentral.mockImplementation(() => ({
|
|
771
|
+
getCallsAggregationData: jest.fn().mockRejectedValue(new Error('aggregation failed')),
|
|
772
|
+
getCallLogData: jest.fn().mockRejectedValue(new Error('call log failed'))
|
|
773
|
+
}));
|
|
774
|
+
|
|
775
|
+
await expect(adminHandler.getAdminReport({ rcAccountId: 'account' })).resolves.toEqual({
|
|
776
|
+
callLogStats: {}
|
|
777
|
+
});
|
|
778
|
+
await expect(adminHandler.getUserReport({ rcAccountId: 'account', rcExtensionId: '101' })).resolves.toBeNull();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('getUserMapping updates existing mappings, creates new matches, and handles oauth revocation', async () => {
|
|
782
|
+
await AdminConfigModel.create({
|
|
783
|
+
id: 'hashed-branch-mapping',
|
|
784
|
+
userMappings: [
|
|
785
|
+
{ crmUserId: 'crm-user-1', rcExtensionId: 'ext-existing' }
|
|
786
|
+
]
|
|
787
|
+
});
|
|
788
|
+
const user = {
|
|
789
|
+
platform: 'testCRM',
|
|
790
|
+
accessToken: 'api-key',
|
|
791
|
+
platformAdditionalInfo: {}
|
|
792
|
+
};
|
|
793
|
+
const connector = {
|
|
794
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
795
|
+
getBasicAuth: jest.fn(() => 'encoded-key'),
|
|
796
|
+
getUserList: jest.fn().mockResolvedValue([
|
|
797
|
+
{ id: 'crm-user-1', name: 'Existing User', email: 'existing@example.com' },
|
|
798
|
+
{ id: 'crm-user-2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
799
|
+
{ id: 'crm-user-3', name: 'No Match', email: 'nomatch@example.com' }
|
|
800
|
+
])
|
|
801
|
+
};
|
|
802
|
+
connectorRegistry.getConnector.mockReturnValue(connector);
|
|
803
|
+
|
|
804
|
+
const result = await adminHandler.getUserMapping({
|
|
805
|
+
user,
|
|
806
|
+
hashedRcAccountId: 'hashed-branch-mapping',
|
|
807
|
+
rcExtensionList: [
|
|
808
|
+
{ id: 'ext-existing', name: 'Existing RC', email: 'existing@example.com', extensionNumber: '100' },
|
|
809
|
+
{ id: 'ext-jane', firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com', extensionNumber: '101' }
|
|
810
|
+
]
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
expect(result).toHaveLength(3);
|
|
814
|
+
expect(result[0].rcUser[0].extensionId).toBe('ext-existing');
|
|
815
|
+
expect(result[1].rcUser[0].extensionId).toBe('ext-jane');
|
|
816
|
+
expect(result[2].rcUser).toEqual([]);
|
|
817
|
+
const updatedConfig = await AdminConfigModel.findByPk('hashed-branch-mapping');
|
|
818
|
+
expect(updatedConfig.userMappings).toEqual(expect.arrayContaining([
|
|
819
|
+
expect.objectContaining({ crmUserId: 'crm-user-2', rcExtensionId: ['ext-jane'] })
|
|
820
|
+
]));
|
|
821
|
+
|
|
822
|
+
connectorRegistry.getConnector.mockReturnValue({
|
|
823
|
+
getUserList: jest.fn(),
|
|
824
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
825
|
+
getOauthInfo: jest.fn().mockResolvedValue({})
|
|
826
|
+
});
|
|
827
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValueOnce(null);
|
|
828
|
+
await expect(adminHandler.getUserMapping({
|
|
829
|
+
user: {
|
|
830
|
+
platform: 'testCRM',
|
|
831
|
+
hostname: 'crm.example.com',
|
|
832
|
+
accessToken: 'expired-token',
|
|
833
|
+
platformAdditionalInfo: {}
|
|
834
|
+
},
|
|
835
|
+
hashedRcAccountId: 'hashed-branch-mapping',
|
|
836
|
+
rcExtensionList: []
|
|
837
|
+
})).resolves.toMatchObject({
|
|
838
|
+
successful: false,
|
|
839
|
+
isRevokeUserSession: true
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test('getUserMapping initializes mappings when no admin config exists and reports database errors', async () => {
|
|
844
|
+
const user = {
|
|
845
|
+
platform: 'testCRM',
|
|
846
|
+
accessToken: 'api-key',
|
|
847
|
+
platformAdditionalInfo: {}
|
|
848
|
+
};
|
|
849
|
+
connectorRegistry.getConnector.mockReturnValue({
|
|
850
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
851
|
+
getBasicAuth: jest.fn(() => 'encoded-key'),
|
|
852
|
+
getUserList: jest.fn().mockResolvedValue([
|
|
853
|
+
{ id: 'crm-user-10', name: 'Alex Agent', email: 'alex@example.com' }
|
|
854
|
+
])
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const result = await adminHandler.getUserMapping({
|
|
858
|
+
user,
|
|
859
|
+
hashedRcAccountId: 'hashed-new-mapping',
|
|
860
|
+
rcExtensionList: [
|
|
861
|
+
{ id: 'ext-alex', firstName: 'Alex', lastName: 'Agent', email: 'alex@example.com', extensionNumber: '201' }
|
|
862
|
+
]
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(result[0].rcUser[0].extensionId).toBe('ext-alex');
|
|
866
|
+
const newConfig = await AdminConfigModel.findByPk('hashed-new-mapping');
|
|
867
|
+
expect(newConfig.userMappings).toEqual([
|
|
868
|
+
{ crmUserId: 'crm-user-10', rcExtensionId: ['ext-alex'] }
|
|
869
|
+
]);
|
|
870
|
+
|
|
871
|
+
jest.spyOn(AdminConfigModel, 'findByPk').mockRejectedValueOnce(new Error('read failed'));
|
|
872
|
+
const errorResult = await adminHandler.getUserMapping({
|
|
873
|
+
user,
|
|
874
|
+
hashedRcAccountId: 'hashed-error',
|
|
875
|
+
rcExtensionList: []
|
|
876
|
+
});
|
|
877
|
+
expect(errorResult.successful).toBe(false);
|
|
878
|
+
AdminConfigModel.findByPk.mockRestore();
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
test('reinitializeUserMapping handles missing capability, proxy limits, oauth revoke, apiKey remap, and update failure', async () => {
|
|
882
|
+
const baseUser = {
|
|
883
|
+
platform: 'testCRM',
|
|
884
|
+
accessToken: 'api-key',
|
|
885
|
+
platformAdditionalInfo: {}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
connectorRegistry.getConnector.mockReturnValueOnce({});
|
|
889
|
+
await expect(adminHandler.reinitializeUserMapping({
|
|
890
|
+
user: baseUser,
|
|
891
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
892
|
+
rcExtensionList: []
|
|
893
|
+
})).resolves.toEqual([]);
|
|
894
|
+
|
|
895
|
+
connectorRegistry.getConnector.mockReturnValueOnce({ getUserList: jest.fn() });
|
|
896
|
+
Connector.getProxyConfig.mockResolvedValueOnce({ operations: {} });
|
|
897
|
+
await expect(adminHandler.reinitializeUserMapping({
|
|
898
|
+
user: {
|
|
899
|
+
...baseUser,
|
|
900
|
+
platformAdditionalInfo: { proxyId: 'proxy-1' }
|
|
901
|
+
},
|
|
902
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
903
|
+
rcExtensionList: []
|
|
904
|
+
})).resolves.toEqual([]);
|
|
905
|
+
|
|
906
|
+
connectorRegistry.getConnector.mockReturnValueOnce({
|
|
907
|
+
getUserList: jest.fn(),
|
|
908
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
909
|
+
getOauthInfo: jest.fn().mockResolvedValue({})
|
|
910
|
+
});
|
|
911
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValueOnce(null);
|
|
912
|
+
await expect(adminHandler.reinitializeUserMapping({
|
|
913
|
+
user: {
|
|
914
|
+
...baseUser,
|
|
915
|
+
hostname: 'crm.example.com'
|
|
916
|
+
},
|
|
917
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
918
|
+
rcExtensionList: []
|
|
919
|
+
})).resolves.toMatchObject({
|
|
920
|
+
successful: false,
|
|
921
|
+
isRevokeUserSession: true
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
connectorRegistry.getConnector.mockReturnValueOnce({
|
|
925
|
+
getUserList: jest.fn().mockResolvedValue([
|
|
926
|
+
{ id: 'crm-user-20', name: 'Matched User', email: 'matched@example.com' },
|
|
927
|
+
{ id: 'crm-user-21', name: 'Unmatched User', email: 'unmatched@example.com' }
|
|
928
|
+
]),
|
|
929
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
930
|
+
getBasicAuth: jest.fn(() => 'encoded-key')
|
|
931
|
+
});
|
|
932
|
+
const result = await adminHandler.reinitializeUserMapping({
|
|
933
|
+
user: baseUser,
|
|
934
|
+
hashedRcAccountId: 'hashed-reinit',
|
|
935
|
+
rcExtensionList: [
|
|
936
|
+
{ id: 'ext-20', firstName: 'Matched', lastName: 'User', email: 'matched@example.com', extensionNumber: '301' }
|
|
937
|
+
]
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
expect(result).toHaveLength(2);
|
|
941
|
+
expect(result[0].rcUser[0].extensionId).toBe('ext-20');
|
|
942
|
+
expect(result[1].rcUser).toEqual([]);
|
|
943
|
+
|
|
944
|
+
connectorRegistry.getConnector.mockReturnValueOnce({
|
|
945
|
+
getUserList: jest.fn().mockResolvedValue([]),
|
|
946
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
947
|
+
getBasicAuth: jest.fn(() => 'encoded-key')
|
|
948
|
+
});
|
|
949
|
+
jest.spyOn(AdminConfigModel, 'create').mockRejectedValueOnce(new Error('write failed'));
|
|
950
|
+
const failure = await adminHandler.reinitializeUserMapping({
|
|
951
|
+
user: baseUser,
|
|
952
|
+
hashedRcAccountId: 'hashed-reinit-fail',
|
|
953
|
+
rcExtensionList: []
|
|
954
|
+
});
|
|
955
|
+
expect(failure.successful).toBe(false);
|
|
956
|
+
AdminConfigModel.create.mockRestore();
|
|
957
|
+
});
|
|
958
|
+
});
|
|
615
959
|
});
|
|
616
960
|
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
jest.mock('../../models/userModel', () => ({
|
|
2
|
+
UserModel: {
|
|
3
|
+
findByPk: jest.fn()
|
|
4
|
+
}
|
|
5
|
+
}));
|
|
6
|
+
jest.mock('../../connector/registry', () => ({
|
|
7
|
+
getConnector: jest.fn()
|
|
8
|
+
}));
|
|
9
|
+
jest.mock('../../lib/oauth', () => ({
|
|
10
|
+
getOAuthApp: jest.fn(() => ({ app: true })),
|
|
11
|
+
checkAndRefreshAccessToken: jest.fn()
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('../../models/dynamo/connectorSchema', () => ({
|
|
14
|
+
Connector: {
|
|
15
|
+
getProxyConfig: jest.fn()
|
|
16
|
+
}
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const appointmentHandler = require('../../handlers/appointment');
|
|
20
|
+
const { UserModel } = require('../../models/userModel');
|
|
21
|
+
const connectorRegistry = require('../../connector/registry');
|
|
22
|
+
const oauth = require('../../lib/oauth');
|
|
23
|
+
const { Connector } = require('../../models/dynamo/connectorSchema');
|
|
24
|
+
|
|
25
|
+
describe('Appointment Handler', () => {
|
|
26
|
+
const baseUser = {
|
|
27
|
+
id: 'user-1',
|
|
28
|
+
hostname: 'crm.example.com',
|
|
29
|
+
accessToken: 'access-token',
|
|
30
|
+
platformAdditionalInfo: {}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function mockApiKeyConnector(overrides = {}) {
|
|
38
|
+
const connector = {
|
|
39
|
+
getAuthType: jest.fn().mockResolvedValue('apiKey'),
|
|
40
|
+
getBasicAuth: jest.fn(() => 'encoded-key'),
|
|
41
|
+
listAppointments: jest.fn().mockResolvedValue({ appointments: [{ id: 'appt-1' }] }),
|
|
42
|
+
createAppointment: jest.fn().mockResolvedValue({ appointmentId: 'appt-2', appointment: { id: 'appt-2' } }),
|
|
43
|
+
updateAppointment: jest.fn().mockResolvedValue({ appointment: { id: 'appt-3' } }),
|
|
44
|
+
refreshAppointment: jest.fn().mockResolvedValue({ appointment: { id: 'appt-4' } }),
|
|
45
|
+
confirmAppointment: jest.fn().mockResolvedValue({ appointment: { id: 'appt-5' } }),
|
|
46
|
+
cancelAppointment: jest.fn().mockResolvedValue({ appointment: { id: 'appt-6' } }),
|
|
47
|
+
...overrides
|
|
48
|
+
};
|
|
49
|
+
connectorRegistry.getConnector.mockReturnValue(connector);
|
|
50
|
+
return connector;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('returns database warning when user lookup fails', async () => {
|
|
54
|
+
UserModel.findByPk.mockRejectedValueOnce(new Error('db unavailable'));
|
|
55
|
+
|
|
56
|
+
const result = await appointmentHandler.listAppointments({
|
|
57
|
+
platform: 'testCRM',
|
|
58
|
+
userId: 'user-1'
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.successful).toBe(false);
|
|
62
|
+
expect(result.returnMessage.messageType).toBe('warning');
|
|
63
|
+
expect(result.returnMessage.message).toBe('Database operation failed');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('returns warning when user is missing or session has no token', async () => {
|
|
67
|
+
UserModel.findByPk.mockResolvedValueOnce(null);
|
|
68
|
+
|
|
69
|
+
await expect(appointmentHandler.listAppointments({
|
|
70
|
+
platform: 'testCRM',
|
|
71
|
+
userId: 'missing-user'
|
|
72
|
+
})).resolves.toMatchObject({
|
|
73
|
+
successful: false,
|
|
74
|
+
returnMessage: {
|
|
75
|
+
message: 'User not found'
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
UserModel.findByPk.mockResolvedValueOnce({ id: 'user-1' });
|
|
80
|
+
const noTokenResult = await appointmentHandler.createAppointment({
|
|
81
|
+
platform: 'testCRM',
|
|
82
|
+
userId: 'user-1',
|
|
83
|
+
payload: { title: 'Meeting' }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(noTokenResult.successful).toBe(false);
|
|
87
|
+
expect(noTokenResult.returnMessage.message).toBe('User not found');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('listAppointments resolves oauth auth with proxy config and returns connector result', async () => {
|
|
91
|
+
const user = {
|
|
92
|
+
...baseUser,
|
|
93
|
+
platformAdditionalInfo: {
|
|
94
|
+
proxyId: 'proxy-1',
|
|
95
|
+
tokenUrl: 'https://token.example.com'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const refreshedUser = {
|
|
99
|
+
...user,
|
|
100
|
+
accessToken: 'fresh-token'
|
|
101
|
+
};
|
|
102
|
+
const connector = {
|
|
103
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
104
|
+
getOauthInfo: jest.fn().mockResolvedValue({ tokenUrl: 'https://token.example.com' }),
|
|
105
|
+
listAppointments: jest.fn().mockResolvedValue({
|
|
106
|
+
appointments: [{ id: 'appt-1' }],
|
|
107
|
+
returnMessage: {
|
|
108
|
+
messageType: 'success',
|
|
109
|
+
message: 'Listed'
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
UserModel.findByPk.mockResolvedValue(user);
|
|
114
|
+
Connector.getProxyConfig.mockResolvedValue({ id: 'proxy-1' });
|
|
115
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValue(refreshedUser);
|
|
116
|
+
connectorRegistry.getConnector.mockReturnValue(connector);
|
|
117
|
+
|
|
118
|
+
const result = await appointmentHandler.listAppointments({
|
|
119
|
+
platform: 'testCRM',
|
|
120
|
+
userId: 'user-1',
|
|
121
|
+
range: { startDate: '2026-07-01' },
|
|
122
|
+
mineOnly: true,
|
|
123
|
+
forceSync: true
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(connector.getOauthInfo).toHaveBeenCalledWith({
|
|
127
|
+
tokenUrl: 'https://token.example.com',
|
|
128
|
+
hostname: 'crm.example.com',
|
|
129
|
+
proxyId: 'proxy-1',
|
|
130
|
+
proxyConfig: { id: 'proxy-1' }
|
|
131
|
+
});
|
|
132
|
+
expect(connector.listAppointments).toHaveBeenCalledWith({
|
|
133
|
+
user: refreshedUser,
|
|
134
|
+
authHeader: 'Bearer fresh-token',
|
|
135
|
+
range: { startDate: '2026-07-01' },
|
|
136
|
+
mineOnly: true,
|
|
137
|
+
forceSync: true,
|
|
138
|
+
proxyConfig: { id: 'proxy-1' }
|
|
139
|
+
});
|
|
140
|
+
expect(result).toMatchObject({
|
|
141
|
+
successful: true,
|
|
142
|
+
appointments: [{ id: 'appt-1' }]
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('oauth auth returns revoke response when token refresh fails', async () => {
|
|
147
|
+
const connector = {
|
|
148
|
+
getAuthType: jest.fn().mockResolvedValue('oauth'),
|
|
149
|
+
getOauthInfo: jest.fn().mockResolvedValue({})
|
|
150
|
+
};
|
|
151
|
+
UserModel.findByPk.mockResolvedValue(baseUser);
|
|
152
|
+
connectorRegistry.getConnector.mockReturnValue(connector);
|
|
153
|
+
oauth.checkAndRefreshAccessToken.mockResolvedValue(null);
|
|
154
|
+
|
|
155
|
+
const result = await appointmentHandler.createAppointment({
|
|
156
|
+
platform: 'testCRM',
|
|
157
|
+
userId: 'user-1',
|
|
158
|
+
payload: { title: 'Meeting' }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result).toMatchObject({
|
|
162
|
+
successful: false,
|
|
163
|
+
isRevokeUserSession: true,
|
|
164
|
+
returnMessage: {
|
|
165
|
+
message: 'User session expired. Please connect again.'
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('apiKey auth is used by create, update, refresh, confirm, and cancel operations', async () => {
|
|
171
|
+
UserModel.findByPk.mockResolvedValue(baseUser);
|
|
172
|
+
const connector = mockApiKeyConnector();
|
|
173
|
+
|
|
174
|
+
const createResult = await appointmentHandler.createAppointment({
|
|
175
|
+
platform: 'testCRM',
|
|
176
|
+
userId: 'user-1',
|
|
177
|
+
payload: { title: 'Create' }
|
|
178
|
+
});
|
|
179
|
+
const updateResult = await appointmentHandler.updateAppointment({
|
|
180
|
+
platform: 'testCRM',
|
|
181
|
+
userId: 'user-1',
|
|
182
|
+
appointmentId: 'appt-2',
|
|
183
|
+
patchBody: { title: 'Update' }
|
|
184
|
+
});
|
|
185
|
+
const refreshResult = await appointmentHandler.refreshAppointment({
|
|
186
|
+
platform: 'testCRM',
|
|
187
|
+
userId: 'user-1',
|
|
188
|
+
appointmentId: 'appt-3'
|
|
189
|
+
});
|
|
190
|
+
const confirmResult = await appointmentHandler.confirmAppointment({
|
|
191
|
+
platform: 'testCRM',
|
|
192
|
+
userId: 'user-1',
|
|
193
|
+
appointmentId: 'appt-4'
|
|
194
|
+
});
|
|
195
|
+
const cancelResult = await appointmentHandler.cancelAppointment({
|
|
196
|
+
platform: 'testCRM',
|
|
197
|
+
userId: 'user-1',
|
|
198
|
+
appointmentId: 'appt-5'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(createResult.appointmentId).toBe('appt-2');
|
|
202
|
+
expect(updateResult.appointment).toEqual({ id: 'appt-3' });
|
|
203
|
+
expect(refreshResult.appointment).toEqual({ id: 'appt-4' });
|
|
204
|
+
expect(confirmResult.appointment).toEqual({ id: 'appt-5' });
|
|
205
|
+
expect(cancelResult.appointment).toEqual({ id: 'appt-6' });
|
|
206
|
+
expect(connector.getBasicAuth).toHaveBeenCalledWith({ apiKey: 'access-token' });
|
|
207
|
+
expect(connector.createAppointment).toHaveBeenCalledWith({
|
|
208
|
+
user: baseUser,
|
|
209
|
+
authHeader: 'Basic encoded-key',
|
|
210
|
+
payload: { title: 'Create' },
|
|
211
|
+
proxyConfig: null
|
|
212
|
+
});
|
|
213
|
+
expect(connector.updateAppointment).toHaveBeenCalledWith({
|
|
214
|
+
user: baseUser,
|
|
215
|
+
authHeader: 'Basic encoded-key',
|
|
216
|
+
appointmentId: 'appt-2',
|
|
217
|
+
patchBody: { title: 'Update' },
|
|
218
|
+
proxyConfig: null
|
|
219
|
+
});
|
|
220
|
+
expect(connector.refreshAppointment).toHaveBeenCalledWith({
|
|
221
|
+
user: baseUser,
|
|
222
|
+
authHeader: 'Basic encoded-key',
|
|
223
|
+
appointmentId: 'appt-3',
|
|
224
|
+
proxyConfig: null
|
|
225
|
+
});
|
|
226
|
+
expect(connector.confirmAppointment).toHaveBeenCalledWith({
|
|
227
|
+
user: baseUser,
|
|
228
|
+
authHeader: 'Basic encoded-key',
|
|
229
|
+
appointmentId: 'appt-4',
|
|
230
|
+
proxyConfig: null
|
|
231
|
+
});
|
|
232
|
+
expect(connector.cancelAppointment).toHaveBeenCalledWith({
|
|
233
|
+
user: baseUser,
|
|
234
|
+
authHeader: 'Basic encoded-key',
|
|
235
|
+
appointmentId: 'appt-5',
|
|
236
|
+
proxyConfig: null
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('operation errors are mapped through API error handler', async () => {
|
|
241
|
+
UserModel.findByPk.mockResolvedValue(baseUser);
|
|
242
|
+
mockApiKeyConnector({
|
|
243
|
+
updateAppointment: jest.fn().mockRejectedValue({
|
|
244
|
+
response: {
|
|
245
|
+
status: 429
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const result = await appointmentHandler.updateAppointment({
|
|
251
|
+
platform: 'testCRM',
|
|
252
|
+
userId: 'user-1',
|
|
253
|
+
appointmentId: 'appt-2',
|
|
254
|
+
patchBody: { title: 'Update' }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(result.successful).toBe(false);
|
|
258
|
+
expect(result.returnMessage.messageType).toBe('warning');
|
|
259
|
+
});
|
|
260
|
+
});
|