Termine-Outbox: notEqual mit genau einem Wert (Appwrite 1.8.1), nachrangige Mails kommen wieder dran
Auf der echten Appwrite 1.8.1 lehnt notEqual mit Liste die Abfrage ab (HTTP 400
general_query_invalid, "NotEqual queries require exactly one value"). Die Klasse normal der
Mail-Warteschlange schickte immer eine Liste, jeder Lauf warf dort, und die nachrangige Klasse
(G05, K20, K21) kam nie dran.
- appwriteAdmin: Query.notEqual nimmt genau einen Wert (eine Liste wirft einen TypeError, der
keinerVon nennt); neu keinerVon(attribute, werte) = je Wert ein notEqual (Appwrite schneidet
sie); dokumenteUrl() baut die Listen-URL ohne Netz, mit derselben Serialisierung wie die Anfrage.
- outbox: klassenFilter liefert fuer jede Klasse eine Liste von Abfragen (normal = sechs notEqual
auf art), die Ausschlussliste ist keinerVon('featureId', ...). AUSSCHLUSS_MAX 90 -> 30 (der nginx
antwortet in dieser Abfrageform ab 46 Funktions-IDs mit 414); der Kommentar "100 Werte je Abfrage"
entfaellt.
- Nachbau (appwriteMock): notEqual mit Liste oder ohne Wert => 400 wie Appwrite; ein Dokument mit
Feld null oder ohne Feld passt nicht zu notEqual (SQL-Semantik).
- Tests: Query-Helfer, keinerVon, dokumenteUrl, Nachbau (400, null), ein Lauf bedient alle drei
Klassen, Ausschlussliste hoechstens 30 und URL unter 6000 Zeichen im schlimmsten Fall.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af63b69ec1
commit
2c7cd26532
5 files changed
+196
-39
No files matched your search
@@ -18,13 +18,13 @@ after(() => aw.close())
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { createDocument, deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { createDocument, deleteDocument, dokumenteUrl } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { loadTemplate } = await import('../server/services/mailTemplates.js')
|
||||
const { icsTermin } = await import('../server/services/termine/ics.js')
|
||||
const O = await import('../server/services/termine/outbox.js')
|
||||
const { einreihen, sendeEintrag, verarbeite, warteAufVersand, zustellFehler, raeumeAuf, beendeFestgefahrene, verwerfeWartende, pausiertBis, resetOutboxForTests } = O
|
||||
const { MAIL_VORRANG, MAIL_NACHRANGIG, mailKlasse, pruefePlatz, mailBudget, verworfeneG01 } = O
|
||||
const { MAIL_VORRANG, MAIL_NACHRANGIG, AUSSCHLUSS_MAX, mailKlasse, pruefePlatz, mailBudget, verworfeneG01 } = O
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
const MIN = 60000
|
||||
@@ -509,15 +509,21 @@ test('Reserve: mit sehr kleinem Stundenbudget darf mindestens eine nachrangige M
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
const listenAbfragen = () => aw.listCalls('termineOutbox')
|
||||
// Appwrite 1.8.1: notEqual nimmt genau einen Wert, mehrere notEqual werden geschnitten. Die normale Klasse sind darum sechs
|
||||
// notEqual auf art, die Ausschlussliste je Funktion eine notEqual auf featureId.
|
||||
const abfrageKlasse = ({ queries }) => {
|
||||
const art = queries.find((q) => q.attribute === 'art')
|
||||
if (art.method === 'notEqual') return 'normal'
|
||||
return art.values.includes('G01') ? 'vorrang' : 'nachrangig'
|
||||
}
|
||||
const abfrageKlassen = () => listenAbfragen().map(abfrageKlasse)
|
||||
const ausschluss = ({ queries }) => queries.find((q) => q.method === 'notEqual' && q.attribute === 'featureId')?.values ?? null
|
||||
const ausschluss = ({ queries }) => {
|
||||
const ids = queries.filter((q) => q.method === 'notEqual' && q.attribute === 'featureId').map((q) => q.values[0])
|
||||
return ids.length ? ids : null
|
||||
}
|
||||
const begrenzteLeseabfragen = (abfragen) => {
|
||||
for (const abfrage of abfragen) {
|
||||
assert.ok(abfrage.queries.filter((q) => q.method === 'notEqual').every((q) => q.values.length === 1), `notEqual mit genau einem Wert (Appwrite 1.8.1): ${JSON.stringify(abfrage.queries)}`)
|
||||
const limit = abfrage.queries.find((q) => q.method === 'limit')
|
||||
assert.ok(limit && limit.values[0] <= 100, `limit höchstens 100: ${JSON.stringify(abfrage.queries)}`)
|
||||
assert.ok(!abfrage.queries.some((q) => q.method === 'cursorAfter' || (q.method === 'orderAsc' && q.attribute === '$id')), 'kein $id-Seitenlauf')
|
||||
@@ -606,6 +612,31 @@ test('Reihenfolge: Vorrang vor normal vor nachrangig, innerhalb einer Klasse die
|
||||
}
|
||||
})
|
||||
|
||||
// G5 (2026-10-05): auf der echten Appwrite 1.8.1 lehnt notEqual mit Liste die Abfrage ab (HTTP 400). Die normale Klasse warf dann bei jedem Lauf,
|
||||
// die nachrangige (G05, K20, K21) kam nie dran. Der Nachbau lehnt es jetzt genauso ab.
|
||||
test('Live-Appwrite: ein Lauf mit fälligen Einträgen aller drei Klassen bedient Vorrang, normal UND nachrangig (G05, K20, K21)', async () => {
|
||||
const arten = ['G02', 'G08', 'G03', 'K17', 'G05', 'K20', 'K21']
|
||||
const docs = []
|
||||
for (const [i, art] of arten.entries()) {
|
||||
docs.push(await legeAn({ art, bookingId: `b_${art}`, an: `${art.toLowerCase()}@example.com`, naechsterVersuch: plus(-(10 - i) * MIN).toISOString() }))
|
||||
}
|
||||
aw.resetListCalls()
|
||||
const ergebnis = await verarbeite(JETZT)
|
||||
assert.deepEqual(ergebnis, { faellig: 7, gesendet: 7 })
|
||||
for (const doc of docs) assert.equal((await lies(doc.$id)).status, 'gesendet', doc.art)
|
||||
assert.deepEqual(empfaenger(), ['g02@example.com', 'g08@example.com', 'g03@example.com', 'k17@example.com', 'g05@example.com', 'k20@example.com', 'k21@example.com'])
|
||||
assert.deepEqual(abfrageKlassen(), ['vorrang', 'normal', 'nachrangig'])
|
||||
begrenzteLeseabfragen(listenAbfragen())
|
||||
// die normale Klasse: sechs einzelne notEqual auf art, zusammen alle Arten der anderen Klassen
|
||||
const normal = listenAbfragen().find((a) => abfrageKlasse(a) === 'normal')
|
||||
const ausgeschlossen = normal.queries.filter((q) => q.method === 'notEqual' && q.attribute === 'art')
|
||||
assert.ok(ausgeschlossen.every((q) => q.values.length === 1))
|
||||
assert.deepEqual(ausgeschlossen.map((q) => q.values[0]).sort(), [...MAIL_VORRANG, ...MAIL_NACHRANGIG].sort())
|
||||
// vorrang und nachrangig: equal mit Liste ist bei Appwrite erlaubt
|
||||
const nachrangig = listenAbfragen().find((a) => abfrageKlasse(a) === 'nachrangig')
|
||||
assert.deepEqual(nachrangig.queries.find((q) => q.attribute === 'art'), { method: 'equal', attribute: 'art', values: [...MAIL_NACHRANGIG] })
|
||||
})
|
||||
|
||||
test('kein Verhungern: 150 fällige G02 der Funktion A (Kontingent 40) vor einem G02 der Funktion B ⇒ ein Lauf sendet 40 von A und die Mail von B', async () => {
|
||||
for (let i = 0; i < 150; i++) await legeAn({ featureId: 'f_A', art: 'G02', bookingId: `b_a${i}`, naechsterVersuch: plus(-3600000 + i * 1000).toISOString() })
|
||||
const b = await legeAn({ featureId: 'f_B', art: 'G02', bookingId: 'b_b', an: 'b@example.com', naechsterVersuch: plus(-1000).toISOString() })
|
||||
@@ -633,21 +664,38 @@ test('Runden: höchstens 5 Abfragen je Klasse; mailListe wird bei jedem Aufruf g
|
||||
assert.deepEqual(await verarbeite(JETZT), { faellig: 2, gesendet: 2 })
|
||||
})
|
||||
|
||||
test('Ausschlussliste: höchstens 90 Funktionen je Abfrage, auch wenn mehr blockiert sind', async () => {
|
||||
// Appwrite 1.8.1 (M4): ab etwa 45 featureId-notEqual in dieser Abfrageform antwortet der nginx mit 414 (URL zu lang); 30 lassen Luft
|
||||
test('Ausschlussliste: höchstens AUSSCHLUSS_MAX (30) Funktionen je Abfrage, auch wenn mehr blockiert sind; die URL bleibt unter 6000 Zeichen', async () => {
|
||||
assert.equal(AUSSCHLUSS_MAX, 30)
|
||||
config.termine.mailsProFunktion = 1
|
||||
// 36 Zeichen lange Funktions-IDs: so lang sind die Dokument-IDs in Produktion
|
||||
const feature = (i) => `f-${String(i).padStart(34, '0')}`
|
||||
assert.equal(feature(7).length, 36)
|
||||
for (let i = 0; i < 100; i++) {
|
||||
for (const teil of ['a', 'b']) {
|
||||
await legeAn({ featureId: `f_${i}`, art: 'G02', bookingId: `b_${i}${teil}`, naechsterVersuch: plus(-3600000 + (2 * i + (teil === 'a' ? 0 : 1)) * 1000).toISOString() })
|
||||
await legeAn({ featureId: feature(i), art: 'G02', bookingId: `b_${i}${teil}`, naechsterVersuch: plus(-3600000 + (2 * i + (teil === 'a' ? 0 : 1)) * 1000).toISOString() })
|
||||
}
|
||||
}
|
||||
aw.resetListCalls()
|
||||
assert.deepEqual(await verarbeite(JETZT), { faellig: 210, gesendet: 100 })
|
||||
// Runde 1: Seite 1 (f0 bis f49) sendet 50, blockiert 50. Runde 2 (die ersten 30 ausgeschlossen): f30b bis f49b sind wieder dabei, dahinter
|
||||
// f50 bis f89 (40 gesendet). Runde 3: 60 + 20 Eintraege, die letzten 10 gehen raus. Die Seite ist dann nicht mehr voll.
|
||||
assert.deepEqual(await verarbeite(JETZT), { faellig: 280, gesendet: 100 })
|
||||
const abfragen = listenAbfragen()
|
||||
begrenzteLeseabfragen(abfragen)
|
||||
const vorrang = abfragen.filter((a) => abfrageKlasse(a) === 'vorrang')
|
||||
assert.deepEqual(vorrang.map((a) => ausschluss(a)?.length ?? 0), [0, 50, 90], 'Runde 3: 100 blockierte Funktionen, 90 davon in der Liste')
|
||||
for (const abfrage of abfragen) assert.ok((ausschluss(abfrage)?.length ?? 0) <= 90)
|
||||
assert.ok(abfragen.filter((a) => abfrageKlasse(a) !== 'vorrang').every((a) => ausschluss(a).length === 90), 'blockierte Funktionen gelten für alle Klassen')
|
||||
assert.deepEqual(vorrang.map((a) => ausschluss(a)?.length ?? 0), [0, 30, 30], '100 Funktionen blockiert, aber nur 30 in der Liste')
|
||||
assert.deepEqual(ausschluss(vorrang[1]), Array.from({ length: 30 }, (_, i) => feature(i)), 'die zuerst blockierten')
|
||||
for (const abfrage of abfragen) assert.ok((ausschluss(abfrage)?.length ?? 0) <= AUSSCHLUSS_MAX)
|
||||
assert.ok(abfragen.filter((a) => abfrageKlasse(a) !== 'vorrang').every((a) => ausschluss(a).length === AUSSCHLUSS_MAX), 'blockierte Funktionen gelten für alle Klassen')
|
||||
assert.equal(alle().filter((d) => d.status === 'gesendet').length, 100)
|
||||
|
||||
// Schlimmster Fall: die normale Klasse (6 notEqual auf art) mit voller Ausschlussliste. Dieselbe Serialisierung wie die Anfrage an Appwrite.
|
||||
const normal = abfragen.find((a) => abfrageKlasse(a) === 'normal')
|
||||
assert.equal(normal.queries.filter((q) => q.method === 'notEqual' && q.attribute === 'art').length, 6)
|
||||
assert.equal(normal.queries.filter((q) => q.method === 'notEqual' && q.attribute === 'featureId').length, 30)
|
||||
const laengen = abfragen.map((a) => dokumenteUrl(col(), a.queries).length)
|
||||
assert.ok(Math.max(...laengen) < 6000, `längste Abfrage-URL ${Math.max(...laengen)} Zeichen`)
|
||||
assert.equal(laengen[abfragen.indexOf(normal)], Math.max(...laengen), 'die normale Klasse ist der längste Fall')
|
||||
})
|
||||
|
||||
test('Reserve erreicht: keine Abfrage der nachrangigen Klasse mehr; Vorrang und normal kommen trotzdem dran', async () => {
|
||||
|
||||
Reference in new issue
Block a user