feat: create MusicBrainz client and data models
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
This commit is contained in:
167
internal/musicbrainz/client_test.go
Normal file
167
internal/musicbrainz/client_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user