// hd-commerce — Unit-Tests für Shop-Mathematik + Sanitizer. Lauf: npm test import assert from 'node:assert'; import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; process.env.DB_PATH = join(mkdtempSync(join(tmpdir(), 'hdc-test-')), 't.db'); const store = await import('../src/lib/store-sqlite.js'); const { sanitizeHtml } = await import('../src/lib/sanitize.js'); let pass = 0, fail = 0; const t = (name, fn) => { try { fn(); pass++; console.log(' ✓', name); } catch (e) { fail++; console.error(' ✗', name, '—', e.message); } }; t('taxFromGross 19% aus 119,00 € = 19,00 €', () => assert.strictEqual(store.taxFromGross(11900, 19), 1900)); t('taxFromGross 7% aus 107,00 € = 7,00 €', () => assert.strictEqual(store.taxFromGross(10700, 7), 700)); t('taxFromGross 0% = 0', () => assert.strictEqual(store.taxFromGross(5000, 0), 0)); t('Versand DE gratis ab 49 € (50 € → 0)', () => assert.strictEqual(store.shippingFor('DE', 5000).price_cents, 0)); t('Versand DE unter Schwelle (10 € → 4,90 €)', () => assert.strictEqual(store.shippingFor('DE', 1000).price_cents, 490)); t('Versand CH = 9,90 €', () => assert.strictEqual(store.shippingFor('CH', 5000).price_cents, 990)); t('Gutschein WILLKOMMEN10 = 10% (50 € → 5 €)', () => { const v = store.validateDiscount('WILLKOMMEN10', 5000); assert.ok(v.ok); assert.strictEqual(v.amountCents, 500); }); t('Gutschein NAEHEN5 = 5% (50 € → 2,50 €)', () => { const v = store.validateDiscount('NAEHEN5', 5000); assert.ok(v.ok); assert.strictEqual(v.amountCents, 250); }); t('Gutschein case-insensitiv (willkommen10)', () => assert.ok(store.validateDiscount('willkommen10', 5000).ok)); t('Gutschein unbekannt → ungültig', () => assert.ok(!store.validateDiscount('GIBTSNICHT', 5000).ok)); t('Sanitizer entfernt ')))); t('Sanitizer entfernt onerror=', () => assert.ok(!/onerror/i.test(sanitizeHtml('')))); t('Sanitizer neutralisiert javascript:', () => assert.ok(!/javascript:/i.test(sanitizeHtml('x')))); t('Sanitizer lässt normales Markup', () => assert.ok(//.test(sanitizeHtml('fett')))); // --- v2.3 Feature-Module --- // Suche: Treffer + leerer Query { const id = store.createProduct({ name: 'Blaues Wollknäuel Merino', shortName: 'Wollknäuel', priceCents: 990, category: 'Garne', desc: 'Weiche Merinowolle in Blau', mwst: 19 }); t('Suche findet Produkt nach Name (woll)', () => { const r = store.searchProducts('woll'); assert.ok(r.some(p => p.id === id)); }); t('Suche case-insensitiv (MERINO)', () => assert.ok(store.searchProducts('MERINO').some(p => p.id === id))); t('Suche findet nach Kategorie (garne)', () => assert.ok(store.searchProducts('garne').some(p => p.id === id))); t('Suche leerer Query → keine Treffer', () => assert.strictEqual(store.searchProducts('').length, 0)); t('Suche ohne Treffer → leeres Array', () => assert.strictEqual(store.searchProducts('xyzgibtsnicht123').length, 0)); } // Bewertungen: Durchschnitt nur über freigegebene { const slug = 'test-review-produkt'; const r1 = store.addReview({ product_slug: slug, name: 'A', rating: 5, text: 'top' }); const r2 = store.addReview({ product_slug: slug, name: 'B', rating: 3, text: 'ok' }); store.addReview({ product_slug: slug, name: 'C', rating: 1, text: 'nicht freigegeben' }); store.setReviewApproved(r1.id, 1); store.setReviewApproved(r2.id, 1); t('Review-Average = 4.0 (5+3)/2, nur freigegebene', () => { const s = store.reviewSummary(slug); assert.strictEqual(s.count, 2); assert.strictEqual(s.average, 4); }); t('Review-Rating ausserhalb 1–5 → Fehler', () => assert.ok(!store.addReview({ product_slug: slug, name: 'D', rating: 9 }).ok)); t('addReview ohne Rating → Fehler', () => assert.ok(!store.addReview({ product_slug: slug, rating: 0 }).ok)); } // Kundenkonten: Registrierung + Passwort-Prüfung (getrennte Auth) { const reg = store.registerCustomer({ name: 'Test Kunde', email: 'kunde@example.com', password: 'geheim123' }); t('Kunde registrieren', () => assert.ok(reg.ok && reg.id)); t('Kunde Login korrekt', () => assert.ok(store.verifyCustomer('kunde@example.com', 'geheim123'))); t('Kunde Login falsches Passwort → null', () => assert.strictEqual(store.verifyCustomer('kunde@example.com', 'falsch'), null)); t('Kurzes Passwort → Fehler', () => assert.ok(!store.registerCustomer({ email: 'x@y.de', password: '123' }).ok)); } // Abandoned-Cart: erfassen, fällig, recovered { const cap = store.captureAbandonedCart({ email: 'cart@example.com', items: [{ name: 'X', priceCents: 1000, qty: 2 }], total_cents: 2000 }); t('Abandoned-Cart erfassen', () => assert.ok(cap.ok && cap.id)); t('Abandoned-Cart Stats zählt offen', () => assert.ok(store.abandonedCartStats().open >= 1)); t('recovered markieren per E-Mail', () => { const n = store.markCartRecoveredByEmail('cart@example.com'); assert.ok(n >= 1); }); t('recovered Karte nicht mehr fällig', () => { const due = store.dueAbandonedCarts(0); assert.ok(!due.some(c => c.email === 'cart@example.com')); }); } console.log(`\n${pass} passed, ${fail} failed`); process.exit(fail ? 1 : 0);