@unito/integration-sdk 2.3.13 → 2.3.15

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.
@@ -1,5 +1,8 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
+ import { Readable } from 'stream';
4
+ import nock from 'nock';
5
+ import https from 'https';
3
6
 
4
7
  import { Provider } from '../../src/resources/provider.js';
5
8
  import * as HttpErrors from '../../src/httpErrors.js';
@@ -26,6 +29,24 @@ describe('Provider', () => {
26
29
 
27
30
  const logger = new Logger();
28
31
 
32
+ // Helper to spy on https.request and capture options
33
+ const spyOnHttpsRequest = (): { getCapturedOptions: () => any; restore: () => void } => {
34
+ let capturedOptions: any;
35
+ const originalRequest = https.request;
36
+
37
+ (https as any).request = function (options: any, callback: any) {
38
+ capturedOptions = options;
39
+ return originalRequest.call(https, options, callback);
40
+ };
41
+
42
+ return {
43
+ getCapturedOptions: () => capturedOptions,
44
+ restore: () => {
45
+ (https as any).request = originalRequest;
46
+ },
47
+ };
48
+ };
49
+
29
50
  it('get', async context => {
30
51
  const response = new Response('{"data": "value"}', {
31
52
  status: 200,
@@ -36,7 +57,7 @@ describe('Provider', () => {
36
57
 
37
58
  const actualResponse = await provider.get('/endpoint', {
38
59
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
39
- logger: logger,
60
+ logger,
40
61
  signal: new AbortController().signal,
41
62
  additionnalheaders: { 'X-Additional-Header': 'value1' },
42
63
  });
@@ -69,7 +90,7 @@ describe('Provider', () => {
69
90
 
70
91
  const actualResponse = await provider.get('/endpoint', {
71
92
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
72
- logger: logger,
93
+ logger,
73
94
  signal: new AbortController().signal,
74
95
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'text/html; charset=UTF-8' },
75
96
  });
@@ -104,7 +125,7 @@ describe('Provider', () => {
104
125
 
105
126
  const actualResponse = await provider.get('/endpoint', {
106
127
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
107
- logger: logger,
128
+ logger,
108
129
  signal: new AbortController().signal,
109
130
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/schema+json; charset=UTF-8' },
110
131
  });
@@ -139,7 +160,7 @@ describe('Provider', () => {
139
160
 
140
161
  const actualResponse = await provider.get('/endpoint', {
141
162
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
142
- logger: logger,
163
+ logger,
143
164
  signal: new AbortController().signal,
144
165
  additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/swagger+json; charset=UTF-8' },
145
166
  });
@@ -174,7 +195,7 @@ describe('Provider', () => {
174
195
 
175
196
  const actualResponse = await provider.get('/endpoint', {
176
197
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
177
- logger: logger,
198
+ logger,
178
199
  signal: new AbortController().signal,
179
200
  additionnalheaders: { 'X-Additional-Header': 'value1' },
180
201
  });
@@ -209,7 +230,7 @@ describe('Provider', () => {
209
230
 
210
231
  const providerResponse = await provider.streamingGet('/endpoint/123', {
211
232
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
212
- logger: logger,
233
+ logger,
213
234
  signal: new AbortController().signal,
214
235
  additionnalheaders: {
215
236
  Accept: 'application/json',
@@ -232,7 +253,7 @@ describe('Provider', () => {
232
253
 
233
254
  const actualResponse = await provider.get('https://my-cdn.my-domain.com/file.png', {
234
255
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
235
- logger: logger,
256
+ logger,
236
257
  signal: new AbortController().signal,
237
258
  });
238
259
 
@@ -263,7 +284,7 @@ describe('Provider', () => {
263
284
 
264
285
  const actualResponse = await provider.get('', {
265
286
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
266
- logger: logger,
287
+ logger,
267
288
  signal: new AbortController().signal,
268
289
  });
269
290
 
@@ -299,7 +320,7 @@ describe('Provider', () => {
299
320
  },
300
321
  {
301
322
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
302
- logger: logger,
323
+ logger,
303
324
  signal: new AbortController().signal,
304
325
  additionnalheaders: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Additional-Header': 'value1' },
305
326
  },
@@ -340,7 +361,7 @@ describe('Provider', () => {
340
361
  ],
341
362
  {
342
363
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
343
- logger: logger,
364
+ logger,
344
365
  signal: new AbortController().signal,
345
366
  additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' },
346
367
  },
@@ -381,7 +402,7 @@ describe('Provider', () => {
381
402
  },
382
403
  {
383
404
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
384
- logger: logger,
405
+ logger,
385
406
  signal: new AbortController().signal,
386
407
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' },
387
408
  },
@@ -419,7 +440,7 @@ describe('Provider', () => {
419
440
  // What matters is that the body of put is a buffer
420
441
  const actualResponse = await provider.putBuffer('endpoint/123', buffer, {
421
442
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
422
- logger: logger,
443
+ logger,
423
444
  signal: new AbortController().signal,
424
445
  additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/octet-stream' },
425
446
  });
@@ -458,7 +479,7 @@ describe('Provider', () => {
458
479
  },
459
480
  {
460
481
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
461
- logger: logger,
482
+ logger,
462
483
  signal: new AbortController().signal,
463
484
  queryParams: { param1: 'value1', param2: 'value2' },
464
485
  additionnalheaders: { 'X-Additional-Header': 'value1' },
@@ -494,7 +515,7 @@ describe('Provider', () => {
494
515
 
495
516
  const actualResponse = await provider.delete('/endpoint/123', {
496
517
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
497
- logger: logger,
518
+ logger,
498
519
  signal: new AbortController().signal,
499
520
  additionnalheaders: { 'X-Additional-Header': 'value1' },
500
521
  });
@@ -530,7 +551,7 @@ describe('Provider', () => {
530
551
  '/webhook',
531
552
  {
532
553
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
533
- logger: logger,
554
+ logger,
534
555
  signal: new AbortController().signal,
535
556
  additionnalheaders: { 'X-Additional-Header': 'value1' },
536
557
  },
@@ -581,7 +602,7 @@ describe('Provider', () => {
581
602
 
582
603
  const options = {
583
604
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
584
- logger: logger,
605
+ logger,
585
606
  signal: new AbortController().signal,
586
607
  additionnalheaders: { 'X-Additional-Header': 'value1' },
587
608
  };
@@ -634,7 +655,7 @@ describe('Provider', () => {
634
655
 
635
656
  const options = {
636
657
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
637
- logger: logger,
658
+ logger,
638
659
  signal: new AbortController().signal,
639
660
  additionnalheaders: { 'X-Additional-Header': 'value1' },
640
661
  };
@@ -681,7 +702,7 @@ describe('Provider', () => {
681
702
 
682
703
  const options = {
683
704
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
684
- logger: logger,
705
+ logger,
685
706
  signal: new AbortController().signal,
686
707
  additionnalheaders: { 'X-Additional-Header': 'value1' },
687
708
  };
@@ -722,7 +743,7 @@ describe('Provider', () => {
722
743
 
723
744
  const options = {
724
745
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
725
- logger: logger,
746
+ logger,
726
747
  signal: new AbortController().signal,
727
748
  additionnalheaders: { 'X-Additional-Header': 'value1' },
728
749
  };
@@ -748,7 +769,7 @@ describe('Provider', () => {
748
769
 
749
770
  const providerResponse = await provider.get<{ validJson: boolean }>('/endpoint/123', {
750
771
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
751
- logger: logger,
772
+ logger,
752
773
  signal: new AbortController().signal,
753
774
  });
754
775
 
@@ -766,7 +787,7 @@ describe('Provider', () => {
766
787
 
767
788
  const providerResponse = await provider.get<{ validJson: boolean }>('/endpoint/123', {
768
789
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
769
- logger: logger,
790
+ logger,
770
791
  signal: new AbortController().signal,
771
792
  });
772
793
 
@@ -784,7 +805,7 @@ describe('Provider', () => {
784
805
 
785
806
  const providerResponse = await provider.streamingGet('/endpoint/123', {
786
807
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
787
- logger: logger,
808
+ logger,
788
809
  signal: new AbortController().signal,
789
810
  });
790
811
 
@@ -805,7 +826,7 @@ describe('Provider', () => {
805
826
  {},
806
827
  {
807
828
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
808
- logger: logger,
829
+ logger,
809
830
  signal: new AbortController().signal,
810
831
  },
811
832
  );
@@ -829,7 +850,7 @@ describe('Provider', () => {
829
850
  try {
830
851
  await provider.get('/endpoint/123', {
831
852
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
832
- logger: logger,
853
+ logger,
833
854
  signal: new AbortController().signal,
834
855
  });
835
856
  } catch (e) {
@@ -853,7 +874,7 @@ describe('Provider', () => {
853
874
  try {
854
875
  await provider.get('/endpoint/123', {
855
876
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
856
- logger: logger,
877
+ logger,
857
878
  signal: new AbortController().signal,
858
879
  });
859
880
  } catch (e) {
@@ -877,7 +898,7 @@ describe('Provider', () => {
877
898
  await provider.get('/endpoint/123', {
878
899
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
879
900
  signal: new AbortController().signal,
880
- logger: logger,
901
+ logger,
881
902
  });
882
903
  } catch (e) {
883
904
  error = e;
@@ -900,7 +921,7 @@ describe('Provider', () => {
900
921
  await provider.get('/endpoint/123', {
901
922
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
902
923
  signal: new AbortController().signal,
903
- logger: logger,
924
+ logger,
904
925
  });
905
926
  } catch (e) {
906
927
  error = e;
@@ -923,7 +944,7 @@ describe('Provider', () => {
923
944
  await provider.get('/endpoint/123', {
924
945
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
925
946
  signal: new AbortController().signal,
926
- logger: logger,
947
+ logger,
927
948
  });
928
949
  } catch (e) {
929
950
  error = e;
@@ -944,7 +965,7 @@ describe('Provider', () => {
944
965
  await provider.get('/endpoint/123', {
945
966
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
946
967
  signal: new AbortController().signal,
947
- logger: logger,
968
+ logger,
948
969
  });
949
970
  } catch (e) {
950
971
  error = e;
@@ -972,7 +993,7 @@ describe('Provider', () => {
972
993
  await provider.get('/endpoint/123', {
973
994
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
974
995
  signal: new AbortController().signal,
975
- logger: logger,
996
+ logger,
976
997
  });
977
998
  } catch (e) {
978
999
  error = e;
@@ -992,7 +1013,7 @@ describe('Provider', () => {
992
1013
  await provider.get('/endpoint/123', {
993
1014
  credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
994
1015
  signal: new AbortController().signal,
995
- logger: logger,
1016
+ logger,
996
1017
  });
997
1018
 
998
1019
  assert.equal(loggerStub.mock.callCount(), 1);
@@ -1001,4 +1022,383 @@ describe('Provider', () => {
1001
1022
  /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/,
1002
1023
  );
1003
1024
  });
1025
+
1026
+ // Stream upload will not load the data in memory and sending a binary in the body
1027
+ it('postStream streams data without buffering', async () => {
1028
+ const streamProvider = new Provider({
1029
+ prepareRequest: requestOptions => ({
1030
+ url: `https://www.${requestOptions.credentials.domain ?? 'myApi.com'}`,
1031
+ headers: {
1032
+ 'X-Custom-Provider-Header': 'value',
1033
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1034
+ },
1035
+ }),
1036
+ });
1037
+
1038
+ const testData = 'test binary data for streaming';
1039
+ const stream = Readable.from([testData]);
1040
+
1041
+ const scope = nock('https://www.myApi.com')
1042
+ .post('/upload', testData)
1043
+ .matchHeader('content-type', 'application/octet-stream')
1044
+ .matchHeader('accept', 'application/json')
1045
+ .matchHeader('x-custom-provider-header', 'value')
1046
+ .matchHeader('x-provider-credential-header', 'apikey#1111')
1047
+ .reply(201, { success: true, id: '12345' });
1048
+
1049
+ const response = await streamProvider.postStream('/upload', stream, {
1050
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1051
+ logger,
1052
+ });
1053
+
1054
+ assert.ok(scope.isDone(), 'HTTPS request should have been made');
1055
+ assert.equal(response.status, 201);
1056
+ assert.deepEqual(response.body, { success: true, id: '12345' });
1057
+ });
1058
+
1059
+ it('postStream with query params', async () => {
1060
+ const streamProvider = new Provider({
1061
+ prepareRequest: requestOptions => ({
1062
+ url: `https://www.myApi.com`,
1063
+ headers: {
1064
+ 'X-Custom-Provider-Header': 'value',
1065
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1066
+ },
1067
+ }),
1068
+ });
1069
+
1070
+ const testData = 'data with params';
1071
+ const stream = Readable.from([testData]);
1072
+
1073
+ const scope = nock('https://www.myApi.com')
1074
+ .post('/upload?key=value&format=json', testData)
1075
+ .reply(200, { uploaded: true });
1076
+
1077
+ const response = await streamProvider.postStream('/upload', stream, {
1078
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1079
+ logger,
1080
+ queryParams: { key: 'value', format: 'json' },
1081
+ });
1082
+
1083
+ assert.ok(scope.isDone());
1084
+ assert.equal(response.status, 200);
1085
+ assert.deepEqual(response.body, { uploaded: true });
1086
+ });
1087
+
1088
+ it('postStream handles error responses', async () => {
1089
+ const streamProvider = new Provider({
1090
+ prepareRequest: requestOptions => ({
1091
+ url: `https://www.myApi.com`,
1092
+ headers: {
1093
+ 'X-Custom-Provider-Header': 'value',
1094
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1095
+ },
1096
+ }),
1097
+ });
1098
+
1099
+ const stream = Readable.from(['error test data']);
1100
+
1101
+ nock('https://www.myApi.com').post('/upload').reply(400, 'Bad request error');
1102
+
1103
+ let error;
1104
+ try {
1105
+ await streamProvider.postStream('/upload', stream, {
1106
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1107
+ logger,
1108
+ });
1109
+ } catch (e) {
1110
+ error = e;
1111
+ }
1112
+
1113
+ assert.ok(error instanceof HttpErrors.BadRequestError);
1114
+ assert.equal(error.message, 'Bad request error');
1115
+ });
1116
+
1117
+ it('postStream with rate limiter', async () => {
1118
+ let rateLimiterCalled = false;
1119
+ let rateLimiterCredentials;
1120
+
1121
+ const mockRateLimiter = async (options: any, request: () => Promise<any>) => {
1122
+ rateLimiterCalled = true;
1123
+ rateLimiterCredentials = options.credentials;
1124
+ return request();
1125
+ };
1126
+
1127
+ const rateLimitedProvider = new Provider({
1128
+ prepareRequest: requestOptions => ({
1129
+ url: `https://www.myApi.com`,
1130
+ headers: {
1131
+ 'X-Custom-Provider-Header': 'value',
1132
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1133
+ },
1134
+ }),
1135
+ rateLimiter: mockRateLimiter,
1136
+ });
1137
+
1138
+ const testData = 'rate limited data';
1139
+ const stream = Readable.from([testData]);
1140
+
1141
+ nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1142
+
1143
+ await rateLimitedProvider.postStream('/upload', stream, {
1144
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1145
+ logger,
1146
+ });
1147
+
1148
+ assert.ok(rateLimiterCalled, 'Rate limiter should have been called');
1149
+ assert.deepEqual(rateLimiterCredentials, {
1150
+ apiKey: 'apikey#1111',
1151
+ unitoCredentialId: '123',
1152
+ });
1153
+ });
1154
+
1155
+ it('postStream sets timeout to 0 (no timeout)', async () => {
1156
+ const streamProvider = new Provider({
1157
+ prepareRequest: requestOptions => ({
1158
+ url: `https://www.myApi.com`,
1159
+ headers: {
1160
+ 'X-Custom-Provider-Header': 'value',
1161
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1162
+ },
1163
+ }),
1164
+ });
1165
+
1166
+ const testData = 'timeout test data';
1167
+ const stream = Readable.from([testData]);
1168
+
1169
+ const spy = spyOnHttpsRequest();
1170
+ const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1171
+
1172
+ try {
1173
+ await streamProvider.postStream('/upload', stream, {
1174
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1175
+ logger,
1176
+ });
1177
+
1178
+ assert.ok(scope.isDone());
1179
+ assert.equal(spy.getCapturedOptions().timeout, 0, 'Timeout should be set to 0 (no timeout)');
1180
+ } finally {
1181
+ spy.restore();
1182
+ }
1183
+ });
1184
+
1185
+ it('postStream handles AbortSignal for request cancellation', async () => {
1186
+ const streamProvider = new Provider({
1187
+ prepareRequest: requestOptions => ({
1188
+ url: `https://www.myApi.com`,
1189
+ headers: {
1190
+ 'X-Custom-Provider-Header': 'value',
1191
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1192
+ },
1193
+ }),
1194
+ });
1195
+
1196
+ const stream = Readable.from(['abort signal test']);
1197
+ const abortController = new AbortController();
1198
+
1199
+ // Simulate aborting the request immediately
1200
+ abortController.abort();
1201
+
1202
+ let error;
1203
+ try {
1204
+ await streamProvider.postStream('/upload', stream, {
1205
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1206
+ logger,
1207
+ signal: abortController.signal,
1208
+ });
1209
+ } catch (e) {
1210
+ error = e;
1211
+ }
1212
+
1213
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1214
+ assert.equal(error.message, 'Timeout');
1215
+ });
1216
+
1217
+ it('postStream handles AbortSignal timeout during request', async () => {
1218
+ const streamProvider = new Provider({
1219
+ prepareRequest: requestOptions => ({
1220
+ url: `https://www.myApi.com`,
1221
+ headers: {
1222
+ 'X-Custom-Provider-Header': 'value',
1223
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1224
+ },
1225
+ }),
1226
+ });
1227
+
1228
+ const stream = Readable.from(['timeout during request test']);
1229
+ const abortController = new AbortController();
1230
+
1231
+ // Delay response to simulate a slow server
1232
+ nock('https://www.myApi.com').post('/upload').delayConnection(100).reply(201, { success: true });
1233
+
1234
+ // Abort after 50ms
1235
+ setTimeout(() => abortController.abort(), 50);
1236
+
1237
+ let error;
1238
+ try {
1239
+ await streamProvider.postStream('/upload', stream, {
1240
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1241
+ logger,
1242
+ signal: abortController.signal,
1243
+ });
1244
+ } catch (e) {
1245
+ error = e;
1246
+ }
1247
+
1248
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1249
+ assert.equal(error.message, 'Timeout');
1250
+ });
1251
+
1252
+ it('postStream cleans up AbortSignal listener on success', async () => {
1253
+ const streamProvider = new Provider({
1254
+ prepareRequest: requestOptions => ({
1255
+ url: `https://www.myApi.com`,
1256
+ headers: {
1257
+ 'X-Custom-Provider-Header': 'value',
1258
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1259
+ },
1260
+ }),
1261
+ });
1262
+
1263
+ const testData = 'cleanup test';
1264
+ const stream = Readable.from([testData]);
1265
+ const abortController = new AbortController();
1266
+
1267
+ const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1268
+
1269
+ const response = await streamProvider.postStream('/upload', stream, {
1270
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1271
+ logger,
1272
+ signal: abortController.signal,
1273
+ });
1274
+
1275
+ assert.ok(scope.isDone());
1276
+ assert.equal(response.status, 201);
1277
+
1278
+ // Verify the listener was removed by checking that aborting after completion
1279
+ // doesn't cause any side effects (if listener wasn't removed, this could cause issues)
1280
+ assert.doesNotThrow(() => {
1281
+ abortController.abort();
1282
+ }, 'Aborting after completion should not throw or cause issues');
1283
+
1284
+ // Verify the signal is indeed aborted
1285
+ assert.ok(abortController.signal.aborted, 'Signal should be aborted');
1286
+ });
1287
+
1288
+ it('postForm handles AbortSignal for request cancellation', async () => {
1289
+ const FormData = (await import('form-data')).default;
1290
+
1291
+ const formProvider = new Provider({
1292
+ prepareRequest: requestOptions => ({
1293
+ url: `https://www.myApi.com`,
1294
+ headers: {
1295
+ 'X-Custom-Provider-Header': 'value',
1296
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1297
+ },
1298
+ }),
1299
+ });
1300
+
1301
+ const form = new FormData();
1302
+ form.append('field1', 'value1');
1303
+ form.append('field2', 'value2');
1304
+
1305
+ const abortController = new AbortController();
1306
+
1307
+ // Simulate aborting the request immediately
1308
+ abortController.abort();
1309
+
1310
+ let error;
1311
+ try {
1312
+ await formProvider.postForm('/upload-form', form, {
1313
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1314
+ logger,
1315
+ signal: abortController.signal,
1316
+ });
1317
+ } catch (e) {
1318
+ error = e;
1319
+ }
1320
+
1321
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1322
+ assert.equal(error.message, 'Timeout');
1323
+ });
1324
+
1325
+ it('postForm handles AbortSignal timeout during request', async () => {
1326
+ const FormData = (await import('form-data')).default;
1327
+
1328
+ const formProvider = new Provider({
1329
+ prepareRequest: requestOptions => ({
1330
+ url: `https://www.myApi.com`,
1331
+ headers: {
1332
+ 'X-Custom-Provider-Header': 'value',
1333
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1334
+ },
1335
+ }),
1336
+ });
1337
+
1338
+ const form = new FormData();
1339
+ form.append('field1', 'value1');
1340
+
1341
+ const abortController = new AbortController();
1342
+
1343
+ // Mock a delayed response
1344
+ nock('https://www.myApi.com').post('/upload-form').delayConnection(100).reply(201, { success: true, id: '12345' });
1345
+
1346
+ // Abort after 50ms
1347
+ setTimeout(() => abortController.abort(), 50);
1348
+
1349
+ let error;
1350
+ try {
1351
+ await formProvider.postForm('/upload-form', form, {
1352
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1353
+ logger,
1354
+ signal: abortController.signal,
1355
+ });
1356
+ } catch (e) {
1357
+ error = e;
1358
+ }
1359
+
1360
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1361
+ assert.equal(error.message, 'Timeout');
1362
+ });
1363
+
1364
+ it('postForm successfully completes with AbortSignal provided', async () => {
1365
+ const FormData = (await import('form-data')).default;
1366
+
1367
+ const formProvider = new Provider({
1368
+ prepareRequest: requestOptions => ({
1369
+ url: `https://www.myApi.com`,
1370
+ headers: {
1371
+ 'X-Custom-Provider-Header': 'value',
1372
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey as string,
1373
+ },
1374
+ }),
1375
+ });
1376
+
1377
+ const form = new FormData();
1378
+ form.append('field1', 'value1');
1379
+ form.append('field2', 'value2');
1380
+
1381
+ const abortController = new AbortController();
1382
+
1383
+ const scope = nock('https://www.myApi.com').post('/upload-form').reply(201, { success: true, id: '12345' });
1384
+
1385
+ const response = await formProvider.postForm('/upload-form', form, {
1386
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1387
+ logger,
1388
+ signal: abortController.signal,
1389
+ });
1390
+
1391
+ assert.ok(scope.isDone());
1392
+ assert.equal(response.status, 201);
1393
+ assert.deepEqual(response.body, { success: true, id: '12345' });
1394
+
1395
+ // Verify the listener was removed by checking that aborting after completion
1396
+ // doesn't cause any side effects
1397
+ assert.doesNotThrow(() => {
1398
+ abortController.abort();
1399
+ }, 'Aborting after completion should not throw or cause issues');
1400
+
1401
+ // Verify the signal is indeed aborted
1402
+ assert.ok(abortController.signal.aborted, 'Signal should be aborted');
1403
+ });
1004
1404
  });