import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import esbuild from 'esbuild' import { loadModule } from './load.mjs' // Stufen-Sperre der Hustler-Seite (src/lib/hustlerStufe.js): eine Quelle der Wahrheit, was die Oberfläche schon zeigen darf. // Stufe 3 = Dienst P3 live (Stand der Produktion): kein Keller (Bot-Leads, P6), keine Antwort-Knöpfe im Pult (P4). Ab Stufe 4 kommen die Antwort-Knöpfe, ab 6 der Keller. // Diese Datei prüft das Modul selbst (Stufe aus dem Bau, Schwellen, Bildschirme, Links). Die Bausteine prüfen ihre Sperre in ihren eigenen Tests, jeweils mit Stufe 3 (gesperrt) // und Stufe 6 (offen): rowModel in workersPult.test.mjs, der Client in workersApi.test.mjs, Pult in workersPultUi.test.mjs, Haus in hustlerHochhausWelt.test.mjs, Seite in // hustlerSeite.test.mjs, Spur-Links in hustlerDialog.test.mjs, der Prüfstand in workersHarness.test.mjs. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8') const stufe = await loadModule('src/lib/hustlerStufe.js') const { HUSTLER_STUFE, STUFE_ANTWORTEN, STUFE_KELLER, stufeAus, antwortenOffen, kellerOffen, tabErlaubt, linkErlaubt } = stufe // ---------- Stufe aus dem Bau ---------- test('stufeAus: nur eine ganze Zahl 1..9 gilt (Zahl oder einzelne Ziffer im Text, Leerraum außen egal), alles andere ergibt den Standard', () => { for (let n = 1; n <= 9; n += 1) { assert.equal(stufeAus(n, 3), n); assert.equal(stufeAus(String(n), 3), n); assert.equal(stufeAus(` ${n}\n`, 3), n) } const ungueltig = [undefined, null, '', ' ', '0', 0, '10', 10, '12', '06', '6.0', '6.5', 6.5, '-1', -1, '+6', 'sechs', 'abc', '6a', 'a6', '6 7', NaN, Infinity, -Infinity, true, false, {}, [], [6], () => 6, '1', '٦'] for (const roh of ungueltig) assert.equal(stufeAus(roh, 3), 3, `Wert ${String(roh)} (${typeof roh})`) // der Standard ist der übergebene assert.equal(stufeAus('x', 5), 5); assert.equal(stufeAus(undefined, 7), 7) }) test('Standard: ohne Wert aus dem Bau gilt die Stufe aus dem Quelltext, einmal und an einer Stelle; sie liegt in 1..9', () => { const quelltext = read('src/lib/hustlerStufe.js') const treffer = [...quelltext.matchAll(/export const HUSTLER_STUFE = stufeAus\(buildWert\(\), (\d)\)/g)] assert.equal(treffer.length, 1, 'genau eine Zeile legt die Stufe fest') assert.equal(HUSTLER_STUFE, Number(treffer[0][1])) assert.ok(Number.isInteger(HUSTLER_STUFE) && HUSTLER_STUFE >= 1 && HUSTLER_STUFE <= 9) assert.ok(quelltext.includes('beim jeweiligen Livegang hier hochsetzen')) assert.equal(STUFE_ANTWORTEN, 4); assert.equal(STUFE_KELLER, 6) }) // Der Wert aus dem Bau (Vite: import.meta.env.VITE_HUSTLER_STUFE, Prüfstand: define von esbuild) wird beim Bauen eingesetzt. Hier derselbe Weg mit esbuild. async function stufeNachBau(define) { const out = await esbuild.build({ stdin: { contents: "export { HUSTLER_STUFE, antwortenOffen, kellerOffen } from './src/lib/hustlerStufe.js'", resolveDir: ROOT, loader: 'js' }, bundle: true, format: 'esm', platform: 'neutral', write: false, define, }) return import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`) } test('Bau: VITE_HUSTLER_STUFE überschreibt die Stufe zur Bauzeit (Prüfstand); ungültige Werte fallen auf den Standard zurück', async () => { for (const [roh, erwartet] of [['6', 6], ['4', 4], ['9', 9], ['1', 1], [' 6 ', 6], ['3', 3], ['0', HUSTLER_STUFE], ['10', HUSTLER_STUFE], ['abc', HUSTLER_STUFE], ['', HUSTLER_STUFE], ['6.5', HUSTLER_STUFE], ['-2', HUSTLER_STUFE]]) { const m = await stufeNachBau({ 'import.meta.env.VITE_HUSTLER_STUFE': JSON.stringify(roh) }) assert.equal(m.HUSTLER_STUFE, erwartet, `VITE_HUSTLER_STUFE=${JSON.stringify(roh)}`) assert.equal(m.kellerOffen(), erwartet >= 6); assert.equal(m.antwortenOffen(), erwartet >= 4) } // nicht gesetzt (Vite: undefined) und ohne import.meta.env (Node): der Standard assert.equal((await stufeNachBau({ 'import.meta.env.VITE_HUSTLER_STUFE': 'undefined' })).HUSTLER_STUFE, HUSTLER_STUFE) assert.equal((await stufeNachBau({})).HUSTLER_STUFE, HUSTLER_STUFE) }) test('keine Überschreibung zur Laufzeit: kein localStorage, keine Adresse, keine Umgebung außer dem Bau; VITE_HUSTLER_STUFE steht nur in hustlerStufe.js', () => { const quelltext = read('src/lib/hustlerStufe.js').split('\n').filter((z) => !z.trimStart().startsWith('//')).join('\n') for (const verboten of ['localStorage', 'sessionStorage', 'location', 'URLSearchParams', 'document', 'window', 'process.env', 'cookie']) assert.ok(!quelltext.includes(verboten), verboten) // genau ein Zugriff auf die Umgebung assert.equal(quelltext.match(/import\.meta\.env/g).length, 1) const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)])) const quellen = walk(path.join(ROOT, 'src')).filter((f) => /\.(jsx?|mjs)$/.test(f) && !f.endsWith(path.join('lib', 'hustlerStufe.js'))) assert.ok(quellen.length > 50) for (const f of quellen) assert.ok(!fs.readFileSync(f, 'utf8').includes('VITE_HUSTLER_STUFE'), path.relative(ROOT, f)) }) // ---------- Schwellen ---------- test('Schwellen: Antwort-Knöpfe ab Stufe 4, Keller ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => { for (let n = 1; n <= 9; n += 1) { assert.equal(antwortenOffen(n), n >= 4, `antwortenOffen(${n})`) assert.equal(kellerOffen(n), n >= 6, `kellerOffen(${n})`) } assert.deepEqual([antwortenOffen(3), kellerOffen(3)], [false, false]) assert.deepEqual([antwortenOffen(6), kellerOffen(6)], [true, true]) for (const kaputt of [undefined, null, 'x', 0, 12, NaN]) { assert.equal(antwortenOffen(kaputt), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerOffen(kaputt), kellerOffen(HUSTLER_STUFE)) } assert.equal(antwortenOffen(), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerOffen(), kellerOffen(HUSTLER_STUFE)) }) // ---------- Bildschirme und Deeplinks ---------- test('tabErlaubt: nur der Keller (botleads) ist gesperrt, und nur unterhalb von Stufe 6', () => { for (const s of [1, 3, 4, 5]) { assert.equal(tabErlaubt('botleads', s), false, `Stufe ${s}`) for (const frei of ['hochhaus', 'freigabe', '', 'unbekannt', undefined]) assert.equal(tabErlaubt(frei, s), true, `${frei} auf Stufe ${s}`) } for (const s of [6, 7, 9]) assert.equal(tabErlaubt('botleads', s), true, `Stufe ${s}`) }) test('linkErlaubt: Links in den Keller sind unterhalb von Stufe 6 kein Link, alles andere bleibt unberührt', () => { const keller = [ '/hustler?tab=botleads', '/hustler?tab=botleads&bot=abc123', '/hustler/?tab=botleads', '/Hustler?tab=botleads', '/hustler?bot=x&tab=botleads', '/workers?tab=botleads&bot=x', '/leads?tab=botleads', '/hustler?tab=botleads#oben', ] for (const href of keller) { assert.equal(linkErlaubt(href, 3), false, `Stufe 3: ${href}`); assert.equal(linkErlaubt(href, 5), false, `Stufe 5: ${href}`) assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${href}`) } const frei = [ '/hustler?tab=freigabe', '/hustler', '/hustler?tab=hochhaus', '/hustler?bot=abc123', '/tickets/100001', '/customers/abc', '/other?tab=botleads', '/hustler/anderes?tab=botleads', 'https://beispiel.de/hustler?tab=botleads', 'http://x.example/?tab=botleads', '//evil.example/hustler?tab=botleads', '/\\evil.example/hustler?tab=botleads', '', 'kein-pfad', null, undefined, 42, ] for (const href of frei) { assert.equal(linkErlaubt(href, 3), true, `Stufe 3: ${String(href)}`); assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${String(href)}`) } })