updated tmdb

This commit is contained in:
2026-01-13 19:11:17 +11:00
parent 7c91cbb08a
commit 1a099a3724
10 changed files with 691 additions and 44 deletions

360
tmdb/request_test.go Normal file
View File

@@ -0,0 +1,360 @@
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)
}
}