74b6e5f078
Pinnwand (Notes): - server/routes/notes.js: GET (sortiert: angeheftet zuerst), POST, PUT, PATCH /pin, DELETE - public/pages/notes.js: Masonry-Grid, Markdown-Light-Renderer (fett/kursiv/Liste), Farb-Auswahl (8 Farben), helle/dunkle Textfarbe je nach Hintergrund, Pin-Toggle - public/styles/notes.css: Masonry-Layout, Sticky-Note-Karten, Hover-Aktionen Kontakte: - server/routes/contacts.js: GET (Kategorie- + Volltextfilter), POST, PUT, DELETE, GET /meta - public/pages/contacts.js: Kategorie-Filter-Chips, Echtzeit-Suche, Gruppenansicht, tel:/mailto:/maps-Links, CRUD-Modal - public/styles/contacts.css: Toolbar mit Suche, Filter-Chips, Kontaktliste, Aktions-Buttons Budget-Tracker: - server/routes/budget.js: GET (Monatfilter), GET /summary (Einnahmen/Ausgaben/Saldo + Aufschlüsselung), GET /export (CSV mit BOM), POST, PUT, DELETE, GET /meta - public/pages/budget.js: Monatsnavigation, 3 Zusammenfassungs-Karten, Kategorie-Balken (reines CSS, kein Canvas), Transaktionsliste, Einnahme/Ausgabe-Toggle, CSV-Download - public/styles/budget.css: Summary-Cards, Balkendiagramm, Transaktionsliste, Modal Tests: 34 neue Tests (10 Notes + 9 Contacts + 15 Budget), gesamt 146/146 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
311 lines
11 KiB
JavaScript
311 lines
11 KiB
JavaScript
/**
|
|
* 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);
|