package api import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) // newTestTrelloClient creates a TrelloClient for testing with custom base URL func newTestTrelloClient(baseURL, apiKey, token string) *TrelloClient { client := NewTrelloClient(apiKey, token) client.BaseURL = baseURL return client } func TestTrelloClient_CreateCard(t *testing.T) { // Mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify method and path if r.Method != "POST" { t.Errorf("Expected POST request, got %s", r.Method) } if r.URL.Path != "/cards" { t.Errorf("Expected path /cards, got %s", r.URL.Path) } // Verify Content-Type if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" { t.Errorf("Expected Content-Type application/x-www-form-urlencoded, got %s", r.Header.Get("Content-Type")) } // Parse form data body, _ := io.ReadAll(r.Body) values, _ := parseFormData(string(body)) // Verify required fields if values["key"] != "test-key" { t.Errorf("Expected key=test-key, got %s", values["key"]) } if values["token"] != "test-token" { t.Errorf("Expected token=test-token, got %s", values["token"]) } if values["idList"] != "list-123" { t.Errorf("Expected idList=list-123, got %s", values["idList"]) } if values["name"] != "Test Card" { t.Errorf("Expected name=Test Card, got %s", values["name"]) } if values["desc"] != "Test description" { t.Errorf("Expected desc=Test description, got %s", values["desc"]) } // Return mock response response := trelloCardResponse{ ID: "card-456", Name: "Test Card", IDList: "list-123", URL: "https://trello.com/c/card-456", Desc: "Test description", IDBoard: "board-789", } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) })) defer server.Close() // Create client with mock server URL client := newTestTrelloClient(server.URL, "test-key", "test-token") // Test CreateCard ctx := context.Background() card, err := client.CreateCard(ctx, "list-123", "Test Card", "Test description", nil) if err != nil { t.Fatalf("CreateCard failed: %v", err) } // Verify response if card.ID != "card-456" { t.Errorf("Expected card ID card-456, got %s", card.ID) } if card.Name != "Test Card" { t.Errorf("Expected card name Test Card, got %s", card.Name) } if card.ListID != "list-123" { t.Errorf("Expected list ID list-123, got %s", card.ListID) } if card.URL != "https://trello.com/c/card-456" { t.Errorf("Expected URL https://trello.com/c/card-456, got %s", card.URL) } } func TestTrelloClient_CreateCard_WithDueDate(t *testing.T) { dueDate := time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC) // Mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Parse form data body, _ := io.ReadAll(r.Body) values, _ := parseFormData(string(body)) // Verify due date is present if values["due"] != dueDate.Format(time.RFC3339) { t.Errorf("Expected due=%s, got %s", dueDate.Format(time.RFC3339), values["due"]) } // Return mock response with due date dueString := dueDate.Format(time.RFC3339) response := trelloCardResponse{ ID: "card-789", Name: "Card with Due Date", IDList: "list-456", URL: "https://trello.com/c/card-789", Due: &dueString, } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response) })) defer server.Close() // Create client client := newTestTrelloClient(server.URL, "test-key", "test-token") // Test CreateCard with due date ctx := context.Background() card, err := client.CreateCard(ctx, "list-456", "Card with Due Date", "", &dueDate) if err != nil { t.Fatalf("CreateCard failed: %v", err) } // Verify due date is set if card.DueDate == nil { t.Error("Expected due date to be set") } else if !card.DueDate.Equal(dueDate) { t.Errorf("Expected due date %v, got %v", dueDate, *card.DueDate) } } func TestTrelloClient_UpdateCard(t *testing.T) { // Mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify method and path if r.Method != "PUT" { t.Errorf("Expected PUT request, got %s", r.Method) } if !strings.HasPrefix(r.URL.Path, "/cards/") { t.Errorf("Expected path to start with /cards/, got %s", r.URL.Path) } // Extract card ID from path cardID := strings.TrimPrefix(r.URL.Path, "/cards/") if cardID != "card-123" { t.Errorf("Expected card ID card-123, got %s", cardID) } // Parse form data body, _ := io.ReadAll(r.Body) values, _ := parseFormData(string(body)) // Verify updated fields if values["name"] != "Updated Name" { t.Errorf("Expected name=Updated Name, got %s", values["name"]) } if values["desc"] != "Updated description" { t.Errorf("Expected desc=Updated description, got %s", values["desc"]) } // Return 200 OK w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"id":"card-123","name":"Updated Name"}`)) })) defer server.Close() // Create client client := newTestTrelloClient(server.URL, "test-key", "test-token") // Test UpdateCard ctx := context.Background() updates := map[string]interface{}{ "name": "Updated Name", "desc": "Updated description", } err := client.UpdateCard(ctx, "card-123", updates) if err != nil { t.Fatalf("UpdateCard failed: %v", err) } } func TestTrelloClient_UpdateCard_Error(t *testing.T) { // Mock server that returns error server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"error":"Invalid card ID"}`)) })) defer server.Close() // Create client client := newTestTrelloClient(server.URL, "test-key", "test-token") // Test UpdateCard with error ctx := context.Background() updates := map[string]interface{}{ "name": "Should Fail", } err := client.UpdateCard(ctx, "invalid-card", updates) if err == nil { t.Error("Expected error, got nil") } if !strings.Contains(err.Error(), "400") { t.Errorf("Expected error to contain status 400, got: %v", err) } } // Helper function to parse form-encoded data func parseFormData(data string) (map[string]string, error) { values, err := url.ParseQuery(data) if err != nil { return nil, err } result := make(map[string]string) for key, vals := range values { if len(vals) > 0 { result[key] = vals[0] } } return result, nil } func TestTrelloClient_GetBoards(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("Expected GET, got %s", r.Method) } if r.URL.Path != "/members/me/boards" { t.Errorf("Expected path /members/me/boards, got %s", r.URL.Path) } response := []trelloBoardResponse{ {ID: "board-1", Name: "Board 1"}, {ID: "board-2", Name: "Board 2"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) })) defer server.Close() client := newTestTrelloClient(server.URL, "test-key", "test-token") boards, err := client.GetBoards(context.Background()) if err != nil { t.Fatalf("GetBoards failed: %v", err) } if len(boards) != 2 { t.Errorf("Expected 2 boards, got %d", len(boards)) } if boards[0].ID != "board-1" { t.Errorf("Expected board ID 'board-1', got '%s'", boards[0].ID) } } func TestTrelloClient_GetCards(t *testing.T) { dueDate := "2024-01-15T12:00:00Z" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("Expected GET, got %s", r.Method) } w.Header().Set("Content-Type", "application/json") // GetCards calls getLists internally if strings.Contains(r.URL.Path, "/lists") { response := []trelloListResponse{ {ID: "list-1", Name: "To Do"}, } json.NewEncoder(w).Encode(response) return } if strings.Contains(r.URL.Path, "/cards") { response := []trelloCardResponse{ {ID: "card-1", Name: "Card 1", IDList: "list-1", IDBoard: "board-1", Due: &dueDate}, {ID: "card-2", Name: "Card 2", IDList: "list-1", IDBoard: "board-1"}, } json.NewEncoder(w).Encode(response) return } t.Errorf("Unexpected path: %s", r.URL.Path) })) defer server.Close() client := newTestTrelloClient(server.URL, "test-key", "test-token") cards, err := client.GetCards(context.Background(), "board-1") if err != nil { t.Fatalf("GetCards failed: %v", err) } if len(cards) != 2 { t.Errorf("Expected 2 cards, got %d", len(cards)) } if cards[0].DueDate == nil { t.Error("Expected due date for card 1") } } func TestTrelloClient_GetLists(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Errorf("Expected GET, got %s", r.Method) } if !strings.Contains(r.URL.Path, "/boards/board-1/lists") { t.Errorf("Expected path to contain /boards/board-1/lists, got %s", r.URL.Path) } response := []trelloListResponse{ {ID: "list-1", Name: "To Do"}, {ID: "list-2", Name: "Done"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) })) defer server.Close() client := newTestTrelloClient(server.URL, "test-key", "test-token") lists, err := client.GetLists(context.Background(), "board-1") if err != nil { t.Fatalf("GetLists failed: %v", err) } if len(lists) != 2 { t.Errorf("Expected 2 lists, got %d", len(lists)) } if lists[0].Name != "To Do" { t.Errorf("Expected 'To Do', got '%s'", lists[0].Name) } } func TestTrelloClient_GetBoardsWithCards(t *testing.T) { requestCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestCount++ if strings.Contains(r.URL.Path, "/members/me/boards") { response := []trelloBoardResponse{ {ID: "board-1", Name: "Board 1"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } if strings.Contains(r.URL.Path, "/lists") { response := []trelloListResponse{ {ID: "list-1", Name: "To Do"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } if strings.Contains(r.URL.Path, "/cards") { response := []trelloCardResponse{ {ID: "card-1", Name: "Card 1", IDList: "list-1", IDBoard: "board-1"}, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } t.Errorf("Unexpected path: %s", r.URL.Path) })) defer server.Close() client := newTestTrelloClient(server.URL, "test-key", "test-token") boards, err := client.GetBoardsWithCards(context.Background()) if err != nil { t.Fatalf("GetBoardsWithCards failed: %v", err) } if len(boards) != 1 { t.Errorf("Expected 1 board, got %d", len(boards)) } if len(boards[0].Cards) != 1 { t.Errorf("Expected 1 card, got %d", len(boards[0].Cards)) } if boards[0].Cards[0].ListName != "To Do" { t.Errorf("Expected list name 'To Do', got '%s'", boards[0].Cards[0].ListName) } }