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:
11
.env.test.example
Normal file
11
.env.test.example
Normal 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
293
README.md
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
15
jest.setup.ts
Normal 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';
|
||||
Reference in New Issue
Block a user