Files
Webklar-Kundenbereich/test/auftraegeAuftragStore.test.js
2026-10-07 14:05:49 +00:00

153 lines
10 KiB
JavaScript

// test/auftraegeAuftragStore.test.js
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { AUFTRAEGE_DATETIMES } from '../server/services/auftraege/schema.js'
// Speicher der Auftragsbestätigungen. Nachbau von Appwrite mit dem Schema aus schema.js, Datumsfeldern wie in Appwrite
// ("+00:00") und dem Standard-Limit 25.
const aw = await startAppwriteMock({ schema: { auftraege: schemaKeys('auftraege') }, datetimes: AUFTRAEGE_DATETIMES, defaultLimit: 25 })
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { docToAuftrag, auftragToDoc, getAuftrag, listAuftraege, createAuftrag, updateAuftrag, deleteAuftrag, findAuftragByTokenHash, findAuftragByNummer } =
await import('../server/services/auftraege/auftragStore.js')
const { AuftragError } = await import('../server/services/auftraege/fehler.js')
const COL = config.collections.auftraege
beforeEach(async () => {
for (const id of [...aw.docs(COL).keys()]) await deleteDocument(COL, id)
})
const ENTWURF = {
kundenId: 'c1', projektId: 'p1', titel: 'Website Bäckerei', zeitquelle: ['t1'], art: 'festpreis',
positionen: [{ beschreibung: 'Startseite', menge: 1, einzelpreisCents: 50000 }],
summeCents: 50000, zahlungsplan: { art: 'anzahlung', prozent: 50, text: '' }, zahlungszielTage: 14,
auftragVom: '2026-10-05', auftragWeg: 'telefon', erstelltVon: 'kenso@webklar.com',
}
const FELDER = 'id nummer jahr laufnummer status kundenId kunde projektId titel zeitquelle art positionen stundensatzCents schaetzungMinuten summeCents zahlungsplan zahlungszielTage auftragVom auftragWeg startAm fertigBis ersetztId ersetztDurchId texteFassung agbFassung pdfDatei pdfSha256 gesendetAm gesendetAn mailStatus mailFehler tokenHash tokenGueltigBis bestaetigtAm bestaetigtUeber bestaetigtPdfSha256 fruehbeginn fruehbeginnFassung storniertAm widerrufenAm erstelltVon erstelltAm geaendertAm'
const HASH = 'a'.repeat(64)
const seed = (id, felder = {}) => aw.seed(COL, { $id: id, kundenId: 'c1', status: 'entwurf', ...felder })
test('createAuftrag legt einen Entwurf an: Status erzwungen, Zeitpunkte gesetzt, JSON-Felder serialisiert', async () => {
const vor = Date.now()
const auftrag = await createAuftrag({ ...ENTWURF, status: 'versendet' })
assert.deepEqual(Object.keys(auftrag).sort(), FELDER.split(' ').sort(), 'genau die 43 Felder aus V3')
assert.match(auftrag.id, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
assert.equal(auftrag.status, 'entwurf')
assert.deepEqual(
{ nummer: auftrag.nummer, jahr: auftrag.jahr, laufnummer: auftrag.laufnummer, kunde: auftrag.kunde, gesendetAm: auftrag.gesendetAm, mailStatus: auftrag.mailStatus, fruehbeginn: auftrag.fruehbeginn },
{ nummer: '', jahr: null, laufnummer: null, kunde: null, gesendetAm: null, mailStatus: '', fruehbeginn: false },
)
assert.deepEqual(auftrag.positionen, ENTWURF.positionen)
assert.deepEqual(auftrag.zahlungsplan, ENTWURF.zahlungsplan)
assert.deepEqual(auftrag.zeitquelle, ['t1'])
assert.deepEqual([auftrag.stundensatzCents, auftrag.schaetzungMinuten], [null, null])
assert.match(auftrag.erstelltAm, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
assert.equal(auftrag.geaendertAm, auftrag.erstelltAm)
assert.ok(Date.parse(auftrag.erstelltAm) >= vor)
const doc = aw.docs(COL).get(auftrag.id)
assert.equal(doc.status, 'entwurf')
assert.equal(doc.positionenJson, JSON.stringify(ENTWURF.positionen))
assert.equal(doc.zeitquelleJson, '["t1"]')
assert.deepEqual(await getAuftrag(auftrag.id), auftrag)
})
test('docToAuftrag: fehlende Felder bekommen Vorgaben, kaputtes JSON wirft nie, Zeitpunkte werden zu ISO mit Z', () => {
const auftrag = docToAuftrag({
$id: 'x1', kundenId: 'c1', status: 'versendet', nummer: 'AB-2026-0003', jahr: 2026, laufnummer: 3,
kundeJson: '', positionenJson: '{kaputt', zeitquelleJson: '["t1","",5," t2 "]', zahlungsplanJson: '[1]',
gesendetAm: '2026-10-06T10:00:00.000+00:00', tokenGueltigBis: null, fruehbeginn: true, summeCents: 1000,
$createdAt: '2026-10-06T08:00:00.000+00:00', $updatedAt: '2026-10-06T09:00:00.000+00:00',
})
assert.equal(auftrag.id, 'x1')
assert.deepEqual([auftrag.nummer, auftrag.jahr, auftrag.laufnummer, auftrag.summeCents], ['AB-2026-0003', 2026, 3, 1000])
assert.deepEqual([auftrag.kunde, auftrag.positionen, auftrag.zeitquelle], [null, [], ['t1', 't2']])
assert.deepEqual(auftrag.zahlungsplan, { art: 'nach_abnahme', prozent: null, text: '' })
assert.equal(auftrag.zahlungszielTage, 14)
assert.deepEqual([auftrag.gesendetAm, auftrag.tokenGueltigBis, auftrag.bestaetigtAm], ['2026-10-06T10:00:00.000Z', null, null])
assert.equal(auftrag.fruehbeginn, true)
assert.deepEqual([auftrag.erstelltAm, auftrag.geaendertAm], ['2026-10-06T08:00:00.000Z', '2026-10-06T09:00:00.000Z'])
})
test('auftragToDoc: nur Felder aus V1, JSON serialisiert, undefined fehlt, null löscht', () => {
const doc = auftragToDoc({
id: 'x1', $id: 'x1', fremd: 1, status: 'versendet', kunde: { name: 'Anna Beispiel', kundenart: 'privat' },
zeitquelle: ['t1'], positionen: [], zahlungsplan: { art: 'frei', prozent: null, text: 'x' },
gesendetAm: '2026-10-06T10:00:00.000Z', tokenGueltigBis: null, titel: undefined, tokenHash: null,
})
assert.deepEqual(doc, {
status: 'versendet', kundeJson: '{"name":"Anna Beispiel","kundenart":"privat"}', zeitquelleJson: '["t1"]', positionenJson: '[]',
zahlungsplanJson: '{"art":"frei","prozent":null,"text":"x"}', gesendetAm: '2026-10-06T10:00:00.000Z', tokenGueltigBis: null, tokenHash: '',
})
assert.equal(auftragToDoc({ kunde: null }).kundeJson, '')
})
test('auftragToDoc: ein Text über der Spaltengröße ist ein 400, ein falscher Typ ein Programmfehler', () => {
assert.throws(() => auftragToDoc({ titel: 'x'.repeat(201) }), (err) => err instanceof AuftragError && err.status === 400 && /titel/.test(err.message))
assert.throws(() => auftragToDoc({ kunde: { name: 'x'.repeat(2001) } }), (err) => err instanceof AuftragError && err.status === 400 && /kunde/.test(err.message))
assert.doesNotThrow(() => auftragToDoc({ titel: 'x'.repeat(200) }))
for (const falsch of [{ titel: 5 }, { summeCents: 1.5 }, { jahr: '2026' }, { fruehbeginn: 'ja' }, { gesendetAm: 'gestern' }, { gesendetAm: '' }]) {
assert.throws(() => auftragToDoc(falsch), TypeError, JSON.stringify(falsch))
}
})
test('getAuftrag, updateAuftrag, deleteAuftrag: unbekannte und unmögliche IDs sind null, 404 bzw. folgenlos', async () => {
seed('u1', { titel: 'Alt', summeCents: 1000, erstelltAm: '2020-01-01T00:00:00.000Z', geaendertAm: '2020-01-01T00:00:00.000Z' })
assert.equal((await getAuftrag('u1')).titel, 'Alt')
for (const id of ['gibtsnicht', '', '../x', 'a/b', undefined, null, 42, 'x'.repeat(37)]) assert.equal(await getAuftrag(id), null, String(id))
const auftrag = await updateAuftrag('u1', { titel: 'Neu', positionen: [{ beschreibung: 'A', menge: 2, einzelpreisCents: 500 }], status: 'versendet' })
assert.deepEqual([auftrag.titel, auftrag.summeCents, auftrag.status], ['Neu', 1000, 'versendet'])
assert.equal(auftrag.positionen[0].menge, 2)
assert.equal(auftrag.erstelltAm, '2020-01-01T00:00:00.000Z')
assert.ok(auftrag.geaendertAm > '2020-01-01T00:00:00.000Z')
assert.equal(aw.docs(COL).get('u1').titel, 'Neu')
for (const id of ['gibtsnicht', '../x']) {
await assert.rejects(() => updateAuftrag(id, { titel: 'x' }), (err) => err instanceof AuftragError && err.status === 404, id)
}
await deleteAuftrag('u1')
assert.equal(aw.docs(COL).size, 0)
await deleteAuftrag('u1')
await deleteAuftrag('../x')
})
test('listAuftraege: Filter, neueste zuerst, mehr als 25 Treffer', async () => {
seed('a1', { projektId: 'p1', jahr: 2026, erstelltAm: '2026-10-01T10:00:00.000Z' })
seed('a2', { projektId: 'p2', jahr: 2026, erstelltAm: '2026-10-03T10:00:00.000Z' })
seed('a3', { projektId: 'p1', jahr: 2025, erstelltAm: '2026-10-02T10:00:00.000Z' })
seed('b1', { kundenId: 'c2', projektId: 'p1', jahr: 2026, erstelltAm: '2026-10-04T10:00:00.000Z' })
const ids = async (filter) => (await listAuftraege(filter)).map((a) => a.id)
assert.deepEqual(await ids({ kundenId: 'c1' }), ['a2', 'a3', 'a1'])
assert.deepEqual(await ids({ kundenId: 'c1', projektId: 'p1' }), ['a3', 'a1'])
assert.deepEqual(await ids({ jahr: 2026 }), ['b1', 'a2', 'a1'])
assert.deepEqual(await ids(), ['b1', 'a2', 'a3', 'a1'])
assert.deepEqual(await ids({ kundenId: 'gibtsnicht' }), [])
for (let i = 0; i < 30; i += 1) seed(`m${String(i).padStart(2, '0')}`, { kundenId: 'c9', erstelltAm: `2026-09-${String(1 + (i % 28)).padStart(2, '0')}T10:00:00.000Z` })
assert.equal((await listAuftraege({ kundenId: 'c9' })).length, 30)
})
test('listAuftraege: ein angegebener, aber leerer oder ungültiger Filter ist ein Fehler, nie „alle“', async () => {
seed('a1')
for (const filter of [{ kundenId: '' }, { kundenId: null }, { kundenId: 5 }, { projektId: '' }, { jahr: '2026' }, { jahr: 2026.5 }, { jahr: null }]) {
await assert.rejects(() => listAuftraege(filter), TypeError, JSON.stringify(filter))
}
})
test('findAuftragByTokenHash und findAuftragByNummer: genau eine AB, leere und ungültige Schlüssel nie', async () => {
seed('e1', { nummer: '', tokenHash: '' })
seed('e2')
seed('v1', { status: 'versendet', nummer: 'AB-2026-0001', tokenHash: HASH })
assert.equal((await findAuftragByTokenHash(HASH)).id, 'v1')
assert.equal((await findAuftragByNummer('AB-2026-0001')).id, 'v1')
assert.equal(await findAuftragByTokenHash('b'.repeat(64)), null)
assert.equal(await findAuftragByNummer('AB-2026-0002'), null)
for (const hash of ['', ' ', 'abc', 'A'.repeat(64), `${HASH}0`, undefined, null, 5]) assert.equal(await findAuftragByTokenHash(hash), null, String(hash))
for (const nummer of ['', 'Entwurf', 'AB-2026-1', 'ab-2026-0001', 'AB-2026-0001 ', undefined, null]) assert.equal(await findAuftragByNummer(nummer), null, String(nummer))
})