@apistudio/apim-cli
Version:
CLI for API Management Products
539 lines (463 loc) • 15.7 kB
text/typescript
/**
* Copyright IBM Corp. 2024, 2025
*/
import {
addErrorToResponse,
errorsArray,
constructErrorResponse,
filterSensitiveData,
generateCSV,
generatePDF,
sanitizeAxiosResponse,
isSuccessStatus,
parseSetCookies,
} from '../../src/helpers/helper.js';
import { Buffer } from 'buffer';
jest.mock('../../src/service/log-wrapper.ts');
describe('addErrorToResponse', () => {
beforeEach(() => {
// Clear errorsArray before each test
errorsArray.length = 0;
});
test('should add error object to errorsArray', () => {
addErrorToResponse('ERR001', 'field1', 'Error description 1');
expect(errorsArray).toHaveLength(1);
expect(errorsArray[0]).toEqual({
code: 'ERR001',
field: 'field1',
description: 'Error description 1',
});
});
test('should add multiple error objects to errorsArray', () => {
addErrorToResponse('ERR001', 'field1', 'Error description 1');
addErrorToResponse('ERR002', 'field2', 'Error description 2');
expect(errorsArray).toHaveLength(2);
expect(errorsArray[0]).toEqual({
code: 'ERR001',
field: 'field1',
description: 'Error description 1',
});
expect(errorsArray[1]).toEqual({
code: 'ERR002',
field: 'field2',
description: 'Error description 2',
});
});
});
describe('constructErrorResponse', () => {
beforeEach(() => {
// Clear errorsArray before each test
errorsArray.length = 0;
});
test('should construct error response object with correct properties', () => {
addErrorToResponse('ERR001', 'field1', 'Error description 1');
addErrorToResponse('ERR002', 'field2', 'Error description 2');
const response = constructErrorResponse();
expect(response.respCode).toBe(400);
expect(response.message).toBe('Invalid Assets or Reference in the Zip');
expect(response.Endpoints).toEqual([]);
expect(response.errors).toHaveLength(2);
expect(response.errors[0]).toEqual({
code: 'ERR001',
field: 'field1',
description: 'Error description 1',
});
expect(response.errors[1]).toEqual({
code: 'ERR002',
field: 'field2',
description: 'Error description 2',
});
});
test('should clear errorsArray after constructing error response', () => {
addErrorToResponse('ERR001', 'field1', 'Error description 1');
constructErrorResponse();
expect(errorsArray).toHaveLength(0);
});
});
describe('filterSensitiveData', () => {
it('should remove exact sensitive keys like password and token', () => {
const input = {
username: 'user1',
contact: {
username: 'user3',
password: 'secret123', // pragma: allowlist secret
},
token: 'abcd1234',
};
const result = filterSensitiveData(input);
expect(result).toEqual({
username: 'user1',
contact: { username: 'user3' },
});
});
it('should remove keys that match sensitive patterns like SECRET and sensitive', () => {
const input = {
API_SECRET: 'hidden', // pragma: allowlist secret
sensitive_info: 'classified',
userId: 42,
};
const result = filterSensitiveData(input);
expect(result).toEqual({ userId: 42 });
});
it('should remove case-insensitive matches', () => {
const input = {
PASSWORD: 'pass',
Token: '123',
SecretKey: 'topsecret',
EnvVar: 'do-not-export',
};
const result = filterSensitiveData(input);
expect(result).toEqual({});
});
it('should retain non-sensitive fields', () => {
const input = {
name: 'Alice',
email: 'alice@example.com',
};
const result = filterSensitiveData(input);
expect(result).toEqual(input);
});
it('should handle an empty input object', () => {
const result = filterSensitiveData({});
expect(result).toEqual({});
});
});
describe('generateCSV', () => {
const testData = [
{ id: '1', name: 'John Doe', age: 30 },
{ id: '2', name: 'Jane Doe', age: 28 },
];
it('should generate a CSV buffer with the correct header and data', () => {
const csvBuffer = generateCSV(testData);
expect(csvBuffer.toString()).toBe(
'id,name,age\n1,John Doe,30\n2,Jane Doe,28\n',
);
});
it('should handle empty data array', () => {
const csvBuffer = generateCSV([]);
expect(csvBuffer.toString('utf-8')).toBe('');
});
it('should handle objects with varying field order by using the first object’s keys as headers', () => {
const data = [
{ firstName: 'Carol', lastName: 'Danvers', score: 95 },
// Second object has keys shuffled
Object.assign({}, { score: 82, lastName: 'Frost', firstName: 'Dave' }),
];
const buffer: Buffer = generateCSV(data);
const csv = buffer.toString('utf-8');
const lines = csv.trim().split('\n');
expect(lines[0]).toBe('firstName,lastName,score');
expect(lines[1]).toBe('Carol,Danvers,95');
expect(lines[2]).toBe('Dave,Frost,82');
});
});
describe('generatePDF', () => {
it('should return a non-empty Buffer', async () => {
const data = [{ name: 'Test', value: 123 }];
const buffer = await generatePDF(data);
expect(buffer.length).toBeGreaterThan(0);
});
});
describe('sanitizeAxiosResponse', () => {
it('should handle successful response', () => {
const mockResponse = {
status: 200,
statusText: 'OK',
headers: [
{ key: 'content-type', value: 'application/json' },
{ key: 'set-cookie', value: ['session=123; Path=/; HttpOnly'] },
],
config: {
method: 'get',
url: 'https://api.example.com/data',
timeout: 5000,
},
data: { id: 1, name: 'Test' },
responseTime: 250,
};
const result = sanitizeAxiosResponse(mockResponse);
expect(result).toEqual({
success: true,
status: 200,
statusText: 'OK',
headers: mockResponse.headers,
cookies: [{ key: 'session', value: '123' }],
config: {
method: 'get',
url: 'https://api.example.com/data',
timeout: 5000,
},
data: { id: 1, name: 'Test' },
responseTime: 250,
});
});
it('should handle error response', () => {
const mockError = {
message: 'Request failed with status code 404',
code: 'ERR_BAD_REQUEST',
response: {
status: 404,
statusText: 'Not Found',
headers: [
{ key: 'content-type', value: 'application/json' },
{ key: 'set-cookie', value: ['session=xyz; Path=/; HttpOnly'] },
],
data: { error: 'Resource not found' },
},
config: {
method: 'get',
url: 'https://api.example.com/missing',
timeout: 3000,
},
};
const result = sanitizeAxiosResponse(null, mockError);
expect(result).toEqual({
success: false,
message: 'Request failed with status code 404',
code: 'ERR_BAD_REQUEST',
status: 404,
statusText: 'Not Found',
headers: mockError.response.headers,
cookies: [{ key: 'session', value: 'xyz' }],
config: {
method: 'get',
url: 'https://api.example.com/missing',
timeout: 3000,
},
data: { error: 'Resource not found' },
});
});
it('should handle error response with missing response object', () => {
const mockError = {
message: 'Network Error',
code: 'ERR_NETWORK',
config: {
method: 'get',
url: 'https://api.example.com/data',
timeout: 3000,
},
};
const result = sanitizeAxiosResponse(null, mockError);
expect(result).toEqual({
success: false,
message: 'Network Error',
code: 'ERR_NETWORK',
status: null,
statusText: null,
headers: {},
cookies: [],
config: {
method: 'get',
url: 'https://api.example.com/data',
timeout: 3000,
},
data: null,
});
});
it('should handle error response with missing headers', () => {
const mockError = {
message: 'Request failed',
code: 'ERR_BAD_REQUEST',
response: {
status: 500,
statusText: 'Internal Server Error',
headers: [],
data: { message: 'Server error' },
},
config: {
method: 'post',
url: 'https://api.example.com/data',
},
};
const result = sanitizeAxiosResponse(null, mockError);
expect(result).toEqual({
success: false,
message: 'Request failed',
code: 'ERR_BAD_REQUEST',
status: 500,
statusText: 'Internal Server Error',
headers: [],
cookies: [],
config: {
method: 'post',
url: 'https://api.example.com/data',
timeout: undefined,
},
data: { message: 'Server error' },
});
});
it('should handle error response with missing config', () => {
const mockError = {
message: 'Request failed',
code: 'ERR_BAD_REQUEST',
response: {
status: 400,
statusText: 'Bad Request',
headers: [{ key: 'content-type', value: 'application/json' }],
data: { message: 'Invalid input' },
},
};
const result = sanitizeAxiosResponse(null, mockError);
expect(result).toEqual({
success: false,
message: 'Request failed',
code: 'ERR_BAD_REQUEST',
status: 400,
statusText: 'Bad Request',
headers: mockError.response.headers,
cookies: [],
config: {
method: undefined,
url: undefined,
timeout: undefined,
},
data: { message: 'Invalid input' },
});
});
});
describe('isSuccessStatus', () => {
it('should return true for status codes in the 2xx range', () => {
expect(isSuccessStatus(200)).toBe(true);
expect(isSuccessStatus(201)).toBe(true);
});
it('should return false for status codes outside the 2xx range', () => {
// 4xx status codes
expect(isSuccessStatus(400)).toBe(false);
expect(isSuccessStatus(404)).toBe(false);
// 5xx status codes
expect(isSuccessStatus(500)).toBe(false);
expect(isSuccessStatus(503)).toBe(false);
});
});
describe('parseSetCookies', () => {
// Test case 1: Handling empty, null, or undefined input
describe('handling empty or invalid inputs', () => {
it('should return an empty array for empty string', () => {
expect(parseSetCookies('')).toEqual([]);
});
it('should handle null-like values correctly', () => {
// @ts-ignore - Testing runtime behavior with null
expect(parseSetCookies(null)).toEqual([]);
// @ts-ignore - Testing runtime behavior with undefined
expect(parseSetCookies(undefined)).toEqual([]);
});
it('should return an empty array for whitespace string', () => {
expect(parseSetCookies(' ')).toEqual([]);
});
it('should return an empty array for empty array', () => {
expect(parseSetCookies([])).toEqual([]);
});
});
// Test case 2: Parsing a single Set-Cookie header
describe('parsing a single Set-Cookie header', () => {
it('should parse a simple name=value cookie', () => {
const result = parseSetCookies('session=abc123');
expect(result).toEqual([{ key: 'session', value: 'abc123' }]);
});
it('should parse a cookie with attributes', () => {
const result = parseSetCookies('session=abc123; Path=/; HttpOnly');
expect(result).toEqual([{ key: 'session', value: 'abc123' }]);
});
it('should handle cookies with equals sign in the value', () => {
const result = parseSetCookies('data=key1=value1&key2=value2; Path=/');
expect(result).toEqual([
{ key: 'data', value: 'key1=value1&key2=value2' },
]);
});
it('should trim whitespace from cookie name and value', () => {
const result = parseSetCookies(' session = abc123 ; Path=/');
expect(result).toEqual([{ key: 'session', value: 'abc123' }]);
});
});
// Test case 3: Parsing multiple Set-Cookie headers
describe('parsing multiple Set-Cookie headers', () => {
it('should parse an array of cookie strings', () => {
const cookies = [
'session=abc123; Path=/; HttpOnly',
'user=john; Path=/account; Secure',
'theme=dark',
];
const result = parseSetCookies(cookies);
expect(result).toEqual([
{ key: 'session', value: 'abc123' },
{ key: 'user', value: 'john' },
{ key: 'theme', value: 'dark' },
]);
});
it('should handle empty strings in the array', () => {
const cookies = ['session=abc123', '', 'theme=dark'];
const result = parseSetCookies(cookies);
expect(result).toEqual([
{ key: 'session', value: 'abc123' },
{ key: '', value: '' },
{ key: 'theme', value: 'dark' },
]);
});
});
// Test case 4: Handling various cookie attributes
describe('handling various cookie attributes', () => {
it('should extract only the cookie name-value pair, ignoring attributes', () => {
const cookie =
'session=abc123; Expires=Wed, 21 Oct 2025 07:28:00 GMT; Max-Age=3600; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Strict';
const result = parseSetCookies(cookie);
expect(result).toEqual([{ key: 'session', value: 'abc123' }]);
});
it('should handle cookies with multiple attributes in different orders', () => {
const cookies = [
'session=abc123; HttpOnly; Path=/; Secure',
'user=john; SameSite=Lax; Domain=example.com',
];
const result = parseSetCookies(cookies);
expect(result).toEqual([
{ key: 'session', value: 'abc123' },
{ key: 'user', value: 'john' },
]);
});
});
// Test case 5: Edge cases and special characters
describe('handling edge cases and special characters', () => {
it('should handle cookies with special characters in values', () => {
const cookies = [
'complex=value@with+special&chars?',
'json={"key":"value"}',
'encoded=hello%20world',
];
const result = parseSetCookies(cookies);
expect(result).toEqual([
{ key: 'complex', value: 'value@with+special&chars?' },
{ key: 'json', value: '{"key":"value"}' },
{ key: 'encoded', value: 'hello%20world' },
]);
});
it('should handle malformed cookies missing values', () => {
const cookies = ['session=', 'user', '=value'];
const result = parseSetCookies(cookies);
expect(result).toEqual([
{ key: 'session', value: '' },
{ key: 'user', value: '' },
{ key: '', value: 'value' },
]);
});
it('should handle cookies with multiple equal signs', () => {
const result = parseSetCookies('token=abc=123=xyz; Path=/');
expect(result).toEqual([{ key: 'token', value: 'abc=123=xyz' }]);
});
});
// Test case 6: Verifying correct object structure
describe('verifying correct object structure', () => {
it('should always return an array of objects with key and value properties', () => {
const result = parseSetCookies('session=abc123');
expect(Array.isArray(result)).toBe(true);
expect(result[0]).toHaveProperty('key');
expect(result[0]).toHaveProperty('value');
expect(Object.keys(result[0]).length).toBe(2); // Only key and value properties
});
it('should maintain the order of cookies as provided in the input', () => {
const cookies = ['first=cookie', 'second=cookie', 'third=cookie'];
const result = parseSetCookies(cookies);
expect(result[0].key).toBe('first');
expect(result[1].key).toBe('second');
expect(result[2].key).toBe('third');
});
});
});