Files
httpclient/httpclient_test.go

177 lines
5.1 KiB
Go
Raw Permalink Normal View History

package httpclient
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"code.nochebuena.dev/go/logz"
"code.nochebuena.dev/go/xerrors"
)
func newLogger() logz.Logger { return logz.New(logz.Options{}) }
func TestNew(t *testing.T) {
if New(newLogger(), DefaultConfig()) == nil {
t.Fatal("New returned nil")
}
}
func TestNewWithDefaults(t *testing.T) {
c := New(newLogger(), DefaultConfig())
if c == nil {
t.Fatal("NewWithDefaults returned nil")
}
}
func TestClient_Do_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
client := New(newLogger(), Config{
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
MaxRetries: 1, RetryDelay: 10 * time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
})
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("want 200, got %d", resp.StatusCode)
}
}
func TestClient_Do_Retry5xx(t *testing.T) {
calls := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
w.WriteHeader(http.StatusInternalServerError)
}))
defer srv.Close()
client := New(newLogger(), Config{
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
MaxRetries: 3, RetryDelay: 1 * time.Millisecond, CBThreshold: 100, CBTimeout: time.Minute,
})
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
_, err := client.Do(req)
if err == nil {
t.Fatal("expected error after retries")
}
if calls < 2 {
t.Errorf("expected multiple calls, got %d", calls)
}
}
func TestClient_Do_InjectsRequestID(t *testing.T) {
var gotHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotHeader = r.Header.Get("X-Request-ID")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
client := New(newLogger(), Config{
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
MaxRetries: 1, RetryDelay: time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
})
ctx := logz.WithRequestID(t.Context(), "req-123")
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL, nil)
_, _ = client.Do(req)
if gotHeader != "req-123" {
t.Errorf("want X-Request-ID=req-123, got %q", gotHeader)
}
}
func TestClient_Do_NoRequestID(t *testing.T) {
var gotHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotHeader = r.Header.Get("X-Request-ID")
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
client := New(newLogger(), Config{
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
MaxRetries: 1, RetryDelay: time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
})
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
_, _ = client.Do(req)
if gotHeader != "" {
t.Errorf("expected no X-Request-ID header, got %q", gotHeader)
}
}
func TestDoJSON_Success(t *testing.T) {
type payload struct{ Name string }
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(payload{Name: "alice"})
}))
defer srv.Close()
client := New(newLogger(), Config{
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
MaxRetries: 1, RetryDelay: time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
})
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
result, err := DoJSON[payload](t.Context(), client, req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Name != "alice" {
t.Errorf("want alice, got %s", result.Name)
}
}
func TestDoJSON_4xx(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
client := New(newLogger(), Config{
Name: "test", Timeout: 5 * time.Second, DialTimeout: 2 * time.Second,
MaxRetries: 1, RetryDelay: time.Millisecond, CBThreshold: 10, CBTimeout: time.Minute,
})
req, _ := http.NewRequest(http.MethodGet, srv.URL, nil)
_, err := DoJSON[struct{}](t.Context(), client, req)
if err == nil {
t.Fatal("expected error")
}
var xe *xerrors.Err
if !errors.As(err, &xe) || xe.Code() != xerrors.ErrNotFound {
t.Errorf("want ErrNotFound, got %v", err)
}
}
func TestMapStatusToError_AllCodes(t *testing.T) {
cases := []struct {
status int
code xerrors.Code
}{
{http.StatusNotFound, xerrors.ErrNotFound},
{http.StatusBadRequest, xerrors.ErrInvalidInput},
{http.StatusUnauthorized, xerrors.ErrUnauthorized},
{http.StatusForbidden, xerrors.ErrPermissionDenied},
{http.StatusConflict, xerrors.ErrAlreadyExists},
{http.StatusTooManyRequests, xerrors.ErrUnavailable},
{http.StatusInternalServerError, xerrors.ErrInternal},
}
for _, tc := range cases {
err := MapStatusToError(tc.status, "msg")
var xe *xerrors.Err
if !errors.As(err, &xe) || xe.Code() != tc.code {
t.Errorf("status %d: want %s, got %v", tc.status, tc.code, err)
}
}
}