All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 2m31s
- Sjablonen kunnen nu secret_format ("hex" of "laravel") en secret_bytes
opgeven. De generator maakte altijd url-veilige base64, en dat is precies wat
Laravel, Homarr en LibreChat weigeren
- Daarmee kwamen twee fouten uit v0.8.00 aan het licht: Homarr en LibreChat
kregen sleutels die hun containers niet accepteren. Rechtgezet
- Snipe-IT en Invoice Ninja alsnog toegevoegd; die vielen vorige ronde af omdat
hun APP_KEY niet te genereren was
- 19 vervangers voor big tech erbij, van 197 naar 218 apps:
Baikal en Radicale (Google Agenda/Contacten), LibreTranslate (Translate),
CryptPad, Collabora en ONLYOFFICE (Google Docs), Formbricks (Forms),
Jitsi Meet (Meet/Zoom), PeerTube, Invidious en Tube Archivist (YouTube),
Owncast (Twitch), Mastodon (X), Pixelfed (Instagram), Lemmy (Reddit),
Cal.com (Calendly), Rallly (Doodle), DocuSeal (DocuSign), Penpot (Figma)
- Communicatie van 12 naar 16 apps, productiviteit van 21 naar 31
- Nieuwe JS-test voor de sleutelformaten; alle 242 images in de catalogus
opnieuw tegen hun registry gecontroleerd
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
170 lines
9.7 KiB
JavaScript
170 lines
9.7 KiB
JavaScript
// Draait de echte Alpine-component uit index.html en toetst de logica van
|
|
// het invulmenu. Wordt aangeroepen door tests/test_ui_logica.py; los draaien
|
|
// kan met: node tests/js/menu.test.js
|
|
const fs = require('fs');
|
|
const html = fs.readFileSync(__dirname + '/../../server-up/templates/index.html', 'utf8');
|
|
// Alleen de component-functie uitsnijden en uitvoeren.
|
|
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);
|
|
const c = app();
|
|
c.t = (k, v) => k; // vertaling niet nodig voor de logica
|
|
c.bpFieldVisible = () => true;
|
|
c.rpc = async () => ({ok: true, preview_files: {}, env_preview: ''});
|
|
c.toast = () => {};
|
|
|
|
function veld(naam, stap, extra = {}) {
|
|
return Object.assign({name: naam, title: naam, step: stap, type: 'str',
|
|
group: 'Algemeen', required: false, advanced: false}, extra);
|
|
}
|
|
|
|
let fouten = 0;
|
|
const check = (naam, waar) => { if (!waar) { fouten++; console.log(' ✖ ' + naam); }
|
|
else console.log(' ✓ ' + naam); };
|
|
|
|
// ── Eenvoudige app: alleen basisvelden ──────────────────────────────────────
|
|
c.install = {format:'boilerplate', loading:false, fields:[
|
|
veld('service_name','basis'), veld('port','basis'), veld('appdata_dir','basis'),
|
|
], values:{}, step:0};
|
|
c.onePage = false; c.showAdvanced = false;
|
|
let s = c.wizardSteps().map(x => x.id);
|
|
check('eenvoudige app: basis, verbinden, controleren',
|
|
JSON.stringify(s) === '["basis","verbinden","controleren"]');
|
|
|
|
// ── App met geheimen ────────────────────────────────────────────────────────
|
|
c.install.fields.push(veld('db_password','geheimen',{required:true, secret:true}));
|
|
s = c.wizardSteps().map(x => x.id);
|
|
check('geheimenstap verschijnt', s.includes('geheimen'));
|
|
|
|
// ── Lege stap wordt overgeslagen ────────────────────────────────────────────
|
|
check('geen lege instellingenstap', !s.includes('instellingen'));
|
|
|
|
// ── Geavanceerde velden ─────────────────────────────────────────────────────
|
|
c.install.fields.push(veld('retention','instellingen',{advanced:true}));
|
|
check('geavanceerd veld verborgen', c.stepFields('instellingen').length === 0);
|
|
check('stap zonder zichtbare velden verdwijnt',
|
|
!c.wizardSteps().map(x=>x.id).includes('instellingen'));
|
|
c.showAdvanced = true;
|
|
check('geavanceerd veld zichtbaar na aanzetten', c.stepFields('instellingen').length === 1);
|
|
check('stap komt terug', c.wizardSteps().map(x=>x.id).includes('instellingen'));
|
|
check('teller klopt', c.advancedCount() === 1);
|
|
|
|
// ── Verplicht veld blokkeert Volgende ───────────────────────────────────────
|
|
c.install.step = c.wizardSteps().findIndex(x => x.id === 'geheimen');
|
|
check('leeg verplicht veld wordt gemeld', c.stepMissing().length === 1);
|
|
c.install.values.db_password = 'ietsGeheims';
|
|
check('ingevuld: niets meer te melden', c.stepMissing().length === 0);
|
|
|
|
// ── Groepsschakelaar uit: veld telt niet mee ────────────────────────────────
|
|
c.install.fields.push(veld('opt_pass','geheimen',
|
|
{required:true, secret:true, group:'Extra', group_toggle:'gebruik_extra'}));
|
|
c.install.values.gebruik_extra = false;
|
|
check('uitgeschakelde groep blokkeert niet', c.stepMissing().length === 0);
|
|
c.install.values.gebruik_extra = true;
|
|
check('ingeschakelde groep blokkeert wel', c.stepMissing().length === 1);
|
|
|
|
// ── Navigatie ───────────────────────────────────────────────────────────────
|
|
c.install.values.opt_pass = 'x';
|
|
c.install.step = 0;
|
|
check('niet op de laatste stap', !c.opLaatsteStap());
|
|
c.install.step = c.wizardSteps().length - 1;
|
|
check('wel op de laatste stap', c.opLaatsteStap());
|
|
check('laatste stap is controleren',
|
|
c.wizardSteps()[c.install.step].id === 'controleren');
|
|
|
|
// ── Alles op één pagina ─────────────────────────────────────────────────────
|
|
c.onePage = true;
|
|
check('wizard uit bij één pagina', !c.wizardActief());
|
|
|
|
// ── Groepskoppen ────────────────────────────────────────────────────────────
|
|
c.onePage = false;
|
|
c.install.fields = [veld('a','basis'), veld('b','basis')];
|
|
check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false);
|
|
c.install.fields.push(veld('c','basis',{group:'Tweede'}));
|
|
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
|
|
|
|
// ── Sleutelformaten ─────────────────────────────────────────────────────────
|
|
// Een willekeurige string is niet genoeg: Laravel wil `base64:` ervoor, Homarr
|
|
// en LibreChat willen hexadecimaal van een vaste lengte. In het verkeerde
|
|
// formaat start de container niet, met een foutmelding die nergens naar de
|
|
// sleutel wijst.
|
|
const standaard = c.veldSecret({});
|
|
check('standaard is url-veilig', /^[A-Za-z0-9_-]+$/.test(standaard));
|
|
check('standaard is 32 tekens', standaard.length === 32);
|
|
|
|
const hex = c.veldSecret({secret_format: 'hex', secret_bytes: 32});
|
|
check('hex is 64 tekens', hex.length === 64);
|
|
check('hex bevat alleen hexcijfers', /^[0-9a-f]{64}$/.test(hex));
|
|
check('hex van 16 bytes is 32 tekens',
|
|
c.veldSecret({secret_format: 'hex', secret_bytes: 16}).length === 32);
|
|
|
|
const lar = c.veldSecret({secret_format: 'laravel', secret_bytes: 32});
|
|
check('laravel begint met base64:', lar.startsWith('base64:'));
|
|
check('laravel is standaard-base64 van 32 bytes',
|
|
Buffer.from(lar.slice(7), 'base64').length === 32);
|
|
check('twee sleutels zijn niet gelijk', c.veldSecret({}) !== c.veldSecret({}));
|
|
|
|
// ── Stap Onderdelen ─────────────────────────────────────────────────────────
|
|
// Een stack als de ARR-stack heeft vijftig schakelbare onderdelen. Die horen in
|
|
// één eigen stap, gegroepeerd per kopje — en nergens anders nóg een keer.
|
|
function schakelaar(naam, sectie, aan = false) {
|
|
return veld('enable_' + naam, 'onderdelen', {
|
|
type: 'bool', is_group_toggle: true, group_toggle: 'enable_' + naam,
|
|
group: naam, section: sectie, default: aan});
|
|
}
|
|
c.install = {format:'boilerplate', loading:false, values:{}, step:0, ips:{}, services:[],
|
|
fields:[
|
|
veld('service_name','basis'),
|
|
schakelaar('sonarr','Bibliotheken'), schakelaar('radarr','Bibliotheken'),
|
|
schakelaar('prowlarr','Indexers'),
|
|
veld('port_sonarr','instellingen',{type:'int', group:'Sonarr',
|
|
group_toggle:'enable_sonarr', advanced:true}),
|
|
veld('port_radarr','instellingen',{type:'int', group:'Radarr',
|
|
group_toggle:'enable_radarr', advanced:true}),
|
|
]};
|
|
c.showAdvanced = true;
|
|
s = c.wizardSteps().map(x => x.id);
|
|
check('onderdelen is de eerste stap', s[0] === 'onderdelen');
|
|
check('drie schakelaars in de stap', c.stepFields('onderdelen').length === 3);
|
|
|
|
const secties = c.stepSections('onderdelen');
|
|
check('twee kopjes', secties.length === 2);
|
|
check('kopjes kloppen',
|
|
secties.map(x => x.name).join() === 'Bibliotheken,Indexers');
|
|
check('nog niets gekozen', c.gekozenOnderdelen() === 0);
|
|
|
|
c.zetSectie(secties[0], true);
|
|
check('sectie in één keer aan', c.gekozenOnderdelen() === 2);
|
|
c.zetSectie(secties[0], false);
|
|
check('en weer uit', c.gekozenOnderdelen() === 0);
|
|
|
|
// De schakelaar hoorde nergens anders op te duiken. Stond hij ook in de kop van
|
|
// zijn eigen groep, dan zag je hem twee keer in twee verschillende stappen.
|
|
c.install.values.enable_sonarr = true;
|
|
const groepen = c.stepGroups('instellingen');
|
|
check('alleen ingeschakelde onderdelen krijgen een kaart', groepen.length === 1);
|
|
check('kaart is van Sonarr', groepen[0].name === 'Sonarr');
|
|
check('geen tweede schakelaar in de groepskop', groepen[0].toggle === null);
|
|
|
|
// ── Eigen IP per container ──────────────────────────────────────────────────
|
|
check('zonder voorvertoning geen containers', c.ipContainers().length === 0);
|
|
c.install.services = ['arr-sonarr', 'arr-radarr'];
|
|
check('containers komen uit de voorvertoning', c.ipContainers().length === 2);
|
|
c.zetContainerIp('arr-sonarr', true);
|
|
check('container aangezet', 'arr-sonarr' in c.install.ips);
|
|
check('andere container blijft op de host', !('arr-radarr' in c.install.ips));
|
|
c.install.ips['arr-sonarr'] = '192.168.1.240';
|
|
c.zetContainerIp('arr-sonarr', false);
|
|
check('en weer uitgezet', !('arr-sonarr' in c.install.ips));
|
|
|
|
console.log(fouten === 0 ? '\nAlle menucontroles geslaagd' : `\n${fouten} FOUTEN`);
|
|
process.exit(fouten ? 1 : 0);
|