package musicbrainz import ( "context" "net/http" "net/http/httptest" "testing" "time" "golang.org/x/time/rate" "naviwatcher/internal/config" ) func TestNewClient_ValidConfig(t *testing.T) { cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } client := NewClient(cfg) defer client.Close() if client == nil { t.Fatal("NewClient() returned nil client") } if client.httpClient == nil { t.Fatal("NewClient() returned client with nil http.Client") } if client.userAgent != cfg.UserAgent { t.Errorf("NewClient().userAgent = %q, want %q", client.userAgent, cfg.UserAgent) } expectedBaseURL := "https://musicbrainz.org/ws/2" if client.baseURL != expectedBaseURL { t.Errorf("NewClient().baseURL = %q, want %q", client.baseURL, expectedBaseURL) } if client.rateLimiter == nil { t.Fatal("NewClient() returned client with nil rate limiter") } } func TestNewClientWithLimiter(t *testing.T) { cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1), 1) client := NewClientWithLimiter(cfg, rl) defer client.Close() if client == nil { t.Fatal("NewClientWithLimiter() returned nil client") } if client.rateLimiter != rl { t.Error("NewClientWithLimiter() did not use provided rate limiter") } } func TestDoGet_Success(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("User-Agent") == "" { t.Error("doGet() request missing User-Agent header") } if r.Header.Get("Accept") != "application/xml" { t.Errorf("doGet() Accept header = %q, want %q", r.Header.Get("Accept"), "application/xml") } w.Header().Set("Content-Type", "application/xml") w.Write([]byte(`ok`)) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) // high rate to avoid blocking in tests client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } defer client.Close() body, err := client.doGet(context.Background(), "/test") if err != nil { t.Fatalf("doGet() error = %v", err) } if string(body) != `ok` { t.Errorf("doGet() body = %q", string(body)) } } func TestDoGet_Non200Status(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("Rate limit exceeded")) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } defer client.Close() _, err := client.doGet(context.Background(), "/test") if err == nil { t.Fatal("doGet() expected error for non-200 status, got nil") } } func TestDoGet_ServerUnreachable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } defer client.Close() _, err := client.doGet(context.Background(), "/test") if err == nil { t.Fatal("doGet() expected error for unreachable server, got nil") } } func TestDoGet_ContextCancellation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`ok`)) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } // Use a rate limiter with 0 burst to force blocking on Wait rl := rate.NewLimiter(rate.Limit(0), 0) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } defer client.Close() ctx, cancel := context.WithCancel(context.Background()) cancel() // cancel immediately _, err := client.doGet(ctx, "/test") if err == nil { t.Fatal("doGet() expected error for cancelled context, got nil") } } func TestRateLimiter_OnePerSecond(t *testing.T) { // Verify that the rate limiter enforces approximately 1 request per second rl := rate.NewLimiter(rate.Limit(1), 1) // First request should be immediate (burst of 1) start := time.Now() if err := rl.Wait(context.Background()); err != nil { t.Fatalf("first Wait() error: %v", err) } elapsed := time.Since(start) if elapsed > 100*time.Millisecond { t.Errorf("first Wait() took %v, expected near-instant", elapsed) } // Second request should block for approximately 1 second start = time.Now() if err := rl.Wait(context.Background()); err != nil { t.Fatalf("second Wait() error: %v", err) } elapsed = time.Since(start) if elapsed < 800*time.Millisecond { t.Errorf("second Wait() took %v, expected at least ~1s", elapsed) } if elapsed > 2*time.Second { t.Errorf("second Wait() took %v, expected less than 2s", elapsed) } } func TestRateLimiter_BurstBehavior(t *testing.T) { // With burst=1, the first request should be immediate rl := rate.NewLimiter(rate.Limit(1), 1) start := time.Now() rl.Wait(context.Background()) elapsed := time.Since(start) if elapsed > 50*time.Millisecond { t.Errorf("burst Wait() took %v, expected near-instant", elapsed) } }