361 lines
9.1 KiB
Go
361 lines
9.1 KiB
Go
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)
|
|
}
|
|
}
|