server-up/tests/js/joblog.test.js
Ramon 3ee09eebc7
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m12s
fix(stacks): geslaagde wijziging niet meer terugdraaien
Het audit-logboek las request.remote_addr uit in de achtergrondthread van de
job. De RuntimeError die daarop volgde belandde in de foutafhandeling, die de
zojuist gemaakte backup terugzette — inclusief het oude compose-bestand.

- Adres vóór de threadstart vastleggen bij herconfigureren, koppelen, backup
  maken en terugrollen.
- UniFi: init-script probeert de beheerders 'unifi' en 'root' en herkent een
  server zonder authenticatie, zodat ook een oud Mongo-volume gerepareerd
  wordt; bestaande rollen blijven staan en 'backup'/'restore' worden aangevuld.
- UniFi: healthcheck van Mongo zonder inloggegevens, anders bleef de container
  ongezond en startte de reparatiecontainer nooit.
- Backups: mongodump/mongorestore gebruiken de beheerder uit compose in plaats
  van de verzonnen gebruiker 'app'.
- Appkaart: knop die de webinterface opent, met het adres uit /api/stacks.
- Joblog: geen dubbele regels meer doordat polls elkaar konden overlappen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
2026-08-06 16:44:01 +02:00

66 lines
2.6 KiB
JavaScript

// Draait de echte Alpine-component uit index.html en toetst dat pollJob niet
// twee keer tegelijk vraagt. Wordt aangeroepen door tests/test_ui_logica.py;
// los draaien kan met: node tests/js/joblog.test.js
const fs = require('fs');
const html = fs.readFileSync(__dirname + '/../../server-up/templates/index.html', 'utf8');
const start = html.indexOf('function app() {');
const eind = html.indexOf('</script>', start);
const bron = html.slice(start, eind);
global.localStorage = { getItem: () => null, setItem: () => {} };
global.location = { hash: '', search: '' };
global.window = { matchMedia: () => ({matches:false, addEventListener(){}}),
addEventListener(){}, innerWidth: 1200 };
global.document = { documentElement: {classList:{add(){},remove(){}}},
addEventListener(){}, querySelector: () => null };
global.navigator = { clipboard: { writeText: async () => {} }, language: 'nl' };
global.crypto = require('crypto').webcrypto;
eval(bron);
let fouten = 0;
const check = (naam, waar) => { if (!waar) { fouten++; console.log(' ✖ ' + naam); }
else console.log(' ✓ ' + naam); };
const rustig = () => new Promise(r => setTimeout(r, 0));
(async () => {
const c = app();
c.t = (k) => k;
c.$nextTick = (fn) => fn();
c.$refs = {};
// De tikken van setInterval zelf in de hand houden: zo kunnen we een tweede
// tik afvuren terwijl het antwoord op de eerste nog onderweg is — precies
// wat er gebeurt bij een trage stap zoals een backup terugzetten.
let tik = null;
global.setInterval = (fn) => { tik = fn; return 1; };
global.clearInterval = () => {};
const openstaand = [];
c.rpc = () => new Promise(res => openstaand.push(res));
c.pollJob('abc123');
tik(); await rustig();
tik(); await rustig();
tik(); await rustig();
check('maar één verzoek tegelijk onderweg', openstaand.length === 1);
openstaand.forEach(res => res({lines: [{level: 'dim', text: 'Backup wordt teruggezet…'}],
status: 'running'}));
await rustig();
check('de regel staat er één keer',
c.job.lines.filter(l => l.text === 'Backup wordt teruggezet…').length === 1);
// Daarna schuift de offset op en mag er weer gevraagd worden.
tik(); await rustig();
check('volgende tik vraagt verder vanaf de nieuwe offset', openstaand.length === 2);
let klaar = '';
c.job.timer = 1;
openstaand[1]({lines: [], status: 'done'});
await rustig();
check('afgeronde job stopt de poll', c.job.status === 'done');
console.log(fouten === 0 ? '\nAlle joblogcontroles geslaagd' : `\n${fouten} FOUTEN`);
process.exit(fouten ? 1 : 0);
})();