Skip to main content
Glama
http-client.test.ts18.2 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { GrafanaHttpClient } from './http-client.js'; import type { Config, TLSConfig } from './types.js'; import fs from 'fs'; // Mock axios to avoid actual HTTP calls vi.mock('axios', () => ({ default: { create: vi.fn(() => ({ interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), })), isAxiosError: vi.fn(() => true), }, })); // Mock fs to avoid file system access vi.mock('fs', () => ({ default: { existsSync: vi.fn(() => true), readFileSync: vi.fn(() => Buffer.from('mock-cert-data')), }, })); // Mock retry client vi.mock('./retry-client.js', () => ({ ResilientErrorHandler: vi.fn().mockImplementation(() => ({ executeWithResilience: vi.fn(async (operation: Function) => operation()), getCircuitBreakerState: vi.fn(() => ({ state: 'CLOSED', failures: 0 })), resetCircuitBreaker: vi.fn(), })), })); // Mock console to prevent noise vi.mock('console', () => ({ log: vi.fn(), error: vi.fn(), })); describe('GrafanaHttpClient', () => { const mockConfig: Config = { GRAFANA_URL: 'https://grafana.test.com', GRAFANA_TOKEN: 'test-token-123', GRAFANA_DEBUG: false, GRAFANA_TIMEOUT: 30000, GRAFANA_DISABLE_TOOLS: [], GRAFANA_TLS_SKIP_VERIFY: false, }; beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { // Clear static cache between tests (GrafanaHttpClient as any).tlsFileCache?.clear(); }); describe('constructor', () => { it('should create client with basic configuration', () => { const client = new GrafanaHttpClient(mockConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); }); it('should handle debug configuration', () => { const debugConfig: Config = { ...mockConfig, GRAFANA_DEBUG: true, }; const client = new GrafanaHttpClient(debugConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); }); it('should handle custom timeout', () => { const timeoutConfig: Config = { ...mockConfig, GRAFANA_TIMEOUT: 60000, }; const client = new GrafanaHttpClient(timeoutConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); }); }); describe('TLS configuration', () => { it('should handle no TLS configuration', () => { const client = new GrafanaHttpClient(mockConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); }); it('should create TLS config when cert and key files are provided', () => { const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_CERT_FILE: '/path/to/cert.pem', GRAFANA_TLS_KEY_FILE: '/path/to/key.pem', }; const client = new GrafanaHttpClient(tlsConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); expect(fs.existsSync).toHaveBeenCalledWith('/path/to/cert.pem'); expect(fs.existsSync).toHaveBeenCalledWith('/path/to/key.pem'); }); it('should create TLS config with CA file', () => { const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_CA_FILE: '/path/to/ca.pem', GRAFANA_TLS_SKIP_VERIFY: true, }; const client = new GrafanaHttpClient(tlsConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); expect(fs.existsSync).toHaveBeenCalledWith('/path/to/ca.pem'); }); it('should throw error when TLS cert file does not exist', () => { vi.mocked(fs.existsSync).mockReturnValue(false); const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_CERT_FILE: '/nonexistent/cert.pem', GRAFANA_TLS_KEY_FILE: '/nonexistent/key.pem', }; expect(() => new GrafanaHttpClient(tlsConfig)).toThrow('TLS file not found'); }); it('should throw error when TLS key file does not exist', () => { vi.mocked(fs.existsSync) .mockReturnValueOnce(true) // cert exists .mockReturnValueOnce(false); // key doesn't exist const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_CERT_FILE: '/path/to/cert.pem', GRAFANA_TLS_KEY_FILE: '/nonexistent/key.pem', }; expect(() => new GrafanaHttpClient(tlsConfig)).toThrow('TLS file not found'); }); it('should cache TLS file contents', () => { const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_CERT_FILE: '/path/to/cert.pem', GRAFANA_TLS_KEY_FILE: '/path/to/key.pem', }; // Create multiple clients with same TLS config new GrafanaHttpClient(tlsConfig); new GrafanaHttpClient(tlsConfig); // File should only be read once due to caching expect(fs.readFileSync).toHaveBeenCalledTimes(2); // Once for cert, once for key }); it('should handle TLS skip verify flag', () => { const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_SKIP_VERIFY: true, }; const client = new GrafanaHttpClient(tlsConfig); expect(client).toBeInstanceOf(GrafanaHttpClient); }); it('should handle file read errors gracefully', () => { vi.mocked(fs.readFileSync).mockImplementation(() => { throw new Error('Permission denied'); }); const tlsConfig: Config = { ...mockConfig, GRAFANA_TLS_CERT_FILE: '/restricted/cert.pem', GRAFANA_TLS_KEY_FILE: '/restricted/key.pem', }; expect(() => new GrafanaHttpClient(tlsConfig)).toThrow('Failed to load TLS configuration'); }); }); describe('cache operations', () => { let client: GrafanaHttpClient; beforeEach(() => { client = new GrafanaHttpClient(mockConfig); }); it('should start with empty cache', () => { const stats = client.getCacheStats(); expect(stats.size).toBe(0); expect(stats.keys).toEqual([]); }); it('should generate consistent cache keys', () => { // Access the private method through any const cacheKey1 = (client as any).generateCacheKey('/api/test', { param: 'value' }); const cacheKey2 = (client as any).generateCacheKey('/api/test', { param: 'value' }); const cacheKey3 = (client as any).generateCacheKey('/api/test', { param: 'other' }); expect(cacheKey1).toBe(cacheKey2); expect(cacheKey1).not.toBe(cacheKey3); }); it('should generate cache keys without parameters', () => { const cacheKey1 = (client as any).generateCacheKey('/api/test'); const cacheKey2 = (client as any).generateCacheKey('/api/test', undefined); expect(cacheKey1).toBe(cacheKey2); }); it('should clear cache', () => { // Manually add something to cache (client as any).setCachedResponse('test-key', { data: 'test' }); expect(client.getCacheStats().size).toBe(1); client.clearCache(); expect(client.getCacheStats().size).toBe(0); }); it('should handle cache expiration', () => { const originalDateNow = Date.now; let mockTime = 1000000; vi.spyOn(Date, 'now').mockImplementation(() => mockTime); // Set cached response (client as any).setCachedResponse('test-key', { data: 'test' }); // Should return cached value const cached1 = (client as any).getCachedResponse('test-key'); expect(cached1).toEqual({ data: 'test' }); // Advance time beyond TTL (60000ms) mockTime += 70000; // Should return null (expired) const cached2 = (client as any).getCachedResponse('test-key'); expect(cached2).toBeNull(); Date.now = originalDateNow; }); }); describe('health and status methods', () => { let client: GrafanaHttpClient; beforeEach(() => { client = new GrafanaHttpClient(mockConfig); }); it('should return cache stats', () => { const stats = client.getCacheStats(); expect(stats).toHaveProperty('size'); expect(stats).toHaveProperty('keys'); expect(Array.isArray(stats.keys)).toBe(true); }); it('should return circuit breaker status', () => { const status = client.getCircuitBreakerStatus(); expect(status).toHaveProperty('state'); expect(status).toHaveProperty('failures'); }); it('should return comprehensive health status', () => { const health = client.getHealthStatus(); expect(health).toHaveProperty('cache'); expect(health).toHaveProperty('circuitBreaker'); expect(health).toHaveProperty('config'); expect(health.config.baseURL).toBe(mockConfig.GRAFANA_URL); expect(health.config.timeout).toBe(mockConfig.GRAFANA_TIMEOUT); expect(health.config.debug).toBe(mockConfig.GRAFANA_DEBUG); }); it('should reset circuit breaker', () => { client.resetCircuitBreaker(); // Should not throw error expect(true).toBe(true); }); }); describe('cleanup', () => { it('should clean up resources', () => { const client = new GrafanaHttpClient(mockConfig); // Add something to cache first (client as any).setCachedResponse('test-key', { data: 'test' }); expect(client.getCacheStats().size).toBe(1); client.cleanup(); expect(client.getCacheStats().size).toBe(0); }); it('should clear static TLS cache', () => { const client = new GrafanaHttpClient(mockConfig); client.cleanup(); // Should not throw error expect(true).toBe(true); }); }); describe('HTTP methods', () => { let client: GrafanaHttpClient; let mockAxiosInstance: any; beforeEach(() => { const axios = vi.mocked(await import('axios')).default; mockAxiosInstance = { get: vi.fn().mockResolvedValue({ data: { result: 'get-data' } }), post: vi.fn().mockResolvedValue({ data: { result: 'post-data' } }), put: vi.fn().mockResolvedValue({ data: { result: 'put-data' } }), patch: vi.fn().mockResolvedValue({ data: { result: 'patch-data' } }), delete: vi.fn().mockResolvedValue({ data: { result: 'delete-data' } }), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, }; axios.create.mockReturnValue(mockAxiosInstance); client = new GrafanaHttpClient(mockConfig); }); it('should make GET requests', async () => { const result = await client.get('/api/test', { param: 'value' }); expect(result).toEqual({ result: 'get-data' }); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/test', { params: { param: 'value' } }); }); it('should make GET requests with caching', async () => { const result1 = await client.get('/api/test', { param: 'value' }, true); const result2 = await client.get('/api/test', { param: 'value' }, true); expect(result1).toEqual({ result: 'get-data' }); expect(result2).toEqual({ result: 'get-data' }); // Should only make one actual HTTP call due to caching expect(mockAxiosInstance.get).toHaveBeenCalledTimes(1); }); it('should make POST requests', async () => { const postData = { name: 'test', value: 'data' }; const result = await client.post('/api/test', postData); expect(result).toEqual({ result: 'post-data' }); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/test', postData, undefined); }); it('should make POST requests with custom config', async () => { const postData = { name: 'test' }; const config = { headers: { 'Custom-Header': 'value' } }; const result = await client.post('/api/test', postData, config); expect(result).toEqual({ result: 'post-data' }); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/test', postData, config); }); it('should make PUT requests', async () => { const putData = { id: 1, name: 'updated' }; const result = await client.put('/api/test/1', putData); expect(result).toEqual({ result: 'put-data' }); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/api/test/1', putData, undefined); }); it('should make PATCH requests', async () => { const patchData = { name: 'patched' }; const result = await client.patch('/api/test/1', patchData); expect(result).toEqual({ result: 'patch-data' }); expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/api/test/1', patchData, undefined); }); it('should make DELETE requests', async () => { const result = await client.delete('/api/test/1'); expect(result).toEqual({ result: 'delete-data' }); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/api/test/1', undefined); }); it('should make DELETE requests with config', async () => { const config = { headers: { 'X-Force-Delete': 'true' } }; const result = await client.delete('/api/test/1', config); expect(result).toEqual({ result: 'delete-data' }); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/api/test/1', config); }); }); describe('testConnection', () => { let client: GrafanaHttpClient; let mockAxiosInstance: any; beforeEach(() => { const axios = vi.mocked(await import('axios')).default; mockAxiosInstance = { get: vi.fn(), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, }; axios.create.mockReturnValue(mockAxiosInstance); client = new GrafanaHttpClient(mockConfig); }); it('should return true for successful connection', async () => { mockAxiosInstance.get.mockResolvedValue({ data: { database: 'ok' } }); const result = await client.testConnection(); expect(result).toBe(true); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/health', { params: undefined }); }); it('should return false for failed connection', async () => { mockAxiosInstance.get.mockRejectedValue(new Error('Connection failed')); const result = await client.testConnection(); expect(result).toBe(false); }); }); describe('getInstanceInfo', () => { let client: GrafanaHttpClient; let mockAxiosInstance: any; beforeEach(() => { const axios = vi.mocked(await import('axios')).default; mockAxiosInstance = { get: vi.fn(), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, }; axios.create.mockReturnValue(mockAxiosInstance); client = new GrafanaHttpClient(mockConfig); }); it('should get instance info', async () => { const mockInfo = { buildInfo: { version: '8.0.0' } }; mockAxiosInstance.get.mockResolvedValue({ data: mockInfo }); const result = await client.getInstanceInfo(); expect(result).toEqual(mockInfo); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/frontend/settings', { params: undefined }); }); }); describe('error handling', () => { it('should handle axios errors in interceptors', () => { const client = new GrafanaHttpClient(mockConfig); // Get the response interceptor error handler const axios = vi.mocked(await import('axios')).default; const mockCreate = axios.create as any; const interceptorArgs = mockCreate.mock.results[0].value.interceptors.response.use.mock.calls[1]; // Second call (error interceptor) const errorHandler = interceptorArgs[1]; const axiosError = { response: { status: 401, statusText: 'Unauthorized', }, message: 'Request failed with status code 401', }; expect(() => errorHandler(axiosError)).rejects.toThrow(); }); }); describe('edge cases', () => { it('should handle undefined parameters in GET requests', async () => { const axios = vi.mocked(await import('axios')).default; const mockAxiosInstance = { get: vi.fn().mockResolvedValue({ data: { result: 'data' } }), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, }; axios.create.mockReturnValue(mockAxiosInstance); const client = new GrafanaHttpClient(mockConfig); const result = await client.get('/api/test'); expect(result).toEqual({ result: 'data' }); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/api/test', { params: undefined }); }); it('should handle cache with different parameter orders', () => { const client = new GrafanaHttpClient(mockConfig); const key1 = (client as any).generateCacheKey('/api/test', { a: '1', b: '2' }); const key2 = (client as any).generateCacheKey('/api/test', { b: '2', a: '1' }); // JSON.stringify may not guarantee same order, but our implementation should be consistent expect(key1).toBeDefined(); expect(key2).toBeDefined(); }); it('should handle cache with complex nested objects', () => { const client = new GrafanaHttpClient(mockConfig); const params = { simple: 'value', nested: { level1: { level2: 'deep', }, }, array: [1, 2, 3], }; const key = (client as any).generateCacheKey('/api/test', params); expect(key).toContain('/api/test'); expect(key).toBeDefined(); }); it('should handle empty config in HTTP method calls', async () => { const axios = vi.mocked(await import('axios')).default; const mockAxiosInstance = { post: vi.fn().mockResolvedValue({ data: 'result' }), interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() }, }, }; axios.create.mockReturnValue(mockAxiosInstance); const client = new GrafanaHttpClient(mockConfig); const result = await client.post('/api/test', { data: 'test' }, {}); expect(result).toBe('result'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/api/test', { data: 'test' }, {}); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quanticsoul4772/grafana-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server