Skip to main content
Glama

HomeAssistant MCP

sse.features.test.ts13.4 kB
import { SSEManager } from "../index"; import type { SSEClient } from "../index"; import type { HassEntity, HassEvent } from "../../interfaces/hass"; import { TokenManager } from "../../security/index"; import { describe, it, expect, beforeEach, afterEach, mock, Mock, test, } from "bun:test"; describe("SSE Core Features", () => { let sseManager: SSEManager; const TEST_IP = "127.0.0.1"; const validToken = "valid_token_that_meets_minimum_length_requirement_123456"; let validateTokenMock: Mock<(token: string, ip?: string) => { valid: boolean; error?: string }>; beforeEach(() => { sseManager = new SSEManager({ pingInterval: 100, // Shorter interval for testing cleanupInterval: 200, maxConnectionAge: 1000, }); // Mock token validation to always succeed with our test token validateTokenMock = mock((token: string) => ({ valid: token === validToken, error: token !== validToken ? "Invalid token" : undefined, })); TokenManager.validateToken = validateTokenMock; }); afterEach(() => { validateTokenMock.mockReset(); }); function createTestClient( id: string, ): Omit<SSEClient, "authenticated" | "subscriptions" | "rateLimit"> { return { id, ip: TEST_IP, connectedAt: new Date(), connectionTime: Date.now(), send: mock((data: string) => { }), }; } describe("State Management", () => { it("should track and update entity states", () => { const client = createTestClient("test-client"); const sseClient = sseManager.addClient(client, validToken); expect(sseClient).toBeTruthy(); const entityId = "light.living_room"; const initialState: HassEntity = { entity_id: entityId, state: "off", attributes: { brightness: 0 }, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }; // Update state sseManager.updateEntityState(entityId, initialState); // Subscribe client to entity sseManager.subscribeToEntity(sseClient.id, entityId); // Verify initial state was sent const sendMock = client.send as Mock<(data: string) => void>; expect(sendMock.mock.calls.length).toBe(1); const sentData = JSON.parse(sendMock.mock.calls[0]?.[0]); expect(sentData.type).toBe("state_changed"); expect(sentData.data.entity_id).toBe(entityId); expect(sentData.data.state).toBe("off"); }); it("should handle state updates and notify subscribers", () => { const client = createTestClient("test-client"); const sseClient = sseManager.addClient(client, validToken); expect(sseClient).toBeTruthy(); const entityId = "light.living_room"; sseManager.subscribeToEntity(sseClient.id, entityId); // Update state multiple times const states: HassEntity[] = [ { entity_id: entityId, state: "off", attributes: { brightness: 0 }, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }, { entity_id: entityId, state: "on", attributes: { brightness: 100 }, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }, { entity_id: entityId, state: "on", attributes: { brightness: 50 }, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }, ]; for (const state of states) { sseManager.updateEntityState(entityId, state); } const sendMock = client.send as Mock<(data: string) => void>; expect(sendMock.mock.calls.length).toBe(states.length); // Verify last state const lastSentData = JSON.parse(sendMock.mock.calls[2]?.[0]); expect(lastSentData.data.state).toBe("on"); expect(lastSentData.data.attributes.brightness).toBe(50); }); }); describe("Domain Subscriptions", () => { it("should handle domain-wide subscriptions", () => { const client = createTestClient("test-client"); const sseClient = sseManager.addClient(client, validToken); expect(sseClient).toBeTruthy(); const domain = "light"; sseManager.subscribeToDomain(sseClient.id, domain); // Update states for multiple entities in the domain const entities = ["light.living_room", "light.kitchen", "light.bedroom"]; for (const entityId of entities) { sseManager.updateEntityState(entityId, { entity_id: entityId, state: "on", attributes: {}, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }); } const sendMock = client.send as Mock<(data: string) => void>; expect(sendMock.mock.calls.length).toBe(entities.length); // Verify non-domain entities don't trigger updates sseManager.updateEntityState("switch.fan", { entity_id: "switch.fan", state: "on", attributes: {}, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }); expect(sendMock.mock.calls.length).toBe(entities.length); // Should not increase }); }); describe("Connection Maintenance", () => { it("should send periodic pings to keep connections alive", async () => { const client = createTestClient("test-client"); const sseClient = sseManager.addClient(client, validToken); expect(sseClient).toBeTruthy(); // Wait for ping interval await new Promise((resolve) => setTimeout(resolve, 150)); const sendMock = client.send as Mock<(data: string) => void>; expect(sendMock.mock.calls.length).toBeGreaterThanOrEqual(1); const pingData = JSON.parse(sendMock.mock.calls[0]?.[0]); expect(pingData.type).toBe("ping"); expect(pingData.timestamp).toBeTruthy(); }); it("should cleanup inactive connections", async () => { const client = createTestClient("test-client"); const sseClient = sseManager.addClient(client, validToken); expect(sseClient).toBeTruthy(); // Simulate connection age exceeding limit sseClient.connectedAt = new Date(Date.now() - 2000); // Older than maxConnectionAge // Wait for cleanup interval await new Promise((resolve) => setTimeout(resolve, 250)); // Client should be removed expect(sseManager.getStatistics().totalClients).toBe(0); }); }); describe("Error Handling", () => { it("should handle client send errors gracefully", async () => { const client = createTestClient("test-client"); const errorMock = mock(() => { console.log("Mock send function throwing error"); throw new Error("Send failed"); }); client.send = errorMock; const sseClient = sseManager.addClient(client, validToken); if (!sseClient) { throw new Error("Failed to add client"); } // Subscribe to entity to ensure we get updates sseManager.subscribeToEntity(sseClient.id, "light.test"); // Get initial client count const initialCount = sseManager.getStatistics().totalClients; console.log(`Initial client count: ${initialCount}`); // Attempt to send message sseManager.updateEntityState("light.test", { entity_id: "light.test", state: "on", attributes: {}, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }); // Wait for error handling to complete await new Promise(resolve => setTimeout(resolve, 50)); // Verify error was thrown expect(errorMock).toHaveBeenCalled(); // Get final client count const finalCount = sseManager.getStatistics().totalClients; console.log(`Final client count: ${finalCount}`); // Client should be removed due to send failure expect(finalCount).toBe(0); }); it("should handle invalid entity updates", () => { const client = createTestClient("test-client"); const sseClient = sseManager.addClient(client, validToken); expect(sseClient).toBeTruthy(); // Subscribe to entity const entityId = "light.test"; sseManager.subscribeToEntity(sseClient.id, entityId); // Update with invalid state const invalidState = { entity_id: entityId, state: undefined, attributes: {}, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, } as unknown as HassEntity; sseManager.updateEntityState(entityId, invalidState); const sendMock = client.send as Mock<(data: string) => void>; expect(sendMock.mock.calls.length).toBe(0); // Should not send invalid state }); }); describe("Memory Management", () => { it("should limit the number of stored entity states", () => { // Create many entities for (let i = 0; i < 1000; i++) { sseManager.updateEntityState(`test.entity_${i}`, { entity_id: `test.entity_${i}`, state: "on", attributes: {}, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }); } // Check that stored states are within reasonable limits expect(Object.keys(sseManager["entityStates"]).length).toBeLessThanOrEqual(1000); }); }); describe("Concurrent Operations", () => { it("should handle multiple simultaneous subscriptions", () => { // Create and add clients const rawClients = Array.from({ length: 5 }, (_, i) => createTestClient(`client_${i}`)); const clients = rawClients .map(client => sseManager.addClient(client, validToken)) .filter((client): client is SSEClient => client !== null); expect(clients.length).toBe(5); // Subscribe all clients to same entity const entityId = "light.test"; clients.forEach(client => { sseManager.subscribeToEntity(client.id, entityId); }); // Update entity state sseManager.updateEntityState(entityId, { entity_id: entityId, state: "on", attributes: {}, last_changed: new Date().toISOString(), last_updated: new Date().toISOString(), context: { id: "test_context" }, }); // Verify all clients received update rawClients.forEach(client => { const sendMock = client.send as Mock<(data: string) => void>; expect(sendMock.mock.calls.length).toBe(1); }); }); }); // Future test cases to implement test.todo("should handle reconnection attempts with exponential backoff"); test.todo("should properly clean up resources when client disconnects"); test.todo("should handle message queuing when client temporarily disconnects"); test.todo("should validate message format before sending to clients"); test.todo("should handle client subscription to multiple domains"); test.todo("should properly handle client unsubscribe requests"); test.todo("should enforce per-domain rate limits"); test.todo("should handle large numbers of concurrent state updates"); test.todo("should maintain message order for each client"); test.todo("should handle client authentication timeout"); });

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/jango-blockchained/advanced-homeassistant-mcp'

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