Push-Zeitmail: Vorlage T15, Zuordnung Pusher → Mitarbeiter, Versand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-30 13:39:12 +00:00
1 parent ec3f22d98e
commit 1a1005d099
5 files changed
+306 -3

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
import { test, before, after } from 'node:test'
import assert from 'node:assert/strict'
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
import { mailSink } from './support/mailSink.js'
let fake
let findEmployeeForPusher, buildPushTimeMail, sendPushTimeMail
let renderMail, verifyPushTimeToken, setMailTransportForTests
const KENSO = { $id: 'k', displayName: 'Kenso Grimm', shortcode: 'KNSO', email: 'kenso@webklar.com', $createdAt: '2026-01-01T00:00:00.000Z' }
const ANDREJ = { $id: 'a', displayName: 'Andrej Stevanovski', shortcode: 'ANDJ', email: '', $createdAt: '2026-01-02T00:00:00.000Z' }
const ticket = { $id: 't1', woid: '100118', customerName: 'Amin Friseur', topic: 'Website Amin Friseur', details: 'Neue Seite', status: 'Occupied' }
const worksheet = { $id: 'ws9', woid: '100118', serviceType: 'GIT' }
const commits = [
{ id: 'abc1234def', message: 'Startseite: Hero neu\n\nlanger Text' },
{ id: 'def5678abc', message: '<script>x</script> & Co' },
]
const input = (extra = {}) => ({
ticket, worksheet, repoFullName: 'WEBklar/amin-friseur', branch: 'main', commits,
pushedAt: new Date('2026-09-30T14:46:00Z'), ...extra,
})
before(async () => {
fake = await startFakeAppwrite({ seed: { employees: [ANDREJ, KENSO] } })
Object.assign(process.env, {
APPWRITE_ENDPOINT: fake.endpoint,
APPWRITE_API_KEY: 'test-key',
PUSH_TIME_SECRET: 'z'.repeat(40),
})
;({ findEmployeeForPusher, buildPushTimeMail, sendPushTimeMail } = await import('../server/services/pushTimeMail.js'))
;({ renderMail } = await import('../server/services/mailTemplates.js'))
;({ verifyPushTimeToken } = await import('../server/services/pushTimeToken.js'))
;({ setMailTransportForTests } = await import('../server/services/mailer.js'))
})
after(async () => {
setMailTransportForTests?.(null)
await fake.close()
})
test('findEmployeeForPusher: Kürzel, voller Name, ohne E-Mail, unbekannt', () => {
const employees = [ANDREJ, KENSO]
assert.equal(findEmployeeForPusher({ username: 'knso' }, employees)?.$id, 'k')
assert.equal(findEmployeeForPusher({ full_name: ' kenso grimm ' }, employees)?.$id, 'k')
assert.equal(findEmployeeForPusher({ login: 'ANDJ' }, employees), null)
assert.equal(findEmployeeForPusher({ username: 'fremd' }, employees), null)
assert.equal(findEmployeeForPusher({}, employees), null)
})
test('buildPushTimeMail: Betreff, Links, Escaping, gültiger Token', () => {
const mail = buildPushTimeMail({ employee: KENSO, ...input() })
assert.equal(mail.template, 'T15')
const out = renderMail('T15', mail.values)
assert.deepEqual(out.problems, [])
assert.equal(out.subject, '[Zeit] #100118 Amin Friseur – 2 Commit(s)')
assert.match(out.html, /Hallo Kenso,/)
assert.match(out.html, /In Arbeit/)
assert.match(out.html, /30\.09\.2026 16:46/)
for (const m of [15, 30, 45, 60, 90, 120, 180]) {
assert.match(out.html, new RegExp(`/zeit/[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\?min=${m}"`))
}
assert.match(out.html, /\/zeit\/[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"/, 'Andere Zeit ohne min')
assert.match(out.html, /https:\/\/ticket\.webklar\.com\/tickets\?woid=100118/)
assert.match(out.html, /abc1234 Startseite: Hero neu/)
assert.doesNotMatch(out.html, /langer Text/)
assert.match(out.html, /&lt;script&gt;x&lt;\/script&gt; &amp; Co/)
assert.doesNotMatch(out.html, /<script>/)
const token = mail.values.link_andere.split('/zeit/')[1]
assert.deepEqual(verifyPushTimeToken(token), { worksheetId: 'ws9', employeeId: 'k' })
})
test('buildPushTimeMail: 12 Commits → 10 Zeilen + Rest', () => {
const many = Array.from({ length: 12 }, (_, i) => ({ id: `${String(i).padStart(7, '0')}ffff`, message: `Commit ${i}` }))
const { values } = buildPushTimeMail({ employee: KENSO, ...input({ commits: many }) })
const lines = values.commits.split('\n')
assert.equal(lines.length, 11)
assert.equal(lines.at(-1), '… und 2 weitere')
assert.equal(values.anzahl_commits, 12)
})
test('buildPushTimeMail: lange Beschreibung gekürzt, ohne Kunde', () => {
const { values } = buildPushTimeMail({
employee: KENSO,
...input({ ticket: { ...ticket, customerName: '', details: 'x'.repeat(400), status: 'Weird' } }),
})
assert.equal(values.ticket_beschreibung.length, 301)
assert.ok(values.ticket_beschreibung.endsWith('…'))
assert.equal(values.kunde, 'ohne Kunde')
assert.equal(values.ticket_status, 'Weird')
})
test('sendPushTimeMail: an den Pusher, ohne BCC', async () => {
const sink = mailSink()
setMailTransportForTests(sink)
const r = await sendPushTimeMail({ ...input(), pusher: { username: 'knso' } })
assert.deepEqual(r, { sent: true })
assert.equal(sink.sent.length, 1)
assert.deepEqual(sink.sent[0].to.map((t) => t.address), ['kenso@webklar.com'])
assert.equal(sink.sent[0].from.name, 'WEBklar Kundenportal')
assert.equal(sink.sent[0].bcc, undefined)
})
test('sendPushTimeMail: unbekannter Pusher → keine Mail', async () => {
const sink = mailSink()
setMailTransportForTests(sink)
const r = await sendPushTimeMail({ ...input(), pusher: { username: 'fremd' } })
assert.equal(r.sent, false)
assert.equal(sink.sent.length, 0)
})
test('sendPushTimeMail: Versandfehler wird geschluckt', async () => {
setMailTransportForTests({ sendMail: async () => { throw new Error('SMTP kaputt') } })
const r = await sendPushTimeMail({ ...input(), pusher: { username: 'knso' } })
assert.equal(r.sent, false)
})