721 lines
38 KiB
JavaScript
721 lines
38 KiB
JavaScript
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { startFakeStripe } from './support/fakeStripe.js'
|
||
import { mailSink } from './support/mailSink.js'
|
||
import { schemaKeys } from './support/schema.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||
|
||
// Gratisweg "Einschalten" von Oeffnungszeiten live im Wartungs-Abo (Aufgabe 16, Spec 3.1 und 3.2):
|
||
// POST /api/funktionen/zeiten/einschalten, einschaltenZeiten/holeZeitenNach (zeiten/einschalten.js), pruefeAboFuerEinschalten
|
||
// (featureOrder.js) und das Angebot in GET /api/funktionen (zeiten, features.zeiten, zeitenStatus, zeitenLeer).
|
||
const aw = await startAppwriteMock({
|
||
schema: {
|
||
portalFeatures: schemaKeys('portalFeatures'),
|
||
subscriptions: schemaKeys('subscriptions'),
|
||
featureConsents: schemaKeys('featureConsents'),
|
||
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
||
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
||
},
|
||
// Datumsfelder wie in Appwrite: Unlesbares oder '' waere dort ein 400 (leeren geht nur mit null)
|
||
datetimes: {
|
||
...ZEITEN_DATETIMES,
|
||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||
featureConsents: ['acceptedAt'],
|
||
},
|
||
defaultLimit: 25,
|
||
})
|
||
const st = await startFakeStripe()
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||
const { config } = await import('../server/config.js')
|
||
const { startTestApp, send, post } = await import('./support/testApp.js')
|
||
const { customerCookie, hostingDoc, kundenansichtCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
|
||
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
|
||
const sf = await import('../server/services/siteFeatures.js')
|
||
const { zeitenDeps } = await import('../server/services/zeiten/neubau.js')
|
||
const { einschaltenZeiten, holeZeitenNach, einschaltenDeps } = await import('../server/services/zeiten/einschalten.js')
|
||
const { pruefeAboFuerEinschalten } = await import('../server/services/featureOrder.js')
|
||
const { speichereZeiten } = await import('../server/services/zeiten/speicher.js')
|
||
const { statusZu, hauptplan } = await import('../server/services/zeiten/zeitlogik.js')
|
||
const { loeschTag } = await import('../server/services/termine/frist.js')
|
||
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
||
|
||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
|
||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||
after(() => setMailTransportForTests(null))
|
||
|
||
const Z = config.features.zeiten
|
||
const MODUS = { mode: config.zeiten.mode, pilotEmails: [...config.zeiten.pilotEmails] }
|
||
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin
|
||
const c1 = customerCookie('c1')
|
||
const KEY_P1 = 'f_p1_zeiten'
|
||
const KLEIN = 'Öffnungszeiten live'
|
||
const KEIN_ABO_TEXT = 'Ohne laufendes Wartungs-Abo kannst Du Öffnungszeiten live für einmalig 30 € bestellen.'
|
||
|
||
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
|
||
const projekt = (id, over = {}) =>
|
||
aw.seed('websiteProjects', { $id: id, customerId: 'c1', projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static', ...over })
|
||
|
||
const einschalten = (body, cookie = c1) => post('/api/funktionen/zeiten/einschalten', body, { cookie })
|
||
const haken = () => Z.consentFrei.items.filter((item) => item.haken).map((item) => item.id)
|
||
const zustimmung = (patch = {}) => ({ version: Z.consentFrei.version, haken: haken(), ...patch })
|
||
const koerper = (extra = {}) => ({ projectId: 'p1', zustimmung: zustimmung(), ...extra })
|
||
const liste = (cookie = c1) => send('GET', '/api/funktionen', { cookie })
|
||
|
||
const featureDocs = () => [...aw.docs('portalFeatures').values()]
|
||
const zeitenDoc = (projectId = 'p1') => aw.docs('portalFeatures').get(`f_${projectId}_zeiten`)
|
||
const tickets = () => [...aw.docs('workorders').values()]
|
||
const nachweise = () => [...aw.docs('featureConsents').values()]
|
||
const spuren = () => ({ features: aw.docs('portalFeatures').size, consents: aw.docs('featureConsents').size, tickets: aw.docs('workorders').size })
|
||
const keineSpuren = () => assert.deepEqual({ ...spuren(), mails: sink.sent.length, neubauten: rebuilds.length }, { features: 0, consents: 0, tickets: 0, mails: 0, neubauten: 0 })
|
||
|
||
let sink
|
||
let rebuilds
|
||
const originalRebuild = zeitenDeps.rebuild
|
||
beforeEach(async () => {
|
||
for (const col of ['subscriptions', 'portalFeatures', 'featureConsents', 'oeffnungszeiten', 'workorders', 'websiteProjects']) {
|
||
for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
|
||
}
|
||
resetZeitenCacheForTests()
|
||
seedFixtures(aw)
|
||
wartung()
|
||
projekt('p1')
|
||
projekt('p3', { customerId: 'c3' })
|
||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
|
||
config.zeiten.mode = 'pilot'
|
||
config.zeiten.pilotEmails = ['*']
|
||
for (const name of ['verfuegbar', 'bestellpruefung', 'unterlagenBereit', 'freischaltbar']) delete Z[name]
|
||
sink = mailSink()
|
||
setMailTransportForTests(sink)
|
||
rebuilds = []
|
||
zeitenDeps.rebuild = async (p) => {
|
||
rebuilds.push({ projektId: p.$id, tickets: aw.docs('workorders').size, einbau: zeitenDoc(p.$id)?.einbau, state: zeitenDoc(p.$id)?.state })
|
||
return { queued: true }
|
||
}
|
||
einschaltenDeps.jetzt = () => JETZT
|
||
mock.method(console, 'error', () => {})
|
||
mock.method(console, 'warn', () => {})
|
||
mock.method(console, 'log', () => {})
|
||
})
|
||
afterEach(() => {
|
||
mock.restoreAll()
|
||
zeitenDeps.rebuild = originalRebuild
|
||
einschaltenDeps.jetzt = () => new Date()
|
||
})
|
||
after(() => {
|
||
config.zeiten.mode = MODUS.mode
|
||
config.zeiten.pilotEmails = MODUS.pilotEmails
|
||
})
|
||
|
||
// -------------------------------------------------------------------------------------------------------- Erfolg
|
||
|
||
test('Einschalten mit laufendem Abo: setup, unlockedBy abo, Nachweis ohne Sitzung', async () => {
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||
assert.deepEqual(Object.keys(res.body).sort(), ['einbau', 'feature'])
|
||
assert.equal(res.body.einbau, 'angestossen')
|
||
assert.equal(res.body.feature.state, 'setup')
|
||
assert.equal(res.body.feature.unlockedBy, 'abo')
|
||
assert.equal(res.body.feature.einbau, 'angestossen')
|
||
assert.equal(typeof res.body.feature.ticketWoid, 'string')
|
||
|
||
const doc = zeitenDoc()
|
||
assert.deepEqual([doc.featureKey, doc.state, doc.enabled, doc.unlockedBy, doc.unlockedByPurchase], ['zeiten', 'setup', false, 'abo', false])
|
||
assert.match(doc.siteKey, /^[a-z2-7]{16}$/)
|
||
assert.equal(doc.einbau, 'angestossen')
|
||
assert.ok(doc.ticketId)
|
||
assert.ok(doc.einschaltMailAt)
|
||
assert.equal(featureDocs().length, 1, 'nur das Dokument der Zeiten')
|
||
|
||
// Nachweis: Fassung und Texte der Gratis-Zustimmung, ohne Stripe-Sitzung, mit dem handelnden Konto
|
||
const [nachweis] = nachweise()
|
||
assert.equal(nachweise().length, 1)
|
||
assert.deepEqual([nachweis.customerId, nachweis.projectId, nachweis.featureKey, nachweis.stripeSessionId], ['c1', 'p1', 'zeiten', ''])
|
||
assert.equal(nachweis.konto, 'Kunde Eins <k@example.invalid>')
|
||
assert.deepEqual(JSON.parse(nachweis.fassungen), { zustimmung: Z.consentFrei.version, leistungsbeschreibung: Z.leistungsbeschreibung.version })
|
||
assert.deepEqual(JSON.parse(nachweis.texte), [
|
||
{ id: 'leistung', text: Z.consentFrei.items[0].text, haken: true },
|
||
{ id: 'knopf', text: 'Einschalten', haken: false },
|
||
])
|
||
})
|
||
|
||
test('Reihenfolge: Neubau vor Ticket, Ticket-Text nennt einbau, dann K27, dann einschaltMailAt', async () => {
|
||
const beimVersand = []
|
||
setMailTransportForTests({
|
||
sent: sink.sent,
|
||
async sendMail(mail) {
|
||
beimVersand.push({ einschaltMailAt: zeitenDoc()?.einschaltMailAt ?? null, tickets: aw.docs('workorders').size })
|
||
return sink.sendMail(mail)
|
||
},
|
||
})
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||
|
||
// Neubau: Zustand schon gespeichert (setup), noch kein Ticket, einbau noch nicht eingetragen
|
||
assert.deepEqual(rebuilds, [{ projektId: 'p1', tickets: 0, einbau: '', state: 'setup' }])
|
||
// Ticket: einmal, Text nennt das Ergebnis des Neubaus und den siteKey
|
||
assert.equal(tickets().length, 1)
|
||
const [ticket] = tickets()
|
||
assert.equal(ticket.topic, 'Öffnungszeiten live einbauen – Projekt p1')
|
||
assert.match(ticket.details, /Einbau: automatisch eingesetzt \(nach dem nächsten Veröffentlichen prüfen\)/)
|
||
assert.ok(ticket.details.includes(`data-site="${zeitenDoc().siteKey}"`))
|
||
assert.equal(zeitenDoc().ticketId, ticket.$id)
|
||
// K27: genau eine Mail, nach dem Ticket, einschaltMailAt erst danach
|
||
assert.equal(sink.sent.length, 1)
|
||
assert.deepEqual(beimVersand, [{ einschaltMailAt: null, tickets: 1 }])
|
||
const mail = sink.sent[0]
|
||
assert.equal(mail.subject, 'Öffnungszeiten live ist eingeschaltet')
|
||
assert.deepEqual(mail.to.map((a) => a.address), ['k@example.invalid'])
|
||
assert.match(mail.text, /erscheinen nach dem nächsten Veröffentlichen/)
|
||
assert.ok(mail.text.includes(`(Ticket #${ticket.woid})`))
|
||
assert.doesNotMatch(mail.text, /nicht automatisch aktualisieren/)
|
||
assert.ok(Date.parse(zeitenDoc().einschaltMailAt) > 0)
|
||
})
|
||
|
||
test('Neubau nicht möglich: einbau manuell, Ticket und K27 sagen „Team baut ein“', async () => {
|
||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Ohne Repo' }) // kein Repo ⇒ manuell
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||
assert.equal(res.body.einbau, 'manuell')
|
||
assert.deepEqual(rebuilds, [])
|
||
assert.equal(zeitenDoc().einbau, 'manuell')
|
||
assert.match(tickets()[0].details, /Einbau: bitte von Hand einsetzen/)
|
||
assert.match(sink.sent[0].text, /nicht automatisch aktualisieren/)
|
||
assert.doesNotMatch(sink.sent[0].text, /erscheinen nach dem nächsten Veröffentlichen/)
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------------------------------ Abo
|
||
|
||
test('kein Abo / gekündigt ⇒ 409 mit Hinweis auf 30 €, keine Spur', async () => {
|
||
aw.seed('subscriptions', hostingDoc)
|
||
const keins = await einschalten(koerper())
|
||
assert.equal(keins.status, 409)
|
||
assert.equal(keins.body.error, KEIN_ABO_TEXT)
|
||
assert.equal(keins.body.code, 'kein_abo')
|
||
assert.equal(keins.body.grund, 'kein_abo')
|
||
keineSpuren()
|
||
|
||
wartung({ cancelAtPeriodEnd: true })
|
||
const gekuendigt = await einschalten(koerper())
|
||
assert.equal(gekuendigt.status, 409)
|
||
assert.equal(gekuendigt.body.error, KEIN_ABO_TEXT)
|
||
assert.equal(gekuendigt.body.code, 'gekuendigt')
|
||
keineSpuren()
|
||
|
||
const ohne = await einschalten({ projectId: 'p3', zustimmung: zustimmung() }, customerCookie('c3'))
|
||
assert.equal(ohne.status, 409)
|
||
assert.equal(ohne.body.code, 'kein_abo')
|
||
keineSpuren()
|
||
})
|
||
|
||
test('Abo nicht aktiv ⇒ 409 „sobald Dein Abo aktiv ist“, keine Spur', async () => {
|
||
for (const status of ['trialing', 'past_due']) {
|
||
wartung({ status })
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 409, status)
|
||
assert.equal(res.body.error, 'Einschaltbar, sobald Dein Abo aktiv ist.', status)
|
||
assert.equal(res.body.code, 'nicht_aktiv', status)
|
||
keineSpuren()
|
||
}
|
||
})
|
||
|
||
test('Abo-Lesefehler ⇒ 503, nichts geschrieben, kein Rohtext', async () => {
|
||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'Interner Appwrite-Text mit Kunde c1' })
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 503)
|
||
assert.equal(res.body.error, 'Gerade nicht prüfbar – bitte versuch es gleich noch einmal.')
|
||
assert.equal(JSON.stringify(res.body).includes('Appwrite'), false)
|
||
keineSpuren()
|
||
})
|
||
|
||
test('pruefeAboFuerEinschalten liest frisch: ok ⇒ nichts, Lesefehler ⇒ 503 ohne Rohtext im Log', async () => {
|
||
await pruefeAboFuerEinschalten('c1') // Wartung aktiv
|
||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'Geheimer Text' })
|
||
await assert.rejects(pruefeAboFuerEinschalten('c1'), (err) => err.status === 503 && err.message === 'Gerade nicht prüfbar – bitte versuch es gleich noch einmal.')
|
||
const log = console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n')
|
||
assert.doesNotMatch(log, /Geheimer Text/)
|
||
// zwei Aufrufe hintereinander lesen zweimal (kein Zwischenspeicher): nach dem Ende des Abos sieht der zweite es sofort
|
||
wartung({ status: 'canceled', endedAt: '2026-09-01T00:00:00.000Z' })
|
||
await assert.rejects(pruefeAboFuerEinschalten('c1'), (err) => err.status === 409 && err.code === 'kein_abo')
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------------------- Vorpruefungen
|
||
|
||
test('Kundenansicht ⇒ 403, keine Spur', async () => {
|
||
const res = await einschalten(koerper(), kundenansichtCookie())
|
||
assert.equal(res.status, 403)
|
||
assert.equal(res.body.error, 'In der Kundenansicht kann nicht eingeschaltet werden.')
|
||
// die Sperre kommt vor jeder anderen Pruefung, auch vor der Zustimmung
|
||
assert.equal((await einschalten({}, kundenansichtCookie())).status, 403)
|
||
keineSpuren()
|
||
})
|
||
|
||
test('ohne Anmeldung 401', async () => {
|
||
assert.equal((await post('/api/funktionen/zeiten/einschalten', koerper())).status, 401)
|
||
keineSpuren()
|
||
})
|
||
|
||
test('Zustimmung fehlt oder veraltet ⇒ 400/409 wie beim Checkout, mit der Gratis-Fassung, keine Spur', async () => {
|
||
for (const body of [{ projectId: 'p1' }, koerper({ zustimmung: zustimmung({ haken: [] }) }), koerper({ zustimmung: zustimmung({ haken: ['leistung', 'beginn'] }) }), koerper({ zustimmung: 'ja' })]) {
|
||
const res = await einschalten(body)
|
||
assert.equal(res.status, 400, JSON.stringify(body))
|
||
assert.equal(res.body.code, 'zustimmung_fehlt')
|
||
assert.equal(res.body.error, 'Bitte stimme der Erklärung zu, um zu bestellen.')
|
||
}
|
||
const alt = await einschalten(koerper({ zustimmung: zustimmung({ version: '2020-01-01' }) }))
|
||
assert.equal(alt.status, 409)
|
||
assert.equal(alt.body.code, 'fassung_veraltet')
|
||
// die neue Fassung ist die des Gratisweges (ein Haken, Knopf „Einschalten“), nicht die der Bestellung
|
||
assert.deepEqual(alt.body.consent, {
|
||
version: Z.consentFrei.version,
|
||
knopf: 'Einschalten',
|
||
items: Z.consentFrei.items.map(({ id, text }) => ({ id, text, haken: true })),
|
||
})
|
||
keineSpuren()
|
||
})
|
||
|
||
test('Altform { consent: true } (News, ohne Fassungsprüfung) wird bei dieser neuen Route nicht angenommen: 400, keine Spur', async () => {
|
||
// sonst würde nach einer Textänderung der Gratis-Zustimmung ein alter Tab den neuen Text bestätigen, ohne ihn gesehen zu haben
|
||
for (const body of [{ projectId: 'p1', consent: true }, { projectId: 'p1', consent: true, zustimmung: null }, { projectId: 'p1', consent: true, zustimmung: {} }]) {
|
||
const res = await einschalten(body)
|
||
assert.equal(res.status, 400, JSON.stringify(body))
|
||
assert.equal(res.body.code, 'zustimmung_fehlt')
|
||
}
|
||
keineSpuren()
|
||
// die Form mit Fassung und Haken geht weiter
|
||
assert.equal((await einschalten(koerper({ consent: true }))).status, 200)
|
||
})
|
||
|
||
test('Projekt: fehlt ⇒ 400, fremd oder unbekannt ⇒ 403, archiviert ⇒ 409', async () => {
|
||
assert.equal((await einschalten({ zustimmung: zustimmung() })).status, 400)
|
||
assert.equal((await einschalten(koerper({ projectId: 'p3' }))).status, 403)
|
||
assert.equal((await einschalten(koerper({ projectId: 'gibtsnicht' }))).status, 403)
|
||
keineSpuren()
|
||
projekt('p_alt', { archived: true })
|
||
const res = await einschalten(koerper({ projectId: 'p_alt' }))
|
||
assert.equal(res.status, 409)
|
||
assert.match(res.body.error, /archiviert/)
|
||
keineSpuren()
|
||
})
|
||
|
||
test('Schalter und Pilotliste: aus oder nicht in der Liste ⇒ 409 nicht verfügbar, keine Spur', async () => {
|
||
config.zeiten.mode = 'off'
|
||
const aus = await einschalten(koerper())
|
||
assert.equal(aus.status, 409)
|
||
assert.equal(aus.body.code, 'nicht_verfuegbar')
|
||
assert.equal(aus.body.error, Z.unavailableMessage)
|
||
config.zeiten.mode = 'pilot'
|
||
config.zeiten.pilotEmails = ['andere@example.test']
|
||
assert.equal((await einschalten(koerper())).status, 409)
|
||
keineSpuren()
|
||
config.zeiten.pilotEmails = ['k@example.invalid']
|
||
assert.equal((await einschalten(koerper())).status, 200)
|
||
})
|
||
|
||
test('Vertragsunterlagen nicht abrufbar ⇒ 503, keine Spur', async () => {
|
||
Z.unterlagenBereit = async () => false
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 503)
|
||
assert.equal(res.body.code, 'unterlagen_fehlen')
|
||
keineSpuren()
|
||
})
|
||
|
||
test('Nachweis nicht speicherbar ⇒ 500 ohne Rohtext, Zustand bleibt off, kein Ticket, keine Mail', async () => {
|
||
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500, message: 'Interner Text' })
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 500)
|
||
assert.equal(JSON.stringify(res.body).includes('Interner Text'), false)
|
||
keineSpuren()
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------------------- Zustaende
|
||
|
||
test('zweiter Klick ist idempotent und holt Fehlendes nach: Ticket, dann genau ein K27', async () => {
|
||
aw.failNext({ collection: 'workorders', method: 'POST', status: 500, message: 'Ticketsystem down' })
|
||
const erster = await einschalten(koerper())
|
||
// Zustand ist gespeichert, das Ticket fehlt: der Kunde bekommt einen Hinweis zum erneuten Versuch, nicht den Rohtext
|
||
assert.equal(erster.status, 503)
|
||
assert.equal(JSON.stringify(erster.body).includes('Ticketsystem'), false)
|
||
assert.equal(zeitenDoc().state, 'setup')
|
||
assert.equal(zeitenDoc().unlockedBy, 'abo')
|
||
assert.ok(!zeitenDoc().ticketId)
|
||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||
assert.equal(tickets().length, 0)
|
||
assert.equal(sink.sent.length, 0)
|
||
assert.equal(nachweise().length, 1)
|
||
assert.equal(rebuilds.length, 1)
|
||
|
||
const zweiter = await einschalten(koerper())
|
||
assert.equal(zweiter.status, 200, JSON.stringify(zweiter.body))
|
||
assert.equal(tickets().length, 1)
|
||
assert.equal(zeitenDoc().ticketId, tickets()[0].$id)
|
||
assert.equal(sink.sent.length, 1)
|
||
assert.ok(zeitenDoc().einschaltMailAt)
|
||
assert.equal(nachweise().length, 1, 'kein zweiter Nachweis')
|
||
assert.equal(rebuilds.length, 1, 'einbau steht schon, kein zweiter Neubau')
|
||
assert.equal(zweiter.body.einbau, 'angestossen')
|
||
|
||
// dritter Klick: alles da, nichts passiert
|
||
const dritter = await einschalten(koerper())
|
||
assert.equal(dritter.status, 200)
|
||
assert.deepEqual([tickets().length, sink.sent.length, nachweise().length, rebuilds.length], [1, 1, 1, 1])
|
||
})
|
||
|
||
test('K27 scheitert: Zustand und Ticket bleiben, einschaltMailAt leer, der nächste Klick sendet nach (ein Ticket, eine Mail)', async () => {
|
||
setMailTransportForTests({ sent: [], async sendMail() { throw new Error('SMTP down') } })
|
||
const erster = await einschalten(koerper())
|
||
assert.equal(erster.status, 200, 'Mail-Fehler kosten die Antwort nichts')
|
||
assert.equal(tickets().length, 1)
|
||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||
setMailTransportForTests(sink)
|
||
assert.equal((await einschalten(koerper())).status, 200)
|
||
assert.equal(tickets().length, 1)
|
||
assert.equal(sink.sent.length, 1)
|
||
assert.ok(zeitenDoc().einschaltMailAt)
|
||
assert.match(sink.sent[0].text, new RegExp(`Ticket #${tickets()[0].woid}`))
|
||
})
|
||
|
||
test('zwei gleichzeitige Klicks: ein Nachweis, ein Neubau, ein Ticket, eine K27', async () => {
|
||
const [a, b] = await Promise.all([einschalten(koerper()), einschalten(koerper())])
|
||
assert.deepEqual([a.status, b.status], [200, 200], JSON.stringify([a.body, b.body]))
|
||
assert.deepEqual([nachweise().length, rebuilds.length, tickets().length, sink.sent.length], [1, 1, 1, 1])
|
||
assert.equal(zeitenDoc().ticketId, tickets()[0].$id)
|
||
})
|
||
|
||
test('Zustand active mit abo: idempotent 200 ohne Schreibzugriff', async () => {
|
||
assert.equal((await einschalten(koerper())).status, 200)
|
||
await updateDocument('portalFeatures', KEY_P1, { state: 'active', enabled: true })
|
||
const vorher = { ...zeitenDoc() }
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 200)
|
||
assert.equal(res.body.feature.state, 'active')
|
||
assert.deepEqual(zeitenDoc(), vorher)
|
||
assert.deepEqual([nachweise().length, rebuilds.length, tickets().length, sink.sent.length], [1, 1, 1, 1])
|
||
})
|
||
|
||
test('andere Zustände: ordered, setup/active vom Kauf ⇒ 409 Meldung der Bestellung; disabled ⇒ 409 Meldung der Abschaltung', async () => {
|
||
const faelle = [
|
||
[{ state: 'ordered' }, Z.orderedConflictMessage],
|
||
[{ state: 'setup', unlockedBy: 'kauf', unlockedByPurchase: true }, Z.orderedConflictMessage],
|
||
[{ state: 'active', enabled: true, unlockedBy: 'kauf', unlockedByPurchase: true }, Z.orderedConflictMessage],
|
||
[{ state: 'disabled', disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' }, Z.disabledMessage],
|
||
[{ state: 'disabled', disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z', unlockedBy: 'abo' }, Z.disabledMessage],
|
||
]
|
||
for (const [felder, text] of faelle) {
|
||
for (const id of aw.docs('portalFeatures').keys()) await deleteDocument('portalFeatures', id)
|
||
aw.seed('portalFeatures', { $id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', enabled: false, siteKey: 'abcdefghijklmnop', ...felder })
|
||
const vorher = { ...zeitenDoc() }
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 409, felder.state + felder.unlockedBy)
|
||
assert.equal(res.body.error, text, felder.state)
|
||
assert.deepEqual(zeitenDoc(), vorher)
|
||
}
|
||
assert.deepEqual([nachweise().length, rebuilds.length, tickets().length, sink.sent.length], [0, 0, 0, 0])
|
||
})
|
||
|
||
test('Abo zuerst: ist es beendet, hilft auch ein vorhandenes setup nicht (409 kein_abo), nichts wird nachgeholt', async () => {
|
||
aw.failNext({ collection: 'workorders', method: 'POST', status: 500 })
|
||
assert.equal((await einschalten(koerper())).status, 503)
|
||
wartung({ status: 'canceled', endedAt: '2026-09-01T00:00:00.000Z' })
|
||
const res = await einschalten(koerper())
|
||
assert.equal(res.status, 409)
|
||
assert.equal(res.body.code, 'kein_abo')
|
||
assert.equal(tickets().length, 0)
|
||
})
|
||
|
||
test('Dokument im Zustand off mit Resten wird sauber auf setup gestellt', async () => {
|
||
aw.seed('portalFeatures', {
|
||
$id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'off', enabled: false, siteKey: 'abcdefghijklmnop',
|
||
disabledAt: '2026-08-01T10:00:00.000Z', disabledReason: 'abo_ende', aboEndeAt: '2026-07-01T10:00:00.000Z', behaltenSessionId: 'cs_alt',
|
||
})
|
||
assert.equal((await einschalten(koerper())).status, 200)
|
||
const doc = zeitenDoc()
|
||
assert.deepEqual([doc.state, doc.siteKey, doc.disabledAt, doc.disabledReason, doc.aboEndeAt, doc.behaltenSessionId], ['setup', 'abcdefghijklmnop', null, null, null, ''])
|
||
})
|
||
|
||
// --------------------------------------------------------------------------------------------- Team und Nachholen
|
||
|
||
test('Team: kein Abo nötig, unlockedBy team, kein Nachweis, Neubau, Ticket, keine K27, Notiz', async () => {
|
||
aw.seed('subscriptions', hostingDoc)
|
||
const { doc, changed } = await einschaltenZeiten({ customerId: 'c1', projectId: 'p1', by: 'team', staff: { name: 'Andrej' }, now: JETZT })
|
||
assert.equal(changed, true)
|
||
assert.deepEqual([doc.state, doc.unlockedBy, doc.unlockedByPurchase, doc.einbau], ['setup', 'team', false, 'angestossen'])
|
||
assert.match(doc.adminNote, /Andrej/)
|
||
assert.equal(nachweise().length, 0)
|
||
assert.equal(tickets().length, 1)
|
||
assert.equal(sink.sent.length, 0)
|
||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||
assert.equal(rebuilds.length, 1)
|
||
// ein zweiter Aufruf aendert nichts
|
||
const nochmal = await einschaltenZeiten({ customerId: 'c1', projectId: 'p1', by: 'team', staff: { name: 'Andrej' }, now: JETZT })
|
||
assert.equal(nochmal.changed, false)
|
||
assert.deepEqual([tickets().length, rebuilds.length], [1, 1])
|
||
})
|
||
|
||
test('Team aus disabled: zurück auf setup, Neubau erneut, kein neues Ticket', async () => {
|
||
aw.seed('portalFeatures', {
|
||
$id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'disabled', enabled: false, siteKey: 'abcdefghijklmnop',
|
||
unlockedBy: 'abo', einbau: 'angestossen', ticketId: 'ticket_alt', disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z', aboEndeAt: '2026-09-01T10:00:00.000Z',
|
||
})
|
||
const { doc } = await einschaltenZeiten({ customerId: 'c1', projectId: 'p1', by: 'team', now: JETZT })
|
||
assert.deepEqual([doc.state, doc.unlockedBy, doc.disabledAt, doc.disabledReason, doc.aboEndeAt, doc.ticketId], ['setup', 'team', null, null, null, 'ticket_alt'])
|
||
assert.equal(rebuilds.length, 1)
|
||
assert.equal(doc.einbau, 'angestossen')
|
||
assert.equal(tickets().length, 0)
|
||
})
|
||
|
||
test('Team aus disabled bei einem gekauften Dokument: kauf und unlockedByPurchase bleiben (Widerrufs-Zuordnung), einbau bleibt, kein Neubau', async () => {
|
||
for (const [nr, kauf] of [[1, { unlockedBy: 'kauf', unlockedByPurchase: true }], [2, { unlockedByPurchase: true }]]) {
|
||
const projectId = `p_k${nr}`
|
||
projekt(projectId)
|
||
aw.seed('portalFeatures', {
|
||
$id: `f_${projectId}_zeiten`, customerId: 'c1', projectId, featureKey: 'zeiten', state: 'disabled', enabled: false, siteKey: 'abcdefghijklmnop',
|
||
einbau: 'angestossen', ticketId: 'ticket_alt', disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z', ...kauf,
|
||
})
|
||
const { doc } = await einschaltenZeiten({ customerId: 'c1', projectId, by: 'team', now: JETZT })
|
||
assert.deepEqual([doc.state, sf.unlockedByVon(doc), doc.unlockedByPurchase, doc.disabledAt, doc.disabledReason, doc.ticketId, doc.einbau], ['setup', 'kauf', true, null, null, 'ticket_alt', 'angestossen'], `Fall ${nr}`)
|
||
}
|
||
assert.deepEqual([rebuilds.length, tickets().length], [0, 0], 'bei gekauften Dokumenten kümmert sich markPaid um Neubau und Ticket, nicht das Nachholen')
|
||
// ein Dokument, das nie gekauft war, wird weiter ein Team-Dokument; ohne Dokument ebenso
|
||
aw.seed('portalFeatures', {
|
||
$id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'disabled', enabled: false, siteKey: 'abcdefghijklmnop',
|
||
unlockedBy: 'abo', unlockedByPurchase: false, einbau: 'angestossen', ticketId: 'ticket_alt',
|
||
})
|
||
const team = await einschaltenZeiten({ customerId: 'c1', projectId: 'p1', by: 'team', now: JETZT })
|
||
assert.deepEqual([team.doc.unlockedBy, team.doc.unlockedByPurchase], ['team', false])
|
||
})
|
||
|
||
test('holeZeitenNach: Ticket fehlt in setup ⇒ anlegen; K27 nur bei unlockedBy abo und leerem einschaltMailAt, danach Marker', async () => {
|
||
aw.seed('portalFeatures', { $id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'setup', enabled: false, siteKey: 'abcdefghijklmnop', unlockedBy: 'abo', einbau: 'manuell' })
|
||
const doc = await holeZeitenNach(zeitenDoc(), { now: JETZT })
|
||
assert.equal(tickets().length, 1)
|
||
assert.equal(doc.ticketId, tickets()[0].$id)
|
||
assert.equal(Date.parse(doc.einschaltMailAt), JETZT.getTime())
|
||
assert.equal(sink.sent.length, 1)
|
||
assert.deepEqual(rebuilds, [], 'einbau steht schon: kein Neubau')
|
||
// noch einmal: nichts
|
||
await holeZeitenNach(zeitenDoc(), { now: JETZT })
|
||
assert.deepEqual([tickets().length, sink.sent.length], [1, 1])
|
||
})
|
||
|
||
test('holeZeitenNach: Neubau nur bei leerem einbau; kauf, active und off bleiben unberührt; team bekommt keine K27', async () => {
|
||
const seed = (id, felder) => {
|
||
projekt(id)
|
||
aw.seed('portalFeatures', { $id: `f_${id}_zeiten`, customerId: 'c1', projectId: id, featureKey: 'zeiten', siteKey: 'abcdefghijklmnop', enabled: false, ...felder })
|
||
return aw.docs('portalFeatures').get(`f_${id}_zeiten`)
|
||
}
|
||
for (const doc of [
|
||
seed('p_kauf', { state: 'setup', unlockedBy: 'kauf', unlockedByPurchase: true }),
|
||
seed('p_aktiv', { state: 'active', enabled: true, unlockedBy: 'abo' }),
|
||
seed('p_aus', { state: 'off' }),
|
||
seed('p_wartet', { state: 'ordered' }),
|
||
]) {
|
||
const vorher = { ...doc }
|
||
assert.deepEqual(await holeZeitenNach(doc, { now: JETZT }), vorher)
|
||
}
|
||
assert.deepEqual([tickets().length, sink.sent.length, rebuilds.length], [0, 0, 0])
|
||
|
||
const team = seed('p_team', { state: 'setup', unlockedBy: 'team' })
|
||
const fertig = await holeZeitenNach(team, { now: JETZT })
|
||
assert.deepEqual([rebuilds.map((r) => r.projektId), tickets().length, sink.sent.length], [['p_team'], 1, 0])
|
||
assert.equal(fertig.einbau, 'angestossen')
|
||
assert.ok(!fertig.einschaltMailAt)
|
||
assert.match(tickets()[0].details, /automatisch eingesetzt/, 'das Ticket kennt das frische einbau')
|
||
})
|
||
|
||
test('holeZeitenNach: Kunde ohne Adresse oder gelöscht ⇒ einschaltMailAt wird gesetzt (nichts zu senden), Lesefehler ⇒ wirft und lässt den Marker leer', async () => {
|
||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: '', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
|
||
aw.seed('portalFeatures', { $id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'setup', enabled: false, siteKey: 'abcdefghijklmnop', unlockedBy: 'abo', einbau: 'manuell', ticketId: 'ticket_da' })
|
||
const doc = await holeZeitenNach(zeitenDoc(), { now: JETZT })
|
||
assert.ok(doc.einschaltMailAt)
|
||
assert.equal(sink.sent.length, 0)
|
||
|
||
await updateDocument('portalFeatures', KEY_P1, { einschaltMailAt: null })
|
||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
|
||
aw.failNext({ collection: 'customers', status: 500 })
|
||
await assert.rejects(holeZeitenNach(zeitenDoc(), { now: JETZT }))
|
||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||
})
|
||
|
||
// ----------------------------------------------------------------------------------------------------- Angebot
|
||
|
||
const angebot = async (cookie = c1) => (await liste(cookie)).body.zeiten
|
||
const FREI = () => ({
|
||
consent: { version: Z.consentFrei.version, knopf: 'Einschalten', items: Z.consentFrei.items.map(({ id, text }) => ({ id, text, haken: true })) },
|
||
leistungsbeschreibung: { version: Z.leistungsbeschreibung.version, url: Z.leistungsbeschreibung.url },
|
||
})
|
||
|
||
test('Angebot: nicht verfügbar ⇒ genau { verfuegbar: false }, das Abo wird nicht gelesen', async () => {
|
||
config.zeiten.mode = 'off'
|
||
aw.resetListCalls()
|
||
assert.deepEqual(await angebot(), { verfuegbar: false })
|
||
assert.equal(aw.listCalls('subscriptions').length, 0)
|
||
config.zeiten.mode = 'pilot'
|
||
config.zeiten.pilotEmails = ['andere@example.test']
|
||
assert.deepEqual(await angebot(), { verfuegbar: false })
|
||
})
|
||
|
||
test('Angebot: inklusive true/false/null je nach Abo-Lage, Preis und Texte aus dem Register', async () => {
|
||
// Ruling R32: die Belehrung der Bestellseite ist genau der Text, den der Haken belehrung der Zeiten auch in den Nachweis schreibt
|
||
const { featureHaken } = await import('../server/services/featureRegistry.js')
|
||
const belehrung = await featureHaken('zeiten').belehrung()
|
||
assert.match(belehrung, /Muster-Widerrufsformular/)
|
||
const erwartet = (inklusive, grund) => ({ verfuegbar: true, inklusive, grund, preis: '30 €', einschalten: FREI(), belehrung })
|
||
assert.deepEqual(await angebot(), erwartet(true, null))
|
||
assert.deepEqual(Object.keys(await angebot()), ['verfuegbar', 'inklusive', 'grund', 'preis', 'einschalten', 'belehrung'])
|
||
|
||
aw.seed('subscriptions', hostingDoc)
|
||
assert.deepEqual(await angebot(), erwartet(false, 'kein_abo'), 'nur Hosting')
|
||
assert.deepEqual(await angebot(customerCookie('c3')), erwartet(false, 'kein_abo'), 'gar kein Abo')
|
||
wartung({ cancelAtPeriodEnd: true })
|
||
assert.deepEqual(await angebot(), erwartet(false, 'gekuendigt'))
|
||
wartung({ status: 'past_due' })
|
||
assert.deepEqual(await angebot(), erwartet(false, 'nicht_aktiv'))
|
||
|
||
wartung()
|
||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'Geheim' })
|
||
const lesefehler = await liste()
|
||
assert.equal(lesefehler.status, 200, 'ein Abo-Lesefehler kostet die Liste nichts')
|
||
assert.deepEqual(lesefehler.body.zeiten, erwartet(null, null))
|
||
assert.doesNotMatch(console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n'), /Geheim/)
|
||
})
|
||
|
||
test('Angebot: die Register-Ansicht der Zeiten bleibt unverändert (keine neuen Schlüssel)', async () => {
|
||
const { features } = (await liste()).body
|
||
assert.deepEqual(Object.keys(features.zeiten).sort(), ['avv', 'consent', 'key', 'kurz', 'leistungsbeschreibung', 'name', 'priceLabel', 'requiresWartungsAbo', 'returnHash', 'setupEstimate', 'widerruf'])
|
||
assert.deepEqual(features.zeiten.consent.items.map((i) => i.id), ['leistung', 'beginn'], 'die bezahlte Zustimmung')
|
||
})
|
||
|
||
test('Ansicht features.zeiten: unlockedBy, aboEndeAt, einbau; loeschungAm nur bei disabled mit abo_ende', async () => {
|
||
const ansicht = async (felder) => {
|
||
for (const id of aw.docs('portalFeatures').keys()) await deleteDocument('portalFeatures', id)
|
||
if (felder) aw.seed('portalFeatures', { $id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', enabled: false, siteKey: 'abcdefghijklmnop', ...felder })
|
||
return (await liste()).body.projects.find((p) => p.projectId === 'p1').features.zeiten
|
||
}
|
||
const neu = await ansicht(null)
|
||
assert.deepEqual([neu.state, neu.unlockedBy, neu.aboEndeAt, neu.einbau, neu.loeschungAm], ['off', null, null, null, null])
|
||
|
||
const setup = await ansicht({ state: 'setup', unlockedBy: 'abo', einbau: 'manuell', aboEndeAt: '2026-10-01T10:00:00.000Z' })
|
||
assert.deepEqual([setup.state, setup.unlockedBy, setup.einbau, setup.loeschungAm], ['setup', 'abo', 'manuell', null])
|
||
assert.equal(Date.parse(setup.aboEndeAt), Date.parse('2026-10-01T10:00:00.000Z'))
|
||
|
||
const alt = await ansicht({ state: 'active', enabled: true, unlockedByPurchase: true })
|
||
assert.equal(alt.unlockedBy, 'kauf', 'Altbestand ohne unlockedBy: aus unlockedByPurchase abgeleitet')
|
||
const teamAlt = await ansicht({ state: 'active', enabled: true })
|
||
assert.equal(teamAlt.unlockedBy, 'team')
|
||
|
||
const aboEnde = await ansicht({ state: 'disabled', unlockedBy: 'abo', disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' })
|
||
assert.deepEqual([aboEnde.disabledReason, aboEnde.loeschungAm], ['abo_ende', '2026-11-04'])
|
||
assert.equal(aboEnde.loeschungAm, loeschTag('2026-10-04T23:30:00.000Z'), 'Berliner Tag')
|
||
const team = await ansicht({ state: 'disabled', unlockedBy: 'team', disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' })
|
||
assert.equal(team.loeschungAm, null)
|
||
|
||
// andere Funktionen behalten ihre Ansicht; einzige Erweiterung (V7, Kundenansicht neu): unlockedBy gilt fuer jede Funktion (null solange sie aus ist)
|
||
const news = (await liste()).body.projects[0].features.news
|
||
assert.equal('einbau' in news || 'aboEndeAt' in news, false)
|
||
assert.equal(news.unlockedBy, null)
|
||
})
|
||
|
||
// ----------------------------------------------------------------------------------------------------- Projektliste
|
||
|
||
function tage(modus) {
|
||
const t = {}
|
||
for (let d = 1; d <= 7; d += 1) t[d] = { modus, fenster: modus === 'zeiten' ? [['09:00', '18:00']] : [] }
|
||
return t
|
||
}
|
||
const eingabe = (modus) => ({
|
||
land: 'RP',
|
||
plaene: [{ id: 'p_aaaaaaa1', name: 'Standard', haupt: true, tage: tage(modus) }],
|
||
feiertage: {},
|
||
ausnahmen: [],
|
||
hinweise: [],
|
||
kontakt: { an: false, telefon: '', adresse: '', whatsapp: null },
|
||
})
|
||
async function mitZeiten(projectId, state, modus) {
|
||
aw.seed('portalFeatures', { $id: `f_${projectId}_zeiten`, customerId: 'c1', projectId, featureKey: 'zeiten', state, enabled: state === 'active', siteKey: 'abcdefghijklmnop', unlockedBy: 'abo' })
|
||
if (modus) await speichereZeiten({ feature: zeitenDoc(projectId), eingabe: eingabe(modus), version: 0, now: JETZT })
|
||
}
|
||
|
||
test('Projektliste enthält zeitenStatus und zeitenLeer ohne Zusatzabfrage pro Kachel', async () => {
|
||
projekt('p_offen')
|
||
projekt('p_zu')
|
||
projekt('p_leer')
|
||
projekt('p_aus')
|
||
projekt('p_bestellt')
|
||
projekt('p_abgeschaltet')
|
||
await mitZeiten('p_offen', 'active', 'zeiten')
|
||
await mitZeiten('p_zu', 'setup', 'zu')
|
||
await mitZeiten('p_leer', 'setup', null)
|
||
await mitZeiten('p_bestellt', 'ordered', 'zeiten')
|
||
await mitZeiten('p_abgeschaltet', 'disabled', 'zeiten')
|
||
// p1 und p_aus ohne Dokument
|
||
|
||
const abfragen = []
|
||
const echt = globalThis.fetch
|
||
globalThis.fetch = async (url, optionen) => {
|
||
if (/\/collections\/oeffnungszeiten\/documents/.test(String(url))) abfragen.push(`${optionen?.method ?? 'GET'} ${String(url).replace(/\?.*/, '').split('/documents')[1]}`)
|
||
return echt(url, optionen)
|
||
}
|
||
let res
|
||
try {
|
||
res = await liste()
|
||
} finally {
|
||
globalThis.fetch = echt
|
||
}
|
||
assert.equal(res.status, 200)
|
||
const von = (id) => res.body.projects.find((p) => p.projectId === id)
|
||
|
||
const daten = eingabe('zeiten')
|
||
assert.deepEqual(von('p_offen').zeitenStatus, statusZu(daten, hauptplan(daten), JETZT))
|
||
assert.equal(von('p_offen').zeitenStatus.offen, true)
|
||
assert.deepEqual(Object.keys(von('p_offen').zeitenStatus).sort(), ['kurz', 'offen', 'text'])
|
||
assert.equal(von('p_offen').zeitenLeer, false)
|
||
assert.equal(von('p_zu').zeitenStatus.offen, false)
|
||
assert.equal(von('p_zu').zeitenLeer, false)
|
||
// keine gespeicherten Zeiten: kein Status, leer
|
||
for (const id of ['p_leer', 'p1', 'p_aus']) assert.deepEqual([von(id).zeitenStatus, von(id).zeitenLeer], [null, true], id)
|
||
// bestellt oder abgeschaltet: Daten da, aber nicht live
|
||
for (const id of ['p_bestellt', 'p_abgeschaltet']) assert.deepEqual([von(id).zeitenStatus, von(id).zeitenLeer], [null, false], id)
|
||
// je Projekt mit Dokument eine Abfrage, keine fuer die anderen
|
||
assert.equal(abfragen.length, 5, abfragen.join(' | '))
|
||
assert.equal(abfragen.every((a) => a.startsWith('GET /')), true)
|
||
})
|
||
|
||
test('Projektliste: nicht verfügbar ⇒ keine Zeiten gelesen; Lesefehler bei einem Projekt kostet die Liste nichts', async () => {
|
||
await mitZeiten('p1', 'active', 'zeiten')
|
||
config.zeiten.mode = 'off'
|
||
const abfragen = []
|
||
const echt = globalThis.fetch
|
||
globalThis.fetch = async (url, optionen) => {
|
||
if (/\/collections\/oeffnungszeiten\//.test(String(url))) abfragen.push(String(url))
|
||
return echt(url, optionen)
|
||
}
|
||
try {
|
||
const aus = await liste()
|
||
assert.deepEqual([aus.body.projects[0].zeitenStatus, aus.body.projects[0].zeitenLeer], [null, true])
|
||
} finally {
|
||
globalThis.fetch = echt
|
||
}
|
||
assert.equal(abfragen.length, 0)
|
||
|
||
config.zeiten.mode = 'pilot'
|
||
aw.failNext({ collection: 'oeffnungszeiten', status: 500, message: 'Geheim' })
|
||
const res = await liste()
|
||
assert.equal(res.status, 200)
|
||
const p1 = res.body.projects.find((p) => p.projectId === 'p1')
|
||
assert.deepEqual([p1.zeitenStatus, p1.zeitenLeer], [null, false])
|
||
assert.doesNotMatch(console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n'), /Geheim/)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------------------------------- siteFeatures
|
||
|
||
test('assertOwnProject: forNewOrder sperrt archivierte Projekte mit 409, ohne die Option bleibt alles wie vorher', async () => {
|
||
projekt('p_alt', { archived: true })
|
||
assert.equal((await sf.assertOwnProject('c1', 'p_alt')).$id, 'p_alt')
|
||
await assert.rejects(sf.assertOwnProject('c1', 'p_alt', { forNewOrder: true }), (err) => err.status === 409)
|
||
assert.equal((await sf.assertOwnProject('c1', 'p1', { forNewOrder: true })).$id, 'p1')
|
||
})
|