feat(recipes): return fail(422) when all ingredients filter to empty
Prevents a silent 400 from the backend when the user submits a form where every ingredient row has quantity <= 0 or blank name. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,13 @@ export const actions: Actions = {
|
|||||||
return fail(400, { error: 'Ungültige Formulardaten' });
|
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 api = apiClient(fetch);
|
||||||
const { error: apiError } = await api.PUT('/v1/recipes/{id}', {
|
const { error: apiError } = await api.PUT('/v1/recipes/{id}', {
|
||||||
params: { path: { id: params.id } },
|
params: { path: { id: params.id } },
|
||||||
@@ -76,14 +83,12 @@ export const actions: Actions = {
|
|||||||
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || null : null,
|
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || null : null,
|
||||||
effort,
|
effort,
|
||||||
heroImageUrl,
|
heroImageUrl,
|
||||||
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
ingredients: filteredIngredients.map((ing, i) => ({
|
||||||
.filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0)
|
newIngredientName: ing.name.trim(),
|
||||||
.map((ing, i) => ({
|
quantity: Number(ing.quantity),
|
||||||
newIngredientName: ing.name.trim(),
|
unit: ing.unit || '',
|
||||||
quantity: Number(ing.quantity),
|
sortOrder: i
|
||||||
unit: ing.unit || '',
|
})),
|
||||||
sortOrder: i
|
|
||||||
})),
|
|
||||||
steps: (parsedSteps as string[])
|
steps: (parsedSteps as string[])
|
||||||
.filter((s) => s?.trim())
|
.filter((s) => s?.trim())
|
||||||
.map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })),
|
.map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })),
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ describe('edit recipe page — update action', () => {
|
|||||||
name: 'Test Rezept',
|
name: 'Test Rezept',
|
||||||
effort: 'easy',
|
effort: 'easy',
|
||||||
tagIds: ['t1'],
|
tagIds: ['t1'],
|
||||||
ingredientsJson: '[]',
|
ingredientsJson: JSON.stringify([{ name: 'Spaghetti', quantity: 200, unit: 'g' }]),
|
||||||
stepsJson: '[]',
|
stepsJson: '[]',
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
@@ -236,6 +236,24 @@ describe('edit recipe page — update action', () => {
|
|||||||
expect(body.ingredients[0].newIngredientName).toBe('Spaghetti');
|
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 () => {
|
it('returns fail(500) when API returns error', async () => {
|
||||||
mockPut.mockResolvedValue({ error: { status: 500 } });
|
mockPut.mockResolvedValue({ error: { status: 500 } });
|
||||||
const result = await actions.update({
|
const result = await actions.update({
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export const actions: Actions = {
|
|||||||
return fail(400, { error: 'Ungültige Formulardaten' });
|
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 api = apiClient(fetch);
|
||||||
const { error: apiError } = await api.POST('/v1/recipes', {
|
const { error: apiError } = await api.POST('/v1/recipes', {
|
||||||
body: {
|
body: {
|
||||||
@@ -48,14 +55,12 @@ export const actions: Actions = {
|
|||||||
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || null : null,
|
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || null : null,
|
||||||
effort,
|
effort,
|
||||||
heroImageUrl,
|
heroImageUrl,
|
||||||
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
ingredients: filteredIngredients.map((ing, i) => ({
|
||||||
.filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0)
|
newIngredientName: ing.name.trim(),
|
||||||
.map((ing, i) => ({
|
quantity: Number(ing.quantity),
|
||||||
newIngredientName: ing.name.trim(),
|
unit: ing.unit || '',
|
||||||
quantity: Number(ing.quantity),
|
sortOrder: i
|
||||||
unit: ing.unit || '',
|
})),
|
||||||
sortOrder: i
|
|
||||||
})),
|
|
||||||
steps: (parsedSteps as string[])
|
steps: (parsedSteps as string[])
|
||||||
.filter((s) => s?.trim())
|
.filter((s) => s?.trim())
|
||||||
.map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })),
|
.map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })),
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe('new recipe page — create action', () => {
|
|||||||
name: 'Test Rezept',
|
name: 'Test Rezept',
|
||||||
effort: 'easy',
|
effort: 'easy',
|
||||||
tagIds: ['t1'],
|
tagIds: ['t1'],
|
||||||
ingredientsJson: '[]',
|
ingredientsJson: JSON.stringify([{ name: 'Spaghetti', quantity: 200, unit: 'g' }]),
|
||||||
stepsJson: '[]',
|
stepsJson: '[]',
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
@@ -189,6 +189,23 @@ describe('new recipe page — create action', () => {
|
|||||||
expect(body.ingredients[0].newIngredientName).toBe('Spaghetti');
|
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 () => {
|
it('returns fail(500) when API returns error', async () => {
|
||||||
mockPost.mockResolvedValue({ error: { status: 500 } });
|
mockPost.mockResolvedValue({ error: { status: 500 } });
|
||||||
const result = await actions.create({
|
const result = await actions.create({
|
||||||
|
|||||||
Reference in New Issue
Block a user