Files
Webklar-Kundenbereich/test/support.test.js
T

164 lines
10 KiB
JavaScript

import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Nachbauten starten und die Umgebung setzen, BEVOR config.js geladen wird
const aw = await startAppwriteMock({ schema: { subscriptions: ['customerId', 'status'] } })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { createDocument, getDocument, listDocuments, Query } = await import('../server/services/appwriteAdmin.js')
const { getStripe } = await import('../server/services/stripeBilling.js')
test('Appwrite-Nachbau: anlegen, lesen, filtern', async () => {
await createDocument('subscriptions', { customerId: 'c1', status: 'active' }, 'sub_a')
await createDocument('subscriptions', { customerId: 'c2', status: 'active' }, 'sub_c')
assert.equal((await getDocument('subscriptions', 'sub_a')).status, 'active')
const list = await listDocuments('subscriptions', [Query.equal('customerId', 'c1'), Query.limit(5)])
assert.deepEqual(list.map((d) => d.$id), ['sub_a'])
})
test('Appwrite-Nachbau: doppelte ID 409, fehlendes Dokument 404', async () => {
await assert.rejects(createDocument('subscriptions', { customerId: 'c1' }, 'sub_a'), (e) => e.status === 409)
await assert.rejects(getDocument('subscriptions', 'gibt_es_nicht'), (e) => e.status === 404)
})
test('Appwrite-Nachbau: Schema wie Appwrite', async () => {
await assert.rejects(createDocument('subscriptions', { unbekannt: 1 }, 'sub_b'), (e) => e.status === 400)
})
// Appwrite 1.8: Dokument-IDs hoechstens 36 Zeichen aus a-z, A-Z, 0-9, Punkt, Bindestrich, Unterstrich,
// nicht mit einem Sonderzeichen beginnend - sonst 400 (Task 7, Fix 1)
test('Appwrite-Nachbau: Dokument-ID nach Appwrite-Regeln', async () => {
const tooLong = 'abschluss-sub_1SD6vPRxAbCdEfGhIjKlMnOp' // 38 Zeichen
await assert.rejects(createDocument('subscriptions', { customerId: 'c1' }, tooLong), (e) => e.status === 400 && /documentId/.test(e.message))
await assert.rejects(createDocument('subscriptions', { customerId: 'c1' }, '-abc'), (e) => e.status === 400)
await assert.rejects(createDocument('subscriptions', { customerId: 'c1' }, 'abc/def'), (e) => e.status === 400)
const ok = await createDocument('subscriptions', { customerId: 'c1' }, `abschluss-${'f'.repeat(26)}`)
assert.equal(ok.$id.length, 36)
})
test('Appwrite-Nachbau: failNext liefert einmal den gewuenschten Fehler', async () => {
aw.failNext({ collection: 'subscriptions', method: 'POST', status: 500 })
await assert.rejects(createDocument('subscriptions', { customerId: 'c9' }, 'sub_fail'), (e) => e.status === 500)
assert.equal((await createDocument('subscriptions', { customerId: 'c9' }, 'sub_fail')).$id, 'sub_fail')
// Nur die passende Anfrage scheitert; andere Collections/Methoden laufen durch
aw.failNext({ collection: 'hourCredits', method: 'POST', status: 404 })
assert.equal((await getDocument('subscriptions', 'sub_fail')).$id, 'sub_fail')
await assert.rejects(createDocument('hourCredits', { customerId: 'c9' }, 'hc_1'), (e) => e.status === 404)
// after: so viele passende Anfragen gehen vorher noch durch (Task 8: erst die zweite PATCH scheitert).
// adminFetch bildet jeden 5xx-Status auf status 500 ab.
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 503, after: 1 })
assert.equal((await getDocument('subscriptions', 'sub_fail')).$id, 'sub_fail')
await assert.rejects(getDocument('subscriptions', 'sub_fail'), (e) => e.status === 500 && /503/.test(e.message))
assert.equal((await getDocument('subscriptions', 'sub_fail')).$id, 'sub_fail')
})
test('Stripe-Nachbau: getStripe spricht mit 127.0.0.1', async () => {
st.subscriptions.set('sub_1', { id: 'sub_1', object: 'subscription', items: { object: 'list', data: [] } })
assert.equal((await getStripe().subscriptions.retrieve('sub_1')).id, 'sub_1')
assert.equal(st.calls.at(-1).path, '/v1/subscriptions/sub_1')
})
test('Stripe-Nachbau: unbekannte ID 404', async () => {
await assert.rejects(getStripe().prices.retrieve('price_weg'), (e) => e.statusCode === 404)
})
test('useTestEnv lehnt fremde Hosts ab', () => {
assert.throws(() => useTestEnv({ appwriteUrl: 'https://ticket.webklar.com/v1', stripeUrl: st.url }))
assert.equal(new URL(process.env.APPWRITE_ENDPOINT).hostname, '127.0.0.1')
})
// F17 (Task 8): POST /v1/subscriptions/:id mit Positionen (anlegen per Preis, aendern per ID), Metadaten
// (leerer Wert loescht wie bei Stripe) und einmaligem Zusatz nextSubscriptionUpdate fuer die Antwort
test('Stripe-Nachbau: Abo-Update mit Positionen, Metadaten, einmaliger Zusatz', async () => {
const stripe = getStripe()
st.prices.set('price_a', { id: 'price_a', object: 'price', active: true, lookup_key: 'lookup_a', unit_amount: 100 })
st.subscriptions.set('sub_u', {
id: 'sub_u',
object: 'subscription',
status: 'active',
metadata: { keep: '1', drop: 'x' },
items: { object: 'list', data: [{ id: 'si_1', object: 'subscription_item', quantity: 1, price: { id: 'price_0', object: 'price' } }] },
})
const added = await stripe.subscriptions.update('sub_u', {
items: [{ price: 'price_a', quantity: 2 }],
metadata: { drop: '', neu: 'y' },
proration_behavior: 'none',
})
assert.equal(added.items.data.length, 2)
const item = added.items.data[1]
assert.equal(item.quantity, 2)
assert.equal(item.price.id, 'price_a')
assert.equal(item.price.lookup_key, 'lookup_a')
assert.match(item.id, /^si_/)
assert.deepEqual(added.metadata, { keep: '1', neu: 'y' })
assert.equal(added.status, 'active')
assert.equal(st.calls.at(-1).body['items[0][price]'], 'price_a')
const changed = await stripe.subscriptions.update('sub_u', { items: [{ id: item.id, quantity: 0 }] })
assert.equal(changed.items.data[1].quantity, 0)
assert.equal((await stripe.subscriptions.retrieve('sub_u')).items.data[1].quantity, 0, 'gespeichert')
// Wie Stripe: derselbe Preis nicht zweimal, unbekannter Preis oder Posten 400/404, unbekanntes Abo 404
await assert.rejects(stripe.subscriptions.update('sub_u', { items: [{ price: 'price_a', quantity: 1 }] }), (e) => e.statusCode === 400)
await assert.rejects(stripe.subscriptions.update('sub_u', { items: [{ price: 'price_gibt_es_nicht', quantity: 1 }] }), (e) => e.statusCode === 400)
await assert.rejects(stripe.subscriptions.update('sub_u', { items: [{ id: 'si_gibt_es_nicht', quantity: 1 }] }), (e) => e.statusCode === 400)
await assert.rejects(stripe.subscriptions.update('sub_gibt_es_nicht', { metadata: { a: 'b' } }), (e) => e.statusCode === 404)
// Kuendigungsfelder wie cancelAtPeriodEnd/cancelSubscriptionAt sie setzen
const cancelled = await stripe.subscriptions.update('sub_u', { cancel_at_period_end: true, metadata: { cancelSource: 'test' } })
assert.equal(cancelled.cancel_at_period_end, true)
assert.equal(cancelled.metadata.cancelSource, 'test')
const dated = await stripe.subscriptions.update('sub_u', { cancel_at: 1800000000, proration_behavior: 'none' })
assert.equal(dated.cancel_at, 1800000000)
// nextSubscriptionUpdate: einmalig in die naechste Antwort gemischt, nicht gespeichert
st.nextSubscriptionUpdate = { pending_update: { expires_at: 1 }, latest_invoice: { status: 'open', hosted_invoice_url: 'https://pay.example/inv' } }
const pending = await stripe.subscriptions.update('sub_u', { metadata: { a: 'b' } })
assert.deepEqual(pending.pending_update, { expires_at: 1 })
assert.equal(pending.latest_invoice.hosted_invoice_url, 'https://pay.example/inv')
assert.equal(pending.metadata.a, 'b')
assert.equal((await stripe.subscriptions.retrieve('sub_u')).pending_update, undefined)
assert.equal((await stripe.subscriptions.update('sub_u', { metadata: { a: 'c' } })).pending_update, undefined)
assert.equal(st.nextSubscriptionUpdate, null)
})
// F17: GET /v1/invoices/:id bettet wie Stripe hoechstens 10 Zeilen ein, GET /v1/invoices/:id/lines blaettert
test('Stripe-Nachbau: Rechnung bettet höchstens 10 Zeilen ein, /lines blättert', async () => {
const stripe = getStripe()
st.invoices.set('in_p', { id: 'in_p', object: 'invoice', lines: { object: 'list', data: Array.from({ length: 12 }, (_, i) => ({ amount: i })) } })
const invoice = await stripe.invoices.retrieve('in_p')
assert.equal(invoice.lines.data.length, 10)
assert.equal(invoice.lines.has_more, true)
assert.ok(invoice.lines.data.every((l) => l.id && l.object === 'line_item'))
const page = await stripe.invoices.listLineItems('in_p', { limit: 5 })
assert.equal(page.data.length, 5)
assert.equal(page.has_more, true)
const all = await stripe.invoices.listLineItems('in_p', { limit: 5 }).autoPagingToArray({ limit: 100 })
assert.deepEqual(all.map((l) => l.amount), [...Array(12).keys()])
const small = await stripe.invoices.retrieve('in_p') // unveraendert seedbar: die Quelle behaelt alle Zeilen
assert.equal(small.lines.data.length, 10)
st.invoices.set('in_q', { id: 'in_q', object: 'invoice', lines: { object: 'list', data: [{ amount: 1 }] } })
assert.equal((await stripe.invoices.retrieve('in_q')).lines.has_more, false)
await assert.rejects(stripe.invoices.listLineItems('in_nix'), (e) => e.statusCode === 404)
})
// F17: POST /v1/invoices/create_preview liefert die vorab hinterlegte Vorschau (Objekt oder Funktion des Aufrufs)
test('Stripe-Nachbau: Rechnungsvorschau aus dem Seed', async () => {
const stripe = getStripe()
const params = { subscription: 'sub_u', subscription_details: { items: [{ id: 'si_1', quantity: 2 }], proration_behavior: 'create_prorations', proration_date: 1 } }
const empty = await stripe.invoices.createPreview(params)
assert.deepEqual(empty.lines.data, [])
st.invoicePreview = { object: 'invoice', lines: { object: 'list', data: [{ amount: 42 }], has_more: false } }
assert.equal((await stripe.invoices.createPreview(params)).lines.data[0].amount, 42)
assert.equal(st.calls.at(-1).body['subscription_details[items][0][quantity]'], '2')
assert.equal(st.calls.at(-1).body['subscription_details[proration_behavior]'], 'create_prorations')
st.invoicePreview = (body) => ({ object: 'invoice', lines: { data: [{ amount: Number(body.subscription_details.items[0].quantity) * 100 }] } })
assert.equal((await stripe.invoices.createPreview(params)).lines.data[0].amount, 200)
await assert.rejects(stripe.invoices.createPreview({ subscription: 'sub_nix' }), (e) => e.statusCode === 404)
st.invoicePreview = null
})