Files
tickte-system/tests/unit/hustlerStufe.test.mjs
ClaudeandClaude Sonnet 5.5 6eb4dd3c51 Hustler-Stufe: Keller begehbar ab Stufe 3, Bot-Leads abrufbar erst ab Stufe 6
Die Stufen-Sperre trennt jetzt zwei Dinge: KELLER_BEGEHBAR_AB = 3 (Keller-Etage als Link,
?tab=botleads gueltig, Spur-Links in den Keller) und BOTLEADS_AB = 6 (Abruf von GET /botleads).
kellerOffen/STUFE_KELLER entfallen, dafuer kellerBegehbar() und botleadsAbrufbar().
workersApi.botleads bleibt unter Stufe 6 hart gesperrt, der Text heisst nun
BOTLEADS_GESPERRT_TEXT. Die Keller-Ansicht selbst (leeres Register ohne Abruf) folgt im
naechsten Commit.

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

121 lines
8.7 KiB
JavaScript
Raw Permalink 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): der Keller ist begehbar (Etage, Bildschirm, Spur-Links), aber leer und ohne Abruf der Bot-Leads (P6), und es gibt keine
// Antwort-Knöpfe im Pult (P4). Ab Stufe 4 kommen die Antwort-Knöpfe, ab 6 liefert der Keller die Bot-Leads.
// 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 (Keller leer)
// und Stufe 6 (Live-Daten), dazu 1 (Keller zu): 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, Keller in botleadList.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, KELLER_BEGEHBAR_AB, BOTLEADS_AB, stufeAus, antwortenOffen, kellerBegehbar, botleadsAbrufbar, 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(KELLER_BEGEHBAR_AB, 3); assert.equal(BOTLEADS_AB, 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, kellerBegehbar, botleadsAbrufbar } 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.kellerBegehbar(), erwartet >= 3); assert.equal(m.botleadsAbrufbar(), 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: Keller begehbar ab Stufe 3, Antwort-Knöpfe ab Stufe 4, Bot-Leads abrufbar ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => {
for (let n = 1; n <= 9; n += 1) {
assert.equal(kellerBegehbar(n), n >= 3, `kellerBegehbar(${n})`)
assert.equal(antwortenOffen(n), n >= 4, `antwortenOffen(${n})`)
assert.equal(botleadsAbrufbar(n), n >= 6, `botleadsAbrufbar(${n})`)
}
// Stufe 3 (Produktion): Keller begehbar, aber weder Antwort-Knöpfe noch Abruf der Bot-Leads; der Abruf setzt die Begehbarkeit voraus
assert.deepEqual([kellerBegehbar(3), antwortenOffen(3), botleadsAbrufbar(3)], [true, false, false])
assert.deepEqual([kellerBegehbar(2), antwortenOffen(2), botleadsAbrufbar(2)], [false, false, false])
assert.deepEqual([kellerBegehbar(6), antwortenOffen(6), botleadsAbrufbar(6)], [true, true, true])
assert.ok(KELLER_BEGEHBAR_AB <= BOTLEADS_AB)
for (const kaputt of [undefined, null, 'x', 0, 12, NaN]) {
assert.equal(antwortenOffen(kaputt), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerBegehbar(kaputt), kellerBegehbar(HUSTLER_STUFE)); assert.equal(botleadsAbrufbar(kaputt), botleadsAbrufbar(HUSTLER_STUFE))
}
assert.equal(antwortenOffen(), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerBegehbar(), kellerBegehbar(HUSTLER_STUFE)); assert.equal(botleadsAbrufbar(), botleadsAbrufbar(HUSTLER_STUFE))
})
// ---------- Bildschirme und Deeplinks ----------
test('tabErlaubt: nur der Keller (botleads) hängt an der Stufe, und nur unterhalb von Stufe 3 ist er gesperrt (ab 3 begehbar, auch ohne Bot-Leads)', () => {
for (const s of [1, 2]) {
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 [3, 4, 5, 6, 7, 9]) {
assert.equal(tabErlaubt('botleads', s), true, `Stufe ${s}`)
for (const frei of ['hochhaus', 'freigabe', '', 'unbekannt', undefined]) assert.equal(tabErlaubt(frei, s), true, `${frei} auf Stufe ${s}`)
}
})
test('linkErlaubt: Links in den Keller sind unterhalb von Stufe 3 kein Link und ab 3 einer, 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, 1), false, `Stufe 1: ${href}`); assert.equal(linkErlaubt(href, 2), false, `Stufe 2: ${href}`)
for (const s of [3, 5, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${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) { for (const s of [1, 3, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${String(href)}`) }
})