Files
NaviWatcher/internal/musicbrainz/client_test.go
Vladimir Zagainov 674daed93b 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
2026-05-26 11:52:17 +03:00

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