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 <noreply@anthropic.com>
This commit is contained in:
@@ -11,14 +11,18 @@ export const GET: RequestHandler = async ({ fetch, url }) => {
|
|||||||
return json({ suggestions: [] });
|
return json({ suggestions: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = apiClient(fetch);
|
try {
|
||||||
const { data } = await api.GET('/v1/week-plans/{id}/suggestions', {
|
const api = apiClient(fetch);
|
||||||
params: { path: { id: planId }, query: { slotDate: date } }
|
const { data } = await api.GET('/v1/week-plans/{id}/suggestions', {
|
||||||
});
|
params: { path: { id: planId }, query: { slotDate: date } }
|
||||||
|
});
|
||||||
|
|
||||||
const suggestions = (data?.suggestions ?? []).sort(
|
const suggestions = (data?.suggestions ?? []).sort(
|
||||||
(a: any, b: any) => (b.simulatedScore ?? 0) - (a.simulatedScore ?? 0)
|
(a: any, b: any) => (b.scoreDelta ?? 0) - (a.scoreDelta ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
return json({ suggestions });
|
return json({ suggestions });
|
||||||
|
} catch {
|
||||||
|
return json({ suggestions: [] });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
91
frontend/src/routes/(app)/planner/server.test.ts
Normal file
91
frontend/src/routes/(app)/planner/server.test.ts
Normal file
@@ -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: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user