cooked: validate prepared save recipe id

Amolith created

Change summary

internal/cooked/client.go                        |  3 
internal/cooked/client_recipe_validation_test.go | 57 ++++++++++++++++++
2 files changed, 60 insertions(+)

Detailed changes

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
 }

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()