mirror of
https://github.com/status-im/proxy-common.git
synced 2026-08-27 18:41:06 +00:00
333 lines
9.5 KiB
Go
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))
|
|
}
|
|
}
|