import { test, after, beforeEach } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { schemaKeys } from './support/schema.js' import { useTestEnv } from './support/testEnv.js' import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js' // Einbau-Ticket von Oeffnungszeiten live (Aufgabe 15, Spec 4.6) und Ersatz-HTML fuer das Team (Spec 4.5, Ruling R9): // zeitenTicketDetails(ctx) und GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=&plan= hinter requireStaffJwt. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), oeffnungszeiten: schemaKeys('oeffnungszeiten'), zeitenErinnerungen: schemaKeys('zeitenErinnerungen'), }, datetimes: ZEITEN_DATETIMES, defaultLimit: 25, }) useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) const { startTestApp, send } = await import('./support/testApp.js') const { seedFixtures } = await import('./support/fixtures.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { TICKET_DETAILS_MAX } = await import('../server/services/featureRegistry.js') const { config } = await import('../server/config.js') const { zeitenTicketDetails } = await import('../server/services/zeiten/ticketDetails.js') const { speichereZeiten, speichereTeam } = await import('../server/services/zeiten/speicher.js') const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js') const { default: teamRoutes } = await import('../server/routes/team/index.js') const app = await startTestApp({ '/api/team': teamRoutes }) after(() => Promise.all([app.close(), aw.close()])) seedFixtures(aw) aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' }) const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } } const SITE_KEY = 'abcdefghijklmnop' const P1 = 'p_aaaaaaa1' const P2 = 'p_aaaaaaa2' const featureDoc = (projectId, over = {}) => ({ $id: `f_${projectId}_zeiten`, customerId: 'c1', projectId, featureKey: 'zeiten', state: 'setup', enabled: false, siteKey: SITE_KEY, ...over, }) const projekt = (over = {}) => ({ $id: 'p1', customerId: 'c1', projectName: 'Salon Haarlinie', subdomain: 'salon', ...over }) const ctxVon = (doc, extra = {}) => ({ feature: config.features.zeiten, doc, project: projekt(), customer: { name: 'Kunde Eins' }, ...extra, }) function tage(woche = {}) { const t = {} for (let d = 1; d <= 7; d += 1) { const w = woche[d] t[d] = w ? { modus: 'zeiten', fenster: [w] } : { modus: 'zu', fenster: [] } } return t } const planDaten = (id, name, extra = {}) => ({ id, name, haupt: false, tage: tage({ 1: ['09:00', '18:00'] }), ...extra }) const eingabe = (plaene) => ({ land: 'RP', plaene, feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null }, }) const EIN_PLAN = () => eingabe([planDaten(P1, 'Standard', { haupt: true })]) const ZWEI_PLAENE = () => eingabe([planDaten(P1, 'Standard', { haupt: true }), planDaten(P2, 'Weihnachten', { tage: tage({ 2: ['10:00', '14:00'] }) })]) /** Speichert Zeiten wie der Kunde (Normalisierung vergibt die Kurznamen) und liefert die Funktionsdokument-Felder. */ async function mitZeiten(doc, daten) { await speichereZeiten({ feature: doc, eingabe: daten, version: 0, now: new Date('2026-10-05T08:00:00.000Z') }) } const ersatz = (customerId, query, opts = STAFF) => send('GET', `/api/team/funktionen/${customerId}/zeiten/ersatz${query}`, opts) beforeEach(async () => { for (const collection of ['portalFeatures', 'oeffnungszeiten', 'zeitenErinnerungen', 'websiteProjects']) { for (const id of [...aw.docs(collection).keys()]) await deleteDocument(collection, id) } resetZeitenCacheForTests() aw.seed('websiteProjects', projekt()) aw.seed('websiteProjects', projekt({ $id: 'p2', projectName: 'Studio', subdomain: 'studio' })) aw.seed('websiteProjects', projekt({ $id: 'p3', customerId: 'c2', projectName: 'Fremder Salon', subdomain: 'fremd' })) }) // ------------------------------------------------------------------------------------------------ Ticket-Text test('Ticket nennt siteKey, Einbau-Zeile und Prüfliste', async () => { const doc = featureDoc('p1', { einbau: 'angestossen' }) aw.seed('portalFeatures', doc) await mitZeiten(doc, ZWEI_PLAENE()) const text = await zeitenTicketDetails(ctxVon(doc)) assert.equal(typeof text, 'string') assert.match(text, /^Funktion: zeiten$/m) assert.match(text, /^Projekt: Salon Haarlinie$/m) assert.match(text, new RegExp(`^siteKey: ${SITE_KEY}$`, 'm')) // Einbau-Zeile wortgleich assert.ok( text.split('\n').includes(``), 'Einbau-Zeile als eigene Zeile' ) assert.match(text, /automatisch eingesetzt \(nach dem nächsten Veröffentlichen prüfen\)/) assert.doesNotMatch(text, /bitte von Hand einsetzen/) // Platzhalter-Beispiele mit den Kurznamen der gespeicherten Pläne assert.match(text, /
| Montag<\/th> | 09:00–18:00<\/td>/)
assert.doesNotMatch(res.body.html, /[^\x00-\x7f]/, 'reines ASCII')
})
test('Ersatz-Endpunkt: Plan per Kurzname, unbekannter Plan ⇒ 404 ohne Rückfall auf den Hauptplan (R9)', async () => {
const doc = featureDoc('p1')
aw.seed('portalFeatures', doc)
await mitZeiten(doc, ZWEI_PLAENE())
const zweiter = await ersatz('c1', '?projectId=p1&plan=weihnachten')
assert.equal(zweiter.status, 200)
assert.match(zweiter.body.html, /| Dienstag<\/th> | 10:00–14:00<\/td>/)
// leerer Wert, falsche Schreibweise und doppelter Parameter zaehlen als angegeben und nicht gefunden
for (const query of ['&plan=gibtsnicht', '&plan=', '&plan=Standard', '&plan=standard&plan=weihnachten']) {
const res = await ersatz('c1', `?projectId=p1${query}`)
assert.equal(res.status, 404, query)
assert.deepEqual(res.body, { error: 'Plan nicht gefunden' }, query)
}
// ohne Parameter gilt der Hauptplan
const haupt = await ersatz('c1', '?projectId=p1')
assert.match(haupt.body.html, / | |
|---|