Files
spore-ui/test/mock-server.js

847 lines
25 KiB
JavaScript

#!/usr/bin/env node
/**
* Complete Mock Server for SPORE UI
*
* This mock server provides a complete simulation of the SPORE embedded system
* without requiring actual hardware or UDP port conflicts. It simulates:
* - Multiple SPORE nodes with different IPs
* - All API endpoints from the OpenAPI specification
* - Discovery system without UDP conflicts
* - Realistic data that changes over time
* - Different scenarios (healthy, degraded, error states)
*/
const express = require('express');
const cors = require('cors');
const path = require('path');
const { getMockConfig } = require('./mock-configs');
// Load mock configuration
const configName = process.env.MOCK_CONFIG || 'healthy';
const baseConfig = getMockConfig(configName);
// Mock server configuration
const MOCK_CONFIG = {
// Server settings
port: process.env.MOCK_PORT || 3002,
baseUrl: process.env.MOCK_BASE_URL || 'http://localhost:3002',
// Load configuration from preset
...baseConfig
};
// Initialize Express app
const app = express();
app.use(cors({
origin: true,
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Mock data generators
class MockDataGenerator {
constructor() {
this.startTime = Date.now();
this.nodeStates = new Map();
this.primaryNodeIndex = 0;
this.initializeNodeStates();
}
initializeNodeStates() {
MOCK_CONFIG.nodes.forEach((node, index) => {
this.nodeStates.set(node.ip, {
...node,
freeHeap: this.generateFreeHeap(),
uptime: 0,
lastSeen: Date.now(),
tasks: this.generateTasks(),
systemInfo: this.generateSystemInfo(node),
apiEndpoints: this.generateApiEndpoints()
});
});
}
generateFreeHeap() {
// Simulate realistic ESP8266 memory usage
const base = 30000;
const variation = 20000;
return Math.floor(base + Math.random() * variation);
}
generateSystemInfo(node) {
return {
freeHeap: this.generateFreeHeap(),
chipId: node.chipId,
sdkVersion: "3.1.2",
cpuFreqMHz: 80,
flashChipSize: 1048576
};
}
generateTasks() {
return [
{
name: "discovery_send",
interval: 1000,
enabled: true,
running: true,
autoStart: true
},
{
name: "heartbeat",
interval: 2000,
enabled: true,
running: true,
autoStart: true
},
{
name: "status_update",
interval: 1000,
enabled: true,
running: true,
autoStart: true
},
{
name: "wifi_monitor",
interval: 5000,
enabled: true,
running: Math.random() > 0.1, // 90% chance of running
autoStart: true
},
{
name: "ota_check",
interval: 30000,
enabled: true,
running: Math.random() > 0.2, // 80% chance of running
autoStart: true
},
{
name: "cluster_sync",
interval: 10000,
enabled: true,
running: Math.random() > 0.05, // 95% chance of running
autoStart: true
}
];
}
generateApiEndpoints() {
return [
{ uri: "/api/node/status", method: "GET" },
{ uri: "/api/tasks/status", method: "GET" },
{ uri: "/api/tasks/control", method: "POST" },
{ uri: "/api/cluster/members", method: "GET" },
{ uri: "/api/node/update", method: "POST" },
{ uri: "/api/node/restart", method: "POST" },
{
uri: "/api/led/brightness",
method: "POST",
params: [
{
name: "brightness",
type: "numberRange",
location: "body",
required: true,
value: 255,
default: 128
}
]
},
{
uri: "/api/led/color",
method: "POST",
params: [
{
name: "color",
type: "color",
location: "body",
required: true,
default: 16711680
}
]
},
{
uri: "/api/sensor/interval",
method: "POST",
params: [
{
name: "interval",
type: "numberRange",
location: "body",
required: true,
value: 10000,
default: 1000
}
]
},
{
uri: "/api/system/mode",
method: "POST",
params: [
{
name: "mode",
type: "string",
location: "body",
required: true,
values: ["normal", "debug", "maintenance"],
default: "normal"
}
]
}
];
}
updateNodeStates() {
if (!MOCK_CONFIG.simulation.enableTimeProgression) return;
this.nodeStates.forEach((nodeState, ip) => {
// Update uptime
nodeState.uptime = Date.now() - this.startTime;
// Update free heap (simulate memory usage changes)
const currentHeap = nodeState.freeHeap;
const change = Math.floor((Math.random() - 0.5) * 1000);
nodeState.freeHeap = Math.max(10000, currentHeap + change);
// Update last seen
nodeState.lastSeen = Date.now();
// Simulate random failures
if (MOCK_CONFIG.simulation.enableRandomFailures && Math.random() < MOCK_CONFIG.simulation.failureRate) {
nodeState.status = Math.random() > 0.5 ? 'INACTIVE' : 'DEAD';
} else {
nodeState.status = 'ACTIVE';
}
// Update task states
nodeState.tasks.forEach(task => {
if (task.enabled && Math.random() > 0.05) { // 95% chance of running when enabled
task.running = true;
} else {
task.running = false;
}
});
});
// Rotate primary node if enabled
if (MOCK_CONFIG.simulation.primaryNodeRotation) {
this.primaryNodeIndex = (this.primaryNodeIndex + 1) % MOCK_CONFIG.nodes.length;
}
}
getPrimaryNode() {
return MOCK_CONFIG.nodes[this.primaryNodeIndex];
}
getAllNodes() {
return Array.from(this.nodeStates.values());
}
getNodeByIp(ip) {
return this.nodeStates.get(ip);
}
}
// Initialize mock data generator
const mockData = new MockDataGenerator();
// Update data periodically
setInterval(() => {
mockData.updateNodeStates();
}, MOCK_CONFIG.simulation.updateInterval);
// API Routes
// Health check endpoint
app.get('/api/health', (req, res) => {
const primaryNode = mockData.getPrimaryNode();
const allNodes = mockData.getAllNodes();
const activeNodes = allNodes.filter(node => node.status === 'ACTIVE');
const health = {
status: activeNodes.length > 0 ? 'healthy' : 'degraded',
timestamp: new Date().toISOString(),
services: {
http: true,
udp: false, // Mock server doesn't use UDP
sporeClient: true
},
discovery: {
totalNodes: allNodes.length,
primaryNode: primaryNode.ip,
udpPort: 4210,
serverRunning: false // Mock server doesn't use UDP
},
mock: {
enabled: true,
nodes: allNodes.length,
activeNodes: activeNodes.length,
simulationMode: MOCK_CONFIG.simulation.enableTimeProgression
}
};
if (activeNodes.length === 0) {
health.status = 'degraded';
health.message = 'No active nodes in mock simulation';
}
res.json(health);
});
// Discovery endpoints (simulated)
app.get('/api/discovery/nodes', (req, res) => {
const primaryNode = mockData.getPrimaryNode();
const allNodes = mockData.getAllNodes();
const response = {
primaryNode: primaryNode.ip,
totalNodes: allNodes.length,
clientInitialized: true,
clientBaseUrl: `http://${primaryNode.ip}`,
nodes: allNodes.map(node => ({
ip: node.ip,
port: 80,
discoveredAt: new Date(node.lastSeen - 60000).toISOString(), // 1 minute ago
lastSeen: new Date(node.lastSeen).toISOString(),
isPrimary: node.ip === primaryNode.ip,
hostname: node.hostname,
status: node.status
}))
};
res.json(response);
});
app.post('/api/discovery/refresh', (req, res) => {
// Simulate discovery refresh
mockData.updateNodeStates();
res.json({
success: true,
message: 'Discovery refresh completed',
timestamp: new Date().toISOString()
});
});
app.post('/api/discovery/primary/:ip', (req, res) => {
const { ip } = req.params;
const node = mockData.getNodeByIp(ip);
if (!node) {
return res.status(404).json({
success: false,
message: `Node ${ip} not found`
});
}
// Find and set as primary
const nodeIndex = MOCK_CONFIG.nodes.findIndex(n => n.ip === ip);
if (nodeIndex !== -1) {
mockData.primaryNodeIndex = nodeIndex;
}
res.json({
success: true,
message: `Primary node set to ${ip}`,
primaryNode: ip
});
});
app.post('/api/discovery/random-primary', (req, res) => {
const allNodes = mockData.getAllNodes();
const activeNodes = allNodes.filter(node => node.status === 'ACTIVE');
if (activeNodes.length === 0) {
return res.status(503).json({
success: false,
message: 'No active nodes available for selection'
});
}
// Randomly select a new primary
const randomIndex = Math.floor(Math.random() * activeNodes.length);
const newPrimary = activeNodes[randomIndex];
const nodeIndex = MOCK_CONFIG.nodes.findIndex(n => n.ip === newPrimary.ip);
if (nodeIndex !== -1) {
mockData.primaryNodeIndex = nodeIndex;
}
res.json({
success: true,
message: `Primary node randomly selected: ${newPrimary.ip}`,
primaryNode: newPrimary.ip,
totalNodes: allNodes.length,
clientInitialized: true
});
});
// Task management endpoints
app.get('/api/tasks/status', (req, res) => {
const { ip } = req.query;
let nodeData;
if (ip) {
nodeData = mockData.getNodeByIp(ip);
if (!nodeData) {
return res.status(404).json({
error: 'Node not found',
message: `Node ${ip} not found in mock simulation`
});
}
} else {
// Use primary node
const primaryNode = mockData.getPrimaryNode();
nodeData = mockData.getNodeByIp(primaryNode.ip);
}
const tasks = nodeData.tasks;
const activeTasks = tasks.filter(task => task.enabled && task.running).length;
const response = {
summary: {
totalTasks: tasks.length,
activeTasks: activeTasks
},
tasks: tasks,
system: {
freeHeap: nodeData.freeHeap,
uptime: nodeData.uptime
}
};
res.json(response);
});
app.post('/api/tasks/control', (req, res) => {
const { task, action } = req.body;
if (!task || !action) {
return res.status(400).json({
success: false,
message: 'Missing parameters. Required: task, action',
example: '{"task": "discovery_send", "action": "status"}'
});
}
const validActions = ['enable', 'disable', 'start', 'stop', 'status'];
if (!validActions.includes(action)) {
return res.status(400).json({
success: false,
message: 'Invalid action. Use: enable, disable, start, stop, or status',
task: task,
action: action
});
}
// Simulate task control
const primaryNode = mockData.getPrimaryNode();
const nodeData = mockData.getNodeByIp(primaryNode.ip);
const taskData = nodeData.tasks.find(t => t.name === task);
if (!taskData) {
return res.status(404).json({
success: false,
message: `Task ${task} not found`
});
}
// Apply action
switch (action) {
case 'enable':
taskData.enabled = true;
break;
case 'disable':
taskData.enabled = false;
taskData.running = false;
break;
case 'start':
if (taskData.enabled) {
taskData.running = true;
}
break;
case 'stop':
taskData.running = false;
break;
case 'status':
// Return detailed status
return res.json({
success: true,
message: 'Task status retrieved',
task: task,
action: action,
taskDetails: {
name: taskData.name,
enabled: taskData.enabled,
running: taskData.running,
interval: taskData.interval,
system: {
freeHeap: nodeData.freeHeap,
uptime: nodeData.uptime
}
}
});
}
res.json({
success: true,
message: `Task ${action}d`,
task: task,
action: action
});
});
// System status endpoint
app.get('/api/node/status', (req, res) => {
const { ip } = req.query;
let nodeData;
if (ip) {
nodeData = mockData.getNodeByIp(ip);
if (!nodeData) {
return res.status(404).json({
error: 'Node not found',
message: `Node ${ip} not found in mock simulation`
});
}
} else {
// Use primary node
const primaryNode = mockData.getPrimaryNode();
nodeData = mockData.getNodeByIp(primaryNode.ip);
}
const response = {
freeHeap: nodeData.freeHeap,
chipId: nodeData.chipId,
sdkVersion: nodeData.systemInfo.sdkVersion,
cpuFreqMHz: nodeData.systemInfo.cpuFreqMHz,
flashChipSize: nodeData.systemInfo.flashChipSize,
api: nodeData.apiEndpoints
};
res.json(response);
});
// Cluster members endpoint
app.get('/api/cluster/members', (req, res) => {
const allNodes = mockData.getAllNodes();
const members = allNodes.map(node => ({
hostname: node.hostname,
ip: node.ip,
lastSeen: Math.floor(node.lastSeen / 1000), // Convert to seconds
latency: node.latency,
status: node.status,
resources: {
freeHeap: node.freeHeap,
chipId: node.chipId,
sdkVersion: node.systemInfo.sdkVersion,
cpuFreqMHz: node.systemInfo.cpuFreqMHz,
flashChipSize: node.systemInfo.flashChipSize
},
api: node.apiEndpoints
}));
res.json({ members });
});
// Node endpoints endpoint
app.get('/api/node/endpoints', (req, res) => {
const { ip } = req.query;
let nodeData;
if (ip) {
nodeData = mockData.getNodeByIp(ip);
if (!nodeData) {
return res.status(404).json({
error: 'Node not found',
message: `Node ${ip} not found in mock simulation`
});
}
} else {
// Use primary node
const primaryNode = mockData.getPrimaryNode();
nodeData = mockData.getNodeByIp(primaryNode.ip);
}
res.json(nodeData.apiEndpoints);
});
// Generic proxy endpoint
app.post('/api/proxy-call', (req, res) => {
const { ip, method, uri, params } = req.body || {};
if (!ip || !method || !uri) {
return res.status(400).json({
error: 'Missing required fields',
message: 'Required: ip, method, uri'
});
}
// Simulate proxy call by routing to appropriate mock endpoint
const nodeData = mockData.getNodeByIp(ip);
if (!nodeData) {
return res.status(404).json({
error: 'Node not found',
message: `Node ${ip} not found in mock simulation`
});
}
// Simulate different responses based on URI
if (uri === '/api/node/status') {
return res.json({
freeHeap: nodeData.freeHeap,
chipId: nodeData.chipId,
sdkVersion: nodeData.systemInfo.sdkVersion,
cpuFreqMHz: nodeData.systemInfo.cpuFreqMHz,
flashChipSize: nodeData.systemInfo.flashChipSize,
api: nodeData.apiEndpoints
});
} else if (uri === '/api/tasks/status') {
const tasks = nodeData.tasks;
const activeTasks = tasks.filter(task => task.enabled && task.running).length;
return res.json({
summary: {
totalTasks: tasks.length,
activeTasks: activeTasks
},
tasks: tasks,
system: {
freeHeap: nodeData.freeHeap,
uptime: nodeData.uptime
}
});
} else if (uri === '/api/monitoring/resources') {
// Return realistic monitoring resources data
const totalHeap = nodeData.systemInfo.flashChipSize || 1048576; // 1MB default
const freeHeap = nodeData.freeHeap;
const usedHeap = totalHeap - freeHeap;
const heapUsagePercent = (usedHeap / totalHeap) * 100;
return res.json({
cpu: {
average_usage: Math.random() * 30 + 10, // 10-40% CPU usage
current_usage: Math.random() * 50 + 5, // 5-55% current usage
frequency_mhz: nodeData.systemInfo.cpuFreqMHz || 80
},
memory: {
total_heap: totalHeap,
free_heap: freeHeap,
used_heap: usedHeap,
heap_usage_percent: heapUsagePercent,
min_free_heap: Math.floor(freeHeap * 0.8), // 80% of current free heap
max_alloc_heap: Math.floor(totalHeap * 0.9) // 90% of total heap
},
filesystem: {
total_bytes: 3145728, // 3MB SPIFFS
used_bytes: Math.floor(3145728 * (0.3 + Math.random() * 0.4)), // 30-70% used
free_bytes: 0 // Will be calculated
},
network: {
wifi_rssi: -30 - Math.floor(Math.random() * 40), // -30 to -70 dBm
wifi_connected: true,
uptime_seconds: nodeData.uptime
},
timestamp: new Date().toISOString()
});
} else {
return res.json({
success: true,
message: `Mock response for ${method} ${uri}`,
node: ip,
timestamp: new Date().toISOString()
});
}
});
// Firmware update endpoint
app.post('/api/node/update', (req, res) => {
// Simulate firmware update
res.json({
status: 'updating',
message: 'Firmware update in progress (mock simulation)'
});
});
// System restart endpoint
app.post('/api/node/restart', (req, res) => {
// Simulate system restart
res.json({
status: 'restarting'
});
});
// Test route
app.get('/test', (req, res) => {
res.send('Mock server is working!');
});
// Serve the mock UI (main UI with modified API client)
app.get('/', (req, res) => {
const filePath = path.join(__dirname, 'mock-ui.html');
console.log('Serving mock UI from:', filePath);
res.sendFile(filePath);
});
// Serve the original mock frontend
app.get('/frontend', (req, res) => {
res.sendFile(path.join(__dirname, 'mock-frontend.html'));
});
// Serve the main UI with modified API client
app.get('/ui', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
// Serve static files from public directory (after custom routes)
// Only serve static files for specific paths, not the root
app.use('/static', express.static(path.join(__dirname, '../public')));
app.use('/styles', express.static(path.join(__dirname, '../public/styles')));
app.use('/scripts', express.static(path.join(__dirname, '../public/scripts')));
app.use('/vendor', express.static(path.join(__dirname, '../public/vendor')));
// Serve mock API client
app.get('/test/mock-api-client.js', (req, res) => {
res.sendFile(path.join(__dirname, 'mock-api-client.js'));
});
// Serve test page
app.get('/test-page', (req, res) => {
res.sendFile(path.join(__dirname, 'test-page.html'));
});
// Serve favicon to prevent 404 errors
app.get('/favicon.ico', (req, res) => {
res.status(204).end(); // No content
});
// Serve mock server info page
app.get('/info', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPORE UI - Mock Server Info</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; }
.status { background: #e8f5e8; padding: 15px; border-radius: 5px; margin: 20px 0; }
.info { background: #f0f8ff; padding: 15px; border-radius: 5px; margin: 20px 0; }
.endpoint { background: #f9f9f9; padding: 10px; margin: 5px 0; border-left: 4px solid #007acc; }
.mock-note { background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0; border-left: 4px solid #ffc107; }
.btn { display: inline-block; padding: 10px 20px; background: #007acc; color: white; text-decoration: none; border-radius: 5px; margin: 5px; }
.btn:hover { background: #005a9e; }
</style>
</head>
<body>
<div class="container">
<h1>🚀 SPORE UI Mock Server</h1>
<div class="mock-note">
<strong>Mock Mode Active:</strong> This is a complete simulation of the SPORE embedded system.
No real hardware or UDP ports are required.
</div>
<div class="status">
<h3>📊 Server Status</h3>
<p><strong>Status:</strong> Running</p>
<p><strong>Port:</strong> ${MOCK_CONFIG.port}</p>
<p><strong>Configuration:</strong> ${MOCK_CONFIG.name}</p>
<p><strong>Mock Nodes:</strong> ${MOCK_CONFIG.nodes.length}</p>
<p><strong>Primary Node:</strong> ${mockData.getPrimaryNode().ip}</p>
</div>
<div class="info">
<h3>🌐 Access Points</h3>
<p><a href="/" class="btn">Mock UI (Port 3002)</a> - Full UI with mock data</p>
<p><a href="/frontend" class="btn">Mock Frontend</a> - Custom mock frontend</p>
<p><a href="/ui" class="btn">Real UI (Port 3002)</a> - Real UI connected to mock server</p>
<p><a href="/api/health" class="btn">API Health</a> - Check server status</p>
</div>
<div class="info">
<h3>🔗 Available Endpoints</h3>
<div class="endpoint"><strong>GET</strong> /api/health - Health check</div>
<div class="endpoint"><strong>GET</strong> /api/discovery/nodes - Discovery status</div>
<div class="endpoint"><strong>GET</strong> /api/tasks/status - Task status</div>
<div class="endpoint"><strong>POST</strong> /api/tasks/control - Control tasks</div>
<div class="endpoint"><strong>GET</strong> /api/node/status - System status</div>
<div class="endpoint"><strong>GET</strong> /api/cluster/members - Cluster members</div>
<div class="endpoint"><strong>POST</strong> /api/proxy-call - Generic proxy</div>
</div>
<div class="info">
<h3>🎮 Mock Features</h3>
<ul>
<li>✅ Multiple simulated SPORE nodes</li>
<li>✅ Realistic data that changes over time</li>
<li>✅ No UDP port conflicts</li>
<li>✅ All API endpoints implemented</li>
<li>✅ Random failures simulation</li>
<li>✅ Primary node rotation</li>
</ul>
</div>
<div class="info">
<h3>🔧 Configuration</h3>
<p>Use npm scripts to change configuration:</p>
<ul>
<li><code>npm run mock:healthy</code> - Healthy cluster (3 nodes)</li>
<li><code>npm run mock:degraded</code> - Degraded cluster (some inactive)</li>
<li><code>npm run mock:large</code> - Large cluster (8 nodes)</li>
<li><code>npm run mock:unstable</code> - Unstable cluster (high failure rate)</li>
<li><code>npm run mock:single</code> - Single node</li>
<li><code>npm run mock:empty</code> - Empty cluster</li>
</ul>
</div>
</div>
</body>
</html>
`);
});
// Start the mock server
const server = app.listen(MOCK_CONFIG.port, () => {
console.log('🚀 SPORE UI Mock Server Started');
console.log('================================');
console.log(`Configuration: ${MOCK_CONFIG.name}`);
console.log(`Description: ${MOCK_CONFIG.description}`);
console.log(`Port: ${MOCK_CONFIG.port}`);
console.log(`URL: http://localhost:${MOCK_CONFIG.port}`);
console.log(`Mock Nodes: ${MOCK_CONFIG.nodes.length}`);
console.log(`Primary Node: ${mockData.getPrimaryNode().ip}`);
console.log('');
console.log('📡 Available Mock Nodes:');
MOCK_CONFIG.nodes.forEach((node, index) => {
console.log(` ${index + 1}. ${node.hostname} (${node.ip}) - ${node.status}`);
});
console.log('');
console.log('🎮 Mock Features:');
console.log(' ✅ No UDP port conflicts');
console.log(' ✅ Realistic data simulation');
console.log(' ✅ All API endpoints');
console.log(` ✅ Time-based data updates (${MOCK_CONFIG.simulation.updateInterval}ms)`);
console.log(` ✅ Random failure simulation (${MOCK_CONFIG.simulation.enableRandomFailures ? 'Enabled' : 'Disabled'})`);
console.log(` ✅ Primary node rotation (${MOCK_CONFIG.simulation.primaryNodeRotation ? 'Enabled' : 'Disabled'})`);
console.log('');
console.log('Press Ctrl+C to stop');
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n\n👋 Mock server stopped. Goodbye!');
server.close(() => {
process.exit(0);
});
});
module.exports = { app, mockData, MOCK_CONFIG };