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{``, ``} { if !strings.Contains(body, want) { t.Errorf("missing %q in %q", want, body) } } }