summaryrefslogtreecommitdiff
path: root/internal/handlers/shopping_edit_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/shopping_edit_test.go')
-rw-r--r--internal/handlers/shopping_edit_test.go238
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)
+ }
+}
+