package handlers import ( "context" "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" ) func TestParseUserItemID(t *testing.T) { cases := []struct { in string want int64 valid bool }{ {"user-42", 42, true}, {"user-1", 1, true}, {"5f2a91bc", 0, false}, // a PlanToEat id {"user-", 0, false}, // malformed {"user-abc", 0, false}, // non-numeric {"", 0, false}, } for _, c := range cases { got, ok := parseUserItemID(c.in) if ok != c.valid || got != c.want { t.Errorf("parseUserItemID(%q) = (%d, %v), want (%d, %v)", c.in, got, ok, c.want, c.valid) } } } func TestShoppingItemEdit_UserItemIsEditable(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() _ = h.store.SaveUserShoppingItemInSection("Milk", "Costco", "Dairy") items, _ := h.store.GetUserShoppingItems() id := items[0].ID req := withURLParam(httptest.NewRequest(http.MethodGet, "/shopping/item/user-1/edit", nil), "id", "user-"+strconv.FormatInt(id, 10)) rec := httptest.NewRecorder() h.HandleShoppingItemEdit(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } } // A PlanToEat item must not be presented as editable — the client is // read-only, so a save would silently do nothing. func TestShoppingItemEdit_PlanToEatItemIsReadOnly(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() req := withURLParam( httptest.NewRequest(http.MethodGet, "/shopping/item/pte-1/edit?source=plantoeat&name=Kale&store=Safeway", nil), "id", "pte-1") rec := httptest.NewRecorder() h.HandleShoppingItemEdit(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d", rec.Code) } } func TestShoppingItemUpdate_RenamesAndMoves(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() _ = h.store.AddShoppingStore("Safeway") _ = h.store.SaveUserShoppingItemInSection("Mlik", "Costco", "Dairy") items, _ := h.store.GetUserShoppingItems() id := items[0].ID form := url.Values{"name": {"Milk"}, "store": {"Safeway"}, "section": {""}} req := withURLParam( httptest.NewRequest(http.MethodPost, "/shopping/item/x", strings.NewReader(form.Encode())), "id", "user-"+strconv.FormatInt(id, 10)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() h.HandleShoppingItemUpdate(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } got, err := h.store.GetUserShoppingItem(id) if err != nil { t.Fatalf("get: %v", err) } if got.Name != "Milk" || got.Store != "Safeway" || got.Section != "" { t.Errorf("got %+v, want renamed and moved to Safeway with no section", got) } } // Editing is only offered for user items; a request naming anything else must // be refused rather than quietly doing nothing. func TestShoppingItemUpdate_RefusesNonUserItems(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() form := url.Values{"name": {"Kale"}, "store": {"Safeway"}} req := withURLParam( httptest.NewRequest(http.MethodPost, "/shopping/item/x", strings.NewReader(form.Encode())), "id", "pte-123") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() h.HandleShoppingItemUpdate(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("status %d, want 400", rec.Code) } } func TestShoppingItemDelete_RemovesUserItem(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() _ = h.store.SaveUserShoppingItem("Milk", "Costco") items, _ := h.store.GetUserShoppingItems() req := withURLParam(httptest.NewRequest(http.MethodDelete, "/shopping/item/x", nil), "id", "user-"+strconv.FormatInt(items[0].ID, 10)) rec := httptest.NewRecorder() h.HandleShoppingItemDelete(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } remaining, _ := h.store.GetUserShoppingItems() if len(remaining) != 0 { t.Errorf("got %d items, want 0", len(remaining)) } } // A PlanToEat item cannot be deleted upstream, so removal marks it checked, // which filters it out of the list. func TestShoppingItemDelete_HidesPlanToEatItem(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() req := withURLParam( httptest.NewRequest(http.MethodDelete, "/shopping/item/x?source=plantoeat", nil), "id", "pte-99") rec := httptest.NewRecorder() h.HandleShoppingItemDelete(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } checks, _ := h.store.GetShoppingItemChecks("plantoeat") if !checks["pte-99"] { t.Error("expected the item to be marked checked so it drops out of the list") } } // The registry's reason to exist: an empty store is still a destination. func TestAggregateShoppingLists_IncludesEmptyRegisteredStores(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() if err := h.store.AddShoppingStore("Hardware Store"); err != nil { t.Fatalf("add store: %v", err) } var found bool for _, st := range h.aggregateShoppingLists(context.Background()) { if st.Name == "Hardware Store" { found = true } } if !found { t.Error("a registered store with no items should still appear, or nothing can be moved into it") } } func TestShoppingStoreCreate(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() form := url.Values{"name": {"Hardware Store"}} req := httptest.NewRequest(http.MethodPost, "/shopping/stores", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() h.HandleShoppingStoreCreate(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } names, _ := h.store.ShoppingStoreNames() if len(names) != 1 || names[0] != "Hardware Store" { t.Errorf("got %v", names) } } func TestShoppingSectionCreate(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() _ = h.store.AddShoppingStore("Costco") form := url.Values{"store": {"Costco"}, "name": {"Produce"}} req := httptest.NewRequest(http.MethodPost, "/shopping/sections", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() h.HandleShoppingSectionCreate(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status %d: %s", rec.Code, rec.Body.String()) } stores, _ := h.store.GetShoppingStores() if len(stores) != 1 || len(stores[0].Sections) != 1 || stores[0].Sections[0].Name != "Produce" { t.Errorf("got %+v", stores) } } func TestShoppingSectionCreate_UnknownStore(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() form := url.Values{"store": {"Nowhere"}, "name": {"Produce"}} req := httptest.NewRequest(http.MethodPost, "/shopping/sections", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() h.HandleShoppingSectionCreate(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("status %d, want 404", rec.Code) } }