@app-connect/core 1.7.1 → 1.7.3

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.
@@ -41,6 +41,11 @@ async function composeCallLog(params) {
41
41
  startTime,
42
42
  duration,
43
43
  result,
44
+ ringSenseTranscript,
45
+ ringSenseSummary,
46
+ ringSenseAIScore,
47
+ ringSenseBulletedSummary,
48
+ ringSenseLink,
44
49
  platform
45
50
  } = params;
46
51
 
@@ -62,14 +67,18 @@ async function composeCallLog(params) {
62
67
  }
63
68
 
64
69
  if (userSettings?.addRingCentralUserName?.value) {
65
- const ringcentralUsername = (callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name) ?? '(pending...)';
66
- body = upsertRingCentralUserName({ body, userName: ringcentralUsername, logFormat });
70
+ const ringcentralUsername = (callLog.direction === 'Inbound' ? callLog?.to?.name : callLog?.from?.name) ?? null;
71
+ if (ringcentralUsername) {
72
+ body = upsertRingCentralUserName({ body, userName: ringcentralUsername, logFormat });
73
+ }
67
74
  }
68
75
 
69
- const ringcentralNumber = callLog.direction === 'Inbound' ? callLog?.to?.phoneNumber : callLog?.from?.phoneNumber;
70
- if (ringcentralNumber && (userSettings?.addRingCentralNumber?.value ?? false)) {
71
- const ringcentralExtensionNumber = callLog.direction === 'Inbound' ? callLog?.from?.extensionNumber : callLog?.to?.extensionNumber;
72
- body = upsertRingCentralNumberAndExtension({ body, number: ringcentralNumber, extension: ringcentralExtensionNumber ?? '', logFormat });
76
+ if (userSettings?.addRingCentralNumber?.value ?? false) {
77
+ const ringcentralNumber = callLog.direction === 'Inbound' ? callLog?.to?.phoneNumber : callLog?.from?.phoneNumber;
78
+ if (ringcentralNumber) {
79
+ const ringcentralExtensionNumber = callLog.direction === 'Inbound' ? callLog?.from?.extensionNumber : callLog?.to?.extensionNumber;
80
+ body = upsertRingCentralNumberAndExtension({ body, number: ringcentralNumber, extension: ringcentralExtensionNumber ?? '', logFormat });
81
+ }
73
82
  }
74
83
 
75
84
  if (subject && (userSettings?.addCallLogSubject?.value ?? true)) {
@@ -115,6 +124,26 @@ async function composeCallLog(params) {
115
124
  body = upsertTranscript({ body, transcript, logFormat });
116
125
  }
117
126
 
127
+ if (ringSenseTranscript && (userSettings?.addCallLogRingSenseRecordingTranscript?.value ?? true)) {
128
+ body = upsertRingSenseTranscript({ body, transcript: ringSenseTranscript, logFormat });
129
+ }
130
+
131
+ if (ringSenseSummary && (userSettings?.addCallLogRingSenseRecordingSummary?.value ?? true)) {
132
+ body = upsertRingSenseSummary({ body, summary: ringSenseSummary, logFormat });
133
+ }
134
+
135
+ if (ringSenseAIScore && (userSettings?.addCallLogRingSenseRecordingAIScore?.value ?? true)) {
136
+ body = upsertRingSenseAIScore({ body, score: ringSenseAIScore, logFormat });
137
+ }
138
+
139
+ if (ringSenseBulletedSummary && (userSettings?.addCallLogRingSenseRecordingBulletedSummary?.value ?? true)) {
140
+ body = upsertRingSenseBulletedSummary({ body, summary: ringSenseBulletedSummary, logFormat });
141
+ }
142
+
143
+ if (ringSenseLink && (userSettings?.addCallLogRingSenseRecordingLink?.value ?? true)) {
144
+ body = upsertRingSenseLink({ body, link: ringSenseLink, logFormat });
145
+ }
146
+
118
147
  if (callLog?.legs && (userSettings?.addCallLogLegs?.value ?? true)) {
119
148
  body = upsertLegs({ body, legs: callLog.legs, logFormat });
120
149
  }
@@ -382,7 +411,7 @@ function upsertCallDuration({ body, duration, logFormat }) {
382
411
  // More flexible regex that handles both with and without newlines
383
412
  const durationRegex = /- Duration: ([^\n-]+)(?=\n-|\n|$)/;
384
413
  if (durationRegex.test(result)) {
385
- result = result.replace(durationRegex, `- Duration: ${formattedDuration}\n`);
414
+ result = result.replace(durationRegex, `- Duration: ${formattedDuration}`);
386
415
  } else {
387
416
  result += `- Duration: ${formattedDuration}\n`;
388
417
  }
@@ -415,7 +444,7 @@ function upsertCallResult({ body, result, logFormat }) {
415
444
  // More flexible regex that handles both with and without newlines
416
445
  const resultRegex = /- Result: ([^\n-]+)(?=\n-|\n|$)/;
417
446
  if (resultRegex.test(bodyResult)) {
418
- bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}\n`);
447
+ bodyResult = bodyResult.replace(resultRegex, `- Result: ${result}`);
419
448
  } else {
420
449
  bodyResult += `- Result: ${result}\n`;
421
450
  }
@@ -429,8 +458,8 @@ function upsertCallRecording({ body, recordingLink, logFormat }) {
429
458
  let result = body;
430
459
 
431
460
  if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
432
- // More flexible regex that handles both <li> wrapped and unwrapped content
433
- const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
461
+ // More flexible regex that handles both <li> wrapped and unwrapped content, and existing <a> anchors
462
+ const recordingLinkRegex = /(?:<li>)?<b>Call recording link<\/b>:\s*(?:<a[^>]*>[^<]*<\/a>|[^<]+)(?:<\/li>|(?=<|$))/i;
434
463
  if (recordingLink) {
435
464
  if (recordingLinkRegex.test(result)) {
436
465
  if (recordingLink.startsWith('http')) {
@@ -615,6 +644,158 @@ function upsertLegs({ body, legs, logFormat }) {
615
644
  return result;
616
645
  }
617
646
 
647
+ function upsertRingSenseTranscript({ body, transcript, logFormat }) {
648
+ if (!transcript) return body;
649
+
650
+ let result = body;
651
+ const clearedTranscript = transcript.replace(/\n+$/, '');
652
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
653
+ const formattedTranscript = clearedTranscript.replace(/(?:\r\n|\r|\n)/g, '<br>');
654
+ const transcriptRegex = /<div><b>RingSense transcript<\/b><br>(.+?)<\/div>/;
655
+ if (transcriptRegex.test(result)) {
656
+ result = result.replace(transcriptRegex, `<div><b>RingSense transcript</b><br>${formattedTranscript}</div>`);
657
+ } else {
658
+ result += `<div><b>RingSense transcript</b><br>${formattedTranscript}</div>`;
659
+ }
660
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
661
+ const transcriptRegex = /### RingSense transcript\n([\s\S]*?)(?=\n### |\n$|$)/;
662
+ if (transcriptRegex.test(result)) {
663
+ result = result.replace(transcriptRegex, `### RingSense transcript\n${clearedTranscript}\n`);
664
+ } else {
665
+ result += `### RingSense transcript\n${clearedTranscript}\n`;
666
+ }
667
+ } else {
668
+ const transcriptRegex = /- RingSense transcript:([\s\S]*?)--- END/;
669
+ if (transcriptRegex.test(result)) {
670
+ result = result.replace(transcriptRegex, `- RingSense transcript:\n${clearedTranscript}\n--- END`);
671
+ } else {
672
+ result += `- RingSense transcript:\n${clearedTranscript}\n--- END\n`;
673
+ }
674
+ }
675
+ return result;
676
+ }
677
+
678
+ function upsertRingSenseSummary({ body, summary, logFormat }) {
679
+ if (!summary) return body;
680
+
681
+ let result = body;
682
+ // remove new line in last line of summary
683
+ const clearedSummary = summary.replace(/\n+$/, '');
684
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
685
+ const summaryRegex = /<div><b>RingSense summary<\/b><br>(.+?)<\/div>/;
686
+ const formattedSummary = clearedSummary.replace(/(?:\r\n|\r|\n)/g, '<br>');
687
+ if (summaryRegex.test(result)) {
688
+ result = result.replace(summaryRegex, `<div><b>RingSense summary</b><br>${formattedSummary}</div>`);
689
+ } else {
690
+ result += `<div><b>RingSense summary</b><br>${formattedSummary}</div>`;
691
+ }
692
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
693
+ const summaryRegex = /### RingSense summary\n([\s\S]*?)(?=\n### |\n$|$)/;
694
+ if (summaryRegex.test(result)) {
695
+ result = result.replace(summaryRegex, `### RingSense summary\n${summary}\n`);
696
+ } else {
697
+ result += `### RingSense summary\n${summary}\n`;
698
+ }
699
+ } else {
700
+ const summaryRegex = /- RingSense summary:([\s\S]*?)--- END/;
701
+ if (summaryRegex.test(result)) {
702
+ result = result.replace(summaryRegex, `- RingSense summary:\n${summary}\n--- END`);
703
+ } else {
704
+ result += `- RingSense summary:\n${summary}\n--- END\n`;
705
+ }
706
+ }
707
+ return result;
708
+ }
709
+
710
+ function upsertRingSenseAIScore({ body, score, logFormat }) {
711
+ if (!score) return body;
712
+
713
+ let result = body;
714
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
715
+ const scoreRegex = /(?:<li>)?<b>Call score<\/b>:\s*([^<\n]+)(?:<\/li>|(?=<|$))/i;
716
+ if (scoreRegex.test(result)) {
717
+ result = result.replace(scoreRegex, `<li><b>Call score</b>: ${score}</li>`);
718
+ } else {
719
+ result += `<li><b>Call score</b>: ${score}</li>`;
720
+ }
721
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
722
+ const scoreRegex = /\*\*Call score\*\*: [^\n]*\n*/;
723
+ if (scoreRegex.test(result)) {
724
+ result = result.replace(scoreRegex, `**Call score**: ${score}\n`);
725
+ } else {
726
+ result += `**Call score**: ${score}\n`;
727
+ }
728
+ } else {
729
+ const scoreRegex = /- Call score:\s*([^<\n]+)(?=\n|$)/i;
730
+ if (scoreRegex.test(result)) {
731
+ result = result.replace(scoreRegex, `- Call score: ${score}`);
732
+ } else {
733
+ result += `- Call score: ${score}\n`;
734
+ }
735
+ }
736
+ return result;
737
+ }
738
+
739
+ function upsertRingSenseBulletedSummary({ body, summary, logFormat }) {
740
+ if (!summary) return body;
741
+
742
+ let result = body;
743
+ const clearedSummary = summary.replace(/\n+$/, '');
744
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
745
+ const summaryRegex = /<div><b>RingSense bulleted summary<\/b><br>(.+?)<\/div>/;
746
+ const formattedSummary = clearedSummary.replace(/(?:\r\n|\r|\n)/g, '<br>');
747
+ if (summaryRegex.test(result)) {
748
+ result = result.replace(summaryRegex, `<div><b>RingSense bulleted summary</b><br>${formattedSummary}</div>`);
749
+ } else {
750
+ result += `<div><b>RingSense bulleted summary</b><br>${formattedSummary}</div>`;
751
+ }
752
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
753
+ const summaryRegex = /### RingSense bulleted summary\n([\s\S]*?)(?=\n### |\n$|$)/;
754
+ if (summaryRegex.test(result)) {
755
+ result = result.replace(summaryRegex, `### RingSense bulleted summary\n${summary}\n`);
756
+ } else {
757
+ result += `### RingSense bulleted summary\n${summary}\n`;
758
+ }
759
+ } else {
760
+ const summaryRegex = /- RingSense bulleted summary:\s*([^<\n]+)(?=\n|$)/i;
761
+ if (summaryRegex.test(result)) {
762
+ result = result.replace(summaryRegex, `- RingSense bulleted summary:\n${summary}\n--- END`);
763
+ } else {
764
+ result += `- RingSense bulleted summary:\n${summary}\n--- END\n`;
765
+ }
766
+ }
767
+ return result;
768
+ }
769
+
770
+ function upsertRingSenseLink({ body, link, logFormat }) {
771
+ if (!link) return body;
772
+
773
+ let result = body;
774
+ if (logFormat === LOG_DETAILS_FORMAT_TYPE.HTML) {
775
+ const linkRegex = /(?:<li>)?<b>RingSense recording link<\/b>:\s*(?:<a[^>]*>[^<]*<\/a>|[^<]+)(?:<\/li>|(?=<|$))/i;
776
+ if (linkRegex.test(result)) {
777
+ result = result.replace(linkRegex, `<li><b>RingSense recording link</b>: <a target="_blank" href="${link}">open</a></li>`);
778
+ } else {
779
+ result += `<li><b>RingSense recording link</b>: <a target="_blank" href="${link}">open</a></li>`;
780
+ }
781
+ } else if (logFormat === LOG_DETAILS_FORMAT_TYPE.MARKDOWN) {
782
+ const linkRegex = /\*\*RingSense recording link\*\*:\s*([^<\n]+)(?=\n|$)/i;
783
+ if (linkRegex.test(result)) {
784
+ result = result.replace(linkRegex, `**RingSense recording link**: ${link}\n`);
785
+ } else {
786
+ result += `**RingSense recording link**: ${link}\n`;
787
+ }
788
+ } else {
789
+ const linkRegex = /- RingSense recording link:\s*([^<\n]+)(?=\n|$)/i;
790
+ if (linkRegex.test(result)) {
791
+ result = result.replace(linkRegex, `- RingSense recording link: ${link}`);
792
+ } else {
793
+ result += `- RingSense recording link: ${link}\n`;
794
+ }
795
+ }
796
+ return result;
797
+ }
798
+
618
799
  module.exports = {
619
800
  composeCallLog,
620
801
  // Export individual upsert functions for backward compatibility
@@ -631,4 +812,9 @@ module.exports = {
631
812
  upsertAiNote,
632
813
  upsertTranscript,
633
814
  upsertLegs,
815
+ upsertRingSenseTranscript,
816
+ upsertRingSenseSummary,
817
+ upsertRingSenseAIScore,
818
+ upsertRingSenseBulletedSummary,
819
+ upsertRingSenseLink,
634
820
  };
package/lib/oauth.js CHANGED
@@ -3,7 +3,6 @@ const ClientOAuth2 = require('client-oauth2');
3
3
  const moment = require('moment');
4
4
  const { UserModel } = require('../models/userModel');
5
5
  const connectorRegistry = require('../connector/registry');
6
- const dynamoose = require('dynamoose');
7
6
 
8
7
  // oauthApp strategy is default to 'code' which use credentials to get accessCode, then exchange for accessToken and refreshToken.
9
8
  // To change to other strategies, please refer to: https://github.com/mulesoft-labs/js-client-oauth2
@@ -0,0 +1,146 @@
1
+ const dynamoose = require('dynamoose');
2
+ const crypto = require('crypto');
3
+
4
+ const CONNECTOR_STATUS = {
5
+ PRIVATE: 'private',
6
+ UNDER_REVIEW: 'under_review',
7
+ APPROVED: 'approved',
8
+ REJECTED: 'rejected',
9
+ };
10
+
11
+ const connectorSchema = new dynamoose.Schema({
12
+ accountId: {
13
+ type: String,
14
+ hashKey: true,
15
+ },
16
+ id: {
17
+ type: String,
18
+ rangeKey: true,
19
+ },
20
+ // Reference to original connector (for partition records)
21
+ originalAccountId: {
22
+ type: String,
23
+ required: false, // Only set for partition records (under_review, approved)
24
+ },
25
+ // Basic Information
26
+ name: {
27
+ type: String,
28
+ required: true,
29
+ },
30
+ displayName: String,
31
+ description: String,
32
+ iconUrl: String,
33
+ // Status and Workflow
34
+ status: {
35
+ type: String,
36
+ required: true,
37
+ enum: Object.values(CONNECTOR_STATUS),
38
+ default: CONNECTOR_STATUS.PRIVATE,
39
+ index: {
40
+ name: 'statusIdIndex',
41
+ global: true,
42
+ rangeKey: 'id',
43
+ project: ['accountId', 'name', 'displayName', 'developer', 'originalAccountId'],
44
+ },
45
+ },
46
+ creatorId: String,
47
+ // Developer Information
48
+ developer: {
49
+ type: Object,
50
+ schema: {
51
+ name: String,
52
+ websiteUrl: String,
53
+ supportUrl: String,
54
+ },
55
+ },
56
+ // Manifest Management
57
+ manifest: {
58
+ type: Object,
59
+ required: true,
60
+ },
61
+ proxyConfig: {
62
+ type: Object,
63
+ required: false,
64
+ },
65
+ proxyId: {
66
+ type: String,
67
+ index: {
68
+ name: 'proxyIdIndex',
69
+ global: true,
70
+ project: ['id', 'accountId', 'creatorId', 'name', 'displayName', 'status', 'developer', 'originalAccountId', 'proxyConfig'],
71
+ }
72
+ },
73
+ // Review and Approval
74
+ submittedAt: Number,
75
+ reviewedBy: String,
76
+ reviewedAt: Number,
77
+ reviewNotes: String,
78
+ rejectionReason: String,
79
+ demoAccounts: String,
80
+ // Usage and Analytics
81
+ usageCount: {
82
+ type: Number,
83
+ default: 0,
84
+ },
85
+ lastUsedAt: Number,
86
+ allowedAccounts: {
87
+ type: Array,
88
+ schema: [String],
89
+ },
90
+ encodedSecretKey: String,
91
+ }, {
92
+ saveUnknown: ['manifest.**', 'proxyConfig.**'],
93
+ timestamps: true,
94
+ });
95
+
96
+ const tableOptions = {
97
+ prefix: process.env.DEVELOPER_DYNAMODB_TABLE_PREFIX,
98
+ };
99
+
100
+ if (process.env.NODE_ENV === 'production') {
101
+ tableOptions.create = false;
102
+ tableOptions.waitForActive = false;
103
+ }
104
+
105
+ const Connector = dynamoose.model('connectors', connectorSchema, tableOptions);
106
+
107
+ function getDeveloperCipherKey() {
108
+ if (!process.env.DEVELOPER_APP_SERVER_SECRET_KEY) {
109
+ throw new Error('DEVELOPER_APP_SERVER_SECRET_KEY is not defined');
110
+ }
111
+ if (process.env.DEVELOPER_APP_SERVER_SECRET_KEY.length < 32) {
112
+ // pad secret key with spaces if it is less than 32 bytes
113
+ return process.env.DEVELOPER_APP_SERVER_SECRET_KEY.padEnd(32, ' ');
114
+ }
115
+ if (process.env.DEVELOPER_APP_SERVER_SECRET_KEY.length > 32) {
116
+ // truncate secret key if it is more than 32 bytes
117
+ return process.env.DEVELOPER_APP_SERVER_SECRET_KEY.slice(0, 32);
118
+ }
119
+ return process.env.DEVELOPER_APP_SERVER_SECRET_KEY;
120
+ }
121
+
122
+ function decode(encryptedData) {
123
+ const decipher = crypto.createDecipheriv('aes-256-cbc', getDeveloperCipherKey(), Buffer.alloc(16, 0));
124
+ return decipher.update(encryptedData, 'hex', 'utf8') + decipher.final('utf8');
125
+ }
126
+
127
+ // ADD static method to get connector by proxyId
128
+ Connector.getProxyConfig = async (proxyId) => {
129
+ const connectors = await Connector
130
+ .query('proxyId')
131
+ .eq(proxyId)
132
+ .using('proxyIdIndex')
133
+ .exec();
134
+ if (connectors.length > 0) {
135
+ const proxyConfig = connectors[0].proxyConfig;
136
+ const encodedSecretKey = connectors[0].encodedSecretKey;
137
+ const secretKey = encodedSecretKey ? decode(encodedSecretKey) : null;
138
+ if (secretKey) {
139
+ proxyConfig.secretKey = secretKey;
140
+ }
141
+ return proxyConfig;
142
+ }
143
+ return null;
144
+ };
145
+
146
+ exports.Connector = Connector;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@app-connect/core",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "RingCentral App Connect Core",
5
5
  "main": "index.js",
6
6
  "repository": {
package/releaseNotes.json CHANGED
@@ -1,4 +1,32 @@
1
1
  {
2
+ "1.7.3": {
3
+ "global": [
4
+ {
5
+ "type": "New",
6
+ "description": "RingSense data logging in server side logging"
7
+ },
8
+ {
9
+ "type": "New",
10
+ "description": "New phone setting item to group phone related settings"
11
+ },
12
+ {
13
+ "type": "Better",
14
+ "description": "HUD enabled by default"
15
+ },
16
+ {
17
+ "type": "Fix",
18
+ "description": "A small issue on contact search feature"
19
+ },
20
+ {
21
+ "type": "Fix",
22
+ "description": "A small issue on embedded URLs"
23
+ },
24
+ {
25
+ "type": "Fix",
26
+ "description": "Server-side logging logged calls cannot be edited for the first attempt"
27
+ }
28
+ ]
29
+ },
2
30
  "1.7.1": {
3
31
  "global": [
4
32
  {
@@ -11,6 +39,18 @@
11
39
  }
12
40
  ]
13
41
  },
42
+ "1.6.11":{
43
+ "global": [
44
+ {
45
+ "type": "Fix",
46
+ "description": "A small issue on contact search feature"
47
+ },
48
+ {
49
+ "type": "Fix",
50
+ "description": "A small issue on embedded URLs"
51
+ }
52
+ ]
53
+ },
14
54
  "1.6.10": {
15
55
  "global": [
16
56
  {
@@ -0,0 +1,93 @@
1
+ const path = require('path');
2
+
3
+ jest.mock('axios', () => jest.fn());
4
+ const axios = require('axios');
5
+
6
+ const {
7
+ renderTemplateString,
8
+ renderDeep,
9
+ joinUrl,
10
+ performRequest,
11
+ } = require('../../../connector/proxy/engine');
12
+
13
+ describe('proxy engine utilities', () => {
14
+ beforeEach(() => {
15
+ axios.mockReset();
16
+ });
17
+
18
+ test('renderTemplateString handles full and partial templates', () => {
19
+ const context = { a: { b: 123 }, name: 'Alice' };
20
+ expect(renderTemplateString('{{a.b}}', context)).toBe(123);
21
+ expect(renderTemplateString('Hello {{name}}', context)).toBe('Hello Alice');
22
+ expect(renderTemplateString('Missing {{x.y}} here', context)).toBe('Missing here');
23
+ });
24
+
25
+ test('renderDeep renders nested objects and arrays', () => {
26
+ const context = { id: 42, name: 'Alice', items: ['x', 'y'] };
27
+ const input = {
28
+ url: '/users/{{id}}',
29
+ body: { name: '{{name}}', tags: ['a', '{{items.1}}'] },
30
+ };
31
+ const out = renderDeep(input, context);
32
+ expect(out.url).toBe('/users/42');
33
+ expect(out.body.name).toBe('Alice');
34
+ expect(out.body.tags).toEqual(['a', 'y']);
35
+ });
36
+
37
+ test('joinUrl joins base and path and preserves absolute urls', () => {
38
+ expect(joinUrl('https://api.example.com', '/v1/items')).toBe('https://api.example.com/v1/items');
39
+ expect(joinUrl('https://api.example.com/', 'v1/items')).toBe('https://api.example.com/v1/items');
40
+ expect(joinUrl('', 'https://full.example.com/x')).toBe('https://full.example.com/x');
41
+ });
42
+
43
+ test('performRequest composes url, headers, params, body and auth', async () => {
44
+ axios.mockResolvedValue({ data: { ok: true } });
45
+ const config = {
46
+ secretKey: 'shh-key',
47
+ auth: {
48
+ type: 'apiKey',
49
+ scheme: 'Basic',
50
+ credentialTemplate: '{{apiKey}}',
51
+ encode: 'base64',
52
+ headerName: 'Authorization'
53
+ },
54
+ requestDefaults: {
55
+ baseUrl: 'https://api.example.com',
56
+ timeoutSeconds: 10,
57
+ defaultHeaders: { Accept: 'application/json', 'X-Secret-Key': '{{secretKey}}' }
58
+ },
59
+ operations: {
60
+ createThing: {
61
+ method: 'POST',
62
+ url: '/things/{{thingId}}',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ query: { search: '{{q}}' },
65
+ body: { id: '{{thingId}}', name: '{{name}}' }
66
+ }
67
+ }
68
+ };
69
+ const user = { accessToken: 'token-123' };
70
+ await performRequest({
71
+ config,
72
+ opName: 'createThing',
73
+ inputs: { thingId: 7, name: 'Widget', q: 'alpha' },
74
+ user,
75
+ authHeader: undefined
76
+ });
77
+
78
+ expect(axios).toHaveBeenCalledTimes(1);
79
+ const args = axios.mock.calls[0][0];
80
+ expect(args.url).toBe('https://api.example.com/things/7');
81
+ expect(args.method).toBe('POST');
82
+ expect(args.params).toEqual({ search: 'alpha' });
83
+ expect(args.data).toEqual({ id: 7, name: 'Widget' });
84
+ expect(args.timeout).toBe(10000);
85
+ expect(args.headers.Accept).toBe('application/json');
86
+ expect(args.headers['Content-Type']).toBe('application/json');
87
+ expect(args.headers['X-Secret-Key']).toBe('shh-key');
88
+ // Basic base64('token-123')
89
+ expect(args.headers.Authorization).toMatch(/^Basic /);
90
+ });
91
+ });
92
+
93
+