Admin: News je Projekt aktivieren und abschalten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:56:41 +00:00
1 parent a421308019
commit 6dac6aa073
6 files changed
+361 -9

No files matched your search

+89 -3
View File
@@ -12,15 +12,23 @@ const aw = await startAppwriteMock({
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, get, post, postWebhook } = await import('./support/testApp.js')
const { customerCookie, seedFixtures } = await import('./support/fixtures.js')
const { startTestApp, get, rawGet, post, postWebhook } = await import('./support/testApp.js')
const { adminCookie, customerCookie, seedFixtures } = await import('./support/fixtures.js')
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
const { default: aboRoutes } = await import('../server/routes/abo.js')
const { default: portalAdminRoutes } = await import('../server/routes/portalAdmin.js')
const { default: publicNewsRoutes } = await import('../server/routes/publicNews.js')
const sf = await import('../server/services/siteFeatures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { mailSink } = await import('./support/mailSink.js')
const app = await startTestApp({ '/api/funktionen': funktionenRoutes, '/api/abo': aboRoutes })
const app = await startTestApp({
'/api/funktionen': funktionenRoutes,
'/api/abo': aboRoutes,
'/api/portal-admin': portalAdminRoutes,
'/api/public/news': publicNewsRoutes,
})
const mails = mailSink()
setMailTransportForTests(mails)
after(() => {
@@ -213,3 +221,81 @@ test('parallele_aktivierung_mailt_einmal', async () => {
assert.equal(a.length + b.length, 1)
assert.equal(mails.sent.slice(mailsBefore).filter((m) => m.subject === 'Deine News-Pinnwand ist bereit').length, 1)
})
// ---------------------------------------------------------------- Admin ---
const adminC = adminCookie()
const adminPost = (customerId, body, cookie = adminC) => post(`/api/portal-admin/funktionen/${customerId}/news`, body, { cookie })
test('admin_aktivieren_ohne_zahlung', async () => {
aw.seed('websiteProjects', { $id: 'pa1', customerId: 'c1', projectName: 'Admin-Test' })
const wosBefore = workorders().length
const res = await adminPost('c1', { projectId: 'pa1', action: 'activate', note: 'Kulanz' })
assert.equal(res.status, 200)
const view = res.body.projects.find((p) => p.projectId === 'pa1').features.news
assert.equal(view.state, 'active')
assert.equal(view.ticketWoid, null)
assert.match(view.adminNote, /^\d{4}-\d\d-\d\dT[\d:.]+Z Kenso: activate – Kulanz$/)
const doc = await sf.getFeatureDoc('c1', 'pa1', 'news')
assert.equal(doc.enabled, true)
assert.equal(doc.unlockedByPurchase, false)
assert.equal(workorders().length, wosBefore)
const list = await get('/api/portal-admin/funktionen/c1', { cookie: adminC })
assert.equal(list.projects.find((p) => p.projectId === 'pa1').features.news.state, 'active')
})
test('admin_abschalten', async () => {
const doc = await sf.getFeatureDoc('c1', 'pa1', 'news')
const publicUrl = `/api/public/news/${doc.siteKey}`
assert.equal((await rawGet(publicUrl)).status, 200)
const res = await adminPost('c1', { projectId: 'pa1', action: 'disable' })
assert.equal(res.status, 200)
const view = res.body.projects[0].features.news
assert.equal(view.state, 'disabled')
assert.match(view.adminNote, /Kenso: disable \| .*Kenso: activate – Kulanz$/)
assert.ok(view.adminNote.length <= 300)
assert.equal((await sf.getFeatureDoc('c1', 'pa1', 'news')).enabled, false)
const { status, body } = await rawGet(publicUrl)
assert.ok(status === 404 || (status === 200 && (body.posts || []).length === 0), `öffentlich: ${status}`)
})
test('admin_ungueltig_und_fremd', async () => {
aw.seed('websiteProjects', { $id: 'pa4', customerId: 'c1', projectName: 'Ohne Dokument' })
assert.equal((await adminPost('c1', { projectId: 'pa1', action: 'loeschen' })).status, 400)
assert.equal((await adminPost('c1', { projectId: 'p5', action: 'activate' })).status, 403)
assert.equal((await adminPost('c1', { projectId: 'gibtsnicht', action: 'activate' })).status, 403)
assert.equal((await adminPost('c1', { projectId: 'pa4', action: 'disable' })).status, 409)
})
test('admin_notiz_bleibt_im_limit', async () => {
aw.seed('websiteProjects', { $id: 'pa2', customerId: 'c1', projectName: 'Notiz' })
for (let i = 0; i < 8; i += 1) {
await adminPost('c1', { projectId: 'pa2', action: i % 2 ? 'disable' : 'activate', note: 'x'.repeat(100) })
}
const note = (await sf.getFeatureDoc('c1', 'pa2', 'news')).adminNote
assert.ok(note.length <= 300)
assert.match(note, /^\S+ Kenso: disable – x+/)
})
test('kunde_darf_admin_route_nicht', async () => {
aw.seed('websiteProjects', { $id: 'pa5', customerId: 'c1', projectName: 'Kundenversuch' })
const res = await adminPost('c1', { projectId: 'pa5', action: 'activate' }, customerCookie('c1'))
assert.ok(res.status === 401 || res.status === 403)
assert.equal(await sf.getFeatureDoc('c1', 'pa5', 'news'), null)
const list = await rawGet('/api/portal-admin/funktionen/c1', { cookie: customerCookie('c1') })
assert.ok(list.status === 401 || list.status === 403)
})
test('ticket_fehlt_markiert', async () => {
aw.seed('websiteProjects', { $id: 'pa3', customerId: 'c1', projectName: 'Ticketlos' })
await checkout('pa3')
await postWebhook(event('checkout.session.completed', paidSession(sessions().at(-1).id, 'pa3')))
const doc = await sf.getFeatureDoc('c1', 'pa3', 'news')
assert.equal(doc.state, 'setup')
const newsOf = async () =>
(await get('/api/portal-admin/funktionen/c1', { cookie: adminC })).projects.find((p) => p.projectId === 'pa3').features.news
assert.equal((await newsOf()).ticketMissing, false)
await deleteDocument(config.collections.workorders, doc.ticketId)
const missing = await newsOf()
assert.equal(missing.ticketMissing, true)
assert.equal(missing.ticketWoid, null)
})
+95
View File
@@ -0,0 +1,95 @@
// Playwright-Check admin_funktionen: statischer Server + JSON-Fixtures, nicht Teil von npm test.
// Aufruf: node test/ui/admin-funktionen.ui.mjs
import http from 'node:http'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from '/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core/index.mjs'
const pub = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../public')
const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon' }
const news = (state, extra = {}) => ({ state, siteKey: 'abcdefghijklmnop', ticketWoid: null, adminNote: '', ticketMissing: false, ...extra })
const projects = [
{ projectId: 'p1', projectName: 'Bäckerei', features: { news: news('off') } },
{ projectId: 'p2', projectName: 'Café', features: { news: news('setup', { ticketMissing: true }) } },
{ projectId: 'p3', projectName: 'Kiosk', features: { news: news('active', { ticketWoid: '1234' }) } },
]
const posts = []
const json = (res, status, body) => {
res.writeHead(status, { 'content-type': 'application/json' })
res.end(JSON.stringify(body))
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://x')
if (url.pathname === '/api/auth/me') return json(res, 200, { authenticated: true, role: 'admin', admin: { name: 'Kenso', email: 'k@w.de' } })
if (url.pathname === '/api/portal-admin/customers') {
return json(res, 200, { customers: [{ id: 'c1', code: '1001', name: 'Test GmbH', email: 't@t.de', customerStatus: 'customer', portalAccessEnabled: true, projectCount: 3 }] })
}
if (url.pathname === '/api/portal-admin/funktionen/c1') return json(res, 200, { projects })
if (url.pathname === '/api/portal-admin/funktionen/c1/news' && req.method === 'POST') {
let raw = ''
for await (const chunk of req) raw += chunk
const body = JSON.parse(raw)
posts.push(body)
const p = projects.find((x) => x.projectId === body.projectId)
p.features.news = news(body.action === 'activate' ? 'active' : 'disabled', { adminNote: `2026-09-30T10:00:00.000Z Kenso: ${body.action}${body.note ? ` – ${body.note}` : ''}` })
return json(res, 200, { projects: [p] })
}
if (url.pathname.startsWith('/api/')) return json(res, 200, {})
try {
const file = path.join(pub, url.pathname === '/' ? 'admin.html' : url.pathname)
res.writeHead(200, { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream' })
res.end(await readFile(file))
} catch {
res.writeHead(404)
res.end()
}
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
let failed = 0
const check = (name, ok, detail = '') => {
console.log(`${ok ? 'OK ' : 'FAIL'} ${name}${detail ? ` (${detail})` : ''}`)
if (!ok) failed += 1
}
const browser = await chromium.launch()
try {
const page = await browser.newPage()
const dialogs = []
page.on('dialog', (d) => { dialogs.push(d.type()); d.dismiss() })
page.on('pageerror', (e) => check('keine Seitenfehler', false, e.message))
await page.goto(`http://127.0.0.1:${server.address().port}/admin.html`)
await page.click('.admin-tab[data-tab="customers"]')
await page.click('[data-features="c1"]')
await page.waitForSelector('[data-feature-project="p1"]')
const row = (id) => page.locator(`[data-feature-project="${id}"]`)
check('3 Zeilen', (await page.locator('[data-feature-project]').count()) === 3)
check('Ticket fehlt sichtbar', (await row('p2').innerText()).includes('Ticket fehlt'))
// Aktivieren: erst Rückfrage in der Zeile, nichts gesendet
await row('p1').locator('[data-feature-ask="activate"]').click()
check('Rückfrage statt Sofort-Aktion', posts.length === 0 && (await row('p1').innerText()).includes('ohne Zahlung aktivieren?'))
await row('p1').locator('[data-feature-no]').click()
check('Abbrechen stellt Knöpfe wieder her', (await row('p1').locator('[data-feature-ask="activate"]').count()) === 1 && posts.length === 0)
await row('p1').locator('[data-feature-ask="activate"]').click()
await row('p1').locator('[data-feature-note]').fill('Kulanz')
await row('p1').locator('[data-feature-yes]').click()
await page.waitForFunction(() => document.querySelector('[data-feature-project="p1"]')?.innerText.includes('Aktiv'))
check('POST activate mit Notiz', JSON.stringify(posts[0]) === JSON.stringify({ projectId: 'p1', action: 'activate', note: 'Kulanz' }), JSON.stringify(posts[0]))
check('Zeile zeigt Aktiv und Notiz', (await row('p1').innerText()).includes('Kenso: activate – Kulanz'))
// Abschalten
await row('p3').locator('[data-feature-ask="disable"]').click()
await row('p3').locator('[data-feature-yes]').click()
await page.waitForFunction(() => document.querySelector('[data-feature-project="p3"]')?.innerText.includes('Abgeschaltet'))
check('Abschalten aktualisiert Zeile', posts[1]?.action === 'disable')
check('keine Browser-Dialoge', dialogs.length === 0)
await page.screenshot({ path: '/tmp/claude-0/-root/a97f1ff7-2f71-45a7-bb9f-eaeacf7e64f1/scratchpad/admin-funktionen.png' })
} finally {
await browser.close()
server.close()
}
console.log(failed ? `admin_funktionen: ${failed} FEHLER` : 'admin_funktionen: alle Prüfungen bestanden')
process.exit(failed ? 1 : 0)