diff --git a/frontend/src/routes/(app)/recipes/[id]/edit/+page.server.ts b/frontend/src/routes/(app)/recipes/[id]/edit/+page.server.ts index b062dba..9faf620 100644 --- a/frontend/src/routes/(app)/recipes/[id]/edit/+page.server.ts +++ b/frontend/src/routes/(app)/recipes/[id]/edit/+page.server.ts @@ -67,6 +67,13 @@ export const actions: Actions = { return fail(400, { error: 'Ungültige Formulardaten' }); } + const filteredIngredients = ( + parsedIngredients as { name: string; quantity: string; unit: string }[] + ).filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0); + + if (!filteredIngredients.length) + return fail(422, { error: 'Mindestens eine gültige Zutat ist erforderlich' }); + const api = apiClient(fetch); const { error: apiError } = await api.PUT('/v1/recipes/{id}', { params: { path: { id: params.id } }, @@ -76,14 +83,12 @@ export const actions: Actions = { cookTimeMin: cookTimeMin ? Number(cookTimeMin) || null : null, effort, heroImageUrl, - ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[]) - .filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0) - .map((ing, i) => ({ - newIngredientName: ing.name.trim(), - quantity: Number(ing.quantity), - unit: ing.unit || '', - sortOrder: i - })), + ingredients: filteredIngredients.map((ing, i) => ({ + newIngredientName: ing.name.trim(), + quantity: Number(ing.quantity), + unit: ing.unit || '', + sortOrder: i + })), steps: (parsedSteps as string[]) .filter((s) => s?.trim()) .map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })), diff --git a/frontend/src/routes/(app)/recipes/[id]/edit/page.server.test.ts b/frontend/src/routes/(app)/recipes/[id]/edit/page.server.test.ts index 5e7e1ef..564eab6 100644 --- a/frontend/src/routes/(app)/recipes/[id]/edit/page.server.test.ts +++ b/frontend/src/routes/(app)/recipes/[id]/edit/page.server.test.ts @@ -84,7 +84,7 @@ describe('edit recipe page — update action', () => { name: 'Test Rezept', effort: 'easy', tagIds: ['t1'], - ingredientsJson: '[]', + ingredientsJson: JSON.stringify([{ name: 'Spaghetti', quantity: 200, unit: 'g' }]), stepsJson: '[]', ...overrides }; @@ -236,6 +236,24 @@ describe('edit recipe page — update action', () => { expect(body.ingredients[0].newIngredientName).toBe('Spaghetti'); }); + it('returns fail(422) when all ingredients filter to empty after quantity check', async () => { + const result = await actions.update({ + request: { + formData: async () => + makeFormData({ + ingredientsJson: JSON.stringify([ + { name: 'Salt', quantity: 0, unit: 'tsp' }, + { name: '', quantity: 100, unit: 'g' } + ]) + }) + }, + fetch: vi.fn(), + params: { id: 'r1' } + } as any); + expect(result.status).toBe(422); + expect(result.data.error).toMatch(/zutat/i); + }); + it('returns fail(500) when API returns error', async () => { mockPut.mockResolvedValue({ error: { status: 500 } }); const result = await actions.update({ diff --git a/frontend/src/routes/(app)/recipes/new/+page.server.ts b/frontend/src/routes/(app)/recipes/new/+page.server.ts index c1f2e30..b3c8e56 100644 --- a/frontend/src/routes/(app)/recipes/new/+page.server.ts +++ b/frontend/src/routes/(app)/recipes/new/+page.server.ts @@ -40,6 +40,13 @@ export const actions: Actions = { return fail(400, { error: 'Ungültige Formulardaten' }); } + const filteredIngredients = ( + parsedIngredients as { name: string; quantity: string; unit: string }[] + ).filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0); + + if (!filteredIngredients.length) + return fail(422, { error: 'Mindestens eine gültige Zutat ist erforderlich' }); + const api = apiClient(fetch); const { error: apiError } = await api.POST('/v1/recipes', { body: { @@ -48,14 +55,12 @@ export const actions: Actions = { cookTimeMin: cookTimeMin ? Number(cookTimeMin) || null : null, effort, heroImageUrl, - ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[]) - .filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0) - .map((ing, i) => ({ - newIngredientName: ing.name.trim(), - quantity: Number(ing.quantity), - unit: ing.unit || '', - sortOrder: i - })), + ingredients: filteredIngredients.map((ing, i) => ({ + newIngredientName: ing.name.trim(), + quantity: Number(ing.quantity), + unit: ing.unit || '', + sortOrder: i + })), steps: (parsedSteps as string[]) .filter((s) => s?.trim()) .map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })), diff --git a/frontend/src/routes/(app)/recipes/new/page.server.test.ts b/frontend/src/routes/(app)/recipes/new/page.server.test.ts index a7fcc4d..3c7bb72 100644 --- a/frontend/src/routes/(app)/recipes/new/page.server.test.ts +++ b/frontend/src/routes/(app)/recipes/new/page.server.test.ts @@ -62,7 +62,7 @@ describe('new recipe page — create action', () => { name: 'Test Rezept', effort: 'easy', tagIds: ['t1'], - ingredientsJson: '[]', + ingredientsJson: JSON.stringify([{ name: 'Spaghetti', quantity: 200, unit: 'g' }]), stepsJson: '[]', ...overrides }; @@ -189,6 +189,23 @@ describe('new recipe page — create action', () => { expect(body.ingredients[0].newIngredientName).toBe('Spaghetti'); }); + it('returns fail(422) when all ingredients filter to empty after quantity check', async () => { + const result = await actions.create({ + request: { + formData: async () => + makeFormData({ + ingredientsJson: JSON.stringify([ + { name: 'Salt', quantity: 0, unit: 'tsp' }, + { name: '', quantity: 100, unit: 'g' } + ]) + }) + }, + fetch: vi.fn() + } as any); + expect(result.status).toBe(422); + expect(result.data.error).toMatch(/zutat/i); + }); + it('returns fail(500) when API returns error', async () => { mockPost.mockResolvedValue({ error: { status: 500 } }); const result = await actions.create({