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>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-22 12:21:59 +00:00
1 parent 8363b63374
commit 6eb4dd3c51
11 files changed
+121 -82

No files matched your search

+4 -4
View File
@@ -385,8 +385,8 @@ test('Spur-Links: unsichere (javascript:, //host, /\\host, data:) bleiben Text;
})
})
// Stufen-Sperre (hustlerStufe.js): unterhalb der Keller-Stufe (6) führt auch kein Spur-Link in den Keller; der Eintrag bleibt als Text stehen
test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) sind auf Stufe 3 Text und ab Stufe 6 Router-Links; alle anderen Links bleiben', async () => {
// Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 3 (KELLER_BEGEHBAR_AB) führt auch kein Spur-Link in den Keller; der Eintrag bleibt als Text stehen
test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) sind auf Stufe 1 und 2 Text und ab Stufe 3 Router-Links; alle anderen Links bleiben', async () => {
const events = [
{ id: '1', ts: '2026-10-22T09:00:00Z', text: 'Keller', link: '/hustler?tab=botleads&bot=abc123' },
{ id: '2', ts: '2026-10-22T08:59:00Z', text: 'Alt', link: '/workers?tab=botleads&bot=abc123' },
@@ -396,7 +396,7 @@ test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) s
{ id: '6', ts: '2026-10-22T08:55:00Z', text: 'Extern', link: 'https://example.org/hustler?tab=botleads' },
]
const ziele = (li) => nodes(li, (n) => n.tag === 'a' || n.tag === 'router-link')
for (const stufe of [1, 3, 5]) {
for (const stufe of [1, 2]) {
await lauf({ props: { workerId: 'lea', stufe }, trace: { trace: { events }, loading: false, error: null } }, (ui) => {
const zeilen = ui.spurZeilen(); const byText = (t) => zeilen.find((z) => z.text === t)
assert.equal(zeilen.length, 6)
@@ -407,7 +407,7 @@ test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) s
assert.ok(!JSON.stringify(ui.find((x) => x.tag === 'a' || x.tag === 'router-link').map((n) => n.props)).includes('botleads&bot'))
})
}
for (const stufe of [6, 9]) {
for (const stufe of [3, 5, 6, 9]) {
await lauf({ props: { workerId: 'lea', stufe }, trace: { trace: { events }, loading: false, error: null } }, (ui) => {
const zeilen = ui.spurZeilen(); const byText = (t) => zeilen.find((z) => z.text === t)
assert.deepEqual(ziele(byText('Keller').li).map((n) => n.props.to), ['/hustler?tab=botleads&bot=abc123'])
+13 -9
View File
@@ -121,7 +121,7 @@ async function start(initial = {}) {
const state = initial.state === undefined ? makeState() : initial.state
const handle = { current: null }
// Stufen-Sperre (hustlerStufe.js): die Zusicherungen dieser Datei gelten für das volle Haus (Stufe 6, Keller offen, Verhalten wie vor der Sperre);
// stufe: 3 (Keller gesperrt) steht in den eigenen Tests am Ende des Abschnitts „Etagen“
// stufe 1 und 2 (Keller zu) und 3 (Keller begehbar, leer) stehen in den eigenen Tests am Ende des Abschnitts „Etagen“
let props = { state, workers: state ? indexWorkers(state) : {}, onZeige() {}, onOpen() {}, stufe: 6, ...initial }
const mounted = __mount(jsx(() => jsx(HochhausWelt, { ...props, ref: handle }), {}))
await settle()
@@ -480,10 +480,10 @@ test('Etagen: ein Etagen-Knopf zoomt heran (Kamera wie kameraEtage), ohne onOpen
} finally { ui.end(); restore() }
})
// Stufen-Sperre: unterhalb der Keller-Stufe (6) gibt es keinen Weg in den Keller; die Etage verhält sich wie jede andere (zoomt heran), Arne bleibt klickbar
test('Etagen bei Stufe 3 und 5: nur Freigabe ist ein Link, die Keller-Etage ist ein Knopf wie die anderen (Beschriftung, Text, kein href)', async () => {
// Stufen-Sperre: unterhalb von Stufe 3 (KELLER_BEGEHBAR_AB) gibt es keinen Weg in den Keller; die Etage verhält sich wie jede andere (zoomt heran), Arne bleibt klickbar
test('Etagen bei Stufe 1 und 2: nur Freigabe ist ein Link, die Keller-Etage ist ein Knopf wie die anderen (Beschriftung, Text, kein href)', async () => {
const { ETAGEN } = await laden()
for (const stufe of [1, 3, 5]) {
for (const stufe of [1, 2]) {
const ui = await start({ stufe })
try {
const etagen = byClass(ui.tree(), 'hk-etage')
@@ -507,11 +507,11 @@ test('Etagen bei Stufe 3 und 5: nur Freigabe ist ein Link, die Keller-Etage ist
}
})
test('Stufe 3: Klick auf die Keller-Etage zoomt heran und öffnet keinen Bildschirm; die Freigabe-Etage öffnet weiter das Pult', async () => {
test('Stufe 1 und 2: Klick auf die Keller-Etage zoomt heran und öffnet keinen Bildschirm; die Freigabe-Etage öffnet weiter das Pult', async () => {
const { kameraEtage, ETAGEN } = await laden()
const geoeffnet = []
const restore = fensterAttrappe()
const ui = await start({ stufe: 3, onOpen: (tab) => geoeffnet.push(tab) })
const ui = await start({ stufe: 2, onOpen: (tab) => geoeffnet.push(tab) })
try {
ui.anhaengen()
const keller = ETAGEN.find((e) => e.id === 'keller')
@@ -540,13 +540,17 @@ test('Stufe 3: Arne bleibt im Haus sichtbar und klickbar (Detail-Dialog), auch d
} finally { ui.end() }
})
test('Stufe 6 und höher: die Keller-Etage bleibt der Link auf die Bot-Leads (wie vor der Sperre)', async () => {
for (const stufe of [6, 7, 9]) {
const ui = await start({ stufe })
// Ab Stufe 3 ist der Keller begehbar (auch ohne Bot-Leads): die Etage ist der Link, genau wie mit Live-Daten ab Stufe 6
test('Ab Stufe 3 (Produktion) und höher: die Keller-Etage ist der Link auf den Keller (wie vor der Sperre), Klick wechselt den Bildschirm', async () => {
for (const stufe of [3, 4, 5, 6, 7, 9]) {
const geoeffnet = []
const ui = await start({ stufe, onOpen: (tab) => geoeffnet.push(tab) })
try {
const keller = ui.etage('Keller · Bot-Leads ▸')
assert.equal(keller.tag, 'a', `Stufe ${stufe}`); assert.equal(keller.props.href, '/hustler?tab=botleads')
assert.equal(keller.props['aria-label'], 'Keller · Bot-Leads: Bot-Leads öffnen')
const e = ereignis(); keller.props.onClick(e)
assert.equal(e.defaultPrevented, true, `Stufe ${stufe}`); assert.deepEqual(geoeffnet, ['botleads'], `Stufe ${stufe}`)
} finally { ui.end() }
}
})
+36 -17
View File
@@ -269,7 +269,7 @@ const loadSeite = () => (seiteBundle ??= esbuild.build({
// Ein Lauf der Seite. query ist der Teil nach dem ?, feed wird vom Test zwischendurch geändert (setFeed) und die Seite neu gerendert.
// stufe (Stufen-Sperre, hustlerStufe.js): die Zusicherungen dieser Datei gelten für die volle Seite (Stufe 6, Keller offen, Verhalten wie vor der Sperre);
// die Tests „Stufe 3“ weiter unten bauen die Seite mit stufe: 3 (Stand der Produktion).
// die Tests „Stufe 1 und 2“ und „Stufe 3“ weiter unten bauen die Seite mit stufe: 1/2 (Keller zu) und 3 (Stand der Produktion: Keller begehbar, ohne Bot-Leads).
async function seite(query, { admin = false, feed, stufe = 6 } = {}) {
const { __mount, jsx, WorkersPage } = await loadSeite()
const setParamsCalls = []
@@ -330,13 +330,13 @@ test('Seite: ?tab=freigabe zeigt das Pult, ?tab=botleads den Keller, beide direk
} finally { keller.end() }
})
// ---------- Stufen-Sperre (Stufe 3 = Stand der Produktion: Dienst P3, ohne Keller) ----------
// ---------- Stufen-Sperre (Keller begehbar ab Stufe 3 = Stand der Produktion: Dienst P3, Keller ohne Bot-Leads) ----------
// Adressen, die auf Stufe 3 in den Keller führen würden: der Reiter, mit Einzelfall, mit Ballast, andere Schreibweisen und erfundene Parameter
// Adressen, die in den Keller führen würden: der Reiter, mit Einzelfall, mit Ballast, andere Schreibweisen und erfundene Parameter
const KELLER_ADRESSEN = ['tab=botleads', 'tab=botleads&bot=abc123', 'bot=abc123&tab=botleads', 'tab=botleads&bot=', 'tab=BotLeads', 'ansicht=keller', 'tab=keller', 'tab=botleads&ansicht=keller']
test('Stufe 3: jede Adresse in den Keller fällt auf das Hochhaus zurück; der Keller wird nie eingehängt, Hochhaus und Seite bleiben ganz', async () => {
for (const stufe of [1, 3, 5]) {
test('Stufe 1 und 2: jede Adresse in den Keller fällt auf das Hochhaus zurück; der Keller wird nie eingehängt, Hochhaus und Seite bleiben ganz', async () => {
for (const stufe of [1, 2]) {
for (const query of KELLER_ADRESSEN) {
const ui = await seite(query, { stufe, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try {
@@ -351,8 +351,8 @@ test('Stufe 3: jede Adresse in den Keller fällt auf das Hochhaus zurück; der K
}
})
test('Stufe 3: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Freigabe bleibt erreichbar; der Rückweg aus dem Pult funktioniert wie sonst', async () => {
const ui = await seite('tab=freigabe', { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
test('Stufe 2: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Freigabe bleibt erreichbar; der Rückweg aus dem Pult funktioniert wie sonst', async () => {
const ui = await seite('tab=freigabe', { stufe: 2, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try {
assert.deepEqual(ui.screen(), ['pult'])
ui.go('tab=botleads&bot=x'); assert.deepEqual(ui.screen(), ['hochhaus']); assert.equal(ui.hk.props.hochhaus.vonTab, 'freigabe')
@@ -362,32 +362,51 @@ test('Stufe 3: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Frei
} finally { ui.end() }
})
test('Stufe 3: das Hochhaus öffnet nur noch die Freigabe (onOpen(\'botleads\') tut nichts); Hochhaus und Pult bekommen die Stufe', async () => {
const ui = await seite('tab=hochhaus', { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
test('Stufe 2: das Hochhaus öffnet nur noch die Freigabe (onOpen(\'botleads\') tut nichts); Hochhaus und Pult bekommen die Stufe', async () => {
const ui = await seite('tab=hochhaus', { stufe: 2, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try {
const p = ui.hk.props.hochhaus
p.onOpen('botleads'); p.onOpen('freigabe')
assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }])
ui.go('tab=freigabe')
assert.equal(ui.hk.props.pult.stufe, 3)
assert.equal(ui.hk.props.pult.stufe, 2)
ui.hk.props.pult.onBack()
assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }, { tab: 'hochhaus' }])
} finally { ui.end() }
})
test('Stufe 6 und höher: ?tab=botleads zeigt den Keller samt Einzelfall (bot), onOpen(\'botleads\') wechselt; Stufe 4 und 5 haben den Keller noch nicht', async () => {
for (const stufe of [6, 8, 9]) {
// Stufe 3 (Produktion) und höher: der Keller ist ein Bildschirm; die Stufe geht an BotLeadList, die darunter von Stufe 6 entscheidet, ob sie Bot-Leads abruft (botleadList.test.mjs)
test('Stufe 3 bis 9: ?tab=botleads (auch alle Schreibweisen mit tab=botleads) zeigt den Keller samt Einzelfall (bot) und bekommt die Stufe; onOpen(\'botleads\') wechselt; Zurück ins Hochhaus', async () => {
for (const stufe of [3, 4, 5, 6, 8, 9]) {
const ui = await seite('tab=botleads&bot=abc123', { stufe, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try {
assert.deepEqual(ui.screen(), ['keller'], `Stufe ${stufe}`); assert.equal(ui.hk.props.keller.bot, 'abc123')
assert.deepEqual(ui.screen(), ['keller'], `Stufe ${stufe}`); assert.equal(ui.hk.props.keller.bot, 'abc123'); assert.equal(ui.hk.props.keller.stufe, stufe)
assert.equal(ui.root().props['data-tab'], 'botleads', `Stufe ${stufe}`)
ui.hk.props.keller.onBack(); ui.hk.props.keller.onClearBot()
assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }, { tab: 'botleads' }], `Stufe ${stufe}`)
ui.go('tab=hochhaus'); ui.hk.props.hochhaus.onOpen('botleads')
assert.deepEqual(ui.setParamsCalls, [{ tab: 'botleads' }], `Stufe ${stufe}`)
assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }, { tab: 'botleads' }, { tab: 'botleads' }], `Stufe ${stufe}`)
} finally { ui.end() }
}
for (const stufe of [4, 5]) {
const ui = await seite('tab=botleads', { stufe })
try { assert.deepEqual(ui.screen(), ['hochhaus'], `Stufe ${stufe}`) } finally { ui.end() }
for (const query of ['tab=botleads', 'bot=abc123&tab=botleads', 'tab=botleads&bot=']) {
const ui = await seite(query, { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try { assert.deepEqual(ui.screen(), ['keller'], query) } finally { ui.end() }
}
// erfundene Werte bleiben beim Hochhaus, auch auf Stufe 3
for (const query of ['tab=BotLeads', 'ansicht=keller', 'tab=keller']) {
const ui = await seite(query, { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
try { assert.deepEqual(ui.screen(), ['hochhaus'], query) } finally { ui.end() }
}
})
test('Stufe 3: Hochhaus und Keller bekommen die Stufe, der Keller zählt als Bildschirm der Seite (data-tab); Pause und Kennzahlen gehen an ihn', async () => {
const state = makeState({ kpis: { pultOpen: 2, dealsMonth: 1, botLeads: 0 } })
const ui = await seite('tab=botleads', { stufe: 3, feed: { state, error: null, loaded: true, refresh() {} } })
try {
assert.equal(ui.hk.props.keller.stufe, 3); assert.equal(ui.hk.props.keller.kpis, state.kpis); assert.equal(ui.hk.props.keller.paused, false)
ui.go('tab=hochhaus')
assert.equal(ui.hk.props.hochhaus.stufe, 3); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads')
} finally { ui.end() }
})
test('Seite: keine Seitenüberschrift, keine Reiterleiste', async () => {
+29 -21
View File
@@ -7,15 +7,16 @@ 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.
// 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, STUFE_KELLER, stufeAus, antwortenOffen, kellerOffen, tabErlaubt, linkErlaubt } = stufe
const { HUSTLER_STUFE, STUFE_ANTWORTEN, KELLER_BEGEHBAR_AB, BOTLEADS_AB, stufeAus, antwortenOffen, kellerBegehbar, botleadsAbrufbar, tabErlaubt, linkErlaubt } = stufe
// ---------- Stufe aus dem Bau ----------
@@ -36,13 +37,13 @@ test('Standard: ohne Wert aus dem Bau gilt die Stufe aus dem Quelltext, einmal u
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)
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, kellerOffen } from './src/lib/hustlerStufe.js'", resolveDir: ROOT, loader: 'js' },
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')}`)
@@ -52,7 +53,7 @@ test('Bau: VITE_HUSTLER_STUFE überschreibt die Stufe zur Bauzeit (Prüfstand);
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)
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)
@@ -72,41 +73,48 @@ test('keine Überschreibung zur Laufzeit: kein localStorage, keine Adresse, kein
// ---------- Schwellen ----------
test('Schwellen: Antwort-Knöpfe ab Stufe 4, Keller ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => {
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(kellerOffen(n), n >= 6, `kellerOffen(${n})`)
assert.equal(botleadsAbrufbar(n), n >= 6, `botleadsAbrufbar(${n})`)
}
assert.deepEqual([antwortenOffen(3), kellerOffen(3)], [false, false])
assert.deepEqual([antwortenOffen(6), kellerOffen(6)], [true, true])
// 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(kellerOffen(kaputt), kellerOffen(HUSTLER_STUFE))
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(kellerOffen(), kellerOffen(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) ist gesperrt, und nur unterhalb von Stufe 6', () => {
for (const s of [1, 3, 4, 5]) {
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 [6, 7, 9]) assert.equal(tabErlaubt('botleads', s), true, `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 6 kein Link, alles andere bleibt unberührt', () => {
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, 3), false, `Stufe 3: ${href}`); assert.equal(linkErlaubt(href, 5), false, `Stufe 5: ${href}`)
assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${href}`)
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) { assert.equal(linkErlaubt(href, 3), true, `Stufe 3: ${String(href)}`); assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${String(href)}`) }
for (const href of frei) { for (const s of [1, 3, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${String(href)}`) }
})
+5 -6
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS, KELLER_GESPERRT_TEXT } = await loadModule('src/lib/workersApi.js')
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS, BOTLEADS_GESPERRT_TEXT } = await loadModule('src/lib/workersApi.js')
const { actionErrorText, UNCERTAIN_TEXT } = await loadModule('src/lib/workersPult.js')
const { createJwtCache } = await loadModule('src/lib/serverpulsAuth.js')
@@ -707,15 +707,14 @@ test('createJWT: Übertragungsfehler beim frischen JWT nach einer 401 sperrt nic
assert.deepEqual(mints, [false, true, false, true]) // keine Sperre: weder für frische JWTs noch für createJWT überhaupt
})
// ---------- Stufen-Sperre (hustlerStufe.js): kein Abruf des Kellers unterhalb von Stufe 6 ----------
// ---------- Stufen-Sperre (hustlerStufe.js): kein Abruf der Bot-Leads unterhalb von Stufe 6 (BOTLEADS_AB; der Keller selbst ist schon ab Stufe 3 begehbar, dann leer) ----------
test('API: auf Stufe 3 geht kein Abruf von /api/workers/botleads hinaus (auch kein JWT), auf Stufe 6 wie bisher', async () => {
for (const s of [1, 3, 5]) {
for (const s of [1, 3, 4, 5]) {
const f = fakeFetch(okReplies(8)); let jwtGeholt = 0
const api = createWorkersApi({ getJwt: async () => { jwtGeholt += 1; return 'jwt' }, fetchImpl: f, stufe: s })
for (const params of [undefined, {}, { room: 'handwerk' }, { q: 'Bäcker', page: 2 }, { id: 'abc' }]) {
await assert.rejects(api.botleads(params), (err) => err.status === 404 && err.message === KELLER_GESPERRT_TEXT, `Stufe ${s}`)
await assert.rejects(api.botleads(params), (err) => err.status === 404 && err.message === BOTLEADS_GESPERRT_TEXT, `Stufe ${s}`)
}
assert.equal(f.calls.length, 0, `Stufe ${s}`); assert.equal(jwtGeholt, 0, `Stufe ${s}`)
// die übrigen Abrufe der Stufe 3 laufen
@@ -728,5 +727,5 @@ test('API: auf Stufe 3 geht kein Abruf von /api/workers/botleads hinaus (auch ke
await api.botleads({ room: 'handwerk', page: 2 })
assert.deepEqual(f.calls.map((c) => [c.init.method || 'GET', c.url]), [['GET', '/api/workers/botleads?room=handwerk&page=2']])
}
assert.match(KELLER_GESPERRT_TEXT, /Keller/)
assert.match(BOTLEADS_GESPERRT_TEXT, /Bot-Leads/)
})