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)
}
}
}