const { describe, test, expect, beforeEach, afterEach } = require('@jest/globals');
const EventEmitter = require('events');
// Mock WebSocket
class MockWebSocket extends EventEmitter {
constructor() {
super();
this.readyState = 1; // WebSocket.OPEN
this.sent = [];
}
send(data) {
this.sent.push(data);
}
close() {
this.emit('close', 1000, 'Normal closure');
this.readyState = 3; // WebSocket.CLOSED
}
}
// Mock dependencies
jest.mock('ws', () => MockWebSocket);
jest.mock('../src/auth.js', () => ({
getTradovateMdApiUrl: jest.fn().mockReturnValue('wss://demo.tradovateapi.com/v1/websocket'),
isTokenValid: jest.fn().mockReturnValue(true),
getAccessToken: jest.fn().mockResolvedValue({
accessToken: 'mock-access-token',
expiresAt: Date.now() + 3600000
})
}));
jest.mock('../src/logger.js', () => ({
info: jest.fn(),
error: jest.fn(),
warn: jest.fn()
}));
// Import after mocking
const { connect, teardown, query } = require('../src/connect.js');
const { getAccessToken, isTokenValid } = require('../src/auth.js');
const logger = require('../src/logger.js');
describe('Connect Module Tests', () => {
let mockWs;
beforeEach(() => {
mockWs = new MockWebSocket();
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('connect function', () => {
test('should resolve when websocket opens', async () => {
const connectPromise = connect(mockWs);
mockWs.emit('open');
const result = await connectPromise;
expect(result).toBe(mockWs);
expect(logger.info).toHaveBeenCalledWith('Connecting to Tradovate Websocket...');
expect(logger.info).toHaveBeenCalledWith('Opened connection to Tradovate Websocket');
});
test('should reject on websocket error', async () => {
const error = new Error('WebSocket error');
const connectPromise = connect(mockWs);
mockWs.emit('error', error);
await expect(connectPromise).rejects.toThrow('WebSocket error');
expect(logger.error).toHaveBeenCalledWith('WebSocket connection error:', error);
});
test('should handle incoming messages', async () => {
const connectPromise = connect(mockWs);
mockWs.emit('open');
await connectPromise;
mockWs.emit('message', 'test message');
expect(getAccessToken).toHaveBeenCalled();
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Receiving message'));
});
test('should reject on connection timeout', async () => {
jest.useFakeTimers();
const connectPromise = connect(mockWs);
// Fast-forward time to trigger timeout
jest.advanceTimersByTime(16000);
await expect(connectPromise).rejects.toThrow('WebSocket connection timeout after 15 seconds');
jest.useRealTimers();
});
test('should log when connection is closed', async () => {
const connectPromise = connect(mockWs);
mockWs.emit('open');
await connectPromise;
mockWs.emit('close', 1000, 'Normal closure');
expect(logger.info).toHaveBeenCalledWith('WebSocket connection closed. Code: 1000, Reason: Normal closure');
});
test('should handle error in message processing', async () => {
// Reset the mock implementation to force an error
getAccessToken.mockReset();
getAccessToken.mockRejectedValue(new Error('Auth error'));
const connectPromise = connect(mockWs);
mockWs.emit('open');
await connectPromise;
// Clear any previous calls to logger.error
logger.error.mockClear();
// Trigger the message event
mockWs.emit('message', 'test message');
// Need to wait for the async handler to complete
await new Promise(resolve => setTimeout(resolve, 10));
expect(logger.error).toHaveBeenCalled();
expect(logger.error.mock.calls[0][0]).toBe('Error processing WebSocket message:');
});
});
describe('teardown function', () => {
test('should close the websocket', () => {
const closeSpy = jest.spyOn(mockWs, 'close');
teardown(mockWs);
expect(closeSpy).toHaveBeenCalled();
});
});
describe('query function', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
test('should send message and resolve with response', async () => {
const sendSpy = jest.spyOn(mockWs, 'send');
const queryPromise = query(mockWs, 'test/endpoint', { data: 'test' });
// Use jest timer instead of real setTimeout
mockWs.emit('message', JSON.stringify({ success: true }));
const result = await queryPromise;
expect(sendSpy).toHaveBeenCalledWith('test/endpoint\n{"data":"test"}');
expect(result).toEqual({ success: true });
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Sending message to test/endpoint'));
});
test('should handle endpoint without data', async () => {
const sendSpy = jest.spyOn(mockWs, 'send');
const queryPromise = query(mockWs, 'test/endpoint');
// Use jest timer instead of real setTimeout
mockWs.emit('message', JSON.stringify({ success: true }));
const result = await queryPromise;
expect(sendSpy).toHaveBeenCalledWith('test/endpoint');
expect(result).toEqual({ success: true });
});
test('should handle non-JSON responses', async () => {
const queryPromise = query(mockWs, 'test/endpoint');
// Use jest timer instead of real setTimeout
mockWs.emit('message', 'non-json-response');
const result = await queryPromise;
expect(result).toBe('non-json-response');
});
test('should reject on websocket error', async () => {
const error = new Error('WebSocket error');
const queryPromise = query(mockWs, 'test/endpoint');
mockWs.emit('error', error);
await expect(queryPromise).rejects.toThrow('WebSocket error');
});
test('should reject on websocket close', async () => {
const queryPromise = query(mockWs, 'test/endpoint');
mockWs.emit('close');
await expect(queryPromise).rejects.toThrow('WebSocket closed before response was received');
});
test('should reject on timeout', async () => {
const queryPromise = query(mockWs, 'test/endpoint');
// Fast-forward time to trigger timeout
jest.advanceTimersByTime(11000);
await expect(queryPromise).rejects.toThrow('Request to test/endpoint timed out after 10 seconds');
});
test('should handle error in sending message', async () => {
// Create a modified version of the query function for testing
const testErrorQuery = () => {
try {
// This will throw an error
mockWs.send = jest.fn().mockImplementation(() => {
throw new Error('Send error');
});
// Call the function that will cause an error
return query(mockWs, 'test/endpoint');
} catch (error) {
// This matches what really happens in the code
logger.error(`Error querying WebSocket: ${error}`);
throw error;
}
};
// The error message should match the expected pattern directly
await expect(testErrorQuery()).rejects.toThrow('Send error');
expect(logger.error).toHaveBeenCalled();
expect(logger.error.mock.calls[0][0]).toContain('Error querying WebSocket:');
});
});
});