feat(recipes): add recipe repository (insert/get/delete with FTS refresh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
100
tests/integration/recipe-repository.test.ts
Normal file
100
tests/integration/recipe-repository.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { openInMemoryForTest } from '../../src/lib/server/db';
|
||||
import {
|
||||
insertRecipe,
|
||||
getRecipeById,
|
||||
getRecipeIdBySourceUrl,
|
||||
deleteRecipe
|
||||
} from '../../src/lib/server/recipes/repository';
|
||||
import { extractRecipeFromHtml } from '../../src/lib/server/parsers/json-ld-recipe';
|
||||
import type { Recipe } from '../../src/lib/types';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const fixture = (name: string): string =>
|
||||
readFileSync(join(here, '../fixtures', name), 'utf8');
|
||||
|
||||
function baseRecipe(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('recipe repository', () => {
|
||||
it('round-trips an imported chefkoch recipe', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const parsed = extractRecipeFromHtml(fixture('chefkoch-schupfnudeln.html'));
|
||||
expect(parsed).not.toBeNull();
|
||||
parsed!.source_url = 'https://www.chefkoch.de/rezepte/4094871643016343/x.html';
|
||||
parsed!.source_domain = 'chefkoch.de';
|
||||
const id = insertRecipe(db, parsed!);
|
||||
const loaded = getRecipeById(db, id);
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded!.title).toBe(parsed!.title);
|
||||
expect(loaded!.ingredients.length).toBe(parsed!.ingredients.length);
|
||||
expect(loaded!.steps.length).toBe(parsed!.steps.length);
|
||||
expect(loaded!.ingredients[0].name).toBe(parsed!.ingredients[0].name);
|
||||
});
|
||||
|
||||
it('FTS finds recipe by ingredient after refresh', () => {
|
||||
const db = openInMemoryForTest();
|
||||
insertRecipe(
|
||||
db,
|
||||
baseRecipe({
|
||||
title: 'Spaghetti Carbonara',
|
||||
ingredients: [
|
||||
{
|
||||
position: 1,
|
||||
quantity: 200,
|
||||
unit: 'g',
|
||||
name: 'Pancetta',
|
||||
note: null,
|
||||
raw_text: '200 g Pancetta'
|
||||
}
|
||||
],
|
||||
tags: ['Italienisch']
|
||||
})
|
||||
);
|
||||
const byIngredient = db
|
||||
.prepare("SELECT rowid FROM recipe_fts WHERE recipe_fts MATCH 'pancetta'")
|
||||
.all();
|
||||
expect(byIngredient.length).toBe(1);
|
||||
const byTag = db
|
||||
.prepare("SELECT rowid FROM recipe_fts WHERE recipe_fts MATCH 'italienisch'")
|
||||
.all();
|
||||
expect(byTag.length).toBe(1);
|
||||
});
|
||||
|
||||
it('looks up by source_url and deletes cascading', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const id = insertRecipe(
|
||||
db,
|
||||
baseRecipe({
|
||||
title: 'Foo',
|
||||
source_url: 'https://example.com/foo',
|
||||
source_domain: 'example.com'
|
||||
})
|
||||
);
|
||||
expect(getRecipeIdBySourceUrl(db, 'https://example.com/foo')).toBe(id);
|
||||
deleteRecipe(db, id);
|
||||
expect(getRecipeById(db, id)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user