Files
kochwas/tests/integration/search-local.test.ts

197 lines
7.1 KiB
TypeScript
Raw Normal View History

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> = {}): 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');
});
});