diff options
Diffstat (limited to 'internal/handlers/shopping_sources_test.go')
| -rw-r--r-- | internal/handlers/shopping_sources_test.go | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/internal/handlers/shopping_sources_test.go b/internal/handlers/shopping_sources_test.go new file mode 100644 index 0000000..517f4b4 --- /dev/null +++ b/internal/handlers/shopping_sources_test.go @@ -0,0 +1,159 @@ +package handlers + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "task-dashboard/internal/models" +) + +// The Trello "Shopping" board is no longer a source for the shopping list. It +// had become a dumping ground, its items were read-only in doot (no rename, no +// move, no delete — only hide), and its list names were doubling as an +// accidental store registry. The two groups worth keeping were imported into +// user items by migration 032; the rest simply stop being displayed. The cards +// themselves are untouched in Trello. +func TestAggregateShoppingLists_IgnoresTrelloShoppingBoard(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + if err := h.store.SaveBoards([]models.Board{{ + ID: "shop-board", + Name: "Shopping", + Cards: []models.Card{ + {ID: "c1", Name: "Broccoli", ListName: ""}, + {ID: "c2", Name: "Wasp traps", ListName: "Hardware Store"}, + {ID: "c3", Name: "Pearl Waffles", ListName: "Costco"}, + }, + }}); err != nil { + t.Fatalf("seeding boards: %v", err) + } + + stores := h.aggregateShoppingLists(context.Background()) + + for _, st := range stores { + for _, cat := range st.Categories { + for _, it := range cat.Items { + if it.Source == "trello" { + t.Errorf("trello item %q leaked into the shopping list (store %q)", it.Name, st.Name) + } + } + } + } +} + +// User-added items are still aggregated — this is the source that remains, so +// a regression here would empty the list entirely. +func TestAggregateShoppingLists_IncludesUserItems(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + if err := h.store.SaveUserShoppingItem("Milk", "Costco"); err != nil { + t.Fatalf("saving user item: %v", err) + } + + stores := h.aggregateShoppingLists(context.Background()) + + var found bool + for _, st := range stores { + for _, cat := range st.Categories { + for _, it := range cat.Items { + if it.Name == "Milk" && it.Source == "user" && st.Name == "Costco" { + found = true + } + } + } + } + if !found { + t.Errorf("user item not found in aggregated list; got %+v", stores) + } +} + +// A Shopping board with cards must not conjure stores into existence. Store +// names used to come partly from Trello list names, which is why removing the +// source changes which stores exist at all. +func TestAggregateShoppingLists_TrelloListsDoNotCreateStores(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + if err := h.store.SaveBoards([]models.Board{{ + ID: "shop-board", + Name: "Shopping", + Cards: []models.Card{ + {ID: "c1", Name: "Gas valve", ListName: "Hardware Store"}, + }, + }}); err != nil { + t.Fatalf("seeding boards: %v", err) + } + + for _, st := range h.aggregateShoppingLists(context.Background()) { + if st.Name == "Hardware Store" { + t.Error(`"Hardware Store" store exists only because of a Trello list name`) + } + } +} + +// The quick-action modal's Shopping tab posts source=shopping. It used to post +// source=trello with a list_id, which after the Trello removal would have +// created a card nobody could see. +func TestUnifiedAdd_ShoppingSourceCreatesUserItem(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + form := url.Values{"title": {"Sponges"}, "source": {"shopping"}, "store": {"Costco"}} + req := httptest.NewRequest(http.MethodPost, "/unified-add", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + + h.HandleUnifiedAdd(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("got status %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + items, err := h.store.GetUserShoppingItems() + if err != nil { + t.Fatalf("reading items: %v", err) + } + if len(items) != 1 || items[0].Name != "Sponges" || items[0].Store != "Costco" { + t.Errorf("got %+v, want one Sponges/Costco item", items) + } +} + +func TestUnifiedAdd_ShoppingRequiresAStore(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + form := url.Values{"title": {"Sponges"}, "source": {"shopping"}} + req := httptest.NewRequest(http.MethodPost, "/unified-add", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + + h.HandleUnifiedAdd(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Errorf("got status %d, want 400 when no store is given", rec.Code) + } +} + +// The store picker must offer the stores that actually exist. +func TestHandleGetShoppingStores_ReturnsOptions(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + _ = h.store.SaveUserShoppingItem("Milk", "Costco") + _ = h.store.SaveUserShoppingItem("Nails", "Hardware Store") + + rec := httptest.NewRecorder() + h.HandleGetShoppingStores(rec, httptest.NewRequest(http.MethodGet, "/partials/shopping-stores", nil)) + + body := rec.Body.String() + for _, want := range []string{`<option value="Costco">Costco</option>`, `<option value="Hardware Store">Hardware Store</option>`} { + if !strings.Contains(body, want) { + t.Errorf("missing %q in %q", want, body) + } + } +} |
