From ce860d68e493bfe1a1863618673aa29b5d817ce1 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 3 Apr 2026 10:00:02 +0200 Subject: [PATCH] feat(recipes): add recipe detail load function with 404 handling Co-Authored-By: Claude Sonnet 4.6 --- .../routes/(app)/recipes/[id]/+page.server.ts | 41 +++++++++++++ .../(app)/recipes/[id]/page.server.test.ts | 57 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 frontend/src/routes/(app)/recipes/[id]/+page.server.ts create mode 100644 frontend/src/routes/(app)/recipes/[id]/page.server.test.ts diff --git a/frontend/src/routes/(app)/recipes/[id]/+page.server.ts b/frontend/src/routes/(app)/recipes/[id]/+page.server.ts new file mode 100644 index 0000000..400a297 --- /dev/null +++ b/frontend/src/routes/(app)/recipes/[id]/+page.server.ts @@ -0,0 +1,41 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { apiClient } from '$lib/server/api'; + +export const load: PageServerLoad = async ({ fetch, params }) => { + const api = apiClient(fetch); + const { data, error: apiError } = await api.GET('/v1/recipes/{id}', { + params: { path: { id: params.id } } + }); + + if (apiError || !data) { + error(404, 'Recipe not found'); + } + + return { + recipe: { + id: data.id!, + name: data.name!, + serves: data.serves, + cookTimeMin: data.cookTimeMin, + effort: data.effort, + heroImageUrl: data.heroImageUrl, + ingredients: (data.ingredients ?? []).map((ing) => ({ + ingredientId: ing.ingredientId, + name: ing.name, + quantity: ing.quantity, + unit: ing.unit, + sortOrder: ing.sortOrder + })), + steps: (data.steps ?? []).map((s) => ({ + stepNumber: s.stepNumber, + instruction: s.instruction + })), + tags: (data.tags ?? []).map((t) => ({ + id: t.id!, + name: t.name!, + tagType: t.tagType + })) + } + }; +}; diff --git a/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts b/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts new file mode 100644 index 0000000..a5dc7bb --- /dev/null +++ b/frontend/src/routes/(app)/recipes/[id]/page.server.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('$env/dynamic/private', () => ({ + env: { BACKEND_URL: 'http://localhost:8080' } +})); + +const mockGet = vi.fn(); +vi.mock('$lib/server/api', () => ({ + apiClient: () => ({ GET: mockGet }) +})); + +describe('recipe detail page — load', () => { + let load: any; + + beforeEach(async () => { + mockGet.mockReset(); + vi.resetModules(); + const mod = await import('./+page.server'); + load = mod.load; + }); + + const mockRecipe = { + id: 'r1', + name: 'Spaghetti Bolognese', + serves: 4, + cookTimeMin: 30, + effort: 'Easy', + heroImageUrl: undefined, + ingredients: [ + { ingredientId: 'i1', name: 'Spaghetti', quantity: 200, unit: 'g' } + ], + steps: [{ stepNumber: 1, instruction: 'Kochen' }], + tags: [] + }; + + it('fetches recipe from GET /v1/recipes/{id}', async () => { + mockGet.mockResolvedValue({ data: mockRecipe, error: undefined }); + await load({ fetch: vi.fn(), params: { id: 'r1' } } as any); + expect(mockGet).toHaveBeenCalledWith('/v1/recipes/{id}', expect.objectContaining({ + params: { path: { id: 'r1' } } + })); + }); + + it('returns recipe data on success', async () => { + mockGet.mockResolvedValue({ data: mockRecipe, error: undefined }); + const result = await load({ fetch: vi.fn(), params: { id: 'r1' } } as any); + expect(result.recipe.name).toBe('Spaghetti Bolognese'); + expect(result.recipe.serves).toBe(4); + }); + + it('throws 404 error when API returns error', async () => { + mockGet.mockResolvedValue({ data: undefined, error: { status: 404 } }); + await expect( + load({ fetch: vi.fn(), params: { id: 'nonexistent' } } as any) + ).rejects.toMatchObject({ status: 404 }); + }); +});