498 lines
24 KiB
JavaScript
498 lines
24 KiB
JavaScript
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'
|
||
|
||
// Ticket aus dem Register (Task B8): Texte je Funktion, Laengendeckel, Notfall-Details und gezielte Wiederholung.
|
||
// websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau.
|
||
const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } })
|
||
const st = await startFakeStripe()
|
||
after(() => Promise.all([aw.close(), st.close()]))
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||
const { config } = await import('../server/config.js')
|
||
const reg = await import('../server/services/featureRegistry.js')
|
||
const sf = await import('../server/services/siteFeatures.js')
|
||
const { createFeatureTicket } = await import('../server/services/ticketAdmin.js')
|
||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
const { mailSink } = await import('./support/mailSink.js')
|
||
|
||
const mails = mailSink()
|
||
setMailTransportForTests(mails)
|
||
after(() => setMailTransportForTests(null))
|
||
|
||
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin', email: 'kunde1@example.test' })
|
||
|
||
const GEKUERZT = '… gekürzt, vollständig im Ticketsystem unter Kunden → Funktionen'
|
||
const HALBES_PAAR = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/
|
||
const workorders = () => [...aw.docs('workorders').values()]
|
||
const zeilen = (n, breite = 49) => Array.from({ length: n }, (_, i) => `Zeile ${String(i).padStart(4, '0')} `.padEnd(breite, 'x')).join('\n')
|
||
|
||
let zaehler = 0
|
||
/** Projekt (Kunde c1) anlegen und die Funktion bestellen: liefert { projectId, sessionId }. */
|
||
async function bestelle(key, projekt = {}) {
|
||
zaehler += 1
|
||
const projectId = `bt${zaehler}`
|
||
const sessionId = `cs_bt${zaehler}`
|
||
aw.seed('websiteProjects', { $id: projectId, customerId: 'c1', projectName: `Projekt ${zaehler}`, ...projekt })
|
||
await sf.markOrdered({ customerId: 'c1', projectId, key, sessionId, consent: {} })
|
||
return { projectId, sessionId }
|
||
}
|
||
|
||
/** Register-Funktion tauschen und danach zurueckstellen. */
|
||
async function mitTermineDetails(fn, run) {
|
||
const alt = config.features.termine.ticket.details
|
||
config.features.termine.ticket.details = fn
|
||
try {
|
||
return await run()
|
||
} finally {
|
||
config.features.termine.ticket.details = alt
|
||
}
|
||
}
|
||
|
||
/**
|
||
* POSTs auf workorders mitschreiben ({ woid, topic, details, status }) und die ersten POSTs scheitern lassen:
|
||
* fail = Liste von { status, message } (der Nachbau liefert den Fehler) oder 'netz' (fetch wirft, kein Status).
|
||
*/
|
||
async function mitPosts(fail, run) {
|
||
const original = globalThis.fetch
|
||
const posts = []
|
||
const queue = [...fail]
|
||
globalThis.fetch = async (input, init = {}) => {
|
||
const url = new URL(String(input?.url ?? input))
|
||
if ((init.method || 'GET') === 'POST' && url.pathname.endsWith('/collections/workorders/documents')) {
|
||
const { data } = JSON.parse(init.body)
|
||
const eintrag = { woid: data.woid, topic: data.topic, details: data.details, status: null }
|
||
posts.push(eintrag)
|
||
const next = queue.shift()
|
||
if (next === 'netz') {
|
||
eintrag.status = 'netz'
|
||
throw new TypeError('fetch failed')
|
||
}
|
||
if (next) aw.failNext({ collection: 'workorders', method: 'POST', status: next.status, message: next.message })
|
||
const res = await original(input, init)
|
||
eintrag.status = res.status
|
||
return res
|
||
}
|
||
return original(input, init)
|
||
}
|
||
try {
|
||
return await run(posts)
|
||
} finally {
|
||
globalThis.fetch = original
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------------------------------------------- kuerze ---
|
||
test('kuerze: kurze Texte bleiben unveraendert, lange werden hart gekuerzt', () => {
|
||
assert.equal(reg.kuerze('abc', 5), 'abc')
|
||
assert.equal(reg.kuerze('abcde', 5, { endzeile: '…' }), 'abcde')
|
||
assert.equal(reg.kuerze('a'.repeat(10), 5), 'aaaaa')
|
||
assert.equal(reg.kuerze('abcdefghij', 6, { endzeile: '…' }), 'abcde…')
|
||
assert.equal(reg.kuerze(null, 5), '')
|
||
})
|
||
|
||
test('kuerze: nie ein halbes Zeichenpaar', () => {
|
||
const kurz = reg.kuerze('😀'.repeat(10), 5)
|
||
assert.equal(kurz, '😀😀')
|
||
assert.doesNotMatch(kurz, HALBES_PAAR)
|
||
const mitEnde = reg.kuerze('😀'.repeat(10), 6, { endzeile: '…' })
|
||
assert.equal(mitEnde, '😀😀…')
|
||
assert.doesNotMatch(mitEnde, HALBES_PAAR)
|
||
for (let max = 1; max < 12; max += 1) {
|
||
const text = reg.kuerze('a😀b😀c😀d😀e', max, { endzeile: max > 3 ? '…' : '' })
|
||
assert.ok(text.length <= max, `max ${max}: ${text.length}`)
|
||
assert.doesNotMatch(text, HALBES_PAAR, `max ${max}`)
|
||
}
|
||
})
|
||
|
||
test('kuerze: anZeile schneidet an der letzten Zeilengrenze und haengt die Endzeile in eine eigene Zeile', () => {
|
||
assert.equal(reg.kuerze('eins\nzwei\ndrei\nvier', 16, { anZeile: true, endzeile: 'ENDE' }), 'eins\nzwei\nENDE')
|
||
// die Zeile endet genau am Budget
|
||
assert.equal(reg.kuerze('aaaa\nbbbb\ncccc\ndddd', 14, { anZeile: true, endzeile: 'ENDE' }), 'aaaa\nbbbb\nENDE')
|
||
// keine Zeilengrenze im Budget: harter Schnitt (surrogatsicher)
|
||
const hart = reg.kuerze('😀'.repeat(30), 21, { anZeile: true, endzeile: 'ENDE' })
|
||
assert.equal(hart, '😀'.repeat(8) + '\nENDE')
|
||
assert.ok(hart.length <= 21)
|
||
assert.doesNotMatch(hart, HALBES_PAAR)
|
||
})
|
||
|
||
// ----------------------------------------------------------------------------- anleitungsLink ---
|
||
test('anleitungsLink: WOMS-Kunde, Reiter funktionen, Projekt kodiert', () => {
|
||
assert.equal(reg.anleitungsLink('c1', 'p1'), `${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=p1`)
|
||
assert.equal(reg.anleitungsLink('c1', 'p 1/ä'), `${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=${encodeURIComponent('p 1/ä')}`)
|
||
})
|
||
|
||
// ------------------------------------------------------------------- minimaleTicketDetails ---
|
||
test('minimaleTicketDetails: Kopfzeilen und Verweis', () => {
|
||
const ctx = {
|
||
feature: reg.featureConfig('termine'),
|
||
doc: { customerId: 'c1', projectId: 'p9', siteKey: 'abcdefghijklmnop' },
|
||
project: { projectName: 'Salon', liveDomain: 'salon.de', previewUrl: 'https://p.example', subdomain: 'salon' },
|
||
customer: { name: 'Frau Muster' },
|
||
}
|
||
const link = `${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=p9`
|
||
assert.equal(
|
||
reg.minimaleTicketDetails(ctx),
|
||
['Funktion: termine', 'Projekt: Salon', 'Domain: salon.de', 'Kunde: Frau Muster', 'siteKey: abcdefghijklmnop', `Verweis: ${link}`].join('\n')
|
||
)
|
||
assert.match(reg.minimaleTicketDetails({ ...ctx, project: { subdomain: 's', previewUrl: 'https://p.example' } }), /^Projekt: s\nDomain: https:\/\/p\.example\n/m)
|
||
// ohne Kunde und ohne Domain bleibt die Zeile bestehen
|
||
const ohne = reg.minimaleTicketDetails({ ...ctx, customer: null, project: { projectName: 'X' } })
|
||
assert.match(ohne, /^Domain: $/m)
|
||
assert.match(ohne, /^Kunde: c1$/m)
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------ ticketTexte ---
|
||
test('ticketTexte: Register-Texte, Deckel und Rueckfall bei unbrauchbaren Details', async () => {
|
||
const feature = reg.featureConfig('termine')
|
||
const doc = { customerId: 'c1', projectId: 'p9', siteKey: 'abcdefghijklmnop' }
|
||
const ctx = { feature, doc, project: { projectName: 'Salon' }, customer: { name: 'Frau Muster' } }
|
||
const minimal = reg.minimaleTicketDetails(ctx)
|
||
|
||
const normal = await mitTermineDetails(async () => 'Anleitung kurz', () => reg.ticketTexte(ctx))
|
||
assert.deepEqual(normal, { topic: 'Terminplaner einbauen – Salon', details: 'Anleitung kurz', fallbackDetails: minimal })
|
||
|
||
for (const unbrauchbar of ['', ' \n ', null, undefined, 42, {}]) {
|
||
const r = await mitTermineDetails(async () => unbrauchbar, () => reg.ticketTexte(ctx))
|
||
assert.equal(r.details, minimal, `Wert ${JSON.stringify(unbrauchbar)}`)
|
||
}
|
||
const wirft = await mitTermineDetails(async () => { throw new Error('kaputt') }, () => reg.ticketTexte(ctx))
|
||
assert.equal(wirft.details, minimal)
|
||
|
||
const lang = await mitTermineDetails(async () => zeilen(400), () => reg.ticketTexte(ctx))
|
||
assert.ok(lang.details.length <= reg.TICKET_DETAILS_MAX)
|
||
assert.ok(lang.details.endsWith(`\n${GEKUERZT}`))
|
||
|
||
const langerName = await reg.ticketTexte({ ...ctx, project: { projectName: 'N'.repeat(255) } })
|
||
assert.ok(langerName.topic.length <= reg.TICKET_TOPIC_MAX)
|
||
assert.ok(langerName.topic.startsWith('Terminplaner einbauen – NNN'))
|
||
})
|
||
|
||
test('Konstanten des Deckels', () => {
|
||
assert.equal(reg.TICKET_TOPIC_MAX, 128)
|
||
assert.equal(reg.TICKET_DETAILS_MAX, 7000)
|
||
assert.equal(reg.LANGE_DETAILS, 1000)
|
||
})
|
||
|
||
// ------------------------------------------------------------------------------ News byte-gleich ---
|
||
test('News-Ticket byte-gleich', async () => {
|
||
const { projectId, sessionId } = await bestelle('news', { projectName: 'Bäckerei', liveDomain: 'baeckerei.de' })
|
||
const { doc, created } = await sf.markPaid({ sessionId })
|
||
assert.equal(created, true)
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.equal(ticket.topic, 'News-Pinnwand einbauen – Bäckerei')
|
||
assert.equal(
|
||
ticket.details,
|
||
[
|
||
'Funktion: news',
|
||
'Projekt: Bäckerei',
|
||
'Domain: baeckerei.de',
|
||
`siteKey: ${doc.siteKey}`,
|
||
`Einbau: <div data-webklar-news="${doc.siteKey}"></div> + https://project.webklar.com/embed/news.js`,
|
||
'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)',
|
||
].join('\n')
|
||
)
|
||
assert.equal(doc.projectId, projectId)
|
||
})
|
||
|
||
// ------------------------------------------------------------------------ Termine aus dem Register ---
|
||
test('Termine-Ticket aus dem Register', async () => {
|
||
const { projectId, sessionId } = await bestelle('termine', { projectName: 'Salon Haarlinie', liveDomain: 'haarlinie.de' })
|
||
const { doc, created } = await sf.markPaid({ sessionId })
|
||
assert.equal(created, true)
|
||
assert.equal(doc.state, 'setup')
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.ok(ticket.topic.startsWith('Terminplaner einbauen – '))
|
||
assert.equal(ticket.topic, 'Terminplaner einbauen – Salon Haarlinie')
|
||
assert.ok(ticket.details.includes(doc.siteKey))
|
||
assert.ok(ticket.details.includes(`?tab=funktionen&anleitung=${projectId}`))
|
||
assert.ok(ticket.details.includes('Kunde: Kunde Eins'))
|
||
assert.ok(ticket.details.includes('Domain: haarlinie.de'))
|
||
// Task D4: die Kurzfassung der Einbauanleitung (ohne Einstellungen: Kopf, Snippet, Schritte, Verweis); Inhalt in test/termineTicket.test.js
|
||
assert.ok(ticket.details.includes('Funktion: Terminplaner\nProjekt: Salon Haarlinie\nDomain: haarlinie.de\nKunde: Kunde Eins\n'), ticket.details)
|
||
assert.ok(ticket.details.includes('Konfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)'))
|
||
assert.ok(ticket.details.includes(`data-webklar-termine="${doc.siteKey}"`))
|
||
assert.ok(ticket.details.includes('Nicht auf das Stundenkontingent anrechnen'))
|
||
assert.ok(ticket.details.endsWith(`Verweis: ${reg.anleitungsLink('c1', projectId)}`))
|
||
assert.equal(ticket.customerId, 'c1')
|
||
})
|
||
|
||
// ----------------------------------------------------------------------------------- Deckel ---
|
||
test('Deckel: Projektname mit 255 Zeichen', async () => {
|
||
const { sessionId } = await bestelle('termine', { projectName: 'P'.repeat(255) })
|
||
const { doc } = await sf.markPaid({ sessionId })
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.ok(ticket.topic.length <= 128, `topic ${ticket.topic.length}`)
|
||
assert.ok(ticket.topic.startsWith('Terminplaner einbauen – PPP'))
|
||
})
|
||
|
||
test('Deckel: 100 Emojis als Projektname ergeben eine gueltige Zeichenkette', async () => {
|
||
const { sessionId } = await bestelle('termine', { projectName: '😀'.repeat(100) })
|
||
const { doc } = await sf.markPaid({ sessionId })
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.ok(ticket.topic.length <= 128)
|
||
assert.doesNotMatch(ticket.topic, HALBES_PAAR)
|
||
assert.ok(ticket.topic.includes('😀'))
|
||
assert.doesNotMatch(ticket.details, HALBES_PAAR)
|
||
assert.equal(JSON.parse(JSON.stringify(ticket.topic)), ticket.topic)
|
||
})
|
||
|
||
test('Deckel: 20000 Zeichen Details werden an einer Zeilengrenze mit Schlusszeile gekuerzt', async () => {
|
||
const original = zeilen(400)
|
||
assert.ok(original.length > 19000)
|
||
const { sessionId } = await bestelle('termine')
|
||
const { doc } = await mitTermineDetails(async () => original, () => sf.markPaid({ sessionId }))
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.ok(ticket.details.length <= 7000, `details ${ticket.details.length}`)
|
||
assert.ok(ticket.details.length > 6800)
|
||
assert.ok(ticket.details.endsWith(`\n${GEKUERZT}`))
|
||
const rumpf = ticket.details.slice(0, -(GEKUERZT.length + 1))
|
||
assert.ok(original.startsWith(`${rumpf}\n`), 'Schnitt an einer Zeilengrenze')
|
||
assert.equal(ticket.details.split(GEKUERZT).length, 2, 'genau eine Schlusszeile')
|
||
})
|
||
|
||
test('Deckel: genau 7000 Zeichen bleiben unveraendert', async () => {
|
||
const genau = zeilen(201, 34).slice(0, 7000)
|
||
assert.equal(genau.length, 7000)
|
||
const { sessionId } = await bestelle('termine')
|
||
const { doc } = await mitTermineDetails(async () => genau, () => sf.markPaid({ sessionId }))
|
||
assert.equal(aw.docs('workorders').get(doc.ticketId).details, genau)
|
||
})
|
||
|
||
// ----------------------------------------------------------------------------------- Notfall ---
|
||
test('Notfall: wirft die Details-Funktion, entsteht das Ticket mit den Minimal-Details', async () => {
|
||
const { projectId, sessionId } = await bestelle('termine', { projectName: 'Notfall', liveDomain: 'notfall.de' })
|
||
const { doc, created } = await mitTermineDetails(async () => {
|
||
throw new Error('Anleitung nicht berechenbar')
|
||
}, () => sf.markPaid({ sessionId }))
|
||
assert.equal(created, true)
|
||
assert.equal(doc.state, 'setup')
|
||
assert.ok(doc.ticketId)
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.equal(
|
||
ticket.details,
|
||
[
|
||
'Funktion: termine',
|
||
'Projekt: Notfall',
|
||
'Domain: notfall.de',
|
||
'Kunde: Kunde Eins',
|
||
`siteKey: ${doc.siteKey}`,
|
||
`Verweis: ${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=${projectId}`,
|
||
].join('\n')
|
||
)
|
||
})
|
||
|
||
test('Notfall: leere oder keine Zeichenkette gilt wie ein Fehler', async () => {
|
||
for (const unbrauchbar of [' ', null, 7]) {
|
||
const { sessionId } = await bestelle('termine')
|
||
const { doc } = await mitTermineDetails(async () => unbrauchbar, () => sf.markPaid({ sessionId }))
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.match(ticket.details, /^Funktion: termine\n/)
|
||
assert.match(ticket.details, /\nKunde: Kunde Eins\n/)
|
||
assert.match(ticket.details, /\nVerweis: .*\?tab=funktionen&anleitung=bt\d+$/)
|
||
assert.doesNotMatch(ticket.details, /Einrichtung bezahlt/)
|
||
}
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Wiederholung nur bei langen Details ---
|
||
test('Wiederholung: lange Details und ein 400 => zweiter Versuch mit den Minimal-Details, gleiche woid', async () => {
|
||
const { projectId, sessionId } = await bestelle('termine', { projectName: 'Lang' })
|
||
const vorher = workorders().length
|
||
const lang = zeilen(100) // 5000 Zeichen
|
||
const { doc, created } = await mitTermineDetails(async () => lang, () =>
|
||
mitPosts([{ status: 400, message: 'zu lang' }], async (posts) => {
|
||
const r = await sf.markPaid({ sessionId })
|
||
assert.equal(posts.length, 2)
|
||
assert.equal(posts[0].status, 400)
|
||
assert.equal(posts[1].status, 201)
|
||
assert.equal(posts[0].woid, posts[1].woid)
|
||
assert.equal(posts[0].details, lang)
|
||
assert.equal(posts[1].details.split('\n').at(0), 'Funktion: termine')
|
||
return r
|
||
})
|
||
)
|
||
assert.equal(created, true)
|
||
assert.equal(workorders().length, vorher + 1, 'genau ein Dokument')
|
||
const ticket = aw.docs('workorders').get(doc.ticketId)
|
||
assert.ok(ticket.details.includes(`anleitung=${projectId}`))
|
||
assert.ok(ticket.details.includes(doc.siteKey))
|
||
assert.doesNotMatch(ticket.details, /Zeile 0001/, 'ohne Snippet')
|
||
assert.doesNotMatch(ticket.details, /Einrichtung bezahlt/)
|
||
})
|
||
|
||
test('Wiederholung: zweimal 400 => der erste Fehler, Dokument bleibt setup, spaeter genau ein Ticket', async () => {
|
||
const { sessionId } = await bestelle('termine', { projectName: 'Doppelt' })
|
||
const vorher = workorders().length
|
||
const lang = zeilen(100)
|
||
await mitTermineDetails(async () => lang, async () => {
|
||
await mitPosts([{ status: 400, message: 'erster Fehler' }, { status: 400, message: 'zweiter Fehler' }], async (posts) => {
|
||
await assert.rejects(sf.markPaid({ sessionId }), (err) => err.status === 400 && err.message === 'erster Fehler')
|
||
assert.equal(posts.length, 2)
|
||
})
|
||
assert.equal(workorders().length, vorher)
|
||
const stuck = (await sf.listCustomerFeatureDocs('c1')).find((d) => d.stripeSessionId === sessionId)
|
||
assert.equal(stuck.state, 'setup')
|
||
assert.ok(!stuck.ticketId)
|
||
// Stripe wiederholt: die Einrichtung wird fortgesetzt, genau ein Ticket
|
||
await mitPosts([], async (posts) => {
|
||
const { doc, created } = await sf.markPaid({ sessionId })
|
||
assert.equal(created, true)
|
||
assert.ok(doc.ticketId)
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
assert.equal(workorders().length, vorher + 1)
|
||
assert.equal((await sf.markPaid({ sessionId })).created, false)
|
||
assert.equal(workorders().length, vorher + 1)
|
||
})
|
||
})
|
||
|
||
test('Wiederholung: 503 bei langen Details wirft ohne Wiederholung (kein Doppel-Ticket)', async () => {
|
||
const { sessionId } = await bestelle('termine', { projectName: 'Fuenf Null Drei' })
|
||
const vorher = workorders().length
|
||
await mitTermineDetails(async () => zeilen(100), () =>
|
||
mitPosts([{ status: 503, message: 'nicht erreichbar' }], async (posts) => {
|
||
await assert.rejects(sf.markPaid({ sessionId }), (err) => err.message === 'nicht erreichbar')
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
)
|
||
assert.equal(workorders().length, vorher)
|
||
})
|
||
|
||
test('Wiederholung: Netzfehler bei langen Details wirft ohne Wiederholung', async () => {
|
||
const { sessionId } = await bestelle('termine', { projectName: 'Netz' })
|
||
const vorher = workorders().length
|
||
await mitTermineDetails(async () => zeilen(100), () =>
|
||
mitPosts(['netz'], async (posts) => {
|
||
await assert.rejects(sf.markPaid({ sessionId }), TypeError)
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
)
|
||
assert.equal(workorders().length, vorher)
|
||
})
|
||
|
||
test('Wiederholung: kurze Details (News, Standardtext) und ein 400 werfen ohne Wiederholung', async () => {
|
||
const vorher = workorders().length
|
||
const news = await bestelle('news', { projectName: 'Kurz News' })
|
||
await mitPosts([{ status: 400, message: 'news kaputt' }], async (posts) => {
|
||
await assert.rejects(sf.markPaid({ sessionId: news.sessionId }), (err) => err.status === 400 && err.message === 'news kaputt')
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
const termine = await bestelle('termine', { projectName: 'Kurz Termine' })
|
||
await mitTermineDetails(async () => 'nur eine kurze Zeile', () =>
|
||
mitPosts([{ status: 400, message: 'termine kaputt' }], async (posts) => {
|
||
await assert.rejects(sf.markPaid({ sessionId: termine.sessionId }), (err) => err.message === 'termine kaputt')
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
)
|
||
assert.equal(workorders().length, vorher)
|
||
// genau 1000 Zeichen zaehlen noch als kurz
|
||
const grenze = await bestelle('termine', { projectName: 'Grenze' })
|
||
await mitTermineDetails(async () => 'g'.repeat(1000), () =>
|
||
mitPosts([{ status: 400, message: 'grenze' }], async (posts) => {
|
||
await assert.rejects(sf.markPaid({ sessionId: grenze.sessionId }), (err) => err.message === 'grenze')
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
)
|
||
})
|
||
|
||
// ------------------------------------------------------------------ Termine ueber den Webhook ---
|
||
test('Termine ueber markPaid: lange Kurzfassung + 400 => Ticket mit Minimal-Details, K15/T16 gehen raus', async () => {
|
||
const { projectId, sessionId } = await bestelle('termine', { projectName: 'Webhook Salon' })
|
||
const vorherTickets = workorders().length
|
||
const vorherMails = mails.sent.length
|
||
const event = {
|
||
id: 'evt_b8',
|
||
object: 'event',
|
||
type: 'checkout.session.completed',
|
||
data: {
|
||
object: {
|
||
id: sessionId,
|
||
object: 'checkout.session',
|
||
mode: 'payment',
|
||
payment_status: 'paid',
|
||
metadata: { customerId: 'c1', projectId, featureKey: 'termine' },
|
||
},
|
||
},
|
||
}
|
||
await mitTermineDetails(async () => zeilen(100), () =>
|
||
mitPosts([{ status: 400, message: 'zu lang' }], async (posts) => {
|
||
await sf.handleFeatureCheckoutEvent(event)
|
||
assert.equal(posts.length, 2)
|
||
})
|
||
)
|
||
assert.equal(workorders().length, vorherTickets + 1)
|
||
const doc = await sf.getFeatureDoc('c1', projectId, 'termine')
|
||
assert.equal(doc.state, 'setup')
|
||
assert.ok(doc.ticketId)
|
||
assert.match(aw.docs('workorders').get(doc.ticketId).details, /^Funktion: termine\n/)
|
||
assert.equal(mails.sent.length - vorherMails, 2, 'K15 an den Kunden und T16 an das Team')
|
||
// Stripe wiederholt: nichts doppelt
|
||
await sf.handleFeatureCheckoutEvent(event)
|
||
assert.equal(workorders().length, vorherTickets + 1)
|
||
assert.equal(mails.sent.length - vorherMails, 2)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------- Analyse unberuehrt ---
|
||
test('Analyse unberuehrt: eigener Text ohne fallbackDetails, ein Aufruf, Deckel nur bei Ueberlaenge', async () => {
|
||
const project = { $id: 'pa_analyse', projectName: 'Analyse Projekt', subdomain: 'ap' }
|
||
await mitPosts([], async (posts) => {
|
||
const { ticketId } = await createFeatureTicket({
|
||
customerId: 'c1',
|
||
project,
|
||
featureKey: 'analyse',
|
||
siteKey: 'abcdefghijklmnop',
|
||
topic: 'Analyse einbauen – Analyse Projekt',
|
||
details: 'Zeile eins\nZeile zwei',
|
||
})
|
||
assert.equal(posts.length, 1)
|
||
const ticket = aw.docs('workorders').get(ticketId)
|
||
assert.equal(ticket.topic, 'Analyse einbauen – Analyse Projekt')
|
||
assert.equal(ticket.details, 'Zeile eins\nZeile zwei')
|
||
})
|
||
// Ueberlaenge wird gedeckelt
|
||
const { ticketId } = await createFeatureTicket({
|
||
customerId: 'c1',
|
||
project,
|
||
featureKey: 'analyse',
|
||
siteKey: 'abcdefghijklmnop',
|
||
topic: 'T'.repeat(200),
|
||
details: zeilen(400),
|
||
})
|
||
const lang = aw.docs('workorders').get(ticketId)
|
||
assert.ok(lang.topic.length <= 128)
|
||
assert.ok(lang.details.length <= 7000)
|
||
assert.ok(lang.details.endsWith(`\n${GEKUERZT}`))
|
||
// lange Details, 400 und kein fallbackDetails: keine Wiederholung
|
||
await mitPosts([{ status: 400, message: 'abgelehnt' }], async (posts) => {
|
||
await assert.rejects(
|
||
createFeatureTicket({ customerId: 'c1', project, featureKey: 'analyse', siteKey: 'abcdefghijklmnop', topic: 'Analyse', details: zeilen(100) }),
|
||
(err) => err.message === 'abgelehnt'
|
||
)
|
||
assert.equal(posts.length, 1)
|
||
})
|
||
})
|
||
|
||
test('Defaults: ohne topic und details der heutige News-Text', async () => {
|
||
const project = { $id: 'pa_def', projectName: 'Bäckerei', liveDomain: 'baeckerei.de' }
|
||
const { ticketId } = await createFeatureTicket({ customerId: 'c1', project, featureKey: 'news', siteKey: 'abcdefghijklmnop' })
|
||
const ticket = aw.docs('workorders').get(ticketId)
|
||
assert.equal(ticket.topic, 'News-Pinnwand einbauen – Bäckerei')
|
||
assert.equal(
|
||
ticket.details,
|
||
[
|
||
'Funktion: news',
|
||
'Projekt: Bäckerei',
|
||
'Domain: baeckerei.de',
|
||
'siteKey: abcdefghijklmnop',
|
||
'Einbau: <div data-webklar-news="abcdefghijklmnop"></div> + https://project.webklar.com/embed/news.js',
|
||
'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)',
|
||
].join('\n')
|
||
)
|
||
// nur topic gesetzt: Details bleiben der Standard
|
||
const { ticketId: zweite } = await createFeatureTicket({ customerId: 'c1', project, featureKey: 'news', siteKey: 'abcdefghijklmnop', topic: 'Mein Thema' })
|
||
assert.equal(aw.docs('workorders').get(zweite).topic, 'Mein Thema')
|
||
assert.equal(aw.docs('workorders').get(zweite).details, ticket.details)
|
||
})
|