diff options
Diffstat (limited to 'internal/handlers/shopping_edit_test.go')
| -rw-r--r-- | internal/handlers/shopping_edit_test.go | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/internal/handlers/shopping_edit_test.go b/internal/handlers/shopping_edit_test.go new file mode 100644 index 0000000..6d862d7 --- /dev/null +++ b/internal/handlers/shopping_edit_test.go @@ -0,0 +1,238 @@ +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) + } +} + |
