Files
appRobotDriver/test/InfoServer.test.js
2026-06-25 18:58:55 +02:00

386 lines
14 KiB
JavaScript

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',
isGCodeReceiver: true,
state: 'connected',
url: null,
isTestMode: false,
error: null,
reconnectAttempt: 0,
reconnectTimer: false,
health: 'ok',
reason: undefined,
grblState: null,
machinePosition: null,
plannerBlocksFree: null,
rxBytesFree: null,
lastError: null,
lastReportAt: null
},
{
name: 'Hand',
isGCodeReceiver: true,
state: 'disconnected',
url: null,
isTestMode: false,
error: null,
reconnectAttempt: 0,
reconnectTimer: false,
health: 'disconnected',
reason: 'no active socket connection',
grblState: null,
machinePosition: null,
plannerBlocksFree: null,
rxBytesFree: null,
lastError: null,
lastReportAt: null
}
]);
});
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',
isGCodeReceiver: true,
state: 'reconnecting',
url: 'reconnect.test',
isTestMode: false,
error: 'timeout',
reconnectAttempt: 2,
reconnectTimer: true,
health: 'warning',
reason: undefined,
grblState: null,
machinePosition: null,
plannerBlocksFree: null,
rxBytesFree: null,
lastError: null,
lastReportAt: null
}
]);
});
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: [] };
// e = Greifer-Öffnung (mm) → position.e; eMotor = Greifer-Motorwert → motorCounts.e.
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, e: 2.5, eMotor: 7 };
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, e: 2.5 });
expect(json.motorCounts.e).toBe(7); // Motorwert (eMotor), nicht die mm-Öffnung
});
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);
});
});