Files
oikos/test-notes-contacts-budget.js
T
Ulas b139eea623 refactor(esm): migrate server and tests from CommonJS to ESM
Convert all server/, test, and setup files from require()/module.exports
to import/export syntax. Activate ESM globally via "type": "module" in
package.json and load dotenv via --import dotenv/config in npm scripts.
2026-04-03 23:11:20 +02:00

309 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
*/
import { DatabaseSync } from 'node:sqlite';
import { MIGRATIONS_SQL } from './server/db-schema-test.js';
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);