Files
oikos/test-meals.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

316 lines
12 KiB
JavaScript

/**
* Modul: Essensplan-Test
* Zweck: Validiert alle Meals-API-Abfragen, Zutaten-CRUD, Wochensortierung,
* Einkaufslisten-Integration
* Ausführen: node --experimental-sqlite test-meals.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]);
// Test-Benutzer
const u1 = db.prepare(`INSERT INTO users (username, display_name, password_hash, role)
VALUES ('admin', 'Admin', 'x', 'admin')`).run();
const uid = u1.lastInsertRowid;
// Einkaufsliste für Integration-Tests
const sl = db.prepare(`INSERT INTO shopping_lists (name, created_by) VALUES ('REWE', ?)`).run(uid);
const listId = sl.lastInsertRowid;
console.log('\n[Meals-Test] Wochenplan, Zutaten, Einkaufslisten-Integration\n');
let mealId1, mealId2, mealId3, ingId1, ingId2;
// --------------------------------------------------------
// Mahlzeit CRUD
// --------------------------------------------------------
test('Mahlzeit erstellen (Mittagessen)', () => {
const r = db.prepare(`
INSERT INTO meals (date, meal_type, title, notes, created_by)
VALUES ('2026-03-23', 'lunch', 'Spaghetti Bolognese', 'Klassiker', ?)
`).run(uid);
mealId1 = r.lastInsertRowid;
assert(mealId1 > 0);
});
test('Mahlzeit erstellen (Frühstück)', () => {
const r = db.prepare(`
INSERT INTO meals (date, meal_type, title, created_by)
VALUES ('2026-03-23', 'breakfast', 'Müsli mit Früchten', ?)
`).run(uid);
mealId2 = r.lastInsertRowid;
assert(mealId2 > 0);
});
test('Mahlzeit erstellen (andere Woche)', () => {
const r = db.prepare(`
INSERT INTO meals (date, meal_type, title, created_by)
VALUES ('2026-03-30', 'dinner', 'Pizza Margherita', ?)
`).run(uid);
mealId3 = r.lastInsertRowid;
assert(mealId3 > 0);
});
test('Mahlzeiten einer Woche abrufen', () => {
const meals = db.prepare(`
SELECT * FROM meals
WHERE date BETWEEN '2026-03-23' AND '2026-03-29'
ORDER BY date ASC,
CASE meal_type
WHEN 'breakfast' THEN 0
WHEN 'lunch' THEN 1
WHEN 'dinner' THEN 2
WHEN 'snack' THEN 3
ELSE 4
END ASC
`).all();
assert(meals.length === 2, `Erwartet 2, erhalten ${meals.length}`);
assert(meals[0].meal_type === 'breakfast', 'Frühstück zuerst');
assert(meals[1].meal_type === 'lunch', 'Mittagessen danach');
});
test('Andere Woche hat nur eigene Mahlzeiten', () => {
const meals = db.prepare(`
SELECT * FROM meals WHERE date BETWEEN '2026-03-30' AND '2026-04-05'
`).all();
assert(meals.length === 1, `Erwartet 1, erhalten ${meals.length}`);
assert(meals[0].title === 'Pizza Margherita');
});
test('Mahlzeit aktualisieren', () => {
db.prepare(`UPDATE meals SET title = 'Spaghetti Carbonara', notes = NULL WHERE id = ?`).run(mealId1);
const m = db.prepare('SELECT title, notes FROM meals WHERE id = ?').get(mealId1);
assert(m.title === 'Spaghetti Carbonara', 'Titel aktualisiert');
assert(m.notes === null, 'Notizen gelöscht');
});
test('Mahlzeit-Typ-Constraint (ungültiger Wert)', () => {
let threw = false;
try {
db.prepare(`INSERT INTO meals (date, meal_type, title, created_by) VALUES ('2026-03-24', 'brunch', 'Test', ?)`).run(uid);
} catch { threw = true; }
assert(threw, 'Constraint muss verletzt werden');
});
// --------------------------------------------------------
// Zutaten CRUD
// --------------------------------------------------------
test('Zutat hinzufügen', () => {
const r = db.prepare(`
INSERT INTO meal_ingredients (meal_id, name, quantity)
VALUES (?, 'Hackfleisch', '500g')
`).run(mealId1);
ingId1 = r.lastInsertRowid;
assert(ingId1 > 0);
});
test('Zweite Zutat hinzufügen', () => {
const r = db.prepare(`
INSERT INTO meal_ingredients (meal_id, name, quantity)
VALUES (?, 'Spaghetti', '400g')
`).run(mealId1);
ingId2 = r.lastInsertRowid;
assert(ingId2 > 0);
});
test('Zutaten einer Mahlzeit abrufen', () => {
const ings = db.prepare(`
SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC
`).all(mealId1);
assert(ings.length === 2, `Erwartet 2, erhalten ${ings.length}`);
assert(ings[0].name === 'Hackfleisch');
assert(ings[1].name === 'Spaghetti');
});
test('Zutat aktualisieren (Menge ändern)', () => {
db.prepare(`UPDATE meal_ingredients SET quantity = '600g' WHERE id = ?`).run(ingId1);
const ing = db.prepare('SELECT quantity FROM meal_ingredients WHERE id = ?').get(ingId1);
assert(ing.quantity === '600g', 'Menge aktualisiert');
});
test('on_shopping_list-Flag setzen', () => {
db.prepare(`UPDATE meal_ingredients SET on_shopping_list = 1 WHERE id = ?`).run(ingId1);
const ing = db.prepare('SELECT on_shopping_list FROM meal_ingredients WHERE id = ?').get(ingId1);
assert(ing.on_shopping_list === 1, 'Flag gesetzt');
});
test('Nur offene Zutaten haben on_shopping_list = 0', () => {
const open = db.prepare(`
SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0
`).all(mealId1);
assert(open.length === 1, `Erwartet 1 offene Zutat, erhalten ${open.length}`);
assert(open[0].name === 'Spaghetti');
});
test('Zutat löschen', () => {
db.prepare('DELETE FROM meal_ingredients WHERE id = ?').run(ingId2);
const remaining = db.prepare('SELECT * FROM meal_ingredients WHERE meal_id = ?').all(mealId1);
assert(remaining.length === 1, 'Nur noch eine Zutat');
});
// --------------------------------------------------------
// Cascade-Verhalten
// --------------------------------------------------------
test('Mahlzeit löschen entfernt Zutaten (CASCADE)', () => {
// Neue Mahlzeit mit Zutat
const m = db.prepare(`
INSERT INTO meals (date, meal_type, title, created_by)
VALUES ('2026-03-25', 'snack', 'Apfel', ?)
`).run(uid);
db.prepare(`INSERT INTO meal_ingredients (meal_id, name) VALUES (?, 'Apfel')`).run(m.lastInsertRowid);
db.prepare('DELETE FROM meals WHERE id = ?').run(m.lastInsertRowid);
const ings = db.prepare('SELECT * FROM meal_ingredients WHERE meal_id = ?').all(m.lastInsertRowid);
assert(ings.length === 0, 'Zutaten nach Mahlzeit-Löschung entfernt');
});
// --------------------------------------------------------
// Einkaufslisten-Integration
// --------------------------------------------------------
test('Offene Zutaten einer Woche abfragen', () => {
// Mahlzeit für Integration-Test anlegen
const m = db.prepare(`
INSERT INTO meals (date, meal_type, title, created_by)
VALUES ('2026-03-24', 'dinner', 'Risotto', ?)
`).run(uid);
const mid = m.lastInsertRowid;
db.prepare(`INSERT INTO meal_ingredients (meal_id, name, quantity) VALUES (?, 'Reis', '300g')`).run(mid);
db.prepare(`INSERT INTO meal_ingredients (meal_id, name, quantity) VALUES (?, 'Parmesan', '100g')`).run(mid);
const open = db.prepare(`
SELECT mi.* FROM meal_ingredients mi
JOIN meals mo ON mo.id = mi.meal_id
WHERE mo.date BETWEEN '2026-03-23' AND '2026-03-29'
AND mi.on_shopping_list = 0
`).all();
// Spaghetti (ingId1 wurde gelöscht), Hackfleisch (on_shopping_list=1 gesetzt), Reis, Parmesan
assert(open.length >= 2, `Mindestens 2 offene Zutaten, erhalten ${open.length}`);
});
test('Zutaten → Einkaufsliste übertragen (INSERT + Flag setzen)', () => {
// Frische Mahlzeit mit 2 Zutaten
const m = db.prepare(`
INSERT INTO meals (date, meal_type, title, created_by)
VALUES ('2026-03-24', 'lunch', 'Suppe', ?)
`).run(uid);
const mid = m.lastInsertRowid;
const i1 = db.prepare(`INSERT INTO meal_ingredients (meal_id, name, quantity) VALUES (?, 'Karotten', '3 Stück')`).run(mid).lastInsertRowid;
const i2 = db.prepare(`INSERT INTO meal_ingredients (meal_id, name) VALUES (?, 'Zwiebeln')`).run(mid).lastInsertRowid;
// Transfer-Logik aus server/routes/meals.js simulieren
const ingredients = db.prepare(`
SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0
`).all(mid);
assert(ingredients.length === 2, `Erwartet 2, erhalten ${ingredients.length}`);
const insertItem = db.prepare(`
INSERT INTO shopping_items (list_id, name, quantity, category, added_from_meal)
VALUES (?, ?, ?, 'Sonstiges', ?)
`);
const markDone = db.prepare(`UPDATE meal_ingredients SET on_shopping_list = 1 WHERE id = ?`);
for (const ing of ingredients) {
insertItem.run(listId, ing.name, ing.quantity, mid);
markDone.run(ing.id);
}
// Prüfen: Artikel in Einkaufsliste
const items = db.prepare(`
SELECT * FROM shopping_items WHERE added_from_meal = ?
`).all(mid);
assert(items.length === 2, `Erwartet 2 Einkaufsartikel, erhalten ${items.length}`);
assert(items[0].name === 'Karotten', `Erster Artikel: ${items[0].name}`);
// Prüfen: Flags gesetzt
const stillOpen = db.prepare(`
SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0
`).all(mid);
assert(stillOpen.length === 0, 'Alle Zutaten als übertragen markiert');
});
test('Zweiter Transfer überträgt nichts (alle bereits markiert)', () => {
// Mahlzeit aus vorherigem Test - alle on_shopping_list = 1
const suppe = db.prepare(`SELECT id FROM meals WHERE title = 'Suppe'`).get();
const open = db.prepare(`
SELECT * FROM meal_ingredients WHERE meal_id = ? AND on_shopping_list = 0
`).all(suppe.id);
assert(open.length === 0, 'Keine offenen Zutaten mehr');
});
test('added_from_meal FK auf meals(id) gesetzt', () => {
const items = db.prepare(`
SELECT si.*, m.title AS meal_title
FROM shopping_items si
JOIN meals m ON m.id = si.added_from_meal
WHERE si.added_from_meal IS NOT NULL
LIMIT 5
`).all();
assert(items.length > 0, 'Mindestens ein Artikel mit Mahlzeit-Referenz');
assert(items[0].meal_title, 'meal_title verknüpft');
});
// --------------------------------------------------------
// Autocomplete-Simulation
// --------------------------------------------------------
test('Mahlzeit-Autocomplete nach Prefix', () => {
const results = db.prepare(`
SELECT DISTINCT title, meal_type FROM meals
WHERE title LIKE ? COLLATE NOCASE
ORDER BY title ASC LIMIT 10
`).all('S%');
assert(results.length >= 1, `Mindestens 1 Treffer, erhalten ${results.length}`);
const titles = results.map((r) => r.title);
assert(titles.some((t) => t.startsWith('S') || t.startsWith('s')), 'Treffer beginnt mit S');
});
test('Autocomplete ohne Treffer gibt leeres Array', () => {
const results = db.prepare(`
SELECT DISTINCT title FROM meals WHERE title LIKE ? COLLATE NOCASE
`).all('XXXXXXXXXXX%');
assert(results.length === 0, 'Leeres Ergebnis erwartet');
});
// --------------------------------------------------------
// Wochenhelfer-Logik (ohne Server)
// --------------------------------------------------------
test('Wochenberechnung: Montag der aktuellen Woche', () => {
// 2026-03-24 ist ein Dienstag → Montag ist 2026-03-23
function getMondayOf(dateStr) {
const d = new Date(dateStr + 'T00:00:00Z');
const day = d.getUTCDay();
const diff = (day === 0 ? -6 : 1 - day);
d.setUTCDate(d.getUTCDate() + diff);
return d.toISOString().slice(0, 10);
}
assert(getMondayOf('2026-03-24') === '2026-03-23', 'Montag korrekt berechnet');
assert(getMondayOf('2026-03-23') === '2026-03-23', 'Montag bleibt Montag');
assert(getMondayOf('2026-03-29') === '2026-03-23', 'Sonntag → gleicher Montag');
assert(getMondayOf('2026-03-30') === '2026-03-30', 'Nächster Montag');
});
// --------------------------------------------------------
// Ergebnis
// --------------------------------------------------------
console.log(`\n[Meals-Test] Ergebnis: ${passed} bestanden, ${failed} fehlgeschlagen\n`);
if (failed > 0) process.exit(1);