Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions client/custom_httpclient_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
127 changes: 127 additions & 0 deletions twilio_custom_httpclient_integration_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
135 changes: 135 additions & 0 deletions twilio_proxy_test.go
Original file line number Diff line number Diff line change
@@ -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")
}