Files
davila7--claude-code-templates/cli-tool/tests/unit/DataService.test.js
T
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

388 lines
13 KiB
JavaScript

/**
* Unit Tests for DataService
* Tests API communication and caching functionality
*/
// Load the DataService class
const fs = require('fs');
const path = require('path');
// Load DataService from the actual file
const DataServicePath = path.join(__dirname, '../../src/analytics-web/services/DataService.js');
const DataServiceCode = fs.readFileSync(DataServicePath, 'utf8');
// Create a module-like environment
const moduleExports = {};
const module = { exports: moduleExports };
// Execute the DataService code in our test environment
eval(DataServiceCode);
const DataService = moduleExports.DataService || global.DataService;
describe('DataService', () => {
let dataService;
let mockWebSocketService;
let mockFetch;
beforeEach(() => {
// Mock WebSocket service
mockWebSocketService = {
on: jest.fn(),
subscribe: jest.fn().mockResolvedValue(true),
isConnected: true,
requestRefresh: jest.fn().mockResolvedValue(true),
getStatus: jest.fn().mockReturnValue({ isConnected: true })
};
// Mock fetch
mockFetch = jest.fn();
global.fetch = mockFetch;
dataService = new DataService(mockWebSocketService);
});
afterEach(() => {
jest.clearAllMocks();
delete global.fetch;
});
describe('constructor', () => {
it('should initialize with default values', () => {
const service = new DataService();
expect(service.cache).toBeInstanceOf(Map);
expect(service.eventListeners).toBeInstanceOf(Set);
expect(service.baseURL).toBe('');
expect(service.realTimeEnabled).toBe(false);
});
it('should setup WebSocket integration when provided', () => {
expect(mockWebSocketService.on).toHaveBeenCalledWith('data_refresh', expect.any(Function));
expect(mockWebSocketService.on).toHaveBeenCalledWith('conversation_state_change', expect.any(Function));
expect(mockWebSocketService.on).toHaveBeenCalledWith('connected', expect.any(Function));
expect(mockWebSocketService.on).toHaveBeenCalledWith('disconnected', expect.any(Function));
});
});
describe('cachedFetch', () => {
const mockEndpoint = '/api/test';
const mockResponse = { data: 'test' };
beforeEach(() => {
mockFetch.mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue(mockResponse)
});
});
it('should fetch data and cache it', async () => {
const result = await dataService.cachedFetch(mockEndpoint);
expect(result).toEqual(mockResponse);
expect(mockFetch).toHaveBeenCalledWith(mockEndpoint, expect.objectContaining({
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}));
expect(dataService.cache.has(`${mockEndpoint}_${JSON.stringify({})}`)).toBe(true);
});
it('should return cached data when available and fresh', async () => {
// First call - cache miss
await dataService.cachedFetch(mockEndpoint);
// Second call - cache hit
const result = await dataService.cachedFetch(mockEndpoint);
expect(result).toEqual(mockResponse);
expect(mockFetch).toHaveBeenCalledTimes(1); // Only called once
});
it('should refetch when cache is expired', async () => {
// First call
await dataService.cachedFetch(mockEndpoint, { cacheDuration: 100 });
// Wait for cache to expire
await testUtils.waitFor(150);
// Second call - should refetch
await dataService.cachedFetch(mockEndpoint, { cacheDuration: 100 });
expect(mockFetch).toHaveBeenCalledTimes(2);
});
it('should handle fetch errors gracefully', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
await expect(dataService.cachedFetch(mockEndpoint)).rejects.toThrow('Network error');
});
it('should return stale cache on fetch error', async () => {
// First successful call
await dataService.cachedFetch(mockEndpoint);
// Simulate network error on second call
mockFetch.mockRejectedValue(new Error('Network error'));
const result = await dataService.cachedFetch(mockEndpoint);
expect(result).toEqual(mockResponse); // Should return cached data
});
it('should handle HTTP errors', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 404
});
await expect(dataService.cachedFetch(mockEndpoint)).rejects.toThrow('HTTP error! status: 404');
});
});
describe('API methods', () => {
beforeEach(() => {
jest.spyOn(dataService, 'cachedFetch').mockResolvedValue({ data: 'mock' });
});
it('should call correct endpoint for getConversations', async () => {
await dataService.getConversations();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/data');
});
it('should call correct endpoint for getConversationStates', async () => {
await dataService.getConversationStates();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/conversation-state',
expect.objectContaining({ cacheDuration: expect.any(Number) })
);
});
it('should adjust cache duration based on real-time status', async () => {
// With real-time enabled
dataService.realTimeEnabled = true;
await dataService.getConversationStates();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/conversation-state',
expect.objectContaining({ cacheDuration: 30000 })
);
// With real-time disabled
dataService.realTimeEnabled = false;
await dataService.getConversationStates();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/conversation-state',
expect.objectContaining({ cacheDuration: 5000 })
);
});
it('should call correct endpoints for other methods', async () => {
await dataService.getChartData();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/charts');
await dataService.getSessionData();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/session/data');
await dataService.getProjectStats();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/session/projects');
await dataService.getSystemHealth();
expect(dataService.cachedFetch).toHaveBeenCalledWith('/api/system/health');
});
});
describe('WebSocket integration', () => {
it('should enable real-time when WebSocket connects', () => {
expect(dataService.realTimeEnabled).toBe(false);
// Simulate WebSocket connection
const connectedHandler = mockWebSocketService.on.mock.calls
.find(call => call[0] === 'connected')[1];
connectedHandler();
expect(dataService.realTimeEnabled).toBe(true);
expect(mockWebSocketService.subscribe).toHaveBeenCalledWith('data_updates');
expect(mockWebSocketService.subscribe).toHaveBeenCalledWith('conversation_updates');
expect(mockWebSocketService.subscribe).toHaveBeenCalledWith('system_updates');
});
it('should disable real-time when WebSocket disconnects', () => {
dataService.realTimeEnabled = true;
jest.spyOn(dataService, 'startFallbackPolling');
// Simulate WebSocket disconnection
const disconnectedHandler = mockWebSocketService.on.mock.calls
.find(call => call[0] === 'disconnected')[1];
disconnectedHandler();
expect(dataService.realTimeEnabled).toBe(false);
expect(dataService.startFallbackPolling).toHaveBeenCalled();
});
it('should handle real-time data refresh', () => {
jest.spyOn(dataService, 'clearCacheEntry');
jest.spyOn(dataService, 'notifyListeners');
const testData = { conversations: [], summary: {} };
// Simulate data refresh event
const dataRefreshHandler = mockWebSocketService.on.mock.calls
.find(call => call[0] === 'data_refresh')[1];
dataRefreshHandler(testData);
expect(dataService.clearCacheEntry).toHaveBeenCalledWith('/api/data');
expect(dataService.clearCacheEntry).toHaveBeenCalledWith('/api/conversation-state');
expect(dataService.notifyListeners).toHaveBeenCalledWith('data_refresh', testData);
});
it('should handle real-time state changes', () => {
jest.spyOn(dataService, 'clearCacheEntry');
jest.spyOn(dataService, 'notifyListeners');
const stateData = { conversationId: 'conv_123', newState: 'active' };
// Simulate state change event
const stateChangeHandler = mockWebSocketService.on.mock.calls
.find(call => call[0] === 'conversation_state_change')[1];
stateChangeHandler(stateData);
expect(dataService.clearCacheEntry).toHaveBeenCalledWith('/api/conversation-state');
expect(dataService.notifyListeners).toHaveBeenCalledWith('conversation_state_change', stateData);
});
});
describe('requestRefresh', () => {
it('should use WebSocket refresh when available', async () => {
dataService.realTimeEnabled = true;
const result = await dataService.requestRefresh();
expect(mockWebSocketService.requestRefresh).toHaveBeenCalled();
expect(result).toBe(true);
});
it('should fallback to cache clearing when WebSocket unavailable', async () => {
dataService.realTimeEnabled = false;
jest.spyOn(dataService, 'clearCache');
const result = await dataService.requestRefresh();
expect(dataService.clearCache).toHaveBeenCalled();
expect(result).toBe(false);
});
it('should handle WebSocket request errors', async () => {
dataService.realTimeEnabled = true;
mockWebSocketService.requestRefresh.mockRejectedValue(new Error('WebSocket error'));
jest.spyOn(dataService, 'clearCache');
const result = await dataService.requestRefresh();
expect(dataService.clearCache).toHaveBeenCalled();
expect(result).toBe(false);
});
});
describe('event listeners', () => {
it('should add and notify event listeners', () => {
const mockCallback = jest.fn();
dataService.addEventListener(mockCallback);
expect(dataService.eventListeners.has(mockCallback)).toBe(true);
dataService.notifyListeners('test_event', 'test_data');
expect(mockCallback).toHaveBeenCalledWith('test_event', 'test_data');
});
it('should remove event listeners', () => {
const mockCallback = jest.fn();
dataService.addEventListener(mockCallback);
dataService.removeEventListener(mockCallback);
expect(dataService.eventListeners.has(mockCallback)).toBe(false);
});
it('should handle listener errors gracefully', () => {
const errorCallback = jest.fn().mockImplementation(() => {
throw new Error('Listener error');
});
dataService.addEventListener(errorCallback);
expect(() => {
dataService.notifyListeners('test_event', 'test_data');
}).not.toThrow();
});
});
describe('cache management', () => {
it('should clear entire cache', () => {
dataService.cache.set('test_key', { data: 'test' });
dataService.clearCache();
expect(dataService.cache.size).toBe(0);
});
it('should clear specific cache entries', () => {
dataService.cache.set('/api/data_{}', { data: 'test1' });
dataService.cache.set('/api/other_{}', { data: 'test2' });
dataService.clearCacheEntry('/api/data');
expect(dataService.cache.has('/api/data_{}')).toBe(false);
expect(dataService.cache.has('/api/other_{}')).toBe(true);
});
});
describe('getCacheStats', () => {
it('should return cache statistics', () => {
dataService.cache.set('test1', { data: 'test' });
dataService.cache.set('test2', { data: 'test' });
const stats = dataService.getCacheStats();
expect(stats).toMatchObject({
size: 2,
keys: expect.arrayContaining(['test1', 'test2']),
listeners: 0,
realTimeEnabled: expect.any(Boolean),
webSocketConnected: true
});
});
});
describe('startPeriodicRefresh', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should skip polling when real-time is enabled', () => {
dataService.realTimeEnabled = true;
dataService.startPeriodicRefresh();
expect(dataService.refreshInterval).toBeUndefined();
});
it('should start polling when real-time is disabled', () => {
dataService.realTimeEnabled = false;
jest.spyOn(dataService, 'getConversations').mockResolvedValue({});
jest.spyOn(dataService, 'getConversationStates').mockResolvedValue({});
dataService.startPeriodicRefresh(1000);
expect(dataService.refreshInterval).toBeDefined();
// Fast forward time
jest.advanceTimersByTime(1000);
expect(dataService.getConversations).toHaveBeenCalled();
expect(dataService.getConversationStates).toHaveBeenCalled();
});
});
});