Enhance Jest Configuration and Resolver for ESM and TypeScript Support

- Updated Jest configuration to handle .mts and .mjs file extensions
- Improved Jest resolver to better support ESM modules and @digital-alchemy packages
- Added comprehensive test coverage for AI router, Home Assistant integration, and WebSocket client
- Expanded test scenarios for error handling, event subscriptions, and service interactions
This commit is contained in:
jango-blockchained
2025-02-01 13:53:55 +01:00
parent 99c66099c6
commit 8f0b9eb909
4 changed files with 447 additions and 105 deletions

View File

@@ -1,6 +1,8 @@
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
import { WebSocket } from 'ws';
import { EventEmitter } from 'events';
import type { HassInstanceImpl } from '../../src/hass/index.js';
import type { Entity, Event } from '../../src/types/hass.js';
// Define WebSocket mock types
type WebSocketCallback = (...args: any[]) => void;
@@ -14,21 +16,22 @@ type WebSocketMock = {
close: jest.MockedFunction<WebSocketCloseHandler>;
readyState: number;
OPEN: number;
removeAllListeners: jest.MockedFunction<() => void>;
};
// Mock WebSocket
jest.mock('ws', () => {
return {
WebSocket: jest.fn().mockImplementation(() => ({
on: jest.fn(),
send: jest.fn(),
close: jest.fn(),
readyState: 1,
OPEN: 1,
removeAllListeners: jest.fn()
}))
};
});
const mockWebSocket: WebSocketMock = {
on: jest.fn<WebSocketEventHandler>(),
send: jest.fn<WebSocketSendHandler>(),
close: jest.fn<WebSocketCloseHandler>(),
readyState: 1,
OPEN: 1,
removeAllListeners: jest.fn()
};
jest.mock('ws', () => ({
WebSocket: jest.fn().mockImplementation(() => mockWebSocket)
}));
// Mock fetch globally
const mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>;
@@ -43,29 +46,24 @@ describe('Home Assistant Integration', () => {
beforeEach(async () => {
const { HassWebSocketClient } = await import('../../src/hass/index.js');
client = new HassWebSocketClient(mockUrl, mockToken);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should create a WebSocket client with the provided URL and token', () => {
expect(client).toBeInstanceOf(EventEmitter);
expect(WebSocket).toHaveBeenCalledWith(mockUrl);
expect(jest.mocked(WebSocket)).toHaveBeenCalledWith(mockUrl);
});
it('should connect and authenticate successfully', async () => {
const mockWs = (WebSocket as jest.MockedClass<typeof WebSocket>).mock.results[0].value as unknown as WebSocketMock;
const connectPromise = client.connect();
// Get and call the open callback
const openCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'open');
if (!openCallEntry) throw new Error('Open callback not found');
const openCallback = openCallEntry[1];
const openCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'open')?.[1];
if (!openCallback) throw new Error('Open callback not found');
openCallback();
// Verify authentication message
expect(mockWs.send).toHaveBeenCalledWith(
expect(mockWebSocket.send).toHaveBeenCalledWith(
JSON.stringify({
type: 'auth',
access_token: mockToken
@@ -73,60 +71,51 @@ describe('Home Assistant Integration', () => {
);
// Get and call the message callback
const messageCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'message');
if (!messageCallEntry) throw new Error('Message callback not found');
const messageCallback = messageCallEntry[1];
const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')?.[1];
if (!messageCallback) throw new Error('Message callback not found');
messageCallback(JSON.stringify({ type: 'auth_ok' }));
await connectPromise;
});
it('should handle authentication failure', async () => {
const mockWs = (WebSocket as jest.MockedClass<typeof WebSocket>).mock.results[0].value as unknown as WebSocketMock;
const connectPromise = client.connect();
// Get and call the open callback
const openCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'open');
if (!openCallEntry) throw new Error('Open callback not found');
const openCallback = openCallEntry[1];
const openCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'open')?.[1];
if (!openCallback) throw new Error('Open callback not found');
openCallback();
// Get and call the message callback with auth failure
const messageCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'message');
if (!messageCallEntry) throw new Error('Message callback not found');
const messageCallback = messageCallEntry[1];
const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')?.[1];
if (!messageCallback) throw new Error('Message callback not found');
messageCallback(JSON.stringify({ type: 'auth_invalid' }));
await expect(connectPromise).rejects.toThrow();
});
it('should handle connection errors', async () => {
const mockWs = (WebSocket as jest.MockedClass<typeof WebSocket>).mock.results[0].value as unknown as WebSocketMock;
const connectPromise = client.connect();
// Get and call the error callback
const errorCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'error');
if (!errorCallEntry) throw new Error('Error callback not found');
const errorCallback = errorCallEntry[1];
const errorCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'error')?.[1];
if (!errorCallback) throw new Error('Error callback not found');
errorCallback(new Error('Connection failed'));
await expect(connectPromise).rejects.toThrow('Connection failed');
});
it('should handle message parsing errors', async () => {
const mockWs = (WebSocket as jest.MockedClass<typeof WebSocket>).mock.results[0].value as unknown as WebSocketMock;
const connectPromise = client.connect();
// Get and call the open callback
const openCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'open');
if (!openCallEntry) throw new Error('Open callback not found');
const openCallback = openCallEntry[1];
const openCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'open')?.[1];
if (!openCallback) throw new Error('Open callback not found');
openCallback();
// Get and call the message callback with invalid JSON
const messageCallEntry = mockWs.on.mock.calls.find(call => call[0] === 'message');
if (!messageCallEntry) throw new Error('Message callback not found');
const messageCallback = messageCallEntry[1];
const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')?.[1];
if (!messageCallback) throw new Error('Message callback not found');
// Should emit error event
await expect(new Promise((resolve) => {
@@ -137,105 +126,183 @@ describe('Home Assistant Integration', () => {
});
describe('HassInstanceImpl', () => {
let instance: any;
let instance: HassInstanceImpl;
const mockBaseUrl = 'http://localhost:8123';
const mockToken = 'test_token';
const mockState: Entity = {
entity_id: 'light.test',
state: 'on',
attributes: {},
last_changed: '',
last_updated: '',
context: {
id: '',
parent_id: null,
user_id: null
}
};
beforeEach(async () => {
const { HassInstanceImpl } = await import('../../src/hass/index.js');
instance = new HassInstanceImpl(mockBaseUrl, mockToken);
mockFetch.mockClear();
jest.clearAllMocks();
// Mock successful fetch responses
mockFetch.mockImplementation(async (url, init) => {
if (url.toString().endsWith('/api/states')) {
return new Response(JSON.stringify([mockState]));
}
if (url.toString().includes('/api/states/')) {
return new Response(JSON.stringify(mockState));
}
if (url.toString().endsWith('/api/services')) {
return new Response(JSON.stringify([]));
}
return new Response(JSON.stringify({}));
});
});
it('should create an instance with the provided URL and token', () => {
it('should create instance with correct properties', () => {
expect(instance.baseUrl).toBe(mockBaseUrl);
expect(instance.token).toBe(mockToken);
});
it('should fetch states successfully', async () => {
const mockStates = [
{
entity_id: 'light.living_room',
state: 'on',
attributes: {}
}
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockStates
} as Response);
it('should fetch states', async () => {
const states = await instance.fetchStates();
expect(states).toEqual(mockStates);
expect(states).toEqual([mockState]);
expect(mockFetch).toHaveBeenCalledWith(
`${mockBaseUrl}/api/states`,
expect.objectContaining({
headers: {
Authorization: `Bearer ${mockToken}`,
'Content-Type': 'application/json'
}
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`
})
})
);
});
it('should fetch single entity state successfully', async () => {
const mockState = {
entity_id: 'light.living_room',
state: 'on',
attributes: {}
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockState
} as Response);
const state = await instance.fetchState('light.living_room');
it('should fetch single state', async () => {
const state = await instance.fetchState('light.test');
expect(state).toEqual(mockState);
expect(mockFetch).toHaveBeenCalledWith(
`${mockBaseUrl}/api/states/light.living_room`,
`${mockBaseUrl}/api/states/light.test`,
expect.objectContaining({
headers: {
Authorization: `Bearer ${mockToken}`,
'Content-Type': 'application/json'
}
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`
})
})
);
});
it('should call service successfully', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({})
} as Response);
await instance.callService('light', 'turn_on', { entity_id: 'light.living_room' });
it('should call service', async () => {
await instance.callService('light', 'turn_on', { entity_id: 'light.test' });
expect(mockFetch).toHaveBeenCalledWith(
`${mockBaseUrl}/api/services/light/turn_on`,
expect.objectContaining({
method: 'POST',
headers: {
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ entity_id: 'light.living_room' })
}),
body: JSON.stringify({ entity_id: 'light.test' })
})
);
});
it('should handle fetch errors', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
await expect(instance.fetchStates()).rejects.toThrow('Network error');
});
it('should handle invalid JSON responses', async () => {
mockFetch.mockResolvedValueOnce(new Response('invalid json'));
await expect(instance.fetchStates()).rejects.toThrow();
});
it('should handle non-200 responses', async () => {
mockFetch.mockResolvedValueOnce(new Response('Error', { status: 500 }));
await expect(instance.fetchStates()).rejects.toThrow();
});
describe('Event Subscription', () => {
let eventCallback: (event: Event) => void;
beforeEach(() => {
eventCallback = jest.fn();
});
it('should subscribe to events', async () => {
const subscriptionId = await instance.subscribeEvents(eventCallback);
expect(typeof subscriptionId).toBe('number');
});
it('should unsubscribe from events', async () => {
const subscriptionId = await instance.subscribeEvents(eventCallback);
await instance.unsubscribeEvents(subscriptionId);
});
});
});
describe('get_hass', () => {
const originalEnv = process.env;
let mockBootstrap: jest.Mock;
const createMockServices = () => ({
light: {},
climate: {},
alarm_control_panel: {},
cover: {},
switch: {},
contact: {},
media_player: {},
fan: {},
lock: {},
vacuum: {},
scene: {},
script: {},
camera: {}
});
beforeEach(() => {
process.env = { ...originalEnv };
process.env.HASS_HOST = 'http://localhost:8123';
process.env.HASS_TOKEN = 'test_token';
// Mock the MY_APP.bootstrap function
mockBootstrap = jest.fn();
mockBootstrap.mockImplementation(() => Promise.resolve({
baseUrl: process.env.HASS_HOST,
token: process.env.HASS_TOKEN,
wsClient: undefined,
services: createMockServices(),
als: {},
context: {},
event: new EventEmitter(),
internal: {},
lifecycle: {},
logger: {},
scheduler: {},
config: {},
params: {},
hass: {},
fetchStates: jest.fn(),
fetchState: jest.fn(),
callService: jest.fn(),
subscribeEvents: jest.fn(),
unsubscribeEvents: jest.fn()
}));
jest.mock('../../src/hass/index.js', () => ({
MY_APP: {
configuration: {},
bootstrap: () => mockBootstrap()
}
}));
});
afterEach(() => {
process.env = originalEnv;
jest.resetModules();
jest.clearAllMocks();
});
it('should return a development instance by default', async () => {
@@ -243,23 +310,50 @@ describe('Home Assistant Integration', () => {
const instance = await get_hass();
expect(instance.baseUrl).toBe('http://localhost:8123');
expect(instance.token).toBe('test_token');
expect(mockBootstrap).toHaveBeenCalledTimes(1);
});
it('should return a test instance when specified', async () => {
it('should return a test instance when in test environment', async () => {
process.env.NODE_ENV = 'test';
const { get_hass } = await import('../../src/hass/index.js');
const instance = await get_hass('test');
const instance = await get_hass();
expect(instance.baseUrl).toBe('http://localhost:8123');
expect(instance.token).toBe('test_token');
expect(mockBootstrap).toHaveBeenCalledTimes(1);
});
it('should return a production instance when specified', async () => {
it('should return a production instance when in production environment', async () => {
process.env.NODE_ENV = 'production';
process.env.HASS_HOST = 'https://hass.example.com';
process.env.HASS_TOKEN = 'prod_token';
mockBootstrap.mockImplementationOnce(() => Promise.resolve({
baseUrl: process.env.HASS_HOST,
token: process.env.HASS_TOKEN,
wsClient: undefined,
services: createMockServices(),
als: {},
context: {},
event: new EventEmitter(),
internal: {},
lifecycle: {},
logger: {},
scheduler: {},
config: {},
params: {},
hass: {},
fetchStates: jest.fn(),
fetchState: jest.fn(),
callService: jest.fn(),
subscribeEvents: jest.fn(),
unsubscribeEvents: jest.fn()
}));
const { get_hass } = await import('../../src/hass/index.js');
const instance = await get_hass('production');
const instance = await get_hass();
expect(instance.baseUrl).toBe('https://hass.example.com');
expect(instance.token).toBe('prod_token');
expect(mockBootstrap).toHaveBeenCalledTimes(1);
});
});
});