Files
Webklar-Kundenbereich/test/appwriteBulk.test.js
T
ClaudeandClaude Sonnet 5.5 a5e12d7f34 Appwrite: Bulk-Create, Bereichsabfragen, seitenweises Laden, genauerer Nachbau
appwriteAdmin: adminFetch kennt die Option raw (Body unverpackt), createDocuments
(Bulk-Create, alles oder nichts, 409 bei vorhandener ID), listAllDocuments (seitenweise
per cursorAfter, sortiert nach $id, Fehler ueber max) und Query.between/greaterThanEqual/
lessThanEqual/cursorAfter.

Nachbau: Bulk-Create mit Alles-oder-nichts (409/400, hoechstens 100), die neuen Abfragen
samt cursorAfter, Zeitpunktvergleich fuer ISO-Datumswerte, Optionen datetimes (Normalisierung
auf +00:00, leeres Datum => 400) und defaultLimit (Standard-Limit wie Appwrite). Ohne die
Optionen unveraendert. testApp exportiert send(method, path, opts).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:43 +00:00

227 lines
13 KiB
JavaScript

import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import express from 'express'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
// Nachbau wie in den Termine-Tests (Schema, Datumsfelder, Standard-Limit 25) und einer ohne Optionen
const aw = await startAppwriteMock({
schema: { termineClaims: schemaKeys('termineClaims'), termineBookings: schemaKeys('termineBookings'), termineOutbox: schemaKeys('termineOutbox') },
datetimes: TERMINE_DATETIMES,
defaultLimit: 25,
})
const plain = await startAppwriteMock()
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), plain.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { Query, createDocument, createDocuments, getDocument, updateDocument, listDocuments, listAllDocuments } = await import(
'../server/services/appwriteAdmin.js'
)
const idsOf = (collection, prefix) => [...aw.docs(collection).keys()].filter((id) => id.startsWith(prefix)).sort()
const cell = (id, over = {}) => ({
$id: id,
featureId: 'f1',
ressourceId: 'r1',
bookingId: 'b1',
zelle: '2026-10-05T10:00:00.000Z',
bis: '2026-10-05T10:30:00.000Z',
...over,
})
const rawDocs = (mock, collection) => `${mock.url}/databases/woms-database/collections/${collection}/documents`
const postJson = (url, body) => fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
test('Query-Helfer bauen die Appwrite-Abfragen', () => {
assert.deepEqual(Query.between('startsAt', 'a', 'b'), { method: 'between', attribute: 'startsAt', values: ['a', 'b'] })
assert.deepEqual(Query.greaterThanEqual('startsAt', 'a'), { method: 'greaterThanEqual', attribute: 'startsAt', values: ['a'] })
assert.deepEqual(Query.lessThanEqual('startsAt', 'b'), { method: 'lessThanEqual', attribute: 'startsAt', values: ['b'] })
assert.deepEqual(Query.cursorAfter('doc1'), { method: 'cursorAfter', values: ['doc1'] })
})
test('Bulk-Create legt 3 Dokumente in einer Anfrage an', async () => {
const created = await createDocuments('termineClaims', [cell('t2_a1'), cell('t2_a2', { bookingId: 'b2' }), cell('t2_a3')])
assert.deepEqual(created.map((d) => d.$id), ['t2_a1', 't2_a2', 't2_a3'])
assert.deepEqual(idsOf('termineClaims', 't2_a'), ['t2_a1', 't2_a2', 't2_a3'])
const second = await getDocument('termineClaims', 't2_a2')
assert.equal(second.bookingId, 'b2')
assert.equal(second.featureId, 'f1')
})
test('Bulk-Create ohne Dokumente sendet nichts', async () => {
assert.deepEqual(await createDocuments('termineClaims', []), [])
})
test('Bulk mit vorhandener ID: 409 und nichts geschrieben', async () => {
await createDocument('termineClaims', cell('t2_b2', { bookingId: 'alt' }), 't2_b2')
const before = [...aw.docs('termineClaims').entries()].length
await assert.rejects(
createDocuments('termineClaims', [cell('t2_b1'), cell('t2_b2', { bookingId: 'neu' }), cell('t2_b3')]),
(err) => err.status === 409 && err.type === 'document_already_exists'
)
assert.deepEqual(idsOf('termineClaims', 't2_b'), ['t2_b2'])
assert.equal(aw.docs('termineClaims').size, before)
assert.equal(aw.docs('termineClaims').get('t2_b2').bookingId, 'alt')
})
test('Bulk mit doppelter ID innerhalb der Anfrage: 409 und nichts geschrieben', async () => {
await assert.rejects(createDocuments('termineClaims', [cell('t2_c1'), cell('t2_c1')]), (err) => err.status === 409)
assert.deepEqual(idsOf('termineClaims', 't2_c'), [])
})
test('Bulk mit unbekanntem Attribut oder ungueltigem Datum: 400 und nichts geschrieben', async () => {
await assert.rejects(
createDocuments('termineClaims', [cell('t2_d1'), cell('t2_d2', { bogus: 1 })]),
(err) => err.status === 400 && /Unknown attribute: "bogus"/.test(err.message)
)
await assert.rejects(createDocuments('termineClaims', [cell('t2_d3'), cell('t2_d4', { zelle: '' })]), (err) => err.status === 400)
await assert.rejects(createDocuments('termineClaims', [cell('t2_d5'), { featureId: 'ohne-id' }]), (err) => err.status === 400)
assert.deepEqual(idsOf('termineClaims', 't2_d'), [])
})
test('Bulk ueber 100 Dokumente: 400 wie Appwrite, nichts geschrieben', async () => {
const many = Array.from({ length: 101 }, (_, i) => cell(`t2_e${String(i).padStart(3, '0')}`))
await assert.rejects(createDocuments('termineClaims', many), (err) => err.status === 400)
assert.deepEqual(idsOf('termineClaims', 't2_e'), [])
const exactly = await createDocuments('termineClaims', many.slice(0, 100))
assert.equal(exactly.length, 100)
})
test('between ist inklusiv, greaterThanEqual/lessThanEqual ebenso', async () => {
const starts = ['10:00', '10:30', '11:00', '11:30', '12:00']
for (const [i, t] of starts.entries()) {
await createDocument('termineBookings', { featureId: 'f_between', startsAt: `2026-10-05T${t}:00.000Z`, status: 'bestaetigt' }, `t2_bk${i}`)
}
const inFeature = Query.equal('featureId', 'f_between')
const order = Query.orderAsc('startsAt')
const between = await listDocuments('termineBookings', [inFeature, Query.between('startsAt', '2026-10-05T10:30:00.000Z', '2026-10-05T11:30:00.000Z'), order])
assert.deepEqual(between.map((d) => d.$id), ['t2_bk1', 't2_bk2', 't2_bk3'])
// dieselben Zeitpunkte in anderer Schreibweise (Berlin, Sommerzeit +02:00)
const offset = await listDocuments('termineBookings', [inFeature, Query.between('startsAt', '2026-10-05T12:30:00+02:00', '2026-10-05T13:30:00+02:00'), order])
assert.deepEqual(offset.map((d) => d.$id), ['t2_bk1', 't2_bk2', 't2_bk3'])
const from = await listDocuments('termineBookings', [inFeature, Query.greaterThanEqual('startsAt', '2026-10-05T11:30:00.000Z'), order])
assert.deepEqual(from.map((d) => d.$id), ['t2_bk3', 't2_bk4'])
const until = await listDocuments('termineBookings', [inFeature, Query.lessThanEqual('startsAt', '2026-10-05T10:30:00.000Z'), order])
assert.deepEqual(until.map((d) => d.$id), ['t2_bk0', 't2_bk1'])
// greaterThan/lessThan bleiben exklusiv
const later = await listDocuments('termineBookings', [inFeature, Query.greaterThan('startsAt', '2026-10-05T11:30:00Z'), order])
assert.deepEqual(later.map((d) => d.$id), ['t2_bk4'])
const equal = await listDocuments('termineBookings', [inFeature, Query.equal('startsAt', '2026-10-05T13:00:00+02:00')])
assert.deepEqual(equal.map((d) => d.$id), ['t2_bk2'])
})
test('cursorAfter liefert die Dokumente nach dem genannten', async () => {
for (let i = 0; i < 6; i += 1) aw.seed('termineClaims', cell(`t2_f${i}`, { featureId: 'f_cursor' }))
const page = await listDocuments('termineClaims', [Query.equal('featureId', 'f_cursor'), Query.orderAsc('$id'), Query.cursorAfter('t2_f1'), Query.limit(3)])
assert.deepEqual(page.map((d) => d.$id), ['t2_f2', 't2_f3', 't2_f4'])
await assert.rejects(listDocuments('termineClaims', [Query.cursorAfter('gibt-es-nicht'), Query.limit(3)]), (err) => err.status === 400)
})
test('listAllDocuments laedt ueber 3 Seiten, in $id-Reihenfolge und nur die passenden', async () => {
for (let i = 0; i < 25; i += 1) aw.seed('termineClaims', cell(`t2_g${String(i).padStart(2, '0')}`, { featureId: 'f_pages' }))
for (let i = 0; i < 7; i += 1) aw.seed('termineClaims', cell(`t2_g_fremd${i}`, { featureId: 'f_andere' }))
const realFetch = globalThis.fetch
let requests = 0
globalThis.fetch = (...args) => {
requests += 1
return realFetch(...args)
}
let all
try {
all = await listAllDocuments('termineClaims', [Query.equal('featureId', 'f_pages')], { pageSize: 10 })
} finally {
globalThis.fetch = realFetch
}
assert.equal(requests, 3)
assert.equal(all.length, 25)
assert.deepEqual(all.map((d) => d.$id), Array.from({ length: 25 }, (_, i) => `t2_g${String(i).padStart(2, '0')}`))
})
test('listAllDocuments: Vielfaches der Seitengroesse, leere Treffermenge und Standard-Seitengroesse', async () => {
for (let i = 0; i < 20; i += 1) aw.seed('termineClaims', cell(`t2_h${String(i).padStart(2, '0')}`, { featureId: 'f_vielfach' }))
assert.equal((await listAllDocuments('termineClaims', [Query.equal('featureId', 'f_vielfach')], { pageSize: 10 })).length, 20)
assert.deepEqual(await listAllDocuments('termineClaims', [Query.equal('featureId', 'f_leer')]), [])
// 30 Treffer > defaultLimit 25: nur mit eigenem limit-Aufruf vollstaendig
for (let i = 0; i < 30; i += 1) aw.seed('termineClaims', cell(`t2_h_x${String(i).padStart(2, '0')}`, { featureId: 'f_dreissig' }))
assert.equal((await listAllDocuments('termineClaims', [Query.equal('featureId', 'f_dreissig')])).length, 30)
})
test('listAllDocuments: mehr Treffer als max ist ein Fehler, genau max nicht', async () => {
for (let i = 0; i < 21; i += 1) aw.seed('termineClaims', cell(`t2_i${String(i).padStart(2, '0')}`, { featureId: 'f_max' }))
const filter = [Query.equal('featureId', 'f_max')]
await assert.rejects(listAllDocuments('termineClaims', filter, { pageSize: 10, max: 20 }), /mehr als 20 /)
assert.equal((await listAllDocuments('termineClaims', filter, { pageSize: 10, max: 21 })).length, 21)
await assert.rejects(listAllDocuments('termineClaims', filter, { pageSize: 0 }), /pageSize/)
})
test('defaultLimit 25 liefert ohne limit-Abfrage 25 von 30', async () => {
for (let i = 0; i < 30; i += 1) aw.seed('termineOutbox', { $id: `t2_j${String(i).padStart(2, '0')}`, featureId: 'f_limit', status: 'wartet' })
const filter = [Query.equal('featureId', 'f_limit')]
assert.equal((await listDocuments('termineOutbox', filter)).length, 25)
assert.equal((await listDocuments('termineOutbox', [...filter, Query.limit(30)])).length, 30)
assert.equal((await listDocuments('termineOutbox', [...filter, Query.limit(5)])).length, 5)
})
test('Datum mit Z kommt als +00:00 zurueck (Anlegen, Aendern, Bulk, Seed)', async () => {
const made = await createDocument('termineBookings', { featureId: 'f_dt', startsAt: '2026-10-05T10:00:00Z', endsAt: '2026-10-05T12:30:00+02:00', status: 'x' }, 't2_dt1')
assert.equal(made.startsAt, '2026-10-05T10:00:00.000+00:00')
assert.equal(made.endsAt, '2026-10-05T10:30:00.000+00:00')
assert.equal((await getDocument('termineBookings', 't2_dt1')).startsAt, '2026-10-05T10:00:00.000+00:00')
const patched = await updateDocument('termineBookings', 't2_dt1', { heldUntil: '2026-10-05T09:59:00.5Z' })
assert.equal(patched.heldUntil, '2026-10-05T09:59:00.500+00:00')
assert.equal(patched.startsAt, '2026-10-05T10:00:00.000+00:00')
const [bulk] = await createDocuments('termineClaims', [cell('t2_dt2', { zelle: '2026-10-05T10:05:00Z' })])
assert.equal(bulk.zelle, '2026-10-05T10:05:00.000+00:00')
assert.equal(bulk.bis, '2026-10-05T10:30:00.000+00:00')
aw.seed('termineBookings', { $id: 't2_dt3', startsAt: '2026-10-05T10:00:00Z' })
assert.equal(aw.docs('termineBookings').get('t2_dt3').startsAt, '2026-10-05T10:00:00.000+00:00')
// Textfelder, die zufaellig wie ein Datum aussehen, bleiben unveraendert
const text = await createDocument('termineBookings', { gastNachricht: '2026-10-05T10:00:00Z' }, 't2_dt4')
assert.equal(text.gastNachricht, '2026-10-05T10:00:00Z')
// null bleibt erlaubt (optionales Datumsfeld leeren)
const cleared = await updateDocument('termineBookings', 't2_dt1', { heldUntil: null })
assert.equal(cleared.heldUntil, null)
})
test('leeres oder unlesbares Datum: 400 beim Anlegen und Aendern', async () => {
await assert.rejects(createDocument('termineBookings', { startsAt: '' }, 't2_dt5'), (err) => err.status === 400)
await assert.rejects(createDocument('termineBookings', { startsAt: 'morgen' }, 't2_dt6'), (err) => err.status === 400)
await assert.rejects(updateDocument('termineBookings', 't2_dt1', { endsAt: '' }), (err) => err.status === 400)
assert.equal(aw.docs('termineBookings').has('t2_dt5'), false)
assert.equal((await getDocument('termineBookings', 't2_dt1')).endsAt, '2026-10-05T10:30:00.000+00:00')
})
test('Nachbau ohne Optionen verhaelt sich wie bisher', async () => {
const col = rawDocs(plain, 'irgendwas')
for (let i = 0; i < 30; i += 1) {
const res = await postJson(col, { documentId: `p${String(i).padStart(2, '0')}`, data: { n: i, am: '2026-10-05T10:00:00Z' } })
assert.equal(res.status, 201)
}
const listed = await (await fetch(col)).json()
assert.equal(listed.documents.length, 30)
assert.equal(listed.documents[0].am, '2026-10-05T10:00:00Z')
const queries = [{ method: 'between', attribute: 'n', values: [5, 7] }, { method: 'orderAsc', attribute: 'n' }]
const between = await (await fetch(`${col}?${queries.map((q) => `queries[]=${encodeURIComponent(JSON.stringify(q))}`).join('&')}`)).json()
assert.deepEqual(between.documents.map((d) => d.n), [5, 6, 7])
const bulk = await postJson(col, { documents: [{ $id: 'q1', n: 100 }, { $id: 'q2', n: 101 }] })
assert.equal(bulk.status, 201)
assert.equal((await postJson(col, { documents: [{ $id: 'q3', n: 102 }, { $id: 'q1', n: 103 }] })).status, 409)
assert.equal(plain.docs('irgendwas').has('q3'), false)
})
test('testApp.send ruft beliebige Methoden auf', async () => {
const { startTestApp, send } = await import('./support/testApp.js')
const router = express.Router()
router.put('/echo', (req, res) => res.json({ method: req.method, body: req.body, via: req.get('x-test') }))
const app = await startTestApp({ '/t': router })
try {
const res = await send('PUT', '/t/echo', { body: { a: 1 }, headers: { 'x-test': 'ja' } })
assert.equal(res.status, 200)
assert.deepEqual(res.body, { method: 'PUT', body: { a: 1 }, via: 'ja' })
} finally {
await app.close()
}
})