134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||
|
|
import type Database from 'better-sqlite3';
|
||
|
|
import { openInMemoryForTest } from '../../src/lib/server/db';
|
||
|
|
import { createProfile } from '../../src/lib/server/profiles/repository';
|
||
|
|
import { insertRecipe } from '../../src/lib/server/recipes/repository';
|
||
|
|
import {
|
||
|
|
addToWishlist,
|
||
|
|
removeFromWishlist,
|
||
|
|
listWishlist,
|
||
|
|
isOnWishlist,
|
||
|
|
likeWish,
|
||
|
|
unlikeWish
|
||
|
|
} from '../../src/lib/server/wishlist/repository';
|
||
|
|
import type { Recipe } from '../../src/lib/types';
|
||
|
|
|
||
|
|
const recipe = (title: string, id?: null): Recipe => ({
|
||
|
|
id: id ?? null,
|
||
|
|
title,
|
||
|
|
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: []
|
||
|
|
});
|
||
|
|
|
||
|
|
let db: Database.Database;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
db = openInMemoryForTest();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('wishlist add/remove', () => {
|
||
|
|
it('adds and lists', () => {
|
||
|
|
const r1 = insertRecipe(db, recipe('Carbonara'));
|
||
|
|
const p = createProfile(db, 'Hendrik');
|
||
|
|
addToWishlist(db, r1, p.id);
|
||
|
|
expect(isOnWishlist(db, r1)).toBe(true);
|
||
|
|
const list = listWishlist(db, p.id);
|
||
|
|
expect(list.length).toBe(1);
|
||
|
|
expect(list[0].title).toBe('Carbonara');
|
||
|
|
expect(list[0].added_by_name).toBe('Hendrik');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is idempotent on double-add', () => {
|
||
|
|
const r1 = insertRecipe(db, recipe('Pizza'));
|
||
|
|
const p = createProfile(db, 'A');
|
||
|
|
addToWishlist(db, r1, p.id);
|
||
|
|
addToWishlist(db, r1, p.id);
|
||
|
|
expect(listWishlist(db, p.id).length).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('removes', () => {
|
||
|
|
const r1 = insertRecipe(db, recipe('X'));
|
||
|
|
addToWishlist(db, r1, null);
|
||
|
|
removeFromWishlist(db, r1);
|
||
|
|
expect(listWishlist(db, null).length).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('cascades with recipe delete', () => {
|
||
|
|
const r1 = insertRecipe(db, recipe('X'));
|
||
|
|
addToWishlist(db, r1, null);
|
||
|
|
db.prepare('DELETE FROM recipe WHERE id = ?').run(r1);
|
||
|
|
expect(listWishlist(db, null).length).toBe(0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('wishlist likes + sort', () => {
|
||
|
|
it('counts likes per entry and shows liked_by_me for active profile', () => {
|
||
|
|
const r1 = insertRecipe(db, recipe('R1'));
|
||
|
|
const r2 = insertRecipe(db, recipe('R2'));
|
||
|
|
const a = createProfile(db, 'A');
|
||
|
|
const b = createProfile(db, 'B');
|
||
|
|
const c = createProfile(db, 'C');
|
||
|
|
addToWishlist(db, r1, a.id);
|
||
|
|
addToWishlist(db, r2, a.id);
|
||
|
|
likeWish(db, r1, a.id);
|
||
|
|
likeWish(db, r1, b.id);
|
||
|
|
likeWish(db, r1, c.id);
|
||
|
|
likeWish(db, r2, a.id);
|
||
|
|
|
||
|
|
const listA = listWishlist(db, a.id, 'popular');
|
||
|
|
expect(listA[0].title).toBe('R1');
|
||
|
|
expect(listA[0].like_count).toBe(3);
|
||
|
|
expect(listA[0].liked_by_me).toBe(1);
|
||
|
|
expect(listA[1].title).toBe('R2');
|
||
|
|
expect(listA[1].like_count).toBe(1);
|
||
|
|
|
||
|
|
const listB = listWishlist(db, b.id);
|
||
|
|
expect(listB.find((e) => e.recipe_id === r1)!.liked_by_me).toBe(1);
|
||
|
|
expect(listB.find((e) => e.recipe_id === r2)!.liked_by_me).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('unlike is idempotent and decrements count', () => {
|
||
|
|
const r = insertRecipe(db, recipe('R'));
|
||
|
|
const a = createProfile(db, 'A');
|
||
|
|
addToWishlist(db, r, a.id);
|
||
|
|
likeWish(db, r, a.id);
|
||
|
|
unlikeWish(db, r, a.id);
|
||
|
|
unlikeWish(db, r, a.id);
|
||
|
|
const [entry] = listWishlist(db, a.id);
|
||
|
|
expect(entry.like_count).toBe(0);
|
||
|
|
expect(entry.liked_by_me).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sort=newest orders by added_at desc, oldest asc', () => {
|
||
|
|
const r1 = insertRecipe(db, recipe('First'));
|
||
|
|
// Force different timestamps via raw insert with explicit added_at
|
||
|
|
db.prepare("INSERT INTO wishlist(recipe_id, added_at) VALUES (?, '2026-01-01 10:00:00')").run(r1);
|
||
|
|
const r2 = insertRecipe(db, recipe('Second'));
|
||
|
|
db.prepare("INSERT INTO wishlist(recipe_id, added_at) VALUES (?, '2026-01-02 10:00:00')").run(r2);
|
||
|
|
|
||
|
|
expect(listWishlist(db, null, 'newest').map((e) => e.title)).toEqual(['Second', 'First']);
|
||
|
|
expect(listWishlist(db, null, 'oldest').map((e) => e.title)).toEqual(['First', 'Second']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles anonymous (no active profile) — liked_by_me always 0', () => {
|
||
|
|
const r = insertRecipe(db, recipe('R'));
|
||
|
|
addToWishlist(db, r, null);
|
||
|
|
likeWish(db, r, createProfile(db, 'A').id);
|
||
|
|
const [entry] = listWishlist(db, null);
|
||
|
|
expect(entry.like_count).toBe(1);
|
||
|
|
expect(entry.liked_by_me).toBe(0);
|
||
|
|
});
|
||
|
|
});
|