package tmdb import ( "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestRequestURL(t *testing.T) { tests := []struct { name string segments []string want string }{ { name: "single segment", segments: []string{"configuration"}, want: "https://api.themoviedb.org/3/configuration", }, { name: "two segments", segments: []string{"search", "movie"}, want: "https://api.themoviedb.org/3/search/movie", }, { name: "movie with id", segments: []string{"movie", "550"}, want: "https://api.themoviedb.org/3/movie/550", }, { name: "movie with id and credits", segments: []string{"movie", "550", "credits"}, want: "https://api.themoviedb.org/3/movie/550/credits", }, { name: "no segments", segments: []string{}, want: "https://api.themoviedb.org/3/", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := requestURL(tt.segments...) if got != tt.want { t.Errorf("requestURL() = %v, want %v", got, tt.want) } }) } } func TestBuildURL(t *testing.T) { tests := []struct { name string segments []string params map[string]string want string }{ { name: "no params", segments: []string{"movie", "550"}, params: nil, want: "https://api.themoviedb.org/3/movie/550?language=en-US", }, { name: "with query param", segments: []string{"search", "movie"}, params: map[string]string{ "query": "Inception", }, want: "https://api.themoviedb.org/3/search/movie?language=en-US&query=Inception", }, { name: "multiple params", segments: []string{"search", "movie"}, params: map[string]string{ "query": "Fight Club", "page": "2", "include_adult": "false", }, // Note: URL params can be in any order, so we check contains instead want: "https://api.themoviedb.org/3/search/movie?", }, { name: "params with special characters", segments: []string{"search", "movie"}, params: map[string]string{ "query": "The Matrix", }, want: "https://api.themoviedb.org/3/search/movie?", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := buildURL(tt.segments, tt.params) if !strings.HasPrefix(got, tt.want) { t.Errorf("buildURL() = %v, want prefix %v", got, tt.want) } // Check that all params are present (checking keys, values may be URL encoded) for key := range tt.params { if !strings.Contains(got, key+"=") { t.Errorf("buildURL() missing param key %s in %v", key, got) } } // Check that language is always added if !strings.Contains(got, "language=en-US") { t.Errorf("buildURL() missing default language param in %v", got) } }) } } func TestAPIGet_Success(t *testing.T) { // Create a test server that returns 200 OK server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify headers if r.Header.Get("accept") != "application/json" { t.Errorf("missing or incorrect accept header") } if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { t.Errorf("missing or incorrect Authorization header") } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"success": true}`)) })) defer server.Close() api := &API{token: "test-token"} body, err := api.get(server.URL) if err != nil { t.Errorf("get() unexpected error: %v", err) } expected := `{"success": true}` if string(body) != expected { t.Errorf("get() = %v, want %v", string(body), expected) } } func TestAPIGet_RateLimitRetry(t *testing.T) { attemptCount := 0 // Create a test server that returns 429 twice, then 200 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attemptCount++ if attemptCount <= 2 { w.WriteHeader(http.StatusTooManyRequests) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"success": true}`)) })) defer server.Close() api := &API{token: "test-token"} start := time.Now() body, err := api.get(server.URL) elapsed := time.Since(start) if err != nil { t.Errorf("get() unexpected error: %v", err) } if attemptCount != 3 { t.Errorf("expected 3 attempts, got %d", attemptCount) } // Should have waited at least 1s + 2s = 3s total if elapsed < 3*time.Second { t.Errorf("expected backoff delay, got %v", elapsed) } expected := `{"success": true}` if string(body) != expected { t.Errorf("get() = %v, want %v", string(body), expected) } } func TestAPIGet_RateLimitExceeded(t *testing.T) { attemptCount := 0 // Create a test server that always returns 429 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attemptCount++ w.WriteHeader(http.StatusTooManyRequests) })) defer server.Close() api := &API{token: "test-token"} _, err := api.get(server.URL) if err == nil { t.Error("get() expected error, got nil") } if !strings.Contains(err.Error(), "rate limit exceeded") { t.Errorf("get() expected rate limit error, got: %v", err) } // Should have attempted maxRetries + 1 times (initial + retries) expectedAttempts := maxRetries + 1 if attemptCount != expectedAttempts { t.Errorf("expected %d attempts, got %d", expectedAttempts, attemptCount) } } func TestAPIGet_RetryAfterHeader(t *testing.T) { attemptCount := 0 // Create a test server that returns 429 with Retry-After header server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attemptCount++ if attemptCount == 1 { w.Header().Set("Retry-After", "2") w.WriteHeader(http.StatusTooManyRequests) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`{"success": true}`)) })) defer server.Close() api := &API{token: "test-token"} start := time.Now() body, err := api.get(server.URL) elapsed := time.Since(start) if err != nil { t.Errorf("get() unexpected error: %v", err) } // Should have waited at least 2s as specified in Retry-After if elapsed < 2*time.Second { t.Errorf("expected at least 2s delay from Retry-After header, got %v", elapsed) } expected := `{"success": true}` if string(body) != expected { t.Errorf("get() = %v, want %v", string(body), expected) } } func TestAPIGet_NonOKStatus(t *testing.T) { tests := []struct { name string statusCode int }{ {"bad request", http.StatusBadRequest}, {"unauthorized", http.StatusUnauthorized}, {"forbidden", http.StatusForbidden}, {"not found", http.StatusNotFound}, {"internal server error", http.StatusInternalServerError}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tt.statusCode) })) defer server.Close() api := &API{token: "test-token"} _, err := api.get(server.URL) if err == nil { t.Error("get() expected error, got nil") } expectedError := fmt.Sprintf("unexpected status code: %d", tt.statusCode) if !strings.Contains(err.Error(), expectedError) { t.Errorf("get() expected error containing %q, got: %v", expectedError, err) } }) } } func TestAPIGet_NetworkError(t *testing.T) { api := &API{token: "test-token"} _, err := api.get("http://invalid-domain-that-does-not-exist.local") if err == nil { t.Error("get() expected error for invalid domain, got nil") } if !strings.Contains(err.Error(), "http.DefaultClient.Do") { t.Errorf("get() expected network error, got: %v", err) } } func TestAPIGet_InvalidURL(t *testing.T) { api := &API{token: "test-token"} _, err := api.get("://invalid-url") if err == nil { t.Error("get() expected error for invalid URL, got nil") } if !strings.Contains(err.Error(), "http.NewRequest") { t.Errorf("get() expected URL parse error, got: %v", err) } } func TestAPIGet_ReadBodyError(t *testing.T) { // Create a test server that closes connection before body is read server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", "100") w.WriteHeader(http.StatusOK) // Don't write anything, causing a read error })) defer server.Close() api := &API{token: "test-token"} // Note: This test may not always fail as expected due to how httptest works // In real scenarios, network issues would cause io.ReadAll to fail _, err := api.get(server.URL) // Just verify we got a response (this test is mainly for coverage) if err != nil && !strings.Contains(err.Error(), "io.ReadAll") { t.Logf("get() error (expected in some cases): %v", err) } } // Benchmark tests func BenchmarkRequestURL(b *testing.B) { for i := 0; i < b.N; i++ { requestURL("movie", "550", "credits") } } func BenchmarkBuildURL(b *testing.B) { params := map[string]string{ "query": "Inception", "page": "1", } for i := 0; i < b.N; i++ { buildURL([]string{"search", "movie"}, params) } } func BenchmarkAPIGet(b *testing.B) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) io.WriteString(w, `{"success": true}`) })) defer server.Close() api := &API{token: "test-token"} b.ResetTimer() for i := 0; i < b.N; i++ { api.get(server.URL) } }