Files
kochwas/tests/integration/recipe-views.test.ts
hsiegeln 6c8de6fa3a feat(db): recordView/listViews fuer recipe_view
INSERT OR REPLACE fuer idempotenten Bump des last_viewed_at Timestamps.
listViews-Helper nur fuer Tests; Sort-Query laeuft direkt in
listAllRecipesPaginated via LEFT JOIN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 14:10:52 +02:00

85 lines
3.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { openInMemoryForTest } from '../../src/lib/server/db';
import { recordView, listViews } from '../../src/lib/server/recipes/views';
import { createProfile } from '../../src/lib/server/profiles/repository';
function seedRecipe(db: ReturnType<typeof openInMemoryForTest>, title: string): number {
const r = db
.prepare("INSERT INTO recipe (title, created_at) VALUES (?, datetime('now')) RETURNING id")
.get(title) as { id: number };
return r.id;
}
describe('014_recipe_views migration', () => {
it('creates recipe_view table with expected columns', () => {
const db = openInMemoryForTest();
const cols = db.prepare("PRAGMA table_info(recipe_view)").all() as Array<{
name: string;
type: string;
notnull: number;
pk: number;
}>;
const byName = Object.fromEntries(cols.map((c) => [c.name, c]));
expect(byName.profile_id?.type).toBe('INTEGER');
expect(byName.profile_id?.notnull).toBe(1);
expect(byName.profile_id?.pk).toBe(1);
expect(byName.recipe_id?.type).toBe('INTEGER');
expect(byName.recipe_id?.notnull).toBe(1);
expect(byName.recipe_id?.pk).toBe(2);
expect(byName.last_viewed_at?.type).toBe('TIMESTAMP');
expect(byName.last_viewed_at?.notnull).toBe(1);
});
it('has index on (profile_id, last_viewed_at DESC)', () => {
const db = openInMemoryForTest();
const idxList = db
.prepare("PRAGMA index_list(recipe_view)")
.all() as Array<{ name: string }>;
expect(idxList.some((i) => i.name === 'idx_recipe_view_recent')).toBe(true);
});
});
describe('recordView', () => {
it('inserts a view row with default timestamp', () => {
const db = openInMemoryForTest();
const profile = createProfile(db, 'Test');
const recipeId = seedRecipe(db, 'Pasta');
recordView(db, profile.id, recipeId);
const rows = listViews(db, profile.id);
expect(rows.length).toBe(1);
expect(rows[0].recipe_id).toBe(recipeId);
expect(rows[0].last_viewed_at).toMatch(/^\d{4}-\d{2}-\d{2}/);
});
it('updates timestamp on subsequent view of same recipe', async () => {
const db = openInMemoryForTest();
const profile = createProfile(db, 'Test');
const recipeId = seedRecipe(db, 'Pasta');
recordView(db, profile.id, recipeId);
const first = listViews(db, profile.id)[0].last_viewed_at;
// tiny delay so the second timestamp differs
await new Promise((r) => setTimeout(r, 1100));
recordView(db, profile.id, recipeId);
const rows = listViews(db, profile.id);
expect(rows.length).toBe(1);
expect(rows[0].last_viewed_at >= first).toBe(true);
});
it('throws on unknown profile_id (FK)', () => {
const db = openInMemoryForTest();
const recipeId = seedRecipe(db, 'Pasta');
expect(() => recordView(db, 999, recipeId)).toThrow();
});
it('throws on unknown recipe_id (FK)', () => {
const db = openInMemoryForTest();
const profile = createProfile(db, 'Test');
expect(() => recordView(db, profile.id, 999)).toThrow();
});
});