import { describe, it, expect, vi, beforeEach } from 'vitest'; vi.mock('$env/dynamic/private', () => ({ env: { BACKEND_URL: 'http://localhost:8080' } })); const mockGet = vi.fn(); const mockPost = vi.fn(); const mockPatch = vi.fn(); const mockDelete = vi.fn(); vi.mock('$lib/server/api', () => ({ apiClient: () => ({ GET: mockGet, POST: mockPost, PATCH: mockPatch, DELETE: mockDelete }) })); const PLAN_UUID = '11111111-1111-1111-1111-111111111111'; const SLOT_UUID = '22222222-2222-2222-2222-222222222222'; const RECIPE_UUID = '33333333-3333-3333-3333-333333333333'; const mockWeekPlan = { id: PLAN_UUID, weekStart: '2026-03-30', status: 'draft', slots: [ { id: SLOT_UUID, slotDate: '2026-03-30', recipe: { id: RECIPE_UUID, name: 'Pasta', effort: 'Easy', cookTimeMin: 30 } } ] }; const mockRecipes = [{ id: RECIPE_UUID, name: 'Pasta', cookTimeMin: 30, effort: 'Easy' }]; const mockVarietyScore = { score: 7.5, ingredientOverlaps: [], tagRepeats: [], recentRepeats: [], duplicatesInPlan: [] }; describe('planner page — load', () => { let load: any; beforeEach(async () => { mockGet.mockReset(); mockPost.mockReset(); vi.resetModules(); const mod = await import('./+page.server'); load = mod.load; }); it('fetches week plan for the current week by default', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) // weekPlan .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) // recipes .mockResolvedValueOnce({ data: mockVarietyScore, error: undefined }); // varietyScore const url = new URL('http://localhost/planner'); await load({ fetch: vi.fn(), url }); expect(mockGet).toHaveBeenCalledWith('/v1/week-plans', expect.objectContaining({ params: expect.objectContaining({ query: expect.objectContaining({ weekStart: expect.any(String) }) }) })); }); it('uses weekStart from URL search params if provided', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) .mockResolvedValueOnce({ data: mockVarietyScore, error: undefined }); const url = new URL('http://localhost/planner?week=2026-03-30'); await load({ fetch: vi.fn(), url }); expect(mockGet).toHaveBeenCalledWith('/v1/week-plans', expect.objectContaining({ params: expect.objectContaining({ query: { weekStart: '2026-03-30' } }) })); }); it('returns weekPlan with slots in page data', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) .mockResolvedValueOnce({ data: mockVarietyScore, error: undefined }); const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.weekPlan).toBeDefined(); expect(result.weekPlan.id).toBe(PLAN_UUID); expect(result.weekPlan.slots).toHaveLength(1); }); it('returns variety score in page data', async () => { const scoreWithOverlap = { score: 7.5, ingredientOverlaps: [{ ingredientName: 'Tomate', days: ['2026-03-30', '2026-03-31'] }] }; mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) .mockResolvedValueOnce({ data: scoreWithOverlap, error: undefined }); const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.varietyScore.score).toBe(7.5); expect(result.varietyScore.ingredientOverlaps).toHaveLength(1); }); it('returns null weekPlan when API returns 404', async () => { mockGet .mockResolvedValueOnce({ data: undefined, error: { status: 404 } }) // weekPlan 404 .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }); // recipes const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.weekPlan).toBeNull(); expect(result.varietyScore).toBeNull(); }); it('returns the weekStart used for the query', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) .mockResolvedValueOnce({ data: mockVarietyScore, error: undefined }); const url = new URL('http://localhost/planner?week=2026-03-30'); const result = await load({ fetch: vi.fn(), url }); expect(result.weekStart).toBe('2026-03-30'); }); it('creates week plan if not found and fetches variety score after creation', async () => { // When no plan exists (404), load should return null weekPlan — creation is done via a POST action, not in load mockGet .mockResolvedValueOnce({ data: undefined, error: { status: 404 } }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }); const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.weekPlan).toBeNull(); }); it('returns recipes in page data', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) .mockResolvedValueOnce({ data: mockVarietyScore, error: undefined }); const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.recipes).toHaveLength(1); expect(result.recipes[0].name).toBe('Pasta'); }); it('returns empty recipes array when recipes API fails', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: undefined, error: { status: 500 } }) // recipes fail .mockResolvedValueOnce({ data: mockVarietyScore, error: undefined }); const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.recipes).toEqual([]); }); }); describe('planner page — actions', () => { let actions: any; beforeEach(async () => { mockGet.mockReset(); mockPost.mockReset(); vi.resetModules(); const mod = await import('./+page.server'); actions = mod.actions; }); it('createPlan action calls POST /v1/week-plans', async () => { mockPost.mockResolvedValue({ data: { id: PLAN_UUID, weekStart: '2026-03-30', slots: [] }, error: undefined }); const formData = new FormData(); formData.set('weekStart', '2026-03-30'); const result = await actions.createPlan({ fetch: vi.fn(), request: { formData: async () => formData }, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' }, haushalt: { id: 'h1', name: 'Test' } } }); expect(mockPost).toHaveBeenCalledWith('/v1/week-plans', expect.objectContaining({ body: { weekStart: '2026-03-30' } })); expect(result).toEqual({ success: true }); }); it('createPlan action returns error when API fails', async () => { mockPost.mockResolvedValue({ data: undefined, error: { message: 'Server error' } }); const formData = new FormData(); formData.set('weekStart', '2026-03-30'); const result = await actions.createPlan({ fetch: vi.fn(), request: { formData: async () => formData }, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' }, haushalt: { id: 'h1', name: 'Test' } } }); expect(result).toEqual({ success: false, error: expect.any(String) }); }); it('createPlan action returns error for invalid weekStart format', async () => { const formData = new FormData(); formData.set('weekStart', 'not-a-date'); const result = await actions.createPlan({ fetch: vi.fn(), request: { formData: async () => formData }, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' }, haushalt: { id: 'h1', name: 'Test' } } }); expect(result).toEqual({ success: false, error: 'Ungültiges Datum.' }); expect(mockPost).not.toHaveBeenCalled(); }); it('createPlan action returns error when weekStart is missing', async () => { const formData = new FormData(); const result = await actions.createPlan({ fetch: vi.fn(), request: { formData: async () => formData }, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'planer' }, haushalt: { id: 'h1', name: 'Test' } } }); expect(result).toEqual({ success: false, error: 'Ungültiges Datum.' }); }); it('createPlan action returns permission error for member role', async () => { const formData = new FormData(); formData.set('weekStart', '2026-03-30'); const result = await actions.createPlan({ fetch: vi.fn(), request: { formData: async () => formData }, locals: { benutzer: { id: 'u1', name: 'Test', rolle: 'mitglied' }, haushalt: { id: 'h1', name: 'Test' } } }); expect(result).toEqual({ success: false, error: 'Keine Berechtigung.' }); expect(mockPost).not.toHaveBeenCalled(); }); }); describe('planner page — variety score partial failure', () => { let load: any; beforeEach(async () => { mockGet.mockReset(); mockPost.mockReset(); vi.resetModules(); const mod = await import('./+page.server'); load = mod.load; }); it('returns weekPlan even when variety score API fails', async () => { mockGet .mockResolvedValueOnce({ data: mockWeekPlan, error: undefined }) .mockResolvedValueOnce({ data: { data: mockRecipes }, error: undefined }) .mockResolvedValueOnce({ data: undefined, error: { status: 500 } }); // variety score fails const url = new URL('http://localhost/planner'); const result = await load({ fetch: vi.fn(), url }); expect(result.weekPlan).toBeDefined(); expect(result.weekPlan.id).toBe(PLAN_UUID); expect(result.varietyScore).toBeNull(); }); }); describe('planner page — slot actions', () => { let actions: any; beforeEach(async () => { mockGet.mockReset(); mockPost.mockReset(); mockPatch.mockReset(); mockDelete.mockReset(); vi.resetModules(); const mod = await import('./+page.server'); actions = mod.actions; }); it('addSlot calls POST /v1/week-plans/{id}/slots and returns success with slot', async () => { const formData = new FormData(); formData.set('planId', PLAN_UUID); formData.set('slotDate', '2026-04-01'); formData.set('recipeId', RECIPE_UUID); mockPost.mockResolvedValue({ data: { id: SLOT_UUID, slotDate: '2026-04-01' }, error: undefined }); const result = await actions.addSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(mockPost).toHaveBeenCalledWith( '/v1/week-plans/{id}/slots', expect.objectContaining({ params: { path: { id: PLAN_UUID } }, body: { slotDate: '2026-04-01', recipeId: RECIPE_UUID } }) ); expect(result.success).toBe(true); expect(result.slot?.id).toBe(SLOT_UUID); }); it('addSlot returns failure when API errors', async () => { const formData = new FormData(); formData.set('planId', PLAN_UUID); formData.set('slotDate', '2026-04-01'); formData.set('recipeId', RECIPE_UUID); mockPost.mockResolvedValue({ data: undefined, error: { status: 500 } }); const result = await actions.addSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(result.success).toBe(false); }); it('addSlot returns validation error when planId is not a UUID', async () => { const formData = new FormData(); formData.set('planId', 'not-a-uuid'); formData.set('slotDate', '2026-04-01'); formData.set('recipeId', RECIPE_UUID); const result = await actions.addSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(result.success).toBe(false); expect(result.error).toBe('Ungültige Eingabe.'); expect(mockPost).not.toHaveBeenCalled(); }); it('addSlot returns validation error when slotDate is missing', async () => { const formData = new FormData(); formData.set('planId', PLAN_UUID); formData.set('recipeId', RECIPE_UUID); const result = await actions.addSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(result.success).toBe(false); expect(result.error).toBe('Ungültige Eingabe.'); expect(mockPost).not.toHaveBeenCalled(); }); it('updateSlot calls PATCH /v1/week-plans/{planId}/slots/{slotId} and returns success', async () => { const formData = new FormData(); formData.set('planId', PLAN_UUID); formData.set('slotId', SLOT_UUID); formData.set('recipeId', RECIPE_UUID); mockPatch.mockResolvedValue({ data: { id: SLOT_UUID }, error: undefined }); const result = await actions.updateSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(mockPatch).toHaveBeenCalledWith( '/v1/week-plans/{planId}/slots/{slotId}', expect.objectContaining({ params: { path: { planId: PLAN_UUID, slotId: SLOT_UUID } }, body: { recipeId: RECIPE_UUID } }) ); expect(result.success).toBe(true); }); it('updateSlot returns validation error when slotId is not a UUID', async () => { const formData = new FormData(); formData.set('planId', PLAN_UUID); formData.set('slotId', 'bad-id'); formData.set('recipeId', RECIPE_UUID); const result = await actions.updateSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(result.success).toBe(false); expect(result.error).toBe('Ungültige Eingabe.'); expect(mockPatch).not.toHaveBeenCalled(); }); it('deleteSlot calls DELETE /v1/week-plans/{planId}/slots/{slotId} and returns success', async () => { const formData = new FormData(); formData.set('planId', PLAN_UUID); formData.set('slotId', SLOT_UUID); mockDelete.mockResolvedValue({ error: undefined }); const result = await actions.deleteSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(mockDelete).toHaveBeenCalledWith( '/v1/week-plans/{planId}/slots/{slotId}', expect.objectContaining({ params: { path: { planId: PLAN_UUID, slotId: SLOT_UUID } } }) ); expect(result.success).toBe(true); }); it('deleteSlot returns validation error when planId is missing', async () => { const formData = new FormData(); formData.set('slotId', SLOT_UUID); const result = await actions.deleteSlot({ fetch: vi.fn(), request: { formData: async () => formData } } as any); expect(result.success).toBe(false); expect(result.error).toBe('Ungültige Eingabe.'); expect(mockDelete).not.toHaveBeenCalled(); }); });