/** * Modul: Aufgaben-Test * Zweck: Validiert alle Tasks-API-Abfragen und Constraints * Ausführen: node --experimental-sqlite test-tasks.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]); // Testdaten const u1 = db.prepare(`INSERT INTO users (username, display_name, password_hash, avatar_color, role) VALUES ('admin', 'Anna', 'x', '#007AFF', 'admin')`).run(); const u2 = db.prepare(`INSERT INTO users (username, display_name, password_hash, avatar_color) VALUES ('max', 'Max', 'x', '#34C759')`).run(); const uid1 = u1.lastInsertRowid; const uid2 = u2.lastInsertRowid; const today = new Date().toISOString().slice(0, 10); const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10); const in3days = new Date(Date.now() + 3 * 86400000).toISOString().slice(0, 10); console.log('\n[Tasks-Test] CRUD + Filter + Subtasks\n'); // -------------------------------------------------------- // Erstellen // -------------------------------------------------------- let task1Id, task2Id, task3Id, subtaskId; test('Aufgabe erstellen', () => { const r = db.prepare(`INSERT INTO tasks (title, category, priority, status, due_date, created_by, assigned_to) VALUES ('Wohnung putzen', 'Haushalt', 'high', 'open', ?, ?, ?)`) .run(today, uid1, uid2); task1Id = r.lastInsertRowid; assert(task1Id > 0, 'ID muss > 0 sein'); }); test('Zweite Aufgabe (überfällig, erledigt)', () => { const r = db.prepare(`INSERT INTO tasks (title, category, priority, status, due_date, created_by) VALUES ('Bereits erledigt', 'Sonstiges', 'low', 'done', ?, ?)`) .run(yesterday, uid1); task2Id = r.lastInsertRowid; assert(task2Id > 0); }); test('Dritte Aufgabe (kein Datum)', () => { const r = db.prepare(`INSERT INTO tasks (title, priority, status, created_by) VALUES ('Später erledigen', 'medium', 'open', ?)`) .run(uid1); task3Id = r.lastInsertRowid; assert(task3Id > 0); }); test('Subtask erstellen (1 Ebene)', () => { const r = db.prepare(`INSERT INTO tasks (title, priority, status, created_by, parent_task_id) VALUES ('Küche putzen', 'medium', 'open', ?, ?)`) .run(uid1, task1Id); subtaskId = r.lastInsertRowid; assert(subtaskId > 0); }); test('Verschachtelungstiefe: Subtask-of-Subtask wird abgelehnt', () => { // Simuliert Backend-Prüfung: parent muss parent_task_id = NULL haben const parent = db.prepare('SELECT parent_task_id FROM tasks WHERE id = ?').get(subtaskId); assert(parent.parent_task_id !== null, 'Subtask hat parent_task_id gesetzt'); // Backend darf keine weiteren Kinder erlauben let threw = false; // CHECK: parent_task_id des subtask ist nicht null → Backend würde 400 zurückgeben if (parent.parent_task_id !== null) threw = true; assert(threw, 'Tiefenprüfung sollte anschlagen'); }); // -------------------------------------------------------- // Lesen + Filter // -------------------------------------------------------- test('Alle Top-Level-Aufgaben mit Subtask-Zähler', () => { const tasks = db.prepare(` SELECT t.*, (SELECT COUNT(*) FROM tasks s WHERE s.parent_task_id = t.id) AS subtask_total, (SELECT COUNT(*) FROM tasks s WHERE s.parent_task_id = t.id AND s.status = 'done') AS subtask_done FROM tasks t WHERE t.parent_task_id IS NULL ORDER BY CASE t.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END `).all(); assert(tasks.length === 3, `Erwartet 3, erhalten ${tasks.length}`); const withSub = tasks.find((t) => t.id === task1Id); assert(withSub.subtask_total === 1, 'subtask_total = 1'); assert(withSub.subtask_done === 0, 'subtask_done = 0'); }); test('Filter nach Status=open', () => { const tasks = db.prepare(`SELECT * FROM tasks WHERE parent_task_id IS NULL AND status = 'open'`).all(); assert(tasks.length === 2, `Erwartet 2 offene, erhalten ${tasks.length}`); assert(tasks.every((t) => t.status === 'open'), 'Alle sollten open sein'); }); test('Filter nach Priority=high', () => { const tasks = db.prepare(`SELECT * FROM tasks WHERE parent_task_id IS NULL AND priority = 'high'`).all(); assert(tasks.length === 1, `Erwartet 1, erhalten ${tasks.length}`); assert(tasks[0].title === 'Wohnung putzen'); }); test('Filter nach assigned_to', () => { const tasks = db.prepare(`SELECT * FROM tasks WHERE parent_task_id IS NULL AND assigned_to = ?`).all(uid2); assert(tasks.length === 1, `Erwartet 1, erhalten ${tasks.length}`); assert(tasks[0].assigned_to === uid2); }); test('Einzelne Aufgabe mit Subtasks und User-Join', () => { const task = db.prepare(` SELECT t.*, u.display_name AS assigned_name, u.avatar_color AS assigned_color FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id WHERE t.id = ? AND t.parent_task_id IS NULL `).get(task1Id); assert(task, 'Aufgabe gefunden'); assert(task.assigned_name === 'Max', 'assigned_name korrekt'); assert(task.assigned_color === '#34C759', 'assigned_color korrekt'); const subtasks = db.prepare(`SELECT * FROM tasks WHERE parent_task_id = ?`).all(task1Id); assert(subtasks.length === 1, 'Ein Subtask'); assert(subtasks[0].title === 'Küche putzen'); }); // -------------------------------------------------------- // Status-Änderungen // -------------------------------------------------------- test('Status ändern: open → done', () => { db.prepare(`UPDATE tasks SET status = 'done' WHERE id = ?`).run(task1Id); const t = db.prepare('SELECT status FROM tasks WHERE id = ?').get(task1Id); assert(t.status === 'done', 'Status sollte done sein'); }); test('Status ändern: done → open', () => { db.prepare(`UPDATE tasks SET status = 'open' WHERE id = ?`).run(task1Id); const t = db.prepare('SELECT status FROM tasks WHERE id = ?').get(task1Id); assert(t.status === 'open', 'Status zurück auf open'); }); test('Subtask-Fortschritt nach Erledigung', () => { db.prepare(`UPDATE tasks SET status = 'done' WHERE id = ?`).run(subtaskId); const progress = db.prepare(` SELECT COUNT(*) AS total, SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) AS done FROM tasks WHERE parent_task_id = ? `).get(task1Id); assert(progress.total === 1, 'total = 1'); assert(progress.done === 1, 'done = 1'); }); // -------------------------------------------------------- // Aktualisieren // -------------------------------------------------------- test('Aufgabe aktualisieren', () => { db.prepare(`UPDATE tasks SET title = 'Wohnung gründlich putzen', priority = 'urgent' WHERE id = ?`) .run(task1Id); const t = db.prepare('SELECT * FROM tasks WHERE id = ?').get(task1Id); assert(t.title === 'Wohnung gründlich putzen', 'Titel aktualisiert'); assert(t.priority === 'urgent', 'Priorität aktualisiert'); }); // -------------------------------------------------------- // Löschen // -------------------------------------------------------- test('Aufgabe löschen löscht Subtasks (CASCADE)', () => { db.prepare('DELETE FROM tasks WHERE id = ?').run(task1Id); const orphan = db.prepare('SELECT * FROM tasks WHERE parent_task_id = ?').get(task1Id); assert(!orphan, 'Subtask sollte gelöscht sein'); }); test('Nicht existierende Aufgabe liefert keine Zeile', () => { const t = db.prepare('SELECT * FROM tasks WHERE id = 99999').get(); assert(!t, 'Sollte undefined sein'); }); // -------------------------------------------------------- // Meta-Endpoint // -------------------------------------------------------- test('Users für Meta-Endpoint abrufbar', () => { const users = db.prepare('SELECT id, display_name, avatar_color FROM users ORDER BY display_name').all(); assert(users.length === 2, `Erwartet 2 User, erhalten ${users.length}`); assert(users[0].avatar_color, 'avatar_color vorhanden'); }); // -------------------------------------------------------- // Ergebnis // -------------------------------------------------------- console.log(`\n[Tasks-Test] Ergebnis: ${passed} bestanden, ${failed} fehlgeschlagen\n`); if (failed > 0) process.exit(1);