Add internal/musicbrainz/ package with: - client.go: MusicBrainzClient struct wrapping net/http.Client with channel-based rate limiter (1 req/sec), doGet method with proper User-Agent header, and Close for cleanup - model.go: ReleaseGroup, Artist, ExternalRelease, and Parsed* structs - XML parsing functions for release-group list and artist responses - Comprehensive tests: XML parsing (success, empty, malformed), client constructor, doGet (success, non-200, unreachable server), rate limiter behavior
168 lines
4.0 KiB
Go
168 lines
4.0 KiB
Go
package musicbrainz
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"naviwatcher/internal/config"
|
|
)
|
|
|
|
func newUnbufferedRateLimiter() *rateLimiter {
|
|
return newRateLimiter(1000) // high rate to avoid blocking in tests
|
|
}
|
|
|
|
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 := newRateLimiter(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(`<metadata><test>ok</test></metadata>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
cfg := config.MusicBrainzConfig{
|
|
UserAgent: "naviwatcher/0.1.0 (test@example.com)",
|
|
}
|
|
|
|
rl := newUnbufferedRateLimiter()
|
|
client := &MusicBrainzClient{
|
|
httpClient: server.Client(),
|
|
userAgent: cfg.UserAgent,
|
|
baseURL: server.URL,
|
|
rateLimiter: rl,
|
|
}
|
|
defer client.Close()
|
|
|
|
body, err := client.doGet("/test")
|
|
if err != nil {
|
|
t.Fatalf("doGet() error = %v", err)
|
|
}
|
|
|
|
if string(body) != `<metadata><test>ok</test></metadata>` {
|
|
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 := newUnbufferedRateLimiter()
|
|
client := &MusicBrainzClient{
|
|
httpClient: server.Client(),
|
|
userAgent: cfg.UserAgent,
|
|
baseURL: server.URL,
|
|
rateLimiter: rl,
|
|
}
|
|
defer client.Close()
|
|
|
|
_, err := client.doGet("/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 := newUnbufferedRateLimiter()
|
|
client := &MusicBrainzClient{
|
|
httpClient: server.Client(),
|
|
userAgent: cfg.UserAgent,
|
|
baseURL: server.URL,
|
|
rateLimiter: rl,
|
|
}
|
|
defer client.Close()
|
|
|
|
_, err := client.doGet("/test")
|
|
if err == nil {
|
|
t.Fatal("doGet() expected error for unreachable server, got nil")
|
|
}
|
|
}
|
|
|
|
func TestRateLimiter_BasicBehavior(t *testing.T) {
|
|
// Test that rate limiter can produce tokens
|
|
rl := newRateLimiter(1)
|
|
defer rl.stop()
|
|
|
|
// Should be able to get a token immediately (bucket was pre-filled)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
rl.wait()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// success
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("rateLimiter.wait() blocked on pre-filled bucket")
|
|
}
|
|
}
|