UNPKG

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