From 22072d6080e928d45a4445c8c7457d532c62be6d Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 10 Jun 2026 20:57:21 -0600 Subject: [PATCH] cooked: validate recipe content responses --- internal/cooked/client.go | 14 +++++ .../cooked/client_recipe_validation_test.go | 54 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/internal/cooked/client.go b/internal/cooked/client.go index 8cfdcb792081c3e41a9bff8ef328ee21ea62742e..e2fed2cd0f0d86fa60c6fe3a1fef444655af1c66 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -284,6 +284,9 @@ func (c *Client) ReadRecipeContent(ctx context.Context, recipeID string) (Recipe if err := c.doAuthenticated(ctx, http.MethodGet, path, nil, decodeContent); err != nil { return RecipeContent{}, err } + if err := validateRecipeContent(content); err != nil { + return RecipeContent{}, err + } return content, nil } @@ -422,6 +425,17 @@ func validateRecipeCards(recipes []RecipeCard) error { return nil } +func validateRecipeContent(content RecipeContent) error { + if strings.TrimSpace(content.Content) == "" { + return fmt.Errorf("cooked recipe content response missing content") + } + if content.Portions < 1 { + return fmt.Errorf("cooked recipe content response missing or invalid portions") + } + + return nil +} + func jsonNumberToInt(number json.Number) (int, error) { text := number.String() if text == "" { diff --git a/internal/cooked/client_recipe_validation_test.go b/internal/cooked/client_recipe_validation_test.go index 793c9038f36b02643da6842c25240eb4b3359723..ea00be12f902902b9ac63219d97b56d61daa87e2 100644 --- a/internal/cooked/client_recipe_validation_test.go +++ b/internal/cooked/client_recipe_validation_test.go @@ -126,6 +126,39 @@ func TestSavePreparedRecipeACIDAPIClientResponses1RejectsMissingOrBlankRecipeID( } } +// api-client.RESPONSES.1 recipes.READ.7 recipes.READ.8 +func TestReadRecipeContentACIDAPIClientResponses1RejectsMissingContentOrPortions(t *testing.T) { + tests := []struct { + name string + body string + wantErr string + }{ + {name: "missing content", body: `{"portions":2}`, wantErr: "missing content"}, + {name: "blank content", body: `{"content":" ","portions":2}`, wantErr: "missing content"}, + {name: "missing portions", body: `{"content":"# Pasta"}`, wantErr: "missing or invalid portions"}, + {name: "zero portions", body: `{"content":"# Pasta","portions":0}`, wantErr: "missing or invalid portions"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var contentCalls int + client, closeServer := newTestClient(t, rawRecipeContentTestHandler(t, &contentCalls, tt.body)) + defer closeServer() + + _, err := client.ReadRecipeContent(context.Background(), "recipe/with space") + if err == nil { + t.Fatal("ReadRecipeContent() error = nil, want invalid recipe content error") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("ReadRecipeContent() error = %v, want %q", err, tt.wantErr) + } + if contentCalls != 1 { + t.Fatalf("content calls = %d, want 1", contentCalls) + } + }) + } +} + func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc { t.Helper() @@ -144,6 +177,27 @@ func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc } } +func rawRecipeContentTestHandler(t *testing.T, contentCalls *int, body string) http.HandlerFunc { + t.Helper() + + return func(w http.ResponseWriter, r *http.Request) { + switch r.URL.EscapedPath() { + case "/api/public/login": + writeLoginResponse(t, w) + case "/api/recipe/recipe%2Fwith%20space/content": + requireMethod(t, r, http.MethodGet, "content") + requireSessionCookie(t, r) + (*contentCalls)++ + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write([]byte(body)); err != nil { + t.Fatalf("write raw recipe content response: %v", err) + } + default: + t.Fatalf("unexpected path %s", r.URL.EscapedPath()) + } + } +} + func rawSavePreparedRecipeTestHandler(t *testing.T, saveCalls *int, body string) http.HandlerFunc { t.Helper()