304 lines
11 KiB
JavaScript
304 lines
11 KiB
JavaScript
const fs = require('fs');
|
|
const https = require('https');
|
|
const path = require('path');
|
|
const express = require('express');
|
|
|
|
const service = require('../server/RobotConfigService');
|
|
|
|
const DATA_DIR = path.join(__dirname, '..', 'data', 'robot');
|
|
const ROBOT_JSON = path.join(DATA_DIR, 'robot.json');
|
|
const TEST_KEY = 'test-api-key-12345';
|
|
|
|
const SAMPLE_ROBOT = {
|
|
links: {
|
|
Arm1: { size: [70, 250, 70] },
|
|
Arm2: { size: [70, 250, 70] },
|
|
Ellbow: { size: [90, 0, 0] }
|
|
}
|
|
};
|
|
|
|
// ── HTTPS-Test-Server ─────────────────────────────────────────────────────────
|
|
|
|
function makeServer(apiKey) {
|
|
const app = express();
|
|
service.register(app, { apiKey });
|
|
const key = fs.readFileSync('https/localhost.key');
|
|
const cert = fs.readFileSync('https/localhost.pem');
|
|
return https.createServer({ key, cert, passphrase: 'abcd' }, app);
|
|
}
|
|
|
|
function listen(server) {
|
|
return new Promise((resolve, reject) => {
|
|
server.listen(0, () => {
|
|
const addr = server.address();
|
|
addr ? resolve(addr.port) : reject(new Error('no port'));
|
|
});
|
|
server.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function closeServer(server) {
|
|
return new Promise(resolve => { server.close(resolve); });
|
|
}
|
|
|
|
function request(method, url, { body, token } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const agent = new https.Agent({ rejectUnauthorized: false });
|
|
const payload = body ? JSON.stringify(body) : undefined;
|
|
const headers = {
|
|
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
};
|
|
const req = https.request(url, { method, agent, headers }, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => { data += chunk; });
|
|
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
|
});
|
|
req.on('error', reject);
|
|
if (payload) req.write(payload);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
// ── Setup / Teardown ──────────────────────────────────────────────────────────
|
|
|
|
function cleanupTestFiles() {
|
|
for (const f of fs.readdirSync(DATA_DIR)) {
|
|
if (f === '.gitkeep' || f === '.apikey') continue;
|
|
fs.unlinkSync(path.join(DATA_DIR, f));
|
|
}
|
|
}
|
|
|
|
// robot.json gehört zur Betriebskonfiguration und darf durch Tests NICHT dauerhaft
|
|
// gelöscht werden. Wir sichern den Inhalt vor der Suite und stellen ihn danach exakt
|
|
// wieder her — inklusive aller Snapshot-Dateien, die schon vorher da waren.
|
|
let _savedRobotJson = null; // null = Datei existierte nicht
|
|
let _savedSnapshotFiles = {}; // filename → content (nur Dateien, die VOR den Tests da waren)
|
|
|
|
beforeAll(() => {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
// robot.json sichern
|
|
try { _savedRobotJson = fs.readFileSync(ROBOT_JSON, 'utf8'); } catch { _savedRobotJson = null; }
|
|
// Snapshot-Dateien sichern (robot_YYYYMMDD_HHmmss.json)
|
|
for (const f of fs.readdirSync(DATA_DIR)) {
|
|
if (/^robot_\d{8}_\d{6}\.json$/.test(f)) {
|
|
_savedSnapshotFiles[f] = fs.readFileSync(path.join(DATA_DIR, f), 'utf8');
|
|
}
|
|
}
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Testdateien bereinigen
|
|
cleanupTestFiles();
|
|
// robot.json wiederherstellen
|
|
if (_savedRobotJson !== null) {
|
|
fs.writeFileSync(ROBOT_JSON, _savedRobotJson, 'utf8');
|
|
}
|
|
// Vor-Test-Snapshots wiederherstellen
|
|
for (const [f, content] of Object.entries(_savedSnapshotFiles)) {
|
|
fs.writeFileSync(path.join(DATA_DIR, f), content, 'utf8');
|
|
}
|
|
});
|
|
|
|
beforeEach(() => {
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
cleanupTestFiles();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanupTestFiles();
|
|
});
|
|
|
|
// ── Einzel-Funktionen ─────────────────────────────────────────────────────────
|
|
|
|
describe('readRobotJson / writeRobotJson', () => {
|
|
test('writeRobotJson schreibt Datei und legt Snapshot an', async () => {
|
|
fs.writeFileSync(ROBOT_JSON, JSON.stringify({ version: 1 }), 'utf8');
|
|
const { snapshotFile } = await service.writeRobotJson({ version: 2 });
|
|
|
|
expect(snapshotFile).toMatch(/^robot_\d{8}_\d{6}\.json$/);
|
|
expect(fs.existsSync(path.join(DATA_DIR, snapshotFile))).toBe(true);
|
|
|
|
const written = JSON.parse(fs.readFileSync(ROBOT_JSON, 'utf8'));
|
|
expect(written).toEqual({ version: 2 });
|
|
|
|
const snapshot = JSON.parse(fs.readFileSync(path.join(DATA_DIR, snapshotFile), 'utf8'));
|
|
expect(snapshot).toEqual({ version: 1 });
|
|
});
|
|
|
|
test('kein Snapshot wenn robot.json noch nicht existiert', async () => {
|
|
const { snapshotFile } = await service.writeRobotJson({ fresh: true });
|
|
expect(fs.existsSync(path.join(DATA_DIR, snapshotFile))).toBe(false);
|
|
});
|
|
|
|
test('readRobotJson liest die aktuelle Datei', async () => {
|
|
fs.writeFileSync(ROBOT_JSON, JSON.stringify(SAMPLE_ROBOT), 'utf8');
|
|
const data = await service.readRobotJson();
|
|
expect(data).toEqual(SAMPLE_ROBOT);
|
|
});
|
|
|
|
test('readRobotJson wirft, wenn robot.json fehlt', async () => {
|
|
await expect(service.readRobotJson()).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('listHistory / readSnapshot', () => {
|
|
test('listHistory gibt leere Liste zurück wenn kein Snapshot', async () => {
|
|
const list = await service.listHistory();
|
|
expect(list).toEqual([]);
|
|
});
|
|
|
|
test('listHistory listet vorhandene Snapshots, neueste zuerst', async () => {
|
|
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260101_120000.json'), '{}', 'utf8');
|
|
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260102_083000.json'), '{}', 'utf8');
|
|
|
|
const list = await service.listHistory();
|
|
expect(list.map(e => e.filename)).toEqual([
|
|
'robot_20260102_083000.json',
|
|
'robot_20260101_120000.json'
|
|
]);
|
|
expect(list[0]).toEqual({ filename: 'robot_20260102_083000.json', day: '20260102', timestamp: '083000' });
|
|
});
|
|
|
|
test('readSnapshot liest einen bestimmten Snapshot', async () => {
|
|
const ts = '20260101_120000';
|
|
fs.writeFileSync(path.join(DATA_DIR, `robot_${ts}.json`), JSON.stringify({ x: 42 }), 'utf8');
|
|
const data = await service.readSnapshot(ts);
|
|
expect(data).toEqual({ x: 42 });
|
|
});
|
|
|
|
test('readSnapshot wirft bei unbekanntem Timestamp', async () => {
|
|
await expect(service.readSnapshot('99999999_999999')).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Pruning', () => {
|
|
test('≤ 100 Snapshots pro Tag bleiben alle erhalten', async () => {
|
|
for (let i = 1; i <= 5; i++) {
|
|
fs.writeFileSync(path.join(DATA_DIR, `robot_2026061${i}_120000.json`), '{}', 'utf8');
|
|
}
|
|
fs.writeFileSync(ROBOT_JSON, '{}', 'utf8');
|
|
await service.writeRobotJson({ pruned: true });
|
|
|
|
const list = await service.listHistory();
|
|
// 5 bestehende (verschiedene Tage) + 1 neuer Snapshot = 6
|
|
expect(list.length).toBe(6);
|
|
});
|
|
|
|
test('> 100 Snapshots eines Tages: nur neuester bleibt', async () => {
|
|
const day = '20260101';
|
|
for (let i = 0; i < 102; i++) {
|
|
const hh = String(Math.floor(i / 3600)).padStart(2, '0');
|
|
const mm = String(Math.floor((i % 3600) / 60)).padStart(2, '0');
|
|
const ss = String(i % 60).padStart(2, '0');
|
|
fs.writeFileSync(path.join(DATA_DIR, `robot_${day}_${hh}${mm}${ss}.json`), `{"i":${i}}`, 'utf8');
|
|
}
|
|
// Neuester (lexikographisch letzter) vorher anlegen
|
|
const newest = `robot_${day}_235959.json`;
|
|
fs.writeFileSync(path.join(DATA_DIR, newest), '{"newest":true}', 'utf8');
|
|
|
|
fs.writeFileSync(ROBOT_JSON, '{}', 'utf8');
|
|
await service.writeRobotJson({ trigger: true });
|
|
|
|
const list = await service.listHistory();
|
|
const daySnapshots = list.filter(e => e.day === day);
|
|
expect(daySnapshots.length).toBe(1);
|
|
expect(daySnapshots[0].filename).toBe(newest);
|
|
});
|
|
});
|
|
|
|
// ── HTTP-Routen ───────────────────────────────────────────────────────────────
|
|
|
|
describe('HTTP GET /api/robot', () => {
|
|
let server, port;
|
|
|
|
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
|
afterEach(async () => { await closeServer(server); });
|
|
|
|
test('404 wenn robot.json nicht existiert', async () => {
|
|
const { statusCode } = await request('GET', `https://127.0.0.1:${port}/api/robot`);
|
|
expect(statusCode).toBe(404);
|
|
});
|
|
|
|
test('200 mit robot.json-Inhalt', async () => {
|
|
fs.writeFileSync(ROBOT_JSON, JSON.stringify(SAMPLE_ROBOT), 'utf8');
|
|
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot`);
|
|
expect(statusCode).toBe(200);
|
|
expect(JSON.parse(body)).toEqual(SAMPLE_ROBOT);
|
|
});
|
|
});
|
|
|
|
describe('HTTP PUT /api/robot', () => {
|
|
let server, port;
|
|
|
|
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
|
afterEach(async () => { await closeServer(server); });
|
|
|
|
test('401 ohne Authorization-Header', async () => {
|
|
const { statusCode } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, { body: SAMPLE_ROBOT });
|
|
expect(statusCode).toBe(401);
|
|
});
|
|
|
|
test('401 mit falschem Key', async () => {
|
|
const { statusCode } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, {
|
|
body: SAMPLE_ROBOT, token: 'wrong-key'
|
|
});
|
|
expect(statusCode).toBe(401);
|
|
});
|
|
|
|
test('200 mit korrektem Key — schreibt Datei', async () => {
|
|
const { statusCode, body } = await request('PUT', `https://127.0.0.1:${port}/api/robot`, {
|
|
body: SAMPLE_ROBOT, token: TEST_KEY
|
|
});
|
|
expect(statusCode).toBe(200);
|
|
const result = JSON.parse(body);
|
|
expect(result.ok).toBe(true);
|
|
expect(result.snapshotFile).toMatch(/^robot_\d{8}_\d{6}\.json$/);
|
|
|
|
const written = JSON.parse(fs.readFileSync(ROBOT_JSON, 'utf8'));
|
|
expect(written).toEqual(SAMPLE_ROBOT);
|
|
});
|
|
});
|
|
|
|
describe('HTTP GET /api/robot/history', () => {
|
|
let server, port;
|
|
|
|
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
|
afterEach(async () => { await closeServer(server); });
|
|
|
|
test('200 mit leerer History', async () => {
|
|
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history`);
|
|
expect(statusCode).toBe(200);
|
|
expect(JSON.parse(body)).toEqual({ history: [] });
|
|
});
|
|
|
|
test('200 listet vorhandene Snapshots', async () => {
|
|
fs.writeFileSync(path.join(DATA_DIR, 'robot_20260611_100000.json'), '{}', 'utf8');
|
|
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history`);
|
|
expect(statusCode).toBe(200);
|
|
const { history } = JSON.parse(body);
|
|
expect(history).toHaveLength(1);
|
|
expect(history[0].filename).toBe('robot_20260611_100000.json');
|
|
});
|
|
});
|
|
|
|
describe('HTTP GET /api/robot/history/:ts', () => {
|
|
let server, port;
|
|
|
|
beforeEach(async () => { server = makeServer(TEST_KEY); port = await listen(server); });
|
|
afterEach(async () => { await closeServer(server); });
|
|
|
|
test('200 für vorhandenen Snapshot', async () => {
|
|
const ts = '20260611_100000';
|
|
fs.writeFileSync(path.join(DATA_DIR, `robot_${ts}.json`), JSON.stringify({ snap: true }), 'utf8');
|
|
const { statusCode, body } = await request('GET', `https://127.0.0.1:${port}/api/robot/history/${ts}`);
|
|
expect(statusCode).toBe(200);
|
|
expect(JSON.parse(body)).toEqual({ snap: true });
|
|
});
|
|
|
|
test('404 für unbekannten Snapshot', async () => {
|
|
const { statusCode } = await request('GET', `https://127.0.0.1:${port}/api/robot/history/99991231_235959`);
|
|
expect(statusCode).toBe(404);
|
|
});
|
|
});
|