429 ohne Schleife (Meldung und Erneut-versuchen-Knopf, Formulare bleiben stehen), Schriftfarbe auf der Betriebsfarbe mit mindestens 4,5 : 1, AGB-Link aus der oeffentlichen Konfiguration (betrieb.agbUrl), Haken starten bei geaenderter Auswahl ohne Haken, Fehler ohne Feld nicht verschluckt, Konfiguration fail closed, ladeNeu fuer Neuladen und neu_laden, Tastatur und Fokus (Modifikatoren, Monatspfeile, Belehrungstext), Aufraeumen. UI-Testserver: Timer, offene Verbindungen, Hinweis auf sessionCookie. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
122 lines
4.6 KiB
JavaScript
122 lines
4.6 KiB
JavaScript
import http from 'node:http'
|
||
import express from 'express'
|
||
import signature from 'cookie-signature'
|
||
import Stripe from 'stripe'
|
||
import { config } from '../../server/config.js'
|
||
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/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
|
||
* Nachbauten, weigert sich dieses Modul beim Laden. Die Anfrage-Helfer sprechen die zuletzt
|
||
* gestartete App an.
|
||
*/
|
||
|
||
function assertTestEnv() {
|
||
for (const [name, value] of [
|
||
['APPWRITE_ENDPOINT', config.appwrite.endpoint],
|
||
['STRIPE_API_BASE', config.stripe.apiBase],
|
||
]) {
|
||
let host = null
|
||
try {
|
||
host = new URL(value || '').hostname
|
||
} catch {
|
||
host = null
|
||
}
|
||
if (host !== '127.0.0.1') {
|
||
throw new Error(`testApp: ${name} zeigt nicht auf 127.0.0.1 – useTestEnv() muss vor dem ersten Import von config.js laufen`)
|
||
}
|
||
}
|
||
}
|
||
assertTestEnv()
|
||
|
||
/**
|
||
* Signierter Session-Cookie wie setPortalSession (cookie-parser: Praefix "s:" + cookie-signature).
|
||
* Kunde: { customerId, appwriteUserId }. Admin: { role: 'admin', appwriteUserId, name, email }.
|
||
*/
|
||
export function sessionCookie(session) {
|
||
const value = `s:${signature.sign(JSON.stringify(session), config.sessionSecret)}`
|
||
return `${config.cookieName}=${encodeURIComponent(value)}`
|
||
}
|
||
|
||
let baseUrl = null
|
||
|
||
/** App mit den Routern (Pfad -> Router) auf 127.0.0.1 starten; liefert { url, server, close } (server: der http.Server, z. B. fuer closeAllConnections). */
|
||
export async function startTestApp(routers = {}) {
|
||
const app = express()
|
||
app.set('trust proxy', 1)
|
||
app.use(sessionMiddleware())
|
||
app.use(
|
||
express.json({
|
||
limit: '2mb',
|
||
verify: (req, _res, buf) => {
|
||
req.rawBody = buf
|
||
},
|
||
})
|
||
)
|
||
for (const [mountPath, router] of Object.entries(routers)) app.use(mountPath, router)
|
||
app.use((err, _req, res, _next) => {
|
||
if (!res.headersSent) res.status(err.status || 500).json({ error: err.message || 'Interner Serverfehler' })
|
||
})
|
||
const server = http.createServer(app)
|
||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||
baseUrl = `http://127.0.0.1:${server.address().port}`
|
||
return { url: baseUrl, server, close: () => new Promise((resolve) => server.close(resolve)) }
|
||
}
|
||
|
||
/** Anfrage an die zuletzt gestartete App (beliebige Methode) -> { status, body, headers }; body ist geparstes JSON, sonst Text. */
|
||
export async function send(method, path, { body, cookie, headers = {} } = {}) {
|
||
if (!baseUrl) throw new Error('testApp: startTestApp() zuerst aufrufen')
|
||
const res = await fetch(baseUrl + path, {
|
||
method,
|
||
headers: {
|
||
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
||
...(cookie ? { cookie } : {}),
|
||
...headers,
|
||
},
|
||
body: body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body),
|
||
redirect: 'manual',
|
||
})
|
||
const text = await res.text()
|
||
let parsed = null
|
||
try {
|
||
parsed = text ? JSON.parse(text) : null
|
||
} catch {
|
||
parsed = text
|
||
}
|
||
return { status: res.status, body: parsed, headers: res.headers }
|
||
}
|
||
|
||
/** GET -> { status, body } */
|
||
export const rawGet = (path, opts = {}) => send('GET', path, opts)
|
||
|
||
/** GET -> JSON-Body; ab Status 400 fliegt ein Fehler mit err.status und err.body. */
|
||
export async function get(path, opts = {}) {
|
||
const { status, body } = await rawGet(path, opts)
|
||
if (status >= 400) {
|
||
const err = new Error(`GET ${path} → ${status}: ${body?.error || JSON.stringify(body)}`)
|
||
err.status = status
|
||
err.body = body
|
||
throw err
|
||
}
|
||
return body
|
||
}
|
||
|
||
/** POST mit JSON-Body -> { status, body } */
|
||
export const post = (path, body, opts = {}) => send('POST', path, { ...opts, body })
|
||
|
||
// Signiert wie Stripe (whsec_test aus useTestEnv); der Schluessel wird nie fuer Netzaufrufe benutzt
|
||
const webhookSigner = new Stripe('sk_test_fake')
|
||
|
||
/** Stripe-Ereignis signiert an den Webhook schicken (F25) -> { status, body }. */
|
||
export function postWebhook(event, { path = '/api/abo/webhook', secret = config.stripe.webhookSecret || 'whsec_test' } = {}) {
|
||
const payload = JSON.stringify(event)
|
||
const stripeSignature = webhookSigner.webhooks.generateTestHeaderString({ payload, secret })
|
||
return send('POST', path, { body: payload, headers: { 'stripe-signature': stripeSignature } })
|
||
}
|