From 609923daf31d83b76779f97ccb60b57b12df0578 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 10 Jun 2026 20:52:44 -0600 Subject: [PATCH] cooked: validate prepared save recipe id --- internal/cooked/client.go | 3 + .../cooked/client_recipe_validation_test.go | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/internal/cooked/client.go b/internal/cooked/client.go index 6b8cdc6f395f5baee108791d9d5f4c0dd6d7e38c..8cfdcb792081c3e41a9bff8ef328ee21ea62742e 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -320,6 +320,9 @@ func (c *Client) SavePreparedRecipe(ctx context.Context, title, markdown string, if err := c.doAuthenticated(ctx, http.MethodPost, "/api/recipe/import/save", body, decodeSave); err != nil { return "", err } + if strings.TrimSpace(response.RecipeID) == "" { + return "", fmt.Errorf("save prepared recipe response missing recipe ID") + } return response.RecipeID, nil } diff --git a/internal/cooked/client_recipe_validation_test.go b/internal/cooked/client_recipe_validation_test.go index 444d06fafc816a57a8d51bbc7d2bc0a5b5edafa3..793c9038f36b02643da6842c25240eb4b3359723 100644 --- a/internal/cooked/client_recipe_validation_test.go +++ b/internal/cooked/client_recipe_validation_test.go @@ -91,6 +91,41 @@ func TestSaveRecipeDraftACIDAPIClientResponses1RejectsMissingRecipeID(t *testing } } +// api-client.RESPONSES.1 recipes.SAVE.2 recipes.SAVE.9 +func TestSavePreparedRecipeACIDAPIClientResponses1RejectsMissingOrBlankRecipeID(t *testing.T) { + tests := []struct { + name string + body string + }{ + {name: "missing recipe ID", body: `{}`}, + {name: "blank recipe ID", body: `{"recipe-id":" "}`}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var saveCalls int + client, closeServer := newTestClient(t, rawSavePreparedRecipeTestHandler(t, &saveCalls, tt.body)) + defer closeServer() + + _, err := client.SavePreparedRecipe( + context.Background(), + "Pasta", + "# Pasta\n\n1. Boil pasta.", + 2, + ) + if err == nil { + t.Fatal("SavePreparedRecipe() error = nil, want missing recipe ID error") + } + if !strings.Contains(err.Error(), "missing recipe ID") { + t.Fatalf("SavePreparedRecipe() error = %v, want missing recipe ID error", err) + } + if saveCalls != 1 { + t.Fatalf("save calls = %d, want 1", saveCalls) + } + }) + } +} + func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc { t.Helper() @@ -109,6 +144,28 @@ func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc } } +func rawSavePreparedRecipeTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc { + t.Helper() + + return func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/public/login": + writeLoginResponse(t, w) + case "/api/recipe/import/save": + requireMethod(t, r, http.MethodPost, "save prepared recipe") + requireSessionCookie(t, r) + requireSavePreparedRecipeRequest(t, r) + (*saveCalls)++ + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write([]byte(body)); err != nil { + t.Fatalf("write raw save prepared response: %v", err) + } + default: + t.Fatalf("unexpected path %s", r.URL.Path) + } + } +} + func rawSaveRecipeDraftTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc { t.Helper()