@unito/integration-sdk 2.3.14 → 2.4.0

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.
@@ -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
  });