pcp-server-nodejs-sdk
Version:
[](https://sonarcloud.io/summary/new_code?id=PAYONE-GmbH_PCP-server-nodeJS-SDK) [ => {
return {
...(await importOriginal()),
default: vi.fn(),
};
});
const mockedFetch = vi.mocked(fetch, true);
// Create a concrete test class since BaseApiClient is abstract-like
class TestApiClient extends BaseApiClient {
async testMakeApiCall(url, requestInit, parseBody) {
return this.makeApiCall(url, requestInit, parseBody);
}
static parseJsonForTest(body) {
return super.parseJson(body);
}
static parseVoidForTest() {
return super.parseVoid();
}
getRequestHeaderGeneratorForTest() {
return this.getRequestHeaderGenerator();
}
}
describe('BaseApiClient', () => {
let testApiClient;
let config;
beforeEach(() => {
vi.clearAllMocks();
config = new CommunicatorConfiguration('apiKey', 'apiSecret', 'https://test.com');
testApiClient = new TestApiClient(config);
});
test('constructs without fetch options', () => {
expect(testApiClient).toBeDefined();
expect(testApiClient.getConfig()).toBe(config);
expect(testApiClient.getRequestHeaderGeneratorForTest()).toBeDefined();
});
test('constructs with client-specific fetch options', () => {
const clientFetchOptions = {
signal: AbortSignal.timeout(15000),
headers: { 'X-Client-Header': 'client-value' },
};
const clientWithOptions = new TestApiClient(config, clientFetchOptions);
expect(clientWithOptions).toBeDefined();
});
test('setFetchOptions updates client-specific options', () => {
const fetchOptions = {
signal: AbortSignal.timeout(20000),
headers: { 'X-Custom': 'value' },
};
testApiClient.setFetchOptions(fetchOptions);
// We can't directly test the private options, but we can test the behavior
expect(testApiClient).toBeDefined();
});
test('makeApiCall uses default fetch when no custom options', async () => {
const mockResponse = {
ok: true,
status: 200,
text: vi.fn().mockResolvedValue('{"result": "success"}'),
};
mockedFetch.mockResolvedValue(mockResponse);
await testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' });
expect(mockedFetch).toHaveBeenCalledWith('https://test.com/api', expect.objectContaining({
method: 'GET',
}));
});
test('makeApiCall merges global fetch options', async () => {
const globalFetchOptions = {
signal: AbortSignal.timeout(30000),
headers: { 'X-Global': 'global-value' },
};
const configWithOptions = new CommunicatorConfiguration('apiKey', 'apiSecret', 'https://test.com', globalFetchOptions);
const clientWithGlobalOptions = new TestApiClient(configWithOptions);
const mockResponse = {
ok: true,
status: 200,
text: vi.fn().mockResolvedValue('{"result": "success"}'),
};
mockedFetch.mockResolvedValue(mockResponse);
await clientWithGlobalOptions.testMakeApiCall('https://test.com/api', { method: 'POST' });
// Verify that fetch was called
expect(mockedFetch).toHaveBeenCalledTimes(1);
const [url, requestInit] = mockedFetch.mock.calls[0];
expect(url).toBe('https://test.com/api');
expect(requestInit).toHaveProperty('method', 'POST');
// Check that signal is present (AbortSignal for timeout)
expect(requestInit).toHaveProperty('signal');
expect(requestInit?.signal).toBeDefined();
// Check that custom headers are present
expect(requestInit).toHaveProperty('headers');
const headers = requestInit?.headers;
if (headers && typeof headers === 'object' && 'forEach' in headers) {
let hasGlobalHeader = false;
headers.forEach((value, key) => {
if (key === 'X-Global' && value === 'global-value') {
hasGlobalHeader = true;
}
});
expect(hasGlobalHeader).toBe(true);
}
});
test('makeApiCall prioritizes client-specific options over global options', async () => {
const globalFetchOptions = {
signal: AbortSignal.timeout(30000),
headers: { 'X-Global': 'global-value', 'X-Shared': 'global-shared' },
};
const clientFetchOptions = {
signal: AbortSignal.timeout(15000),
headers: { 'X-Client': 'client-value', 'X-Shared': 'client-shared' },
};
const configWithOptions = new CommunicatorConfiguration('apiKey', 'apiSecret', 'https://test.com', globalFetchOptions);
const clientWithBothOptions = new TestApiClient(configWithOptions, clientFetchOptions);
const mockResponse = {
ok: true,
status: 200,
text: vi.fn().mockResolvedValue('{"result": "success"}'),
};
mockedFetch.mockResolvedValue(mockResponse);
await clientWithBothOptions.testMakeApiCall('https://test.com/api', { method: 'PUT' });
// Verify that fetch was called
expect(mockedFetch).toHaveBeenCalledTimes(1);
const [url, requestInit] = mockedFetch.mock.calls[0];
expect(url).toBe('https://test.com/api');
expect(requestInit).toHaveProperty('method', 'PUT');
// Check that signal is present (client-specific AbortSignal should take precedence)
expect(requestInit).toHaveProperty('signal');
expect(requestInit?.signal).toBeDefined();
// Check that headers are properly merged with client-specific taking precedence
expect(requestInit).toHaveProperty('headers');
const headers = requestInit?.headers;
if (headers && typeof headers === 'object' && 'forEach' in headers) {
const headerMap = {};
headers.forEach((value, key) => {
headerMap[key] = value;
});
// Global header should be present
expect(headerMap['X-Global']).toBe('global-value');
// Client-specific header should be present
expect(headerMap['X-Client']).toBe('client-value');
// Client-specific should override global for shared header
expect(headerMap['X-Shared']).toBe('client-shared');
}
});
test('makeApiCall merges custom headers with SDK headers', async () => {
const customFetchOptions = {
signal: AbortSignal.timeout(25000),
headers: {
'X-Custom': 'custom-value',
'User-Agent': 'MyApp/1.0',
},
};
testApiClient.setFetchOptions(customFetchOptions);
const mockResponse = {
ok: true,
status: 200,
text: vi.fn().mockResolvedValue('{"result": "success"}'),
};
mockedFetch.mockResolvedValue(mockResponse);
await testApiClient.testMakeApiCall('https://test.com/api', {
method: 'GET',
});
// Verify that fetch was called with merged options
expect(mockedFetch).toHaveBeenCalledTimes(1);
const [url, requestInit] = mockedFetch.mock.calls[0];
expect(url).toBe('https://test.com/api');
expect(requestInit).toHaveProperty('method', 'GET');
expect(requestInit).toHaveProperty('headers');
// Verify that both custom and SDK headers are present
const headers = requestInit?.headers;
expect(headers).toBeDefined();
// The headers should include SDK-generated headers (Authorization, Date, etc.)
// and custom headers should be merged in
if (headers && typeof headers === 'object' && 'forEach' in headers) {
let hasCustomHeader = false;
let hasAuthHeader = false;
let hasDateHeader = false;
headers.forEach((value, key) => {
if (key === 'X-Custom')
hasCustomHeader = true;
if (key === 'Authorization')
hasAuthHeader = true;
if (key === 'Date')
hasDateHeader = true;
});
expect(hasCustomHeader).toBe(true);
expect(hasAuthHeader).toBe(true);
expect(hasDateHeader).toBe(true);
}
});
test('parses JSON objects and rejects JSON primitives', () => {
expect(TestApiClient.parseJsonForTest('{"result":"success"}')).toEqual({
result: 'success',
});
expect(() => TestApiClient.parseJsonForTest('null')).toThrow('Parsed JSON is not an object');
expect(() => TestApiClient.parseJsonForTest('"value"')).toThrow('Parsed JSON is not an object');
});
test('parses void response bodies', () => {
expect(TestApiClient.parseVoidForTest()).toBeUndefined();
});
test('wraps parsing errors with the response details and error cause', async () => {
const parsingError = new Error('Unable to parse');
const mockResponse = {
ok: true,
status: 200,
text: vi.fn().mockResolvedValue('response body'),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' }, () => {
throw parsingError;
})).rejects.toMatchObject({
statusCode: 200,
responseBody: 'response body',
stack: parsingError.stack,
});
});
test('wraps non-Error parsing failures without a cause', async () => {
const mockResponse = {
ok: true,
status: 200,
text: vi.fn().mockResolvedValue('response body'),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' }, () => {
throw 'Unable to parse';
})).rejects.toBeInstanceOf(ApiResponseRetrievalException);
});
test('throws API error responses with their errors', async () => {
const body = '{"errorId":"request-id","errors":[{"errorCode":"INVALID"}]}';
const mockResponse = {
ok: false,
status: 400,
text: vi.fn().mockResolvedValue(body),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' })).rejects.toMatchObject({
statusCode: 400,
responseBody: body,
errors: [{ errorCode: 'INVALID' }],
});
});
test('throws retrieval errors for malformed API error responses', async () => {
const body = '{"errorId":123}';
const mockResponse = {
ok: false,
status: 500,
text: vi.fn().mockResolvedValue(body),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' })).rejects.toMatchObject({
statusCode: 500,
responseBody: body,
});
});
test('throws retrieval errors when an API error response has invalid errors', async () => {
const body = '{"errorId":"request-id","errors":{}}';
const mockResponse = {
ok: false,
status: 500,
text: vi.fn().mockResolvedValue(body),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' })).rejects.toBeInstanceOf(ApiResponseRetrievalException);
});
test.each([
null,
'not an object',
])('throws retrieval errors for non-object error bodies', async (parsed) => {
const mockResponse = {
ok: false,
status: 500,
text: vi.fn().mockResolvedValue('response body'),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' }, () => parsed)).rejects.toBeInstanceOf(ApiResponseRetrievalException);
});
test('throws retrieval errors when an API error response has no errors array', async () => {
const body = '{"errorId":"request-id"}';
const mockResponse = {
ok: false,
status: 400,
text: vi.fn().mockResolvedValue(body),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' })).rejects.toBeInstanceOf(ApiResponseRetrievalException);
});
test('throws an API error with no errors for an empty error object', async () => {
const mockResponse = {
ok: false,
status: 400,
text: vi.fn().mockResolvedValue('response body'),
};
mockedFetch.mockResolvedValue(mockResponse);
await expect(testApiClient.testMakeApiCall('https://test.com/api', { method: 'GET' }, () => ({}))).rejects.toMatchObject({ errors: [] });
});
});
//# sourceMappingURL=BaseApiClient.test.js.map