package test import ( "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "task-dashboard/internal/api" "task-dashboard/internal/config" "task-dashboard/internal/handlers" "task-dashboard/internal/models" "task-dashboard/internal/store" ) // setupTestServer creates a test HTTP server with all routes func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, func()) { t.Helper() // Create temp database tmpFile, err := os.CreateTemp("", "acceptance_*.db") if err != nil { t.Fatalf("Failed to create temp db: %v", err) } tmpFile.Close() // Save current directory and change to project root // This ensures migrations can be found originalDir, err := os.Getwd() if err != nil { t.Fatalf("Failed to get working directory: %v", err) } // Change to project root (1 level up from test/) if err := os.Chdir("../"); err != nil { t.Fatalf("Failed to change to project root: %v", err) } // Initialize store (this runs migrations) db, err := store.New(tmpFile.Name()) if err != nil { os.Chdir(originalDir) os.Remove(tmpFile.Name()) t.Fatalf("Failed to initialize store: %v", err) } // Return to original directory os.Chdir(originalDir) // Create mock API clients // (In real acceptance tests, you'd use test API endpoints or mocks) todoistClient := api.NewTodoistClient("test_key") trelloClient := api.NewTrelloClient("test_key", "test_token") cfg := &config.Config{ CacheTTLMinutes: 5, Port: "8080", } // Initialize handlers h := handlers.New(db, todoistClient, trelloClient, nil, cfg) // Set up router (same as main.go) r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(60 * time.Second)) // Routes r.Get("/", h.HandleDashboard) r.Post("/api/refresh", h.HandleRefresh) r.Get("/api/tasks", h.HandleGetTasks) r.Get("/api/meals", h.HandleGetMeals) r.Get("/api/boards", h.HandleGetBoards) // Create test server server := httptest.NewServer(r) cleanup := func() { server.Close() db.Close() os.Remove(tmpFile.Name()) } return server, db, cleanup } // TestFullWorkflow tests a complete user workflow func TestFullWorkflow(t *testing.T) { server, db, cleanup := setupTestServer(t) defer cleanup() // Seed database with test data testTasks := []models.Task{ { ID: "task1", Content: "Buy groceries", Description: "Milk, eggs, bread", ProjectID: "proj1", ProjectName: "Personal", Priority: 1, Completed: false, Labels: []string{"shopping"}, URL: "https://todoist.com/task/1", CreatedAt: time.Now(), }, } testBoards := []models.Board{ { ID: "board1", Name: "Work", Cards: []models.Card{ { ID: "card1", Name: "Complete project proposal", ListID: "list1", ListName: "In Progress", URL: "https://trello.com/c/card1", }, }, }, } if err := db.SaveTasks(testTasks); err != nil { t.Fatalf("Failed to seed tasks: %v", err) } if err := db.SaveBoards(testBoards); err != nil { t.Fatalf("Failed to seed boards: %v", err) } // Test 1: GET /api/tasks t.Run("GetTasks", func(t *testing.T) { resp, err := http.Get(server.URL + "/api/tasks") if err != nil { t.Fatalf("Failed to get tasks: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("Expected status 200, got %d", resp.StatusCode) } var tasks []models.Task if err := json.NewDecoder(resp.Body).Decode(&tasks); err != nil { t.Fatalf("Failed to decode tasks: %v", err) } if len(tasks) != 1 { t.Errorf("Expected 1 task, got %d", len(tasks)) } if tasks[0].Content != "Buy groceries" { t.Errorf("Expected task content 'Buy groceries', got '%s'", tasks[0].Content) } }) // Test 2: GET /api/boards t.Run("GetBoards", func(t *testing.T) { resp, err := http.Get(server.URL + "/api/boards") if err != nil { t.Fatalf("Failed to get boards: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("Expected status 200, got %d", resp.StatusCode) } var boards []models.Board if err := json.NewDecoder(resp.Body).Decode(&boards); err != nil { t.Fatalf("Failed to decode boards: %v", err) } if len(boards) != 1 { t.Errorf("Expected 1 board, got %d", len(boards)) } if boards[0].Name != "Work" { t.Errorf("Expected board name 'Work', got '%s'", boards[0].Name) } if len(boards[0].Cards) != 1 { t.Errorf("Expected 1 card in board, got %d", len(boards[0].Cards)) } }) // Test 3: POST /api/refresh t.Run("RefreshData", func(t *testing.T) { resp, err := http.Post(server.URL+"/api/refresh", "application/json", nil) if err != nil { t.Fatalf("Failed to refresh: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) t.Errorf("Expected status 200, got %d: %s", resp.StatusCode, string(body)) } // Just verify we got a 200 OK // The response can be either success message or dashboard data }) t.Run("GetMealsEmpty", func(t *testing.T) { resp, err := http.Get(server.URL + "/api/meals") if err != nil { t.Fatalf("Failed to get meals: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("Expected status 200, got %d", resp.StatusCode) } var meals []models.Meal if err := json.NewDecoder(resp.Body).Decode(&meals); err != nil { t.Fatalf("Failed to decode meals: %v", err) } if len(meals) != 0 { t.Errorf("Expected 0 meals (no PlanToEat client), got %d", len(meals)) } }) // Test 5: GET / (Dashboard) t.Run("GetDashboard", func(t *testing.T) { resp, err := http.Get(server.URL + "/") if err != nil { t.Fatalf("Failed to get dashboard: %v", err) } defer resp.Body.Close() // Dashboard returns HTML or JSON depending on template availability // Just verify it responds with 200 or 500 (template not found) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusInternalServerError { t.Errorf("Expected status 200 or 500, got %d", resp.StatusCode) } }) } // TestCaching tests the caching behavior func TestCaching(t *testing.T) { server, db, cleanup := setupTestServer(t) defer cleanup() // Seed initial data testTasks := []models.Task{ { ID: "task1", Content: "Initial task", }, } db.SaveTasks(testTasks) db.UpdateCacheMetadata("todoist_tasks", 5) // Test 1: First request should use cache t.Run("UsesCache", func(t *testing.T) { resp, err := http.Get(server.URL + "/api/tasks") if err != nil { t.Fatalf("Failed to get tasks: %v", err) } defer resp.Body.Close() var tasks []models.Task json.NewDecoder(resp.Body).Decode(&tasks) if len(tasks) != 1 { t.Errorf("Expected 1 task from cache, got %d", len(tasks)) } }) // Test 2: Refresh should invalidate cache t.Run("RefreshInvalidatesCache", func(t *testing.T) { // Force refresh resp, err := http.Post(server.URL+"/api/refresh", "application/json", nil) if err != nil { t.Fatalf("Failed to refresh: %v", err) } resp.Body.Close() // Check cache was updated (this is implicit in the refresh handler) if resp.StatusCode != http.StatusOK { t.Errorf("Expected refresh to succeed, got status %d", resp.StatusCode) } }) } // TestErrorHandling tests error scenarios func TestErrorHandling(t *testing.T) { server, _, cleanup := setupTestServer(t) defer cleanup() // Test 1: Invalid routes should 404 t.Run("InvalidRoute", func(t *testing.T) { resp, err := http.Get(server.URL + "/api/invalid") if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("Expected status 404, got %d", resp.StatusCode) } }) // Test 2: Wrong method should 405 t.Run("WrongMethod", func(t *testing.T) { resp, err := http.Get(server.URL + "/api/refresh") if err != nil { t.Fatalf("Failed to make request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusMethodNotAllowed { t.Errorf("Expected status 405, got %d", resp.StatusCode) } }) } // TestConcurrentRequests tests handling of concurrent requests func TestConcurrentRequests(t *testing.T) { server, db, cleanup := setupTestServer(t) defer cleanup() // Seed data testBoards := []models.Board{ {ID: "board1", Name: "Board 1", Cards: []models.Card{}}, {ID: "board2", Name: "Board 2", Cards: []models.Card{}}, } db.SaveBoards(testBoards) // Make 10 concurrent requests const numRequests = 10 done := make(chan bool, numRequests) errors := make(chan error, numRequests) for i := 0; i < numRequests; i++ { go func(id int) { resp, err := http.Get(fmt.Sprintf("%s/api/boards", server.URL)) if err != nil { errors <- fmt.Errorf("request %d failed: %w", id, err) done <- false return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { errors <- fmt.Errorf("request %d got status %d", id, resp.StatusCode) done <- false return } var boards []models.Board if err := json.NewDecoder(resp.Body).Decode(&boards); err != nil { errors <- fmt.Errorf("request %d decode failed: %w", id, err) done <- false return } done <- true }(i) } // Wait for all requests successCount := 0 for i := 0; i < numRequests; i++ { select { case success := <-done: if success { successCount++ } case err := <-errors: t.Errorf("Concurrent request error: %v", err) case <-time.After(10 * time.Second): t.Fatal("Timeout waiting for concurrent requests") } } if successCount != numRequests { t.Errorf("Expected %d successful requests, got %d", numRequests, successCount) } }