Files
Webklar-Kundenbereich/test/featureTicket.test.js
T

498 lines
24 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 } 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)
})