import { describe, it, expect, beforeEach, afterEach, jest } from 'bun:test'; import { EventEmitter } from 'events'; import { WebSocket } from 'ws'; import { StrategyExecutionService } from '../../core/execution/StrategyExecutionService'; import { StrategyRegistry } from '../../core/strategies/StrategyRegistry'; import { MarketDataFeed } from '../../core/backtesting/MarketDataFeed'; import { BaseStrategy, BarData, Order } from '../../core/Strategy'; // Mock WebSocket to avoid actual network connections during tests jest.mock('ws', () => { const EventEmitter = require('events'); class MockWebSocket extends EventEmitter { static OPEN = 1; readyState = 1; close = jest.fn(); send = jest.fn(); } class MockServer extends EventEmitter { clients = new Set(); constructor() { super(); // Add a mock client to the set const mockClient = new MockWebSocket(); this.clients.add(mockClient); } close(callback) { callback(); } } return { WebSocket: MockWebSocket, Server: MockServer }; }); // Mock MarketDataFeed to avoid actual API calls jest.mock('../../core/backtesting/MarketDataFeed', () => { return { MarketDataFeed: class { async getHistoricalData(symbol, resolution, startDate, endDate) { // Return mock data return [ { symbol, timestamp: new Date(), open: 100, high: 105, low: 95, close: 102, volume: 1000 } ]; } } }; }); // Mock strategy for testing class MockStrategy extends BaseStrategy { name = 'MockStrategy'; description = 'A mock strategy for testing'; symbols = ['AAPL', 'MSFT']; parameters = { param1: 1, param2: 2 }; constructor(id: string) { super(id); } async start(): Promise {} async stop(): Promise {} onBar(bar: BarData) { // Return a mock signal return { action: 'BUY', symbol: bar.symbol, price: bar.close, quantity: 10, metadata: { reason: 'Test signal' } }; } async onOrderFilled(order: Order): Promise {} } // Mock StrategyRegistry jest.mock('../../core/strategies/StrategyRegistry', () => { const mockInstance = { getStrategyById: jest.fn(), getStrategyTypes: () => [{ id: 'mock-strategy', name: 'Mock Strategy' }], getAllStrategies: () => [new MockStrategy('mock-1')] }; return { StrategyRegistry: { getInstance: () => mockInstance } }; }); describe('StrategyExecutionService', () => { let executionService: StrategyExecutionService; let strategyRegistry: typeof StrategyRegistry.getInstance; beforeEach(() => { // Reset mocks jest.clearAllMocks(); // Create a new execution service for each test executionService = new StrategyExecutionService('http://localhost:3001/api', 8082); strategyRegistry = StrategyRegistry.getInstance(); // Setup mock strategy const mockStrategy = new MockStrategy('test-strategy'); (strategyRegistry.getStrategyById as jest.Mock).mockReturnValue(mockStrategy); }); afterEach(() => { executionService.shutdown(); }); it('should initialize correctly', () => { expect(executionService).toBeDefined(); }); it('should start a strategy correctly', () => { // Arrange & Act const result = executionService.startStrategy('test-strategy'); // Assert expect(result).toBe(true); expect(strategyRegistry.getStrategyById).toHaveBeenCalledWith('test-strategy'); // Check if WebSocket broadcast happened const ws = executionService['webSocketServer'].clients.values().next().value; expect(ws.send).toHaveBeenCalled(); // Check the broadcast message contains the correct type const lastCall = ws.send.mock.calls[0][0]; const message = JSON.parse(lastCall); expect(message.type).toBe('strategy_started'); expect(message.data.strategyId).toBe('test-strategy'); }); it('should stop a strategy correctly', () => { // Arrange executionService.startStrategy('test-strategy'); // Act const result = executionService.stopStrategy('test-strategy'); // Assert expect(result).toBe(true); // Check if WebSocket broadcast happened const ws = executionService['webSocketServer'].clients.values().next().value; const lastCallIndex = ws.send.mock.calls.length - 1; const lastCall = ws.send.mock.calls[lastCallIndex][0]; const message = JSON.parse(lastCall); expect(message.type).toBe('strategy_stopped'); expect(message.data.strategyId).toBe('test-strategy'); }); it('should pause a strategy correctly', () => { // Arrange executionService.startStrategy('test-strategy'); // Act const result = executionService.pauseStrategy('test-strategy'); // Assert expect(result).toBe(true); // Check if WebSocket broadcast happened const ws = executionService['webSocketServer'].clients.values().next().value; const lastCallIndex = ws.send.mock.calls.length - 1; const lastCall = ws.send.mock.calls[lastCallIndex][0]; const message = JSON.parse(lastCall); expect(message.type).toBe('strategy_paused'); expect(message.data.strategyId).toBe('test-strategy'); }); it('should process market data and generate signals', async () => { // Arrange executionService.startStrategy('test-strategy'); // Act - Trigger market data polling manually await executionService['pollMarketData']('test-strategy'); // Assert - Check if signal was generated and broadcast const ws = executionService['webSocketServer'].clients.values().next().value; // Find the strategy_signal message const signalMessages = ws.send.mock.calls .map(call => JSON.parse(call[0])) .filter(msg => msg.type === 'strategy_signal'); expect(signalMessages.length).toBeGreaterThan(0); expect(signalMessages[0].data.action).toBe('BUY'); expect(signalMessages[0].data.strategyId).toBe('test-strategy'); }); it('should handle WebSocket client connections', () => { // Arrange const mockWs = new WebSocket(); const mockMessage = JSON.stringify({ type: 'get_active_strategies' }); // Act - Simulate connection and message executionService['webSocketServer'].emit('connection', mockWs); mockWs.emit('message', mockMessage); // Assert expect(mockWs.send).toHaveBeenCalled(); // Check that the response is a strategy_status_list message const lastCall = mockWs.send.mock.calls[0][0]; const message = JSON.parse(lastCall); expect(message.type).toBe('strategy_status_list'); }); it('should shut down correctly', () => { // Act executionService.shutdown(); // Assert - WebSocket server should be closed const ws = executionService['webSocketServer'].clients.values().next().value; expect(ws.close).toHaveBeenCalled(); }); });