package api import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "testing" "time" ) // Research tests for API optimization // Run with: go test -v -run TestResearch ./internal/api/ // Requires TODOIST_API_KEY and TRELLO_API_KEY/TRELLO_TOKEN environment variables func TestTodoistSyncResearch(t *testing.T) { apiKey := os.Getenv("TODOIST_API_KEY") if apiKey == "" { t.Skip("TODOIST_API_KEY not set, skipping research test") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Todoist Sync API endpoint syncURL := "https://api.todoist.com/sync/v9/sync" // First sync with "*" to get all data params := url.Values{} params.Set("sync_token", "*") params.Set("resource_types", `["items"]`) // "items" = tasks in Sync API req, err := http.NewRequestWithContext(ctx, "POST", syncURL, nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } req.URL.RawQuery = params.Encode() req.Header.Set("Authorization", "Bearer "+apiKey) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { t.Fatalf("Failed to call Sync API: %v", err) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("Failed to read response: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Sync API error (status %d): %s", resp.StatusCode, string(body)) } // Parse response var syncResp map[string]interface{} if err := json.Unmarshal(body, &syncResp); err != nil { t.Fatalf("Failed to parse response: %v", err) } // Log findings t.Logf("=== TODOIST SYNC API RESEARCH ===") t.Logf("Response size: %d bytes", len(body)) if syncToken, ok := syncResp["sync_token"].(string); ok { t.Logf("sync_token: %s... (length: %d)", syncToken[:min(20, len(syncToken))], len(syncToken)) } if items, ok := syncResp["items"].([]interface{}); ok { t.Logf("Number of items (tasks): %d", len(items)) if len(items) > 0 { // Show structure of first item firstItem, _ := json.MarshalIndent(items[0], "", " ") t.Logf("First item structure:\n%s", string(firstItem)) } } // List top-level keys t.Logf("Top-level response keys:") for key := range syncResp { t.Logf(" - %s", key) } // Save sync_token for incremental sync test syncToken := syncResp["sync_token"].(string) // Test incremental sync (should return minimal data if nothing changed) t.Logf("\n=== INCREMENTAL SYNC TEST ===") params2 := url.Values{} params2.Set("sync_token", syncToken) params2.Set("resource_types", `["items"]`) req2, _ := http.NewRequestWithContext(ctx, "POST", syncURL, nil) req2.URL.RawQuery = params2.Encode() req2.Header.Set("Authorization", "Bearer "+apiKey) resp2, err := client.Do(req2) if err != nil { t.Fatalf("Incremental sync failed: %v", err) } defer func() { _ = resp2.Body.Close() }() body2, _ := io.ReadAll(resp2.Body) t.Logf("Incremental response size: %d bytes (vs full: %d bytes)", len(body2), len(body)) var syncResp2 map[string]interface{} _ = json.Unmarshal(body2, &syncResp2) if items2, ok := syncResp2["items"].([]interface{}); ok { t.Logf("Incremental items count: %d", len(items2)) } } func TestTrelloOptimizationResearch(t *testing.T) { apiKey := os.Getenv("TRELLO_API_KEY") token := os.Getenv("TRELLO_TOKEN") if apiKey == "" || token == "" { t.Skip("TRELLO_API_KEY or TRELLO_TOKEN not set, skipping research test") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() client := &http.Client{Timeout: 30 * time.Second} baseURL := "https://api.trello.com/1" // Test 1: Full response (current implementation) t.Logf("=== TRELLO OPTIMIZATION RESEARCH ===") params := url.Values{} params.Set("key", apiKey) params.Set("token", token) params.Set("filter", "open") start := time.Now() req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/members/me/boards?%s", baseURL, params.Encode()), nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Failed to fetch boards: %v", err) } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() fullDuration := time.Since(start) t.Logf("Test 1 - Full boards response:") t.Logf(" Size: %d bytes", len(body)) t.Logf(" Duration: %v", fullDuration) var boards []map[string]interface{} _ = json.Unmarshal(body, &boards) t.Logf(" Board count: %d", len(boards)) if len(boards) > 0 { t.Logf(" Fields in first board: %v", getKeys(boards[0])) } // Test 2: Limited fields params2 := url.Values{} params2.Set("key", apiKey) params2.Set("token", token) params2.Set("filter", "open") params2.Set("fields", "id,name") // Only essential fields start = time.Now() req2, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/members/me/boards?%s", baseURL, params2.Encode()), nil) resp2, err := client.Do(req2) if err != nil { t.Fatalf("Failed to fetch limited boards: %v", err) } body2, _ := io.ReadAll(resp2.Body) _ = resp2.Body.Close() limitedDuration := time.Since(start) t.Logf("\nTest 2 - Limited fields (id,name):") t.Logf(" Size: %d bytes (%.1f%% of full)", len(body2), float64(len(body2))/float64(len(body))*100) t.Logf(" Duration: %v", limitedDuration) // Test 3: Batch request with cards included if len(boards) > 0 { boardID := boards[0]["id"].(string) params3 := url.Values{} params3.Set("key", apiKey) params3.Set("token", token) params3.Set("cards", "visible") params3.Set("card_fields", "id,name,idList,due,url") params3.Set("lists", "open") params3.Set("list_fields", "id,name") start = time.Now() req3, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/boards/%s?%s", baseURL, boardID, params3.Encode()), nil) resp3, err := client.Do(req3) if err != nil { t.Fatalf("Failed to fetch board with cards: %v", err) } body3, _ := io.ReadAll(resp3.Body) _ = resp3.Body.Close() batchDuration := time.Since(start) t.Logf("\nTest 3 - Single board with cards/lists embedded:") t.Logf(" Size: %d bytes", len(body3)) t.Logf(" Duration: %v", batchDuration) var boardWithCards map[string]interface{} _ = json.Unmarshal(body3, &boardWithCards) if cards, ok := boardWithCards["cards"].([]interface{}); ok { t.Logf(" Cards count: %d", len(cards)) } if lists, ok := boardWithCards["lists"].([]interface{}); ok { t.Logf(" Lists count: %d", len(lists)) } } // Test 4: Check for webhooks/since parameter t.Logf("\nTest 4 - Checking for 'since' parameter support:") params4 := url.Values{} params4.Set("key", apiKey) params4.Set("token", token) params4.Set("filter", "open") params4.Set("since", time.Now().Add(-24*time.Hour).Format(time.RFC3339)) req4, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/members/me/boards?%s", baseURL, params4.Encode()), nil) resp4, err := client.Do(req4) if err != nil { t.Logf(" 'since' parameter: Error - %v", err) } else { body4, _ := io.ReadAll(resp4.Body) _ = resp4.Body.Close() t.Logf(" 'since' parameter: Status %d, Size %d bytes", resp4.StatusCode, len(body4)) } } func getKeys(m map[string]interface{}) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys }