diff options
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/sqlite.go | 18 | ||||
| -rw-r--r-- | internal/store/sqlite_test.go | 258 |
2 files changed, 276 insertions, 0 deletions
diff --git a/internal/store/sqlite.go b/internal/store/sqlite.go index c97d0af..79f5cc8 100644 --- a/internal/store/sqlite.go +++ b/internal/store/sqlite.go @@ -167,6 +167,12 @@ func (s *Store) GetTasks() ([]models.Task, error) { return tasks, rows.Err() } +// DeleteTask removes a task from the cache by ID +func (s *Store) DeleteTask(id string) error { + _, err := s.db.Exec(`DELETE FROM tasks WHERE id = ?`, id) + return err +} + // Notes operations // SaveNotes saves multiple notes to the database @@ -403,6 +409,12 @@ func (s *Store) IsCacheValid(key string) (bool, error) { return cm.IsCacheValid(), nil } +// InvalidateCache removes the cache metadata for a given key, forcing a refresh on next fetch +func (s *Store) InvalidateCache(key string) error { + _, err := s.db.Exec(`DELETE FROM cache_metadata WHERE key = ?`, key) + return err +} + // Boards operations // SaveBoards saves multiple boards to the database @@ -538,3 +550,9 @@ func (s *Store) GetBoards() ([]models.Board, error) { return boards, cardRows.Err() } + +// DeleteCard removes a card from the cache by ID +func (s *Store) DeleteCard(id string) error { + _, err := s.db.Exec(`DELETE FROM cards WHERE id = ?`, id) + return err +} diff --git a/internal/store/sqlite_test.go b/internal/store/sqlite_test.go index 3379a11..962a1bf 100644 --- a/internal/store/sqlite_test.go +++ b/internal/store/sqlite_test.go @@ -336,3 +336,261 @@ func TestGetNotes_SQLInjectionAttempt(t *testing.T) { // and is not vulnerable to SQL injection via the LIMIT clause fmt.Println("✓ LIMIT clause is properly parameterized") } + +// setupTestStoreWithTasks creates a test store with tasks table +func setupTestStoreWithTasks(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 tasks ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + description TEXT, + project_id TEXT, + project_name TEXT, + due_date DATETIME, + priority INTEGER DEFAULT 1, + completed BOOLEAN DEFAULT FALSE, + labels TEXT, + url TEXT, + created_at DATETIME, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + ` + if _, err := db.Exec(schema); err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + return store +} + +// setupTestStoreWithCards creates a test store with boards and cards tables +func setupTestStoreWithCards(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 boards ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS cards ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + board_id TEXT NOT NULL, + list_id TEXT, + list_name TEXT, + due_date DATETIME, + url TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + ` + if _, err := db.Exec(schema); err != nil { + t.Fatalf("Failed to create schema: %v", err) + } + + return store +} + +// TestDeleteTask verifies that DeleteTask removes a task from the cache +func TestDeleteTask(t *testing.T) { + store := setupTestStoreWithTasks(t) + defer store.Close() + + // Save some tasks + tasks := []models.Task{ + { + ID: "task1", + Content: "Task 1", + Description: "Description 1", + Priority: 1, + Completed: false, + Labels: []string{}, + CreatedAt: time.Now(), + }, + { + ID: "task2", + Content: "Task 2", + Description: "Description 2", + Priority: 2, + Completed: false, + Labels: []string{}, + CreatedAt: time.Now(), + }, + { + ID: "task3", + Content: "Task 3", + Description: "Description 3", + Priority: 3, + Completed: false, + Labels: []string{}, + CreatedAt: time.Now(), + }, + } + + if err := store.SaveTasks(tasks); err != nil { + t.Fatalf("Failed to save tasks: %v", err) + } + + // Verify all 3 tasks exist + result, err := store.GetTasks() + if err != nil { + t.Fatalf("Failed to get tasks: %v", err) + } + if len(result) != 3 { + t.Fatalf("Expected 3 tasks, got %d", len(result)) + } + + // Delete task2 + if err := store.DeleteTask("task2"); err != nil { + t.Fatalf("DeleteTask failed: %v", err) + } + + // Verify only 2 tasks remain + result, err = store.GetTasks() + if err != nil { + t.Fatalf("Failed to get tasks after delete: %v", err) + } + if len(result) != 2 { + t.Errorf("Expected 2 tasks after delete, got %d", len(result)) + } + + // Verify task2 is gone + for _, task := range result { + if task.ID == "task2" { + t.Errorf("task2 should have been deleted but was found") + } + } + + // Verify task1 and task3 still exist + foundTask1, foundTask3 := false, false + for _, task := range result { + if task.ID == "task1" { + foundTask1 = true + } + if task.ID == "task3" { + foundTask3 = true + } + } + if !foundTask1 { + t.Error("task1 should still exist") + } + if !foundTask3 { + t.Error("task3 should still exist") + } +} + +// TestDeleteTask_NonExistent verifies that deleting a non-existent task doesn't error +func TestDeleteTask_NonExistent(t *testing.T) { + store := setupTestStoreWithTasks(t) + defer store.Close() + + // Delete a task that doesn't exist - should not error + err := store.DeleteTask("nonexistent") + if err != nil { + t.Errorf("DeleteTask on non-existent task should not error, got: %v", err) + } +} + +// TestDeleteCard verifies that DeleteCard removes a card from the cache +func TestDeleteCard(t *testing.T) { + store := setupTestStoreWithCards(t) + defer store.Close() + + // First create a board + _, err := store.db.Exec(`INSERT INTO boards (id, name) VALUES (?, ?)`, "board1", "Test Board") + if err != nil { + t.Fatalf("Failed to create board: %v", err) + } + + // Insert cards directly + cards := []struct { + id string + name string + boardID string + }{ + {"card1", "Card 1", "board1"}, + {"card2", "Card 2", "board1"}, + {"card3", "Card 3", "board1"}, + } + + for _, card := range cards { + _, err := store.db.Exec( + `INSERT INTO cards (id, name, board_id, list_id, list_name) VALUES (?, ?, ?, ?, ?)`, + card.id, card.name, card.boardID, "list1", "To Do", + ) + if err != nil { + t.Fatalf("Failed to insert card: %v", err) + } + } + + // Verify all 3 cards exist + var count int + err = store.db.QueryRow(`SELECT COUNT(*) FROM cards`).Scan(&count) + if err != nil { + t.Fatalf("Failed to count cards: %v", err) + } + if count != 3 { + t.Fatalf("Expected 3 cards, got %d", count) + } + + // Delete card2 + if err := store.DeleteCard("card2"); err != nil { + t.Fatalf("DeleteCard failed: %v", err) + } + + // Verify only 2 cards remain + err = store.db.QueryRow(`SELECT COUNT(*) FROM cards`).Scan(&count) + if err != nil { + t.Fatalf("Failed to count cards after delete: %v", err) + } + if count != 2 { + t.Errorf("Expected 2 cards after delete, got %d", count) + } + + // Verify card2 is gone + var exists int + err = store.db.QueryRow(`SELECT COUNT(*) FROM cards WHERE id = ?`, "card2").Scan(&exists) + if err != nil { + t.Fatalf("Failed to check card2: %v", err) + } + if exists != 0 { + t.Errorf("card2 should have been deleted") + } +} + +// TestDeleteCard_NonExistent verifies that deleting a non-existent card doesn't error +func TestDeleteCard_NonExistent(t *testing.T) { + store := setupTestStoreWithCards(t) + defer store.Close() + + // Delete a card that doesn't exist - should not error + err := store.DeleteCard("nonexistent") + if err != nil { + t.Errorf("DeleteCard on non-existent card should not error, got: %v", err) + } +} |
