Files
tickte-system/tests/unit/hustlerStufe.test.mjs
T
ClaudeandClaude Sonnet 5.5 7cd74a23d2 Hustler-Stufe: Modul hustlerStufe.js mit HUSTLER_STUFE (Standard 3), Bauzeit-Wert VITE_HUSTLER_STUFE, Schwellen für Pult-Antworten (4) und Keller (6)
Eine Quelle der Wahrheit dafür, was die Oberfläche schon zeigen darf, solange der Dienst nur P1 und P3 kann. Der Wert aus dem Bau gilt nur als Ziffer 1 bis 9,
sonst die Standardstufe; keine Überschreibung zur Laufzeit. Dazu tabErlaubt (Keller = ?tab=botleads) und linkErlaubt (Links in den Keller, auch über /workers und /leads).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 10:36:44 +00:00

113 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)}`) }
})