UNPKG

@unito/integration-sdk

Version:

Integration SDK

777 lines (776 loc) 34.7 kB
import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { Provider } from '../../src/resources/provider.js'; import * as HttpErrors from '../../src/httpErrors.js'; import Logger from '../../src/resources/logger.js'; // There is currently an issue with node 20.12 and fetch mocking. A quick fix is to first call fetch so it's getter // get properly instantiated, which allow it to be mocked properly. // Issue: https://github.com/nodejs/node/issues/52015 // PR fix: https://github.com/nodejs/node/pull/52275 globalThis.fetch = fetch; describe('Provider', () => { const provider = new Provider({ prepareRequest: requestOptions => { return { url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`, headers: { 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': requestOptions.credentials.apiKey, }, }; }, }); const logger = new Logger(); it('get', async (context) => { const response = new Response('{"data": "value"}', { status: 200, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.get('/endpoint', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'GET', body: null, signal: new AbortController().signal, headers: { Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } }); }); it('accepts text/html type response', async (context) => { const response = new Response('', { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.get('/endpoint', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'text/html; charset=UTF-8' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'GET', body: null, signal: new AbortController().signal, headers: { Accept: 'text/html; charset=UTF-8', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: '' }); }); it('accepts application/schema+json type response', async (context) => { const response = new Response('{"data": "value"}', { status: 200, headers: { 'Content-Type': 'application/schema+json; charset=UTF-8' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.get('/endpoint', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/schema+json; charset=UTF-8' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'GET', body: null, signal: new AbortController().signal, headers: { Accept: 'application/schema+json; charset=UTF-8', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } }); }); it('accepts application/swagger+json type response', async (context) => { const response = new Response('{"data": "value"}', { status: 200, headers: { 'Content-Type': 'application/swagger+json; charset=UTF-8' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.get('/endpoint', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1', Accept: 'application/swagger+json; charset=UTF-8' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'GET', body: null, signal: new AbortController().signal, headers: { Accept: 'application/swagger+json; charset=UTF-8', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } }); }); it('accepts application/vnd.oracle.resource+json type response', async (context) => { const response = new Response('{"data": "value"}', { status: 200, headers: { 'Content-Type': 'application/vnd.oracle.resource+json; type=collection; charset=UTF-8' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.get('/endpoint', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'GET', body: null, signal: new AbortController().signal, headers: { Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } }); }); it('returns the raw response body if specified', async (context) => { const response = new Response(`IMAGINE A HUGE PAYLOAD`, { status: 200, headers: { 'Content-Type': 'image/png' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const providerResponse = await provider.streamingGet('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { Accept: 'application/json', }, rawBody: true, }); assert.ok(providerResponse); // What matters: still returns a stream assert.ok(providerResponse.body instanceof ReadableStream); }); it('gets an endpoint which is an absolute url', async (context) => { const response = new Response('{"data": "value"}', { status: 200, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.get('https://my-cdn.my-domain.com/file.png', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'https://my-cdn.my-domain.com/file.png', { method: 'GET', body: null, signal: new AbortController().signal, headers: { Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', }, }, ]); assert.deepEqual(actualResponse, { status: 200, headers: response.headers, body: { data: 'value' } }); }); it('post with url encoded body', async (context) => { const response = new Response('{"data": "value"}', { status: 201, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.post('/endpoint', { data: 'createdItemInfo', }, { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Additional-Header': 'value1' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'POST', body: 'data=createdItemInfo', signal: new AbortController().signal, headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } }); }); it('accepts an array as body for post request', async (context) => { const response = new Response('{"data": "value"}', { status: 201, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.post('/endpoint', [ { data: '1', data2: '2' }, { data: '3', data2: '4' }, ], { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'Content-Type': 'application/json-patch+json', 'X-Additional-Header': 'value1' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint', { method: 'POST', body: '[{"data":"1","data2":"2"},{"data":"3","data2":"4"}]', signal: new AbortController().signal, headers: { 'Content-Type': 'application/json-patch+json', Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } }); }); it('put with json body', async (context) => { const response = new Response('{"data": "value"}', { status: 201, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); // Removing leading '/' on endpoint to make sure we support both cases const actualResponse = await provider.put('endpoint/123', { data: 'updatedItemInfo', }, { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/json' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint/123', { method: 'PUT', body: JSON.stringify({ data: 'updatedItemInfo' }), signal: new AbortController().signal, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } }); }); it('putBuffer with Buffer body', async (context) => { const response = new Response('{"data": "value"}', { status: 201, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const buffer = Buffer.from('binary data content'); // What matters is that the body of put is a buffer const actualResponse = await provider.putBuffer('endpoint/123', buffer, { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1', 'Content-Type': 'application/octet-stream' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint/123', { method: 'PUT', body: buffer, signal: new AbortController().signal, headers: { 'Content-Type': 'application/octet-stream', Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } }); }); it('patch with query params', async (context) => { const response = new Response('{"data": "value"}', { status: 201, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.patch('/endpoint/123', { data: 'updatedItemInfo', }, { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, queryParams: { param1: 'value1', param2: 'value2' }, additionnalheaders: { 'X-Additional-Header': 'value1' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint/123?param1=value1&param2=value2', { method: 'PATCH', body: JSON.stringify({ data: 'updatedItemInfo' }), signal: new AbortController().signal, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 201, headers: response.headers, body: { data: 'value' } }); }); it('delete', async (context) => { const response = new Response(undefined, { status: 204, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const actualResponse = await provider.delete('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint/123', { method: 'DELETE', body: null, signal: new AbortController().signal, headers: { Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined }); }); it('uses rate limiter if provided', async (context) => { const mockRateLimiter = context.mock.fn((_context, request) => Promise.resolve(request())); const rateLimitedProvider = new Provider({ prepareRequest: requestOptions => { return { url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`, headers: { 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': requestOptions.credentials.apiKey, }, }; }, rateLimiter: mockRateLimiter, }); const response = new Response(undefined, { status: 204, headers: { 'Content-Type': 'application/json' }, }); const fetchMock = context.mock.method(global, 'fetch', () => Promise.resolve(response)); const options = { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }; const actualResponse = await rateLimitedProvider.delete('/endpoint/123', options); assert.equal(mockRateLimiter.mock.calls.length, 1); assert.deepEqual(mockRateLimiter.mock.calls[0]?.arguments[0]?.credentials, options.credentials); assert.equal(fetchMock.mock.calls.length, 1); assert.deepEqual(fetchMock.mock.calls[0]?.arguments, [ 'www.myApi.com/endpoint/123', { method: 'DELETE', body: null, signal: new AbortController().signal, headers: { Accept: 'application/json', 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': 'apikey#1111', 'X-Additional-Header': 'value1', }, }, ]); assert.deepEqual(actualResponse, { status: 204, headers: response.headers, body: undefined }); }); it('uses custom error handler if provided', async (context) => { const rateLimitedProvider = new Provider({ prepareRequest: requestOptions => { return { url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`, headers: { 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': requestOptions.credentials.apiKey, }, }; }, rateLimiter: undefined, // Change from normal behavior 400 -> 429 customErrorHandler: (responseStatus) => responseStatus === 400 ? new HttpErrors.RateLimitExceededError('Weird provider behavior') : undefined, }); const response = new Response(undefined, { status: 400, headers: { 'Content-Type': 'application/json' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const options = { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }; let error; try { await rateLimitedProvider.delete('/endpoint/123', options); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.HttpError); assert.equal(error.message, 'Weird provider behavior'); }); it('contains the credential in the custom error handler', async (context) => { const provider = new Provider({ prepareRequest: requestOptions => { return { url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`, headers: { 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': requestOptions.credentials.apiKey, }, }; }, rateLimiter: undefined, customErrorHandler: (responseStatus, _message, options) => { if (responseStatus === 400) { // What matter is that we have access to the context in the error handler throw new HttpErrors.BadRequestError(`Error with API key ${options?.credentials.apiKey}`); } return undefined; }, }); const response = new Response(undefined, { status: 400, headers: { 'Content-Type': 'application/json' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const options = { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }; let error; try { await provider.delete('/endpoint/123', options); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.HttpError); assert.equal(error.message, 'Error with API key apikey#1111'); }); it('uses default behavior if custom error handler returns undefined', async (context) => { const rateLimitedProvider = new Provider({ prepareRequest: requestOptions => { return { url: `www.${requestOptions.credentials.domain ?? 'myApi.com'}`, headers: { 'X-Custom-Provider-Header': 'value', 'X-Provider-Credential-Header': requestOptions.credentials.apiKey, }, }; }, rateLimiter: undefined, // Custom Error Handler returning undefined (default behavior should apply) customErrorHandler: () => undefined, }); const response = new Response(undefined, { status: 404, headers: { 'Content-Type': 'application/json' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const options = { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, additionnalheaders: { 'X-Additional-Header': 'value1' }, }; let error; try { await rateLimitedProvider.delete('/endpoint/123', options); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.HttpError); assert.equal(error.message, 'Not found'); }); it('returns valid json response', async (context) => { const response = new Response(`{ "validJson": true }`, { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const providerResponse = await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); assert.ok(providerResponse); assert.ok(providerResponse.body); assert.equal(providerResponse.body.validJson, true); }); it('returns successfully on missing Content-Type header', async (context) => { const response = new Response(undefined, { status: 201, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const providerResponse = await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); assert.ok(providerResponse); assert.equal(providerResponse.body, undefined); }); it('returns streamable response on streaming get calls', async (context) => { const response = new Response(`IMAGINE A HUGE PAYLOAD`, { status: 200, headers: { 'Content-Type': 'video/mp4' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const providerResponse = await provider.streamingGet('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); assert.ok(providerResponse); assert.ok(providerResponse.body instanceof ReadableStream); }); it('returns successfully on unexpected content-type response with no body', async (context) => { const response = new Response(null, { status: 201, headers: { 'Content-Type': 'html/text' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const providerResponse = await provider.post('/endpoint/123', {}, { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); assert.ok(providerResponse); assert.strictEqual(providerResponse.status, response.status); assert.strictEqual(providerResponse.headers, response.headers); assert.strictEqual(providerResponse.body, undefined); }); it('throws on invalid json response', async (context) => { const response = new Response('{invalidJSON}', { status: 200, headers: { 'Content-Type': 'application/json' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.HttpError); assert.equal(error.message, 'Invalid JSON response'); }); it('throws on unexpected content-type response', async (context) => { const response = new Response('text', { status: 200, headers: { 'Content-Type': 'application/text' }, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, logger: logger, signal: new AbortController().signal, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.HttpError); assert.equal(error.status, 500); }); it('throws on status 400', async (context) => { const response = new Response('response body', { status: 400, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, signal: new AbortController().signal, logger: logger, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.BadRequestError); assert.equal(error.message, 'response body'); }); it('throws on timeout', async (context) => { context.mock.method(global, 'fetch', () => { const error = new Error(); error.name = 'TimeoutError'; throw error; }); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, signal: new AbortController().signal, logger: logger, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.TimeoutError); assert.equal(error.message, 'Request timeout'); }); it('throws on abort', async (context) => { context.mock.method(global, 'fetch', () => { const error = new Error(); error.name = 'AbortError'; throw error; }); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, signal: new AbortController().signal, logger: logger, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.TimeoutError); assert.equal(error.message, 'Request aborted'); }); it('throws on unknown errors', async (context) => { context.mock.method(global, 'fetch', () => { throw new TypeError('foo', { cause: new Error('bar') }); }); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, signal: new AbortController().signal, logger: logger, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.HttpError); assert.ok(error.message.startsWith('Unexpected error while calling the provider.')); assert.ok(error.message.includes('ErrorName: "TypeError"')); assert.ok(error.message.includes('message: "foo"')); assert.ok(error.message.includes('stack:')); assert.ok(error.message.includes('cause:')); assert.ok(error.message.includes('causeStack:')); }); it('throws on status 429', async (context) => { const response = new Response('response body', { status: 429, }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); let error; try { await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, signal: new AbortController().signal, logger: logger, }); } catch (e) { error = e; } assert.ok(error instanceof HttpErrors.RateLimitExceededError); assert.equal(error.message, 'response body'); }); it('logs provider requests', async (context) => { const response = new Response(undefined, { status: 201 }); context.mock.method(global, 'fetch', () => Promise.resolve(response)); const loggerStub = context.mock.method(logger, 'info'); await provider.get('/endpoint/123', { credentials: { apiKey: 'apikey#1111', unitoCredentialId: '123' }, signal: new AbortController().signal, logger: logger, }); assert.equal(loggerStub.mock.callCount(), 1); assert.match(String(loggerStub.mock.calls[0]?.arguments[0]), /Connector API Request GET www.myApi.com\/endpoint\/123 201 - \d+ ms/); }); });