/** * Modul: Notes / Contacts / Budget — Tests * Zweck: Validiert CRUD, Constraints, Filterabfragen, Aggregation für alle drei Module * Ausführen: node --experimental-sqlite test-notes-contacts-budget.js */ 'use strict'; const { DatabaseSync } = require('node:sqlite'); const { MIGRATIONS_SQL } = require('./server/db-schema-test'); let passed = 0; let failed = 0; function test(name, fn) { try { fn(); console.log(` ✓ ${name}`); passed++; } catch (err) { console.error(` ✗ ${name}: ${err.message}`); failed++; } } function assert(cond, msg) { if (!cond) throw new Error(msg || 'Assertion fehlgeschlagen'); } const db = new DatabaseSync(':memory:'); db.exec('PRAGMA foreign_keys = ON;'); db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, description TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) );`); db.exec(MIGRATIONS_SQL[1]); const u1 = db.prepare(`INSERT INTO users (username, display_name, password_hash, role) VALUES ('admin', 'Admin', 'x', 'admin')`).run(); const uid = u1.lastInsertRowid; // ============================================================ // NOTES // ============================================================ console.log('\n[Notes-Test] Notizen, Pin, Sortierung\n'); let noteId1, noteId2, noteId3; test('Notiz erstellen', () => { const r = db.prepare(`INSERT INTO notes (content, color, pinned, created_by) VALUES ('Einkaufen nicht vergessen', '#FFEB3B', 0, ?)`).run(uid); noteId1 = r.lastInsertRowid; assert(noteId1 > 0); }); test('Zweite Notiz mit Titel erstellen', () => { const r = db.prepare(`INSERT INTO notes (title, content, color, pinned, created_by) VALUES ('Wichtig', 'Arzttermin morgen', '#90CAF9', 1, ?)`).run(uid); noteId2 = r.lastInsertRowid; assert(noteId2 > 0); }); test('Dritte Notiz erstellen', () => { const r = db.prepare(`INSERT INTO notes (content, color, created_by) VALUES ('Notiz drei', '#A5D6A7', ?)`).run(uid); noteId3 = r.lastInsertRowid; assert(noteId3 > 0); }); test('Sortierung: Angepinnte zuerst', () => { const notes = db.prepare(` SELECT * FROM notes ORDER BY pinned DESC, updated_at DESC `).all(); assert(notes.length === 3); assert(notes[0].pinned === 1, `Erste Notiz muss angeheftet sein, ist: ${notes[0].pinned}`); }); test('Notiz aktualisieren (Inhalt + Farbe)', () => { db.prepare(`UPDATE notes SET content = 'Neuer Inhalt', color = '#FF9500' WHERE id = ?`).run(noteId1); const n = db.prepare('SELECT content, color FROM notes WHERE id = ?').get(noteId1); assert(n.content === 'Neuer Inhalt'); assert(n.color === '#FF9500'); }); test('Pin-Toggle: pinned 0 → 1', () => { const before = db.prepare('SELECT pinned FROM notes WHERE id = ?').get(noteId1); const newPin = before.pinned ? 0 : 1; db.prepare('UPDATE notes SET pinned = ? WHERE id = ?').run(newPin, noteId1); const after = db.prepare('SELECT pinned FROM notes WHERE id = ?').get(noteId1); assert(after.pinned === 1, 'Jetzt angeheftet'); }); test('Notiz löschen', () => { db.prepare('DELETE FROM notes WHERE id = ?').run(noteId3); const n = db.prepare('SELECT * FROM notes WHERE id = ?').get(noteId3); assert(!n, 'Notiz gelöscht'); }); test('Verbleibende Notizen nach Löschung: 2', () => { const notes = db.prepare('SELECT * FROM notes').all(); assert(notes.length === 2, `Erwartet 2, erhalten ${notes.length}`); }); test('JOIN: Ersteller-Name verfügbar', () => { const n = db.prepare(` SELECT n.*, u.display_name AS creator_name FROM notes n LEFT JOIN users u ON u.id = n.created_by WHERE n.id = ? `).get(noteId2); assert(n.creator_name === 'Admin'); }); test('Index idx_notes_pinned genutzt', () => { const plan = db.prepare(`EXPLAIN QUERY PLAN SELECT * FROM notes WHERE pinned = 1`).all(); const usesIndex = plan.some((r) => (r.detail || '').includes('INDEX')); assert(usesIndex, JSON.stringify(plan)); }); // ============================================================ // CONTACTS // ============================================================ console.log('\n[Contacts-Test] CRUD, Kategorien, Suche\n'); let cId1, cId2, cId3; test('Kontakt erstellen (Arzt)', () => { const r = db.prepare(`INSERT INTO contacts (name, category, phone, email) VALUES ('Dr. Müller', 'Arzt', '+49 30 12345', 'mueller@praxis.de')`).run(); cId1 = r.lastInsertRowid; assert(cId1 > 0); }); test('Kontakt erstellen (Notfall)', () => { const r = db.prepare(`INSERT INTO contacts (name, category, phone) VALUES ('Feuerwehr', 'Notfall', '112')`).run(); cId2 = r.lastInsertRowid; assert(cId2 > 0); }); test('Kontakt erstellen (Handwerker)', () => { const r = db.prepare(`INSERT INTO contacts (name, category, phone, address) VALUES ('Klempner Fritz', 'Handwerker', '+49 170 99999', 'Musterstr. 1, Berlin')`).run(); cId3 = r.lastInsertRowid; assert(cId3 > 0); }); test('Alle Kontakte abrufen', () => { const contacts = db.prepare('SELECT * FROM contacts ORDER BY category ASC, name ASC').all(); assert(contacts.length === 3); }); test('Nach Kategorie filtern (Arzt)', () => { const contacts = db.prepare(`SELECT * FROM contacts WHERE category = 'Arzt'`).all(); assert(contacts.length === 1); assert(contacts[0].name === 'Dr. Müller'); }); test('Volltextsuche nach Name', () => { const q = '%Feuerwehr%'; const contacts = db.prepare(` SELECT * FROM contacts WHERE name LIKE ? OR phone LIKE ? OR email LIKE ? `).all(q, q, q); assert(contacts.length === 1); assert(contacts[0].category === 'Notfall'); }); test('Suche nach Telefonnummer', () => { const q = '%112%'; const contacts = db.prepare(`SELECT * FROM contacts WHERE phone LIKE ?`).all(q); assert(contacts.length === 1); }); test('Kontakt aktualisieren', () => { db.prepare(`UPDATE contacts SET phone = '+49 30 99999' WHERE id = ?`).run(cId1); const c = db.prepare('SELECT phone FROM contacts WHERE id = ?').get(cId1); assert(c.phone === '+49 30 99999'); }); test('Kontakt löschen', () => { db.prepare('DELETE FROM contacts WHERE id = ?').run(cId3); const c = db.prepare('SELECT * FROM contacts WHERE id = ?').get(cId3); assert(!c, 'Kontakt gelöscht'); }); // ============================================================ // BUDGET // ============================================================ console.log('\n[Budget-Test] Einnahmen, Ausgaben, Saldo, Aggregation, CSV-Vorbereitung\n'); let bId1, bId2, bId3, bId4; test('Ausgabe eintragen (Lebensmittel)', () => { const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by) VALUES ('REWE', -85.40, 'Lebensmittel', '2026-03-10', ?)`).run(uid); bId1 = r.lastInsertRowid; assert(bId1 > 0); }); test('Einnahme eintragen (Gehalt)', () => { const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by) VALUES ('Gehalt März', 2800.00, 'Sonstiges', '2026-03-01', ?)`).run(uid); bId2 = r.lastInsertRowid; assert(bId2 > 0); }); test('Ausgabe (Miete)', () => { const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, is_recurring, created_by) VALUES ('Miete', -950.00, 'Miete', '2026-03-01', 1, ?)`).run(uid); bId3 = r.lastInsertRowid; assert(bId3 > 0); }); test('Ausgabe im anderen Monat (April)', () => { const r = db.prepare(`INSERT INTO budget_entries (title, amount, category, date, created_by) VALUES ('Strom April', -55.00, 'Sonstiges', '2026-04-15', ?)`).run(uid); bId4 = r.lastInsertRowid; assert(bId4 > 0); }); test('Monatsfilter März: nur März-Einträge', () => { const entries = db.prepare(` SELECT * FROM budget_entries WHERE date BETWEEN '2026-03-01' AND '2026-03-31' ORDER BY date ASC `).all(); assert(entries.length === 3, `Erwartet 3, erhalten ${entries.length}`); }); test('Monatsfilter April: nur April-Eintrag', () => { const entries = db.prepare(` SELECT * FROM budget_entries WHERE date BETWEEN '2026-04-01' AND '2026-04-30' `).all(); assert(entries.length === 1); assert(entries[0].title === 'Strom April'); }); test('Einnahmen-Summe März', () => { const row = db.prepare(` SELECT SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) AS income FROM budget_entries WHERE date BETWEEN '2026-03-01' AND '2026-03-31' `).get(); assert(Math.abs(row.income - 2800.00) < 0.01, `Einnahmen: ${row.income}`); }); test('Ausgaben-Summe März', () => { const row = db.prepare(` SELECT SUM(CASE WHEN amount < 0 THEN amount ELSE 0 END) AS expenses FROM budget_entries WHERE date BETWEEN '2026-03-01' AND '2026-03-31' `).get(); const expected = -(85.40 + 950.00); assert(Math.abs(row.expenses - expected) < 0.01, `Ausgaben: ${row.expenses}`); }); test('Saldo März positiv', () => { const row = db.prepare(` SELECT SUM(amount) AS balance FROM budget_entries WHERE date BETWEEN '2026-03-01' AND '2026-03-31' `).get(); assert(row.balance > 0, `Saldo: ${row.balance}`); }); test('Aggregation nach Kategorie', () => { const cats = db.prepare(` SELECT category, SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) AS income, SUM(CASE WHEN amount < 0 THEN amount ELSE 0 END) AS expenses, SUM(amount) AS total FROM budget_entries WHERE date BETWEEN '2026-03-01' AND '2026-03-31' GROUP BY category ORDER BY ABS(SUM(amount)) DESC `).all(); assert(cats.length >= 2, `Mindestens 2 Kategorien, erhalten ${cats.length}`); // Miete sollte die größte Ausgabe sein const miete = cats.find((c) => c.category === 'Miete'); assert(miete, 'Miete in Kategorien vorhanden'); assert(Math.abs(miete.expenses + 950.00) < 0.01, `Miete-Ausgaben: ${miete.expenses}`); }); test('Wiederkehrend-Flag korrekt', () => { const r = db.prepare('SELECT is_recurring FROM budget_entries WHERE id = ?').get(bId3); assert(r.is_recurring === 1, 'Miete ist wiederkehrend'); }); test('Eintrag aktualisieren', () => { db.prepare(`UPDATE budget_entries SET amount = -90.50 WHERE id = ?`).run(bId1); const e = db.prepare('SELECT amount FROM budget_entries WHERE id = ?').get(bId1); assert(Math.abs(e.amount + 90.50) < 0.01); }); test('Eintrag löschen', () => { db.prepare('DELETE FROM budget_entries WHERE id = ?').run(bId4); const e = db.prepare('SELECT * FROM budget_entries WHERE id = ?').get(bId4); assert(!e, 'Eintrag gelöscht'); }); test('CSV-Vorbereitung: alle März-Einträge mit JOIN', () => { const entries = db.prepare(` SELECT b.*, u.display_name AS creator_name FROM budget_entries b LEFT JOIN users u ON u.id = b.created_by WHERE b.date BETWEEN '2026-03-01' AND '2026-03-31' ORDER BY b.date ASC `).all(); assert(entries.length === 3); assert(entries[0].creator_name === 'Admin'); }); test('Index idx_budget_date genutzt', () => { const plan = db.prepare(` EXPLAIN QUERY PLAN SELECT * FROM budget_entries WHERE date BETWEEN '2026-03-01' AND '2026-03-31' `).all(); const usesIndex = plan.some((r) => (r.detail || '').includes('INDEX')); assert(usesIndex, JSON.stringify(plan)); }); // -------------------------------------------------------- // Ergebnis // -------------------------------------------------------- console.log(`\n[Notes/Contacts/Budget-Test] Ergebnis: ${passed} bestanden, ${failed} fehlgeschlagen\n`); if (failed > 0) process.exit(1);