Files
Webklar-Kundenbereich/test/featureFreischaltung.test.js
T
ClaudeandClaude Sonnet 5.5 b974b3a3ec TP2 Abschlussprüfung: Zahlung/Team (Wiederzustellung, K16-Warnung, Sperrschlüssel)
- adoptPaidSession übernimmt ein erneut zugestelltes checkout.session.completed nicht mehr, wenn das Dokument nach 30 Tagen zurückgesetzt wurde
  (off, purchaseStatus zurueckgesetzt, gleiche Sitzung): kein Setup, kein Ticket, keine K15/T16
- meldeFreigeschaltet liefert true/false; die Staff-Aktivierung in Textform vermerkt eine nicht gesendete K16 mit Unterlagen in adminNote und
  antwortet mit warnung: true
- disable() nutzt die Sperre feature-activate:<id> statt feature-disable:<id> (Freischaltung und Abschalten überholen sich nicht)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-05 06:18:34 +00:00

482 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
// Freischaltung ueber das Register (Task B11): activateClosedTickets liest die Schluessel mit ticketFreischaltung aus
// config.features und fragt je Dokument unter der Sperre (nach dem frischen Lesen) den Haken freischaltbar. Termine:
// Einstellung vorhanden und laufendes Wartungs-Abo (Spec 4.5). Blockiert: das Dokument bleibt 'setup', EIN adminNote-Eintrag,
// keine Mail. websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
termineConfig: schemaKeys('termineConfig'),
contractEvents: schemaKeys('contractEvents'),
},
})
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { withLocks } = await import('../server/services/keyedLock.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { mailSink } = await import('./support/mailSink.js')
const reg = await import('../server/services/featureRegistry.js')
const sf = await import('../server/services/siteFeatures.js')
const mails = mailSink()
setMailTransportForTests(mails)
after(() => setMailTransportForTests(null))
const BASE = 'https://project.webklar.com'
const JETZT = new Date('2026-10-05T08:00:00.000Z')
const NOTIZ = /^\d{4}-\d\d-\d\dT[\d:.]+Z System: Freischaltung wartet: \S/
const TERMINE = config.features.termine
const NEWS = config.features.news
// seedFixtures gibt c1 nur ein Hosting-Abo (sub_h): Termine-Faelle ueberschreiben es mit einem Wartungs-Abo
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
const wartung2 = (patch = {}) =>
aw.seed('subscriptions', subscriptionDoc('sub_h2', { ...wartungDoc, customerId: 'c2', email: 'k2@example.invalid', stripeCustomerId: 'cus_c2', ...patch }))
let nr = 0
/**
* Dokument im Zustand 'setup' samt Projekt und Ticket (Standard: Ticket geschlossen). Liefert die ID des Dokuments.
* Termine: dazu optional die gespeicherte Einstellung (einstellung: true).
*/
function einrichtung(key, { customerId = 'c1', ticket = 'Closed', einstellung = false, ...over } = {}) {
nr += 1
const id = `f_${key}_${nr}`
const projectId = `p_${key}_${nr}`
aw.seed('websiteProjects', { $id: projectId, customerId, projectName: `Projekt ${nr}` })
aw.seed('workorders', { $id: `t_${id}`, woid: `WO-${nr}`, status: ticket, topic: 'x' })
aw.seed('portalFeatures', {
$id: id,
customerId,
projectId,
featureKey: key,
state: 'setup',
enabled: false,
siteKey: `siteke${String(nr).padStart(10, 'a')}`.slice(0, 16),
ticketId: `t_${id}`,
purchaseStatus: 'paid',
unlockedByPurchase: true,
...over,
})
if (einstellung) speichere(id, customerId, projectId)
return id
}
function speichere(featureId, customerId = 'c1', projectId = 'p') {
aw.seed('termineConfig', { $id: featureId, customerId, projectId, einstellung: JSON.stringify({ betrieb: { name: 'Salon' } }), version: 1, updatedAt: JETZT.toISOString(), updatedBy: 'test' })
}
const feld = (id, name) => aw.docs('portalFeatures').get(id)[name]
const zustand = (id) => feld(id, 'state')
const wartetEintraege = (id) => String(feld(id, 'adminNote') || '').split(' | ').filter((e) => e.includes('Freischaltung wartet'))
const k16 = (subject) => mails.sent.filter((m) => m.subject === subject)
const STUMM = (t) => {
t.mock.method(console, 'error', () => {})
t.mock.method(console, 'warn', () => {})
}
beforeEach(async () => {
for (const col of ['portalFeatures', 'workorders', 'termineConfig', 'subscriptions', 'websiteProjects', 'contractEvents']) {
for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
seedFixtures(aw)
wartung()
mails.sent.length = 0
config.termine.mode = 'pilot'
config.termine.pilotEmails = ['*']
})
afterEach(() => {
delete TERMINE.freischaltbar
delete NEWS.freischaltbar
NEWS.ticketFreischaltung = true
})
// ---------------------------------------------------------------------------------------- Register ---
test('ticketFreischaltungKeys: Register-Schluessel mit ticketFreischaltung, in Registerreihenfolge', () => {
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine'])
NEWS.ticketFreischaltung = false
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine'])
NEWS.ticketFreischaltung = 'ja'
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine'], 'nur true zaehlt')
NEWS.ticketFreischaltung = true
TERMINE.ticketFreischaltung = false
try {
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news'])
} finally {
TERMINE.ticketFreischaltung = true
}
})
test('Standard-Haken: termine hat freischaltbar, news nicht', () => {
assert.equal(typeof reg.featureHaken('termine').freischaltbar, 'function')
assert.equal(reg.featureHaken('news').freischaltbar, undefined)
})
// ------------------------------------------------------------------------------------------- News ---
test('News unveraendert: Ticket geschlossen => active, K16 der News-Pinnwand, ohne Abo, Einstellung oder Notiz', async () => {
const id = einrichtung('news')
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(feld(id, 'enabled'), true)
assert.equal(feld(id, 'activatedAt'), JETZT.toISOString())
assert.ok(!feld(id, 'adminNote'), 'keine Notiz')
assert.equal(k16('Deine News-Pinnwand ist bereit').length, 1)
})
test('News: offenes Ticket bleibt setup', async () => {
const id = einrichtung('news', { ticket: 'Open' })
assert.deepEqual(await sf.activateClosedTickets({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
})
// ------------------------------------------------------------------------------ Termine: Freigabe ---
test('Termine: Ticket geschlossen + Einstellung + Abo => active, K16 "Dein Terminplaner ist bereit" mit Knopf "Termine oeffnen"', async () => {
const id = einrichtung('termine', { einstellung: true })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(feld(id, 'enabled'), true)
assert.equal(feld(id, 'activatedAt'), JETZT.toISOString())
assert.ok(!feld(id, 'adminNote'), 'frei => keine Wartenotiz')
const gesendet = k16('Dein Terminplaner ist bereit')
assert.equal(gesendet.length, 1, mails.sent.map((m) => m.subject).join(' / '))
assert.equal(mails.sent.length, 1, 'genau eine Mail')
const [mail] = gesendet
assert.deepEqual(mail.to.map((t) => t.address), ['k@example.invalid'])
assert.match(mail.text, /Hallo Kunde Eins,\nder Terminplaner ist auf Deiner Website eingebaut und freigeschaltet\./)
assert.match(mail.text, /Funktion: Terminplaner\nProjekt: Projekt \d+/)
assert.ok(mail.text.includes(`Termine öffnen: ${BASE}/dashboard.html#termine`), mail.text)
assert.match(mail.html, /href="https:\/\/project\.webklar\.com\/dashboard\.html#termine"/)
assert.doesNotMatch(mail.text, /News|Beitr/)
// zweiter Lauf: nichts mehr offen
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(mails.sent.length, 1)
})
test('Termine: Abo past_due zaehlt als laufend (wie laeuft)', async () => {
wartung({ status: 'past_due' })
const id = einrichtung('termine', { einstellung: true })
assert.equal((await sf.activateClosedTickets({ now: JETZT })).length, 1)
assert.equal(zustand(id), 'active')
})
test('Termine: kein Abo => bleibt setup, ein adminNote-Eintrag, keine Mail; Abo kommt zurueck => naechster Lauf aktiviert und sendet K16', async (t) => {
STUMM(t)
const id = einrichtung('termine', { einstellung: true })
// c1 hat nur ein Hosting-Abo: laeuft false
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, productKey: 'hosting' }))
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(feld(id, 'enabled'), false)
assert.equal(wartetEintraege(id).length, 1)
assert.match(feld(id, 'adminNote'), NOTIZ)
assert.equal(mails.sent.length, 0, 'keine Mail alle 5 Minuten')
// zweiter und dritter Lauf: weder ein zweiter Eintrag noch eine Mail
const notiz = feld(id, 'adminNote')
assert.deepEqual(await sf.runActivation({ now: new Date(JETZT.getTime() + 5 * 60000) }), [])
assert.deepEqual(await sf.runActivation({ now: new Date(JETZT.getTime() + 10 * 60000) }), [])
assert.equal(feld(id, 'adminNote'), notiz, 'Notiz unveraendert')
assert.equal(wartetEintraege(id).length, 1)
assert.equal(mails.sent.length, 0)
// Abo kommt zurueck
wartung()
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(k16('Dein Terminplaner ist bereit').length, 1)
assert.equal(mails.sent.length, 1)
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(mails.sent.length, 1)
})
test('Termine: Abo beendet (canceled) => bleibt setup', async (t) => {
STUMM(t)
wartung({ status: 'canceled' })
const id = einrichtung('termine', { einstellung: true })
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.match(feld(id, 'adminNote'), /Freischaltung wartet/)
assert.equal(mails.sent.length, 0)
})
test('Termine: Wechsel-Widerruf (Spiegel Wartung active, juengstes wechsel-Ereignis widerrufen) => bleibt setup, keine Mail; ein neueres erledigtes Ereignis gibt frei', async (t) => {
STUMM(t)
const wechsel = (nr, status) =>
aw.seed('contractEvents', {
$id: `ev_w${nr}`,
$createdAt: new Date(Date.UTC(2026, 8, 1, 0, 0, nr)).toISOString(),
customerId: 'c1',
subscriptionId: 'sub_h',
type: 'wechsel',
productKey: 'wartung',
details: '{}',
status,
})
const id = einrichtung('termine', { einstellung: true })
wechsel(1, 'widerrufen')
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(feld(id, 'enabled'), false)
assert.equal(wartetEintraege(id).length, 1)
assert.equal(mails.sent.length, 0)
wechsel(2, 'erledigt')
assert.deepEqual((await sf.runActivation({ now: JETZT })).map((d) => d.$id), [id])
assert.equal(zustand(id), 'active')
assert.equal(k16('Dein Terminplaner ist bereit').length, 1)
})
test('Termine: keine Einstellung => bleibt setup (mit Notiz), mit gespeicherter Einstellung aktiviert der naechste Lauf', async (t) => {
STUMM(t)
const id = einrichtung('termine')
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(wartetEintraege(id).length, 1)
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(wartetEintraege(id).length, 1, 'kein zweiter Eintrag')
assert.equal(mails.sent.length, 0)
speichere(id, 'c1', feld(id, 'projectId'))
assert.equal((await sf.runActivation({ now: JETZT })).length, 1)
assert.equal(zustand(id), 'active')
assert.equal(k16('Dein Terminplaner ist bereit').length, 1)
})
test('Termine: aendert sich der Grund, kommt ein neuer Eintrag; Notiz bleibt auf 300 Zeichen und aeltere Eintraege bleiben stehen', async (t) => {
STUMM(t)
const id = einrichtung('termine', { adminNote: '2026-10-01T10:00:00.000Z Andrej: activate – Kulanz' })
await sf.activateClosedTickets({ now: JETZT })
assert.equal(wartetEintraege(id).length, 1)
assert.match(feld(id, 'adminNote'), /\| 2026-10-01T10:00:00\.000Z Andrej: activate – Kulanz$/, 'aelterer Eintrag bleibt')
assert.ok(feld(id, 'adminNote').startsWith(JETZT.toISOString()), 'neuester Eintrag vorn, mit der Zeit des Laufs')
assert.ok(feld(id, 'adminNote').length <= 300)
})
// ------------------------------------------------------------------------------- Termine: Fehler ---
test('Abo-Lesefehler => bleibt setup, runActivation wirft nicht, andere Dokumente werden trotzdem bearbeitet', async (t) => {
STUMM(t)
const termine = einrichtung('termine', { einstellung: true })
const news = einrichtung('news')
aw.failNext({ collection: 'subscriptions', status: 500 })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [news])
assert.equal(zustand(termine), 'setup', 'nie freischalten, wenn das Abo nicht lesbar war')
assert.equal(zustand(news), 'active')
assert.match(feld(termine, 'adminNote'), /Freischaltung wartet/)
assert.equal(k16('Deine News-Pinnwand ist bereit').length, 1)
assert.equal(k16('Dein Terminplaner ist bereit').length, 0)
const geloggt = console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n')
assert.match(geloggt, /Freischaltung termine \(f_termine_\d+\) nicht prüfbar/, 'Fehler wird geloggt')
// der naechste Lauf liest wieder und aktiviert
const spaeter = await sf.runActivation({ now: JETZT })
assert.deepEqual(spaeter.map((d) => d.$id), [termine])
assert.equal(zustand(termine), 'active')
})
test('Lesefehler bei der Einstellung (termineConfig) => bleibt setup, kein Wurf', async (t) => {
STUMM(t)
const id = einrichtung('termine', { einstellung: true })
aw.failNext({ collection: 'termineConfig', status: 500 })
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(id), 'setup')
assert.equal(mails.sent.length, 0)
})
test('Haken wirft => bleibt setup (Log, Notiz), die uebrigen Dokumente werden bearbeitet', async (t) => {
STUMM(t)
TERMINE.freischaltbar = async () => {
throw new Error('Pruefung kaputt')
}
const termine = einrichtung('termine', { einstellung: true })
const news = einrichtung('news')
assert.deepEqual((await sf.runActivation({ now: JETZT })).map((d) => d.$id), [news])
assert.equal(zustand(termine), 'setup')
assert.match(feld(termine, 'adminNote'), /Freischaltung wartet/)
assert.match(console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n'), /Pruefung kaputt/)
})
test('Haken meldet etwas anderes als true (false, undefined, Text) => bleibt setup (fail-closed)', async (t) => {
STUMM(t)
const id = einrichtung('termine', { einstellung: true })
for (const antwort of [false, undefined, null, 'ja', 1]) {
TERMINE.freischaltbar = async () => antwort
assert.deepEqual(await sf.activateClosedTickets({ now: JETZT }), [], String(antwort))
assert.equal(zustand(id), 'setup', String(antwort))
}
})
test('Schreiben der Wartenotiz scheitert => kein Wurf, das naechste Dokument wird trotzdem bearbeitet', async (t) => {
STUMM(t)
const termine = einrichtung('termine')
const news = einrichtung('news')
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 500 })
const done = await sf.activateClosedTickets({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [news])
assert.equal(zustand(termine), 'setup')
assert.equal(zustand(news), 'active')
})
// ------------------------------------------------------------------------ Sperre und frisches Lesen ---
test('Abo endet zwischen Listenlesen und Sperre (der Haken meldet beim frischen Lesen false) => bleibt setup', async (t) => {
STUMM(t)
const id = einrichtung('termine', { einstellung: true })
let lauf
await withLocks([`feature-activate:${id}`], async () => {
// der Lauf liest die Liste und das Ticket und wartet dann auf die Sperre
lauf = sf.activateClosedTickets({ now: JETZT })
for (let i = 0; i < 100 && aw.listCalls('portalFeatures').length === 0; i += 1) await new Promise((r) => setTimeout(r, 5))
await new Promise((r) => setTimeout(r, 50))
wartung({ status: 'canceled' })
})
assert.deepEqual(await lauf, [])
assert.equal(zustand(id), 'setup')
assert.match(feld(id, 'adminNote'), /Freischaltung wartet/)
})
test('Der Haken bekommt das unter der Sperre frisch gelesene Dokument; ein zwischendurch aktiviertes wird nicht noch einmal angefasst', async () => {
const gesehen = []
TERMINE.freischaltbar = async (doc) => {
gesehen.push({ state: doc.state, marker: doc.adminNote })
return true
}
const frisch = einrichtung('termine')
const schonAktiv = einrichtung('termine')
let lauf
await withLocks([`feature-activate:${frisch}`, `feature-activate:${schonAktiv}`], async () => {
lauf = sf.activateClosedTickets({ now: JETZT })
for (let i = 0; i < 100 && aw.listCalls('portalFeatures').length === 0; i += 1) await new Promise((r) => setTimeout(r, 5))
await new Promise((r) => setTimeout(r, 50))
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(frisch), adminNote: 'frisch gelesen' })
aw.seed('portalFeatures', { ...aw.docs('portalFeatures').get(schonAktiv), state: 'active', enabled: true })
})
const done = await lauf
assert.deepEqual(done.map((d) => d.$id), [frisch])
assert.deepEqual(gesehen, [{ state: 'setup', marker: 'frisch gelesen' }], 'der Haken sieht nur das frische, noch offene Dokument')
})
test('A3: disable() teilt die Sperre feature-activate:<id> mit der Freischaltung - wer die Freischaltung haelt, laesst das Abschalten warten', async () => {
const id = einrichtung('termine', { einstellung: true })
let abschalten
await withLocks([`feature-activate:${id}`], async () => {
abschalten = sf.disable(id, { reason: 'team' })
await new Promise((r) => setTimeout(r, 80))
assert.equal(zustand(id), 'setup', 'solange die Freischaltung die Sperre haelt, schaltet disable() nicht ab')
})
const doc = await abschalten
assert.equal(doc.state, 'disabled')
assert.equal(zustand(id), 'disabled')
})
// ------------------------------------------------------------------------------- Tickets, Kunden ---
test('Ticket Cancelled (oder Open) => bleibt setup, der Haken wird gar nicht gefragt', async () => {
let gefragt = 0
TERMINE.freischaltbar = async () => {
gefragt += 1
return true
}
const a = einrichtung('termine', { einstellung: true, ticket: 'Cancelled' })
const b = einrichtung('termine', { einstellung: true, ticket: 'Open' })
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
assert.equal(zustand(a), 'setup')
assert.equal(zustand(b), 'setup')
assert.equal(gefragt, 0)
assert.equal(mails.sent.length, 0)
})
test('customerId-Filter: nur Dokumente dieses Kunden', async () => {
wartung2()
const eins = einrichtung('termine', { einstellung: true })
const zwei = einrichtung('termine', { einstellung: true, customerId: 'c2' })
const done = await sf.runActivation({ now: JETZT, customerId: 'c1' })
assert.deepEqual(done.map((d) => d.$id), [eins])
assert.equal(zustand(eins), 'active')
assert.equal(zustand(zwei), 'setup', 'anderer Kunde bleibt liegen')
assert.deepEqual(mails.sent.map((m) => m.to[0].address), ['k@example.invalid'])
// ohne Filter kommt der andere Kunde dran
assert.deepEqual((await sf.runActivation({ now: JETZT })).map((d) => d.$id), [zwei])
assert.deepEqual(mails.sent.map((m) => m.to[0].address), ['k@example.invalid', 'k2@example.invalid'])
})
// -------------------------------------------------------------------------------- Haken je Funktion ---
test('Der Haken gilt nur fuer seine Funktion: ein News-Dokument im selben Lauf wird aktiviert', async (t) => {
STUMM(t)
const gefragt = []
TERMINE.freischaltbar = async (doc) => {
gefragt.push(doc.featureKey)
return false
}
const termine = einrichtung('termine', { einstellung: true })
const news = einrichtung('news')
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [news])
assert.equal(zustand(termine), 'setup')
assert.equal(zustand(news), 'active')
assert.deepEqual(gefragt, ['termine'], 'der Termine-Haken wird nie fuer News gefragt')
})
test('Ein Haken im Register ueberschreibt den Standard: true gibt frei, auch ohne Abo und Einstellung', async () => {
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, productKey: 'hosting' }))
TERMINE.freischaltbar = async () => true
const id = einrichtung('termine')
assert.equal((await sf.runActivation({ now: JETZT })).length, 1)
assert.equal(zustand(id), 'active')
assert.equal(k16('Dein Terminplaner ist bereit').length, 1)
})
test('Funktionen ohne ticketFreischaltung im Register werden nicht geoeffnet', async () => {
NEWS.ticketFreischaltung = false
const news = einrichtung('news')
assert.deepEqual(await sf.activateClosedTickets({ now: JETZT }), [])
assert.equal(zustand(news), 'setup')
})
test('Dokument einer anderen Funktion mit setup (analyse) bleibt unberuehrt', async () => {
const analyse = einrichtung('analyse')
const unbekannt = einrichtung('gibtsnicht')
const termine = einrichtung('termine', { einstellung: true })
const done = await sf.runActivation({ now: JETZT })
assert.deepEqual(done.map((d) => d.$id), [termine])
for (const id of [analyse, unbekannt]) {
assert.equal(zustand(id), 'setup', id)
assert.equal(feld(id, 'enabled'), false, id)
assert.ok(!feld(id, 'adminNote'), id)
}
assert.equal(mails.sent.length, 1)
})
// --------------------------------------------------------------------------------- parallele Laeufe ---
test('Parallele Laeufe (runActivation() und runActivation({ customerId })) => ein Terminplaner-K16', async () => {
const id = einrichtung('termine', { einstellung: true })
const [a, b] = await Promise.all([sf.runActivation({ now: JETZT }), sf.runActivation({ now: JETZT, customerId: 'c1' })])
assert.equal(a.length + b.length, 1)
assert.equal(zustand(id), 'active')
assert.equal(k16('Dein Terminplaner ist bereit').length, 1)
assert.equal(mails.sent.length, 1)
})
test('Parallele Laeufe ohne Abo => hoechstens eine Wartenotiz, keine Mail', async (t) => {
STUMM(t)
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, productKey: 'hosting' }))
const id = einrichtung('termine', { einstellung: true })
await Promise.all([sf.runActivation({ now: JETZT }), sf.runActivation({ now: JETZT, customerId: 'c1' }), sf.runActivation({ now: JETZT })])
assert.equal(zustand(id), 'setup')
assert.equal(wartetEintraege(id).length, 1)
assert.equal(mails.sent.length, 0)
})