const fs = require('fs'); const https = require('https'); const createInfoServer = require('../server/InfoServer'); const GCode = require('../robot/GCode'); function listen(server) { return new Promise((resolve, reject) => { server.listen(0, () => { const address = server.address(); if (address && address.port) { resolve(address.port); } else { reject(new Error('Failed to get server port')); } }); server.on('error', reject); }); } function request(url) { return new Promise((resolve, reject) => { const agent = new https.Agent({ rejectUnauthorized: false }); https.get(url, { agent }, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk.toString(); }); res.on('end', () => { resolve({ statusCode: res.statusCode, body: data }); }); }).on('error', reject); }); } function post(url) { return new Promise((resolve, reject) => { const agent = new https.Agent({ rejectUnauthorized: false }); const parsed = new URL(url); const options = { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: 'POST', agent, headers: { 'Content-Length': 0 } }; const req = https.request(options, res => { let data = ''; res.on('data', c => { data += c.toString(); }); res.on('end', () => resolve({ statusCode: res.statusCode, body: data })); }); req.on('error', reject); req.end(); }); } describe('InfoServer', () => { let server; let port; afterEach(async () => { if (server) { await new Promise((resolve) => server.close(resolve)); server = null; } }); test('returns status JSON with sender and shared state information', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: ['127.0.0.1'], lastCommands: ['G1 X10 Y10'], lastPings: ['Ping'] }; const robot = { x: 1, y: 2, z: 3, phi: 0, theta: 0, psi: 0 }; const senders = [ { name: 'Base', instance: { tSocket: {} } }, { name: 'Hand', instance: null } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/status`); expect(statusCode).toBe(200); const status = JSON.parse(body); expect(status.clients).toEqual(['127.0.0.1']); expect(status.lastCommands).toEqual(['G1 X10 Y10']); expect(status.lastPings).toEqual(['Ping']); // Machine-readable top-level health summary (one of two senders connected) expect(status.health).toEqual({ ok: false, connectedSenders: 1, totalSenders: 2 }); expect(typeof status.generatedAt).toBe('string'); expect(Number.isNaN(Date.parse(status.generatedAt))).toBe(false); expect(status.senders).toEqual([ { name: 'Base', state: 'connected', url: null, isTestMode: false, error: null, reconnectAttempt: 0, reconnectTimer: false, health: 'ok', reason: undefined }, { name: 'Hand', state: 'disconnected', url: null, isTestMode: false, error: null, reconnectAttempt: 0, reconnectTimer: false, health: 'disconnected', reason: 'no active socket connection' } ]); }); test('returns sender health details from instance.getStatus()', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0 }; const senders = [ { name: 'Reconnect', instance: { getStatus: () => ({ state: 'reconnecting', url: 'reconnect.test', error: 'timeout', isTestMode: false, reconnectAttempt: 2, reconnectTimer: true }) } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/status`); expect(statusCode).toBe(200); const status = JSON.parse(body); expect(status.senders).toEqual([ { name: 'Reconnect', state: 'reconnecting', url: 'reconnect.test', isTestMode: false, error: 'timeout', reconnectAttempt: 2, reconnectTimer: true, health: 'warning', reason: undefined } ]); }); test('returns position JSON from GCode.getM114', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 10, y: 20, z: 30, phi: 0.1, theta: 0.2, psi: 0.3, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const senders = []; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/position`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.position).toEqual({ x: 10, y: 20, z: 30, a: 0.1, b: 0.2, c: 0.3 }); }); test('returns 404 for unknown endpoints', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const senders = []; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode } = await request(`https://127.0.0.1:${port}/api/unknown`); expect(statusCode).toBe(404); }); // ── Emergency Stop Endpoints ───────────────────────────────────────────── test('POST /api/emergency-stop ruft emergencyStop() auf allen Sendern auf', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const stopA = jest.fn(() => Promise.resolve({ ok: true })); const stopB = jest.fn(() => Promise.resolve({ ok: true, skipped: true })); const senders = [ { name: 'Base', instance: { emergencyStop: stopA, alarmUnlock: jest.fn(() => Promise.resolve({ ok: true })) } }, { name: 'EmergencyStop', instance: { emergencyStop: stopB, alarmUnlock: jest.fn(() => Promise.resolve({ ok: true, skipped: true })) } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/emergency-stop`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.ok).toBe(true); expect(stopA).toHaveBeenCalledTimes(1); expect(stopB).toHaveBeenCalledTimes(1); expect(json.results).toHaveLength(2); }); test('POST /api/alarm-unlock ruft alarmUnlock() auf allen Sendern auf', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const unlockA = jest.fn(() => Promise.resolve({ ok: true })); const unlockB = jest.fn(() => Promise.resolve({ ok: true, skipped: true })); const senders = [ { name: 'Base', instance: { emergencyStop: jest.fn(() => Promise.resolve({ ok: true })), alarmUnlock: unlockA } }, { name: 'EmergencyStop', instance: { emergencyStop: jest.fn(() => Promise.resolve({ ok: true })), alarmUnlock: unlockB } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/alarm-unlock`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.ok).toBe(true); expect(unlockA).toHaveBeenCalledTimes(1); expect(unlockB).toHaveBeenCalledTimes(1); }); test('POST /api/emergency-stop ok=false wenn ein Sender fehlschlägt', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const senders = [ { name: 'Base', instance: { emergencyStop: () => Promise.resolve({ ok: false, error: 'not connected' }), alarmUnlock: () => Promise.resolve({ ok: true }) } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/emergency-stop`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.ok).toBe(false); expect(json.results[0].ok).toBe(false); }); test('GET /api/power-status gibt armed=true zurück wenn Shelly output:true', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const getArmedFn = jest.fn(() => Promise.resolve({ ok: true, armed: true, voltage: 234.9, power: 15.5 })); const senders = [ { name: 'EmergencyStop', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }), getArmed: getArmedFn } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/power-status`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.ok).toBe(true); expect(json.armed).toBe(true); expect(getArmedFn).toHaveBeenCalledTimes(1); }); test('GET /api/power-status gibt armed=false zurück wenn kein Shelly konfiguriert', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const senders = [ { name: 'Base', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }) } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await request(`https://127.0.0.1:${port}/api/power-status`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.ok).toBe(false); expect(json.armed).toBe(false); expect(json.error).toMatch(/no shelly/); }); test('POST /api/power-on ruft powerOn() auf Shelly-Sender auf', async () => { const key = fs.readFileSync('https/localhost.key'); const cert = fs.readFileSync('https/localhost.pem'); const httpsOptions = { key, cert, passphrase: 'abcd' }; const sharedState = { connectedClients: [], lastCommands: [], lastPings: [] }; const robot = { x: 0, y: 0, z: 0, phi: 0, theta: 0, psi: 0, xMotor: 0, alpha: 0, beta: 0, a: 0, b: 0, c: 0 }; const powerOnFn = jest.fn(() => Promise.resolve({ ok: true, status: 200 })); const senders = [ { name: 'Base', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }) } }, { name: 'EmergencyStop', instance: { emergencyStop: () => Promise.resolve({ ok: true }), alarmUnlock: () => Promise.resolve({ ok: true }), powerOn: powerOnFn } } ]; server = createInfoServer(httpsOptions, sharedState, robot, GCode, senders); port = await listen(server); const { statusCode, body } = await post(`https://127.0.0.1:${port}/api/power-on`); expect(statusCode).toBe(200); const json = JSON.parse(body); expect(json.ok).toBe(true); expect(powerOnFn).toHaveBeenCalledTimes(1); }); });