Push-Zeitmail: Webhook schickt Mail nach neuem Git-Arbeitsblatt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a28ae21389
commit
7d4767a137
4 files changed
+165
-1
No files matched your search
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '../../services/previewDeploy.js'
|
} from '../../services/previewDeploy.js'
|
||||||
import { ensureRepoPreviewConfig } from '../../services/giteaAdmin.js'
|
import { ensureRepoPreviewConfig } from '../../services/giteaAdmin.js'
|
||||||
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
||||||
|
import { sendPushTimeMail } from '../../services/pushTimeMail.js'
|
||||||
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
||||||
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
||||||
import { withLocks } from '../../services/keyedLock.js'
|
import { withLocks } from '../../services/keyedLock.js'
|
||||||
@@ -184,6 +185,22 @@ router.post('/gitea', async (req, res) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 6: Push-Zeitmail an die pushende Person - nur fuer ein neu angelegtes Arbeitsblatt,
|
||||||
|
// ohne auf den Versand zu warten; ein Fehler aendert die Webhook-Antwort nicht.
|
||||||
|
if (gitWorksheet?.created) {
|
||||||
|
sendPushTimeMail({
|
||||||
|
ticket: gitWorksheet.ticket,
|
||||||
|
worksheet: gitWorksheet.worksheet,
|
||||||
|
repoFullName: current,
|
||||||
|
branch,
|
||||||
|
commits,
|
||||||
|
pusher,
|
||||||
|
pushedAt: new Date(),
|
||||||
|
}).catch((err) => console.error('[webhook] Zeitmail:', err.message))
|
||||||
|
const { created, wsid, worksheetId } = gitWorksheet
|
||||||
|
gitWorksheet = { created, wsid, worksheetId }
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
repoFullName: current,
|
repoFullName: current,
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export async function createGitPushWorksheet({
|
|||||||
ID.unique()
|
ID.unique()
|
||||||
)
|
)
|
||||||
|
|
||||||
return { created: true, wsid, worksheetId: worksheet.$id }
|
// ticket + worksheet fuer die Push-Zeitmail (routes/webhook/gitea.js), nicht fuer die Webhook-Antwort
|
||||||
|
return { created: true, wsid, worksheetId: worksheet.$id, ticket: workorder, worksheet }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,9 @@ test('Push-Zeit in deutscher Zeit', async () => {
|
|||||||
assert.deepEqual([ws.startDate, ws.startTime, ws.endDate, ws.endTime], ['27.09.2026', '', '27.09.2026', '1646'])
|
assert.deepEqual([ws.startDate, ws.startTime, ws.endDate, ws.endTime], ['27.09.2026', '', '27.09.2026', '1646'])
|
||||||
assert.equal(ws.totalTime, 0); assert.equal(ws.isComment, true); assert.equal(ws.serviceType, 'GIT')
|
assert.equal(ws.totalTime, 0); assert.equal(ws.isComment, true); assert.equal(ws.serviceType, 'GIT')
|
||||||
assert.match(ws.details, /Commit: 26bc22e/)
|
assert.match(ws.details, /Commit: 26bc22e/)
|
||||||
|
// Push-Zeitmail: Ticket und Arbeitsblatt kommen mit zurück (nicht doppelt laden)
|
||||||
|
assert.equal(r.ticket.woid, '100115')
|
||||||
|
assert.equal(r.worksheet.$id, r.worksheetId)
|
||||||
})
|
})
|
||||||
test('zweiter Aufruf: Duplikat', async () => {
|
test('zweiter Aufruf: Duplikat', async () => {
|
||||||
assert.deepEqual(await createGitPushWorksheet({ ...base }), { skipped: true, reason: 'duplicate_commit' })
|
assert.deepEqual(await createGitPushWorksheet({ ...base }), { skipped: true, reason: 'duplicate_commit' })
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { test, before, after } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { createHmac } from 'node:crypto'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import express from 'express'
|
||||||
|
import { startFakeGitea } from './helpers/fakeGitea.js'
|
||||||
|
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
||||||
|
import { mailSink } from './support/mailSink.js'
|
||||||
|
|
||||||
|
const WEBHOOK_TOKEN = 's3cret'
|
||||||
|
const REPO = 'WEBklar/amin-friseur'
|
||||||
|
|
||||||
|
let gitea
|
||||||
|
let aw
|
||||||
|
let tmp
|
||||||
|
let originalCwd
|
||||||
|
let server
|
||||||
|
let baseUrl
|
||||||
|
let setMailTransportForTests
|
||||||
|
|
||||||
|
function pushPayload({ sha, pusher }) {
|
||||||
|
return {
|
||||||
|
ref: 'refs/heads/main',
|
||||||
|
repository: { full_name: REPO, name: 'amin-friseur', html_url: `${gitea.baseUrl}/${REPO}` },
|
||||||
|
commits: [{ id: sha, message: `Commit ${sha}`, author: { name: pusher }, url: `${gitea.baseUrl}/${REPO}/commit/${sha}` }],
|
||||||
|
pusher: { username: pusher },
|
||||||
|
after: sha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function webhook(payload) {
|
||||||
|
const bodyText = JSON.stringify(payload)
|
||||||
|
const signature = createHmac('sha256', WEBHOOK_TOKEN).update(bodyText).digest('hex')
|
||||||
|
const res = await fetch(`${baseUrl}/webhook/gitea`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Gitea-Signature': signature },
|
||||||
|
body: bodyText,
|
||||||
|
})
|
||||||
|
return { status: res.status, body: await res.json().catch(() => null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die Mail geht ohne await auf die Webhook-Antwort raus - kurz warten, bis sie da ist. */
|
||||||
|
async function waitFor(check, ms = 1000) {
|
||||||
|
const end = Date.now() + ms
|
||||||
|
while (Date.now() < end) {
|
||||||
|
if (check()) return true
|
||||||
|
await new Promise((r) => setTimeout(r, 10))
|
||||||
|
}
|
||||||
|
return check()
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
gitea = await startFakeGitea({ repos: [{ full_name: REPO }] })
|
||||||
|
aw = await startFakeAppwrite({
|
||||||
|
seed: {
|
||||||
|
websiteProjects: [{ $id: 'p1', repoFullName: REPO, projectName: 'amin-friseur', customerId: 'c1', ticketId: 't1' }],
|
||||||
|
workorders: [{ $id: 't1', woid: '100118', customerName: 'Amin Friseur', topic: 'Website', status: 'Open' }],
|
||||||
|
customers: [{ $id: 'c1', name: 'Amin Friseur' }],
|
||||||
|
employees: [{ $id: 'k', displayName: 'Kenso Grimm', shortcode: 'KNSO', email: 'kenso@webklar.com' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
originalCwd = process.cwd()
|
||||||
|
tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'push-zeitmail-'))
|
||||||
|
process.chdir(tmp)
|
||||||
|
|
||||||
|
Object.assign(process.env, {
|
||||||
|
GITEA_BASE_URL: gitea.baseUrl,
|
||||||
|
GITEA_API_TOKEN: 'test-token',
|
||||||
|
GITEA_WEBHOOK_TOKEN: WEBHOOK_TOKEN,
|
||||||
|
APPWRITE_ENDPOINT: aw.endpoint,
|
||||||
|
APPWRITE_API_KEY: 'test-key',
|
||||||
|
PREVIEW_DEPLOY_ROOT: '',
|
||||||
|
TRAEFIK_DYNAMIC_DIR: '',
|
||||||
|
PUSH_TIME_SECRET: 'w'.repeat(40),
|
||||||
|
})
|
||||||
|
|
||||||
|
const [{ default: giteaWebhookRoutes }, mailer] = await Promise.all([
|
||||||
|
import('../server/routes/webhook/gitea.js'),
|
||||||
|
import('../server/services/mailer.js'),
|
||||||
|
])
|
||||||
|
setMailTransportForTests = mailer.setMailTransportForTests
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json({ limit: '2mb', verify: (req, _res, buf) => { req.rawBody = buf } }))
|
||||||
|
app.use('/webhook', giteaWebhookRoutes)
|
||||||
|
server = await new Promise((resolve) => {
|
||||||
|
const s = app.listen(0, '127.0.0.1', () => resolve(s))
|
||||||
|
})
|
||||||
|
baseUrl = `http://127.0.0.1:${server.address().port}`
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
setMailTransportForTests?.(null)
|
||||||
|
if (server) await new Promise((resolve) => server.close(resolve))
|
||||||
|
if (originalCwd) process.chdir(originalCwd)
|
||||||
|
await gitea?.close()
|
||||||
|
await aw?.close()
|
||||||
|
if (tmp) await fs.rm(tmp, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Push von knso → eine Zeitmail, Antwort ohne Dokumente', async () => {
|
||||||
|
const sink = mailSink()
|
||||||
|
setMailTransportForTests(sink)
|
||||||
|
const r = await webhook(pushPayload({ sha: 'aaaa1111', pusher: 'knso' }))
|
||||||
|
assert.equal(r.status, 200)
|
||||||
|
assert.equal(r.body.gitWorksheet.created, true)
|
||||||
|
assert.equal(r.body.gitWorksheet.ticket, undefined)
|
||||||
|
assert.equal(r.body.gitWorksheet.worksheet, undefined)
|
||||||
|
assert.ok(r.body.gitWorksheet.worksheetId)
|
||||||
|
assert.ok(await waitFor(() => sink.sent.length === 1))
|
||||||
|
assert.deepEqual(sink.sent[0].to.map((t) => t.address), ['kenso@webklar.com'])
|
||||||
|
assert.match(sink.sent[0].subject, /^\[Zeit\] #100118 Amin Friseur/)
|
||||||
|
assert.match(sink.sent[0].html, new RegExp(`/zeit/[^"?]+\\?min=60`))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('derselbe Push erneut → keine zweite Mail', async () => {
|
||||||
|
const sink = mailSink()
|
||||||
|
setMailTransportForTests(sink)
|
||||||
|
const r = await webhook(pushPayload({ sha: 'aaaa1111', pusher: 'knso' }))
|
||||||
|
assert.equal(r.status, 200)
|
||||||
|
assert.equal(r.body.gitWorksheet.reason, 'duplicate_commit')
|
||||||
|
await new Promise((res) => setTimeout(res, 100))
|
||||||
|
assert.equal(sink.sent.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('System-Konto → keine Mail', async () => {
|
||||||
|
const sink = mailSink()
|
||||||
|
setMailTransportForTests(sink)
|
||||||
|
const r = await webhook(pushPayload({ sha: 'bbbb2222', pusher: 'webklar-system' }))
|
||||||
|
assert.equal(r.status, 200)
|
||||||
|
await new Promise((res) => setTimeout(res, 100))
|
||||||
|
assert.equal(sink.sent.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Mailfehler ändert die Webhook-Antwort nicht', async () => {
|
||||||
|
setMailTransportForTests({ sendMail: async () => { throw new Error('SMTP kaputt') } })
|
||||||
|
const r = await webhook(pushPayload({ sha: 'cccc3333', pusher: 'knso' }))
|
||||||
|
assert.equal(r.status, 200)
|
||||||
|
assert.equal(r.body.gitWorksheet.created, true)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user