Files
mealprep/frontend/src/routes/(app)/recipes/new/page.server.test.ts
Marcel Raddatz 6505cb4251 test(recipes): add action tests and harden create/update form actions
- Add try-catch around JSON.parse with fail(400) for malformed input
- Validate effort against allowed values ['Easy','Medium','Hard']
- Fix NaN risk: Number(serves)||undefined instead of Number(serves)
- Add action tests for create/update: validation, JSON.parse crash, success, API error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 10:27:54 +02:00

155 lines
4.6 KiB
TypeScript

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();
vi.mock('$lib/server/api', () => ({
apiClient: () => ({ GET: mockGet, POST: mockPost })
}));
describe('new recipe page — load', () => {
let load: any;
beforeEach(async () => {
mockGet.mockReset();
mockPost.mockReset();
vi.resetModules();
const mod = await import('./+page.server');
load = mod.load;
});
const mockTags = [
{ id: 't1', name: 'Pasta', tagType: 'category' },
{ id: 't2', name: 'Fleisch', tagType: 'category' }
];
it('fetches tags from GET /v1/tags', async () => {
mockGet.mockResolvedValue({ data: mockTags, error: undefined });
await load({ fetch: vi.fn() } as any);
expect(mockGet).toHaveBeenCalledWith('/v1/tags', expect.anything());
});
it('returns categories filtered from tags', async () => {
mockGet.mockResolvedValue({ data: mockTags, error: undefined });
const result = await load({ fetch: vi.fn() } as any);
expect(result.categories).toHaveLength(2);
expect(result.categories[0].name).toBe('Pasta');
});
it('returns empty categories when API fails', async () => {
mockGet.mockResolvedValue({ data: undefined, error: { status: 500 } });
const result = await load({ fetch: vi.fn() } as any);
expect(result.categories).toEqual([]);
});
it('returns null recipe for new form', async () => {
mockGet.mockResolvedValue({ data: mockTags, error: undefined });
const result = await load({ fetch: vi.fn() } as any);
expect(result.recipe).toBeNull();
});
});
describe('new recipe page — create action', () => {
let actions: any;
const makeFormData = (overrides: Record<string, string | string[]> = {}) => {
const base: Record<string, string | string[]> = {
name: 'Test Rezept',
effort: 'Easy',
tagIds: ['t1'],
ingredientsJson: '[]',
stepsJson: '[]',
...overrides
};
const fd = new FormData();
for (const [key, val] of Object.entries(base)) {
if (Array.isArray(val)) {
for (const v of val) fd.append(key, v);
} else {
fd.append(key, val);
}
}
return fd;
};
beforeEach(async () => {
mockGet.mockReset();
mockPost.mockReset();
vi.resetModules();
const mod = await import('./+page.server');
actions = mod.actions;
});
it('returns fail(422) when name is missing', async () => {
const result = await actions.create({
request: { formData: async () => makeFormData({ name: '' }) },
fetch: vi.fn()
} as any);
expect(result.status).toBe(422);
});
it('returns fail(422) when effort is missing', async () => {
const result = await actions.create({
request: { formData: async () => makeFormData({ effort: '' }) },
fetch: vi.fn()
} as any);
expect(result.status).toBe(422);
});
it('returns fail(422) when effort is not a valid value', async () => {
const result = await actions.create({
request: { formData: async () => makeFormData({ effort: 'InvalidEffort' }) },
fetch: vi.fn()
} as any);
expect(result.status).toBe(422);
});
it('returns fail(422) when no tagIds', async () => {
const result = await actions.create({
request: { formData: async () => makeFormData({ tagIds: [] }) },
fetch: vi.fn()
} as any);
expect(result.status).toBe(422);
});
it('returns fail(400) when ingredientsJson is invalid JSON', async () => {
const result = await actions.create({
request: { formData: async () => makeFormData({ ingredientsJson: 'not-json' }) },
fetch: vi.fn()
} as any);
expect(result.status).toBe(400);
});
it('returns fail(400) when stepsJson is invalid JSON', async () => {
const result = await actions.create({
request: { formData: async () => makeFormData({ stepsJson: '{broken' }) },
fetch: vi.fn()
} as any);
expect(result.status).toBe(400);
});
it('calls POST /v1/recipes with correct body on success', async () => {
mockPost.mockResolvedValue({ error: undefined });
const fd = makeFormData({
ingredientsJson: JSON.stringify([{ name: 'Spaghetti', quantity: 200, unit: 'g' }]),
stepsJson: JSON.stringify(['Kochen'])
});
await actions.create({ request: { formData: async () => fd }, fetch: vi.fn() } as any).catch(
() => {}
);
expect(mockPost).toHaveBeenCalledWith('/v1/recipes', expect.objectContaining({ body: expect.objectContaining({ name: 'Test Rezept', effort: 'Easy' }) }));
});
it('returns fail(500) when API returns error', async () => {
mockPost.mockResolvedValue({ error: { status: 500 } });
const result = await actions.create({
request: { formData: async () => makeFormData() },
fetch: vi.fn()
} as any);
expect(result.status).toBe(500);
});
});