From 59366b6e9c5983b88ff8c7f842c18d09da8bed7d Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 9 Apr 2026 11:39:50 +0200 Subject: [PATCH] feat(planner): add server.test.ts for GET /planner, fix sort + add error handling - Sort uses scoreDelta instead of removed simulatedScore - try/catch degrades gracefully to suggestions=[] on backend errors - 6 tests cover: missing params, success, backend error, network throw, empty result Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/(app)/planner/+server.ts | 20 ++-- .../src/routes/(app)/planner/server.test.ts | 91 +++++++++++++++++++ 2 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 frontend/src/routes/(app)/planner/server.test.ts diff --git a/frontend/src/routes/(app)/planner/+server.ts b/frontend/src/routes/(app)/planner/+server.ts index 910efc5..af3aa80 100644 --- a/frontend/src/routes/(app)/planner/+server.ts +++ b/frontend/src/routes/(app)/planner/+server.ts @@ -11,14 +11,18 @@ export const GET: RequestHandler = async ({ fetch, url }) => { return json({ suggestions: [] }); } - const api = apiClient(fetch); - const { data } = await api.GET('/v1/week-plans/{id}/suggestions', { - params: { path: { id: planId }, query: { slotDate: date } } - }); + try { + const api = apiClient(fetch); + const { data } = await api.GET('/v1/week-plans/{id}/suggestions', { + params: { path: { id: planId }, query: { slotDate: date } } + }); - const suggestions = (data?.suggestions ?? []).sort( - (a: any, b: any) => (b.simulatedScore ?? 0) - (a.simulatedScore ?? 0) - ); + const suggestions = (data?.suggestions ?? []).sort( + (a: any, b: any) => (b.scoreDelta ?? 0) - (a.scoreDelta ?? 0) + ); - return json({ suggestions }); + return json({ suggestions }); + } catch { + return json({ suggestions: [] }); + } }; diff --git a/frontend/src/routes/(app)/planner/server.test.ts b/frontend/src/routes/(app)/planner/server.test.ts new file mode 100644 index 0000000..023d0fd --- /dev/null +++ b/frontend/src/routes/(app)/planner/server.test.ts @@ -0,0 +1,91 @@ +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 }) +})); + +const PLAN_UUID = '11111111-1111-1111-1111-111111111111'; +const DATE = '2026-04-09'; + +const mockSuggestions = [ + { recipe: { id: 'r1', name: 'Lachsfilet', effort: 'easy', cookTimeMin: 25 }, scoreDelta: 0.0, hasConflict: true }, + { recipe: { id: 'r2', name: 'Nudeln', effort: 'easy', cookTimeMin: 20 }, scoreDelta: -1.5, hasConflict: true } +]; + +describe('GET /planner — suggestions route handler', () => { + let GET: any; + + beforeEach(async () => { + mockGet.mockReset(); + vi.resetModules(); + const mod = await import('./+server'); + GET = mod.GET; + }); + + it('returns { suggestions: [] } when planId is missing', async () => { + const url = new URL('http://localhost/planner?date=' + DATE); + const response = await GET({ fetch: vi.fn(), url }); + const body = await response.json(); + expect(body).toEqual({ suggestions: [] }); + expect(mockGet).not.toHaveBeenCalled(); + }); + + it('returns { suggestions: [] } when date is missing', async () => { + const url = new URL('http://localhost/planner?planId=' + PLAN_UUID); + const response = await GET({ fetch: vi.fn(), url }); + const body = await response.json(); + expect(body).toEqual({ suggestions: [] }); + expect(mockGet).not.toHaveBeenCalled(); + }); + + it('returns sorted suggestions from backend on success', async () => { + mockGet.mockResolvedValueOnce({ data: { suggestions: mockSuggestions }, error: undefined }); + + const url = new URL(`http://localhost/planner?planId=${PLAN_UUID}&date=${DATE}`); + const response = await GET({ fetch: vi.fn(), url }); + const body = await response.json(); + + expect(mockGet).toHaveBeenCalledWith('/v1/week-plans/{id}/suggestions', expect.objectContaining({ + params: { path: { id: PLAN_UUID }, query: { slotDate: DATE } } + })); + expect(body.suggestions).toHaveLength(2); + // sorted by scoreDelta desc: 0.0 before -1.5 + expect(body.suggestions[0].recipe.name).toBe('Lachsfilet'); + expect(body.suggestions[1].recipe.name).toBe('Nudeln'); + }); + + it('returns { suggestions: [] } when backend returns error', async () => { + mockGet.mockResolvedValueOnce({ data: undefined, error: { status: 500 } }); + + const url = new URL(`http://localhost/planner?planId=${PLAN_UUID}&date=${DATE}`); + const response = await GET({ fetch: vi.fn(), url }); + const body = await response.json(); + + expect(body).toEqual({ suggestions: [] }); + }); + + it('returns { suggestions: [] } when backend throws (network error)', async () => { + mockGet.mockRejectedValueOnce(new Error('Network error')); + + const url = new URL(`http://localhost/planner?planId=${PLAN_UUID}&date=${DATE}`); + const response = await GET({ fetch: vi.fn(), url }); + const body = await response.json(); + + expect(body).toEqual({ suggestions: [] }); + }); + + it('returns empty suggestions when backend returns empty array', async () => { + mockGet.mockResolvedValueOnce({ data: { suggestions: [] }, error: undefined }); + + const url = new URL(`http://localhost/planner?planId=${PLAN_UUID}&date=${DATE}`); + const response = await GET({ fetch: vi.fn(), url }); + const body = await response.json(); + + expect(body).toEqual({ suggestions: [] }); + }); +});