Files
2026-02-06 18:14:28 +04:00

333 lines
9.5 KiB
Go

package httpclient
import (
"bytes"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// MockHttpStatusHandler implements IHttpStatusHandler for testing
type MockHttpStatusHandler struct {
requestStatuses []string
retryCount int
}
func NewMockHttpStatusHandler() *MockHttpStatusHandler {
return &MockHttpStatusHandler{
requestStatuses: make([]string, 0),
retryCount: 0,
}
}
func (m *MockHttpStatusHandler) OnRequest(status string) {
m.requestStatuses = append(m.requestStatuses, status)
}
func (m *MockHttpStatusHandler) OnRetry() {
m.retryCount++
}
// GetRequestCount returns the number of requests recorded
func (m *MockHttpStatusHandler) GetRequestCount() int {
return len(m.requestStatuses)
}
// GetRetryCount returns the number of retries recorded
func (m *MockHttpStatusHandler) GetRetryCount() int {
return m.retryCount
}
// TestHTTPClientWithRetries_Timeouts tests that the Client correctly applies timeouts
func TestHTTPClientWithRetries_Timeouts(t *testing.T) {
// Create a test server that sleeps to simulate slow responses
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
delay := r.URL.Query().Get("delay")
switch delay {
case "connection":
time.Sleep(500 * time.Millisecond)
case "response":
time.Sleep(500 * time.Millisecond)
}
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil {
return
}
}))
defer server.Close()
// Test with short timeout
t.Run("RequestTimeout", func(t *testing.T) {
opts := DefaultRetryOptions()
opts.RequestTimeout = 100 * time.Millisecond // Very short timeout
opts.MaxRetries = 3
mockHandler := NewMockHttpStatusHandler()
client := NewHTTPClientWithRetries(opts, mockHandler, nil)
req, _ := http.NewRequest("GET", server.URL+"?delay=response", nil)
_, _, _, err := client.ExecuteRequest(req)
if err == nil {
t.Error("Expected timeout error, got none")
}
// Should have recorded errors
if mockHandler.GetRequestCount() < 1 {
t.Errorf("Expected at least 1 request status, got %d", mockHandler.GetRequestCount())
}
// Should have recorded retries
if mockHandler.GetRetryCount() < 1 {
t.Errorf("Expected at least 1 retry, got %d", mockHandler.GetRetryCount())
}
})
// Test with sufficient timeout
t.Run("NoTimeout", func(t *testing.T) {
opts := DefaultRetryOptions()
opts.RequestTimeout = 2 * time.Second // Sufficient timeout
mockHandler := NewMockHttpStatusHandler()
client := NewHTTPClientWithRetries(opts, mockHandler, nil)
req, _ := http.NewRequest("GET", server.URL, nil)
_, _, _, err := client.ExecuteRequest(req)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
// Should have recorded a success
if mockHandler.GetRequestCount() != 1 || mockHandler.requestStatuses[0] != "success" {
t.Errorf("Expected 1 success status, got %v", mockHandler.requestStatuses)
}
// Should not have recorded retries
if mockHandler.GetRetryCount() != 0 {
t.Errorf("Expected 0 retries, got %d", mockHandler.GetRetryCount())
}
})
}
// TestHTTPClientWithRetries_Retries tests the retry behavior
func TestHTTPClientWithRetries_Retries(t *testing.T) {
// Track request attempts
attempts := 0
// Create a test server that fails initially and succeeds after retries
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
// Fail the first two attempts
if attempts <= 2 {
w.WriteHeader(http.StatusServiceUnavailable) // 503 Service Unavailable
if _, err := w.Write([]byte(`{"error":"service unavailable"}`)); err != nil {
return
}
return
}
// Succeed on the third attempt
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil {
return
}
}))
defer server.Close()
// Configure Client with 3 retries and minimal backoff
opts := DefaultRetryOptions()
opts.MaxRetries = 3
opts.BaseBackoff = 10 * time.Millisecond // Minimal backoff for tests
mockHandler := NewMockHttpStatusHandler()
client := NewHTTPClientWithRetries(opts, mockHandler, nil)
req, _ := http.NewRequest("GET", server.URL, nil)
resp, body, duration, err := client.ExecuteRequest(req)
// Check results
if err != nil {
t.Errorf("Expected successful request after retries, got error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
if string(body) != `{"status":"ok"}` {
t.Errorf("Expected body '{\"status\":\"ok\"}', got '%s'", string(body))
}
if attempts != 3 {
t.Errorf("Expected 3 attempts, got %d", attempts)
}
if duration <= 0 {
t.Errorf("Expected positive duration, got %v", duration)
}
// Should have recorded 2 rate_limited and 1 success
if mockHandler.GetRequestCount() != 3 ||
mockHandler.requestStatuses[0] != "rate_limited" ||
mockHandler.requestStatuses[1] != "rate_limited" ||
mockHandler.requestStatuses[2] != "success" {
t.Errorf("Expected [rate_limited, rate_limited, success] statuses, got %v", mockHandler.requestStatuses)
}
// Should have recorded 2 retries
if mockHandler.GetRetryCount() != 2 {
t.Errorf("Expected 2 retries, got %d", mockHandler.GetRetryCount())
}
}
// TestHTTPClientWithRetries_NonRetryableError tests that non-retryable errors fail immediately
func TestHTTPClientWithRetries_NonRetryableError(t *testing.T) {
// Track request attempts
attempts := 0
// Create a test server that returns a non-retryable error (400 Bad Request)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request - non-retryable
if _, err := w.Write([]byte(`{"error":"bad request"}`)); err != nil {
return
}
}))
defer server.Close()
// Configure Client with retries
opts := DefaultRetryOptions()
opts.MaxRetries = 3
mockHandler := NewMockHttpStatusHandler()
client := NewHTTPClientWithRetries(opts, mockHandler, nil)
req, _ := http.NewRequest("GET", server.URL, nil)
_, _, _, err := client.ExecuteRequest(req)
// Should get an error
if err == nil {
t.Error("Expected error for non-retryable status code, got none")
}
// Should have attempted only once
if attempts != 1 {
t.Errorf("Expected 1 attempt for non-retryable error, got %d", attempts)
}
// Should have recorded error without retry
if mockHandler.GetRequestCount() != 1 || mockHandler.requestStatuses[0] != "error" {
t.Errorf("Expected [error] status, got %v", mockHandler.requestStatuses)
}
// Should not have recorded retries
if mockHandler.GetRetryCount() != 0 {
t.Errorf("Expected 0 retries, got %d", mockHandler.GetRetryCount())
}
}
// mockTransport is a mock http.RoundTripper for testing custom behavior
type mockTransport struct {
roundTripFunc func(req *http.Request) (*http.Response, error)
}
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return m.roundTripFunc(req)
}
// TestHTTPClientWithRetries_NetworkErrors tests handling of various network errors
func TestHTTPClientWithRetries_NetworkErrors(t *testing.T) {
// Configure Client with retries
opts := DefaultRetryOptions()
opts.MaxRetries = 2
opts.BaseBackoff = 10 * time.Millisecond
mockHandler := NewMockHttpStatusHandler()
client := NewHTTPClientWithRetries(opts, mockHandler, nil)
// Replace the Client's transport with our mock
errorReturned := false
client.Client.Transport = &mockTransport{
roundTripFunc: func(req *http.Request) (*http.Response, error) {
if !errorReturned {
// First request fails with connection reset
errorReturned = true
return nil, errors.New("connection reset by peer")
}
// Subsequent requests succeed
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(`{"status":"ok"}`)),
Header: make(http.Header),
Request: req,
}, nil
},
}
req, _ := http.NewRequest("GET", "http://example.com", nil)
resp, body, _, err := client.ExecuteRequest(req)
// Should not get an error
if err != nil {
t.Errorf("Expected success after retrying network error, got: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
if string(body) != `{"status":"ok"}` {
t.Errorf("Expected body '{\"status\":\"ok\"}', got '%s'", string(body))
}
// Should have recorded error followed by success
if mockHandler.GetRequestCount() != 2 ||
mockHandler.requestStatuses[0] != "error" ||
mockHandler.requestStatuses[1] != "success" {
t.Errorf("Expected [error, success] statuses, got %v", mockHandler.requestStatuses)
}
// Should have recorded 1 retry
if mockHandler.GetRetryCount() != 1 {
t.Errorf("Expected 1 retry, got %d", mockHandler.GetRetryCount())
}
}
// Test with no metrics handler
func TestHTTPClientWithRetries_NoHandler(t *testing.T) {
// Create a server that always succeeds
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil {
return
}
}))
defer server.Close()
// Configure Client with null handler
opts := DefaultRetryOptions()
client := NewHTTPClientWithRetries(opts, nil, nil)
req, _ := http.NewRequest("GET", server.URL, nil)
resp, body, _, err := client.ExecuteRequest(req)
// Should succeed without error
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
if string(body) != `{"status":"ok"}` {
t.Errorf("Expected body '{\"status\":\"ok\"}', got '%s'", string(body))
}
}