Add test environment configuration and update Jest setup

- Created `.env.test.example` with test environment variables
- Updated `jest.config.cjs` to simplify configuration and improve test setup
- Added `jest.setup.ts` to load and configure test environment variables
- Updated test files to use dynamic test environment configuration
- Enhanced README.md with additional configuration details for testing
This commit is contained in:
jango-blockchained
2025-01-30 09:56:19 +01:00
parent b3fa5f729e
commit 3115c41e87
5 changed files with 362 additions and 42 deletions

11
.env.test.example Normal file
View File

@@ -0,0 +1,11 @@
# Test Environment Configuration
# Home Assistant Test Instance
TEST_HASS_HOST=http://localhost:8123
TEST_HASS_TOKEN=test_token
TEST_HASS_SOCKET_URL=ws://localhost:8123/api/websocket
# Test Server Configuration
TEST_PORT=3001
NODE_ENV=test
DEBUG=false

293
README.md
View File

@@ -136,6 +136,11 @@ npm run build
# Server Configuration
PORT=3000
NODE_ENV=production
DEBUG=false
# Test Configuration
TEST_HASS_HOST=http://localhost:8123
TEST_HASS_TOKEN=test_token
```
3. **Launch with Docker Compose:**
@@ -143,6 +148,32 @@ npm run build
docker-compose up -d
```
## Configuration
### Environment Variables
```env
# Home Assistant Configuration
HASS_HOST=http://homeassistant.local:8123 # Your Home Assistant instance URL
HASS_TOKEN=your_home_assistant_token # Long-lived access token
HASS_SOCKET_URL=ws://homeassistant.local:8123/api/websocket # WebSocket URL
# Server Configuration
PORT=3000 # Server port (default: 3000)
NODE_ENV=production # Environment (production/development)
DEBUG=false # Enable debug mode
# Test Configuration
TEST_HASS_HOST=http://localhost:8123 # Test instance URL
TEST_HASS_TOKEN=test_token # Test token
```
### Configuration Files
1. **Development**: Copy `.env.example` to `.env.development`
2. **Production**: Copy `.env.example` to `.env.production`
3. **Testing**: Copy `.env.example` to `.env.test`
## API Reference
### Device Control
@@ -258,6 +289,268 @@ npm run build
}
```
### Core Functions
#### State Management
```http
GET /api/state
POST /api/state
```
Manages the current state of the system.
**Example Request:**
```json
POST /api/state
{
"context": "living_room",
"state": {
"lights": "on",
"temperature": 22
}
}
```
#### Context Updates
```http
POST /api/context
```
Updates the current context with new information.
**Example Request:**
```json
POST /api/context
{
"user": "john",
"location": "kitchen",
"time": "morning",
"activity": "cooking"
}
```
### Action Endpoints
#### Execute Action
```http
POST /api/action
```
Executes a specified action with given parameters.
**Example Request:**
```json
POST /api/action
{
"action": "turn_on_lights",
"parameters": {
"room": "living_room",
"brightness": 80
}
}
```
#### Batch Actions
```http
POST /api/actions/batch
```
Executes multiple actions in sequence.
**Example Request:**
```json
POST /api/actions/batch
{
"actions": [
{
"action": "turn_on_lights",
"parameters": {
"room": "living_room"
}
},
{
"action": "set_temperature",
"parameters": {
"temperature": 22
}
}
]
}
```
### Query Functions
#### Get Available Actions
```http
GET /api/actions
```
Returns a list of all available actions.
**Example Response:**
```json
{
"actions": [
{
"name": "turn_on_lights",
"parameters": ["room", "brightness"],
"description": "Turns on lights in specified room"
},
{
"name": "set_temperature",
"parameters": ["temperature"],
"description": "Sets temperature in current context"
}
]
}
```
#### Context Query
```http
GET /api/context?type=current
```
Retrieves context information.
**Example Response:**
```json
{
"current_context": {
"user": "john",
"location": "kitchen",
"time": "morning",
"activity": "cooking"
}
}
```
### WebSocket Events
The server supports real-time updates via WebSocket connections.
```javascript
// Client-side connection example
const ws = new WebSocket('ws://localhost:3000/ws');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received update:', data);
};
```
#### Supported Events
- `state_change`: Emitted when system state changes
- `context_update`: Emitted when context is updated
- `action_executed`: Emitted when an action is completed
- `error`: Emitted when an error occurs
**Example Event Data:**
```json
{
"event": "state_change",
"data": {
"previous_state": {
"lights": "off"
},
"current_state": {
"lights": "on"
},
"timestamp": "2024-03-20T10:30:00Z"
}
}
```
### Error Handling
All endpoints return standard HTTP status codes:
- 200: Success
- 400: Bad Request
- 401: Unauthorized
- 403: Forbidden
- 404: Not Found
- 500: Internal Server Error
**Error Response Format:**
```json
{
"error": {
"code": "INVALID_PARAMETERS",
"message": "Missing required parameter: room",
"details": {
"missing_fields": ["room"]
}
}
}
```
### Rate Limiting
The API implements rate limiting to prevent abuse:
- 100 requests per minute per IP for regular endpoints
- 1000 requests per minute per IP for WebSocket connections
When rate limit is exceeded, the server returns:
```json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"reset_time": "2024-03-20T10:31:00Z"
}
}
```
### Example Usage
#### Using curl
```bash
# Get current state
curl -X GET \
http://localhost:3000/api/state \
-H 'Authorization: ApiKey your_api_key_here'
# Execute action
curl -X POST \
http://localhost:3000/api/action \
-H 'Authorization: ApiKey your_api_key_here' \
-H 'Content-Type: application/json' \
-d '{
"action": "turn_on_lights",
"parameters": {
"room": "living_room",
"brightness": 80
}
}'
```
#### Using JavaScript
```javascript
// Execute action
async function executeAction() {
const response = await fetch('http://localhost:3000/api/action', {
method: 'POST',
headers: {
'Authorization': 'ApiKey your_api_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'turn_on_lights',
parameters: {
room: 'living_room',
brightness: 80
}
})
});
const data = await response.json();
console.log('Action result:', data);
}
```
## Development
```bash

View File

@@ -4,9 +4,15 @@ 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';
// Load test environment variables with defaults
const TEST_HASS_HOST = process.env.TEST_HASS_HOST || 'http://localhost:8123';
const TEST_HASS_TOKEN = process.env.TEST_HASS_TOKEN || 'test_token';
const TEST_HASS_SOCKET_URL = process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket';
// Set environment variables for testing
process.env.HASS_HOST = TEST_HASS_HOST;
process.env.HASS_TOKEN = TEST_HASS_TOKEN;
process.env.HASS_SOCKET_URL = TEST_HASS_SOCKET_URL;
// Mock fetch
const mockFetchResponse = {
@@ -230,11 +236,11 @@ describe('Home Assistant MCP Server', () => {
// Verify the fetch call
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/light/turn_on',
`${TEST_HASS_HOST}/api/services/light/turn_on`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -312,11 +318,11 @@ describe('Home Assistant MCP Server', () => {
// Verify the fetch call
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/climate/set_temperature',
`${TEST_HASS_HOST}/api/services/climate/set_temperature`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -354,11 +360,11 @@ describe('Home Assistant MCP Server', () => {
// Verify the fetch call
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/cover/set_position',
`${TEST_HASS_HOST}/api/services/cover/set_position`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -415,7 +421,7 @@ describe('Home Assistant MCP Server', () => {
expect.stringContaining('/api/history/period/2024-01-01T00:00:00Z?'),
expect.objectContaining({
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
}
})
@@ -513,11 +519,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.message).toBe('Successfully activated scene scene.movie_time');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/scene/turn_on',
`${TEST_HASS_HOST}/api/services/scene/turn_on`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -550,11 +556,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.message).toBe('Notification sent successfully');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/notify/mobile_app_phone',
`${TEST_HASS_HOST}/api/services/notify/mobile_app_phone`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -581,7 +587,7 @@ describe('Home Assistant MCP Server', () => {
});
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/notify/notify',
`${TEST_HASS_HOST}/api/services/notify/notify`,
expect.any(Object)
);
});
@@ -657,11 +663,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.message).toBe('Successfully toggled automation automation.morning_routine');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/automation/toggle',
`${TEST_HASS_HOST}/api/services/automation/toggle`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -690,11 +696,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.message).toBe('Successfully triggered automation automation.morning_routine');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/services/automation/trigger',
`${TEST_HASS_HOST}/api/services/automation/trigger`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -782,11 +788,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.message).toBe('Successfully installed add-on core_configurator');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/hassio/addons/core_configurator/install',
`${TEST_HASS_HOST}/api/hassio/addons/core_configurator/install`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ version: '5.6.0' })
@@ -851,11 +857,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.message).toBe('Successfully installed package hacs/integration');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/hacs/repository/install',
`${TEST_HASS_HOST}/api/hacs/repository/install`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
@@ -910,11 +916,11 @@ describe('Home Assistant MCP Server', () => {
expect(result.automation_id).toBe('new_automation_1');
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/config/automation/config',
`${TEST_HASS_HOST}/api/config/automation/config`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(mockAutomationConfig)
@@ -950,17 +956,17 @@ describe('Home Assistant MCP Server', () => {
// Verify both API calls
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/config/automation/config/automation.test',
`${TEST_HASS_HOST}/api/config/automation/config/automation.test`,
expect.any(Object)
);
const duplicateConfig = { ...mockAutomationConfig, alias: 'Test Automation (Copy)' };
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:8123/api/config/automation/config',
`${TEST_HASS_HOST}/api/config/automation/config`,
{
method: 'POST',
headers: {
Authorization: 'Bearer test_token',
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(duplicateConfig)

View File

@@ -1,9 +1,8 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest/presets/default-esm',
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
setupFiles: ['./jest.setup.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
'#(.*)': '<rootDir>/node_modules/$1',
@@ -13,20 +12,15 @@ module.exports = {
'#supports-color': '<rootDir>/node_modules/supports-color/index.js'
},
transform: {
'^.+\\.(ts|tsx|js|jsx)$': [
'ts-jest',
{
useESM: true,
tsconfig: 'tsconfig.json'
},
],
'^.+\\.tsx?$': ['ts-jest', {
useESM: true,
}],
},
transformIgnorePatterns: [
'node_modules/(?!(@digital-alchemy|chalk|#ansi-styles|#supports-color)/)'
],
resolver: '<rootDir>/jest-resolver.cjs',
testMatch: ['**/__tests__/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
globals: {
'ts-jest': {
useESM: true,
@@ -36,9 +30,9 @@ module.exports = {
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'clover', 'html'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.test.ts',
'!src/types/**/*',
'!src/polyfills.ts'
],
@@ -49,5 +43,6 @@ module.exports = {
lines: 80,
statements: 80
}
}
},
verbose: true
};

15
jest.setup.ts Normal file
View File

@@ -0,0 +1,15 @@
import { config } from 'dotenv';
import { resolve } from 'path';
// Load test environment variables
const envFile = process.env.NODE_ENV === 'test' ? '.env.test' : '.env';
config({ path: resolve(__dirname, envFile) });
// Set default test environment variables if not provided
process.env.TEST_HASS_HOST = process.env.TEST_HASS_HOST || 'http://localhost:8123';
process.env.TEST_HASS_TOKEN = process.env.TEST_HASS_TOKEN || 'test_token';
process.env.TEST_HASS_SOCKET_URL = process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket';
process.env.TEST_PORT = process.env.TEST_PORT || '3001';
// Ensure test environment
process.env.NODE_ENV = 'test';