186 lines
4.7 KiB
Go
186 lines
4.7 KiB
Go
// ©AngelaMos | 2026
|
|
// client_test.go
|
|
|
|
package httpx_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/time/rate"
|
|
|
|
"github.com/carterperez-dev/monitor-the-situation/backend/internal/httpx"
|
|
)
|
|
|
|
func TestClient_GetJSONHonorsRateLimit(t *testing.T) {
|
|
var hits atomic.Int32
|
|
srv := httptest.NewServer(
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
hits.Add(1)
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}),
|
|
)
|
|
defer srv.Close()
|
|
|
|
c := httpx.New(httpx.Config{
|
|
Name: "test",
|
|
BaseURL: srv.URL,
|
|
Rate: rate.Every(50 * time.Millisecond),
|
|
Burst: 1,
|
|
ConsecutiveFailureBudget: 5,
|
|
BreakerTimeout: time.Second,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
for i := 0; i < 3; i++ {
|
|
var out struct {
|
|
Ok bool `json:"ok"`
|
|
}
|
|
require.NoError(t, c.GetJSON(ctx, "/", nil, &out))
|
|
require.True(t, out.Ok)
|
|
}
|
|
require.GreaterOrEqual(t, time.Since(start), 100*time.Millisecond)
|
|
require.EqualValues(t, 3, hits.Load())
|
|
}
|
|
|
|
func TestClient_RetriesOn429WithRetryAfter(t *testing.T) {
|
|
var hits atomic.Int32
|
|
srv := httptest.NewServer(
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
if hits.Add(1) == 1 {
|
|
w.Header().Set("Retry-After", "1")
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}),
|
|
)
|
|
defer srv.Close()
|
|
|
|
c := httpx.New(httpx.Config{
|
|
Name: "retry",
|
|
BaseURL: srv.URL,
|
|
Rate: rate.Inf,
|
|
Burst: 10,
|
|
ConsecutiveFailureBudget: 5,
|
|
BreakerTimeout: time.Second,
|
|
MaxRetryElapsed: 3 * time.Second,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
|
defer cancel()
|
|
|
|
var out struct {
|
|
Ok bool `json:"ok"`
|
|
}
|
|
require.NoError(t, c.GetJSON(ctx, "/", nil, &out))
|
|
require.True(t, out.Ok)
|
|
require.EqualValues(t, 2, hits.Load())
|
|
}
|
|
|
|
func TestClient_RetriesOn5xx(t *testing.T) {
|
|
var hits atomic.Int32
|
|
srv := httptest.NewServer(
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
if hits.Add(1) < 3 {
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}),
|
|
)
|
|
defer srv.Close()
|
|
|
|
c := httpx.New(httpx.Config{
|
|
Name: "5xx",
|
|
BaseURL: srv.URL,
|
|
Rate: rate.Inf,
|
|
Burst: 10,
|
|
ConsecutiveFailureBudget: 10,
|
|
BreakerTimeout: time.Second,
|
|
MaxRetryElapsed: 5 * time.Second,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
|
defer cancel()
|
|
|
|
var out struct {
|
|
Ok bool `json:"ok"`
|
|
}
|
|
require.NoError(t, c.GetJSON(ctx, "/", nil, &out))
|
|
require.EqualValues(t, 3, hits.Load())
|
|
}
|
|
|
|
func TestClient_PermanentErrorOn4xx(t *testing.T) {
|
|
var hits atomic.Int32
|
|
srv := httptest.NewServer(
|
|
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
hits.Add(1)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`bad`))
|
|
}),
|
|
)
|
|
defer srv.Close()
|
|
|
|
c := httpx.New(httpx.Config{
|
|
Name: "perm",
|
|
BaseURL: srv.URL,
|
|
Rate: rate.Inf,
|
|
Burst: 10,
|
|
ConsecutiveFailureBudget: 10,
|
|
BreakerTimeout: time.Second,
|
|
MaxRetryElapsed: 3 * time.Second,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
var out struct{}
|
|
err := c.GetJSON(ctx, "/", nil, &out)
|
|
require.Error(t, err)
|
|
require.EqualValues(t, 1, hits.Load())
|
|
}
|
|
|
|
func TestClient_AddsBearerAndAPIKeyHeaders(t *testing.T) {
|
|
var bearer, apiKey, ua string
|
|
srv := httptest.NewServer(
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
bearer = r.Header.Get("Authorization")
|
|
apiKey = r.Header.Get("X-Custom-Key")
|
|
ua = r.Header.Get("User-Agent")
|
|
_, _ = w.Write([]byte(`{}`))
|
|
}),
|
|
)
|
|
defer srv.Close()
|
|
|
|
c := httpx.New(httpx.Config{
|
|
Name: "headers",
|
|
BaseURL: srv.URL,
|
|
BearerToken: "bearer-abc",
|
|
APIKey: "key-xyz",
|
|
APIKeyHeader: "X-Custom-Key",
|
|
UserAgent: "monitor-test/1.0",
|
|
Rate: rate.Inf,
|
|
Burst: 1,
|
|
ConsecutiveFailureBudget: 5,
|
|
BreakerTimeout: time.Second,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
var out struct{}
|
|
require.NoError(t, c.GetJSON(ctx, "/", nil, &out))
|
|
require.Equal(t, "Bearer bearer-abc", bearer)
|
|
require.Equal(t, "key-xyz", apiKey)
|
|
require.Equal(t, "monitor-test/1.0", ua)
|
|
}
|