recipes: save extraction draft client

Amolith created

Change summary

internal/cooked/client.go      | 23 +++++++++++++++-
internal/cooked/client_test.go | 50 +++++++++++++++++++++++++++++++----
2 files changed, 65 insertions(+), 8 deletions(-)

Detailed changes

internal/cooked/client.go 🔗

@@ -295,9 +295,28 @@ func (c *Client) SavePreparedRecipe(ctx context.Context, title, markdown string,
 	return response.RecipeID, nil
 }
 
+// SaveRecipeDraft logs in when needed and saves a reviewed URL import draft.
+func (c *Client) SaveRecipeDraft(ctx context.Context, draftID, markdown string, portions int) (string, error) {
+	body, err := json.Marshal(recipeContentRequest{Description: markdown, Portions: portions})
+	if err != nil {
+		return "", fmt.Errorf("encode Cooked recipe draft save request: %w", err)
+	}
+
+	var response saveRecipeResponse
+	decodeSave := func(decoder *json.Decoder) error {
+		return decoder.Decode(&response)
+	}
+	path := "/api/extract/" + url.PathEscape(draftID) + "/save"
+	if err := c.doAuthenticated(ctx, http.MethodPost, path, body, decodeSave); err != nil {
+		return "", err
+	}
+
+	return response.RecipeID, nil
+}
+
 // UpdateRecipeContent logs in when needed and updates an existing recipe's content.
 func (c *Client) UpdateRecipeContent(ctx context.Context, recipeID, markdown string, portions int) error {
-	body, err := json.Marshal(updateRecipeContentRequest{Description: markdown, Portions: portions})
+	body, err := json.Marshal(recipeContentRequest{Description: markdown, Portions: portions})
 	if err != nil {
 		return fmt.Errorf("encode Cooked recipe update request: %w", err)
 	}
@@ -516,7 +535,7 @@ type savePreparedRecipeRequest struct {
 	Portions    int    `json:"portions"`
 }
 
-type updateRecipeContentRequest struct {
+type recipeContentRequest struct {
 	Description string `json:"description"`
 	Portions    int    `json:"portions"`
 }

internal/cooked/client_test.go 🔗

@@ -144,6 +144,25 @@ func TestSavePreparedRecipeACIDRecipesSave2And9And13SavesMarkdownAsDescription(t
 	}
 }
 
+// recipes.SAVE.7 recipes.SAVE.9 recipes.SAVE.13
+func TestSaveRecipeDraftACIDRecipesSave7And9And13SavesReviewedExtractionDraft(t *testing.T) {
+	var saveCalls int
+	client, closeServer := newTestClient(t, saveRecipeDraftTestHandler(t, &saveCalls))
+	defer closeServer()
+
+	recipeID, err := client.SaveRecipeDraft(context.Background(), "draft/with space", "# Pasta\n\n1. Boil pasta.", 4)
+	if err != nil {
+		t.Fatalf("SaveRecipeDraft() error = %v", err)
+	}
+
+	if saveCalls != 1 {
+		t.Fatalf("save calls = %d, want 1", saveCalls)
+	}
+	if recipeID != "recipe-1" {
+		t.Fatalf("recipe ID = %q, want recipe-1", recipeID)
+	}
+}
+
 func TestUpdateRecipeContentACIDRecipesSave8And9And13UpdatesExistingRecipe(t *testing.T) {
 	var updateCalls int
 	client, closeServer := newTestClient(t, updateRecipeContentTestHandler(t, &updateCalls))
@@ -492,6 +511,25 @@ func savePreparedRecipeTestHandler(t *testing.T, saveCalls *int) http.HandlerFun
 	}
 }
 
+func saveRecipeDraftTestHandler(t *testing.T, saveCalls *int) 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)++
+			writeJSON(t, w, saveRecipeResponse{RecipeID: "recipe-1"})
+		default:
+			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
+		}
+	}
+}
+
 func updateRecipeContentTestHandler(t *testing.T, updateCalls *int) http.HandlerFunc {
 	t.Helper()
 
@@ -502,7 +540,7 @@ func updateRecipeContentTestHandler(t *testing.T, updateCalls *int) http.Handler
 		case "/api/recipe/recipe%2Fwith%20space/content":
 			requireMethod(t, r, http.MethodPost, "update recipe content")
 			requireSessionCookie(t, r)
-			requireUpdateRecipeContentRequest(t, r)
+			requireRecipeContentRequest(t, r, "update")
 			(*updateCalls)++
 			w.WriteHeader(http.StatusNoContent)
 		default:
@@ -760,18 +798,18 @@ func requireSavePreparedRecipeRequest(t *testing.T, r *http.Request) {
 	}
 }
 
-func requireUpdateRecipeContentRequest(t *testing.T, r *http.Request) {
+func requireRecipeContentRequest(t *testing.T, r *http.Request, description string) {
 	t.Helper()
 
-	var request updateRecipeContentRequest
+	var request recipeContentRequest
 	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
-		t.Fatalf("decode update recipe content request: %v", err)
+		t.Fatalf("decode %s recipe content request: %v", description, err)
 	}
 	if request.Description != "# Pasta\n\n1. Boil pasta." {
-		t.Fatalf("update description = %q, want markdown", request.Description)
+		t.Fatalf("%s description = %q, want markdown", description, request.Description)
 	}
 	if request.Portions != 4 {
-		t.Fatalf("update portions = %d, want 4", request.Portions)
+		t.Fatalf("%s portions = %d, want 4", description, request.Portions)
 	}
 }