Projektarten: Zuordnen-Ablauf serverseitig

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-09-30 18:30:53 +00:00
1 parent df0c7a05e8
commit 36c7822783
3 files changed
+318

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
import { test, before, after } from 'node:test'
import assert from 'node:assert/strict'
import express from 'express'
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
let aw
let server
let baseUrl
let completeAssignment
const now = new Date('2026-09-30T10:00:00Z')
const newProj = (id, repo, extra = {}) => ({ $id: id, repoFullName: `JUSN/${repo}`, projectName: repo, customerId: '', ticketId: '', ...extra })
function stubs({ failAssign = false } = {}) {
const calls = { assign: [], update: [] }
return {
calls,
deps: {
assignProject: async (id, data) => {
calls.assign.push([id, data])
if (failAssign) throw new Error('Gitea kaputt')
return { ok: true }
},
updateWebsiteProject: async (id, data) => {
calls.update.push([id, data])
return {}
},
},
}
}
before(async () => {
aw = await startFakeAppwrite({
seed: {
websiteProjects: [
newProj('p1', 'intern'),
newProj('p2', 'vorhanden'),
newProj('p3', 'foo'),
newProj('p4', 'umzug'),
newProj('p5', 'fremd'),
newProj('p6', 'zu'),
newProj('p7', 'art'),
newProj('p8', 'fehler'),
newProj('p9', 'doppelt'),
newProj('p10', 'prev'),
newProj('p11', 'route'),
newProj('p12', 'schon', { kind: 'app', kindSetBy: 'Max', kindSetAt: '2026-09-29T08:00:00.000Z' }),
],
workorders: [
{ $id: 't1', woid: '100115', customerId: 'c1', status: 'Open' },
{ $id: 't2', woid: '100116', customerId: 'c2', status: 'Open' },
{ $id: 't3', woid: '100117', customerId: 'c1', status: 'Closed' },
],
customers: [{ $id: 'c1', name: 'Döner Nation', location: 'Berlin' }, { $id: 'c2', name: 'Andere' }],
},
})
Object.assign(process.env, { APPWRITE_ENDPOINT: aw.endpoint, APPWRITE_API_KEY: 'test-key' })
;({ completeAssignment } = await import('../server/services/projectKindAssign.js'))
const { default: routes } = await import('../server/routes/admin/websiteProjects.js')
const app = express()
app.use(express.json())
app.use('/api/admin/website-projects', routes)
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 () => {
await new Promise((resolve) => server.close(resolve))
await aw.close()
})
const opts = (deps) => ({ staffName: 'Kenso', now, deps })
test('intern speichert nur Art', async () => {
const s = stubs()
const r = await completeAssignment('p1', { kind: 'other', forCustomer: false }, opts(s.deps))
assert.deepEqual(r, { ticketId: null })
const p = aw.get('websiteProjects', 'p1')
assert.equal(p.kind, 'other')
assert.equal(p.forCustomer, false)
assert.equal(p.kindSetBy, 'Kenso')
assert.equal(p.kindSetAt, now.toISOString())
assert.equal(s.calls.assign.length, 0)
})
test('vorhandenes Ticket wird zugewiesen', async () => {
const s = stubs()
const before = aw.all('workorders').length
const r = await completeAssignment('p2', { kind: 'webapp', forCustomer: true, customerId: 'c1', ticketId: 't1' }, opts(s.deps))
assert.deepEqual(r, { ticketId: 't1' })
assert.deepEqual(s.calls.assign, [['p2', { customerId: 'c1', ticketId: 't1' }]])
assert.equal(aw.all('workorders').length, before)
assert.equal(aw.get('websiteProjects', 'p2').kind, 'webapp')
assert.equal(aw.get('websiteProjects', 'p2').forCustomer, true)
})
test('neues Ticket nur beim Abschliessen, Thema "Website – foo"', async () => {
const s = stubs()
const before = aw.all('workorders').length
const r = await completeAssignment(
'p3',
{ kind: 'website', forCustomer: true, customerId: 'c1', newTicket: { topic: '', requestedBy: 'Max', startDate: '2026-10-05', details: 'd' } },
opts(s.deps)
)
assert.equal(aw.all('workorders').length, before + 1)
const t = aw.get('workorders', r.ticketId)
assert.equal(t.topic, 'Website – foo')
assert.equal(t.type, 'Webpage')
assert.equal(t.status, 'Open')
assert.equal(t.startDate, '05.10.2026')
assert.deepEqual(s.calls.assign, [['p3', { customerId: 'c1', ticketId: r.ticketId }]])
})
test('Migration ergibt Ticket-Art Migration', async () => {
const s = stubs()
const r = await completeAssignment('p4', { kind: 'migration', forCustomer: true, customerId: 'c1', newTicket: { topic: 'Umzug X' } }, opts(s.deps))
const t = aw.get('workorders', r.ticketId)
assert.equal(t.type, 'Migration')
assert.equal(t.topic, 'Umzug X')
})
test('Ticket anderer Kunde ⇒ 400', async () => {
const s = stubs()
await assert.rejects(
() => completeAssignment('p5', { kind: 'website', forCustomer: true, customerId: 'c1', ticketId: 't2' }, opts(s.deps)),
(e) => e.status === 400
)
assert.equal(aw.get('websiteProjects', 'p5').kind, undefined)
})
test('Ticket Closed ⇒ 400', async () => {
const s = stubs()
await assert.rejects(
() => completeAssignment('p6', { kind: 'website', forCustomer: true, customerId: 'c1', ticketId: 't3' }, opts(s.deps)),
(e) => e.status === 400
)
})
test('unbekannte Art ⇒ 400', async () => {
const s = stubs()
await assert.rejects(
() => completeAssignment('p7', { kind: 'nix', forCustomer: false }, opts(s.deps)),
(e) => e.status === 400
)
assert.equal(aw.get('websiteProjects', 'p7').kind, undefined)
})
test('assignProject wirft ⇒ neues Ticket geloescht, kind leer', async () => {
const s = stubs({ failAssign: true })
const before = aw.all('workorders').length
await assert.rejects(
() => completeAssignment('p8', { kind: 'website', forCustomer: true, customerId: 'c1', newTicket: { topic: 'x' } }, opts(s.deps)),
/Gitea kaputt/
)
assert.equal(aw.all('workorders').length, before)
assert.equal(aw.get('websiteProjects', 'p8').kind, undefined)
})
test('zweiter Aufruf ⇒ 409, nur ein Ticket', async () => {
const s = stubs()
const before = aw.all('workorders').length
const body = { kind: 'app', forCustomer: true, customerId: 'c1', newTicket: { topic: 'eins' } }
await completeAssignment('p9', body, opts(s.deps))
await assert.rejects(
() => completeAssignment('p9', body, opts(s.deps)),
(e) => e.status === 409 && e.kindSetBy === 'Kenso'
)
assert.equal(aw.all('workorders').length, before + 1)
})
test('Website mit Subdomain ⇒ updateWebsiteProject(preview)', async () => {
const s = stubs()
await completeAssignment('p10', { kind: 'website', forCustomer: true, customerId: 'c1', ticketId: 't1', preview: { subdomain: 'prev' } }, opts(s.deps))
assert.deepEqual(s.calls.update, [['p10', { subdomain: 'prev', projectType: 'preview' }]])
})
test('Route: ohne Staff-JWT ⇒ 401, mit JWT intern 200, bereits zugeordnet 409, unbekannt 404', async () => {
const post = (id, body, jwt) =>
fetch(`${baseUrl}/api/admin/website-projects/${id}/zuordnen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}) },
body: JSON.stringify(body),
}).then(async (r) => ({ status: r.status, body: await r.json() }))
assert.equal((await post('p11', { kind: 'other', forCustomer: false })).status, 401)
const ok = await post('p11', { kind: 'other', forCustomer: false }, 'jwt-admin')
assert.deepEqual([ok.status, ok.body], [200, { ticketId: null }])
assert.ok(aw.get('websiteProjects', 'p11').kindSetAt) // Fake-Nutzer hat weder name noch email
const again = await post('p12', { kind: 'other', forCustomer: false }, 'jwt-admin')
assert.deepEqual([again.status, again.body], [409, { error: 'bereits zugeordnet', kindSetBy: 'Max', kindSetAt: '2026-09-29T08:00:00.000Z' }])
assert.equal((await post('nix', { kind: 'other', forCustomer: false }, 'jwt-admin')).status, 404)
})