Some checks failed
Deploy server-up (dev) / deploy (push) Failing after 16m45s
De app store toonde geen kaarten met daaronder een knop die 106 apps beloofde. Het raster liep over de repolijst, de tellers over de opgehaalde apps: liepen die uit elkaar, dan stond er een getal onder een leeg scherm zonder dat er ergens een fout te zien was. - storeResultaat en storeVerborgen tellen nu over dezelfde lijst die ook de kaarten oplevert - een lege store zegt wat er aan de hand is (geen repository, of geen apps die aan het filter voldoen) in plaats van leeg te blijven - GET /api/repos gaf een 500 op een repo zonder id, waardoor de repolijst in de interface leeg werd en dus de hele store; zulke regels worden overgeslagen Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
488 lines
25 KiB
JavaScript
488 lines
25 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();
|
|
// De echte t() vult {plaatshouders} in. Dat nabootsen scheelt niet alleen
|
|
// realisme: zonder invulling kun je niet toetsen of de containernaam in de
|
|
// melding belandt.
|
|
c.t = (k, v) => v ? k + ':' + Object.values(v).join(',') : k;
|
|
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));
|
|
|
|
// ── Poortveld ───────────────────────────────────────────────────────────────
|
|
// De backend kon al controleren of een poort vrij is, maar het formulier vroeg
|
|
// het nooit. En je kon alleen typen — geen manier om er snel eentje bij te doen.
|
|
check('een poortveld herkennen', c.isPoortVeld({name: 'port'})
|
|
&& c.isPoortVeld({name: 'port_ui'}) && c.isPoortVeld({name: 'poort'}));
|
|
check('puid is geen poort', !c.isPoortVeld({name: 'puid'})
|
|
&& !c.isPoortVeld({name: 'pgid'}));
|
|
|
|
c.install = {format:'boilerplate', loading:false, fields:[], values:{port: 8080}, step:0};
|
|
c.poortCheck = {};
|
|
let gevraagd = [];
|
|
c.rpc = async (u) => {
|
|
gevraagd.push(u);
|
|
const p = Number(new URL('http://x' + u).searchParams.get('port'));
|
|
return {ok:true, port:p, free:p !== 8080, suggested:8081,
|
|
used_by:p === 8080 ? 'nginx' : '', host_checked:true};
|
|
};
|
|
|
|
c.stapPoort({name:'port'}, 1);
|
|
check('plus verhoogt', c.install.values.port === 8081);
|
|
c.stapPoort({name:'port'}, -1);
|
|
check('min verlaagt', c.install.values.port === 8080);
|
|
c.install.values.port = 1;
|
|
c.stapPoort({name:'port'}, -1);
|
|
check('niet onder 1', c.install.values.port === 1);
|
|
c.install.values.port = 65535;
|
|
c.stapPoort({name:'port'}, 1);
|
|
check('niet boven 65535', c.install.values.port === 65535);
|
|
|
|
// ── Leesbare namen ──────────────────────────────────────────────────────────
|
|
// Het auditlog bewaart ruwe id's; die horen niet in beeld te staan.
|
|
c.T = {audit_cat_store: 'App Store', audit_act_reconfigure: 'Opnieuw ingesteld'};
|
|
check('categorie wordt vertaald', c.auditNaam('cat', 'store') === 'App Store');
|
|
check('actie wordt vertaald', c.auditNaam('act', 'reconfigure') === 'Opnieuw ingesteld');
|
|
check('onbekende waarde blijft leesbaar zoals hij is',
|
|
c.auditNaam('cat', 'eigenmodule') === 'eigenmodule');
|
|
check('lege waarde geeft niets', c.auditNaam('act', '') === '');
|
|
|
|
c.modules = [{id: 'mijnmodule', name: 'Mijn Module'}];
|
|
c.page = 'mod:mijnmodule';
|
|
check('modulepagina toont de naam, niet mod:id', c.paginaTitel === 'Mijn Module');
|
|
c.page = 'mod:onbekend';
|
|
check('onbekende module valt terug op de id zonder voorvoegsel',
|
|
c.paginaTitel === 'onbekend');
|
|
c.T = {}; c.page = 'stacks';
|
|
|
|
// ── Menu per stack op de telefoon ───────────────────────────────────────────
|
|
const acties = c.stackMenuItems({name: 's', editable: false, connected: true});
|
|
check('het menu heeft alle acties', acties.length === 9);
|
|
check('elke actie heeft een naam', acties.every(a => a.label && a.label.length));
|
|
check('instellingen uit als de stack niet bewerkbaar is',
|
|
acties.find(a => a.id === 'settings').uit === true);
|
|
check('koppelen uit als hij al gekoppeld is',
|
|
acties.find(a => a.id === 'connect').uit === true);
|
|
check('verwijderen is als gevaarlijk gemarkeerd',
|
|
acties.find(a => a.id === 'remove').gevaar === true);
|
|
|
|
// ── Vanuit de store naar een geïnstalleerde stack ───────────────────────────
|
|
c.stackQuery = 'iets anders'; c.stackCat = 'media'; c.page = 'app_store';
|
|
c.goto = (p) => { c.page = p; };
|
|
c.gaNaarStack('mijn-sonarr');
|
|
check('doorklikken zet de zoekterm', c.stackQuery === 'mijn-sonarr');
|
|
check('doorklikken wist het categoriefilter', c.stackCat === '');
|
|
check('doorklikken opent de stackspagina', c.page === 'stacks');
|
|
|
|
// ── Filtermenu van de app store ─────────────────────────────────────────────
|
|
// De labelrij was afgekapt op veertien, waardoor je de rest niet kon kiezen. Het
|
|
// menu zoekt nu in de volledige lijst.
|
|
// De teller volgt de repolijst, want die bepaalt ook welke kaarten er komen.
|
|
c.repos = [{id:'r', name:'Proefrepo'}];
|
|
c.storeStacks = { r: [
|
|
{dir:'sonarr', name:'Sonarr', categories:['media'], tags:['media','series']},
|
|
{dir:'radarr', name:'Radarr', categories:['media'], tags:['media','films']},
|
|
{dir:'loki', name:'Loki', categories:['monitoring'], tags:['logs']},
|
|
{dir:'zeldzaam',name:'Zeldzaam',categories:['overig'], tags:['heelzeldzaam']},
|
|
]};
|
|
c.categories = [
|
|
{id:'media', name:'Media', color:'#8b5cf6', icon:'mdi-play'},
|
|
{id:'monitoring', name:'Monitoring', color:'#f97316', icon:'mdi-chart-line'},
|
|
{id:'overig', name:'Overig', color:'#94a3b8', icon:'mdi-shape'},
|
|
];
|
|
c.storeQuery = ''; c.storeCat = ''; c.storeTag = ''; c.filterTagQuery = '';
|
|
|
|
check('geen filter: alles telt mee', c.storeResultaat === 4);
|
|
check('geen actieve filters', c.filterCount === 0);
|
|
check('labels hebben een aantal',
|
|
c.storeTags[0].tag === 'media' && c.storeTags[0].count === 2);
|
|
check('alle labels blijven beschikbaar', c.storeTags.length === 5);
|
|
|
|
// De cache: dezelfde filterstand hoort niet opnieuw gerekend te worden. Dit
|
|
// werd per repo drie keer per tick aangeroepen, met een toLowerCase() per veld
|
|
// per app — bij elke toetsaanslag.
|
|
const eerste = c.filteredStore('r');
|
|
check('zelfde filterstand geeft hetzelfde resultaat', c.filteredStore('r') === eerste);
|
|
c.storeQuery = 'son';
|
|
check('andere zoekterm geeft een nieuwe lijst', c.filteredStore('r') !== eerste);
|
|
check('en die klopt', c.filteredStore('r').length === 1);
|
|
c.storeQuery = '';
|
|
|
|
// De limiet: niet alles tegelijk in de DOM.
|
|
c.storeLimiet = 2;
|
|
check('niet meer dan de limiet', c.zichtbareStore('r').length === 2);
|
|
check('de rest wacht achter de knop', c.storeVerborgen === 2);
|
|
c.meerTonen();
|
|
check('meer tonen verhoogt de limiet', c.storeLimiet === 62);
|
|
check('en dan staat alles er', c.zichtbareStore('r').length === 4);
|
|
check('niets meer verborgen', c.storeVerborgen === 0);
|
|
|
|
c.storeLimiet = 2;
|
|
c.wisFilters();
|
|
check('filters wissen zet de limiet terug', c.storeLimiet === 60);
|
|
|
|
c.storeCat = 'media';
|
|
check('categorie filtert', c.storeResultaat === 2);
|
|
check('teller telt de categorie', c.filterCount === 1);
|
|
c.storeTag = 'films';
|
|
check('categorie en label samen', c.storeResultaat === 1);
|
|
check('teller telt allebei', c.filterCount === 2);
|
|
|
|
c.wisFilters();
|
|
check('wissen zet alles terug', c.storeResultaat === 4 && c.filterCount === 0);
|
|
|
|
c.filterTagQuery = 'zeld';
|
|
check('zoeken vindt een zeldzaam label',
|
|
c.zichtbareTags.length === 1 && c.zichtbareTags[0].tag === 'heelzeldzaam');
|
|
check('bij zoeken niets verborgen', c.verborgenTags === 0);
|
|
c.filterTagQuery = 'bestaatniet';
|
|
check('geen treffers geeft een lege lijst', c.zichtbareTags.length === 0);
|
|
|
|
// Een gekozen label mag niet uit beeld vallen omdat het zeldzaam is.
|
|
c.filterTagQuery = '';
|
|
c.storeTag = 'heelzeldzaam';
|
|
check('gekozen label blijft zichtbaar',
|
|
c.zichtbareTags.some(t => t.tag === 'heelzeldzaam'));
|
|
|
|
// ── Afhankelijkheden ────────────────────────────────────────────────────────
|
|
// De melding hoort alleen te verschijnen voor wat er níét draait, en te
|
|
// verdwijnen zodra het er wel is.
|
|
c.install = {format:'boilerplate', loading:false, fields:[], values:{}, step:0,
|
|
depends_on: [
|
|
{app:'mosquitto', reason:'MQTT-broker', installed:false},
|
|
{app:'loki', reason:'logopslag', installed:true, stacks:['loki']},
|
|
]};
|
|
check('alleen het ontbrekende wordt gemeld', c.ontbrekendeAfhankelijkheden().length === 1);
|
|
check('en dat is mosquitto', c.ontbrekendeAfhankelijkheden()[0].app === 'mosquitto');
|
|
c.install.depends_on[0].installed = true;
|
|
check('niets meer te melden zodra alles draait',
|
|
c.ontbrekendeAfhankelijkheden().length === 0);
|
|
c.install.depends_on = [];
|
|
check('een app zonder afhankelijkheden meldt niets',
|
|
c.ontbrekendeAfhankelijkheden().length === 0);
|
|
|
|
// ── 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));
|
|
|
|
// ── Mappen ──────────────────────────────────────────────────────────────────
|
|
// De poortcontroles hieronder hebben hun eigen antwoord nodig; even bewaren.
|
|
const poortRpc = c.rpc;
|
|
c.settings = { LIBRARY_DIR: '/opt/serverup/stacks', DATA_DIR: '/opt/serverup/appdata',
|
|
BACKUP_DIR: '/opt/serverup/backups' };
|
|
c.padenVoorWijziging = Object.assign({}, c.settings);
|
|
c.autoSave = () => {};
|
|
check('niets gewijzigd, niets te verplaatsen', c.teVerplaatsen.length === 0);
|
|
|
|
c.afgeleidePaden = { LIBRARY_DIR: '/srv/serverup/stacks',
|
|
DATA_DIR: '/srv/serverup/appdata',
|
|
BACKUP_DIR: '/srv/serverup/backups' };
|
|
c.neemAllesOver();
|
|
check('alles overnemen vult de drie velden', c.settings.DATA_DIR === '/srv/serverup/appdata');
|
|
check('en meldt drie verhuizingen', c.teVerplaatsen.length === 3);
|
|
check('met oud én nieuw erbij',
|
|
c.teVerplaatsen[0].oud === '/opt/serverup/stacks' &&
|
|
c.teVerplaatsen[0].nieuw === '/srv/serverup/stacks');
|
|
|
|
// De voortgangsbalk leest rechtstreeks uit de job; zonder voortgang blijft hij weg.
|
|
c.job = { status: 'running', lines: [], progress: null };
|
|
check('geen balk zonder voortgang', !c.job.progress);
|
|
c.job.progress = { done: 512, total: 1024, label: 'sonarr/config.xml', pct: 50 };
|
|
check('balk staat halverwege', c.job.progress.pct === 50);
|
|
|
|
// De poortcontrole praat met de backend, dus die checks zijn asynchroon.
|
|
// Top-level await kan niet in een CommonJS-script; vandaar deze afsluiting.
|
|
(async () => {
|
|
// ── De hoofdmap ───────────────────────────────────────────────────────────
|
|
let gevraagdPad = null;
|
|
c.rpc = async (url) => {
|
|
gevraagdPad = url;
|
|
return { ok: true, afgeleid: { LIBRARY_DIR: '/srv/su/stacks',
|
|
DATA_DIR: '/srv/su/appdata',
|
|
BACKUP_DIR: '/srv/su/backups' } };
|
|
};
|
|
c.hoofdmap = 'srv/su';
|
|
await c.checkHoofdmap();
|
|
check('een relatief pad gaat niet naar de server', gevraagdPad === null);
|
|
check('en levert geen voorstellen op', Object.keys(c.afgeleidePaden).length === 0);
|
|
|
|
c.hoofdmap = '/srv/su';
|
|
await c.checkHoofdmap();
|
|
check('een volledig pad levert drie submappen op',
|
|
c.afgeleidePaden.DATA_DIR === '/srv/su/appdata');
|
|
check('zonder klacht', !c.padCheck.reden);
|
|
|
|
// Buiten de gemounte map schrijft de container naar zijn eigen overlay; dat
|
|
// moet je zien staan vóór je verhuist, niet erna.
|
|
c.rpc = async () => ({ ok: false, reden: 'buiten_mount', afgeleid: {},
|
|
msg: 'Zet BASE_DIR=/srv in je .env' });
|
|
await c.checkHoofdmap();
|
|
check('een onbereikbaar pad wordt gemeld', c.padCheck.reden === 'buiten_mount');
|
|
check('met de regel voor .env erbij', c.padCheck.msg.includes('BASE_DIR=/srv'));
|
|
|
|
// Verhuizen zonder wijziging hoort geen job te starten.
|
|
let verstuurd = null;
|
|
global.confirm = () => true;
|
|
c.rpc = async (url, body) => { verstuurd = body; return { job_id: 'x1' }; };
|
|
c.pollJob = () => {};
|
|
c.padenVoorWijziging = Object.assign({}, c.settings);
|
|
await c.verplaatsPaden();
|
|
check('geen verhuizing zonder wijziging', verstuurd === null);
|
|
|
|
c.settings.DATA_DIR = '/srv/su/appdata';
|
|
await c.verplaatsPaden();
|
|
check('alleen de gewijzigde map wordt verstuurd',
|
|
verstuurd && Object.keys(verstuurd).join() === 'DATA_DIR');
|
|
check('met het nieuwe pad', verstuurd.DATA_DIR === '/srv/su/appdata');
|
|
|
|
// Afbreken bij de bevestiging hoort niets te doen: dit gooit data weg.
|
|
verstuurd = null;
|
|
global.confirm = () => false;
|
|
await c.verplaatsPaden();
|
|
check('afbreken bij de bevestiging start niets', verstuurd === null);
|
|
|
|
// ── De store telt wat hij ook laat zien ───────────────────────────────────
|
|
// Het raster liep over `repos`, de teller over `storeStacks`. Liepen die twee
|
|
// uit elkaar, dan stond er "Nog 106 tonen" onder een leeg scherm — zonder dat
|
|
// er ergens een fout te zien was.
|
|
const nepApps = n => Array.from({length: n},
|
|
(_, i) => ({dir: 'a' + i, name: 'App' + i, categories: [], tags: []}));
|
|
const kaarten = () => c.repos.reduce((n, r) => n + c.zichtbareStore(r.id).length, 0);
|
|
|
|
c._storeCache = {sleutel: null, lijsten: {}};
|
|
c.storeQuery = ''; c.storeCat = ''; c.storeTag = ''; c.storeLimiet = 60;
|
|
c.repos = [{id: 'server-up', name: 'Server Up'}];
|
|
c.storeStacks = {'server-up': nepApps(106)};
|
|
check('de eerste zestig staan in beeld', kaarten() === 60);
|
|
check('en de rest wacht achter de knop', c.storeVerborgen === 46);
|
|
c.meerTonen();
|
|
c._storeCache = {sleutel: null, lijsten: {}};
|
|
check('meer tonen laat de rest zien', kaarten() === 106);
|
|
check('en de knop verdwijnt', c.storeVerborgen === 0);
|
|
|
|
// Zonder repo's is er niets te tonen — en dus ook niets te beloven.
|
|
c._storeCache = {sleutel: null, lijsten: {}};
|
|
c.storeLimiet = 60;
|
|
c.repos = [];
|
|
check('geen repo\'s: geen kaarten', kaarten() === 0);
|
|
check('en geen knop die apps belooft', c.storeVerborgen === 0);
|
|
check('de teller is ook nul', c.storeResultaat === 0);
|
|
|
|
c.rpc = poortRpc;
|
|
|
|
// ── Eén lijst voor beheerde en handmatig gestarte apps ────────────────────
|
|
// Ze stonden in twee lijsten onder elkaar, waarvan de tweede een kop, een
|
|
// uitleg en een eigen vormgeving had. Wie zijn apps kwijt was, moest weten
|
|
// dat er een tweede lijst bestond.
|
|
c.stacks = [{name: 'sonarr', categories: ['media']},
|
|
{name: 'vaultwarden', categories: ['tools']}];
|
|
c.hostContainers = {containers: [
|
|
{id: '1', name: 'oude-nginx', image: 'nginx:alpine', managed: false},
|
|
{id: '2', name: 'sonarr', image: 'lscr.io/sonarr', managed: true},
|
|
]};
|
|
c.stackQuery = ''; c.stackCat = '';
|
|
check('beheerde apps staan in de lijst', c.filteredStacks.length === 2);
|
|
check('handmatig gestarte apps ook', c.filteredLos.length === 1);
|
|
check('en niet dubbel', c.filteredLos[0].name === 'oude-nginx');
|
|
|
|
c.stackQuery = 'nginx';
|
|
check('de zoekterm geldt voor allebei',
|
|
c.filteredStacks.length === 0 && c.filteredLos.length === 1);
|
|
c.stackQuery = 'sonarr';
|
|
check('een beheerde app blijft over', c.filteredStacks.length === 1);
|
|
check('en de handmatige valt af', c.filteredLos.length === 0);
|
|
|
|
c.stackQuery = 'lscr';
|
|
check('zoeken kan ook op image', c.filteredLos.length === 0);
|
|
c.stackQuery = 'alpine';
|
|
check('van een handmatig gestarte app', c.filteredLos.length === 1);
|
|
|
|
// Een categoriefilter hoort ze te laten vallen: ze hébben geen categorie.
|
|
c.stackQuery = ''; c.stackCat = 'media';
|
|
check('categoriefilter laat handmatige apps vallen', c.filteredLos.length === 0);
|
|
check('en houdt de beheerde over', c.filteredStacks.length === 1);
|
|
|
|
// Een leeg of onzinnig veld hoort geen verzoek te sturen.
|
|
c.install.values.port = 0;
|
|
gevraagd = [];
|
|
await c.checkPoort({name:'port'});
|
|
check('geen controle bij een lege poort', gevraagd.length === 0);
|
|
check('en geen status', !c.poortCheck.port);
|
|
|
|
c.install.values.port = 8080;
|
|
await c.checkPoort({name:'port'});
|
|
check('bezette poort wordt gemeld', c.poortCheck.port.free === false);
|
|
check('met de container erbij',
|
|
c.poortStatus({name:'port'}).includes('nginx'));
|
|
check('en een voorstel', c.poortCheck.port.suggested === 8081);
|
|
|
|
c.install.values.port = 8081;
|
|
await c.checkPoort({name:'port'});
|
|
check('vrije poort wordt gemeld', c.poortCheck.port.free === true);
|
|
|
|
// Zonder de hulpcontainer is "vrij" een aanname; dat hoort de melding te zeggen.
|
|
c.rpc = async () => ({ok:true, port:8081, free:true, suggested:8081,
|
|
used_by:'', host_checked:false});
|
|
await c.checkPoort({name:'port'});
|
|
check('onzekerheid wordt benoemd',
|
|
c.poortStatus({name:'port'}) !== c.t('port_free'));
|
|
|
|
console.log(fouten === 0 ? '\nAlle menucontroles geslaagd' : `\n${fouten} FOUTEN`);
|
|
process.exit(fouten ? 1 : 0);
|
|
})();
|