Files
homeassistant-mcp/__tests__/websocket/events.test.ts
jango-blockchained d17d881e7b test: enhance security middleware and token management tests
- Added comprehensive test coverage for TokenManager encryption and validation methods
- Implemented detailed test scenarios for security middleware functions
- Updated test cases to handle edge cases and improve input validation
- Refactored test mocks to provide more robust and realistic testing environment
- Improved error handling and input validation in security-related components
2025-02-03 18:44:38 +01:00

339 lines
11 KiB
TypeScript

import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { HassWebSocketClient } from '../../src/websocket/client.js';
import WebSocket from 'ws';
import { EventEmitter } from 'events';
import * as HomeAssistant from '../../src/types/hass.js';
// Mock WebSocket
jest.mock('ws');
describe('WebSocket Event Handling', () => {
let client: HassWebSocketClient;
let mockWebSocket: jest.Mocked<WebSocket>;
let eventEmitter: EventEmitter;
beforeEach(() => {
// Clear all mocks
jest.clearAllMocks();
// Create event emitter for mocking WebSocket events
eventEmitter = new EventEmitter();
// Create mock WebSocket instance
mockWebSocket = {
on: jest.fn((event: string, listener: (...args: any[]) => void) => {
eventEmitter.on(event, listener);
return mockWebSocket;
}),
send: jest.fn(),
close: jest.fn(),
readyState: WebSocket.OPEN,
removeAllListeners: jest.fn(),
// Add required WebSocket properties
binaryType: 'arraybuffer',
bufferedAmount: 0,
extensions: '',
protocol: '',
url: 'ws://test.com',
isPaused: () => false,
ping: jest.fn(),
pong: jest.fn(),
terminate: jest.fn()
} as unknown as jest.Mocked<WebSocket>;
// Mock WebSocket constructor
(WebSocket as unknown as jest.Mock).mockImplementation(() => mockWebSocket);
// Create client instance
client = new HassWebSocketClient('ws://test.com', 'test-token');
});
afterEach(() => {
eventEmitter.removeAllListeners();
client.disconnect();
});
it('should handle connection events', () => {
// Simulate open event
eventEmitter.emit('open');
// Verify authentication message was sent
expect(mockWebSocket.send).toHaveBeenCalledWith(
expect.stringContaining('"type":"auth"')
);
});
it('should handle authentication response', () => {
// Simulate auth_ok message
eventEmitter.emit('message', JSON.stringify({ type: 'auth_ok' }));
// Verify client is ready for commands
expect(mockWebSocket.readyState).toBe(WebSocket.OPEN);
});
it('should handle auth failure', () => {
// Simulate auth_invalid message
eventEmitter.emit('message', JSON.stringify({
type: 'auth_invalid',
message: 'Invalid token'
}));
// Verify client attempts to close connection
expect(mockWebSocket.close).toHaveBeenCalled();
});
it('should handle connection errors', () => {
// Create error spy
const errorSpy = jest.fn();
client.on('error', errorSpy);
// Simulate error
const testError = new Error('Test error');
eventEmitter.emit('error', testError);
// Verify error was handled
expect(errorSpy).toHaveBeenCalledWith(testError);
});
it('should handle disconnection', () => {
// Create close spy
const closeSpy = jest.fn();
client.on('close', closeSpy);
// Simulate close
eventEmitter.emit('close');
// Verify close was handled
expect(closeSpy).toHaveBeenCalled();
});
it('should handle event messages', () => {
// Create event spy
const eventSpy = jest.fn();
client.on('event', eventSpy);
// Simulate event message
const eventData = {
type: 'event',
event: {
event_type: 'state_changed',
data: {
entity_id: 'light.test',
new_state: { state: 'on' }
}
}
};
eventEmitter.emit('message', JSON.stringify(eventData));
// Verify event was handled
expect(eventSpy).toHaveBeenCalledWith(eventData.event);
});
describe('Connection Events', () => {
it('should handle successful connection', (done) => {
client.on('open', () => {
expect(mockWebSocket.send).toHaveBeenCalled();
done();
});
eventEmitter.emit('open');
});
it('should handle connection errors', (done) => {
const error = new Error('Connection failed');
client.on('error', (err: Error) => {
expect(err).toBe(error);
done();
});
eventEmitter.emit('error', error);
});
it('should handle connection close', (done) => {
client.on('disconnected', () => {
expect(mockWebSocket.close).toHaveBeenCalled();
done();
});
eventEmitter.emit('close');
});
});
describe('Authentication', () => {
it('should send authentication message on connect', () => {
const authMessage: HomeAssistant.AuthMessage = {
type: 'auth',
access_token: 'test_token'
};
client.connect();
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(authMessage));
});
it('should handle successful authentication', (done) => {
client.on('auth_ok', () => {
done();
});
client.connect();
eventEmitter.emit('message', JSON.stringify({ type: 'auth_ok' }));
});
it('should handle authentication failure', (done) => {
client.on('auth_invalid', () => {
done();
});
client.connect();
eventEmitter.emit('message', JSON.stringify({ type: 'auth_invalid' }));
});
});
describe('Event Subscription', () => {
it('should handle state changed events', (done) => {
const stateEvent: HomeAssistant.StateChangedEvent = {
event_type: 'state_changed',
data: {
entity_id: 'light.living_room',
new_state: {
entity_id: 'light.living_room',
state: 'on',
attributes: { brightness: 255 },
last_changed: '2024-01-01T00:00:00Z',
last_updated: '2024-01-01T00:00:00Z',
context: {
id: '123',
parent_id: null,
user_id: null
}
},
old_state: {
entity_id: 'light.living_room',
state: 'off',
attributes: {},
last_changed: '2024-01-01T00:00:00Z',
last_updated: '2024-01-01T00:00:00Z',
context: {
id: '122',
parent_id: null,
user_id: null
}
}
},
origin: 'LOCAL',
time_fired: '2024-01-01T00:00:00Z',
context: {
id: '123',
parent_id: null,
user_id: null
}
};
client.on('event', (event) => {
expect(event.data.entity_id).toBe('light.living_room');
expect(event.data.new_state.state).toBe('on');
expect(event.data.old_state.state).toBe('off');
done();
});
eventEmitter.emit('message', JSON.stringify({ type: 'event', event: stateEvent }));
});
it('should subscribe to specific events', async () => {
const subscriptionId = 1;
const callback = jest.fn();
// Mock successful subscription
const subscribePromise = client.subscribeEvents('state_changed', callback);
eventEmitter.emit('message', JSON.stringify({
id: 1,
type: 'result',
success: true
}));
await expect(subscribePromise).resolves.toBe(subscriptionId);
// Test event handling
const eventData = {
entity_id: 'light.living_room',
state: 'on'
};
eventEmitter.emit('message', JSON.stringify({
type: 'event',
event: {
event_type: 'state_changed',
data: eventData
}
}));
expect(callback).toHaveBeenCalledWith(eventData);
});
it('should unsubscribe from events', async () => {
// First subscribe
const subscriptionId = await client.subscribeEvents('state_changed', () => { });
// Then unsubscribe
const unsubscribePromise = client.unsubscribeEvents(subscriptionId);
eventEmitter.emit('message', JSON.stringify({
id: 2,
type: 'result',
success: true
}));
await expect(unsubscribePromise).resolves.toBeUndefined();
});
});
describe('Message Handling', () => {
it('should handle malformed messages', (done) => {
client.on('error', (error: Error) => {
expect(error.message).toContain('Unexpected token');
done();
});
eventEmitter.emit('message', 'invalid json');
});
it('should handle unknown message types', (done) => {
const unknownMessage = {
type: 'unknown_type',
data: {}
};
client.on('error', (error: Error) => {
expect(error.message).toContain('Unknown message type');
done();
});
eventEmitter.emit('message', JSON.stringify(unknownMessage));
});
});
describe('Reconnection', () => {
it('should attempt to reconnect on connection loss', (done) => {
let reconnectAttempts = 0;
client.on('disconnected', () => {
reconnectAttempts++;
if (reconnectAttempts === 1) {
expect(WebSocket).toHaveBeenCalledTimes(2);
done();
}
});
eventEmitter.emit('close');
});
it('should re-authenticate after reconnection', (done) => {
client.connect();
client.on('auth_ok', () => {
done();
});
eventEmitter.emit('close');
eventEmitter.emit('open');
eventEmitter.emit('message', JSON.stringify({ type: 'auth_ok' }));
});
});
});