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 func() { _ = 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 func() { _ = 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 func() { _ = 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 func() { _ = 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 func() { _ = 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 func() { _ = 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 func() { _ = 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 — should include all tasks <= end 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 (today + tomorrow, not next week), got %d", len(results)) } } func TestGetTasksByDateRange_IncludesOverdue(t *testing.T) { store := setupTestStoreWithTasks(t) defer func() { _ = store.Close() }() now := time.Now() yesterday := now.Add(-24 * time.Hour) tomorrow := now.Add(24 * time.Hour) tasks := []models.Task{ {ID: "overdue", Content: "Overdue", DueDate: &yesterday}, {ID: "current", Content: "Current", DueDate: &now}, } if err := store.SaveTasks(tasks); err != nil { t.Fatalf("Failed to save tasks: %v", err) } // Query from "today" onward — overdue tasks (before start) should also be included results, err := store.GetTasksByDateRange(now, tomorrow) if err != nil { t.Fatalf("GetTasksByDateRange failed: %v", err) } if len(results) != 2 { t.Errorf("Expected 2 tasks (overdue + current), got %d", len(results)) for _, r := range results { t.Logf(" task: %s due=%v", r.Content, r.DueDate) } } } func TestGetTasksByDateRange_ExcludesCompleted(t *testing.T) { store := setupTestStoreWithTasks(t) defer func() { _ = store.Close() }() now := time.Now() yesterday := now.Add(-24 * time.Hour) tomorrow := now.Add(24 * time.Hour) tasks := []models.Task{ {ID: "done", Content: "Completed overdue", DueDate: &yesterday, Completed: true}, {ID: "active", Content: "Active", DueDate: &now}, } if err := store.SaveTasks(tasks); err != nil { t.Fatalf("Failed to save tasks: %v", err) } results, err := store.GetTasksByDateRange(now, tomorrow) if err != nil { t.Fatalf("GetTasksByDateRange failed: %v", err) } if len(results) != 1 { t.Errorf("Expected 1 task (only active), got %d", len(results)) } if len(results) > 0 && results[0].ID != "active" { t.Errorf("Expected active task, got %s", results[0].ID) } } func TestGetMealsByDateRange(t *testing.T) { store := setupTestStoreWithMeals(t) defer func() { _ = 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 func() { _ = 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, list_id, list_name, due_date, url) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?) `, "card1", "Card 1", "board1", "list1", "To Do", now, "https://trello.com/c/card1", "card2", "Card 2", "board1", "list1", "To Do", tomorrow, "https://trello.com/c/card2") 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) } } // setupTestStoreWithBugs creates a test store with bugs table func setupTestStoreWithBugs(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 bugs ( id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, resolved_at DATETIME DEFAULT NULL ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestBugResolution(t *testing.T) { store := setupTestStoreWithBugs(t) defer func() { _ = store.Close() }() // Save some bugs if err := store.SaveBug("Bug 1"); err != nil { t.Fatalf("Failed to save bug 1: %v", err) } if err := store.SaveBug("Bug 2"); err != nil { t.Fatalf("Failed to save bug 2: %v", err) } if err := store.SaveBug("Bug 3"); err != nil { t.Fatalf("Failed to save bug 3: %v", err) } // Verify all 3 bugs are unresolved unresolved, err := store.GetUnresolvedBugs() if err != nil { t.Fatalf("GetUnresolvedBugs failed: %v", err) } if len(unresolved) != 3 { t.Errorf("Expected 3 unresolved bugs, got %d", len(unresolved)) } // Resolve bug 2 if err := store.ResolveBug(2); err != nil { t.Fatalf("ResolveBug failed: %v", err) } // Verify only 2 unresolved bugs remain unresolved, err = store.GetUnresolvedBugs() if err != nil { t.Fatalf("GetUnresolvedBugs after resolve failed: %v", err) } if len(unresolved) != 2 { t.Errorf("Expected 2 unresolved bugs, got %d", len(unresolved)) } // Verify bug 2 is not in the unresolved list for _, bug := range unresolved { if bug.ID == 2 { t.Error("Bug 2 should have been resolved but is still in unresolved list") } } // Verify all bugs still exist in GetBugs (including resolved) allBugs, err := store.GetBugs() if err != nil { t.Fatalf("GetBugs failed: %v", err) } if len(allBugs) != 3 { t.Errorf("Expected 3 total bugs, got %d", len(allBugs)) } // Verify bug 2 has resolved_at set for _, bug := range allBugs { if bug.ID == 2 { if bug.ResolvedAt == nil { t.Error("Bug 2 should have resolved_at set") } } } // Unresolve bug 2 if err := store.UnresolveBug(2); err != nil { t.Fatalf("UnresolveBug failed: %v", err) } // Verify all 3 bugs are unresolved again unresolved, err = store.GetUnresolvedBugs() if err != nil { t.Fatalf("GetUnresolvedBugs after unresolve failed: %v", err) } if len(unresolved) != 3 { t.Errorf("Expected 3 unresolved bugs after unresolve, got %d", len(unresolved)) } } func TestResolveBug_NonExistent(t *testing.T) { store := setupTestStoreWithBugs(t) defer func() { _ = store.Close() }() // Resolving a non-existent bug should not error (no rows affected is fine) err := store.ResolveBug(999) if err != nil { t.Errorf("ResolveBug on non-existent bug should not error, got: %v", err) } } // ============================================================================= // 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 // ============================================================================= func setupTestStoreWithFeatureToggles(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 feature_toggles ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, description TEXT, enabled BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestFeatureToggles_CRUD(t *testing.T) { store := setupTestStoreWithFeatureToggles(t) defer func() { _ = store.Close() }() // Create feature toggle if err := store.CreateFeatureToggle("new_feature", "A new feature", false); err != nil { t.Fatalf("Failed to create feature toggle: %v", err) } // Get all toggles toggles, err := store.GetFeatureToggles() if err != nil { t.Fatalf("Failed to get feature toggles: %v", err) } if len(toggles) != 1 { t.Errorf("Expected 1 toggle, got %d", len(toggles)) } if toggles[0].Name != "new_feature" { t.Errorf("Expected name 'new_feature', got '%s'", toggles[0].Name) } if toggles[0].Enabled { t.Error("New feature should be disabled") } // Check if enabled if store.IsFeatureEnabled("new_feature") { t.Error("IsFeatureEnabled should return false for disabled feature") } // Enable feature if err := store.SetFeatureEnabled("new_feature", true); err != nil { t.Fatalf("Failed to enable feature: %v", err) } if !store.IsFeatureEnabled("new_feature") { t.Error("IsFeatureEnabled should return true after enabling") } // Delete feature if err := store.DeleteFeatureToggle("new_feature"); err != nil { t.Fatalf("Failed to delete feature toggle: %v", err) } toggles, _ = store.GetFeatureToggles() if len(toggles) != 0 { t.Errorf("Expected 0 toggles after delete, got %d", len(toggles)) } } func TestIsFeatureEnabled_NonExistent(t *testing.T) { store := setupTestStoreWithFeatureToggles(t) defer func() { _ = store.Close() }() // Non-existent feature should return false if store.IsFeatureEnabled("does_not_exist") { t.Error("Non-existent feature should return false") } } // ============================================================================= // Completed Tasks Tests // ============================================================================= func setupTestStoreWithCompletedTasks(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 completed_tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, source_id TEXT NOT NULL, title TEXT NOT NULL, due_date TEXT, completed_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(source, source_id) ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestCompletedTasks_SaveAndGet(t *testing.T) { store := setupTestStoreWithCompletedTasks(t) defer func() { _ = store.Close() }() now := time.Now() // Save completed task with due date if err := store.SaveCompletedTask("todoist", "task-123", "Buy groceries", &now); err != nil { t.Fatalf("Failed to save completed task: %v", err) } // Save completed task without due date if err := store.SaveCompletedTask("trello", "card-456", "Review PR", nil); err != nil { t.Fatalf("Failed to save second completed task: %v", err) } // Get completed tasks tasks, err := store.GetCompletedTasks(10) if err != nil { t.Fatalf("Failed to get completed tasks: %v", err) } if len(tasks) != 2 { t.Errorf("Expected 2 completed tasks, got %d", len(tasks)) } // Verify task data var todoistTask models.CompletedTask for _, task := range tasks { if task.Source == "todoist" { todoistTask = task break } } if todoistTask.Title != "Buy groceries" { t.Errorf("Expected title 'Buy groceries', got '%s'", todoistTask.Title) } if todoistTask.DueDate == nil { t.Error("Expected due date to be set") } } func TestCompletedTasks_Limit(t *testing.T) { store := setupTestStoreWithCompletedTasks(t) defer func() { _ = store.Close() }() // Save multiple tasks for i := 0; i < 10; i++ { _ = store.SaveCompletedTask("todoist", "task-"+string(rune('0'+i)), "Task "+string(rune('0'+i)), nil) } // Get with limit tasks, _ := store.GetCompletedTasks(5) if len(tasks) != 5 { t.Errorf("Expected 5 tasks with limit, got %d", len(tasks)) } } // ============================================================================= // Source Configuration Tests // ============================================================================= func setupTestStoreWithSourceConfig(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 source_config ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, item_type TEXT NOT NULL, item_id TEXT NOT NULL, item_name TEXT NOT NULL, enabled BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(source, item_type, item_id) ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestSourceConfig_UpsertAndGet(t *testing.T) { store := setupTestStoreWithSourceConfig(t) defer func() { _ = store.Close() }() // Upsert configs cfg1 := models.SourceConfig{ Source: "trello", ItemType: "board", ItemID: "board-123", ItemName: "Work Board", Enabled: true, } if err := store.UpsertSourceConfig(cfg1); err != nil { t.Fatalf("Failed to upsert config: %v", err) } cfg2 := models.SourceConfig{ Source: "trello", ItemType: "board", ItemID: "board-456", ItemName: "Personal Board", Enabled: false, } if err := store.UpsertSourceConfig(cfg2); err != nil { t.Fatalf("Failed to upsert second config: %v", err) } // Get all configs configs, err := store.GetSourceConfigs() if err != nil { t.Fatalf("Failed to get configs: %v", err) } if len(configs) != 2 { t.Errorf("Expected 2 configs, got %d", len(configs)) } // Get by source trelloConfigs, err := store.GetSourceConfigsBySource("trello") if err != nil { t.Fatalf("Failed to get trello configs: %v", err) } if len(trelloConfigs) != 2 { t.Errorf("Expected 2 trello configs, got %d", len(trelloConfigs)) } // Get enabled IDs enabledIDs, err := store.GetEnabledSourceIDs("trello", "board") if err != nil { t.Fatalf("Failed to get enabled IDs: %v", err) } if len(enabledIDs) != 1 { t.Errorf("Expected 1 enabled ID, got %d", len(enabledIDs)) } if enabledIDs[0] != "board-123" { t.Errorf("Expected 'board-123', got '%s'", enabledIDs[0]) } } func TestSourceConfig_SetEnabled(t *testing.T) { store := setupTestStoreWithSourceConfig(t) defer func() { _ = store.Close() }() // Create a config cfg := models.SourceConfig{ Source: "calendar", ItemType: "calendar", ItemID: "cal-1", ItemName: "Primary", Enabled: true, } _ = store.UpsertSourceConfig(cfg) // Disable it if err := store.SetSourceConfigEnabled("calendar", "calendar", "cal-1", false); err != nil { t.Fatalf("Failed to set enabled: %v", err) } // Verify enabledIDs, _ := store.GetEnabledSourceIDs("calendar", "calendar") if len(enabledIDs) != 0 { t.Error("Expected no enabled calendars after disabling") } } // ============================================================================= // Cache Metadata Tests // ============================================================================= func setupTestStoreWithCacheMetadata(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 cache_metadata ( key TEXT PRIMARY KEY, last_fetch DATETIME NOT NULL, ttl_minutes INTEGER DEFAULT 5, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestCacheMetadata_UpdateAndCheck(t *testing.T) { store := setupTestStoreWithCacheMetadata(t) defer func() { _ = store.Close() }() // Initially no metadata valid, _ := store.IsCacheValid("test_key") if valid { t.Error("Cache should be invalid when no metadata exists") } // Update cache metadata if err := store.UpdateCacheMetadata("test_key", 5); err != nil { t.Fatalf("Failed to update cache metadata: %v", err) } // Now cache should be valid valid, err := store.IsCacheValid("test_key") if err != nil { t.Fatalf("Failed to check cache validity: %v", err) } if !valid { t.Error("Cache should be valid after update") } // Get metadata metadata, err := store.GetCacheMetadata("test_key") if err != nil { t.Fatalf("Failed to get cache metadata: %v", err) } if metadata == nil { t.Fatal("Expected metadata to exist") } if metadata.TTLMinutes != 5 { t.Errorf("Expected TTL 5, got %d", metadata.TTLMinutes) } // Invalidate cache if err := store.InvalidateCache("test_key"); err != nil { t.Fatalf("Failed to invalidate cache: %v", err) } valid, _ = store.IsCacheValid("test_key") if valid { t.Error("Cache should be invalid after invalidation") } } func TestCacheMetadata_ExpiredCache(t *testing.T) { store := setupTestStoreWithCacheMetadata(t) defer func() { _ = store.Close() }() // Insert old cache entry directly oldTime := time.Now().Add(-10 * time.Minute) _, err := store.db.Exec(` INSERT INTO cache_metadata (key, last_fetch, ttl_minutes) VALUES (?, ?, ?) `, "expired_key", oldTime, 5) if err != nil { t.Fatalf("Failed to insert old metadata: %v", err) } // Cache should be invalid (expired) valid, _ := store.IsCacheValid("expired_key") if valid { t.Error("Expired cache should be invalid") } } // ============================================================================= // Sync Token Tests // ============================================================================= func setupTestStoreWithSyncTokens(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 sync_tokens ( service TEXT PRIMARY KEY, token TEXT NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestSyncTokens_SetGetClear(t *testing.T) { store := setupTestStoreWithSyncTokens(t) defer func() { _ = store.Close() }() // Get non-existent token token, err := store.GetSyncToken("todoist") if err != nil { t.Fatalf("Failed to get token: %v", err) } if token != "" { t.Errorf("Expected empty token, got '%s'", token) } // Set token if err := store.SetSyncToken("todoist", "sync-token-123"); err != nil { t.Fatalf("Failed to set token: %v", err) } // Get token token, err = store.GetSyncToken("todoist") if err != nil { t.Fatalf("Failed to get token after set: %v", err) } if token != "sync-token-123" { t.Errorf("Expected 'sync-token-123', got '%s'", token) } // Clear token if err := store.ClearSyncToken("todoist"); err != nil { t.Fatalf("Failed to clear token: %v", err) } token, _ = store.GetSyncToken("todoist") if token != "" { t.Errorf("Expected empty token after clear, got '%s'", token) } } // ============================================================================= // Agent Session Tests // ============================================================================= func setupTestStoreWithAgents(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 agents ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, agent_id TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_seen DATETIME, trusted BOOLEAN DEFAULT 1 ); CREATE TABLE IF NOT EXISTS agent_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, request_token TEXT UNIQUE NOT NULL, agent_name TEXT NOT NULL, agent_id TEXT NOT NULL, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, session_token TEXT, session_expires_at DATETIME ); ` if _, err := db.Exec(schema); err != nil { t.Fatalf("Failed to create schema: %v", err) } return store } func TestAgentSession_CreateAndRetrieve(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() expiresAt := time.Now().Add(5 * time.Minute) session := &models.AgentSession{ RequestToken: "req-token-123", AgentName: "TestAgent", AgentID: "agent-uuid-123", ExpiresAt: expiresAt, } // Create session if err := store.CreateAgentSession(session); err != nil { t.Fatalf("Failed to create session: %v", err) } if session.ID == 0 { t.Error("Session ID should be set after create") } // Get by request token retrieved, err := store.GetAgentSessionByRequestToken("req-token-123") if err != nil { t.Fatalf("Failed to get session: %v", err) } if retrieved == nil { t.Fatal("Expected session to exist") } if retrieved.AgentName != "TestAgent" { t.Errorf("Expected name 'TestAgent', got '%s'", retrieved.AgentName) } if retrieved.Status != "pending" { t.Errorf("Expected status 'pending', got '%s'", retrieved.Status) } // Get pending by agent ID pending, err := store.GetPendingAgentSessionByAgentID("agent-uuid-123") if err != nil { t.Fatalf("Failed to get pending session: %v", err) } if pending == nil { t.Fatal("Expected pending session to exist") } } func TestAgentSession_ApproveAndDeny(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() // Create two sessions session1 := &models.AgentSession{ RequestToken: "approve-token", AgentName: "Agent1", AgentID: "agent-1", ExpiresAt: time.Now().Add(5 * time.Minute), } session2 := &models.AgentSession{ RequestToken: "deny-token", AgentName: "Agent2", AgentID: "agent-2", ExpiresAt: time.Now().Add(5 * time.Minute), } _ = store.CreateAgentSession(session1) _ = store.CreateAgentSession(session2) // Approve session1 sessionExpiry := time.Now().Add(1 * time.Hour) if err := store.ApproveAgentSession("approve-token", "session-token-abc", sessionExpiry); err != nil { t.Fatalf("Failed to approve session: %v", err) } // Verify approval approved, _ := store.GetAgentSessionByRequestToken("approve-token") if approved.Status != "approved" { t.Errorf("Expected status 'approved', got '%s'", approved.Status) } if approved.SessionToken != "session-token-abc" { t.Errorf("Expected session token 'session-token-abc', got '%s'", approved.SessionToken) } // Deny session2 if err := store.DenyAgentSession("deny-token"); err != nil { t.Fatalf("Failed to deny session: %v", err) } denied, _ := store.GetAgentSessionByRequestToken("deny-token") if denied.Status != "denied" { t.Errorf("Expected status 'denied', got '%s'", denied.Status) } } func TestAgentSession_GetBySessionToken(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() session := &models.AgentSession{ RequestToken: "req-for-session", AgentName: "SessionAgent", AgentID: "session-agent", ExpiresAt: time.Now().Add(5 * time.Minute), } _ = store.CreateAgentSession(session) _ = store.ApproveAgentSession("req-for-session", "active-session", time.Now().Add(1*time.Hour)) // Get by session token retrieved, err := store.GetAgentSessionBySessionToken("active-session") if err != nil { t.Fatalf("Failed to get by session token: %v", err) } if retrieved == nil { t.Fatal("Expected session to exist") } if retrieved.AgentName != "SessionAgent" { t.Errorf("Expected 'SessionAgent', got '%s'", retrieved.AgentName) } } func TestAgentSession_GetPending(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() // Create pending sessions for i := 0; i < 3; i++ { session := &models.AgentSession{ RequestToken: "pending-" + string(rune('0'+i)), AgentName: "Agent" + string(rune('0'+i)), AgentID: "agent-" + string(rune('0'+i)), ExpiresAt: time.Now().Add(5 * time.Minute), } _ = store.CreateAgentSession(session) } // Get pending sessions pending, err := store.GetPendingAgentSessions() if err != nil { t.Fatalf("Failed to get pending sessions: %v", err) } if len(pending) != 3 { t.Errorf("Expected 3 pending sessions, got %d", len(pending)) } } func TestAgentSession_Invalidate(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() // Create sessions for same agent for i := 0; i < 2; i++ { session := &models.AgentSession{ RequestToken: "inv-" + string(rune('0'+i)), AgentName: "SameAgent", AgentID: "same-agent", ExpiresAt: time.Now().Add(5 * time.Minute), } _ = store.CreateAgentSession(session) } // Invalidate all sessions for agent if err := store.InvalidatePreviousAgentSessions("same-agent"); err != nil { t.Fatalf("Failed to invalidate sessions: %v", err) } // Verify no pending sessions pending, _ := store.GetPendingAgentSessions() for _, s := range pending { if s.AgentID == "same-agent" { t.Error("Session should be invalidated") } } } // ============================================================================= // Agent Tests // ============================================================================= func TestAgent_CreateAndRetrieve(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() // Create agent if err := store.CreateOrUpdateAgent("TestBot", "bot-uuid-123"); err != nil { t.Fatalf("Failed to create agent: %v", err) } // Get by agent ID agent, err := store.GetAgentByAgentID("bot-uuid-123") if err != nil { t.Fatalf("Failed to get agent: %v", err) } if agent == nil { t.Fatal("Expected agent to exist") } if agent.Name != "TestBot" { t.Errorf("Expected name 'TestBot', got '%s'", agent.Name) } if !agent.Trusted { t.Error("New agent should be trusted by default") } // Get by name byName, err := store.GetAgentByName("TestBot") if err != nil { t.Fatalf("Failed to get agent by name: %v", err) } if byName == nil { t.Fatal("Expected agent to exist by name") } // Get all agents all, err := store.GetAllAgents() if err != nil { t.Fatalf("Failed to get all agents: %v", err) } if len(all) != 1 { t.Errorf("Expected 1 agent, got %d", len(all)) } } func TestAgent_UpdateLastSeen(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() _ = store.CreateOrUpdateAgent("SeenBot", "seen-uuid") // Update last seen if err := store.UpdateAgentLastSeen("seen-uuid"); err != nil { t.Fatalf("Failed to update last seen: %v", err) } agent, _ := store.GetAgentByAgentID("seen-uuid") if agent.LastSeen == nil { t.Error("LastSeen should be set after update") } } func TestAgent_TrustLevels(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() // Check trust for unknown agent (new) trust, err := store.CheckAgentTrust("UnknownBot", "unknown-uuid") if err != nil { t.Fatalf("Failed to check trust: %v", err) } if trust != models.AgentTrustNew { t.Errorf("Expected AgentTrustNew, got %v", trust) } // Create agent _ = store.CreateOrUpdateAgent("TrustBot", "trust-uuid") // Check trust for recognized agent trust, _ = store.CheckAgentTrust("TrustBot", "trust-uuid") if trust != models.AgentTrustRecognized { t.Errorf("Expected AgentTrustRecognized, got %v", trust) } // Check trust for suspicious agent (same name, different uuid) trust, _ = store.CheckAgentTrust("TrustBot", "different-uuid") if trust != models.AgentTrustSuspicious { t.Errorf("Expected AgentTrustSuspicious, got %v", trust) } } func TestAgent_Revoke(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() _ = store.CreateOrUpdateAgent("RevokeBot", "revoke-uuid") // Verify agent exists agent, _ := store.GetAgentByAgentID("revoke-uuid") if agent == nil { t.Fatal("Agent should exist") } // Revoke agent if err := store.RevokeAgent("revoke-uuid"); err != nil { t.Fatalf("Failed to revoke agent: %v", err) } // After revoke, agent should still exist but be in different state // (revoke doesn't delete, just marks somehow - let's verify it doesn't error) } func TestAgent_NonExistent(t *testing.T) { store := setupTestStoreWithAgents(t) defer func() { _ = store.Close() }() // Get non-existent agent agent, err := store.GetAgentByAgentID("does-not-exist") if err != nil { t.Fatalf("Should not error for non-existent agent: %v", err) } if agent != nil { t.Error("Agent should be nil for non-existent") } // Get non-existent by name byName, err := store.GetAgentByName("unknown-name") if err != nil { t.Fatalf("Should not error for non-existent name: %v", err) } if byName != nil { t.Error("Agent should be nil for non-existent name") } // Check trust for non-existent (should be new) trust, _ := store.CheckAgentTrust("UnknownBot", "unknown-uuid") if trust != models.AgentTrustNew { t.Errorf("Expected AgentTrustNew for unknown, got %v", trust) } }