summaryrefslogtreecommitdiff
path: root/internal/handlers/shopping_sources_test.go
blob: 517f4b4becb0207983bd5da5e47aff8229ca86ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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)
		}
	}
}