package store import ( "database/sql" "path/filepath" "testing" "time" _ "github.com/mattn/go-sqlite3" "task-dashboard/internal/models" ) // 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 } // setupTestStoreWithMeals creates a test store with meals table func setupTestStoreWithMeals(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 meals ( id TEXT PRIMARY KEY, recipe_name TEXT NOT NULL, date DATETIME, meal_type TEXT, recipe_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) } } // TestSaveAndGetBoards_MultipleBoards verifies that all boards and cards are // correctly saved and retrieved. This tests the fix for slice reallocation bug // where pointers in boardMap became stale when the boards slice grew. func TestSaveAndGetBoards_MultipleBoards(t *testing.T) { store := setupTestStoreWithCards(t) defer store.Close() // Create multiple boards with varying numbers of cards // Use enough boards to trigger slice reallocation boards := []models.Board{ { ID: "board1", Name: "Board 1", Cards: []models.Card{ {ID: "card1a", Name: "Card 1A", ListID: "list1", ListName: "To Do"}, {ID: "card1b", Name: "Card 1B", ListID: "list1", ListName: "To Do"}, }, }, { ID: "board2", Name: "Board 2", Cards: []models.Card{ {ID: "card2a", Name: "Card 2A", ListID: "list2", ListName: "Doing"}, {ID: "card2b", Name: "Card 2B", ListID: "list2", ListName: "Doing"}, {ID: "card2c", Name: "Card 2C", ListID: "list2", ListName: "Doing"}, }, }, { ID: "board3", Name: "Board 3", Cards: []models.Card{ {ID: "card3a", Name: "Card 3A", ListID: "list3", ListName: "Done"}, }, }, { ID: "board4", Name: "Board 4", Cards: []models.Card{ {ID: "card4a", Name: "Card 4A", ListID: "list4", ListName: "Backlog"}, {ID: "card4b", Name: "Card 4B", ListID: "list4", ListName: "Backlog"}, }, }, { ID: "board5", Name: "Board 5", Cards: []models.Card{ {ID: "card5a", Name: "Card 5A", ListID: "list5", ListName: "Ideas"}, {ID: "card5b", Name: "Card 5B", ListID: "list5", ListName: "Ideas"}, {ID: "card5c", Name: "Card 5C", ListID: "list5", ListName: "Ideas"}, {ID: "card5d", Name: "Card 5D", ListID: "list5", ListName: "Ideas"}, }, }, } // Calculate expected totals expectedBoards := len(boards) expectedCards := 0 for _, b := range boards { expectedCards += len(b.Cards) } // Save boards if err := store.SaveBoards(boards); err != nil { t.Fatalf("SaveBoards failed: %v", err) } // Retrieve boards result, err := store.GetBoards() if err != nil { t.Fatalf("GetBoards failed: %v", err) } // Verify board count if len(result) != expectedBoards { t.Errorf("Expected %d boards, got %d", expectedBoards, len(result)) } // Verify total card count totalCards := 0 for _, b := range result { totalCards += len(b.Cards) } if totalCards != expectedCards { t.Errorf("Expected %d total cards, got %d", expectedCards, totalCards) } // Verify each board has correct card count expectedCardCounts := map[string]int{ "board1": 2, "board2": 3, "board3": 1, "board4": 2, "board5": 4, } for _, b := range result { expected, ok := expectedCardCounts[b.ID] if !ok { t.Errorf("Unexpected board ID: %s", b.ID) continue } if len(b.Cards) != expected { t.Errorf("Board %s: expected %d cards, got %d", b.ID, expected, len(b.Cards)) } } } // TestSaveAndGetBoards_ManyBoards verifies correct behavior with many boards // to ensure slice reallocation is thoroughly tested func TestSaveAndGetBoards_ManyBoards(t *testing.T) { store := setupTestStoreWithCards(t) defer store.Close() // Create 20 boards with 5 cards each = 100 cards total numBoards := 20 cardsPerBoard := 5 boards := make([]models.Board, numBoards) for i := 0; i < numBoards; i++ { boards[i] = models.Board{ ID: string(rune('A' + i)), Name: "Board " + string(rune('A'+i)), Cards: make([]models.Card, cardsPerBoard), } for j := 0; j < cardsPerBoard; j++ { boards[i].Cards[j] = models.Card{ ID: string(rune('A'+i)) + "_card" + string(rune('0'+j)), Name: "Card " + string(rune('0'+j)), ListID: "list1", ListName: "To Do", } } } // Save boards if err := store.SaveBoards(boards); err != nil { t.Fatalf("SaveBoards failed: %v", err) } // Retrieve boards result, err := store.GetBoards() if err != nil { t.Fatalf("GetBoards failed: %v", err) } // Verify counts if len(result) != numBoards { t.Errorf("Expected %d boards, got %d", numBoards, len(result)) } totalCards := 0 for _, b := range result { totalCards += len(b.Cards) if len(b.Cards) != cardsPerBoard { t.Errorf("Board %s: expected %d cards, got %d", b.ID, cardsPerBoard, len(b.Cards)) } } expectedTotal := numBoards * cardsPerBoard if totalCards != expectedTotal { t.Errorf("Expected %d total cards, got %d", expectedTotal, totalCards) } } func TestGetTasksByDateRange(t *testing.T) { store := setupTestStoreWithTasks(t) defer store.Close() now := time.Now() tomorrow := now.Add(24 * time.Hour) nextWeek := now.Add(7 * 24 * time.Hour) tasks := []models.Task{ {ID: "1", Content: "Task 1", DueDate: &now}, {ID: "2", Content: "Task 2", DueDate: &tomorrow}, {ID: "3", Content: "Task 3", DueDate: &nextWeek}, } if err := store.SaveTasks(tasks); err != nil { t.Fatalf("Failed to save tasks: %v", err) } // Test range covering today and tomorrow start := now.Add(-1 * time.Hour) end := tomorrow.Add(1 * time.Hour) results, err := store.GetTasksByDateRange(start, end) if err != nil { t.Fatalf("GetTasksByDateRange failed: %v", err) } if len(results) != 2 { t.Errorf("Expected 2 tasks, got %d", len(results)) } } func TestGetMealsByDateRange(t *testing.T) { store := setupTestStoreWithMeals(t) defer store.Close() now := time.Now() tomorrow := now.Add(24 * time.Hour) meals := []models.Meal{ {ID: "1", RecipeName: "Meal 1", Date: now, MealType: "lunch"}, {ID: "2", RecipeName: "Meal 2", Date: tomorrow, MealType: "dinner"}, } if err := store.SaveMeals(meals); err != nil { t.Fatalf("Failed to save meals: %v", err) } start := now.Add(-1 * time.Hour) end := now.Add(1 * time.Hour) results, err := store.GetMealsByDateRange(start, end) if err != nil { t.Fatalf("GetMealsByDateRange failed: %v", err) } if len(results) != 1 { t.Errorf("Expected 1 meal, got %d", len(results)) } if results[0].ID != "1" { t.Errorf("Expected meal 1, got %s", results[0].ID) } } func TestGetCardsByDateRange(t *testing.T) { store := setupTestStoreWithCards(t) defer store.Close() now := time.Now() tomorrow := now.Add(24 * time.Hour) // Create 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) } // Create cards _, err = store.db.Exec(` INSERT INTO cards (id, name, board_id, due_date) VALUES (?, ?, ?, ?), (?, ?, ?, ?) `, "card1", "Card 1", "board1", now, "card2", "Card 2", "board1", tomorrow) if err != nil { t.Fatalf("Failed to insert cards: %v", err) } start := now.Add(-1 * time.Hour) end := now.Add(1 * time.Hour) results, err := store.GetCardsByDateRange(start, end) if err != nil { t.Fatalf("GetCardsByDateRange failed: %v", err) } if len(results) != 1 { t.Errorf("Expected 1 card, got %d", len(results)) } if results[0].ID != "card1" { t.Errorf("Expected card1, got %s", results[0].ID) } }