Projektarten: neue Repos erkennen und Zuordnungs-Mail senden
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de4b6f3a35
commit
1f1ffb5275
7 files changed
+332
-3
No files matched your search
@@ -54,7 +54,10 @@ after(async () => {
|
||||
})
|
||||
|
||||
test('importNewRepos legt nur fehlende an', async () => {
|
||||
assert.deepEqual(await importNewRepos(), { created: 2 })
|
||||
const r = await importNewRepos()
|
||||
assert.equal(r.created, 2)
|
||||
assert.deepEqual(r.createdRepos.map((x) => x.repo.full_name).sort(), ['JUSN/neu', 'knso/privat'])
|
||||
assert.ok(r.createdRepos.every((x) => x.projectId))
|
||||
const neu = aw.all('websiteProjects').find((d) => d.repoFullName === 'JUSN/neu')
|
||||
assert.deepEqual([neu.projectName, neu.status, neu.subdomain || '', neu.previewUrl || ''], ['Döner Nation', 'synced', '', ''])
|
||||
assert.ok(aw.all('websiteProjects').some((d) => d.repoFullName === 'knso/privat'))
|
||||
|
||||
@@ -137,6 +137,7 @@ export async function startFakeGitea(options = {}) {
|
||||
private: Boolean(def.private),
|
||||
description: def.description || '',
|
||||
empty: false,
|
||||
created_at: def.created_at || new Date().toISOString(),
|
||||
}
|
||||
repos.set(keyFrom(fullName), record)
|
||||
return record
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { test, before, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHmac } from 'node:crypto'
|
||||
import express from 'express'
|
||||
import { startFakeGitea } from './helpers/fakeGitea.js'
|
||||
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
const TOKEN = 's3cret'
|
||||
const NOW = new Date()
|
||||
const ago = (h) => new Date(NOW.getTime() - h * 3600_000).toISOString()
|
||||
|
||||
let gitea, aw, server, baseUrl, setMailTransportForTests, runRepoAssignJob, sink
|
||||
|
||||
async function hook(event, payload) {
|
||||
const body = JSON.stringify(payload)
|
||||
const res = await fetch(`${baseUrl}/webhook/gitea`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Gitea-Event': event,
|
||||
'X-Gitea-Signature': createHmac('sha256', TOKEN).update(body).digest('hex'),
|
||||
},
|
||||
body,
|
||||
})
|
||||
return { status: res.status, body: await res.json().catch(() => null) }
|
||||
}
|
||||
const created = (full, extra = {}) => ({
|
||||
action: 'created',
|
||||
repository: { full_name: full, name: full.split('/')[1], html_url: `${gitea.baseUrl}/${full}`, created_at: NOW.toISOString(), owner: { login: full.split('/')[0] } },
|
||||
sender: { login: 'knso' },
|
||||
...extra,
|
||||
})
|
||||
const docsOf = (full) => aw.all('websiteProjects').filter((d) => d.repoFullName === full)
|
||||
|
||||
before(async () => {
|
||||
gitea = await startFakeGitea({ repos: [] })
|
||||
aw = await startFakeAppwrite({
|
||||
seed: {
|
||||
websiteProjects: [],
|
||||
employees: [{ $id: 'k', displayName: 'Kenso Grimm', shortcode: 'KNSO', email: 'kenso@webklar.com' }],
|
||||
},
|
||||
})
|
||||
Object.assign(process.env, {
|
||||
GITEA_BASE_URL: gitea.baseUrl, GITEA_API_TOKEN: 'test-token', GITEA_WEBHOOK_TOKEN: TOKEN,
|
||||
APPWRITE_ENDPOINT: aw.endpoint, APPWRITE_API_KEY: 'test-key', PREVIEW_DEPLOY_ROOT: '', TRAEFIK_DYNAMIC_DIR: '',
|
||||
})
|
||||
const [{ default: routes }, mailer, job] = await Promise.all([
|
||||
import('../server/routes/webhook/gitea.js'),
|
||||
import('../server/services/mailer.js'),
|
||||
import('../server/services/repoAssignJob.js'),
|
||||
])
|
||||
setMailTransportForTests = mailer.setMailTransportForTests
|
||||
runRepoAssignJob = job.runRepoAssignJob
|
||||
const app = express()
|
||||
app.use(express.json({ verify: (req, _r, buf) => { req.rawBody = buf } }))
|
||||
app.use('/webhook', routes)
|
||||
server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)) })
|
||||
baseUrl = `http://127.0.0.1:${server.address().port}`
|
||||
})
|
||||
after(async () => {
|
||||
setMailTransportForTests?.(null)
|
||||
await new Promise((r) => server.close(r))
|
||||
await gitea.close()
|
||||
await aw.close()
|
||||
})
|
||||
const freshSink = () => { sink = mailSink(); setMailTransportForTests(sink) }
|
||||
|
||||
test('repository/created → 1 Dokument + 1 Mail; Wiederholung → keine zweite', async () => {
|
||||
freshSink()
|
||||
const r = await hook('repository', created('knso/neu1'))
|
||||
assert.equal(r.status, 200)
|
||||
assert.equal(docsOf('knso/neu1').length, 1)
|
||||
assert.equal(docsOf('knso/neu1')[0].createdByGitea, 'knso')
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.match(sink.sent[0].html, /ticket\.webklar\.com\/projects\/zuordnen\//)
|
||||
await hook('repository', created('knso/neu1'))
|
||||
assert.equal(docsOf('knso/neu1').length, 1)
|
||||
assert.equal(sink.sent.length, 1)
|
||||
})
|
||||
|
||||
test('gleichzeitig Webhook + Job für dasselbe Repo → 1 Dokument, 1 Mail', async () => {
|
||||
freshSink()
|
||||
gitea.addRepo({ full_name: 'knso/race', created_at: NOW.toISOString() })
|
||||
await Promise.all([hook('repository', created('knso/race')), runRepoAssignJob({ now: NOW }), hook('repository', created('knso/race'))])
|
||||
assert.equal(docsOf('knso/race').length, 1)
|
||||
assert.equal(sink.sent.length, 1)
|
||||
})
|
||||
|
||||
test('repository/deleted und ohne Signatur', async () => {
|
||||
freshSink()
|
||||
const before = aw.all('websiteProjects').length
|
||||
const r = await hook('repository', { ...created('knso/weg'), action: 'deleted' })
|
||||
assert.equal(r.status, 200)
|
||||
assert.equal(aw.all('websiteProjects').length, before)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
const res = await fetch(`${baseUrl}/webhook/gitea`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Gitea-Event': 'repository' }, body: '{}' })
|
||||
assert.equal(res.status, 401)
|
||||
})
|
||||
|
||||
test('Job: 20 alte Repos → 0 Mails', async () => {
|
||||
freshSink()
|
||||
for (let i = 0; i < 20; i++) gitea.addRepo({ full_name: `knso/alt${i}`, created_at: ago(72) })
|
||||
const c = await runRepoAssignJob({ now: NOW })
|
||||
assert.equal(c.imported, 20)
|
||||
assert.equal(c.mails, 0)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
|
||||
test('Job: neues Nutzer-Repo → Mail, Org-Repo → keine Mail', async () => {
|
||||
freshSink()
|
||||
gitea.addRepo({ full_name: 'knso/jobneu', created_at: ago(1) })
|
||||
gitea.addRepo({ full_name: 'WEBklar/orgneu', created_at: ago(1) })
|
||||
const c = await runRepoAssignJob({ now: NOW })
|
||||
assert.equal(c.mails, 1)
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.equal(docsOf('knso/jobneu')[0].createdByGitea, 'knso')
|
||||
assert.ok(!docsOf('WEBklar/orgneu')[0].createdByGitea)
|
||||
assert.equal((await runRepoAssignJob({ now: NOW })).mails, 0)
|
||||
})
|
||||
|
||||
test('Job: Erinnerung nach 24 h genau einmal', async () => {
|
||||
freshSink()
|
||||
await runRepoAssignJob({ now: NOW })
|
||||
assert.equal(sink.sent.length, 0)
|
||||
const later = new Date(NOW.getTime() + 25 * 3600_000)
|
||||
assert.equal((await runRepoAssignJob({ now: later })).reminders >= 1, true)
|
||||
const n = sink.sent.length
|
||||
assert.ok(n >= 1)
|
||||
assert.equal((await runRepoAssignJob({ now: later })).reminders, 0)
|
||||
assert.equal(sink.sent.length, n)
|
||||
})
|
||||
|
||||
test('Job: fehlgeschlagene erste Mail wird wiederholt (< 3 Versuche)', async () => {
|
||||
gitea.addRepo({ full_name: 'knso/retry', created_at: ago(1) })
|
||||
setMailTransportForTests({ async sendMail() { throw new Error('smtp down') } })
|
||||
const silent = console.error; console.error = () => {}
|
||||
try { await runRepoAssignJob({ now: NOW }) } finally { console.error = silent }
|
||||
assert.equal(docsOf('knso/retry')[0].assignMailAttempts, 1)
|
||||
assert.ok(!docsOf('knso/retry')[0].assignMailSentAt)
|
||||
freshSink()
|
||||
const c = await runRepoAssignJob({ now: NOW })
|
||||
assert.equal(c.retries, 1)
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.ok(docsOf('knso/retry')[0].assignMailSentAt)
|
||||
})
|
||||
|
||||
test('Job: Appwrite-Fehler bricht den Lauf ab, ohne zu werfen', async () => {
|
||||
const silent = console.error; console.error = () => {}
|
||||
const awUrl = process.env.APPWRITE_ENDPOINT
|
||||
try {
|
||||
const { config } = await import('../server/config.js')
|
||||
const old = config.appwrite.endpoint
|
||||
config.appwrite.endpoint = 'http://127.0.0.1:1/v1'
|
||||
const c = await runRepoAssignJob({ now: NOW })
|
||||
config.appwrite.endpoint = old
|
||||
assert.ok(c.error)
|
||||
} finally { console.error = silent }
|
||||
assert.ok(awUrl)
|
||||
})
|
||||
Reference in new issue
Block a user