shopping-list: replace selection client

Amolith created

Change summary

internal/cooked/client.go      | 20 +++++++++++++++
internal/cooked/client_test.go | 48 ++++++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+)

Detailed changes

internal/cooked/client.go 🔗

@@ -167,6 +167,22 @@ func (c *Client) RemoveShoppingListProductGroups(ctx context.Context, ids []stri
 	return c.doAuthenticated(ctx, http.MethodDelete, "/api/user/{username}/shopping-list/product-groups", body, nil)
 }
 
+// ReplaceShoppingListSelection logs in when needed and replaces the selected shopping-list product groups.
+func (c *Client) ReplaceShoppingListSelection(ctx context.Context, ids []string) error {
+	body, err := json.Marshal(replaceShoppingListSelectionRequest{Selected: ids})
+	if err != nil {
+		return fmt.Errorf("encode Cooked shopping-list selection request: %w", err)
+	}
+
+	return c.doAuthenticated(
+		ctx,
+		http.MethodPut,
+		"/api/user/{username}/shopping-list/product-groups/selection",
+		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{}
@@ -422,6 +438,10 @@ type removeShoppingListProductGroupsRequest struct {
 	IDs []string `json:"ids"`
 }
 
+type replaceShoppingListSelectionRequest struct {
+	Selected []string `json:"selected"`
+}
+
 type recipeListResponse struct {
 	Recipes []RecipeCard `json:"recipes"`
 }

internal/cooked/client_test.go 🔗

@@ -229,6 +229,21 @@ func TestRemoveShoppingListProductGroupsACIDShoppingListRemove1RemovesProductGro
 	}
 }
 
+func TestReplaceShoppingListSelectionACIDShoppingListSelection1And8ReplacesSelectedSet(t *testing.T) {
+	var replaceCalls int
+	client, closeServer := newTestClient(t, replaceShoppingListSelectionTestHandler(t, &replaceCalls))
+	defer closeServer()
+
+	err := client.ReplaceShoppingListSelection(context.Background(), []string{"pasta", "tomato"})
+	if err != nil {
+		t.Fatalf("ReplaceShoppingListSelection() error = %v", err)
+	}
+
+	if replaceCalls != 1 {
+		t.Fatalf("replace calls = %d, want 1", replaceCalls)
+	}
+}
+
 func TestUserPathEscapesAuthenticatedUsername(t *testing.T) {
 	client := &Client{authenticatedUsername: "user/name"}
 
@@ -531,6 +546,25 @@ func removeShoppingListProductGroupsTestHandler(t *testing.T, removeCalls *int)
 	}
 }
 
+func replaceShoppingListSelectionTestHandler(t *testing.T, replaceCalls *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/selection":
+			requireMethod(t, r, http.MethodPut, "replace shopping-list selection")
+			requireSessionCookie(t, r)
+			requireReplaceShoppingListSelectionRequest(t, r)
+			(*replaceCalls)++
+			w.WriteHeader(http.StatusNoContent)
+		default:
+			t.Fatalf("unexpected path %s", r.URL.EscapedPath())
+		}
+	}
+}
+
 func requireRemoveShoppingListProductGroupsRequest(t *testing.T, r *http.Request) {
 	t.Helper()
 
@@ -545,6 +579,20 @@ func requireRemoveShoppingListProductGroupsRequest(t *testing.T, r *http.Request
 	}
 }
 
+func requireReplaceShoppingListSelectionRequest(t *testing.T, r *http.Request) {
+	t.Helper()
+
+	var request struct {
+		Selected []string `json:"selected"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
+		t.Fatalf("decode replace shopping-list selection request: %v", err)
+	}
+	if len(request.Selected) != 2 || request.Selected[0] != "pasta" || request.Selected[1] != "tomato" {
+		t.Fatalf("selected IDs = %#v, want pasta and tomato", request.Selected)
+	}
+}
+
 func requirePreviewRecipeTextRequest(t *testing.T, r *http.Request) {
 	t.Helper()