213 lines
6.0 KiB
Go
213 lines
6.0 KiB
Go
package coverart
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHTTPClient_GetJSON_Success(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"name":"alice"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{Name: "test"})
|
|
var got struct{ Name string }
|
|
if err := c.getJSON(context.Background(), srv.URL, &got); err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if got.Name != "alice" {
|
|
t.Errorf("Name = %q, want alice", got.Name)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_404IsNotFound(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{Name: "test"})
|
|
var out any
|
|
err := c.getJSON(context.Background(), srv.URL, &out)
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("err = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_RetryOn429ThenSuccess(t *testing.T) {
|
|
var calls atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
n := calls.Add(1)
|
|
if n < 3 {
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{
|
|
Name: "test",
|
|
MaxRetries: 3,
|
|
BaseBackoff: 1 * time.Millisecond, // tiny so the test is fast
|
|
})
|
|
var got struct{ OK bool }
|
|
if err := c.getJSON(context.Background(), srv.URL, &got); err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if calls.Load() != 3 {
|
|
t.Errorf("calls = %d, want 3", calls.Load())
|
|
}
|
|
if !got.OK {
|
|
t.Errorf("OK = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_RetryOn5xxThenExhausted(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{
|
|
Name: "test",
|
|
MaxRetries: 2,
|
|
BaseBackoff: 1 * time.Millisecond,
|
|
})
|
|
var out any
|
|
err := c.getJSON(context.Background(), srv.URL, &out)
|
|
if !errors.Is(err, ErrTransient) {
|
|
t.Errorf("err = %v, want ErrTransient", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_AuthAsTransient(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{
|
|
Name: "test",
|
|
TreatAuthAsTransient: true,
|
|
})
|
|
var out any
|
|
err := c.getJSON(context.Background(), srv.URL, &out)
|
|
if !errors.Is(err, ErrTransient) {
|
|
t.Errorf("err = %v, want ErrTransient", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_AuthPassthroughByDefault(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{Name: "test"})
|
|
var out any
|
|
err := c.getJSON(context.Background(), srv.URL, &out)
|
|
if !errors.Is(err, ErrTransient) {
|
|
// 401 is non-2xx so it falls into the generic transient bucket.
|
|
t.Errorf("err = %v, want ErrTransient (generic non-2xx)", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_MalformedBody(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`not json`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{Name: "test"})
|
|
var got struct{ Name string }
|
|
err := c.getJSON(context.Background(), srv.URL, &got)
|
|
if !errors.Is(err, ErrTransient) {
|
|
t.Errorf("err = %v, want ErrTransient", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetJSON_ContextCancel(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{
|
|
Name: "test",
|
|
MaxRetries: 5,
|
|
BaseBackoff: 200 * time.Millisecond,
|
|
})
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
var out any
|
|
err := c.getJSON(ctx, srv.URL, &out)
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("err = %v, want context.DeadlineExceeded", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetImage_Success(t *testing.T) {
|
|
want := []byte("\x89PNG\x0d\x0a\x1a\x0a-fake-png-bytes")
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(want)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{Name: "test"})
|
|
got, err := c.getImage(context.Background(), srv.URL)
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Errorf("body mismatch")
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_GetImage_404IsNotFound(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{Name: "test"})
|
|
_, err := c.getImage(context.Background(), srv.URL)
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("err = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestHTTPClient_RateLimit(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(struct{ OK bool }{true})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := newHTTPClient(httpClientOptions{
|
|
Name: "test",
|
|
MinInterval: 50 * time.Millisecond,
|
|
})
|
|
var out struct{ OK bool }
|
|
t0 := time.Now()
|
|
if err := c.getJSON(context.Background(), srv.URL, &out); err != nil {
|
|
t.Fatalf("first call: %v", err)
|
|
}
|
|
if err := c.getJSON(context.Background(), srv.URL, &out); err != nil {
|
|
t.Fatalf("second call: %v", err)
|
|
}
|
|
elapsed := time.Since(t0)
|
|
if elapsed < 50*time.Millisecond {
|
|
t.Errorf("elapsed %v, want >= 50ms (rate limit)", elapsed)
|
|
}
|
|
}
|