203 lines
12 KiB
JavaScript
203 lines
12 KiB
JavaScript
import { test, after, beforeEach, afterEach } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
|
import { useTestEnv } from './support/testEnv.js'
|
|
import { schemaKeys } from './support/schema.js'
|
|
|
|
// Buchungskalender Task 6: oeffentliche API (Konfiguration, Belegung, Anfrage)
|
|
const NAMEN = ['portalFeatures', 'websiteProjects', 'customers', 'ukEinstellungen', 'ukUnterkuenfte', 'ukPreise', 'ukSperren', 'ukAnfragen', 'ukRechnungen']
|
|
const aw = await startAppwriteMock({
|
|
schema: Object.fromEntries(NAMEN.filter((n) => !['websiteProjects', 'customers'].includes(n)).map((n) => [n, schemaKeys(n)])),
|
|
datetimes: { ukAnfragen: ['fristAt', 'freigegebenAt', 'gebuchtAt', 'abgelehntAt', 'abgelaufenAt', 'storniertAt', 'bezahltAt'] },
|
|
defaultLimit: 25,
|
|
})
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1/v1' })
|
|
|
|
const { config } = await import('../server/config.js')
|
|
const { startTestApp, send } = await import('./support/testApp.js')
|
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|
const K = await import('../server/services/unterkunft/kontext.js')
|
|
const T = await import('../server/services/unterkunft/token.js')
|
|
const mails = await import('../server/services/unterkunft/mails.js')
|
|
const { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js')
|
|
const { default: publicRoutes, setzeUhrFuerTests } = await import('../server/routes/publicUnterkunft.js')
|
|
const app = await startTestApp({ '/api/public/unterkunft/v1': publicRoutes })
|
|
after(async () => {
|
|
await app.close()
|
|
await aw.close()
|
|
})
|
|
|
|
const SITE = 'abcdefghijklmnop'
|
|
const FEATURE = 'f_uk'
|
|
const NOW = new Date('2026-11-01T12:00:00.000Z')
|
|
const BASE = `/api/public/unterkunft/v1/${SITE}`
|
|
const ORIGIN = 'https://donau-home.example'
|
|
const vorher = { ...config.unterkunft }
|
|
|
|
beforeEach(async () => {
|
|
for (const c of NAMEN) for (const id of [...aw.docs(c).keys()]) await deleteDocument(config.collections[c], id)
|
|
Object.assign(config.unterkunft, vorher, { mode: 'on', minAntwortMs: 0, formMinSekunden: 3 })
|
|
K.leereCache()
|
|
resetTermineLimitsForTests()
|
|
mails._aufrufe.length = 0
|
|
setzeUhrFuerTests(() => NOW)
|
|
aw.seed('portalFeatures', { $id: FEATURE, customerId: 'c1', projectId: 'p1', featureKey: 'unterkunft', siteKey: SITE, state: 'active', enabled: true })
|
|
aw.seed('customers', { $id: 'c1', name: 'Mimi', email: 'mimi@example.com' })
|
|
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Donau', subdomain: 'donau', liveDomain: 'donau-home.example' })
|
|
aw.seed('ukEinstellungen', { $id: FEATURE, featureId: FEATURE, firma: 'Donau Home', ort: 'Stetten', email: 'x@y.de', steuernummer: 'GEHEIM', iban: 'DE00', akzentfarbe: '#112233' })
|
|
aw.seed('ukUnterkuenfte', { $id: 'u2', featureId: FEATURE, customerId: 'c1', projectId: 'p1', name: 'Zweite', maxPersonen: 2, mindestNaechte: 1, checkIn: '15:00', checkOut: '10:00', sort: 2, aktiv: true, icalExportToken: 'geheim' })
|
|
aw.seed('ukUnterkuenfte', { $id: 'u1', featureId: FEATURE, customerId: 'c1', projectId: 'p1', name: 'Erste', maxPersonen: 4, mindestNaechte: 2, checkIn: '15:00', checkOut: '10:00', sort: 1, aktiv: true })
|
|
aw.seed('ukUnterkuenfte', { $id: 'u3', featureId: FEATURE, name: 'Aus', maxPersonen: 4, mindestNaechte: 1, sort: 0, aktiv: false })
|
|
aw.seed('ukPreise', { $id: 'p1', unterkunftId: 'u1', featureId: FEATURE, staffeln: JSON.stringify([{ abNaechten: 1, preisProNachtCent: 10000 }]), personenInklusive: 2, steuerart: 'regel' })
|
|
})
|
|
afterEach(() => {
|
|
Object.assign(config.unterkunft, vorher)
|
|
})
|
|
|
|
const formToken = (alter = 60_000) => T.formToken(SITE, new Date(NOW.getTime() - alter))
|
|
const anfrage = (extra = {}) => ({ unterkunft: 'u1', von: '2026-11-10', bis: '2026-11-12', personen: 2, name: 'Eva Gast', firma: '', email: 'eva@example.com', telefon: '', nachricht: 'Hallo', formToken: formToken(), kontakt_url: '', ...extra })
|
|
const post = (body, headers = { origin: ORIGIN }) => send('POST', `${BASE}/anfrage`, { body, headers })
|
|
|
|
test('Konfiguration: unbekannter oder inaktiver siteKey und Modus off -> {aktiv:false}', async () => {
|
|
assert.deepEqual((await send('GET', '/api/public/unterkunft/v1/zzzzzzzzzzzzzzzz')).body, { aktiv: false })
|
|
assert.deepEqual((await send('GET', '/api/public/unterkunft/v1/kurz')).body, { aktiv: false })
|
|
aw.seed('portalFeatures', { $id: FEATURE, customerId: 'c1', projectId: 'p1', featureKey: 'unterkunft', siteKey: SITE, state: 'off', enabled: true })
|
|
K.leereCache()
|
|
assert.deepEqual((await send('GET', BASE)).body, { aktiv: false })
|
|
aw.seed('portalFeatures', { $id: FEATURE, customerId: 'c1', projectId: 'p1', featureKey: 'unterkunft', siteKey: SITE, state: 'active', enabled: true })
|
|
config.unterkunft.mode = 'off'
|
|
K.leereCache()
|
|
const r = await send('GET', BASE)
|
|
assert.deepEqual(r.body, { aktiv: false })
|
|
assert.equal(r.headers.get('access-control-allow-origin'), '*')
|
|
})
|
|
|
|
test('Konfiguration: aktiv, nur aktive Unterkuenfte sortiert, keine internen Felder', async () => {
|
|
const r = await send('GET', BASE)
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.headers.get('access-control-allow-origin'), '*')
|
|
assert.equal(r.body.aktiv, true)
|
|
assert.deepEqual(r.body.anbieter, { name: 'Donau Home', ort: 'Stetten', datenschutzUrl: 'https://donau-home.example/datenschutz' })
|
|
assert.equal(r.body.akzentfarbe, '#112233')
|
|
assert.equal(T.pruefeFormToken(SITE, r.body.formToken, new Date(NOW.getTime() + 10_000)), 'ok')
|
|
assert.deepEqual(r.body.unterkuenfte.map((u) => u.id), ['u1', 'u2'])
|
|
assert.deepEqual(Object.keys(r.body.unterkuenfte[0]).sort(), ['checkIn', 'checkOut', 'id', 'maxPersonen', 'mindestNaechte', 'name'])
|
|
const roh = JSON.stringify(r.body)
|
|
for (const geheim of ['c1', 'p1', 'GEHEIM', 'DE00', 'mimi@example.com', FEATURE, 'geheim']) assert.ok(!roh.includes(geheim), geheim)
|
|
})
|
|
|
|
test('Konfiguration: ohne Einstellungen und Domain -> Standardfarbe, datenschutzUrl null', async () => {
|
|
await deleteDocument(config.collections.ukEinstellungen, FEATURE)
|
|
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Donau' })
|
|
K.leereCache()
|
|
const r = await send('GET', BASE)
|
|
assert.equal(r.body.akzentfarbe, '#8a6a3f')
|
|
assert.equal(r.body.anbieter.datenschutzUrl, null)
|
|
assert.equal(r.body.anbieter.name, '')
|
|
})
|
|
|
|
test('Belegung: fasst angrenzende Bereiche zusammen, ohne Quelle, auf das Fenster zugeschnitten', async () => {
|
|
aw.seed('ukSperren', { $id: 's1', unterkunftId: 'u1', featureId: FEATURE, von: '2026-11-05', bis: '2026-11-08', quelle: 'manuell' })
|
|
aw.seed('ukSperren', { $id: 's2', unterkunftId: 'u1', featureId: FEATURE, von: '2026-11-08', bis: '2026-11-10', quelle: 'ical:abc', notiz: 'x' })
|
|
aw.seed('ukAnfragen', { $id: 'a1', featureId: FEATURE, unterkunftId: 'u1', nummer: 'A-AAAA', von: '2026-11-20', bis: '2026-11-25', status: 'gebucht', personen: 2 })
|
|
aw.seed('ukSperren', { $id: 's3', unterkunftId: 'u1', featureId: FEATURE, von: '2026-11-12', bis: '2026-11-14', quelle: 'manuell' })
|
|
const r = await send('GET', `${BASE}/belegung?unterkunft=u1&von=2026-11-01&bis=2026-11-22`)
|
|
assert.equal(r.status, 200)
|
|
assert.deepEqual(r.body.belegt, [
|
|
{ von: '2026-11-05', bis: '2026-11-10' },
|
|
{ von: '2026-11-12', bis: '2026-11-14' },
|
|
{ von: '2026-11-20', bis: '2026-11-22' },
|
|
])
|
|
assert.equal(r.body.heute, '2026-11-01')
|
|
assert.equal(r.body.bisMax, '2028-05-01')
|
|
assert.equal(r.headers.get('access-control-allow-origin'), '*')
|
|
})
|
|
|
|
test('Belegung: ueber 18 Monate hinaus, kaputte Daten, fremde oder inaktive Unterkunft', async () => {
|
|
assert.equal((await send('GET', `${BASE}/belegung?unterkunft=u1&von=2026-11-01&bis=2028-05-02`)).status, 400)
|
|
assert.equal((await send('GET', `${BASE}/belegung?unterkunft=u1&von=2026-11-01&bis=2028-05-01`)).status, 200)
|
|
assert.equal((await send('GET', `${BASE}/belegung?unterkunft=u1&von=2026-11-05&bis=2026-11-05`)).status, 400)
|
|
assert.equal((await send('GET', `${BASE}/belegung?unterkunft=u1&von=morgen&bis=2026-11-05`)).status, 400)
|
|
assert.equal((await send('GET', `${BASE}/belegung?von=2026-11-01&bis=2026-11-05`)).status, 400)
|
|
assert.equal((await send('GET', `${BASE}/belegung?unterkunft=u3&von=2026-11-01&bis=2026-11-05`)).status, 404)
|
|
assert.equal((await send('GET', `${BASE}/belegung?unterkunft=nix&von=2026-11-01&bis=2026-11-05`)).status, 404)
|
|
assert.deepEqual((await send('GET', `/api/public/unterkunft/v1/zzzzzzzzzzzzzzzz/belegung?unterkunft=u1&von=2026-11-01&bis=2026-11-05`)).body, { aktiv: false })
|
|
})
|
|
|
|
test('Anfrage: gueltig -> 201 {nummer} + Dokument neu, CORS-Origin gespiegelt', async () => {
|
|
const r = await post(anfrage())
|
|
assert.equal(r.status, 201)
|
|
assert.match(r.body.nummer, /^A-[A-Z2-9]{4}$/)
|
|
assert.equal(r.headers.get('access-control-allow-origin'), ORIGIN)
|
|
const docs = [...aw.docs('ukAnfragen').values()]
|
|
assert.equal(docs.length, 1)
|
|
assert.equal(docs[0].status, 'neu')
|
|
assert.equal(docs[0].nummer, r.body.nummer)
|
|
assert.equal(docs[0].personen, 2)
|
|
assert.deepEqual(mails._aufrufe.map((m) => m.art || m.template), ['G16', 'K31'])
|
|
})
|
|
|
|
test('Anfrage: Honigtopf und zu schnelles Token -> 201 mit Schein-Nummer, nichts gespeichert', async () => {
|
|
const a = await post(anfrage({ kontakt_url: 'http://spam.example' }))
|
|
assert.equal(a.status, 201)
|
|
assert.match(a.body.nummer, /^A-[A-Z2-9]{4}$/)
|
|
const b = await post(anfrage({ formToken: formToken(500) }))
|
|
assert.equal(b.status, 201)
|
|
assert.match(b.body.nummer, /^A-[A-Z2-9]{4}$/)
|
|
assert.equal(aw.docs('ukAnfragen').size, 0)
|
|
assert.equal(mails._aufrufe.length, 0)
|
|
})
|
|
|
|
test('Anfrage: fehlende E-Mail, ungueltiges Token, Fachfehler', async () => {
|
|
const a = await post(anfrage({ email: '' }))
|
|
assert.equal(a.status, 400)
|
|
assert.equal(a.body.fehler, 'ungueltig')
|
|
assert.ok(a.body.felder.email)
|
|
const b = await post(anfrage({ formToken: 'quatsch' }))
|
|
assert.equal(b.status, 400)
|
|
assert.equal(b.body.fehler, 'neu_laden')
|
|
const c = await post(anfrage({ personen: 9 }))
|
|
assert.equal(c.status, 400)
|
|
assert.equal(c.body.fehler, 'zu_viele_personen')
|
|
assert.equal(c.body.felder.maxPersonen, 4)
|
|
const d = await post(anfrage({ personen: '2', von: '2026-11-10', bis: '2026-11-11' }))
|
|
assert.equal(d.body.fehler, 'mindestnaechte')
|
|
aw.seed('ukSperren', { $id: 's1', unterkunftId: 'u1', featureId: FEATURE, von: '2026-11-10', bis: '2026-11-12', quelle: 'manuell' })
|
|
const e = await post(anfrage())
|
|
assert.equal(e.status, 409)
|
|
assert.equal(e.body.fehler, 'zeitraum_belegt')
|
|
const f = await send('POST', `${BASE}/anfrage`, { body: '{kaputt', headers: { origin: ORIGIN, 'content-type': 'application/json' } })
|
|
assert.equal(f.status, 400)
|
|
assert.equal(aw.docs('ukAnfragen').size, 0)
|
|
})
|
|
|
|
test('Anfrage: mehr als anfragenJeSiteStunde je Seite -> 429', async () => {
|
|
config.unterkunft.anfragenJeSiteStunde = 2
|
|
assert.equal((await post(anfrage({ von: '2026-11-10', bis: '2026-11-12' }))).status, 201)
|
|
assert.equal((await post(anfrage({ von: '2026-12-10', bis: '2026-12-12' }))).status, 201)
|
|
const r = await post(anfrage({ von: '2027-01-10', bis: '2027-01-12' }))
|
|
assert.equal(r.status, 429)
|
|
assert.equal(r.body.fehler, 'zu_viele_anfragen')
|
|
assert.ok(r.headers.get('retry-after'))
|
|
assert.equal(aw.docs('ukAnfragen').size, 2)
|
|
})
|
|
|
|
test('CORS: Preflight erlaubt/fremd, POST von fremder oder fehlender Origin -> 403', async () => {
|
|
const ok = await send('OPTIONS', `${BASE}/anfrage`, { headers: { origin: ORIGIN, 'access-control-request-method': 'POST' } })
|
|
assert.equal(ok.status, 204)
|
|
assert.equal(ok.headers.get('access-control-allow-origin'), ORIGIN)
|
|
const sub = await send('OPTIONS', `${BASE}/anfrage`, { headers: { origin: 'https://donau.project.webklar.com' } })
|
|
assert.equal(sub.headers.get('access-control-allow-origin'), 'https://donau.project.webklar.com')
|
|
const fremd = await send('OPTIONS', `${BASE}/anfrage`, { headers: { origin: 'https://boese.example' } })
|
|
assert.equal(fremd.status, 403)
|
|
assert.equal(fremd.headers.get('access-control-allow-origin'), null)
|
|
const p = await post(anfrage(), { origin: 'https://boese.example' })
|
|
assert.equal(p.status, 403)
|
|
assert.equal((await post(anfrage(), {})).status, 403)
|
|
assert.equal(aw.docs('ukAnfragen').size, 0)
|
|
// unbekannte Seite: Preflight verraet nichts
|
|
const unbekannt = await send('OPTIONS', '/api/public/unterkunft/v1/zzzzzzzzzzzzzzzz/anfrage', { headers: { origin: 'https://boese.example' } })
|
|
assert.equal(unbekannt.status, 204)
|
|
assert.equal(unbekannt.headers.get('access-control-allow-origin'), '*')
|
|
})
|