diff options
| -rw-r--r-- | internal/api/research_test.go | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/internal/api/research_test.go b/internal/api/research_test.go new file mode 100644 index 0000000..83a52b4 --- /dev/null +++ b/internal/api/research_test.go @@ -0,0 +1,241 @@ +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 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 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 +} |
