diff options
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/shopping.go | 82 | ||||
| -rw-r--r-- | internal/store/shopping_test.go | 151 | ||||
| -rw-r--r-- | internal/store/sqlite.go | 79 | ||||
| -rw-r--r-- | internal/store/sqlite_test.go | 142 |
4 files changed, 233 insertions, 221 deletions
diff --git a/internal/store/shopping.go b/internal/store/shopping.go new file mode 100644 index 0000000..40571ac --- /dev/null +++ b/internal/store/shopping.go @@ -0,0 +1,82 @@ +package store + +import "time" + +type UserShoppingItem struct { + ID int64 + Name string + Store string + Checked bool + CreatedAt time.Time +} + +// SaveUserShoppingItem saves a new user shopping item +func (s *Store) SaveUserShoppingItem(name, store string) error { + _, err := s.db.Exec(`INSERT INTO user_shopping_items (name, store) VALUES (?, ?)`, name, store) + return err +} + +// GetUserShoppingItems retrieves all user shopping items +func (s *Store) GetUserShoppingItems() ([]UserShoppingItem, error) { + rows, err := s.db.Query(`SELECT id, name, store, checked, created_at FROM user_shopping_items ORDER BY store, created_at DESC`) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + var items []UserShoppingItem + for rows.Next() { + var item UserShoppingItem + if err := rows.Scan(&item.ID, &item.Name, &item.Store, &item.Checked, &item.CreatedAt); err != nil { + return nil, err + } + items = append(items, item) + } + return items, rows.Err() +} + +// ToggleUserShoppingItem toggles the checked state of a user shopping item +func (s *Store) ToggleUserShoppingItem(id int64, checked bool) error { + _, err := s.db.Exec(`UPDATE user_shopping_items SET checked = ? WHERE id = ?`, checked, id) + return err +} + +// DeleteUserShoppingItem removes a user shopping item +func (s *Store) DeleteUserShoppingItem(id int64) error { + _, err := s.db.Exec(`DELETE FROM user_shopping_items WHERE id = ?`, id) + return err +} + +// SetShoppingItemChecked sets the checked state for an external shopping item +func (s *Store) SetShoppingItemChecked(source, itemID string, checked bool) error { + checkedInt := 0 + if checked { + checkedInt = 1 + } + _, err := s.db.Exec(` + INSERT INTO shopping_item_checks (source, item_id, checked, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(source, item_id) DO UPDATE SET checked = ?, updated_at = CURRENT_TIMESTAMP + `, source, itemID, checkedInt, checkedInt) + return err +} + +// GetShoppingItemChecks returns a map of item_id -> checked for a given source +func (s *Store) GetShoppingItemChecks(source string) (map[string]bool, error) { + rows, err := s.db.Query(`SELECT item_id, checked FROM shopping_item_checks WHERE source = ?`, source) + if err != nil { + return nil, err + } + defer func() { _ = rows.Close() }() + + checks := make(map[string]bool) + for rows.Next() { + var itemID string + var checked int + if err := rows.Scan(&itemID, &checked); err != nil { + return nil, err + } + checks[itemID] = checked == 1 + } + return checks, rows.Err() +} diff --git a/internal/store/shopping_test.go b/internal/store/shopping_test.go new file mode 100644 index 0000000..01f106c --- /dev/null +++ b/internal/store/shopping_test.go @@ -0,0 +1,151 @@ +package store + +import ( + "database/sql" + "path/filepath" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func setupTestStoreWithShopping(t *testing.T) *Store { + t.Helper() + + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test.db") + + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + t.Fatalf("Failed to open test database: %v", err) + } + + db.SetMaxOpenConns(1) + store := &Store{db: db} + + schema := ` + CREATE TABLE IF NOT EXISTS user_shopping_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + store TEXT NOT NULL, + checked INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS shopping_item_checks ( + source TEXT NOT NULL, + item_id TEXT NOT NULL, + checked INTEGER DEFAULT 0, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (source, item_id) + ); + ` + if _, err := db.Exec(schema); err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + return store +} + +func TestUserShoppingItems_CRUD(t *testing.T) { + store := setupTestStoreWithShopping(t) + defer func() { _ = store.Close() }() + + // Save items + if err := store.SaveUserShoppingItem("Milk", "Costco"); err != nil { + t.Fatalf("Failed to save item: %v", err) + } + if err := store.SaveUserShoppingItem("Bread", "Safeway"); err != nil { + t.Fatalf("Failed to save second item: %v", err) + } + + // Get items + items, err := store.GetUserShoppingItems() + if err != nil { + t.Fatalf("Failed to get items: %v", err) + } + if len(items) != 2 { + t.Errorf("Expected 2 items, got %d", len(items)) + } + + // Verify item data + var milkItem UserShoppingItem + for _, item := range items { + if item.Name == "Milk" { + milkItem = item + break + } + } + if milkItem.Name != "Milk" { + t.Error("Could not find Milk item") + } + if milkItem.Store != "Costco" { + t.Errorf("Expected store 'Costco', got '%s'", milkItem.Store) + } + if milkItem.Checked { + t.Error("New item should not be checked") + } + + // Toggle item + if err := store.ToggleUserShoppingItem(milkItem.ID, true); err != nil { + t.Fatalf("Failed to toggle item: %v", err) + } + + items, _ = store.GetUserShoppingItems() + for _, item := range items { + if item.ID == milkItem.ID && !item.Checked { + t.Error("Item should be checked after toggle") + } + } + + // Delete item + if err := store.DeleteUserShoppingItem(milkItem.ID); err != nil { + t.Fatalf("Failed to delete item: %v", err) + } + + items, _ = store.GetUserShoppingItems() + if len(items) != 1 { + t.Errorf("Expected 1 item after delete, got %d", len(items)) + } +} + +func TestShoppingItemChecks_ExternalSources(t *testing.T) { + store := setupTestStoreWithShopping(t) + defer func() { _ = store.Close() }() + + // Set checked for trello item + if err := store.SetShoppingItemChecked("trello", "card-123", true); err != nil { + t.Fatalf("Failed to set trello checked: %v", err) + } + + // Set checked for plantoeat item + if err := store.SetShoppingItemChecked("plantoeat", "pte-456", true); err != nil { + t.Fatalf("Failed to set plantoeat checked: %v", err) + } + + // Get trello checks + trelloChecks, err := store.GetShoppingItemChecks("trello") + if err != nil { + t.Fatalf("Failed to get trello checks: %v", err) + } + if !trelloChecks["card-123"] { + t.Error("Expected trello card to be checked") + } + + // Get plantoeat checks + pteChecks, err := store.GetShoppingItemChecks("plantoeat") + if err != nil { + t.Fatalf("Failed to get plantoeat checks: %v", err) + } + if !pteChecks["pte-456"] { + t.Error("Expected plantoeat item to be checked") + } + + // Uncheck trello item + if err := store.SetShoppingItemChecked("trello", "card-123", false); err != nil { + t.Fatalf("Failed to uncheck trello item: %v", err) + } + + trelloChecks, _ = store.GetShoppingItemChecks("trello") + if trelloChecks["card-123"] { + t.Error("Trello item should be unchecked after update") + } +} diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index e8c9eac..1662c6a 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -386,85 +386,6 @@ func (s *Store) ClearSyncToken(service string) error { } // UserShoppingItem represents a user-added shopping item -type UserShoppingItem struct { - ID int64 - Name string - Store string - Checked bool - CreatedAt time.Time -} - -// SaveUserShoppingItem saves a new user shopping item -func (s *Store) SaveUserShoppingItem(name, store string) error { - _, err := s.db.Exec(`INSERT INTO user_shopping_items (name, store) VALUES (?, ?)`, name, store) - return err -} - -// GetUserShoppingItems retrieves all user shopping items -func (s *Store) GetUserShoppingItems() ([]UserShoppingItem, error) { - rows, err := s.db.Query(`SELECT id, name, store, checked, created_at FROM user_shopping_items ORDER BY store, created_at DESC`) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - - var items []UserShoppingItem - for rows.Next() { - var item UserShoppingItem - if err := rows.Scan(&item.ID, &item.Name, &item.Store, &item.Checked, &item.CreatedAt); err != nil { - return nil, err - } - items = append(items, item) - } - return items, rows.Err() -} - -// ToggleUserShoppingItem toggles the checked state of a user shopping item -func (s *Store) ToggleUserShoppingItem(id int64, checked bool) error { - _, err := s.db.Exec(`UPDATE user_shopping_items SET checked = ? WHERE id = ?`, checked, id) - return err -} - -// DeleteUserShoppingItem removes a user shopping item -func (s *Store) DeleteUserShoppingItem(id int64) error { - _, err := s.db.Exec(`DELETE FROM user_shopping_items WHERE id = ?`, id) - return err -} - -// SetShoppingItemChecked sets the checked state for an external shopping item -func (s *Store) SetShoppingItemChecked(source, itemID string, checked bool) error { - checkedInt := 0 - if checked { - checkedInt = 1 - } - _, err := s.db.Exec(` - INSERT INTO shopping_item_checks (source, item_id, checked, updated_at) - VALUES (?, ?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(source, item_id) DO UPDATE SET checked = ?, updated_at = CURRENT_TIMESTAMP - `, source, itemID, checkedInt, checkedInt) - return err -} - -// GetShoppingItemChecks returns a map of item_id -> checked for a given source -func (s *Store) GetShoppingItemChecks(source string) (map[string]bool, error) { - rows, err := s.db.Query(`SELECT item_id, checked FROM shopping_item_checks WHERE source = ?`, source) - if err != nil { - return nil, err - } - defer func() { _ = rows.Close() }() - - checks := make(map[string]bool) - for rows.Next() { - var itemID string - var checked int - if err := rows.Scan(&itemID, &checked); err != nil { - return nil, err - } - checks[itemID] = checked == 1 - } - return checks, rows.Err() -} - // GetCardsByDateRange retrieves cards due within a specific date range, // including overdue cards (due before start) so they appear in the timeline. func (s *Store) GetCardsByDateRange(start, end time.Time) ([]models.Card, error) { diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index 55e1510..dc9bfad 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -628,148 +628,6 @@ func TestGetCardsByDateRange(t *testing.T) { // User Shopping Items Tests // ============================================================================= -func setupTestStoreWithShopping(t *testing.T) *Store { - t.Helper() - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test.db") - - db, err := sql.Open("sqlite3", dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - - db.SetMaxOpenConns(1) - store := &Store{db: db} - - schema := ` - CREATE TABLE IF NOT EXISTS user_shopping_items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - store TEXT NOT NULL, - checked INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - CREATE TABLE IF NOT EXISTS shopping_item_checks ( - source TEXT NOT NULL, - item_id TEXT NOT NULL, - checked INTEGER DEFAULT 0, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (source, item_id) - ); - ` - if _, err := db.Exec(schema); err != nil { - t.Fatalf("Failed to create schema: %v", err) - } - - return store -} - -func TestUserShoppingItems_CRUD(t *testing.T) { - store := setupTestStoreWithShopping(t) - defer func() { _ = store.Close() }() - - // Save items - if err := store.SaveUserShoppingItem("Milk", "Costco"); err != nil { - t.Fatalf("Failed to save item: %v", err) - } - if err := store.SaveUserShoppingItem("Bread", "Safeway"); err != nil { - t.Fatalf("Failed to save second item: %v", err) - } - - // Get items - items, err := store.GetUserShoppingItems() - if err != nil { - t.Fatalf("Failed to get items: %v", err) - } - if len(items) != 2 { - t.Errorf("Expected 2 items, got %d", len(items)) - } - - // Verify item data - var milkItem UserShoppingItem - for _, item := range items { - if item.Name == "Milk" { - milkItem = item - break - } - } - if milkItem.Name != "Milk" { - t.Error("Could not find Milk item") - } - if milkItem.Store != "Costco" { - t.Errorf("Expected store 'Costco', got '%s'", milkItem.Store) - } - if milkItem.Checked { - t.Error("New item should not be checked") - } - - // Toggle item - if err := store.ToggleUserShoppingItem(milkItem.ID, true); err != nil { - t.Fatalf("Failed to toggle item: %v", err) - } - - items, _ = store.GetUserShoppingItems() - for _, item := range items { - if item.ID == milkItem.ID && !item.Checked { - t.Error("Item should be checked after toggle") - } - } - - // Delete item - if err := store.DeleteUserShoppingItem(milkItem.ID); err != nil { - t.Fatalf("Failed to delete item: %v", err) - } - - items, _ = store.GetUserShoppingItems() - if len(items) != 1 { - t.Errorf("Expected 1 item after delete, got %d", len(items)) - } -} - -func TestShoppingItemChecks_ExternalSources(t *testing.T) { - store := setupTestStoreWithShopping(t) - defer func() { _ = store.Close() }() - - // Set checked for trello item - if err := store.SetShoppingItemChecked("trello", "card-123", true); err != nil { - t.Fatalf("Failed to set trello checked: %v", err) - } - - // Set checked for plantoeat item - if err := store.SetShoppingItemChecked("plantoeat", "pte-456", true); err != nil { - t.Fatalf("Failed to set plantoeat checked: %v", err) - } - - // Get trello checks - trelloChecks, err := store.GetShoppingItemChecks("trello") - if err != nil { - t.Fatalf("Failed to get trello checks: %v", err) - } - if !trelloChecks["card-123"] { - t.Error("Expected trello card to be checked") - } - - // Get plantoeat checks - pteChecks, err := store.GetShoppingItemChecks("plantoeat") - if err != nil { - t.Fatalf("Failed to get plantoeat checks: %v", err) - } - if !pteChecks["pte-456"] { - t.Error("Expected plantoeat item to be checked") - } - - // Uncheck trello item - if err := store.SetShoppingItemChecked("trello", "card-123", false); err != nil { - t.Fatalf("Failed to uncheck trello item: %v", err) - } - - trelloChecks, _ = store.GetShoppingItemChecks("trello") - if trelloChecks["card-123"] { - t.Error("Trello item should be unchecked after update") - } -} - // ============================================================================= // Feature Toggles Tests // ============================================================================= |
