433124790f
Backend: - GET /tasks mit Filtern (status, priority, assigned_to, category) - GET /tasks/:id mit Subtasks - POST /tasks mit Tiefenlimit (max. 2 Ebenen) - PUT /tasks/:id, PATCH /tasks/:id/status, DELETE /tasks/:id - GET /tasks/meta/options für Dropdown-Daten - Sortierung: Priorität → Fälligkeit, done-Tasks ans Ende Frontend: - Listenansicht gruppiert nach Kategorie oder Fälligkeit (umschaltbar) - Filter-Chips: Status, Priorität, Person (horizontal scrollbar) - Task-Card: Prioritäts-Badge, Fälligkeitsdatum, Avatar, Edit-Button - Status-Toggle per Checkbox (open ↔ done) - Subtask-Fortschrittsbalken + ein-/ausklappbare Subtask-Liste - Subtask inline abhaken oder neu erstellen - Overdue-Badge in der Navigation - CRUD-Modal: Titel, Beschreibung, Priorität, Kategorie, Datum, Zuweisung - Skeleton-Loading während API-Call Tests: 17/17 bestanden (54 gesamt) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
210 lines
8.5 KiB
JavaScript
210 lines
8.5 KiB
JavaScript
/**
|
|
* 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);
|