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