@vepler/http-client
Version:
A flexible and extensible API service library for making HTTP requests with built-in authentication support for bearer tokens and API keys.
303 lines (256 loc) • 9.28 kB
text/typescript
import {
sanitizeHeaders,
truncateData,
sanitizeConfig,
sanitizeResponse,
parseAxiosError
} from '../../src/errors/error-utils';
import { AxiosHeaders } from 'axios';
import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
describe('sanitizeHeaders', () => {
test('should redact sensitive headers with mask', () => {
const headers = {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0',
'x-api-key': 'api-key-12345-67890',
'Content-Type': 'application/json',
'custom-token': 'test-token',
'private-key': 'private-key-value'
};
const sanitized = sanitizeHeaders(headers);
expect(sanitized['Authorization']).toMatch(/^Bear/);
expect(sanitized['x-api-key']).toMatch(/^api-/);
expect(sanitized['Content-Type']).toBe('application/json');
expect(sanitized['custom-token']).toMatch(/^test/);
expect(sanitized['private-key']).toMatch(/^priv/);
});
test('should handle undefined headers', () => {
const sanitized = sanitizeHeaders(undefined);
expect(sanitized).toEqual({});
});
test('should completely redact short sensitive values', () => {
const headers = {
'key': '123',
'token': 'abc'
};
const sanitized = sanitizeHeaders(headers);
expect(sanitized['key']).toBe('[REDACTED]');
expect(sanitized['token']).toBe('[REDACTED]');
});
});
describe('truncateData', () => {
test('should truncate long strings', () => {
const longString = 'a'.repeat(1500);
const truncated = truncateData(longString);
expect(truncated.length).toBeLessThan(1500);
expect(truncated).toContain('... [truncated');
});
test('should truncate long arrays', () => {
const longArray = Array(100).fill('item');
const truncated = truncateData(longArray);
expect(truncated.length).toBe(51); // 50 items + truncation message
expect(truncated[50]).toContain('more items');
});
test('should truncate large objects', () => {
const largeObject: Record<string, string> = {};
for (let i = 0; i < 50; i++) {
largeObject[`key${i}`] = `value${i}`;
}
const truncated = truncateData(largeObject);
expect(Object.keys(truncated).length).toBe(21); // 20 properties + truncation message
expect(truncated['[truncated]']).toContain('more properties');
});
test('should handle nested objects with depth limit', () => {
const nestedObject = {
level1: {
level2: {
level3: {
level4: {
deep: 'value'
}
}
}
}
};
const truncated = truncateData(nestedObject);
expect(truncated.level1.level2.level3.level4).toBe('[Nested Object]');
});
test('should redact sensitive fields in objects', () => {
const object = {
username: 'testuser',
password: 'secret123',
apiKey: 'api-key-value',
data: {
token: 'jwt-token'
}
};
const truncated = truncateData(object);
expect(truncated.username).toBe('testuser');
expect(truncated.password).toBe('[REDACTED]');
expect(truncated.apiKey).toBe('[REDACTED]');
expect(truncated.data.token).toBe('[REDACTED]');
});
});
describe('sanitizeConfig', () => {
test('should sanitize axios request config', () => {
const config: AxiosRequestConfig = {
url: '/api/users',
method: 'post',
baseURL: 'https://api.example.com',
headers: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/json'
},
params: {
include: 'profile',
apiKey: 'secret-api-key'
},
data: {
username: 'testuser',
password: 'secret123'
},
auth: {
username: 'admin',
password: 'admin123'
}
};
const sanitized = sanitizeConfig(config);
// URL and method should be preserved
expect(sanitized.url).toBe('/api/users');
expect(sanitized.method).toBe('post');
expect(sanitized.baseURL).toBe('https://api.example.com');
// Headers should be sanitized
expect(sanitized.headers).toBeDefined();
expect(sanitized.headers!['Authorization']).not.toBe('Bearer token123');
expect(sanitized.headers!['Content-Type']).toBe('application/json');
// Auth should be sanitized
expect(sanitized.auth).toBeDefined();
expect(sanitized.auth!.username).toBe('admin');
expect(sanitized.auth!.password).toBe('[REDACTED]');
// Params should be sanitized
expect(sanitized.params).toBeDefined();
expect(sanitized.params!.include).toBe('profile');
expect(sanitized.params!.apiKey).toBe('[REDACTED]');
// Data should be sanitized
expect(sanitized.data).toBeDefined();
expect(sanitized.data!.username).toBe('testuser');
expect(sanitized.data!.password).toBe('[REDACTED]');
});
test('should handle undefined config', () => {
const sanitized = sanitizeConfig(undefined);
expect(sanitized).toEqual({});
});
});
describe('sanitizeResponse', () => {
test('should sanitize axios response', () => {
const response: AxiosResponse = {
status: 200,
statusText: 'OK',
headers: new AxiosHeaders({
'content-type': 'application/json',
'set-cookie': ['session=abc123']
}),
data: {
user: {
id: 1,
username: 'testuser',
token: 'sensitive-token'
}
},
config: {
url: '/api/login',
method: 'post',
headers: new AxiosHeaders({
'Authorization': 'Bearer token123'
}),
transitional: {}
} as any,
request: {}
};
const sanitized = sanitizeResponse(response);
// Status should be preserved
expect(sanitized.status).toBe(200);
expect(sanitized.statusText).toBe('OK');
// Headers should be sanitized
expect(sanitized.headers['content-type']).toBe('application/json');
expect(sanitized.headers['set-cookie']).toBeDefined();
// Config should be sanitized
expect(sanitized.config.url).toBe('/api/login');
expect(sanitized.config.headers['Authorization']).not.toBe('Bearer token123');
// Data should be sanitized
expect(sanitized.data.user.id).toBe(1);
expect(sanitized.data.user.username).toBe('testuser');
expect(sanitized.data.user.token).toBe('[REDACTED]');
});
test('should handle undefined response', () => {
const sanitized = sanitizeResponse(undefined);
expect(sanitized).toBeNull();
});
});
describe('parseAxiosError', () => {
test('should parse response error', () => {
const error = new Error('Request failed with status code 404') as any;
error.name = 'AxiosError';
error.isAxiosError = true;
error.code = 'ERR_BAD_RESPONSE';
error.response = {
status: 404,
statusText: 'Not Found',
headers: new AxiosHeaders({ 'content-type': 'application/json' }),
data: { error: 'Resource not found' }
};
error.config = {
url: '/api/users/999',
method: 'get',
headers: new AxiosHeaders({ 'Authorization': 'Bearer token123' })
};
error.toJSON = () => ({ error: 'json representation' });
const parsed = parseAxiosError(error);
expect(parsed.name).toBe('AxiosError');
expect(parsed.message).toBe('Request failed with status code 404');
expect(parsed.code).toBe('ERR_BAD_RESPONSE');
expect(parsed.status).toBe(404);
expect(parsed.statusText).toBe('Not Found');
expect(parsed.data).toEqual({ error: 'Resource not found' });
expect(parsed.request.url).toBe('/api/users/999');
expect(parsed.request.method).toBe('GET');
});
test('should parse network error', () => {
const error = new Error('Network Error') as any;
error.name = 'AxiosError';
error.isAxiosError = true;
error.code = 'ERR_NETWORK';
error.request = {};
error.config = {
url: '/api/users',
method: 'get'
};
error.toJSON = () => ({ error: 'json representation' });
const parsed = parseAxiosError(error);
expect(parsed.name).toBe('AxiosError');
expect(parsed.message).toBe('Network Error');
expect(parsed.code).toBe('ERR_NETWORK');
expect(parsed.request).toBe('[Request sent, no response]');
expect(parsed.type).toBe('network');
});
test('should parse client error', () => {
const error = new Error('Request setup error') as any;
error.name = 'AxiosError';
error.isAxiosError = true;
error.code = 'ERR_BAD_REQUEST';
error.toJSON = () => ({ error: 'json representation' });
const parsed = parseAxiosError(error);
expect(parsed.name).toBe('AxiosError');
expect(parsed.message).toBe('Request setup error');
expect(parsed.code).toBe('ERR_BAD_REQUEST');
expect(parsed.type).toBe('client');
});
test('should include custom properties', () => {
const error = new Error('Custom error') as any;
error.name = 'AxiosError';
error.isAxiosError = true;
error.customProp = 'custom value';
error.toJSON = () => ({ error: 'json representation' });
const parsed = parseAxiosError(error);
expect(parsed.customProp).toBe('custom value');
});
});