Konfig in robot.json
This commit is contained in:
272
test/RobotConfigService.test.js
Normal file
272
test/RobotConfigService.test.js
Normal file
@@ -0,0 +1,272 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user