diff --git a/internal/cooked/client.go b/internal/cooked/client.go index 722dc78194d628b9438345b5f0ab55276e4cee57..8141aa02535fff0bdde7a65fd3cf568a4f8d22ea 100644 --- a/internal/cooked/client.go +++ b/internal/cooked/client.go @@ -62,6 +62,14 @@ type AddShoppingListResult struct { Ingredients []string } +// ShoppingListProductGroupUpdate is a full shopping-list product-group update. +type ShoppingListProductGroupUpdate struct { + Name string + Quantity string + AisleID string + Selected bool +} + // RecipeCard is a saved recipe summary returned by Cooked list and search endpoints. type RecipeCard struct { ID string `json:"id"` @@ -183,6 +191,22 @@ func (c *Client) ReplaceShoppingListSelection(ctx context.Context, ids []string) ) } +// UpdateShoppingListProductGroup logs in when needed and updates a shopping-list product group. +func (c *Client) UpdateShoppingListProductGroup( + ctx context.Context, + productGroupID string, + update ShoppingListProductGroupUpdate, +) error { + body, err := json.Marshal(updateShoppingListProductGroupRequest(update)) + if err != nil { + return fmt.Errorf("encode Cooked shopping-list product-group update request: %w", err) + } + + path := "/api/user/{username}/shopping-list/product-groups/" + url.PathEscape(productGroupID) + + return c.doAuthenticated(ctx, http.MethodPut, path, body, nil) +} + // ListRecipes logs in when needed and returns one page of saved recipes. func (c *Client) ListRecipes(ctx context.Context, page, limit int) ([]RecipeCard, error) { query := url.Values{} @@ -442,6 +466,13 @@ type replaceShoppingListSelectionRequest struct { Selected []string `json:"selected"` } +type updateShoppingListProductGroupRequest struct { + Name string `json:"name"` + Quantity string `json:"quantity"` + AisleID string `json:"aisle-id"` + Selected bool `json:"selected"` +} + type recipeListResponse struct { Recipes []RecipeCard `json:"recipes"` } diff --git a/internal/cooked/client_test.go b/internal/cooked/client_test.go index 29bc27e12ad009cfb6e6d57c17668699a8bb8561..a9db0b0b415b438db6403c6f5534fb951ae01bb8 100644 --- a/internal/cooked/client_test.go +++ b/internal/cooked/client_test.go @@ -244,6 +244,30 @@ func TestReplaceShoppingListSelectionACIDShoppingListSelection1And8ReplacesSelec } } +func TestUpdateShoppingListProductGroupACIDShoppingListUpdateItem1UpdatesProductGroup(t *testing.T) { + var updateCalls int + client, closeServer := newTestClient(t, updateShoppingListProductGroupTestHandler(t, &updateCalls)) + defer closeServer() + + err := client.UpdateShoppingListProductGroup( + context.Background(), + "product/with space", + ShoppingListProductGroupUpdate{ + Name: "Pasta", + Quantity: "250g", + AisleID: "pantry", + Selected: false, + }, + ) + if err != nil { + t.Fatalf("UpdateShoppingListProductGroup() error = %v", err) + } + + if updateCalls != 1 { + t.Fatalf("update calls = %d, want 1", updateCalls) + } +} + func TestUserPathEscapesAuthenticatedUsername(t *testing.T) { client := &Client{authenticatedUsername: "user/name"} @@ -565,6 +589,25 @@ func replaceShoppingListSelectionTestHandler(t *testing.T, replaceCalls *int) ht } } +func updateShoppingListProductGroupTestHandler(t *testing.T, updateCalls *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/user/returned-user/shopping-list/product-groups/product%2Fwith%20space": + requireMethod(t, r, http.MethodPut, "update shopping-list product group") + requireSessionCookie(t, r) + requireUpdateShoppingListProductGroupRequest(t, r) + (*updateCalls)++ + w.WriteHeader(http.StatusNoContent) + default: + t.Fatalf("unexpected path %s", r.URL.EscapedPath()) + } + } +} + func requireRemoveShoppingListProductGroupsRequest(t *testing.T, r *http.Request) { t.Helper() @@ -593,6 +636,29 @@ func requireReplaceShoppingListSelectionRequest(t *testing.T, r *http.Request) { } } +func requireUpdateShoppingListProductGroupRequest(t *testing.T, r *http.Request) { + t.Helper() + + var request struct { + Name string `json:"name"` + Quantity string `json:"quantity"` + AisleID string `json:"aisle-id"` + Selected *bool `json:"selected"` + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Fatalf("decode update shopping-list product group request: %v", err) + } + if request.Name != "Pasta" || request.Quantity != "250g" || request.AisleID != "pantry" { + t.Fatalf("update request = %#v, want configured name, quantity, and aisle", request) + } + if request.Selected == nil { + t.Fatal("update request selected missing, want explicit false") + } + if *request.Selected { + t.Fatal("update request selected = true, want false") + } +} + func requirePreviewRecipeTextRequest(t *testing.T, r *http.Request) { t.Helper()