2026-04-17 15:04:59 +02:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { openInMemoryForTest } from '../../src/lib/server/db';
|
|
|
|
|
import { runMigrations } from '../../src/lib/server/db/migrate';
|
|
|
|
|
|
|
|
|
|
describe('db migrations', () => {
|
|
|
|
|
it('creates all expected tables', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const tables = (
|
|
|
|
|
db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all() as {
|
|
|
|
|
name: string;
|
|
|
|
|
}[]
|
|
|
|
|
).map((r) => r.name);
|
|
|
|
|
for (const t of [
|
|
|
|
|
'profile',
|
|
|
|
|
'recipe',
|
|
|
|
|
'ingredient',
|
|
|
|
|
'step',
|
|
|
|
|
'tag',
|
|
|
|
|
'recipe_tag',
|
|
|
|
|
'rating',
|
|
|
|
|
'comment',
|
|
|
|
|
'favorite',
|
|
|
|
|
'cooking_log',
|
|
|
|
|
'allowed_domain',
|
|
|
|
|
'schema_migration'
|
|
|
|
|
]) {
|
|
|
|
|
expect(tables).toContain(t);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('is idempotent', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
2026-04-17 17:08:22 +02:00
|
|
|
const countBefore = (
|
|
|
|
|
db.prepare('SELECT COUNT(*) AS c FROM schema_migration').get() as { c: number }
|
|
|
|
|
).c;
|
2026-04-17 15:04:59 +02:00
|
|
|
runMigrations(db);
|
2026-04-17 17:08:22 +02:00
|
|
|
const countAfter = (
|
|
|
|
|
db.prepare('SELECT COUNT(*) AS c FROM schema_migration').get() as { c: number }
|
|
|
|
|
).c;
|
|
|
|
|
expect(countAfter).toBe(countBefore);
|
2026-04-17 15:04:59 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cascades recipe delete to ingredients and steps', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const row = db
|
|
|
|
|
.prepare('INSERT INTO recipe(title) VALUES (?) RETURNING id')
|
|
|
|
|
.get('Test') as { id: number };
|
|
|
|
|
db.prepare('INSERT INTO ingredient(recipe_id, position, name) VALUES (?, ?, ?)').run(
|
|
|
|
|
row.id,
|
|
|
|
|
1,
|
|
|
|
|
'Salz'
|
|
|
|
|
);
|
|
|
|
|
db.prepare('INSERT INTO step(recipe_id, position, text) VALUES (?, ?, ?)').run(
|
|
|
|
|
row.id,
|
|
|
|
|
1,
|
|
|
|
|
'Kochen'
|
|
|
|
|
);
|
|
|
|
|
db.prepare('DELETE FROM recipe WHERE id = ?').run(row.id);
|
|
|
|
|
const ings = db.prepare('SELECT COUNT(*) AS c FROM ingredient').get() as { c: number };
|
|
|
|
|
const steps = db.prepare('SELECT COUNT(*) AS c FROM step').get() as { c: number };
|
|
|
|
|
expect(ings.c).toBe(0);
|
|
|
|
|
expect(steps.c).toBe(0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('FTS5 index finds recipes by title', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
db.prepare('INSERT INTO recipe(title, description) VALUES (?, ?)').run(
|
|
|
|
|
'Spaghetti Carbonara',
|
|
|
|
|
'Italienisches Pasta-Klassiker'
|
|
|
|
|
);
|
|
|
|
|
const hits = db
|
|
|
|
|
.prepare("SELECT rowid FROM recipe_fts WHERE recipe_fts MATCH 'carbonara'")
|
|
|
|
|
.all();
|
|
|
|
|
expect(hits.length).toBe(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('enforces rating stars 1..5', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
db.prepare('INSERT INTO profile(name) VALUES (?)').run('Hendrik');
|
|
|
|
|
const r = db
|
|
|
|
|
.prepare('INSERT INTO recipe(title) VALUES (?) RETURNING id')
|
|
|
|
|
.get('Test') as { id: number };
|
|
|
|
|
db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 5)').run(r.id);
|
|
|
|
|
expect(() =>
|
|
|
|
|
db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 6)').run(r.id)
|
|
|
|
|
).toThrow();
|
|
|
|
|
});
|
|
|
|
|
});
|