Refactor Home Assistant API and schema validation

- Completely rewrote HassInstance class with fetch-based API methods
- Updated Home Assistant schemas to be more precise and flexible
- Removed deprecated test environment configuration file
- Enhanced WebSocket client implementation
- Improved test coverage for Home Assistant API and schema validation
- Simplified type definitions and error handling
This commit is contained in:
jango-blockchained
2025-01-30 10:51:25 +01:00
parent 732a727d27
commit 8152313f52
8 changed files with 561 additions and 200 deletions

View File

@@ -1,157 +1,134 @@
import { get_hass } from '../../src/hass/index.js';
import { HassInstance } from '../../src/hass/index.js';
import * as HomeAssistant from '../../src/types/hass.js';
// Mock the entire hass module
jest.mock('../../src/hass/index.js', () => ({
get_hass: jest.fn()
}));
describe('Home Assistant API Integration', () => {
const MOCK_HASS_HOST = 'http://localhost:8123';
const MOCK_HASS_TOKEN = 'mock_token_12345';
const mockHassInstance = {
getStates: jest.fn(),
getState: jest.fn(),
callService: jest.fn(),
subscribeEvents: jest.fn()
};
describe('Home Assistant API', () => {
let hass: HassInstance;
beforeEach(() => {
process.env.HASS_HOST = MOCK_HASS_HOST;
process.env.HASS_TOKEN = MOCK_HASS_TOKEN;
jest.clearAllMocks();
(get_hass as jest.Mock).mockResolvedValue(mockHassInstance);
});
describe('API Connection', () => {
it('should initialize connection with valid credentials', async () => {
const hass = await get_hass();
expect(hass).toBeDefined();
expect(hass).toBe(mockHassInstance);
});
it('should handle connection errors', async () => {
(get_hass as jest.Mock).mockRejectedValueOnce(new Error('Connection failed'));
await expect(get_hass()).rejects.toThrow('Connection failed');
});
it('should handle invalid credentials', async () => {
(get_hass as jest.Mock).mockRejectedValueOnce(new Error('Unauthorized'));
await expect(get_hass()).rejects.toThrow('Unauthorized');
});
it('should handle missing environment variables', async () => {
delete process.env.HASS_HOST;
delete process.env.HASS_TOKEN;
(get_hass as jest.Mock).mockRejectedValueOnce(new Error('Missing required environment variables'));
await expect(get_hass()).rejects.toThrow('Missing required environment variables');
});
hass = new HassInstance('http://localhost:8123', 'test_token');
});
describe('State Management', () => {
const mockStates = [
{
entity_id: 'light.living_room',
state: 'on',
attributes: {
brightness: 255,
friendly_name: 'Living Room Light'
it('should fetch all states', async () => {
const mockStates: HomeAssistant.Entity[] = [
{
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 }
}
},
{
entity_id: 'switch.kitchen',
state: 'off',
attributes: {
friendly_name: 'Kitchen Switch'
}
}
];
];
it('should fetch states successfully', async () => {
mockHassInstance.getStates.mockResolvedValueOnce(mockStates);
const hass = await get_hass();
const states = await hass.getStates();
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockStates)
});
const states = await hass.fetchStates();
expect(states).toEqual(mockStates);
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8123/api/states',
expect.any(Object)
);
});
it('should get single entity state', async () => {
const mockState = mockStates[0];
mockHassInstance.getState.mockResolvedValueOnce(mockState);
const hass = await get_hass();
const state = await hass.getState('light.living_room');
it('should fetch single state', async () => {
const mockState: HomeAssistant.Entity = {
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 }
};
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockState)
});
const state = await hass.fetchState('light.living_room');
expect(state).toEqual(mockState);
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8123/api/states/light.living_room',
expect.any(Object)
);
});
it('should handle state fetch errors', async () => {
mockHassInstance.getStates.mockRejectedValueOnce(new Error('Failed to fetch states'));
const hass = await get_hass();
await expect(hass.getStates()).rejects.toThrow('Failed to fetch states');
global.fetch = jest.fn().mockRejectedValueOnce(new Error('Failed to fetch states'));
await expect(hass.fetchStates()).rejects.toThrow('Failed to fetch states');
});
});
describe('Service Calls', () => {
it('should call services successfully', async () => {
mockHassInstance.callService.mockResolvedValueOnce(undefined);
const hass = await get_hass();
it('should call service', async () => {
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({})
});
await hass.callService('light', 'turn_on', {
entity_id: 'light.living_room',
brightness: 255
});
expect(mockHassInstance.callService).toHaveBeenCalledWith(
'light',
'turn_on',
{
entity_id: 'light.living_room',
brightness: 255
}
expect(fetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/light/turn_on',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
entity_id: 'light.living_room',
brightness: 255
})
})
);
});
it('should handle service call errors', async () => {
mockHassInstance.callService.mockRejectedValueOnce(new Error('Bad Request'));
const hass = await get_hass();
global.fetch = jest.fn().mockRejectedValueOnce(new Error('Service call failed'));
await expect(
hass.callService('invalid_domain', 'invalid_service', {})
).rejects.toThrow('Bad Request');
).rejects.toThrow('Service call failed');
});
});
describe('Event Handling', () => {
describe('Event Subscription', () => {
it('should subscribe to events', async () => {
mockHassInstance.subscribeEvents.mockResolvedValueOnce(undefined);
const hass = await get_hass();
const callback = jest.fn();
const mockWs = {
send: jest.fn(),
close: jest.fn(),
addEventListener: jest.fn()
};
global.WebSocket = jest.fn().mockImplementation(() => mockWs);
await hass.subscribeEvents(callback, 'state_changed');
expect(mockHassInstance.subscribeEvents).toHaveBeenCalledWith(
callback,
'state_changed'
expect(WebSocket).toHaveBeenCalledWith(
'ws://localhost:8123/api/websocket'
);
});
it('should handle event subscription errors', async () => {
mockHassInstance.subscribeEvents.mockRejectedValueOnce(new Error('WebSocket error'));
const hass = await get_hass();
it('should handle subscription errors', async () => {
const callback = jest.fn();
global.WebSocket = jest.fn().mockImplementation(() => {
throw new Error('WebSocket connection failed');
});
await expect(
hass.subscribeEvents(callback, 'state_changed')
).rejects.toThrow('WebSocket error');
});
});
describe('Error Handling', () => {
it('should handle network errors gracefully', async () => {
(get_hass as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
await expect(get_hass()).rejects.toThrow('Network error');
});
it('should handle rate limiting', async () => {
(get_hass as jest.Mock).mockRejectedValueOnce(new Error('Too Many Requests'));
await expect(get_hass()).rejects.toThrow('Too Many Requests');
});
it('should handle server errors', async () => {
(get_hass as jest.Mock).mockRejectedValueOnce(new Error('Internal Server Error'));
await expect(get_hass()).rejects.toThrow('Internal Server Error');
).rejects.toThrow('WebSocket connection failed');
});
});
});