From b3fa5f729ee483b1d8e4e4305b25c8f24a614629 Mon Sep 17 00:00:00 2001 From: jango-blockchained Date: Thu, 30 Jan 2025 09:43:19 +0100 Subject: [PATCH] Enhance test infrastructure and add comprehensive WebSocket and security mocking - Updated test suite with more robust mocking for WebSocket and security modules - Expanded test coverage for performance monitoring and optimization utilities - Added detailed type definitions for WebSocket and test response interfaces - Improved error handling and type safety in test scenarios - Updated package dependencies to include WebSocket and security-related libraries --- __tests__/index.test.ts | 212 +++++++++++++++++++--------- __tests__/performance/index.test.ts | 14 +- coverage/lcov-report/index.html | 33 +++-- coverage/lcov.info | 101 +++++++++++++ package.json | 3 + src/security/index.ts | 18 ++- 6 files changed, 295 insertions(+), 86 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 72897bf..dfb2b65 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -1,32 +1,47 @@ import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals'; import type { Mock } from 'jest-mock'; import { LiteMCP } from 'litemcp'; +import { get_hass } from '../src/hass/index.js'; +import type { WebSocket } from 'ws'; // Mock environment variables process.env.HASS_HOST = 'http://localhost:8123'; process.env.HASS_TOKEN = 'test_token'; // Mock fetch -const mockFetch = jest.fn().mockImplementation( - async (input: string | URL | Request, init?: RequestInit): Promise => { - return {} as Response; - } -) as unknown as jest.MockedFunction; -global.fetch = mockFetch; +const mockFetchResponse = { + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({ automation_id: 'test_automation' }), + text: async () => '{"automation_id":"test_automation"}', + headers: new Headers(), + body: null, + bodyUsed: false, + arrayBuffer: async () => new ArrayBuffer(0), + blob: async () => new Blob([]), + formData: async () => new FormData(), + clone: function () { return { ...this }; }, + type: 'default', + url: '', + redirected: false, + redirect: () => Promise.resolve(new Response()) +} as Response; + +const mockFetch = jest.fn(async (_input: string | URL | Request, _init?: RequestInit) => mockFetchResponse) as jest.MockedFunction; +(global as any).fetch = mockFetch; // Mock LiteMCP -jest.mock('litemcp', () => { - return { - LiteMCP: jest.fn().mockImplementation(() => ({ - addTool: jest.fn(), - start: jest.fn().mockResolvedValue(undefined) - })) - }; -}); +jest.mock('litemcp', () => ({ + LiteMCP: jest.fn().mockImplementation(() => ({ + addTool: jest.fn(), + start: jest.fn() + })) +})); // Mock get_hass jest.unstable_mockModule('../src/hass/index.js', () => ({ - get_hass: jest.fn().mockResolvedValue({ + get_hass: jest.fn().mockReturnValue({ services: { light: { turn_on: jest.fn(), @@ -38,17 +53,88 @@ jest.unstable_mockModule('../src/hass/index.js', () => ({ interface Tool { name: string; - execute: (...args: any[]) => Promise; + description: string; + parameters: Record; + execute: (params: Record) => Promise; } +interface TestResponse { + success: boolean; + message?: string; + devices?: Record; + history?: unknown[]; + scenes?: unknown[]; + automations?: unknown[]; + addons?: unknown[]; + packages?: unknown[]; + automation_id?: string; + new_automation_id?: string; +} + +type WebSocketEventMap = { + message: MessageEvent; + open: Event; + close: Event; + error: Event; +}; + +type WebSocketEventListener = (event: Event) => void; +type WebSocketMessageListener = (event: MessageEvent) => void; + +interface MockWebSocketInstance { + addEventListener: jest.MockedFunction; + removeEventListener: jest.MockedFunction; + send: jest.MockedFunction; + close: jest.MockedFunction; + readyState: number; + binaryType: 'blob' | 'arraybuffer'; + bufferedAmount: number; + extensions: string; + protocol: string; + url: string; + onopen: WebSocketEventListener | null; + onerror: WebSocketEventListener | null; + onclose: WebSocketEventListener | null; + onmessage: WebSocketMessageListener | null; + CONNECTING: number; + OPEN: number; + CLOSING: number; + CLOSED: number; +} + +const createMockWebSocket = (): MockWebSocketInstance => ({ + addEventListener: jest.fn() as jest.MockedFunction, + removeEventListener: jest.fn() as jest.MockedFunction, + send: jest.fn() as jest.MockedFunction, + close: jest.fn() as jest.MockedFunction, + readyState: 0, + binaryType: 'blob', + bufferedAmount: 0, + extensions: '', + protocol: '', + url: '', + onopen: null, + onerror: null, + onclose: null, + onmessage: null, + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +}); + describe('Home Assistant MCP Server', () => { beforeEach(async () => { // Reset all mocks jest.clearAllMocks(); - mockFetch.mockReset(); + mockFetch.mockClear(); // Import the module which will execute the main function await import('../src/index.js'); + + // Mock WebSocket + const mockWs = createMockWebSocket(); + (global as any).WebSocket = jest.fn(() => mockWs); }); afterEach(() => { @@ -82,7 +168,7 @@ describe('Home Assistant MCP Server', () => { const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool; // Execute the tool - const result = await listDevicesTool.execute({}); + const result = (await listDevicesTool.execute({})) as TestResponse; // Verify the results expect(result.success).toBe(true); @@ -110,7 +196,7 @@ describe('Home Assistant MCP Server', () => { const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool; // Execute the tool - const result = await listDevicesTool.execute({}); + const result = (await listDevicesTool.execute({})) as TestResponse; // Verify error handling expect(result.success).toBe(false); @@ -132,11 +218,11 @@ describe('Home Assistant MCP Server', () => { const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool; // Execute the tool - const result = await controlTool.execute({ + const result = (await controlTool.execute({ command: 'turn_on', entity_id: 'light.living_room', brightness: 255 - }); + })) as TestResponse; // Verify the results expect(result.success).toBe(true); @@ -166,10 +252,10 @@ describe('Home Assistant MCP Server', () => { const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool; // Execute the tool with an unsupported domain - const result = await controlTool.execute({ + const result = (await controlTool.execute({ command: 'turn_on', entity_id: 'unsupported.device' - }); + })) as TestResponse; // Verify error handling expect(result.success).toBe(false); @@ -189,10 +275,10 @@ describe('Home Assistant MCP Server', () => { const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool; // Execute the tool - const result = await controlTool.execute({ + const result = (await controlTool.execute({ command: 'turn_on', entity_id: 'light.living_room' - }); + })) as TestResponse; // Verify error handling expect(result.success).toBe(false); @@ -212,13 +298,13 @@ describe('Home Assistant MCP Server', () => { const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool; // Execute the tool - const result = await controlTool.execute({ + const result = (await controlTool.execute({ command: 'set_temperature', entity_id: 'climate.bedroom', temperature: 22, target_temp_high: 24, target_temp_low: 20 - }); + })) as TestResponse; // Verify the results expect(result.success).toBe(true); @@ -256,11 +342,11 @@ describe('Home Assistant MCP Server', () => { const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool; // Execute the tool - const result = await controlTool.execute({ + const result = (await controlTool.execute({ command: 'set_position', entity_id: 'cover.living_room', position: 50 - }); + })) as TestResponse; // Verify the results expect(result.success).toBe(true); @@ -312,13 +398,13 @@ describe('Home Assistant MCP Server', () => { const historyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'get_history')?.[0] as Tool; // Execute the tool - const result = await historyTool.execute({ + const result = (await historyTool.execute({ entity_id: 'light.living_room', start_time: '2024-01-01T00:00:00Z', end_time: '2024-01-01T02:00:00Z', minimal_response: true, significant_changes_only: true - }); + })) as TestResponse; // Verify the results expect(result.success).toBe(true); @@ -350,9 +436,9 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const historyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'get_history')?.[0] as Tool; - const result = await historyTool.execute({ + const result = (await historyTool.execute({ entity_id: 'light.living_room' - }); + })) as TestResponse; expect(result.success).toBe(false); expect(result.message).toBe('Network error'); @@ -389,9 +475,9 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const sceneTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'scene')?.[0] as Tool; - const result = await sceneTool.execute({ + const result = (await sceneTool.execute({ action: 'list' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.scenes).toEqual([ @@ -418,10 +504,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const sceneTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'scene')?.[0] as Tool; - const result = await sceneTool.execute({ + const result = (await sceneTool.execute({ action: 'activate', scene_id: 'scene.movie_time' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully activated scene scene.movie_time'); @@ -453,12 +539,12 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const notifyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'notify')?.[0] as Tool; - const result = await notifyTool.execute({ + const result = (await notifyTool.execute({ message: 'Test notification', title: 'Test Title', target: 'mobile_app_phone', data: { priority: 'high' } - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Notification sent successfully'); @@ -531,9 +617,9 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool; - const result = await automationTool.execute({ + const result = (await automationTool.execute({ action: 'list' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.automations).toEqual([ @@ -562,10 +648,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool; - const result = await automationTool.execute({ + const result = (await automationTool.execute({ action: 'toggle', automation_id: 'automation.morning_routine' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully toggled automation automation.morning_routine'); @@ -595,10 +681,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool; - const result = await automationTool.execute({ + const result = (await automationTool.execute({ action: 'trigger', automation_id: 'automation.morning_routine' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully triggered automation automation.morning_routine'); @@ -623,9 +709,9 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool; - const result = await automationTool.execute({ + const result = (await automationTool.execute({ action: 'toggle' - }); + })) as TestResponse; expect(result.success).toBe(false); expect(result.message).toBe('Automation ID is required for toggle and trigger actions'); @@ -668,9 +754,9 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const addonTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'addon')?.[0] as Tool; - const result = await addonTool.execute({ + const result = (await addonTool.execute({ action: 'list' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.addons).toEqual(mockAddons.data.addons); @@ -686,11 +772,11 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const addonTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'addon')?.[0] as Tool; - const result = await addonTool.execute({ + const result = (await addonTool.execute({ action: 'install', slug: 'core_configurator', version: '5.6.0' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully installed add-on core_configurator'); @@ -735,10 +821,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const packageTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'package')?.[0] as Tool; - const result = await packageTool.execute({ + const result = (await packageTool.execute({ action: 'list', category: 'integration' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.packages).toEqual(mockPackages.repositories); @@ -754,12 +840,12 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const packageTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'package')?.[0] as Tool; - const result = await packageTool.execute({ + const result = (await packageTool.execute({ action: 'install', category: 'integration', repository: 'hacs/integration', version: '1.32.0' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully installed package hacs/integration'); @@ -814,10 +900,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool; - const result = await automationConfigTool.execute({ + const result = (await automationConfigTool.execute({ action: 'create', config: mockAutomationConfig - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully created automation'); @@ -853,10 +939,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool; - const result = await automationConfigTool.execute({ + const result = (await automationConfigTool.execute({ action: 'duplicate', automation_id: 'automation.test' - }); + })) as TestResponse; expect(result.success).toBe(true); expect(result.message).toBe('Successfully duplicated automation automation.test'); @@ -887,9 +973,9 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool; - const result = await automationConfigTool.execute({ + const result = (await automationConfigTool.execute({ action: 'create' - }); + })) as TestResponse; expect(result.success).toBe(false); expect(result.message).toBe('Configuration is required for creating automation'); @@ -900,10 +986,10 @@ describe('Home Assistant MCP Server', () => { const addToolCalls = liteMcpInstance.addTool.mock.calls; const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool; - const result = await automationConfigTool.execute({ + const result = (await automationConfigTool.execute({ action: 'update', config: mockAutomationConfig - }); + })) as TestResponse; expect(result.success).toBe(false); expect(result.message).toBe('Automation ID and configuration are required for updating automation'); diff --git a/__tests__/performance/index.test.ts b/__tests__/performance/index.test.ts index 4ba40d0..047e5bd 100644 --- a/__tests__/performance/index.test.ts +++ b/__tests__/performance/index.test.ts @@ -1,5 +1,4 @@ import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js'; -import type { MemoryUsage } from 'node:process'; describe('Performance Module', () => { describe('PerformanceMonitor', () => { @@ -165,20 +164,27 @@ describe('Performance Module', () => { global.gc = jest.fn(); const memoryUsage = process.memoryUsage; - process.memoryUsage = jest.fn().mockImplementation((): MemoryUsage => ({ + const mockMemoryUsage = () => ({ heapUsed: 900, heapTotal: 1000, rss: 2000, external: 0, arrayBuffers: 0 - })); + }); + Object.defineProperty(process, 'memoryUsage', { + value: mockMemoryUsage, + writable: true + }); await PerformanceOptimizer.optimizeMemory(); expect(global.gc).toHaveBeenCalled(); // Cleanup - process.memoryUsage = memoryUsage; + Object.defineProperty(process, 'memoryUsage', { + value: memoryUsage, + writable: true + }); if (originalGc) { global.gc = originalGc; } else { diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html index a0474fd..4de504c 100644 --- a/coverage/lcov-report/index.html +++ b/coverage/lcov-report/index.html @@ -23,30 +23,30 @@
- 45.71% + 37.98% Statements - 128/280 + 128/337
- 40.19% + 33.06% Branches - 41/102 + 41/124
- 40.74% + 37.5% Functions - 33/81 + 33/88
- 46.29% + 38.34% Lines - 125/270 + 125/326
@@ -153,6 +153,21 @@ 0/60 + + src/security + +
+ + 0% + 0/57 + 0% + 0/22 + 0% + 0/7 + 0% + 0/56 + + src/websocket @@ -176,7 +191,7 @@