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