Portal: Admin-Panel, Rechnungsanalyse und alte Admin-Schnittstellen entfernt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a4fa581c0
commit
7c00633db0
43 files changed
+229
-4001
No files matched your search
@@ -20,7 +20,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
process.env.ABO_TEST_EMAILS = 'test@example.invalid'
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, sessionCookie, get, rawGet, post, postWebhook } = await import('./support/testApp.js')
|
||||
const { hostingDoc, wartungDoc, consent, customerCookie, adminCookie, viewAsCookie, seedFixtures, subscriptionDoc, stripeSubscription } =
|
||||
const { hostingDoc, wartungDoc, consent, customerCookie, adminCookie, kundenansichtCookie, seedFixtures, subscriptionDoc, stripeSubscription } =
|
||||
await import('./support/fixtures.js')
|
||||
const { default: aboRoutes } = await import('../server/routes/abo.js')
|
||||
const { recordContractEvent, listContractEvents, eventIdFor } = await import('../server/services/contractEvents.js')
|
||||
@@ -123,7 +123,7 @@ test('GET /api/abo: Altbestand ohne productKey gilt als Wartungs-Abo', async ()
|
||||
assert.equal(r.subscription.canSwitch, false)
|
||||
})
|
||||
|
||||
test('Ohne Session 401, Admin-Session ist keine Kunden-Session', async () => {
|
||||
test('Ohne Session 401, alte Mitarbeiter-Sitzung ist keine Kunden-Session', async () => {
|
||||
assert.equal((await rawGet('/api/abo')).status, 401)
|
||||
assert.equal((await rawGet('/api/abo', { cookie: adminCookie() })).status, 401)
|
||||
await assert.rejects(get('/api/abo'), (e) => e.status === 401)
|
||||
@@ -225,8 +225,8 @@ test('Checkout mit laufendem Abo → 409', async () => {
|
||||
assert.equal(call, null)
|
||||
})
|
||||
|
||||
test('Checkout im Modus „Als Kunde ansehen“ → 403', async () => {
|
||||
const { res, call } = await checkout(orderBody('hosting'), { cookie: viewAsCookie('c3') })
|
||||
test('Checkout in der Kundenansicht → 403', async () => {
|
||||
const { res, call } = await checkout(orderBody('hosting'), { cookie: kundenansichtCookie({ customerId: 'c3' }) })
|
||||
assert.equal(res.status, 403)
|
||||
assert.equal(call, null)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { Router } from 'express'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Etappe 4: Admin-Panel, Rechnungsanalyse und alte Admin-Schnittstellen sind weg; was das Ticketsystem nutzt, bleibt
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp } = await import('./support/testApp.js')
|
||||
const { viewAsCookie } = await import('./support/fixtures.js')
|
||||
const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js')
|
||||
|
||||
// Hinter dem Waechter: eine Lese- und eine Schreib-Route, die selbst immer 200 liefern
|
||||
const probe = Router()
|
||||
probe.get('/', (_req, res) => res.json({ ok: true }))
|
||||
probe.post('/', (_req, res) => res.json({ ok: true }))
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, '/api/projects': probe })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
const root = new URL('..', import.meta.url).pathname
|
||||
const clears = (res, name) => res.headers.getSetCookie().some((c) => c.startsWith(`${name}=`) && /Expires=Thu, 01 Jan 1970/.test(c))
|
||||
|
||||
function filesUnder(dir) {
|
||||
const out = []
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = path.join(dir, name)
|
||||
if (statSync(full).isDirectory()) out.push(...filesUnder(full))
|
||||
else if (/\.(js|mjs|html|css)$/.test(name)) out.push(full)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
test('Admin-Panel und alte Admin-Schnittstellen sind weg', () => {
|
||||
for (const f of [
|
||||
'public/admin.html', 'public/admin.js', 'public/kalkulation.html', 'public/kalkulation.js', 'public/kalkulation.css',
|
||||
'server/routes/portalAdmin.js', 'server/routes/admin/switchLink.js', 'server/routes/admin/previewOpen.js', 'server/services/appointments.js',
|
||||
]) {
|
||||
assert.equal(existsSync(path.join(root, f)), false, f)
|
||||
}
|
||||
const index = readFileSync(path.join(root, 'server/index.js'), 'utf8')
|
||||
for (const mount of ['/api/portal-admin', '/api/admin/switch-link', '/api/admin/preview-open']) assert.ok(!index.includes(`'${mount}'`), mount)
|
||||
for (const f of ['public/chat-composer.js', 'public/chat-composer.css', 'public/kundenansicht.js', 'server/services/staffPreviewLink.js']) {
|
||||
assert.ok(existsSync(path.join(root, f)), `${f} muss bleiben`)
|
||||
}
|
||||
})
|
||||
|
||||
test('keine Verweise mehr auf admin.html, kalkulation.html oder /api/portal-admin in server/ und public/', () => {
|
||||
// Mail-Vorlagen und ihre Links stellt Task 3 um (eigener Test in mailTemplates.test.js)
|
||||
const skip = [path.join(root, 'server/mail-templates/'), path.join(root, 'server/services/takeoverCredits.js')]
|
||||
const hits = []
|
||||
for (const file of [...filesUnder(path.join(root, 'server')), ...filesUnder(path.join(root, 'public'))]) {
|
||||
if (skip.some((s) => file.startsWith(s))) continue
|
||||
readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
|
||||
if (/admin\.html|kalkulation\.html|\/api\/portal-admin/.test(line)) hits.push(`${path.relative(root, file)}:${i + 1}`)
|
||||
})
|
||||
}
|
||||
assert.deepEqual(hits, [])
|
||||
})
|
||||
|
||||
test('Alt-Ansicht ohne Kennung: Lesen und Schreiben 401 view_expired, Cookies geloescht', async () => {
|
||||
for (const method of ['GET', 'POST']) {
|
||||
const res = await fetch(`${app.url}/api/projects`, {
|
||||
method,
|
||||
headers: { cookie: viewAsCookie('c1'), ...(method === 'POST' ? { 'content-type': 'application/json' } : {}) },
|
||||
body: method === 'POST' ? '{}' : undefined,
|
||||
})
|
||||
assert.equal(res.status, 401, method)
|
||||
assert.equal((await res.json()).code, 'view_expired', method)
|
||||
assert.ok(clears(res, config.cookieName), `${method}: Portal-Cookie geloescht`)
|
||||
assert.ok(clears(res, 'preview_session'), `${method}: Preview-Cookie geloescht`)
|
||||
}
|
||||
})
|
||||
@@ -49,7 +49,7 @@ test('Sessions: nach Passwortaenderung ungueltig, alte ohne Stand und Admins ble
|
||||
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u1', customerId: 'c1', pwv: '2026-09-01T08:00:00.000+00:00' }), false)
|
||||
// Session von vor dieser Pruefung (ohne pwv) laeuft normal aus
|
||||
assert.equal(await isSessionPasswordCurrent({ appwriteUserId: 'u1', customerId: 'c1' }), true)
|
||||
// "Als Kunde ansehen": appwriteUserId ist die des Admins
|
||||
// Kundenansicht: appwriteUserId ist die des Mitarbeiters
|
||||
assert.equal(await isSessionPasswordCurrent({ role: 'admin', viewAs: true, appwriteUserId: 'u1', customerId: 'c1', pwv: 'x' }), true)
|
||||
// Konto ohne Passwort (geleert): keine Session gilt mehr
|
||||
rememberPasswordVersion('u2', '')
|
||||
|
||||
@@ -14,10 +14,10 @@ const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, get, rawGet, post, postWebhook } = await import('./support/testApp.js')
|
||||
const { adminCookie, customerCookie, seedFixtures } = await import('./support/fixtures.js')
|
||||
const { 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: teamRoutes } = await import('../server/routes/team/index.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')
|
||||
@@ -27,7 +27,7 @@ const { mailSink } = await import('./support/mailSink.js')
|
||||
const app = await startTestApp({
|
||||
'/api/funktionen': funktionenRoutes,
|
||||
'/api/abo': aboRoutes,
|
||||
'/api/portal-admin': portalAdminRoutes,
|
||||
'/api/team': teamRoutes,
|
||||
'/api/public/news': publicNewsRoutes,
|
||||
})
|
||||
const mails = mailSink()
|
||||
@@ -235,9 +235,11 @@ test('parallele_aktivierung_mailt_einmal', async () => {
|
||||
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 })
|
||||
// ------------------------------------------------- Ticketsystem (Team) ---
|
||||
// Mitarbeiter ohne Admin-Label ueber den Team-Eingang (JWT)
|
||||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } }
|
||||
const adminPost = (customerId, body, opts = STAFF) => post(`/api/team/funktionen/${customerId}/news`, body, opts)
|
||||
|
||||
test('admin_aktivieren_ohne_zahlung', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'pa1', customerId: 'c1', projectName: 'Admin-Test' })
|
||||
@@ -247,12 +249,12 @@ test('admin_aktivieren_ohne_zahlung', async () => {
|
||||
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$/)
|
||||
assert.match(view.adminNote, /^\d{4}-\d\d-\d\dT[\d:.]+Z Andrej: 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 })
|
||||
const list = await get('/api/team/funktionen/c1', STAFF)
|
||||
assert.equal(list.projects.find((p) => p.projectId === 'pa1').features.news.state, 'active')
|
||||
})
|
||||
|
||||
@@ -264,7 +266,7 @@ test('admin_abschalten', async () => {
|
||||
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.match(view.adminNote, /Andrej: disable \| .*Andrej: activate – Kulanz$/)
|
||||
assert.ok(view.adminNote.length <= 300)
|
||||
assert.equal((await sf.getFeatureDoc('c1', 'pa1', 'news')).enabled, false)
|
||||
const { status, body } = await rawGet(publicUrl)
|
||||
@@ -286,15 +288,15 @@ test('admin_notiz_bleibt_im_limit', async () => {
|
||||
}
|
||||
const note = (await sf.getFeatureDoc('c1', 'pa2', 'news')).adminNote
|
||||
assert.ok(note.length <= 300)
|
||||
assert.match(note, /^\S+ Kenso: disable – x+/)
|
||||
assert.match(note, /^\S+ Andrej: 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'))
|
||||
const res = await adminPost('c1', { projectId: 'pa5', action: 'activate' }, { cookie: 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') })
|
||||
const list = await rawGet('/api/team/funktionen/c1', { cookie: customerCookie('c1') })
|
||||
assert.ok(list.status === 401 || list.status === 403)
|
||||
})
|
||||
|
||||
@@ -305,7 +307,7 @@ test('ticket_fehlt_markiert', async () => {
|
||||
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
|
||||
(await get('/api/team/funktionen/c1', STAFF)).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()
|
||||
|
||||
@@ -156,13 +156,16 @@ test('genau bei viewExpiresAt abgelaufen: 401, Cookies geloescht, Protokoll abge
|
||||
assert.deepEqual([expired.at(-1).customerId, expired.at(-1).staffName], ['c1', 'Andrej'])
|
||||
})
|
||||
|
||||
test('alte Ansicht aus admin.html (viewAsCookie, ohne viewId): Lesen ok, News anlegen 403, Logout ok', async () => {
|
||||
test('alte Ansicht ohne viewId (frueheres Admin-Panel): Lesen, Schreiben, Logout 401 view_expired, Cookies weg, kein Protokoll', async () => {
|
||||
const cookie = viewAsCookie('c1')
|
||||
assert.equal((await call('GET', '/api/news?projectId=p1', { cookie })).status, 200)
|
||||
const write = await call('POST', '/api/news', { cookie, body: note })
|
||||
assert.equal(write.status, 403)
|
||||
assert.equal(write.body.code, 'view_blocked')
|
||||
assert.equal((await call('POST', '/api/auth/logout', { cookie })).status, 200)
|
||||
const before = logEntries().length
|
||||
for (const [method, p, body] of [['GET', '/api/news?projectId=p1'], ['POST', '/api/news', note], ['POST', '/api/auth/logout']]) {
|
||||
const res = await call(method, p, { cookie, body })
|
||||
assert.equal(res.status, 401, `${method} ${p}`)
|
||||
assert.equal(res.body.code, 'view_expired', `${method} ${p}`)
|
||||
assert.ok(clears(res, config.cookieName), `${method} ${p}: Portal-Cookie geloescht`)
|
||||
}
|
||||
assert.equal(logEntries().length, before)
|
||||
})
|
||||
|
||||
test('normale Kunden-Sitzung wird nicht beruehrt (News anlegen 201 ohne Header)', async () => {
|
||||
@@ -192,18 +195,18 @@ test('Grossschreibung umgeht den Waechter nicht: /API/... wie /api/...', async (
|
||||
assert.equal(res.status, 403, `${method} ${upper}`)
|
||||
}
|
||||
const legacy = await call('POST', '/API/news', { cookie: viewAsCookie('c1'), body: note })
|
||||
assert.equal(legacy.status, 403)
|
||||
assert.equal(legacy.status, 401)
|
||||
const expired = await call('GET', '/Api/Projects', kv({ viewExpiresAt: Date.now() - 1000 }))
|
||||
assert.equal(expired.status, 401)
|
||||
})
|
||||
|
||||
test('Tab mit Kennung, Cookie ist keine Kundenansicht mehr -> 409 statt fremder Daten', async () => {
|
||||
const header = { 'x-kundenansicht': VIEW }
|
||||
for (const cookie of [customerCookie('c2'), viewAsCookie('c2')]) {
|
||||
const res = await call('GET', '/api/projects', { cookie, headers: header })
|
||||
assert.equal(res.status, 409)
|
||||
assert.deepEqual([res.body.code, res.body.kunde], ['view_replaced', 'Kunde Zwei'])
|
||||
}
|
||||
const res = await call('GET', '/api/projects', { cookie: customerCookie('c2'), headers: header })
|
||||
assert.equal(res.status, 409)
|
||||
assert.deepEqual([res.body.code, res.body.kunde], ['view_replaced', 'Kunde Zwei'])
|
||||
// alte Ansicht ohne Kennung: beendet, ebenfalls keine fremden Daten
|
||||
assert.equal((await call('GET', '/api/projects', { cookie: viewAsCookie('c2'), headers: header })).status, 401)
|
||||
// ohne Cookie: kein fremder Kunde, die Route antwortet selbst mit 401
|
||||
assert.equal((await call('GET', '/api/projects', { headers: header })).status, 401)
|
||||
})
|
||||
@@ -18,7 +18,7 @@ const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, post, rawGet, postWebhook } = await import('./support/testApp.js')
|
||||
const { hostingDoc, consent, customerCookie, viewAsCookie, seedFixtures, stripeSubscription } = await import('./support/fixtures.js')
|
||||
const { hostingDoc, consent, customerCookie, kundenansichtCookie, seedFixtures, stripeSubscription } = await import('./support/fixtures.js')
|
||||
const { default: aboRoutes } = await import('../server/routes/abo.js')
|
||||
const { planStorageChange, paidUnitsOf, previewStorageChange, changeStorage, recordPaidStorage, storageTaskKind } =
|
||||
await import('../server/services/mailStorage.js')
|
||||
@@ -1024,13 +1024,13 @@ test('Webhook invoice.paid: Rechnung einmal frisch geladen, alle Zeilen, Stunde
|
||||
// Routen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('Routen: ohne Session 401, im Modus „Als Kunde ansehen“ 403, ohne Stripe-Aufruf', async () => {
|
||||
test('Routen: ohne Session 401, in der Kundenansicht 403, ohne Stripe-Aufruf', async () => {
|
||||
await resetSubH({ units: 0 })
|
||||
const before = st.calls.length
|
||||
assert.equal((await post('/api/abo/storage/preview', { units: 2 })).status, 401)
|
||||
assert.equal((await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: config.abo.termsVersion })).status, 401)
|
||||
assert.equal((await post('/api/abo/storage/preview', { units: 2 }, { cookie: viewAsCookie() })).status, 403)
|
||||
assert.equal((await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: config.abo.termsVersion }, { cookie: viewAsCookie() })).status, 403)
|
||||
assert.equal((await post('/api/abo/storage/preview', { units: 2 }, { cookie: kundenansichtCookie() })).status, 403)
|
||||
assert.equal((await post('/api/abo/storage', { units: 2, acceptTerms: true, termsVersion: config.abo.termsVersion }, { cookie: kundenansichtCookie() })).status, 403)
|
||||
assert.equal(st.calls.length, before)
|
||||
assert.equal((await rawGet('/api/abo/storage')).status, 404)
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ const { customerCookie, seedFixtures } = await import('./support/fixtures.js')
|
||||
const { default: newsRoutes } = await import('../server/routes/news.js')
|
||||
const { default: publicNewsRoutes } = await import('../server/routes/publicNews.js')
|
||||
|
||||
// Waechter wie in server/index.js: er sperrt Schreibzugriffe der alten Ansicht "Als Kunde ansehen"
|
||||
// Waechter wie in server/index.js: alte Ansichten ohne Kennung (frueheres Admin-Panel) gelten als beendet
|
||||
const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js')
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, '/api/public/news': publicNewsRoutes, '/api/news': newsRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
@@ -55,10 +55,12 @@ test('nicht_aktiv_409', async () => {
|
||||
assert.equal(res.body.error, 'News ist für diese Website nicht aktiv.')
|
||||
})
|
||||
|
||||
test('fremdes_projekt_403_und_viewAs_403', async () => {
|
||||
test('fremdes_projekt_403_und_alte_Ansicht_401', async () => {
|
||||
assert.equal((await post('/api/news', { ...note, projectId: 'p3' }, { cookie })).status, 403)
|
||||
const admin = sessionCookie({ customerId: 'c1', appwriteUserId: 'u1', viewAs: true })
|
||||
assert.equal((await post('/api/news', note, { cookie: admin })).status, 403)
|
||||
const alt = sessionCookie({ customerId: 'c1', appwriteUserId: 'u1', viewAs: true })
|
||||
const res = await post('/api/news', note, { cookie: alt })
|
||||
assert.equal(res.status, 401)
|
||||
assert.equal(res.body.code, 'view_expired')
|
||||
})
|
||||
|
||||
test('kunde_crud', async () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
process.env.ABO_TEST_EMAILS = 'test@example.invalid'
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, get, post, postWebhook } = await import('./support/testApp.js')
|
||||
const { hostingDoc, hosting2Doc, consent, customerCookie, viewAsCookie, seedFixtures, stripeSubscription, subscriptionDoc } =
|
||||
const { hostingDoc, hosting2Doc, consent, customerCookie, kundenansichtCookie, seedFixtures, stripeSubscription, subscriptionDoc } =
|
||||
await import('./support/fixtures.js')
|
||||
const { default: aboRoutes } = await import('../server/routes/abo.js')
|
||||
const { previewSwitch, switchToWartung, markSwitchFailed, previewProblem, paymentMethodTypeOf, reconcilePendingSwitch, SWITCH_FAILED_REASONS } =
|
||||
@@ -590,7 +590,7 @@ test('Route: ohne earlyStart 400', async () => {
|
||||
assert.equal(res.status, 400)
|
||||
})
|
||||
|
||||
test('Routen: fehlende Angaben 400, alte Fassung 409, ohne Session 401, „Als Kunde ansehen“ 403, ohne Stripe-Aufruf', async () => {
|
||||
test('Routen: fehlende Angaben 400, alte Fassung 409, ohne Session 401, Kundenansicht 403, ohne Stripe-Aufruf', async () => {
|
||||
await resetHosting()
|
||||
const cookie = customerCookie()
|
||||
const body = { acceptTerms: true, earlyStart: true, termsVersion: config.abo.termsVersion, prorationDate: nowUnix(), amountDueCents: 4100 }
|
||||
@@ -607,8 +607,8 @@ test('Routen: fehlende Angaben 400, alte Fassung 409, ohne Session 401, „Als K
|
||||
assert.match(stale.body.error, /Bedingungen/)
|
||||
assert.equal((await post('/api/abo/switch', body)).status, 401)
|
||||
assert.equal((await post('/api/abo/switch/preview', {})).status, 401)
|
||||
assert.equal((await post('/api/abo/switch', body, { cookie: viewAsCookie() })).status, 403)
|
||||
assert.equal((await post('/api/abo/switch/preview', {}, { cookie: viewAsCookie() })).status, 403)
|
||||
assert.equal((await post('/api/abo/switch', body, { cookie: kundenansichtCookie() })).status, 403)
|
||||
assert.equal((await post('/api/abo/switch/preview', {}, { cookie: kundenansichtCookie() })).status, 403)
|
||||
assert.equal(st.calls.length, before)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { test, after } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Wechsel Ticketsystem -> Projekt-Admin per Einmal-Link (purpose 'switch'), Rueckweg-Button im Admin
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, post, rawGet } = await import('./support/testApp.js')
|
||||
const { default: authRoutes } = await import('../server/routes/auth.js')
|
||||
const { default: switchLinkRoutes } = await import('../server/routes/admin/switchLink.js')
|
||||
const { createStaffPreviewToken } = await import('../server/services/staffPreviewTokens.js')
|
||||
const app = await startTestApp({ '/api/auth': authRoutes, '/api/admin/switch-link': switchLinkRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
|
||||
const bearer = (jwt) => ({ headers: { authorization: `Bearer ${jwt}` } })
|
||||
const switchLink = (jwt) => post('/api/admin/switch-link', {}, jwt ? bearer(jwt) : {})
|
||||
const tokenOf = (openUrl) => new URL(openUrl).searchParams.get('token')
|
||||
const portalSession = (res) => {
|
||||
const cookie = res.headers.getSetCookie().find((c) => c.startsWith(`${config.cookieName}=`))
|
||||
if (!cookie) return null
|
||||
const raw = decodeURIComponent(cookie.split(';')[0].slice(config.cookieName.length + 1))
|
||||
return JSON.parse(raw.slice(2, raw.lastIndexOf('.')))
|
||||
}
|
||||
|
||||
test('switch-link: ohne, ungueltiges und halbes JWT -> 401', async () => {
|
||||
assert.equal((await switchLink(null)).status, 401)
|
||||
assert.equal((await switchLink('jwt-falsch')).status, 401)
|
||||
assert.equal((await switchLink('jwt-halb')).status, 401)
|
||||
})
|
||||
|
||||
test('switch-link: kein Mitarbeiter -> 403', async () => {
|
||||
assert.equal((await switchLink('jwt-kunde')).status, 403)
|
||||
})
|
||||
|
||||
test('switch-link: Mitarbeiter ohne Admin-Label bekommt einen Link', async () => {
|
||||
const res = await switchLink('jwt-staff')
|
||||
assert.equal(res.status, 200)
|
||||
assert.match(res.body.openUrl, /^https:\/\/project\.webklar\.com\/api\/auth\/staff-switch\?token=[0-9a-f]{64}$/)
|
||||
})
|
||||
|
||||
test('admin.html: Rueckweg ins Ticketsystem in der Kopfzeile', () => {
|
||||
const html = readFileSync(new URL('../public/admin.html', import.meta.url), 'utf8')
|
||||
const actions = html.slice(html.indexOf('portal-header-actions'), html.indexOf('id="logout-btn"'))
|
||||
assert.match(actions, /<a class="portal-btn-outline" href="https:\/\/ticket\.webklar\.com\/tickets">← Ticketsystem<\/a>/)
|
||||
})
|
||||
+34
-39
@@ -7,8 +7,8 @@ import { schemaKeys } from './support/schema.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
/**
|
||||
* Task 13: Admin-Reiter "E-Mail-Speicher" (GET/POST /api/portal-admin/storage-tasks), Schnittstelle fuers
|
||||
* Ticketsystem (GET /api/admin/customers/:id/abo) und der Wechsel-Abgleich beim Laden des Reiters.
|
||||
* "E-Mail-Speicher" im Ticketsystem (GET/POST /api/team/storage-tasks, alle Mitarbeiter), Schnittstelle fuers
|
||||
* Ticketsystem (GET /api/admin/customers/:id/abo) und der Wechsel-Abgleich beim Laden der Liste.
|
||||
*/
|
||||
|
||||
// Nachbauten starten und die Umgebung setzen, BEVOR config.js (ueber testApp/fixtures/Routen) geladen wird
|
||||
@@ -21,16 +21,16 @@ 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, rawGet, sessionCookie } = await import('./support/testApp.js')
|
||||
const { hostingDoc, hosting2Doc, wartungDoc, subscriptionDoc, consent, customerCookie, adminCookie, seedFixtures, stripeSubscription } =
|
||||
const { startTestApp, get, post, rawGet } = await import('./support/testApp.js')
|
||||
const { hostingDoc, hosting2Doc, wartungDoc, subscriptionDoc, consent, customerCookie, seedFixtures, stripeSubscription } =
|
||||
await import('./support/fixtures.js')
|
||||
const { default: portalAdminRoutes } = await import('../server/routes/portalAdmin.js')
|
||||
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||||
const { default: adminCustomersRoutes } = await import('../server/routes/admin/customers.js')
|
||||
const { listContractEvents, recordContractEvent, getContractEvent } = await import('../server/services/contractEvents.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
|
||||
const app = await startTestApp({ '/api/portal-admin': portalAdminRoutes, '/api/admin/customers': adminCustomersRoutes })
|
||||
const app = await startTestApp({ '/api/team': teamRoutes, '/api/admin/customers': adminCustomersRoutes })
|
||||
// F25: Mails (K14 beim Wechsel-Abgleich) landen in der Senke statt bei SMTP
|
||||
const mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
@@ -46,7 +46,8 @@ st.prices.set('price_mailStorage', price('price_mailStorage', config.mailStorage
|
||||
|
||||
const HOUR = 3600000
|
||||
const iso = (offsetMs) => new Date(Date.now() + offsetMs).toISOString()
|
||||
const ADMIN = { cookie: adminCookie() }
|
||||
// Mitarbeiter ohne Admin-Label (Team-Eingang: alle Mitarbeiter gleich)
|
||||
const ADMIN = { headers: { Authorization: 'Bearer jwt-staff' } }
|
||||
const STAFF_JWT = { headers: { Authorization: 'Bearer jwt-admin' } }
|
||||
|
||||
async function clearEvents() {
|
||||
@@ -77,6 +78,7 @@ beforeEach(async () => {
|
||||
await clearEvents()
|
||||
mails.sent.length = 0
|
||||
seedFixtures(aw)
|
||||
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||
// sub_h wurde von 3 auf 1 Paket verringert; konsoleH haelt bis zum Periodenende noch 3 (offene Aufgabe unten)
|
||||
aw.seed('subscriptions', { ...hostingDoc, storageUnits: 1, storagePaidUnits: 3, storagePaidPeriodEnd: hostingDoc.currentPeriodEnd })
|
||||
aw.seed('subscriptions', { ...hosting2Doc, storageUnits: 2, storagePaidUnits: 2, storagePaidPeriodEnd: hosting2Doc.currentPeriodEnd })
|
||||
@@ -88,7 +90,7 @@ beforeEach(async () => {
|
||||
|
||||
test('Admin: offene Speicher-Aufgaben und vergebene GB', async () => {
|
||||
// sub_h: 1 Paket, aber 3→1 erst zum Periodenende → konsoleH haelt 5 + 15 = 20 GB; sub_h2: 5 + 10 = 15 GB
|
||||
const r = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const r = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal(r.allocatedGb, 35)
|
||||
assert.equal(r.packageGb, 50)
|
||||
assert.equal(r.warnGb, 40)
|
||||
@@ -116,7 +118,7 @@ test('Admin: vergebene GB zaehlen nur laufende Abos (active, trialing, past_due)
|
||||
aw.seed('subscriptions', subscriptionDoc('sub_alt', { ...hostingDoc, customerId: 'c3', status: 'canceled', storageUnits: 10 }))
|
||||
aw.seed('subscriptions', subscriptionDoc('sub_pd', { ...wartungDoc, customerId: 'c3', status: 'past_due', storageUnits: 1 }))
|
||||
try {
|
||||
const r = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const r = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal(r.allocatedGb, 45)
|
||||
} finally {
|
||||
await deleteDocument(config.collections.subscriptions, 'sub_alt')
|
||||
@@ -125,31 +127,24 @@ test('Admin: vergebene GB zaehlen nur laufende Abos (active, trialing, past_due)
|
||||
})
|
||||
|
||||
test('Admin: belegter Speicher zaehlt die Verringerung erst nach Erledigt', async () => {
|
||||
const before = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const before = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal(before.allocatedGb, 35, 'offene Aufgabe 3→1: konsoleH haelt noch 20 GB fuer sub_h')
|
||||
const task = before.tasks.find((t) => t.kind === 'periodenende')
|
||||
await post(`/api/portal-admin/storage-tasks/${task.id}/done`, {}, ADMIN)
|
||||
const after = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
await post(`/api/team/storage-tasks/${task.id}/done`, {}, ADMIN)
|
||||
const after = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal(after.allocatedGb, 25, 'nach Erledigt: 5 + 5 = 10 GB fuer sub_h, dazu 15 GB fuer sub_h2')
|
||||
})
|
||||
|
||||
test('Admin: Aufgabe nennt Gesamtspeicher des Kunden und Kontakt-Domain', async () => {
|
||||
const { tasks } = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const { tasks } = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal(tasks[0].targetGb, 15)
|
||||
assert.equal(tasks[0].contactDomain, 'example.invalid')
|
||||
assert.equal(tasks[1].targetGb, 10)
|
||||
})
|
||||
|
||||
test('Admin: Erledigt ohne E-Mail des Mitarbeiters wird abgelehnt (400)', async () => {
|
||||
const noEmail = { cookie: sessionCookie({ role: 'admin', appwriteUserId: 'u_admin', name: 'Kenso' }) }
|
||||
const r = await post('/api/portal-admin/storage-tasks/speicher-einrichten-c2/done', {}, noEmail)
|
||||
assert.equal(r.status, 400)
|
||||
assert.equal((await getContractEvent('speicher-einrichten-c2')).status, 'offen')
|
||||
})
|
||||
|
||||
test('Admin: Erledigt auf ein gescheitertes Ereignis → 409', async () => {
|
||||
await storageEvent({ id: 'speicher-gescheitert-x', customerId: 'c1', subscriptionId: 'sub_h', kind: 'einrichten', fromUnits: 0, toUnits: 4, dueAt: iso(-2000), status: 'gescheitert' })
|
||||
const r = await post('/api/portal-admin/storage-tasks/speicher-gescheitert-x/done', {}, ADMIN)
|
||||
const r = await post('/api/team/storage-tasks/speicher-gescheitert-x/done', {}, ADMIN)
|
||||
assert.equal(r.status, 409)
|
||||
assert.equal((await getContractEvent('speicher-gescheitert-x')).status, 'gescheitert')
|
||||
})
|
||||
@@ -159,41 +154,41 @@ test('Admin: gescheiterte Aenderungen nur der letzten 30 Tage', async () => {
|
||||
await storageEvent({ id: 'speicher-gescheitert-alt', customerId: 'c1', subscriptionId: 'sub_h', kind: 'einrichten', fromUnits: 0, toUnits: 5, dueAt: iso(-2000), status: 'gescheitert' })
|
||||
aw.seed('contractEvents', { ...(await getContractEvent('speicher-gescheitert-alt')), $createdAt: iso(-31 * 24 * HOUR) })
|
||||
aw.seed('contractEvents', { ...(await getContractEvent('speicher-gescheitert-neu')), $createdAt: iso(-29 * 24 * HOUR) })
|
||||
const r = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const r = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.deepEqual(r.failed.map((f) => f.id), ['speicher-gescheitert-neu'])
|
||||
})
|
||||
|
||||
test('Admin: Aufgabe erledigen', async () => {
|
||||
const { tasks } = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const { status, body: { task } } = await post(`/api/portal-admin/storage-tasks/${tasks[0].id}/done`, {}, ADMIN)
|
||||
const { tasks } = await get('/api/team/storage-tasks', ADMIN)
|
||||
const { status, body: { task } } = await post(`/api/team/storage-tasks/${tasks[0].id}/done`, {}, ADMIN)
|
||||
assert.equal(status, 200)
|
||||
assert.equal(task.status, 'erledigt')
|
||||
assert.equal(task.doneBy, 'kenso@webklar.com')
|
||||
assert.equal(task.doneBy, 'andrej@webklar.com')
|
||||
assert.ok(task.doneAt)
|
||||
assert.equal(task.id, tasks[0].id)
|
||||
const stored = await getContractEvent(tasks[0].id)
|
||||
assert.equal(stored.status, 'erledigt')
|
||||
assert.equal(stored.doneBy, 'kenso@webklar.com')
|
||||
assert.equal(stored.doneBy, 'andrej@webklar.com')
|
||||
// Die Liste zeigt sie nicht mehr
|
||||
const after = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const after = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.deepEqual(after.tasks.map((t) => t.id), [tasks[1].id])
|
||||
})
|
||||
|
||||
test('Admin: Erledigt nur fuer offene Speicher-Aufgaben (404 unbekannt, 409 schon erledigt)', async () => {
|
||||
assert.equal((await post('/api/portal-admin/storage-tasks/gibt-es-nicht/done', {}, ADMIN)).status, 404)
|
||||
const { tasks } = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
await post(`/api/portal-admin/storage-tasks/${tasks[0].id}/done`, {}, ADMIN)
|
||||
const again = await post(`/api/portal-admin/storage-tasks/${tasks[0].id}/done`, {}, ADMIN)
|
||||
assert.equal((await post('/api/team/storage-tasks/gibt-es-nicht/done', {}, ADMIN)).status, 404)
|
||||
const { tasks } = await get('/api/team/storage-tasks', ADMIN)
|
||||
await post(`/api/team/storage-tasks/${tasks[0].id}/done`, {}, ADMIN)
|
||||
const again = await post(`/api/team/storage-tasks/${tasks[0].id}/done`, {}, ADMIN)
|
||||
assert.equal(again.status, 409)
|
||||
// Ein Wechsel-Ereignis ist keine Speicher-Aufgabe
|
||||
await recordContractEvent({ id: 'wechsel-offen-x', customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung', details: {}, consent, status: 'offen' })
|
||||
assert.equal((await post('/api/portal-admin/storage-tasks/wechsel-offen-x/done', {}, ADMIN)).status, 404)
|
||||
assert.equal((await post('/api/team/storage-tasks/wechsel-offen-x/done', {}, ADMIN)).status, 404)
|
||||
})
|
||||
|
||||
test('Admin: teilweise (Stripe-Schritt 2 gescheitert) und gescheiterte Aenderungen sind sichtbar', async () => {
|
||||
await storageEvent({ id: 'speicher-teilweise-c2', customerId: 'c2', subscriptionId: 'sub_h2', kind: 'einrichten', fromUnits: 2, toUnits: 5, dueAt: iso(-1000), extra: { partial: true, error: 'Stripe: card_declined' } })
|
||||
await storageEvent({ id: 'speicher-gescheitert-c1', customerId: 'c1', subscriptionId: 'sub_h', kind: 'einrichten', fromUnits: 0, toUnits: 4, dueAt: iso(-2000), status: 'gescheitert', extra: { error: 'Stripe: resource_missing' } })
|
||||
const r = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const r = await get('/api/team/storage-tasks', ADMIN)
|
||||
const partial = r.tasks.find((t) => t.id === 'speicher-teilweise-c2')
|
||||
assert.equal(partial.partial, true)
|
||||
assert.equal(partial.error, 'Stripe: card_declined')
|
||||
@@ -218,7 +213,7 @@ test('Admin: Wechsel-Abgleich beim Laden des Reiters (verlorener Stripe-Webhook)
|
||||
consent,
|
||||
status: 'offen',
|
||||
})
|
||||
const r = await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
const r = await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal(r.tasks.length, 2)
|
||||
const ev = await getContractEvent('wechsel-abgelaufen-c1')
|
||||
assert.equal(ev.status, 'gescheitert')
|
||||
@@ -237,15 +232,15 @@ test('Admin: Wechsel-Abgleich laesst einen noch laufenden Wechsel in Ruhe', asyn
|
||||
status: 'offen',
|
||||
})
|
||||
const before = st.calls.length
|
||||
await get('/api/portal-admin/storage-tasks', ADMIN)
|
||||
await get('/api/team/storage-tasks', ADMIN)
|
||||
assert.equal((await getContractEvent('wechsel-laufend-c1')).status, 'offen')
|
||||
assert.equal(st.calls.length, before, 'kein Stripe-Abruf fuer einen nicht abgelaufenen Wechsel')
|
||||
})
|
||||
|
||||
test('Ohne Admin-Session 401', async () => {
|
||||
assert.equal((await rawGet('/api/portal-admin/storage-tasks', { cookie: customerCookie() })).status, 401)
|
||||
assert.equal((await rawGet('/api/portal-admin/storage-tasks')).status, 401)
|
||||
assert.equal((await post('/api/portal-admin/storage-tasks/speicher-einrichten-c2/done', {}, { cookie: customerCookie() })).status, 401)
|
||||
test('Ohne Mitarbeiter-JWT 401 (auch mit Kunden-Cookie)', async () => {
|
||||
assert.equal((await rawGet('/api/team/storage-tasks', { cookie: customerCookie() })).status, 401)
|
||||
assert.equal((await rawGet('/api/team/storage-tasks')).status, 401)
|
||||
assert.equal((await post('/api/team/storage-tasks/speicher-einrichten-c2/done', {}, { cookie: customerCookie() })).status, 401)
|
||||
assert.equal((await getContractEvent('speicher-einrichten-c2')).status, 'offen')
|
||||
})
|
||||
|
||||
|
||||
@@ -92,11 +92,11 @@ export const customerCookie = (customerId = 'c1') =>
|
||||
email: customers[customerId]?.email || '',
|
||||
})
|
||||
|
||||
/** Admin-Session (Ticketsystem-Mitarbeiter im Portal), F11. */
|
||||
/** Alte Mitarbeiter-Sitzung im Portal (Login bis Etappe 4): /api/auth/me beendet sie, Kunden-Routen 401. */
|
||||
export const adminCookie = () =>
|
||||
sessionCookie({ role: 'admin', appwriteUserId: 'u_admin', name: 'Kenso', email: 'kenso@webklar.com' })
|
||||
|
||||
/** Admin im Modus "Als Kunde ansehen" (portalAdmin.js /view-as): Bestellen und Aendern sind gesperrt. */
|
||||
/** Alte Ansicht "Als Kunde ansehen" aus dem frueheren Admin-Panel (ohne viewId): gilt als beendet (401 view_expired). */
|
||||
export const viewAsCookie = (customerId = 'c1') =>
|
||||
sessionCookie({
|
||||
role: 'admin',
|
||||
|
||||
@@ -9,7 +9,7 @@ import { sessionMiddleware } from '../../server/middleware/session.js'
|
||||
* Express-App für Routen-Tests: wie server/index.js mit sessionMiddleware() (cookieParser mit
|
||||
* SESSION_SECRET) und express.json samt rawBody (Stripe-Webhook), aber nur mit den Routern, die der
|
||||
* Test übergibt:
|
||||
* const app = await startTestApp({ '/api/abo': aboRoutes, '/api/portal-admin': portalAdminRoutes })
|
||||
* const app = await startTestApp({ '/api/abo': aboRoutes, '/api/team': teamRoutes })
|
||||
*
|
||||
* Reihenfolge im Test: Nachbauten starten, useTestEnv(), dann dieses Modul und die Server-Module
|
||||
* dynamisch importieren. config.js liest die Umgebung beim ersten Import; zeigt sie nicht auf die
|
||||
|
||||
@@ -8,17 +8,16 @@ import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
// Chat ueber den Team-Eingang (/api/team, JWT) mit signierten Anhang-Links; Admin-Panel (/api/portal-admin) unveraendert
|
||||
// Chat ueber den Team-Eingang (/api/team, JWT) mit signierten Anhang-Links
|
||||
process.env.CHAT_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'team-chat-'))
|
||||
const aw = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { startTestApp, rawGet, post } = await import('./support/testApp.js')
|
||||
const { seedFixtures, adminCookie } = await import('./support/fixtures.js')
|
||||
const { seedFixtures } = await import('./support/fixtures.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||||
const { default: portalAdminRoutes } = await import('../server/routes/portalAdmin.js')
|
||||
const app = await startTestApp({ '/api/team': teamRoutes, '/api/portal-admin': portalAdminRoutes })
|
||||
const app = await startTestApp({ '/api/team': teamRoutes })
|
||||
const sink = mailSink()
|
||||
setMailTransportForTests(sink)
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
@@ -93,11 +92,6 @@ test('Lesen markiert Kundennachrichten', async () => {
|
||||
assert.equal((await rawGet('/api/team/chats/unread-count', staff)).body.unread, 0)
|
||||
})
|
||||
|
||||
test('Admin-Panel unveraendert: /api/portal-admin/chats mit Admin-Cookie 200, ohne 401', async () => {
|
||||
assert.equal((await rawGet('/api/portal-admin/chats', { cookie: adminCookie() })).status, 200)
|
||||
assert.equal((await rawGet('/api/portal-admin/chats')).status, 401)
|
||||
})
|
||||
|
||||
test('Team-Chat ohne JWT -> 401', async () => {
|
||||
assert.equal((await rawGet('/api/team/chats')).status, 401)
|
||||
assert.equal((await post('/api/team/chats/c1/messages', { text: 'x' })).status, 401)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// 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)
|
||||
Reference in new issue
Block a user