Files
Webklar-Kundenbereich/test/shopEinstellungen.test.js
ClaudeandClaude Opus 5.5 1f56b86791 Shop Review B I-4: Steuerart und Lieferzeit ohne Vorbelegung
kleinunternehmer ist ab jetzt null (nicht gewaehlt), lieferzeit leer. istVollstaendig verlangt eine
bewusste Wahl (true oder false), der Shop kann sonst nicht geoeffnet werden. Im Reiter
Einstellungen gibt es zwei Pflicht-Radiobuttons ohne Vorauswahl; Preise, Artikel und Muster
rechnen bei null wie bei Regelbesteuerung. Tests und Fixtures angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:50:54 +00:00

277 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
// Einstellungen des Shops: reines Normalisieren (Fehler je Feld) und Speichern mit Versionspruefung gegen den Appwrite-Nachbau.
const aw = await startAppwriteMock({ schema: { shopConfig: schemaKeys('shopConfig') }, datetimes: SHOP_DATETIMES })
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { config } = await import('../server/config.js')
const { createDocument, getDocument } = await import('../server/services/appwriteAdmin.js')
const E = await import('../server/services/shop/einstellungen.js')
const { STANDARD_EINSTELLUNGEN, normalisiereEinstellungen, ladeEinstellungen, speichereEinstellungen, nachSpeichern, istVollstaendig } = E
const komplett = (over = {}) => ({
shopname: 'Seifenmanufaktur Mustermann',
anbieter: {
name: 'Erika Mustermann',
strasse: 'Musterstraße 1',
plz: '80331',
ort: 'München',
land: 'DE',
email: 'Erika@Example.com',
telefon: '089 123456',
ustId: '',
},
kleinunternehmer: true,
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
abholung: { aktiv: false, adresse: '', hinweis: '' },
lieferzeit: '2–4 Werktage',
zahlungsarten: ['card'],
akzent: '#111111',
mindestbestellCent: null,
ruecksendekosten: 'kaeufer',
...over,
})
const fehler = (eingabe) => normalisiereEinstellungen(eingabe).fehler
const ok = (eingabe) => {
const r = normalisiereEinstellungen(eingabe)
assert.equal(r.fehler, undefined, JSON.stringify(r.fehler))
return r.einstellungen
}
test('Standardwerte sind eingefroren, so wie im Plan und selbst gueltig', () => {
assert.ok(Object.isFrozen(STANDARD_EINSTELLUNGEN))
assert.ok(Object.isFrozen(STANDARD_EINSTELLUNGEN.versand))
assert.ok(Object.isFrozen(STANDARD_EINSTELLUNGEN.anbieter))
assert.deepEqual(STANDARD_EINSTELLUNGEN, {
shopname: '',
anbieter: { name: '', strasse: '', plz: '', ort: '', land: 'DE', email: '', telefon: '', ustId: '' },
kleinunternehmer: null,
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
abholung: { aktiv: false, adresse: '', hinweis: '' },
lieferzeit: '',
zahlungsarten: ['card'],
akzent: '#111111',
mindestbestellCent: null,
ruecksendekosten: 'kaeufer',
})
assert.deepEqual(ok(STANDARD_EINSTELLUNGEN), STANDARD_EINSTELLUNGEN)
assert.equal(istVollstaendig(STANDARD_EINSTELLUNGEN), false)
})
test('komplette Eingabe: normalisiert, E-Mail klein, ohne gemeinsame Referenzen', () => {
const eingabe = komplett()
const e = ok(eingabe)
assert.equal(e.anbieter.email, 'erika@example.com')
assert.notEqual(e.versand, eingabe.versand)
assert.notEqual(e.versand.laender, eingabe.versand.laender)
assert.notEqual(e.versand.kostenCent, eingabe.versand.kostenCent)
assert.notEqual(e.anbieter, eingabe.anbieter)
assert.notEqual(e.zahlungsarten, eingabe.zahlungsarten)
e.versand.laender.push('X')
assert.deepEqual(eingabe.versand.laender, ['DE'])
assert.deepEqual(STANDARD_EINSTELLUNGEN.versand.laender, ['DE'])
assert.equal(istVollstaendig(komplett()), true)
})
test('leere und fehlende Eingabe: Standardwerte; keine Objekte: Fehler', () => {
assert.deepEqual(ok({}), STANDARD_EINSTELLUNGEN)
for (const x of [null, undefined, 'x', 5, [], true]) assert.ok(fehler(x)?.einstellungen, String(x))
})
test('unbekannte Schluessel werden ignoriert, auch __proto__', () => {
const e = ok(JSON.parse('{"foo":1,"__proto__":{"x":1},"anbieter":{"zusatz":"a"},"versand":{"y":2}}'))
assert.deepEqual(Object.keys(e).sort(), Object.keys(STANDARD_EINSTELLUNGEN).sort())
assert.equal(e.anbieter.zusatz, undefined)
assert.equal(e.versand.y, undefined)
assert.equal({}.x, undefined)
})
test('Texte: getrimmt, Steuerzeichen entfernt, Laengengrenzen je Feld', () => {
const e = ok(komplett({ shopname: ' Laden\u0000​ Eins \n', abholung: { aktiv: true, adresse: 'Weg 1\r\n80331 München\u0007', hinweis: 'Klingeln' } }))
assert.equal(e.shopname, 'Laden Eins')
assert.equal(e.abholung.adresse, 'Weg 1\n80331 München')
const grenzen = [
['shopname', komplett({ shopname: 'a'.repeat(81) })],
['anbieter.name', komplett({ anbieter: { ...komplett().anbieter, name: 'a'.repeat(121) } })],
['anbieter.strasse', komplett({ anbieter: { ...komplett().anbieter, strasse: 'a'.repeat(121) } })],
['anbieter.ort', komplett({ anbieter: { ...komplett().anbieter, ort: 'a'.repeat(81) } })],
['anbieter.telefon', komplett({ anbieter: { ...komplett().anbieter, telefon: '1'.repeat(41) } })],
['lieferzeit', komplett({ lieferzeit: 'a'.repeat(81) })],
['abholung.hinweis', komplett({ abholung: { aktiv: true, adresse: 'Weg 1', hinweis: 'a'.repeat(301) } })],
['abholung.adresse', komplett({ abholung: { aktiv: true, adresse: 'a'.repeat(301), hinweis: '' } })],
]
for (const [pfad, eingabe] of grenzen) assert.ok(fehler(eingabe)?.[pfad], pfad)
ok(komplett({ shopname: 'a'.repeat(80), lieferzeit: 'abc' }))
assert.ok(fehler(komplett({ lieferzeit: 'ab' }))?.lieferzeit)
assert.ok(fehler(komplett({ shopname: 5 }))?.shopname)
})
test('Anbieter: E-Mail, PLZ je Land, Land, USt-IdNr.', () => {
const an = (over) => komplett({ anbieter: { ...komplett().anbieter, ...over } })
for (const mail of ['x', 'a@b', 'a b@c.de', 'a@b.de, c@d.de', `${'a'.repeat(250)}@b.de`, 'a@@b.de', '<a@b.de>']) {
assert.ok(fehler(an({ email: mail }))?.['anbieter.email'], mail)
}
assert.equal(ok(an({ email: ' A@B.de ' })).anbieter.email, 'a@b.de')
assert.ok(fehler(an({ plz: '8033' }))?.['anbieter.plz'])
assert.ok(fehler(an({ plz: '80a31' }))?.['anbieter.plz'])
assert.equal(ok(an({ land: 'AT', plz: '1010' })).anbieter.land, 'AT')
assert.ok(fehler(an({ land: 'AT', plz: '80331' }))?.['anbieter.plz'])
assert.ok(fehler(an({ land: 'FR' }))?.['anbieter.land'])
assert.equal(ok(an({ ustId: ' de123456789 ' })).anbieter.ustId, 'DE123456789')
assert.equal(ok(an({ ustId: 'ATU12345678' })).anbieter.ustId, 'ATU12345678')
for (const u of ['DE12345678', 'FR123456789', 'ATU1234567', 'DE1234567890']) assert.ok(fehler(an({ ustId: u }))?.['anbieter.ustId'], u)
})
test('Versand: Laender, Kosten je Land, Freigrenze', () => {
const vs = (over) => komplett({ versand: { ...komplett().versand, ...over } })
const at = ok(vs({ laender: ['AT', 'DE'], kostenCent: { DE: 490, AT: 990, FR: 5 } }))
assert.deepEqual(at.versand.laender, ['DE', 'AT'])
assert.deepEqual(at.versand.kostenCent, { DE: 490, AT: 990 })
assert.ok(fehler(vs({ laender: ['DE', 'AT'], kostenCent: { DE: 490 } }))?.['versand.kostenCent.AT'])
assert.ok(fehler(vs({ kostenCent: { DE: 4.9 } }))?.['versand.kostenCent.DE'])
assert.ok(fehler(vs({ kostenCent: { DE: '490' } }))?.['versand.kostenCent.DE'])
assert.ok(fehler(vs({ kostenCent: { DE: -1 } }))?.['versand.kostenCent.DE'])
assert.ok(fehler(vs({ kostenCent: { DE: 100001 } }))?.['versand.kostenCent.DE'])
assert.equal(ok(vs({ kostenCent: { DE: 0 } })).versand.kostenCent.DE, 0)
assert.equal(ok(vs({ kostenCent: { DE: 100000 } })).versand.kostenCent.DE, 100000)
for (const l of [['AT'], ['DE', 'FR'], [], 'DE', ['DE', 'DE', 'AT', 'CH']]) assert.ok(fehler(vs({ laender: l }))?.['versand.laender'], JSON.stringify(l))
assert.equal(ok(vs({ freiAbCent: 5000 })).versand.freiAbCent, 5000)
assert.equal(ok(vs({ freiAbCent: null })).versand.freiAbCent, null)
for (const f of [0, -5, 12.5, '5000', Infinity]) assert.ok(fehler(vs({ freiAbCent: f }))?.['versand.freiAbCent'], String(f))
assert.ok(fehler(vs({ aktiv: 'ja' }))?.['versand.aktiv'])
})
test('mindestens Versand oder Abholung; Abholadresse Pflicht', () => {
const nichts = fehler(komplett({ versand: { ...komplett().versand, aktiv: false } }))
assert.ok(nichts?.['versand.aktiv'])
const abholungNur = ok(komplett({ versand: { ...komplett().versand, aktiv: false }, abholung: { aktiv: true, adresse: 'Lager 3', hinweis: 'Mo–Fr' } }))
assert.equal(abholungNur.abholung.adresse, 'Lager 3')
assert.ok(fehler(komplett({ abholung: { aktiv: true, adresse: ' ', hinweis: '' } }))?.['abholung.adresse'])
// inaktive Abholung braucht keine Adresse
ok(komplett({ abholung: { aktiv: false, adresse: '', hinweis: '' } }))
})
test('Zahlungsarten: card immer dabei, nur bekannte, feste Reihenfolge', () => {
assert.deepEqual(ok(komplett({ zahlungsarten: ['sepa_debit', 'klarna'] })).zahlungsarten, ['card', 'klarna', 'sepa_debit'])
assert.deepEqual(ok(komplett({ zahlungsarten: ['card', 'card'] })).zahlungsarten, ['card'])
assert.deepEqual(ok(komplett({ zahlungsarten: [] })).zahlungsarten, ['card'])
assert.ok(fehler(komplett({ zahlungsarten: ['card', 'paypal'] }))?.zahlungsarten)
assert.ok(fehler(komplett({ zahlungsarten: 'card' }))?.zahlungsarten)
})
test('Akzent, Mindestbestellwert, Ruecksendekosten, Kleinunternehmer', () => {
assert.equal(ok(komplett({ akzent: '#AABBCC' })).akzent, '#aabbcc')
for (const a of ['AABBCC', '#abc', '#12345g', 'red', '#1234567']) assert.ok(fehler(komplett({ akzent: a }))?.akzent, a)
assert.equal(ok(komplett({ mindestbestellCent: 1000 })).mindestbestellCent, 1000)
for (const m of [0, -1, 1.5, '10']) assert.ok(fehler(komplett({ mindestbestellCent: m }))?.mindestbestellCent, String(m))
assert.equal(ok(komplett({ ruecksendekosten: 'haendler' })).ruecksendekosten, 'haendler')
assert.ok(fehler(komplett({ ruecksendekosten: 'egal' }))?.ruecksendekosten)
assert.equal(ok(komplett({ kleinunternehmer: false })).kleinunternehmer, false)
assert.ok(fehler(komplett({ kleinunternehmer: 'nein' }))?.kleinunternehmer)
// I-4: keine Vorbelegung - null (oder fehlend) bleibt null, zaehlt aber nicht als vollstaendig
assert.equal(ok(komplett({ kleinunternehmer: null })).kleinunternehmer, null)
assert.equal(ok({ ...komplett(), kleinunternehmer: undefined }).kleinunternehmer, null)
assert.equal(istVollstaendig(komplett({ kleinunternehmer: null })), false)
assert.equal(istVollstaendig(komplett({ kleinunternehmer: true })), true)
assert.equal(istVollstaendig(komplett({ kleinunternehmer: false })), true)
assert.equal(istVollstaendig(komplett({ lieferzeit: undefined })), false, 'Lieferzeit ohne Vorbelegung')
})
test('mehrere Fehler zugleich, alle Texte in Du-Form ohne Sie', () => {
const f = fehler(komplett({ akzent: 'x', lieferzeit: 'a', zahlungsarten: ['paypal'], anbieter: { ...komplett().anbieter, email: 'x', plz: '1' } }))
assert.deepEqual(Object.keys(f).sort(), ['akzent', 'anbieter.email', 'anbieter.plz', 'lieferzeit', 'zahlungsarten'])
for (const t of Object.values(f)) {
assert.equal(typeof t, 'string')
assert.doesNotMatch(t, /\bSie\b|\bIhre?[mnrs]?\b/)
}
})
test('istVollstaendig: Pflichtangaben und gueltige Normalisierung', () => {
assert.equal(istVollstaendig(komplett()), true)
for (const pfad of ['shopname', 'lieferzeit']) assert.equal(istVollstaendig(komplett({ [pfad]: '' })), false, pfad)
for (const feld of ['name', 'strasse', 'plz', 'ort', 'email']) {
assert.equal(istVollstaendig(komplett({ anbieter: { ...komplett().anbieter, [feld]: '' } })), false, feld)
}
assert.equal(istVollstaendig(komplett({ anbieter: { ...komplett().anbieter, telefon: '', ustId: '' } })), true)
assert.equal(istVollstaendig(komplett({ akzent: 'kaputt' })), false)
assert.equal(istVollstaendig(null), false)
})
test('ladeEinstellungen: kaputt oder fehlend ergibt Standard, Teilwerte werden aufgefuellt', () => {
for (const doc of [null, undefined, {}, { einstellungen: '' }, { einstellungen: '{kaputt' }, { einstellungen: '[1]' }, { einstellungen: 'null' }, { einstellungen: '"x"' }]) {
assert.deepEqual(ladeEinstellungen(doc), STANDARD_EINSTELLUNGEN)
}
const teil = ladeEinstellungen({ einstellungen: JSON.stringify({ shopname: 'Laden', anbieter: { ort: 'Ulm' }, versand: { freiAbCent: 5000 } }) })
assert.equal(teil.shopname, 'Laden')
assert.equal(teil.anbieter.ort, 'Ulm')
assert.equal(teil.anbieter.land, 'DE')
assert.equal(teil.versand.freiAbCent, 5000)
assert.deepEqual(teil.versand.kostenCent, { DE: 490 })
assert.equal(teil.lieferzeit, '')
assert.equal(teil.kleinunternehmer, null, 'Steuerart ist nicht vorbelegt')
// gespeicherte Werte werden nicht in die Standardwerte geschrieben
teil.versand.laender.push('AT')
assert.deepEqual(STANDARD_EINSTELLUNGEN.versand.laender, ['DE'])
assert.notEqual(ladeEinstellungen(null), STANDARD_EINSTELLUNGEN)
})
test('speichereEinstellungen: Version steigt, Versionspruefung, Hooks', async () => {
const id = 'f_p_shop0001_shop'
await createDocument(config.collections.shopConfig, { customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop' }, id)
const gerufen = []
const abmelden = nachSpeichern((shopId) => gerufen.push(shopId))
nachSpeichern(() => {
throw new Error('Hook kaputt')
})
nachSpeichern(async () => {
throw new Error('Hook async kaputt')
})
const r1 = await speichereEinstellungen(id, komplett(), { erwarteteVersion: 0 })
assert.equal(r1.version, 1)
assert.equal(r1.einstellungen.anbieter.email, 'erika@example.com')
let doc = await getDocument(config.collections.shopConfig, id)
assert.equal(doc.version, 1)
assert.equal(JSON.parse(doc.einstellungen).shopname, 'Seifenmanufaktur Mustermann')
assert.deepEqual(ladeEinstellungen(doc), r1.einstellungen)
assert.deepEqual(gerufen, [id])
const r2 = await speichereEinstellungen(id, komplett({ shopname: 'Neu' }))
assert.equal(r2.version, 2)
await assert.rejects(speichereEinstellungen(id, komplett({ shopname: 'Alt' }), { erwarteteVersion: 1 }), (err) => {
assert.equal(err.status, 409)
assert.equal(err.message, 'Die Einstellungen wurden inzwischen geändert. Bitte lade die Seite neu.')
return true
})
doc = await getDocument(config.collections.shopConfig, id)
assert.equal(doc.version, 2)
assert.equal(JSON.parse(doc.einstellungen).shopname, 'Neu')
assert.equal(gerufen.length, 2)
// ungueltige Einstellungen werden nicht gespeichert: 400 mit Feldfehlern
await assert.rejects(speichereEinstellungen(id, komplett({ akzent: 'x' })), (err) => err.status === 400 && Boolean(err.fehler.akzent))
assert.equal((await getDocument(config.collections.shopConfig, id)).version, 2)
assert.equal(gerufen.length, 2)
// gleichzeitige Speicherungen mit derselben erwarteten Version: genau eine gewinnt
const rennen = await Promise.allSettled([
speichereEinstellungen(id, komplett({ shopname: 'A' }), { erwarteteVersion: 2 }),
speichereEinstellungen(id, komplett({ shopname: 'B' }), { erwarteteVersion: 2 }),
])
assert.equal(rennen.filter((x) => x.status === 'fulfilled').length, 1)
assert.equal((await getDocument(config.collections.shopConfig, id)).version, 3)
abmelden()
await speichereEinstellungen(id, komplett({ shopname: 'C' }))
assert.equal(gerufen.length, 3) // A oder B war der dritte Aufruf, danach keiner mehr
// unbekannter Shop
await assert.rejects(speichereEinstellungen('gibt-es-nicht', komplett()), (err) => err.status === 404)
})