diff --git a/client/custom_httpclient_test.go b/client/custom_httpclient_test.go new file mode 100644 index 000000000..7e3f0f2d5 --- /dev/null +++ b/client/custom_httpclient_test.go @@ -0,0 +1,135 @@ +package client_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/twilio/twilio-go/client" +) + +// TestCustomHTTPClientUsed tests that a custom HTTPClient is actually used for requests +func TestCustomHTTPClientUsed(t *testing.T) { + // Track if custom transport was used + customTransportUsed := false + + // Create a custom transport that tracks usage + customTransport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + customTransportUsed = true + // Return no proxy, just track that this was called + return nil, nil + }, + } + + // Create http.Client with custom transport + httpClient := &http.Client{ + Transport: customTransport, + } + + // Create mock server + mockServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(200) + writer.Write([]byte(`{"status":"ok"}`)) + })) + defer mockServer.Close() + + // Create base client with custom HTTPClient + baseClient := &client.Client{ + Credentials: client.NewCredentials("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), + HTTPClient: httpClient, + } + baseClient.SetAccountSid("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // Test sending a request through the client + resp, err := baseClient.SendRequest("GET", mockServer.URL+"/test", nil, map[string]interface{}{ + "Content-Type": "application/x-www-form-urlencoded", + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, 200, resp.StatusCode) + + // Verify custom transport was used + assert.True(t, customTransportUsed, "Custom HTTPClient transport should have been used") +} + +// TestCustomHTTPClientViaRequestHandler tests that custom HTTPClient works through RequestHandler +func TestCustomHTTPClientViaRequestHandler(t *testing.T) { + // Track if custom transport was used + customTransportUsed := false + + // Create a custom transport that tracks usage + customTransport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + customTransportUsed = true + // Return no proxy, just track that this was called + return nil, nil + }, + } + + // Create http.Client with custom transport + httpClient := &http.Client{ + Transport: customTransport, + } + + // Create mock server + mockServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(200) + writer.Write([]byte(`{"status":"ok"}`)) + })) + defer mockServer.Close() + + // Create base client with custom HTTPClient + baseClient := &client.Client{ + Credentials: client.NewCredentials("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), + HTTPClient: httpClient, + } + baseClient.SetAccountSid("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // Create RequestHandler with the base client + requestHandler := client.NewRequestHandler(baseClient) + + // Test sending a request through the RequestHandler + resp, err := requestHandler.Get(mockServer.URL+"/test", nil, map[string]interface{}{ + "Content-Type": "application/x-www-form-urlencoded", + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, 200, resp.StatusCode) + + // Verify custom transport was used + assert.True(t, customTransportUsed, "Custom HTTPClient transport should have been used through RequestHandler") +} + +// TestDefaultHTTPClientCreatedWhenNil tests that a default HTTPClient is created when nil +func TestDefaultHTTPClientCreatedWhenNil(t *testing.T) { + // Create mock server + mockServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(200) + writer.Write([]byte(`{"status":"ok"}`)) + })) + defer mockServer.Close() + + // Create base client WITHOUT custom HTTPClient (nil) + baseClient := &client.Client{ + Credentials: client.NewCredentials("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), + HTTPClient: nil, // Explicitly set to nil + } + baseClient.SetAccountSid("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // Test sending a request - should create default HTTP client + resp, err := baseClient.SendRequest("GET", mockServer.URL+"/test", nil, map[string]interface{}{ + "Content-Type": "application/x-www-form-urlencoded", + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, 200, resp.StatusCode) +} diff --git a/twilio_custom_httpclient_integration_test.go b/twilio_custom_httpclient_integration_test.go new file mode 100644 index 000000000..af9725aea --- /dev/null +++ b/twilio_custom_httpclient_integration_test.go @@ -0,0 +1,127 @@ +package twilio + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/twilio/twilio-go/client" +) + +// TestRestClientWithCustomHTTPClient tests that custom HTTPClient works through full RestClient flow +func TestRestClientWithCustomHTTPClient(t *testing.T) { + // Track if custom transport was used + customTransportUsed := false + + // Create a custom transport that tracks usage + customTransport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + customTransportUsed = true + // Return no proxy, just track that this was called + return nil, nil + }, + } + + // Create http.Client with custom transport + httpClient := &http.Client{ + Transport: customTransport, + } + + // Create mock Twilio API server + mockServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + // Verify Authorization header is present (basic auth) + authHeader := request.Header.Get("Authorization") + assert.NotEmpty(t, authHeader, "Authorization header should be present") + + writer.WriteHeader(200) + writer.Write([]byte(`{"account_sid":"ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","balance":"100.00","currency":"USD"}`)) + })) + defer mockServer.Close() + + // Create Twilio base client with custom HTTPClient (following documentation pattern) + baseClient := &client.Client{ + Credentials: client.NewCredentials("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), + HTTPClient: httpClient, + } + baseClient.SetAccountSid("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // Create Twilio RestClient with custom client + twilioClient := NewRestClientWithParams(ClientParams{ + Client: baseClient, + }) + + // Verify the custom client was passed through + assert.NotNil(t, twilioClient.RequestHandler) + assert.NotNil(t, twilioClient.RequestHandler.Client) + assert.Equal(t, baseClient, twilioClient.RequestHandler.Client) + + // Verify HTTPClient is accessible through the chain + clientImpl, ok := twilioClient.RequestHandler.Client.(*client.Client) + assert.True(t, ok, "Client should be of type *client.Client") + assert.Equal(t, httpClient, clientImpl.HTTPClient, "Custom HTTPClient should be preserved") + + // Make a request through the client directly to verify custom HTTPClient is used + resp, err := baseClient.SendRequest("GET", mockServer.URL+"/Balance.json", nil, map[string]interface{}{ + "Content-Type": "application/x-www-form-urlencoded", + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, 200, resp.StatusCode) + + // Verify custom transport was used + assert.True(t, customTransportUsed, "Custom HTTPClient transport should have been used for API requests") +} + +// TestRestClientWithCustomHTTPClientAndTimeout tests that SetTimeout works with custom HTTPClient +func TestRestClientWithCustomHTTPClientAndTimeout(t *testing.T) { + // Create a custom http.Client + customHTTPClient := &http.Client{ + Transport: http.DefaultTransport, + } + + // Create Twilio base client with custom HTTPClient + baseClient := &client.Client{ + Credentials: client.NewCredentials("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), + HTTPClient: customHTTPClient, + } + baseClient.SetAccountSid("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // Create Twilio RestClient + twilioClient := NewRestClientWithParams(ClientParams{ + Client: baseClient, + }) + + // Set timeout via RestClient + twilioClient.SetTimeout(30 * time.Second) + + // Verify the custom HTTPClient is still the same instance (not replaced) + clientImpl, ok := twilioClient.RequestHandler.Client.(*client.Client) + assert.True(t, ok) + assert.Equal(t, customHTTPClient, clientImpl.HTTPClient, "SetTimeout should not replace custom HTTPClient") + assert.Equal(t, 30*time.Second, clientImpl.HTTPClient.Timeout, "Timeout should be updated on custom HTTPClient") +} + +// TestRestClientWithoutCustomClient tests default behavior when no custom client provided +func TestRestClientWithoutCustomClient(t *testing.T) { + // Create RestClient without custom client (should create default) + twilioClient := NewRestClientWithParams(ClientParams{ + Username: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + Password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + }) + + // Verify a client was created + assert.NotNil(t, twilioClient.RequestHandler) + assert.NotNil(t, twilioClient.RequestHandler.Client) + + // Verify it's a default client + clientImpl, ok := twilioClient.RequestHandler.Client.(*client.Client) + assert.True(t, ok) + + // HTTPClient will be nil until first request (lazy initialization via defaultHTTPClient()) + assert.Nil(t, clientImpl.HTTPClient, "HTTPClient should be nil before first use") +} diff --git a/twilio_proxy_test.go b/twilio_proxy_test.go new file mode 100644 index 000000000..4d234a62a --- /dev/null +++ b/twilio_proxy_test.go @@ -0,0 +1,135 @@ +package twilio + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/twilio/twilio-go/client" +) + +// TestCustomHTTPClientWithProxyURL tests the exact scenario from the issue: +// Using http.ProxyURL with a custom HTTPClient +func TestCustomHTTPClientWithProxyURL(t *testing.T) { + // Create a mock proxy server that tracks if it received requests + mockProxyServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + // A real proxy would forward the request, but we just track it + writer.WriteHeader(200) + writer.Write([]byte(`{"status":"proxied"}`)) + })) + defer mockProxyServer.Close() + + // Parse proxy URL + proxyURL, err := url.Parse(mockProxyServer.URL) + assert.NoError(t, err) + + // Create http.Client with proxy (EXACT pattern from issue description) + httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, + } + + // Create Twilio client with custom HTTPClient (EXACT pattern from issue description) + accountSid := "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + authToken := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + + baseClient := &client.Client{ + Credentials: client.NewCredentials(accountSid, authToken), + HTTPClient: httpClient, + } + baseClient.SetAccountSid(accountSid) + + twilioClient := NewRestClientWithParams(ClientParams{ + Client: baseClient, + }) + + // Verify client setup + assert.NotNil(t, twilioClient) + assert.NotNil(t, twilioClient.RequestHandler) + assert.NotNil(t, twilioClient.RequestHandler.Client) + + // Create a mock Twilio API server (simulating api.twilio.com) + mockTwilioServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + // This represents the actual Twilio API + writer.WriteHeader(200) + writer.Write([]byte(`{"account_sid":"ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","balance":"100.00","currency":"USD"}`)) + })) + defer mockTwilioServer.Close() + + // Make a request - should go through proxy + // Note: In a real scenario with a real proxy, this would work + // For testing, we verify the HTTPClient with Proxy is set up correctly + resp, err := baseClient.SendRequest("GET", mockTwilioServer.URL+"/Balance.json", nil, map[string]interface{}{ + "Content-Type": "application/x-www-form-urlencoded", + }) + + // The request should succeed (our mock server responds directly) + // In a real proxy setup, the proxy would forward to the real server + assert.NoError(t, err) + assert.NotNil(t, resp) + + // The key verification: the Transport with Proxy is configured + clientImpl, ok := twilioClient.RequestHandler.Client.(*client.Client) + assert.True(t, ok) + assert.NotNil(t, clientImpl.HTTPClient) + assert.NotNil(t, clientImpl.HTTPClient.Transport) + + transportImpl, ok := clientImpl.HTTPClient.Transport.(*http.Transport) + assert.True(t, ok, "Transport should be *http.Transport") + assert.NotNil(t, transportImpl.Proxy, "Proxy function should be set") + + // Verify proxy function returns the correct proxy URL + proxyURLFromTransport, err := transportImpl.Proxy(&http.Request{URL: &url.URL{Scheme: "https", Host: "api.twilio.com"}}) + assert.NoError(t, err) + assert.Equal(t, proxyURL.String(), proxyURLFromTransport.String(), "Proxy URL should match") +} + +// TestCustomHTTPClientProxyFunctionCalled tests that the proxy function is actually invoked +func TestCustomHTTPClientProxyFunctionCalled(t *testing.T) { + proxyFunctionCalled := false + callCount := 0 + + // Create custom transport with tracking proxy function + customTransport := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + proxyFunctionCalled = true + callCount++ + // Don't actually use a proxy, return nil + return nil, nil + }, + } + + httpClient := &http.Client{ + Transport: customTransport, + } + + // Create Twilio client + baseClient := &client.Client{ + Credentials: client.NewCredentials("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), + HTTPClient: httpClient, + } + baseClient.SetAccountSid("ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // Create mock server + mockServer := httptest.NewServer(http.HandlerFunc( + func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(200) + writer.Write([]byte(`{}`)) + })) + defer mockServer.Close() + + // Make request + resp, err := baseClient.SendRequest("GET", mockServer.URL, nil, map[string]interface{}{ + "Content-Type": "application/x-www-form-urlencoded", + }) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.True(t, proxyFunctionCalled, "Proxy function should have been called") + assert.Greater(t, callCount, 0, "Proxy function should have been called at least once") +}