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>
This commit is contained in:
1 parent
75ed38e68d
commit
7cd74a23d2
2 files changed
+181
No files matched your search
@@ -0,0 +1,112 @@
|
||||
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)}`) }
|
||||
})
|
||||
Reference in new issue
Block a user