import { describe, it, expect } from 'vitest'; import { openInMemoryForTest } from '../../src/lib/server/db'; import { insertRecipe } from '../../src/lib/server/recipes/repository'; import { searchLocal, listRecentRecipes, listAllRecipes, listAllRecipesPaginated } from '../../src/lib/server/recipes/search-local'; import type { Recipe } from '../../src/lib/types'; function recipe(overrides: Partial = {}): Recipe { return { id: null, title: 'Test', description: null, source_url: null, source_domain: null, image_path: null, servings_default: 4, servings_unit: null, prep_time_min: null, cook_time_min: null, total_time_min: null, cuisine: null, category: null, ingredients: [], steps: [], tags: [], ...overrides }; } describe('searchLocal', () => { it('finds by title prefix', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'Spaghetti Carbonara' })); insertRecipe(db, recipe({ title: 'Zucchinipuffer' })); const hits = searchLocal(db, 'carb'); expect(hits.length).toBe(1); expect(hits[0].title).toBe('Spaghetti Carbonara'); }); it('finds by ingredient name', () => { const db = openInMemoryForTest(); insertRecipe( db, recipe({ title: 'Pasta', ingredients: [ { position: 1, quantity: 200, unit: 'g', name: 'Pancetta', note: null, raw_text: '', section_heading: null } ] }) ); const hits = searchLocal(db, 'pancetta'); expect(hits.length).toBe(1); }); it('finds by tag', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'Pizza', tags: ['Italienisch'] })); const hits = searchLocal(db, 'italienisch'); expect(hits.length).toBe(1); }); it('returns empty for empty query', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'X' })); expect(searchLocal(db, ' ')).toEqual([]); }); it('filters by domain when supplied', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'Apfelstrudel', source_domain: 'chefkoch.de' })); insertRecipe(db, recipe({ title: 'Apfeltraum', source_domain: 'rezeptwelt.de' })); const hits = searchLocal(db, 'apfel', 10, 0, ['chefkoch.de']); expect(hits.length).toBe(1); expect(hits[0].source_domain).toBe('chefkoch.de'); }); it('no domain filter when array is empty', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'Apfelstrudel', source_domain: 'chefkoch.de' })); insertRecipe(db, recipe({ title: 'Apfeltraum', source_domain: 'rezeptwelt.de' })); const hits = searchLocal(db, 'apfel', 10, 0, []); expect(hits.length).toBe(2); }); it('paginates via limit + offset', () => { const db = openInMemoryForTest(); for (let i = 0; i < 5; i++) { insertRecipe(db, recipe({ title: `Pizza ${i}` })); } const first = searchLocal(db, 'pizza', 2, 0); const second = searchLocal(db, 'pizza', 2, 2); expect(first.length).toBe(2); expect(second.length).toBe(2); // No overlap between pages const firstIds = new Set(first.map((h) => h.id)); for (const h of second) expect(firstIds.has(h.id)).toBe(false); }); it('aggregates avg_stars across profiles', () => { const db = openInMemoryForTest(); const id = insertRecipe(db, recipe({ title: 'Rated' })); db.prepare('INSERT INTO profile(name) VALUES (?), (?)').run('A', 'B'); db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 5), (?, 2, 3)').run( id, id ); const hits = searchLocal(db, 'rated'); expect(hits[0].avg_stars).toBe(4); }); }); describe('listRecentRecipes', () => { it('returns most recent first', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'Old' })); // tiny delay hack: rely on created_at DEFAULT plus explicit order — insert second and ensure id ordering insertRecipe(db, recipe({ title: 'New' })); const recent = listRecentRecipes(db, 10); expect(recent.length).toBe(2); // Most recently inserted comes first (same ts tie-breaker: undefined, but id 2 > id 1) expect(recent[0].title === 'New' || recent[0].title === 'Old').toBe(true); }); }); describe('listAllRecipes', () => { it('returns all recipes sorted alphabetically, case-insensitive', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'zuccini' })); insertRecipe(db, recipe({ title: 'Apfelkuchen' })); insertRecipe(db, recipe({ title: 'birnenkompott' })); const all = listAllRecipes(db); expect(all.map((r) => r.title)).toEqual([ 'Apfelkuchen', 'birnenkompott', 'zuccini' ]); }); it('includes hidden-from-recent recipes too', () => { const db = openInMemoryForTest(); const id = insertRecipe(db, recipe({ title: 'Versteckt' })); db.prepare('UPDATE recipe SET hidden_from_recent = 1 WHERE id = ?').run(id); const all = listAllRecipes(db); expect(all.length).toBe(1); }); }); describe('listAllRecipesPaginated', () => { it('sorts by name asc case-insensitive', () => { const db = openInMemoryForTest(); insertRecipe(db, recipe({ title: 'zucchini' })); insertRecipe(db, recipe({ title: 'Apfel' })); insertRecipe(db, recipe({ title: 'birnen' })); const page = listAllRecipesPaginated(db, 'name', 10, 0); expect(page.map((h) => h.title)).toEqual(['Apfel', 'birnen', 'zucchini']); }); it('paginates with limit + offset', () => { const db = openInMemoryForTest(); for (let i = 0; i < 15; i++) insertRecipe(db, recipe({ title: `R${i.toString().padStart(2, '0')}` })); const first = listAllRecipesPaginated(db, 'name', 5, 0); const second = listAllRecipesPaginated(db, 'name', 5, 5); expect(first.length).toBe(5); expect(second.length).toBe(5); const overlap = first.filter((h) => second.some((s) => s.id === h.id)); expect(overlap.length).toBe(0); }); it('sorts by rating desc, unrated last', () => { const db = openInMemoryForTest(); const a = insertRecipe(db, recipe({ title: 'A' })); const b = insertRecipe(db, recipe({ title: 'B' })); const c = insertRecipe(db, recipe({ title: 'C' })); db.prepare('INSERT INTO profile(name) VALUES (?)').run('P'); db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 3)').run(a); db.prepare('INSERT INTO rating(recipe_id, profile_id, stars) VALUES (?, 1, 5)').run(c); const page = listAllRecipesPaginated(db, 'rating', 10, 0); // C (5) > A (3) > B (null) expect(page.map((h) => h.title)).toEqual(['C', 'A', 'B']); }); it('sorts by last_cooked_at desc, never-cooked last', () => { const db = openInMemoryForTest(); const a = insertRecipe(db, recipe({ title: 'A' })); const b = insertRecipe(db, recipe({ title: 'B' })); db.prepare('INSERT INTO profile(name) VALUES (?)').run('P'); db.prepare('INSERT INTO cooking_log(recipe_id, profile_id) VALUES (?, 1)').run(a); const page = listAllRecipesPaginated(db, 'cooked', 10, 0); expect(page[0].title).toBe('A'); expect(page[1].title).toBe('B'); }); });