v2.3: Feature-Module live — Suche, Merkliste, Kundenkonten+Adressbuch, Bewertungen, Abandoned-Cart
- feature_search: Storefront-Header-Suche + /suche (SSR, SQLite LIKE, case-insensitiv; Name/Kurz/Desc/Material/Kategorie), Treffer als Karten, Leer-Zustand - feature_wishlist: Herz-Button auf Karten/PDP (localStorage, public/wishlist.js) + /merkliste - feature_accounts: getrennte Kunden-Session (Cookie hdc_customer, scrypt), /konto/registrieren|anmelden|abmelden, /konto (Bestellhistorie+Adressbuch), Tabelle customer_addresses, Checkout-Vorbefuellung + orders.customer_id-Zuordnung; Gast-Checkout bleibt - feature_reviews: Tabelle reviews (1-5, Moderation), /api/review (approved=0), PDP-Anzeige Durchschnitt+Reviews + aggregateRating-JSON-LD, Admin /bewertungen (Freigeben/Verbergen/Loeschen) + Nav-Zaehler - feature_abandoned_cart: Tabelle abandoned_carts, /api/cart-capture beim Checkout-Start, /api/cron/abandoned (CRON_TOKEN) sendet Erinnerungsmail (Mailer/Log) + reminded=1, recovered=1 bei Bestellung; Status in Einstellungen - Gating: Flag aus => Storefront-Elemente weg, Routen 302/404, Admin-Nav-Punkt entfaellt; KEIN 'in Vorbereitung' mehr - API/MCP: reviews CRUD + abandoned_carts (read) in admin-api + ai-admin.txt + MCP-Tools; Manifest v2.3 - README + .env.example (CRON_TOKEN, ABANDONED_AFTER_MINUTES); 16 neue Unit-Tests (Suche/Review-Avg/Kunden/Abandoned)
This commit is contained in:
@@ -29,5 +29,47 @@ t('Sanitizer entfernt onerror=', () => assert.ok(!/onerror/i.test(sanitizeHtml('
|
||||
t('Sanitizer neutralisiert javascript:', () => assert.ok(!/javascript:/i.test(sanitizeHtml('<a href="javascript:alert(1)">x</a>'))));
|
||||
t('Sanitizer lässt normales Markup', () => assert.ok(/<strong>/.test(sanitizeHtml('<strong>fett</strong>'))));
|
||||
|
||||
// --- 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);
|
||||
|
||||
Reference in New Issue
Block a user