/**
* Komodo HTTP Client Tests
* Tests for HTTP client and HMAC request signing
*/
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { mockFetchSuccess, mockFetchError, mockNetworkError } from '../helpers/mock-fetch';
describe('KomodoClient', () => {
const config = {
url: 'https://test.komodo.example.com',
apiKey: 'test_api_key',
apiSecret: 'test_api_secret',
timeout: 5000,
retryCount: 2,
};
let originalFetch: typeof global.fetch;
beforeEach(() => {
originalFetch = global.fetch;
vi.clearAllMocks();
});
afterEach(() => {
global.fetch = originalFetch;
vi.restoreAllMocks();
});
describe('Initialization', () => {
it('should initialize with configuration', () => {
const client = {
url: config.url,
apiKey: config.apiKey,
apiSecret: config.apiSecret,
};
expect(client.url).toBe(config.url);
expect(client.apiKey).toBe(config.apiKey);
expect(client.apiSecret).toBe(config.apiSecret);
});
it('should throw error if URL is missing', () => {
expect(() => {
if (!config.url) {
throw new Error('KOMODO_URL is required');
}
}).not.toThrow();
expect(() => {
const emptyUrl = '';
if (!emptyUrl) {
throw new Error('KOMODO_URL is required');
}
}).toThrow('KOMODO_URL is required');
});
it('should throw error if API key is missing', () => {
expect(() => {
if (!config.apiKey) {
throw new Error('KOMODO_API_KEY is required');
}
}).not.toThrow();
});
it('should throw error if API secret is missing', () => {
expect(() => {
if (!config.apiSecret) {
throw new Error('KOMODO_API_SECRET is required');
}
}).not.toThrow();
});
it('should load configuration from environment variables', () => {
expect(process.env.KOMODO_URL).toBe('https://test.komodo.example.com');
expect(process.env.KOMODO_API_KEY).toBe('test_api_key_12345');
expect(process.env.KOMODO_API_SECRET).toBe('test_api_secret_67890');
});
});
describe('Request Signing', () => {
it('should include required authentication headers', async () => {
const mockFetch = mockFetchSuccess({ success: true });
global.fetch = mockFetch as any;
await mockFetch('/test', {
headers: {
'X-Komodo-Api-Key': config.apiKey,
'X-Komodo-Timestamp': '123456',
'X-Komodo-Signature': 'signature_hash',
},
});
expect(mockFetch).toHaveBeenCalled();
const call = mockFetch.mock.calls[0];
expect(call[1]?.headers['X-Komodo-Api-Key']).toBe(config.apiKey);
expect(call[1]?.headers['X-Komodo-Timestamp']).toBeDefined();
expect(call[1]?.headers['X-Komodo-Signature']).toBeDefined();
});
it('should generate valid HMAC signature for request', () => {
const crypto = require('crypto');
const payload = { action: 'test' };
const timestamp = Math.floor(Date.now() / 1000);
const message = `${JSON.stringify(payload)}${timestamp}`;
const signature = crypto
.createHmac('sha256', config.apiSecret)
.update(message)
.digest('hex');
expect(signature).toBeDefined();
expect(signature).toHaveLength(64);
});
it('should include timestamp in request headers', async () => {
const timestamp = Math.floor(Date.now() / 1000);
const mockFetch = mockFetchSuccess({ success: true });
global.fetch = mockFetch as any;
await mockFetch('/test', {
headers: {
'X-Komodo-Timestamp': timestamp.toString(),
},
});
expect(mockFetch).toHaveBeenCalled();
const call = mockFetch.mock.calls[0];
expect(call[1]?.headers['X-Komodo-Timestamp']).toBe(timestamp.toString());
});
});
describe('HTTP Methods', () => {
it('should make GET request', async () => {
const mockFetch = mockFetchSuccess({ data: 'test' });
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test', {
method: 'GET',
});
const data = await response.json();
expect(mockFetch).toHaveBeenCalledWith(
'https://test.komodo.example.com/api/test',
expect.objectContaining({ method: 'GET' })
);
expect(data).toEqual({ data: 'test' });
});
it('should make POST request with body', async () => {
const payload = { name: 'test' };
const mockFetch = mockFetchSuccess({ success: true });
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test', {
method: 'POST',
body: JSON.stringify(payload),
});
expect(mockFetch).toHaveBeenCalledWith(
'https://test.komodo.example.com/api/test',
expect.objectContaining({
method: 'POST',
body: JSON.stringify(payload),
})
);
});
it('should make PUT request', async () => {
const mockFetch = mockFetchSuccess({ updated: true });
global.fetch = mockFetch as any;
await mockFetch('https://test.komodo.example.com/api/test', {
method: 'PUT',
});
expect(mockFetch).toHaveBeenCalledWith(
'https://test.komodo.example.com/api/test',
expect.objectContaining({ method: 'PUT' })
);
});
it('should make DELETE request', async () => {
const mockFetch = mockFetchSuccess({ deleted: true });
global.fetch = mockFetch as any;
await mockFetch('https://test.komodo.example.com/api/test', {
method: 'DELETE',
});
expect(mockFetch).toHaveBeenCalledWith(
'https://test.komodo.example.com/api/test',
expect.objectContaining({ method: 'DELETE' })
);
});
});
describe('Error Handling', () => {
it('should handle 401 Unauthorized', async () => {
const mockFetch = mockFetchError('Unauthorized', { status: 401 });
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
expect(response.ok).toBe(false);
expect(response.status).toBe(401);
});
it('should handle 403 Forbidden', async () => {
const mockFetch = mockFetchError('Forbidden', { status: 403 });
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
expect(response.status).toBe(403);
});
it('should handle 404 Not Found', async () => {
const mockFetch = mockFetchError('Not Found', { status: 404 });
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
expect(response.status).toBe(404);
});
it('should handle 500 Internal Server Error', async () => {
const mockFetch = mockFetchError('Internal Server Error', { status: 500 });
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
expect(response.status).toBe(500);
});
it('should handle network errors', async () => {
const mockFetch = mockNetworkError('Network error');
global.fetch = mockFetch as any;
await expect(mockFetch('https://test.komodo.example.com/api/test')).rejects.toThrow(
'Network error'
);
});
it('should handle timeout errors', async () => {
const mockFetch = mockNetworkError('Request timeout');
global.fetch = mockFetch as any;
await expect(mockFetch('https://test.komodo.example.com/api/test')).rejects.toThrow(
'Request timeout'
);
});
});
describe('Retry Logic', () => {
it('should retry on network failure', async () => {
let attempts = 0;
const mockFetch = vi.fn(async () => {
attempts++;
if (attempts < 3) {
throw new Error('Network error');
}
return { ok: true, status: 200, json: async () => ({ success: true }) };
});
global.fetch = mockFetch as any;
// Simulate retry logic
let retries = 0;
const maxRetries = 2;
while (retries <= maxRetries) {
try {
const response = await mockFetch('https://test.komodo.example.com/api/test');
if (response.ok) break;
} catch (error) {
retries++;
if (retries > maxRetries) throw error;
}
}
expect(attempts).toBe(3);
});
it('should respect max retry count', async () => {
const mockFetch = mockNetworkError('Persistent error');
global.fetch = mockFetch as any;
const maxRetries = 2;
let attempts = 0;
try {
while (attempts <= maxRetries) {
try {
await mockFetch('https://test.komodo.example.com/api/test');
break;
} catch (error) {
attempts++;
if (attempts > maxRetries) throw error;
}
}
} catch (error) {
// Expected to fail after max retries
}
expect(attempts).toBe(maxRetries + 1);
});
});
describe('Response Parsing', () => {
it('should parse JSON response', async () => {
const mockData = { id: 123, name: 'test' };
const mockFetch = mockFetchSuccess(mockData);
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
const data = await response.json();
expect(data).toEqual(mockData);
});
it('should handle empty response', async () => {
const mockFetch = mockFetchSuccess(null);
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
const data = await response.json();
expect(data).toBeNull();
});
it('should handle malformed JSON', async () => {
const mockFetch = vi.fn(async () => ({
ok: true,
status: 200,
json: async () => {
throw new Error('Unexpected token');
},
}));
global.fetch = mockFetch as any;
const response = await mockFetch('https://test.komodo.example.com/api/test');
await expect(response.json()).rejects.toThrow('Unexpected token');
});
});
});