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
This commit is contained in:
@@ -1,32 +1,47 @@
|
|||||||
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||||
import type { Mock } from 'jest-mock';
|
import type { Mock } from 'jest-mock';
|
||||||
import { LiteMCP } from 'litemcp';
|
import { LiteMCP } from 'litemcp';
|
||||||
|
import { get_hass } from '../src/hass/index.js';
|
||||||
|
import type { WebSocket } from 'ws';
|
||||||
|
|
||||||
// Mock environment variables
|
// Mock environment variables
|
||||||
process.env.HASS_HOST = 'http://localhost:8123';
|
process.env.HASS_HOST = 'http://localhost:8123';
|
||||||
process.env.HASS_TOKEN = 'test_token';
|
process.env.HASS_TOKEN = 'test_token';
|
||||||
|
|
||||||
// Mock fetch
|
// Mock fetch
|
||||||
const mockFetch = jest.fn().mockImplementation(
|
const mockFetchResponse = {
|
||||||
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
ok: true,
|
||||||
return {} as Response;
|
status: 200,
|
||||||
}
|
statusText: 'OK',
|
||||||
) as unknown as jest.MockedFunction<typeof fetch>;
|
json: async () => ({ automation_id: 'test_automation' }),
|
||||||
global.fetch = mockFetch;
|
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<typeof fetch>;
|
||||||
|
(global as any).fetch = mockFetch;
|
||||||
|
|
||||||
// Mock LiteMCP
|
// Mock LiteMCP
|
||||||
jest.mock('litemcp', () => {
|
jest.mock('litemcp', () => ({
|
||||||
return {
|
LiteMCP: jest.fn().mockImplementation(() => ({
|
||||||
LiteMCP: jest.fn().mockImplementation(() => ({
|
addTool: jest.fn(),
|
||||||
addTool: jest.fn(),
|
start: jest.fn()
|
||||||
start: jest.fn().mockResolvedValue(undefined)
|
}))
|
||||||
}))
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock get_hass
|
// Mock get_hass
|
||||||
jest.unstable_mockModule('../src/hass/index.js', () => ({
|
jest.unstable_mockModule('../src/hass/index.js', () => ({
|
||||||
get_hass: jest.fn().mockResolvedValue({
|
get_hass: jest.fn().mockReturnValue({
|
||||||
services: {
|
services: {
|
||||||
light: {
|
light: {
|
||||||
turn_on: jest.fn(),
|
turn_on: jest.fn(),
|
||||||
@@ -38,17 +53,88 @@ jest.unstable_mockModule('../src/hass/index.js', () => ({
|
|||||||
|
|
||||||
interface Tool {
|
interface Tool {
|
||||||
name: string;
|
name: string;
|
||||||
execute: (...args: any[]) => Promise<any>;
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
execute: (params: Record<string, unknown>) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TestResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
devices?: Record<string, unknown>;
|
||||||
|
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<typeof Function>;
|
||||||
|
removeEventListener: jest.MockedFunction<typeof Function>;
|
||||||
|
send: jest.MockedFunction<typeof Function>;
|
||||||
|
close: jest.MockedFunction<typeof Function>;
|
||||||
|
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<typeof Function>,
|
||||||
|
removeEventListener: jest.fn() as jest.MockedFunction<typeof Function>,
|
||||||
|
send: jest.fn() as jest.MockedFunction<typeof Function>,
|
||||||
|
close: jest.fn() as jest.MockedFunction<typeof Function>,
|
||||||
|
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', () => {
|
describe('Home Assistant MCP Server', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Reset all mocks
|
// Reset all mocks
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockFetch.mockReset();
|
mockFetch.mockClear();
|
||||||
|
|
||||||
// Import the module which will execute the main function
|
// Import the module which will execute the main function
|
||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
|
|
||||||
|
// Mock WebSocket
|
||||||
|
const mockWs = createMockWebSocket();
|
||||||
|
(global as any).WebSocket = jest.fn(() => mockWs);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
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;
|
const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await listDevicesTool.execute({});
|
const result = (await listDevicesTool.execute({})) as TestResponse;
|
||||||
|
|
||||||
// Verify the results
|
// Verify the results
|
||||||
expect(result.success).toBe(true);
|
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;
|
const listDevicesTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'list_devices')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await listDevicesTool.execute({});
|
const result = (await listDevicesTool.execute({})) as TestResponse;
|
||||||
|
|
||||||
// Verify error handling
|
// Verify error handling
|
||||||
expect(result.success).toBe(false);
|
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;
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await controlTool.execute({
|
const result = (await controlTool.execute({
|
||||||
command: 'turn_on',
|
command: 'turn_on',
|
||||||
entity_id: 'light.living_room',
|
entity_id: 'light.living_room',
|
||||||
brightness: 255
|
brightness: 255
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
// Verify the results
|
// Verify the results
|
||||||
expect(result.success).toBe(true);
|
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;
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool with an unsupported domain
|
// Execute the tool with an unsupported domain
|
||||||
const result = await controlTool.execute({
|
const result = (await controlTool.execute({
|
||||||
command: 'turn_on',
|
command: 'turn_on',
|
||||||
entity_id: 'unsupported.device'
|
entity_id: 'unsupported.device'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
// Verify error handling
|
// Verify error handling
|
||||||
expect(result.success).toBe(false);
|
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;
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await controlTool.execute({
|
const result = (await controlTool.execute({
|
||||||
command: 'turn_on',
|
command: 'turn_on',
|
||||||
entity_id: 'light.living_room'
|
entity_id: 'light.living_room'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
// Verify error handling
|
// Verify error handling
|
||||||
expect(result.success).toBe(false);
|
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;
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await controlTool.execute({
|
const result = (await controlTool.execute({
|
||||||
command: 'set_temperature',
|
command: 'set_temperature',
|
||||||
entity_id: 'climate.bedroom',
|
entity_id: 'climate.bedroom',
|
||||||
temperature: 22,
|
temperature: 22,
|
||||||
target_temp_high: 24,
|
target_temp_high: 24,
|
||||||
target_temp_low: 20
|
target_temp_low: 20
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
// Verify the results
|
// Verify the results
|
||||||
expect(result.success).toBe(true);
|
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;
|
const controlTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'control')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await controlTool.execute({
|
const result = (await controlTool.execute({
|
||||||
command: 'set_position',
|
command: 'set_position',
|
||||||
entity_id: 'cover.living_room',
|
entity_id: 'cover.living_room',
|
||||||
position: 50
|
position: 50
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
// Verify the results
|
// Verify the results
|
||||||
expect(result.success).toBe(true);
|
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;
|
const historyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'get_history')?.[0] as Tool;
|
||||||
|
|
||||||
// Execute the tool
|
// Execute the tool
|
||||||
const result = await historyTool.execute({
|
const result = (await historyTool.execute({
|
||||||
entity_id: 'light.living_room',
|
entity_id: 'light.living_room',
|
||||||
start_time: '2024-01-01T00:00:00Z',
|
start_time: '2024-01-01T00:00:00Z',
|
||||||
end_time: '2024-01-01T02:00:00Z',
|
end_time: '2024-01-01T02:00:00Z',
|
||||||
minimal_response: true,
|
minimal_response: true,
|
||||||
significant_changes_only: true
|
significant_changes_only: true
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
// Verify the results
|
// Verify the results
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
@@ -350,9 +436,9 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const historyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'get_history')?.[0] as Tool;
|
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'
|
entity_id: 'light.living_room'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toBe('Network error');
|
expect(result.message).toBe('Network error');
|
||||||
@@ -389,9 +475,9 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const sceneTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'scene')?.[0] as Tool;
|
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'
|
action: 'list'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.scenes).toEqual([
|
expect(result.scenes).toEqual([
|
||||||
@@ -418,10 +504,10 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const sceneTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'scene')?.[0] as Tool;
|
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',
|
action: 'activate',
|
||||||
scene_id: 'scene.movie_time'
|
scene_id: 'scene.movie_time'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully activated scene scene.movie_time');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const notifyTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'notify')?.[0] as Tool;
|
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',
|
message: 'Test notification',
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
target: 'mobile_app_phone',
|
target: 'mobile_app_phone',
|
||||||
data: { priority: 'high' }
|
data: { priority: 'high' }
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Notification sent successfully');
|
expect(result.message).toBe('Notification sent successfully');
|
||||||
@@ -531,9 +617,9 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
|
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'
|
action: 'list'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.automations).toEqual([
|
expect(result.automations).toEqual([
|
||||||
@@ -562,10 +648,10 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
|
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',
|
action: 'toggle',
|
||||||
automation_id: 'automation.morning_routine'
|
automation_id: 'automation.morning_routine'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully toggled automation automation.morning_routine');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
|
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',
|
action: 'trigger',
|
||||||
automation_id: 'automation.morning_routine'
|
automation_id: 'automation.morning_routine'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully triggered automation automation.morning_routine');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation')?.[0] as Tool;
|
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'
|
action: 'toggle'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toBe('Automation ID is required for toggle and trigger actions');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const addonTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'addon')?.[0] as Tool;
|
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'
|
action: 'list'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.addons).toEqual(mockAddons.data.addons);
|
expect(result.addons).toEqual(mockAddons.data.addons);
|
||||||
@@ -686,11 +772,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const addonTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'addon')?.[0] as Tool;
|
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',
|
action: 'install',
|
||||||
slug: 'core_configurator',
|
slug: 'core_configurator',
|
||||||
version: '5.6.0'
|
version: '5.6.0'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully installed add-on core_configurator');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const packageTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'package')?.[0] as Tool;
|
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',
|
action: 'list',
|
||||||
category: 'integration'
|
category: 'integration'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.packages).toEqual(mockPackages.repositories);
|
expect(result.packages).toEqual(mockPackages.repositories);
|
||||||
@@ -754,12 +840,12 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const packageTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'package')?.[0] as Tool;
|
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',
|
action: 'install',
|
||||||
category: 'integration',
|
category: 'integration',
|
||||||
repository: 'hacs/integration',
|
repository: 'hacs/integration',
|
||||||
version: '1.32.0'
|
version: '1.32.0'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully installed package hacs/integration');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
|
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',
|
action: 'create',
|
||||||
config: mockAutomationConfig
|
config: mockAutomationConfig
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully created automation');
|
expect(result.message).toBe('Successfully created automation');
|
||||||
@@ -853,10 +939,10 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
const addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
|
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',
|
action: 'duplicate',
|
||||||
automation_id: 'automation.test'
|
automation_id: 'automation.test'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.message).toBe('Successfully duplicated automation automation.test');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
|
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'
|
action: 'create'
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toBe('Configuration is required for creating automation');
|
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 addToolCalls = liteMcpInstance.addTool.mock.calls;
|
||||||
const automationConfigTool = addToolCalls.find((call: { 0: Tool }) => call[0].name === 'automation_config')?.[0] as Tool;
|
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',
|
action: 'update',
|
||||||
config: mockAutomationConfig
|
config: mockAutomationConfig
|
||||||
});
|
})) as TestResponse;
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.message).toBe('Automation ID and configuration are required for updating automation');
|
expect(result.message).toBe('Automation ID and configuration are required for updating automation');
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js';
|
import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js';
|
||||||
import type { MemoryUsage } from 'node:process';
|
|
||||||
|
|
||||||
describe('Performance Module', () => {
|
describe('Performance Module', () => {
|
||||||
describe('PerformanceMonitor', () => {
|
describe('PerformanceMonitor', () => {
|
||||||
@@ -165,20 +164,27 @@ describe('Performance Module', () => {
|
|||||||
global.gc = jest.fn();
|
global.gc = jest.fn();
|
||||||
|
|
||||||
const memoryUsage = process.memoryUsage;
|
const memoryUsage = process.memoryUsage;
|
||||||
process.memoryUsage = jest.fn().mockImplementation((): MemoryUsage => ({
|
const mockMemoryUsage = () => ({
|
||||||
heapUsed: 900,
|
heapUsed: 900,
|
||||||
heapTotal: 1000,
|
heapTotal: 1000,
|
||||||
rss: 2000,
|
rss: 2000,
|
||||||
external: 0,
|
external: 0,
|
||||||
arrayBuffers: 0
|
arrayBuffers: 0
|
||||||
}));
|
});
|
||||||
|
Object.defineProperty(process, 'memoryUsage', {
|
||||||
|
value: mockMemoryUsage,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
|
||||||
await PerformanceOptimizer.optimizeMemory();
|
await PerformanceOptimizer.optimizeMemory();
|
||||||
|
|
||||||
expect(global.gc).toHaveBeenCalled();
|
expect(global.gc).toHaveBeenCalled();
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
process.memoryUsage = memoryUsage;
|
Object.defineProperty(process, 'memoryUsage', {
|
||||||
|
value: memoryUsage,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
if (originalGc) {
|
if (originalGc) {
|
||||||
global.gc = originalGc;
|
global.gc = originalGc;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,30 +23,30 @@
|
|||||||
<div class='clearfix'>
|
<div class='clearfix'>
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
<div class='fl pad1y space-right2'>
|
||||||
<span class="strong">45.71% </span>
|
<span class="strong">37.98% </span>
|
||||||
<span class="quiet">Statements</span>
|
<span class="quiet">Statements</span>
|
||||||
<span class='fraction'>128/280</span>
|
<span class='fraction'>128/337</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
<div class='fl pad1y space-right2'>
|
||||||
<span class="strong">40.19% </span>
|
<span class="strong">33.06% </span>
|
||||||
<span class="quiet">Branches</span>
|
<span class="quiet">Branches</span>
|
||||||
<span class='fraction'>41/102</span>
|
<span class='fraction'>41/124</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
<div class='fl pad1y space-right2'>
|
||||||
<span class="strong">40.74% </span>
|
<span class="strong">37.5% </span>
|
||||||
<span class="quiet">Functions</span>
|
<span class="quiet">Functions</span>
|
||||||
<span class='fraction'>33/81</span>
|
<span class='fraction'>33/88</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class='fl pad1y space-right2'>
|
<div class='fl pad1y space-right2'>
|
||||||
<span class="strong">46.29% </span>
|
<span class="strong">38.34% </span>
|
||||||
<span class="quiet">Lines</span>
|
<span class="quiet">Lines</span>
|
||||||
<span class='fraction'>125/270</span>
|
<span class='fraction'>125/326</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -153,6 +153,21 @@
|
|||||||
<td data-value="60" class="abs low">0/60</td>
|
<td data-value="60" class="abs low">0/60</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file low" data-value="src/security"><a href="src/security/index.html">src/security</a></td>
|
||||||
|
<td data-value="0" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="57" class="abs low">0/57</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="22" class="abs low">0/22</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="7" class="abs low">0/7</td>
|
||||||
|
<td data-value="0" class="pct low">0%</td>
|
||||||
|
<td data-value="56" class="abs low">0/56</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="file low" data-value="src/websocket"><a href="src/websocket/index.html">src/websocket</a></td>
|
<td class="file low" data-value="src/websocket"><a href="src/websocket/index.html">src/websocket</a></td>
|
||||||
<td data-value="0" class="pic low">
|
<td data-value="0" class="pic low">
|
||||||
@@ -176,7 +191,7 @@
|
|||||||
<div class='footer quiet pad2 space-top1 center small'>
|
<div class='footer quiet pad2 space-top1 center small'>
|
||||||
Code coverage generated by
|
Code coverage generated by
|
||||||
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
||||||
at 2025-01-30T08:26:17.384Z
|
at 2025-01-30T08:31:24.499Z
|
||||||
</div>
|
</div>
|
||||||
<script src="prettify.js"></script>
|
<script src="prettify.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -435,6 +435,107 @@ BRF:22
|
|||||||
BRH:0
|
BRH:0
|
||||||
end_of_record
|
end_of_record
|
||||||
TN:
|
TN:
|
||||||
|
SF:src/security/index.ts
|
||||||
|
FN:57,(anonymous_0)
|
||||||
|
FN:67,(anonymous_1)
|
||||||
|
FN:84,(anonymous_2)
|
||||||
|
FN:103,(anonymous_3)
|
||||||
|
FN:131,validateRequest
|
||||||
|
FN:158,sanitizeInput
|
||||||
|
FN:169,errorHandler
|
||||||
|
FNF:7
|
||||||
|
FNH:0
|
||||||
|
FNDA:0,(anonymous_0)
|
||||||
|
FNDA:0,(anonymous_1)
|
||||||
|
FNDA:0,(anonymous_2)
|
||||||
|
FNDA:0,(anonymous_3)
|
||||||
|
FNDA:0,validateRequest
|
||||||
|
FNDA:0,sanitizeInput
|
||||||
|
FNDA:0,errorHandler
|
||||||
|
DA:7,0
|
||||||
|
DA:8,0
|
||||||
|
DA:9,0
|
||||||
|
DA:12,0
|
||||||
|
DA:19,0
|
||||||
|
DA:45,0
|
||||||
|
DA:49,0
|
||||||
|
DA:50,0
|
||||||
|
DA:51,0
|
||||||
|
DA:52,0
|
||||||
|
DA:53,0
|
||||||
|
DA:54,0
|
||||||
|
DA:55,0
|
||||||
|
DA:58,0
|
||||||
|
DA:68,0
|
||||||
|
DA:69,0
|
||||||
|
DA:70,0
|
||||||
|
DA:71,0
|
||||||
|
DA:75,0
|
||||||
|
DA:79,0
|
||||||
|
DA:81,0
|
||||||
|
DA:85,0
|
||||||
|
DA:86,0
|
||||||
|
DA:87,0
|
||||||
|
DA:88,0
|
||||||
|
DA:92,0
|
||||||
|
DA:93,0
|
||||||
|
DA:95,0
|
||||||
|
DA:98,0
|
||||||
|
DA:100,0
|
||||||
|
DA:104,0
|
||||||
|
DA:106,0
|
||||||
|
DA:108,0
|
||||||
|
DA:109,0
|
||||||
|
DA:113,0
|
||||||
|
DA:114,0
|
||||||
|
DA:115,0
|
||||||
|
DA:118,0
|
||||||
|
DA:119,0
|
||||||
|
DA:123,0
|
||||||
|
DA:125,0
|
||||||
|
DA:133,0
|
||||||
|
DA:134,0
|
||||||
|
DA:140,0
|
||||||
|
DA:141,0
|
||||||
|
DA:142,0
|
||||||
|
DA:148,0
|
||||||
|
DA:149,0
|
||||||
|
DA:154,0
|
||||||
|
DA:159,0
|
||||||
|
DA:160,0
|
||||||
|
DA:163,0
|
||||||
|
DA:165,0
|
||||||
|
DA:170,0
|
||||||
|
DA:171,0
|
||||||
|
DA:178,0
|
||||||
|
LF:56
|
||||||
|
LH:0
|
||||||
|
BRDA:26,0,0,0
|
||||||
|
BRDA:26,0,1,0
|
||||||
|
BRDA:104,1,0,0
|
||||||
|
BRDA:108,2,0,0
|
||||||
|
BRDA:118,3,0,0
|
||||||
|
BRDA:118,4,0,0
|
||||||
|
BRDA:118,4,1,0
|
||||||
|
BRDA:133,5,0,0
|
||||||
|
BRDA:133,6,0,0
|
||||||
|
BRDA:133,6,1,0
|
||||||
|
BRDA:141,7,0,0
|
||||||
|
BRDA:141,8,0,0
|
||||||
|
BRDA:141,8,1,0
|
||||||
|
BRDA:148,9,0,0
|
||||||
|
BRDA:148,10,0,0
|
||||||
|
BRDA:148,10,1,0
|
||||||
|
BRDA:148,10,2,0
|
||||||
|
BRDA:159,11,0,0
|
||||||
|
BRDA:159,12,0,0
|
||||||
|
BRDA:159,12,1,0
|
||||||
|
BRDA:173,13,0,0
|
||||||
|
BRDA:173,13,1,0
|
||||||
|
BRF:22
|
||||||
|
BRH:0
|
||||||
|
end_of_record
|
||||||
|
TN:
|
||||||
SF:src/websocket/client.ts
|
SF:src/websocket/client.ts
|
||||||
FN:13,(anonymous_0)
|
FN:13,(anonymous_0)
|
||||||
FN:27,(anonymous_1)
|
FN:27,(anonymous_1)
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
"@digital-alchemy/hass": "^24.11.4",
|
"@digital-alchemy/hass": "^24.11.4",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"express-rate-limit": "^7.5.0",
|
||||||
|
"helmet": "^8.0.0",
|
||||||
"litemcp": "^0.7.0",
|
"litemcp": "^0.7.0",
|
||||||
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ export const rateLimiter = rateLimit({
|
|||||||
message: 'Too many requests from this IP, please try again later'
|
message: 'Too many requests from this IP, please try again later'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Security headers middleware
|
// Security configuration
|
||||||
export const securityHeaders = helmet({
|
const helmetConfig = {
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
directives: {
|
||||||
defaultSrc: ["'self'"],
|
defaultSrc: ["'self'"],
|
||||||
@@ -27,9 +27,6 @@ export const securityHeaders = helmet({
|
|||||||
upgradeInsecureRequests: true
|
upgradeInsecureRequests: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
crossOriginEmbedderPolicy: true,
|
|
||||||
crossOriginOpenerPolicy: true,
|
|
||||||
crossOriginResourcePolicy: { policy: 'same-site' },
|
|
||||||
dnsPrefetchControl: true,
|
dnsPrefetchControl: true,
|
||||||
frameguard: {
|
frameguard: {
|
||||||
action: 'deny'
|
action: 'deny'
|
||||||
@@ -40,11 +37,12 @@ export const securityHeaders = helmet({
|
|||||||
includeSubDomains: true,
|
includeSubDomains: true,
|
||||||
preload: true
|
preload: true
|
||||||
},
|
},
|
||||||
ieNoOpen: true,
|
|
||||||
noSniff: true,
|
noSniff: true,
|
||||||
referrerPolicy: { policy: 'no-referrer' },
|
referrerPolicy: { policy: 'no-referrer' }
|
||||||
xssFilter: true
|
};
|
||||||
});
|
|
||||||
|
// Security headers middleware
|
||||||
|
export const securityHeaders = helmet(helmetConfig);
|
||||||
|
|
||||||
// Token validation and encryption
|
// Token validation and encryption
|
||||||
export class TokenManager {
|
export class TokenManager {
|
||||||
@@ -178,7 +176,7 @@ export function errorHandler(err: Error, req: Request, res: Response, next: Next
|
|||||||
|
|
||||||
// Export security middleware chain
|
// Export security middleware chain
|
||||||
export const securityMiddleware = [
|
export const securityMiddleware = [
|
||||||
helmet(),
|
helmet(helmetConfig),
|
||||||
rateLimit({
|
rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 15 * 60 * 1000,
|
||||||
max: 100
|
max: 100
|
||||||
|
|||||||
Reference in New Issue
Block a user