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)
}
}