@unito/integration-sdk 2.3.14 → 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.
@@ -1369,6 +1369,21 @@ class Provider {
1369
1369
  request.on('error', error => {
1370
1370
  reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
1371
1371
  });
1372
+ if (options.signal) {
1373
+ const abortHandler = () => {
1374
+ request.destroy();
1375
+ reject(this.handleError(408, 'Timeout', options));
1376
+ };
1377
+ if (options.signal.aborted) {
1378
+ abortHandler();
1379
+ }
1380
+ options.signal.addEventListener('abort', abortHandler);
1381
+ request.on('close', () => {
1382
+ if (options.signal) {
1383
+ options.signal.removeEventListener('abort', abortHandler);
1384
+ }
1385
+ });
1386
+ }
1372
1387
  form.pipe(request);
1373
1388
  }
1374
1389
  catch (error) {
@@ -1423,6 +1438,7 @@ class Provider {
1423
1438
  path: urlObj.pathname + urlObj.search,
1424
1439
  method: 'POST',
1425
1440
  headers,
1441
+ timeout: 0,
1426
1442
  };
1427
1443
  const request = https.request(requestOptions, response => {
1428
1444
  response.setEncoding('utf8');
@@ -1462,6 +1478,21 @@ class Provider {
1462
1478
  request.destroy();
1463
1479
  safeReject(this.handleError(500, `Stream error: "${error}"`, options));
1464
1480
  });
1481
+ if (options.signal) {
1482
+ const abortHandler = () => {
1483
+ request.destroy();
1484
+ safeReject(this.handleError(408, 'Timeout', options));
1485
+ };
1486
+ if (options.signal.aborted) {
1487
+ abortHandler();
1488
+ }
1489
+ options.signal.addEventListener('abort', abortHandler);
1490
+ request.on('close', () => {
1491
+ if (options.signal) {
1492
+ options.signal.removeEventListener('abort', abortHandler);
1493
+ }
1494
+ });
1495
+ }
1465
1496
  // Stream the data directly without buffering
1466
1497
  stream.pipe(request);
1467
1498
  }
@@ -150,6 +150,21 @@ export class Provider {
150
150
  request.on('error', error => {
151
151
  reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
152
152
  });
153
+ if (options.signal) {
154
+ const abortHandler = () => {
155
+ request.destroy();
156
+ reject(this.handleError(408, 'Timeout', options));
157
+ };
158
+ if (options.signal.aborted) {
159
+ abortHandler();
160
+ }
161
+ options.signal.addEventListener('abort', abortHandler);
162
+ request.on('close', () => {
163
+ if (options.signal) {
164
+ options.signal.removeEventListener('abort', abortHandler);
165
+ }
166
+ });
167
+ }
153
168
  form.pipe(request);
154
169
  }
155
170
  catch (error) {
@@ -204,6 +219,7 @@ export class Provider {
204
219
  path: urlObj.pathname + urlObj.search,
205
220
  method: 'POST',
206
221
  headers,
222
+ timeout: 0,
207
223
  };
208
224
  const request = https.request(requestOptions, response => {
209
225
  response.setEncoding('utf8');
@@ -243,6 +259,21 @@ export class Provider {
243
259
  request.destroy();
244
260
  safeReject(this.handleError(500, `Stream error: "${error}"`, options));
245
261
  });
262
+ if (options.signal) {
263
+ const abortHandler = () => {
264
+ request.destroy();
265
+ safeReject(this.handleError(408, 'Timeout', options));
266
+ };
267
+ if (options.signal.aborted) {
268
+ abortHandler();
269
+ }
270
+ options.signal.addEventListener('abort', abortHandler);
271
+ request.on('close', () => {
272
+ if (options.signal) {
273
+ options.signal.removeEventListener('abort', abortHandler);
274
+ }
275
+ });
276
+ }
246
277
  // Stream the data directly without buffering
247
278
  stream.pipe(request);
248
279
  }
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
3
  import { Readable } from 'stream';
4
4
  import nock from 'nock';
5
+ import https from 'https';
5
6
  import { Provider } from '../../src/resources/provider.js';
6
7
  import * as HttpErrors from '../../src/httpErrors.js';
7
8
  import Logger from '../../src/resources/logger.js';
@@ -23,6 +24,21 @@ describe('Provider', () => {
23
24
  },
24
25
  });
25
26
  const logger = new Logger();
27
+ // Helper to spy on https.request and capture options
28
+ const spyOnHttpsRequest = () => {
29
+ let capturedOptions;
30
+ const originalRequest = https.request;
31
+ https.request = function (options, callback) {
32
+ capturedOptions = options;
33
+ return originalRequest.call(https, options, callback);
34
+ };
35
+ return {
36
+ getCapturedOptions: () => capturedOptions,
37
+ restore: () => {
38
+ https.request = originalRequest;
39
+ },
40
+ };
41
+ };
26
42
  it('get', async (context) => {
27
43
  const response = new Response('{"data": "value"}', {
28
44
  status: 200,
@@ -941,4 +957,212 @@ describe('Provider', () => {
941
957
  unitoCredentialId: '123',
942
958
  });
943
959
  });
960
+ it('postStream sets timeout to 0 (no timeout)', async () => {
961
+ const streamProvider = new Provider({
962
+ prepareRequest: requestOptions => ({
963
+ url: `https://www.myApi.com`,
964
+ headers: {
965
+ 'X-Custom-Provider-Header': 'value',
966
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
967
+ },
968
+ }),
969
+ });
970
+ const testData = 'timeout test data';
971
+ const stream = Readable.from([testData]);
972
+ const spy = spyOnHttpsRequest();
973
+ const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
974
+ try {
975
+ await streamProvider.postStream('/upload', stream, {
976
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
977
+ logger,
978
+ });
979
+ assert.ok(scope.isDone());
980
+ assert.equal(spy.getCapturedOptions().timeout, 0, 'Timeout should be set to 0 (no timeout)');
981
+ }
982
+ finally {
983
+ spy.restore();
984
+ }
985
+ });
986
+ it('postStream handles AbortSignal for request cancellation', async () => {
987
+ const streamProvider = new Provider({
988
+ prepareRequest: requestOptions => ({
989
+ url: `https://www.myApi.com`,
990
+ headers: {
991
+ 'X-Custom-Provider-Header': 'value',
992
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
993
+ },
994
+ }),
995
+ });
996
+ const stream = Readable.from(['abort signal test']);
997
+ const abortController = new AbortController();
998
+ // Simulate aborting the request immediately
999
+ abortController.abort();
1000
+ let error;
1001
+ try {
1002
+ await streamProvider.postStream('/upload', stream, {
1003
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1004
+ logger,
1005
+ signal: abortController.signal,
1006
+ });
1007
+ }
1008
+ catch (e) {
1009
+ error = e;
1010
+ }
1011
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1012
+ assert.equal(error.message, 'Timeout');
1013
+ });
1014
+ it('postStream handles AbortSignal timeout during request', async () => {
1015
+ const streamProvider = new Provider({
1016
+ prepareRequest: requestOptions => ({
1017
+ url: `https://www.myApi.com`,
1018
+ headers: {
1019
+ 'X-Custom-Provider-Header': 'value',
1020
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
1021
+ },
1022
+ }),
1023
+ });
1024
+ const stream = Readable.from(['timeout during request test']);
1025
+ const abortController = new AbortController();
1026
+ // Delay response to simulate a slow server
1027
+ nock('https://www.myApi.com').post('/upload').delayConnection(100).reply(201, { success: true });
1028
+ // Abort after 50ms
1029
+ setTimeout(() => abortController.abort(), 50);
1030
+ let error;
1031
+ try {
1032
+ await streamProvider.postStream('/upload', stream, {
1033
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1034
+ logger,
1035
+ signal: abortController.signal,
1036
+ });
1037
+ }
1038
+ catch (e) {
1039
+ error = e;
1040
+ }
1041
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1042
+ assert.equal(error.message, 'Timeout');
1043
+ });
1044
+ it('postStream cleans up AbortSignal listener on success', async () => {
1045
+ const streamProvider = new Provider({
1046
+ prepareRequest: requestOptions => ({
1047
+ url: `https://www.myApi.com`,
1048
+ headers: {
1049
+ 'X-Custom-Provider-Header': 'value',
1050
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
1051
+ },
1052
+ }),
1053
+ });
1054
+ const testData = 'cleanup test';
1055
+ const stream = Readable.from([testData]);
1056
+ const abortController = new AbortController();
1057
+ const scope = nock('https://www.myApi.com').post('/upload', testData).reply(201, { success: true });
1058
+ const response = await streamProvider.postStream('/upload', stream, {
1059
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1060
+ logger,
1061
+ signal: abortController.signal,
1062
+ });
1063
+ assert.ok(scope.isDone());
1064
+ assert.equal(response.status, 201);
1065
+ // Verify the listener was removed by checking that aborting after completion
1066
+ // doesn't cause any side effects (if listener wasn't removed, this could cause issues)
1067
+ assert.doesNotThrow(() => {
1068
+ abortController.abort();
1069
+ }, 'Aborting after completion should not throw or cause issues');
1070
+ // Verify the signal is indeed aborted
1071
+ assert.ok(abortController.signal.aborted, 'Signal should be aborted');
1072
+ });
1073
+ it('postForm handles AbortSignal for request cancellation', async () => {
1074
+ const FormData = (await import('form-data')).default;
1075
+ const formProvider = new Provider({
1076
+ prepareRequest: requestOptions => ({
1077
+ url: `https://www.myApi.com`,
1078
+ headers: {
1079
+ 'X-Custom-Provider-Header': 'value',
1080
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
1081
+ },
1082
+ }),
1083
+ });
1084
+ const form = new FormData();
1085
+ form.append('field1', 'value1');
1086
+ form.append('field2', 'value2');
1087
+ const abortController = new AbortController();
1088
+ // Simulate aborting the request immediately
1089
+ abortController.abort();
1090
+ let error;
1091
+ try {
1092
+ await formProvider.postForm('/upload-form', form, {
1093
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1094
+ logger,
1095
+ signal: abortController.signal,
1096
+ });
1097
+ }
1098
+ catch (e) {
1099
+ error = e;
1100
+ }
1101
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1102
+ assert.equal(error.message, 'Timeout');
1103
+ });
1104
+ it('postForm handles AbortSignal timeout during request', async () => {
1105
+ const FormData = (await import('form-data')).default;
1106
+ const formProvider = new Provider({
1107
+ prepareRequest: requestOptions => ({
1108
+ url: `https://www.myApi.com`,
1109
+ headers: {
1110
+ 'X-Custom-Provider-Header': 'value',
1111
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
1112
+ },
1113
+ }),
1114
+ });
1115
+ const form = new FormData();
1116
+ form.append('field1', 'value1');
1117
+ const abortController = new AbortController();
1118
+ // Mock a delayed response
1119
+ nock('https://www.myApi.com').post('/upload-form').delayConnection(100).reply(201, { success: true, id: '12345' });
1120
+ // Abort after 50ms
1121
+ setTimeout(() => abortController.abort(), 50);
1122
+ let error;
1123
+ try {
1124
+ await formProvider.postForm('/upload-form', form, {
1125
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1126
+ logger,
1127
+ signal: abortController.signal,
1128
+ });
1129
+ }
1130
+ catch (e) {
1131
+ error = e;
1132
+ }
1133
+ assert.ok(error instanceof HttpErrors.TimeoutError);
1134
+ assert.equal(error.message, 'Timeout');
1135
+ });
1136
+ it('postForm successfully completes with AbortSignal provided', async () => {
1137
+ const FormData = (await import('form-data')).default;
1138
+ const formProvider = new Provider({
1139
+ prepareRequest: requestOptions => ({
1140
+ url: `https://www.myApi.com`,
1141
+ headers: {
1142
+ 'X-Custom-Provider-Header': 'value',
1143
+ 'X-Provider-Credential-Header': requestOptions.credentials.apiKey,
1144
+ },
1145
+ }),
1146
+ });
1147
+ const form = new FormData();
1148
+ form.append('field1', 'value1');
1149
+ form.append('field2', 'value2');
1150
+ const abortController = new AbortController();
1151
+ const scope = nock('https://www.myApi.com').post('/upload-form').reply(201, { success: true, id: '12345' });
1152
+ const response = await formProvider.postForm('/upload-form', form, {
1153
+ credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' },
1154
+ logger,
1155
+ signal: abortController.signal,
1156
+ });
1157
+ assert.ok(scope.isDone());
1158
+ assert.equal(response.status, 201);
1159
+ assert.deepEqual(response.body, { success: true, id: '12345' });
1160
+ // Verify the listener was removed by checking that aborting after completion
1161
+ // doesn't cause any side effects
1162
+ assert.doesNotThrow(() => {
1163
+ abortController.abort();
1164
+ }, 'Aborting after completion should not throw or cause issues');
1165
+ // Verify the signal is indeed aborted
1166
+ assert.ok(abortController.signal.aborted, 'Signal should be aborted');
1167
+ });
944
1168
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unito/integration-sdk",
3
- "version": "2.3.14",
3
+ "version": "2.3.15",
4
4
  "description": "Integration SDK",
5
5
  "type": "module",
6
6
  "types": "dist/src/index.d.ts",
@@ -243,6 +243,25 @@ export class Provider {
243
243
  reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
244
244
  });
245
245
 
246
+ if (options.signal) {
247
+ const abortHandler = () => {
248
+ request.destroy();
249
+ reject(this.handleError(408, 'Timeout', options));
250
+ };
251
+
252
+ if (options.signal.aborted) {
253
+ abortHandler();
254
+ }
255
+
256
+ options.signal.addEventListener('abort', abortHandler);
257
+
258
+ request.on('close', () => {
259
+ if (options.signal) {
260
+ options.signal.removeEventListener('abort', abortHandler);
261
+ }
262
+ });
263
+ }
264
+
246
265
  form.pipe(request);
247
266
  } catch (error) {
248
267
  reject(this.handleError(500, `Unexpected error while calling the provider: "${error}"`, options));
@@ -303,6 +322,7 @@ export class Provider {
303
322
  path: urlObj.pathname + urlObj.search,
304
323
  method: 'POST',
305
324
  headers,
325
+ timeout: 0,
306
326
  };
307
327
 
308
328
  const request = https.request(requestOptions, response => {
@@ -350,6 +370,25 @@ export class Provider {
350
370
  safeReject(this.handleError(500, `Stream error: "${error}"`, options));
351
371
  });
352
372
 
373
+ if (options.signal) {
374
+ const abortHandler = () => {
375
+ request.destroy();
376
+ safeReject(this.handleError(408, 'Timeout', options));
377
+ };
378
+
379
+ if (options.signal.aborted) {
380
+ abortHandler();
381
+ }
382
+
383
+ options.signal.addEventListener('abort', abortHandler);
384
+
385
+ request.on('close', () => {
386
+ if (options.signal) {
387
+ options.signal.removeEventListener('abort', abortHandler);
388
+ }
389
+ });
390
+ }
391
+
353
392
  // Stream the data directly without buffering
354
393
  stream.pipe(request);
355
394
  } catch (error) {
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
3
  import { Readable } from 'stream';
4
4
  import nock from 'nock';
5
+ import https from 'https';
5
6
 
6
7
  import { Provider } from '../../src/resources/provider.js';
7
8
  import * as HttpErrors from '../../src/httpErrors.js';
@@ -28,6 +29,24 @@ describe('Provider', () => {
28
29
 
29
30
  const logger = new Logger();
30
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
+
31
50
  it('get', async context => {
32
51
  const response = new Response('{"data": "value"}', {
33
52
  status: 200,
@@ -1132,4 +1151,254 @@ describe('Provider', () => {
1132
1151
  unitoCredentialId: '123',
1133
1152
  });
1134
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
+ });
1135
1404
  });