import { test, after } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' // Terminplaner als zweite Funktion (Aufgabe 17): Team-Route /:customerId/:key, GET /api/funktionen mit Schalter, // Kunde loeschen schaltet den Terminplaner ab. News verhaelt sich wie bisher (test/funktionenRoutes.test.js). const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions') }, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { startTestApp, get, rawGet, post } = await import('./support/testApp.js') const { customerCookie, seedFixtures } = await import('./support/fixtures.js') const { default: funktionenRoutes } = await import('../server/routes/funktionen.js') const { default: teamRoutes } = await import('../server/routes/team/index.js') const sf = await import('../server/services/siteFeatures.js') const { deleteCustomerWithPortalAccess } = await import('../server/services/customerAdmin.js') const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/team': teamRoutes }) after(() => Promise.all([app.close(), aw.close(), st.close()])) seedFixtures(aw) aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' }) aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Salon' }) aw.seed('websiteProjects', { $id: 'pt2', customerId: 'c1', projectName: 'Studio' }) aw.seed('websiteProjects', { $id: 'pt3', customerId: 'c2', projectName: 'Fremder Salon' }) const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } } const cookie = customerCookie('c1') const team = (customerId, key, body, opts = STAFF) => post(`/api/team/funktionen/${customerId}/${key}`, body, opts) const featuresOf = (res, projectId) => res.body.projects.find((p) => p.projectId === projectId).features const workorders = () => aw.docs('workorders').size const docOf = (projectId, key) => sf.getFeatureDoc('c1', projectId, key) test('Team aktiviert termine fuer ein Projekt, News bleibt unveraendert', async () => { const tickets = workorders() const res = await team('c1', 'termine', { projectId: 'pt1', action: 'activate', note: 'Pilot' }) assert.equal(res.status, 200) assert.deepEqual(res.body.projects.map((p) => p.projectId), ['pt1']) const view = featuresOf(res, 'pt1') assert.equal(view.termine.state, 'active') assert.equal(view.termine.ticketWoid, null) assert.match(view.termine.siteKey, /^[a-z2-7]{16}$/) assert.match(view.termine.adminNote, /^\d{4}-\d\d-\d\dT[\d:.]+Z Andrej: activate – Pilot$/) assert.equal(view.termine.ticketMissing, false) assert.equal(view.news.state, 'off') assert.equal(view.news.adminNote, '') const doc = await docOf('pt1', 'termine') assert.deepEqual([doc.featureKey, doc.state, doc.enabled, doc.unlockedByPurchase], ['termine', 'active', true, false]) assert.equal(await docOf('pt1', 'news'), null, 'kein News-Dokument entstanden') assert.equal(workorders(), tickets, 'kein Ticket') // das Projekt ohne Aktion bleibt aus const list = await get('/api/team/funktionen/c1', STAFF) assert.equal(list.projects.find((p) => p.projectId === 'pt2').features.termine.state, 'off') }) test('/news bleibt gleichwertig und aendert den Terminplaner nicht', async () => { const res = await team('c1', 'news', { projectId: 'pt1', action: 'activate', note: 'Kulanz' }) assert.equal(res.status, 200) const view = featuresOf(res, 'pt1') assert.equal(view.news.state, 'active') assert.match(view.news.adminNote, /Andrej: activate – Kulanz$/) assert.equal(view.termine.state, 'active') assert.doesNotMatch(view.termine.adminNote, /Kulanz/) assert.notEqual((await docOf('pt1', 'news')).siteKey, (await docOf('pt1', 'termine')).siteKey) assert.equal((await team('c1', 'news', { projectId: 'pt1', action: 'disable' })).status, 200) const after = await docOf('pt1', 'termine') assert.equal(after.state, 'active') }) test('Abschalten setzt disabledAt, erneutes Aktivieren leert es (R36)', async () => { const off = await team('c1', 'termine', { projectId: 'pt1', action: 'disable' }) assert.equal(off.status, 200) assert.equal(featuresOf(off, 'pt1').termine.state, 'disabled') const doc = await docOf('pt1', 'termine') assert.equal(doc.enabled, false) assert.ok(Math.abs(Date.parse(doc.disabledAt) - Date.now()) < 5000) const on = await team('c1', 'termine', { projectId: 'pt1', action: 'activate' }) assert.equal(featuresOf(on, 'pt1').termine.state, 'active') assert.equal((await docOf('pt1', 'termine')).disabledAt, null) }) test('unbekannter Schluessel -> 404, auch /kalender; nichts wird angelegt', async () => { const docs = aw.docs('portalFeatures').size for (const key of ['kalender', 'analyse', 'Termine', 'termine2']) { const res = await team('c1', key, { projectId: 'pt2', action: 'activate' }) assert.equal(res.status, 404, key) assert.equal(typeof res.body.error, 'string', key) } // der Schluessel wird vor dem Rumpf geprueft assert.equal((await team('c1', 'kalender', {})).status, 404) assert.equal(aw.docs('portalFeatures').size, docs) assert.equal(await docOf('pt2', 'termine'), null) }) test('Meldungen sind je Funktion grammatisch', async () => { aw.seed('websiteProjects', { $id: 'pt4', customerId: 'c1', projectName: 'Ohne Dokument' }) const termine = await team('c1', 'termine', { projectId: 'pt4', action: 'disable' }) assert.equal(termine.status, 409) assert.equal(termine.body.error, 'Für dieses Projekt gibt es noch keinen Terminplaner') const news = await team('c1', 'news', { projectId: 'pt4', action: 'disable' }) assert.equal(news.status, 409) assert.equal(news.body.error, 'Für dieses Projekt gibt es noch keine News-Pinnwand') assert.equal(await docOf('pt4', 'termine'), null) }) test('Pruefungen wie bei News: Aktion, Projekt, fremdes Projekt, Kunde', async () => { assert.equal((await team('c1', 'termine', { projectId: 'pt1', action: 'loeschen' })).status, 400) assert.equal((await team('c1', 'termine', { action: 'activate' })).status, 400) assert.equal((await team('c1', 'termine', { projectId: 'pt3', action: 'activate' })).status, 403) assert.equal((await team('c1', 'termine', { projectId: 'gibtsnicht', action: 'activate' })).status, 403) assert.equal(await sf.getFeatureDoc('c2', 'pt3', 'termine'), null) const asCustomer = await team('c1', 'termine', { projectId: 'pt2', action: 'activate' }, { cookie }) assert.ok(asCustomer.status === 401 || asCustomer.status === 403) assert.equal(await docOf('pt2', 'termine'), null) }) test('Die Ansicht des Teams liest die Funktionsdokumente des Kunden einmal', async (t) => { const echt = globalThis.fetch const abfragen = [] globalThis.fetch = async (url, optionen) => { if (/\/collections\/portalFeatures\/documents/.test(String(url))) abfragen.push(`${optionen?.method ?? 'GET'} ${String(url).replace(/\?.*/, '')}`) return echt(url, optionen) } t.after(() => (globalThis.fetch = echt)) const list = await get('/api/team/funktionen/c1', STAFF) assert.ok(list.projects.length >= 3) assert.equal(abfragen.length, 1, abfragen.join(' | ')) assert.match(abfragen[0], /^GET .*\/collections\/portalFeatures\/documents$/) }) // ------------------------------------------------------------- Kundenliste --- const MODUS = { mode: config.termine.mode, pilotEmails: [...config.termine.pilotEmails] } const termineMit = (mode, pilotEmails = []) => { config.termine.mode = mode config.termine.pilotEmails = pilotEmails } after(() => termineMit(MODUS.mode, MODUS.pilotEmails)) const verfuegbar = async (c = cookie) => (await get('/api/funktionen', { cookie: c })).termine test('GET /api/funktionen: termine.verfuegbar je Modus und Pilot-E-Mail', async () => { termineMit('off') assert.deepEqual(await verfuegbar(), { verfuegbar: false }) termineMit('pilot', ['k@example.invalid']) assert.deepEqual(await verfuegbar(), { verfuegbar: true }) assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: false }, 'Kunde nicht in der Pilotliste') termineMit('pilot', []) assert.deepEqual(await verfuegbar(), { verfuegbar: false }) termineMit('pilot', ['*']) assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: true }) termineMit('on') assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: true }) }) test('GET /api/funktionen: Projekte tragen features.termine und News wie bisher', async () => { termineMit('pilot', ['*']) const body = await get('/api/funktionen', { cookie }) assert.equal(body.setupEstimate, 'meist innerhalb von 1–2 Wochen') const pt1 = body.projects.find((p) => p.projectId === 'pt1') assert.equal(pt1.features.termine.state, 'active') assert.equal(pt1.features.news.state, 'disabled') const pt2 = body.projects.find((p) => p.projectId === 'pt2') assert.deepEqual([pt2.features.termine.state, pt2.features.news.state], ['off', 'off']) assert.deepEqual(Object.keys(pt2.features), ['news', 'termine']) // ohne Sitzung wie bisher gesperrt assert.equal((await rawGet('/api/funktionen')).status, 401) }) // --------------------------------------------------------- Kunde loeschen --- test('Kunde loeschen schaltet seine Terminplaner ab (Notiz Kunde geloescht), nur seine', async () => { aw.seed('customers', { $id: 'kt1', name: 'Weg Salon', email: 'weg@example.invalid' }) aw.seed('websiteProjects', { $id: 'ktp1', customerId: 'kt1', projectName: 'Weg Eins' }) aw.seed('websiteProjects', { $id: 'ktp2', customerId: 'kt1', projectName: 'Weg Zwei' }) aw.seed('websiteProjects', { $id: 'ktp3', customerId: 'kt1', projectName: 'Weg Drei' }) await sf.adminActivate('kt1', 'ktp1', 'termine', { by: 'test' }) await sf.adminActivate('kt1', 'ktp1', 'news', { by: 'test' }) await sf.adminActivate('kt1', 'ktp2', 'termine', { by: 'test' }) const schonAus = await sf.adminActivate('kt1', 'ktp3', 'termine', { by: 'test' }) const frueher = (await sf.disable(schonAus.$id, { by: 'test' })).disabledAt const fremd = await sf.getFeatureDoc('c1', 'pt1', 'termine') await deleteCustomerWithPortalAccess('kt1') assert.equal(aw.docs('customers').has('kt1'), false) for (const projekt of ['ktp1', 'ktp2']) { const doc = await sf.getFeatureDoc('kt1', projekt, 'termine') assert.deepEqual([doc.state, doc.enabled, doc.adminNote], ['disabled', false, 'Kunde gelöscht'], projekt) assert.ok(Math.abs(Date.parse(doc.disabledAt) - Date.now()) < 5000, projekt) } assert.equal((await sf.getFeatureDoc('kt1', 'ktp3', 'termine')).disabledAt, frueher, 'Beginn der 30 Tage bleibt') assert.equal((await sf.getFeatureDoc('kt1', 'ktp1', 'news')).state, 'active', 'News ist nicht Sache des Terminplaners') assert.equal((await sf.getFeatureDoc('c1', 'pt1', 'termine')).state, fremd.state, 'anderer Kunde') }) test('Kunde loeschen gelingt auch, wenn das Abschalten scheitert (Fehler im Log)', async (t) => { const log = t.mock.method(console, 'error', () => {}) aw.seed('customers', { $id: 'kt2', name: 'Weg Zwei', email: 'weg2@example.invalid' }) aw.seed('websiteProjects', { $id: 'ktp4', customerId: 'kt2', projectName: 'Weg Vier' }) await sf.adminActivate('kt2', 'ktp4', 'termine', { by: 'test' }) aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 500, message: 'kaputt' }) await deleteCustomerWithPortalAccess('kt2') assert.equal(aw.docs('customers').has('kt2'), false) assert.ok(log.mock.calls.some((c) => /Terminplaner von Kunde kt2 nicht abgeschaltet/.test(String(c.arguments[0])))) })