cooked: validate draft save recipe id

Amolith created

Change summary

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

Detailed changes

internal/cooked/client.go 🔗

@@ -339,6 +339,9 @@ func (c *Client) SaveRecipeDraft(ctx context.Context, draftID, markdown string,
 	if err := c.doAuthenticated(ctx, http.MethodPost, path, body, decodeSave); err != nil {
 		return "", err
 	}
+	if strings.TrimSpace(response.RecipeID) == "" {
+		return "", fmt.Errorf("save recipe draft response missing recipe ID")
+	}
 
 	return response.RecipeID, nil
 }

internal/cooked/client_recipe_validation_test.go 🔗

@@ -7,6 +7,7 @@ package cooked
 import (
 	"context"
 	"net/http"
+	"strings"
 	"testing"
 )
 
@@ -67,6 +68,29 @@ func TestSearchRecipesACIDAPIClientResponses1RejectsRecipeCardsWithoutIDOrTitle(
 	}
 }
 
+// api-client.RESPONSES.1 recipes.SAVE.7 recipes.SAVE.9
+func TestSaveRecipeDraftACIDAPIClientResponses1RejectsMissingRecipeID(t *testing.T) {
+	var saveCalls int
+	client, closeServer := newTestClient(t, rawSaveRecipeDraftTestHandler(t, &saveCalls, `{}`))
+	defer closeServer()
+
+	_, err := client.SaveRecipeDraft(
+		context.Background(),
+		"draft/with space",
+		"# Pasta\n\n1. Boil pasta.",
+		4,
+	)
+	if err == nil {
+		t.Fatal("SaveRecipeDraft() error = nil, want missing recipe ID error")
+	}
+	if !strings.Contains(err.Error(), "missing recipe ID") {
+		t.Fatalf("SaveRecipeDraft() 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()
 
@@ -84,3 +108,25 @@ func rawRecipeListTestHandler(t *testing.T, path, body string) http.HandlerFunc
 		}
 	}
 }
+
+func rawSaveRecipeDraftTestHandler(t *testing.T, saveCalls *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/extract/draft%2Fwith%20space/save":
+			requireMethod(t, r, http.MethodPost, "save recipe draft")
+			requireSessionCookie(t, r)
+			requireRecipeContentRequest(t, r, "draft save")
+			(*saveCalls)++
+			w.Header().Set("Content-Type", "application/json")
+			if _, err := w.Write([]byte(body)); err != nil {
+				t.Fatalf("write raw save draft response: %v", err)
+			}
+		default:
+			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
+		}
+	}
+}