Terminplaner: oeffentliche Schnittstelle v1 mit Missbrauchsschutz
Router /api/public/termine/v1 (config, tage, zeiten, buchungen, widerruf, Token-Seiten /termin/*, Kalender-Abo) mit eigenem 16-KB-Parser und Fehlermiddleware, CORS nach Herkunft, Honeypot und Formular-Token, Grenzen je IP (IPv6 je /64), siteKey und Adresse in limits.js, Mindestantwortzeit (TERMINE_MIN_ANTWORT_MS) und 10-Sekunden-Cache fuer tage/zeiten. Vor dem globalen express.json eingehaengt; /api/public/ ist vom Kundenansicht-Waechter ausgenommen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43ac05aa06
commit
2b0c05b8e9
10 files changed
+1928
-7
No files matched your search
@@ -37,7 +37,9 @@ const MOUNTS = [
|
||||
['/api/abo', (await import('../server/routes/abo.js')).default],
|
||||
['/api/funktionen', (await import('../server/routes/funktionen.js')).default],
|
||||
]
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, ...Object.fromEntries(MOUNTS) })
|
||||
// Oeffentliche Schnittstelle (ohne Session) wie in server/index.js hinter dem Waechter; nicht in MOUNTS, weil sie keine Kunden-Route ist
|
||||
const oeffentlich = (await import('../server/routes/publicTermine.js')).default
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, ...Object.fromEntries(MOUNTS), '/api/public/termine/v1': oeffentlich })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
seedFixtures(aw)
|
||||
@@ -210,3 +212,29 @@ test('Tab mit Kennung, Cookie ist keine Kundenansicht mehr -> 409 statt fremder
|
||||
// ohne Cookie: kein fremder Kunde, die Route antwortet selbst mit 401
|
||||
assert.equal((await call('GET', '/api/projects', { headers: header })).status, 401)
|
||||
})
|
||||
|
||||
// Terminplaner (Aufgabe 16): Gaeste buchen ohne Sitzung; ein Kundenansicht-Cookie im selben Browser (Mitarbeiter testet die
|
||||
// Website des Kunden) darf ihre POSTs weder sperren noch beenden
|
||||
test('/api/public/ ist ausgenommen: Kundenansicht-Cookie sperrt oeffentliche POSTs nicht und beendet keine Ansicht', async (t) => {
|
||||
const vorher = config.termine.minAntwortMs
|
||||
config.termine.minAntwortMs = 0 // sonst wartet jede Buchungsantwort 300 ms
|
||||
t.after(() => (config.termine.minAntwortMs = vorher))
|
||||
const url = '/api/public/termine/v1/abcdefghijklmnop/buchungen'
|
||||
const faelle = [
|
||||
['Kundenansicht mit Kennung', kv()],
|
||||
['Kundenansicht ohne Kennung (sonst 409)', { cookie: kundenansichtCookie() }],
|
||||
['fremde Kennung (sonst 409)', { cookie: kundenansichtCookie(), headers: { 'x-kundenansicht': 'b'.repeat(32) } }],
|
||||
['abgelaufene Ansicht (sonst 401)', kv({ viewExpiresAt: Date.now() - 1000 })],
|
||||
['alte Ansicht ohne viewId (sonst 401)', { cookie: viewAsCookie('c1') }],
|
||||
]
|
||||
for (const [was, opts] of faelle) {
|
||||
for (const pfad of [url, url.replace('/api/public/', '/API/Public/')]) {
|
||||
const res = await call('POST', pfad, { ...opts, body: {} })
|
||||
assert.equal(res.status, 200, `${was} ${pfad}`)
|
||||
assert.deepEqual(res.body, { aktiv: false }, `${was} ${pfad}`) // Modus off: die Schnittstelle antwortet selbst
|
||||
assert.equal(clears(res, config.cookieName), false, `${was}: das Portal-Cookie bleibt`)
|
||||
}
|
||||
}
|
||||
// ausserhalb von /api/public/ bleibt die Sperre
|
||||
assert.equal((await call('POST', '/api/chat/read', kv())).status, 403)
|
||||
})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,161 @@
|
||||
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import http from 'node:http'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import express from 'express'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||||
|
||||
// Der Router bringt seinen eigenen Parser (16 KB) und seine Fehlermiddleware mit: hier haengt er an einer nackten
|
||||
// express()-App ohne globalen Parser und ohne eigene Fehlerbehandlung. Was beim Gast ankommt, kommt nur vom Router.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
termineConfig: schemaKeys('termineConfig'),
|
||||
termineBookings: schemaKeys('termineBookings'),
|
||||
termineClaims: schemaKeys('termineClaims'),
|
||||
termineOutbox: schemaKeys('termineOutbox'),
|
||||
},
|
||||
datetimes: TERMINE_DATETIMES,
|
||||
defaultLimit: 25,
|
||||
})
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js')
|
||||
const { resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||||
const { default: router, resetPublicTermineForTests } = await import('../server/routes/publicTermine.js')
|
||||
|
||||
const app = express()
|
||||
app.use('/api/public/termine/v1', router)
|
||||
const server = http.createServer(app)
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
const PORT = server.address().port
|
||||
after(() => Promise.all([new Promise((resolve) => server.close(resolve)), aw.close()]))
|
||||
|
||||
const SITE_KEY = 'abcdefghijklmnop'
|
||||
const PFAD = `/api/public/termine/v1/${SITE_KEY}/buchungen`
|
||||
const MODUS_VORHER = config.termine.mode
|
||||
const MIN_ANTWORT_VORHER = config.termine.minAntwortMs
|
||||
const ZU_GROSS = { fehler: { _: 'Die Anfrage ist zu groß.' } }
|
||||
const UNGUELTIG = { fehler: { _: 'Ungültige Anfrage.' } }
|
||||
|
||||
beforeEach(() => {
|
||||
resetTermineLimitsForTests()
|
||||
resetKontextForTests()
|
||||
resetPublicTermineForTests()
|
||||
config.termine.mode = 'pilot'
|
||||
config.termine.minAntwortMs = 0
|
||||
mock.method(console, 'error', () => {})
|
||||
})
|
||||
afterEach(() => mock.restoreAll())
|
||||
after(() => {
|
||||
config.termine.mode = MODUS_VORHER
|
||||
config.termine.minAntwortMs = MIN_ANTWORT_VORHER
|
||||
})
|
||||
|
||||
/** Roher Aufruf mit Chunks (ohne content-length) oder festem Body; => { status, text, headers }. */
|
||||
function roh({ pfad = PFAD, method = 'POST', headers = {}, chunks = [] } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({ host: '127.0.0.1', port: PORT, path: pfad, method, headers }, (res) => {
|
||||
let text = ''
|
||||
res.setEncoding('utf8')
|
||||
res.on('data', (d) => (text += d))
|
||||
res.on('end', () => resolve({ status: res.statusCode, text, headers: res.headers }))
|
||||
})
|
||||
req.on('error', reject)
|
||||
for (const chunk of chunks) req.write(chunk)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
test('20 KB ohne content-length (chunked): 413 vom Router-Parser, ohne Rohtext', async () => {
|
||||
const json = `{"name":"${'x'.repeat(20 * 1024)}"}`
|
||||
const teile = json.match(/[\s\S]{1,2000}/g)
|
||||
const res = await roh({ headers: { 'content-type': 'application/json', 'transfer-encoding': 'chunked' }, chunks: teile })
|
||||
assert.equal(res.status, 413)
|
||||
assert.deepEqual(JSON.parse(res.text), ZU_GROSS)
|
||||
assert.match(res.headers['content-type'], /^application\/json/)
|
||||
assert.equal(res.headers['cache-control'], 'no-store')
|
||||
assert.ok(!res.text.includes('xxxx'))
|
||||
})
|
||||
|
||||
test('content-length ueber 16 384: 413 vorab, auch fuer einen Body, der gar nicht mehr geschickt wird', async () => {
|
||||
const res = await roh({ headers: { 'content-type': 'application/json', 'content-length': '16385' }, chunks: [] }).catch((err) => err)
|
||||
// der Server antwortet, bevor der Rumpf kommt; der Client darf dabei zurueckgesetzt werden
|
||||
if (res instanceof Error) assert.match(res.code, /ECONNRESET|EPIPE|ECONNABORTED/)
|
||||
else {
|
||||
assert.equal(res.status, 413)
|
||||
assert.deepEqual(JSON.parse(res.text), ZU_GROSS)
|
||||
}
|
||||
const gross = await fetch(`http://127.0.0.1:${PORT}${PFAD}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ a: 'y'.repeat(16400) }) })
|
||||
assert.equal(gross.status, 413)
|
||||
assert.deepEqual(await gross.json(), ZU_GROSS)
|
||||
})
|
||||
|
||||
test('genau 16 384 Bytes sind noch erlaubt', async () => {
|
||||
const kopf = '{"kontakt_url":"'
|
||||
const ende = '"}'
|
||||
const fuell = 'z'.repeat(16384 - kopf.length - ende.length)
|
||||
const body = kopf + fuell + ende
|
||||
assert.equal(Buffer.byteLength(body), 16384)
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}${PFAD}`, { method: 'POST', headers: { 'content-type': 'application/json', origin: 'https://salon.de' }, body })
|
||||
// nicht 413: der Router liest den Rumpf; der siteKey ist hier unbekannt (nichts gespeichert), also { aktiv: false }
|
||||
assert.equal(res.status, 200)
|
||||
assert.deepEqual(await res.json(), { aktiv: false })
|
||||
})
|
||||
|
||||
test('kaputtes JSON: 400 mit fester Meldung, nie der Text des Parsers', async () => {
|
||||
for (const body of ['{"name": ', '{"a":}', 'nicht json', '{"a":1}}', '[1,2', "{'a':1}"]) {
|
||||
const res = await roh({ headers: { 'content-type': 'application/json', 'content-length': String(Buffer.byteLength(body)) }, chunks: [body] })
|
||||
assert.equal(res.status, 400, body)
|
||||
assert.deepEqual(JSON.parse(res.text), UNGUELTIG, body)
|
||||
assert.doesNotMatch(res.text, /Unexpected|JSON|position|token|SyntaxError|at /i, body)
|
||||
}
|
||||
// dasselbe auf den Token-Seiten
|
||||
const token = await roh({ pfad: '/api/public/termine/v1/termin/vorschau', headers: { 'content-type': 'application/json' }, chunks: ['{"token":'] })
|
||||
assert.equal(token.status, 400)
|
||||
assert.deepEqual(JSON.parse(token.text), UNGUELTIG)
|
||||
})
|
||||
|
||||
test('unbekannte Kodierung, falscher Zeichensatz und abgebrochene Anfragen: 4xx als { fehler }, kein Stacktrace', async () => {
|
||||
const gzip = await roh({ headers: { 'content-type': 'application/json', 'content-encoding': 'br', 'content-length': '2' }, chunks: ['{}'] })
|
||||
assert.equal(gzip.status, 400)
|
||||
assert.deepEqual(JSON.parse(gzip.text), UNGUELTIG)
|
||||
const zeichensatz = await roh({ headers: { 'content-type': 'application/json; charset=iso-8859-1', 'content-length': '2' }, chunks: ['{}'] })
|
||||
assert.equal(zeichensatz.status, 400)
|
||||
assert.deepEqual(JSON.parse(zeichensatz.text), UNGUELTIG)
|
||||
})
|
||||
|
||||
test('interner Fehler: 503 { fehler } ohne err.message, auch ohne Fehlerbehandlung der App', async () => {
|
||||
const RAW = 'Nachbau kaputt: geheimer Rohtext'
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: RAW })
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}/api/public/termine/v1/${SITE_KEY}/config`)
|
||||
assert.equal(res.status, 503)
|
||||
const text = await res.text()
|
||||
assert.deepEqual(JSON.parse(text), { fehler: { _: 'Gerade nicht erreichbar' } })
|
||||
assert.ok(!text.includes('Nachbau') && !text.includes('geheimer'))
|
||||
})
|
||||
|
||||
test('Router und App: der Router beantwortet alles unter seinem Pfad selbst (404 als { fehler }), die App darunter sieht nichts', async () => {
|
||||
const res = await fetch(`http://127.0.0.1:${PORT}/api/public/termine/v1/irgendwas/unbekannt`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"a":1}' })
|
||||
assert.equal(res.status, 404)
|
||||
assert.deepEqual(await res.json(), { fehler: { _: 'Nicht gefunden' } })
|
||||
// ausserhalb des Pfades antwortet Express selbst
|
||||
const aussen = await fetch(`http://127.0.0.1:${PORT}/api/public/termine/v2/x`)
|
||||
assert.equal(aussen.status, 404)
|
||||
})
|
||||
|
||||
test('Reihenfolge in server/index.js: kundenansichtGuard, dann der Router der oeffentlichen Schnittstelle, dann der globale express.json', () => {
|
||||
const quelle = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8')
|
||||
const guard = quelle.indexOf('app.use(kundenansichtGuard)')
|
||||
const oeffentlich = quelle.indexOf("app.use('/api/public/termine/v1'")
|
||||
const global = quelle.indexOf('app.use(express.json(')
|
||||
assert.ok(guard >= 0 && oeffentlich >= 0 && global >= 0, 'alle drei Aufrufe gefunden')
|
||||
assert.equal(quelle.indexOf("app.use('/api/public/termine/v1'", oeffentlich + 1), -1, 'nur einmal eingehaengt')
|
||||
assert.ok(guard < oeffentlich, 'erst der Waechter der Kundenansicht')
|
||||
assert.ok(oeffentlich < global, 'vor dem globalen Parser (2 MB)')
|
||||
assert.match(quelle, /import publicTermineRoutes from '\.\/routes\/publicTermine\.js'/)
|
||||
})
|
||||
@@ -1,11 +1,25 @@
|
||||
import { test, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { zaehle, zaehleAlle, darfG08, darfK21NichtZugeordnet, resetTermineLimitsForTests } from '../server/services/termine/limits.js'
|
||||
import {
|
||||
zaehle,
|
||||
zaehleAlle,
|
||||
darfG08,
|
||||
darfK21NichtZugeordnet,
|
||||
darfAnfrage,
|
||||
darfAnfrageSite,
|
||||
darfBuchungsversuch,
|
||||
darfHaltung,
|
||||
darfWiderrufsversuch,
|
||||
darfG01,
|
||||
ipSchluessel,
|
||||
resetTermineLimitsForTests,
|
||||
} from '../server/services/termine/limits.js'
|
||||
|
||||
// Grenzen im Speicher: festes Fenster ab dem ersten Ereignis, alle-oder-keine bei mehreren Regeln, Obergrenze der Schluessel.
|
||||
const T0 = new Date('2026-10-05T08:00:00.000Z')
|
||||
const STUNDE = 60 * 60 * 1000
|
||||
const TAG = 24 * STUNDE
|
||||
const MINUTE = 60 * 1000
|
||||
const plus = (ms) => new Date(T0.getTime() + ms)
|
||||
|
||||
beforeEach(() => resetTermineLimitsForTests())
|
||||
@@ -81,3 +95,75 @@ test('resetTermineLimitsForTests leert alles', () => {
|
||||
resetTermineLimitsForTests()
|
||||
assert.equal(zaehle('a', 1, TAG, T0), true)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Aufgabe 16: IP, siteKey, G01
|
||||
|
||||
test('ipSchluessel: IPv4 bleibt, IPv4-in-IPv6 wird IPv4, IPv6 zaehlt je /64 (auch in anderer Schreibweise)', () => {
|
||||
assert.equal(ipSchluessel('203.0.113.7'), '203.0.113.7')
|
||||
assert.equal(ipSchluessel('::ffff:203.0.113.7'), '203.0.113.7')
|
||||
assert.equal(ipSchluessel('::FFFF:203.0.113.7'), '203.0.113.7')
|
||||
const a = ipSchluessel('2001:db8:1:2::1')
|
||||
assert.equal(ipSchluessel('2001:0db8:0001:0002:ffff:ffff:ffff:ffff'), a, 'dieselbe /64, ausgeschrieben')
|
||||
assert.equal(ipSchluessel('2001:DB8:1:2:abcd::9'), a, 'dieselbe /64, Grossbuchstaben und Kurzform')
|
||||
assert.equal(ipSchluessel('2001:db8:1:2:0:0:0:0'), a)
|
||||
assert.notEqual(ipSchluessel('2001:db8:1:3::1'), a, 'andere /64')
|
||||
assert.notEqual(ipSchluessel('2001:db8:2:2::1'), a)
|
||||
assert.equal(ipSchluessel('fe80::1%eth0'), ipSchluessel('fe80::2'), 'Zonenkennung faellt weg')
|
||||
assert.equal(ipSchluessel('::1'), ipSchluessel('0:0:0:0:ffff:ffff:ffff:ffff'), 'Loopback ist die /64 ::')
|
||||
assert.notEqual(ipSchluessel('203.0.113.7'), ipSchluessel('203.0.113.8'))
|
||||
assert.notEqual(ipSchluessel('203.0.113.7'), a, 'IPv4 und IPv6 stossen nie zusammen')
|
||||
})
|
||||
|
||||
test('ipSchluessel: fehlende oder unlesbare Adresse ergibt einen festen Ersatzschluessel (nie einen Fehler)', () => {
|
||||
for (const wert of [undefined, null, '', ' ', 'kaputt', '1.2.3', '999.1.1.1', 42, {}]) {
|
||||
assert.equal(ipSchluessel(wert), 'unbekannt', JSON.stringify(wert))
|
||||
}
|
||||
})
|
||||
|
||||
test('darfAnfrage: 120 je IP und Minute (IPv6 je /64); darfAnfrageSite: 600 je siteKey und Minute', () => {
|
||||
assert.equal(Array.from({ length: 125 }, () => darfAnfrage('203.0.113.7', T0)).filter(Boolean).length, 120)
|
||||
assert.equal(darfAnfrage('203.0.113.8', T0), true, 'andere IP')
|
||||
assert.equal(darfAnfrage('203.0.113.7', plus(MINUTE)), true, 'nach einer Minute wieder')
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
const erlaubt = Array.from({ length: 125 }, (_, i) => darfAnfrage(`2001:db8:1:2::${i + 1}`, T0))
|
||||
assert.equal(erlaubt.filter(Boolean).length, 120, 'alle Adressen der /64 teilen sich 120')
|
||||
assert.equal(darfAnfrage('2001:db8:1:3::1', T0), true)
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
assert.equal(Array.from({ length: 605 }, () => darfAnfrageSite('abcdefghijklmnop', T0)).filter(Boolean).length, 600)
|
||||
assert.equal(darfAnfrageSite('bcdefghijklmnopq', T0), true)
|
||||
assert.equal(darfAnfrageSite('abcdefghijklmnop', plus(MINUTE)), true)
|
||||
})
|
||||
|
||||
test('darfBuchungsversuch: 10 je IP und 10 Minuten (IPv6 je /64); darfHaltung: 60 je siteKey und Stunde', () => {
|
||||
assert.equal(Array.from({ length: 12 }, () => darfBuchungsversuch('203.0.113.7', T0)).filter(Boolean).length, 10)
|
||||
assert.equal(darfBuchungsversuch('203.0.113.7', plus(10 * MINUTE - 1)), false)
|
||||
assert.equal(darfBuchungsversuch('203.0.113.7', plus(10 * MINUTE)), true)
|
||||
assert.equal(darfBuchungsversuch('2001:db8::1', T0) && darfBuchungsversuch('2001:db8:0:0:1::1', T0), true, 'getrennt von der IPv4')
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
for (let i = 0; i < 5; i += 1) assert.equal(darfBuchungsversuch(`2001:db8:9:9::${i + 1}`, T0), true)
|
||||
for (let i = 0; i < 5; i += 1) assert.equal(darfBuchungsversuch(`2001:db8:9:9:1::${i + 1}`, T0), true)
|
||||
assert.equal(darfBuchungsversuch('2001:db8:9:9:ffff::1', T0), false, 'elf Versuche aus einer /64')
|
||||
|
||||
resetTermineLimitsForTests()
|
||||
assert.equal(Array.from({ length: 65 }, () => darfHaltung('abcdefghijklmnop', T0)).filter(Boolean).length, 60)
|
||||
assert.equal(darfHaltung('bcdefghijklmnopq', T0), true)
|
||||
assert.equal(darfHaltung('abcdefghijklmnop', plus(STUNDE)), true)
|
||||
})
|
||||
|
||||
test('darfWiderrufsversuch: 10 je IP und Stunde', () => {
|
||||
assert.equal(Array.from({ length: 12 }, () => darfWiderrufsversuch('203.0.113.7', T0)).filter(Boolean).length, 10)
|
||||
assert.equal(darfWiderrufsversuch('203.0.113.8', T0), true)
|
||||
assert.equal(darfWiderrufsversuch('203.0.113.7', plus(STUNDE)), true)
|
||||
})
|
||||
|
||||
test('darfG01: 5 je Adresse und Funktion und Tag; die Zaehler der Arten stoeren sich nicht', () => {
|
||||
assert.deepEqual(Array.from({ length: 7 }, () => darfG01('f1', 'max@example.com', T0)), [true, true, true, true, true, false, false])
|
||||
assert.equal(darfG01('f2', 'max@example.com', T0), true, 'andere Funktion')
|
||||
assert.equal(darfG01('f1', 'anna@example.com', T0), true, 'andere Adresse')
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(TAG - 1)), false)
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(TAG)), true, 'am naechsten Tag wieder')
|
||||
assert.equal(darfG08('f1', 'max@example.com', T0), true, 'G08 zaehlt getrennt von G01')
|
||||
})
|
||||
@@ -7,6 +7,7 @@ const TERMINE_ENV = [
|
||||
'TERMINE_ENABLED',
|
||||
'TERMINE_PILOT_EMAILS',
|
||||
'TERMINE_FORM_MIN_SECONDS',
|
||||
'TERMINE_MIN_ANTWORT_MS',
|
||||
'APPWRITE_COLLECTION_TERMINE_CONFIG',
|
||||
'APPWRITE_COLLECTION_TERMINE_BOOKINGS',
|
||||
'APPWRITE_COLLECTION_TERMINE_CLAIMS',
|
||||
@@ -47,6 +48,7 @@ test('Konfiguration: Vorgaben ohne Umgebungsvariablen', async () => {
|
||||
zelleMin: 5,
|
||||
mailsProStunde: 200,
|
||||
formMinSekunden: 3,
|
||||
minAntwortMs: 300,
|
||||
})
|
||||
assert.equal(fresh.collections.termineConfig, 'termineConfig')
|
||||
assert.equal(fresh.collections.termineBookings, 'termineBookings')
|
||||
@@ -59,6 +61,7 @@ test('Konfiguration: Umgebungsvariablen werden gelesen und bereinigt', async ()
|
||||
TERMINE_ENABLED: ' Pilot ',
|
||||
TERMINE_PILOT_EMAILS: ' Kenso.Gri@Gmail.com, ,justin@webklar.com ',
|
||||
TERMINE_FORM_MIN_SECONDS: '0',
|
||||
TERMINE_MIN_ANTWORT_MS: '0',
|
||||
APPWRITE_COLLECTION_TERMINE_CONFIG: 'tConfig',
|
||||
APPWRITE_COLLECTION_TERMINE_BOOKINGS: 'tBookings',
|
||||
APPWRITE_COLLECTION_TERMINE_CLAIMS: 'tClaims',
|
||||
@@ -67,6 +70,7 @@ test('Konfiguration: Umgebungsvariablen werden gelesen und bereinigt', async ()
|
||||
assert.equal(fresh.termine.mode, 'pilot')
|
||||
assert.deepEqual(fresh.termine.pilotEmails, ['kenso.gri@gmail.com', 'justin@webklar.com'])
|
||||
assert.equal(fresh.termine.formMinSekunden, 0)
|
||||
assert.equal(fresh.termine.minAntwortMs, 0)
|
||||
assert.deepEqual(
|
||||
[fresh.collections.termineConfig, fresh.collections.termineBookings, fresh.collections.termineClaims, fresh.collections.termineOutbox],
|
||||
['tConfig', 'tBookings', 'tClaims', 'tOutbox'],
|
||||
@@ -83,6 +87,11 @@ test('Konfiguration: leerer oder unbekannter Schalter ist off, ungueltige Sekund
|
||||
assert.equal(fresh.termine.formMinSekunden, 3, `TERMINE_FORM_MIN_SECONDS=${JSON.stringify(value)}`)
|
||||
}
|
||||
assert.equal((await freshConfig({ TERMINE_FORM_MIN_SECONDS: '7' })).termine.formMinSekunden, 7)
|
||||
for (const value of ['', 'abc', '-1', 'NaN']) {
|
||||
const fresh = await freshConfig({ TERMINE_MIN_ANTWORT_MS: value })
|
||||
assert.equal(fresh.termine.minAntwortMs, 300, `TERMINE_MIN_ANTWORT_MS=${JSON.stringify(value)}`)
|
||||
}
|
||||
assert.equal((await freshConfig({ TERMINE_MIN_ANTWORT_MS: '750' })).termine.minAntwortMs, 750)
|
||||
})
|
||||
|
||||
test('termineModus: leer und unbekannt sind off, pilot und on bleiben', () => {
|
||||
|
||||
Reference in new issue
Block a user