Skip to content

Commit eff0d0a

Browse files
authored
feat: support custom HTTP client injection (#168)
* feat: support custom HTTP client injection * fix: use custom http client for userinfo fetching * test: address review comments
1 parent 88fa464 commit eff0d0a

File tree

4 files changed

+145
-8
lines changed

4 files changed

+145
-8
lines changed

client/client.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,24 @@ type GetOrganizationTokenClaimsOptions struct {
3131
OrganizationId string
3232
}
3333

34+
// LogtoClientOption is a functional option for configuring LogtoClient
35+
type LogtoClientOption func(*LogtoClient)
36+
37+
// WithHttpClient sets a custom HTTP client for the LogtoClient
38+
func WithHttpClient(client *http.Client) LogtoClientOption {
39+
return func(c *LogtoClient) {
40+
c.httpClient = client
41+
}
42+
}
43+
3444
type LogtoClient struct {
3545
httpClient *http.Client
3646
logtoConfig *LogtoConfig
3747
storage Storage
3848
accessTokenMap map[string]AccessToken
3949
}
4050

41-
func NewLogtoClient(config *LogtoConfig, storage Storage) *LogtoClient {
51+
func NewLogtoClient(config *LogtoConfig, storage Storage, opts ...LogtoClientOption) *LogtoClient {
4252
config.normalized()
4353
logtoClient := LogtoClient{
4454
httpClient: &http.Client{},
@@ -47,6 +57,11 @@ func NewLogtoClient(config *LogtoConfig, storage Storage) *LogtoClient {
4757
accessTokenMap: make(map[string]AccessToken),
4858
}
4959

60+
// Apply options
61+
for _, opt := range opts {
62+
opt(&logtoClient)
63+
}
64+
5065
logtoClient.loadAccessTokenMap()
5166

5267
return &logtoClient
@@ -236,5 +251,5 @@ func (logtoClient *LogtoClient) FetchUserInfo() (core.UserInfoResponse, error) {
236251
return core.UserInfoResponse{}, getAccessTokenErr
237252
}
238253

239-
return core.FetchUserInfo(oidcConfig.UserinfoEndpoint, accessToken.Token)
254+
return core.FetchUserInfoWithClient(logtoClient.httpClient, oidcConfig.UserinfoEndpoint, accessToken.Token)
240255
}

client/client_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ func TestFetchUserInfoShouldReturnCorrectUserInfoResponse(t *testing.T) {
181181
})
182182
defer patchesForGetAccessToken.Reset()
183183

184-
patchesCoreFetchUserInfo := gomonkey.ApplyFunc(core.FetchUserInfo, func(userInfoEndpoint, accessToken string) (core.UserInfoResponse, error) {
184+
patchesCoreFetchUserInfo := gomonkey.ApplyFunc(core.FetchUserInfoWithClient, func(client *http.Client, userInfoEndpoint, accessToken string) (core.UserInfoResponse, error) {
185185
return testUserInfoResponse, nil
186186
})
187187
defer patchesCoreFetchUserInfo.Reset()
@@ -254,3 +254,85 @@ func TestFetchUserInfoShouldReturnErrorIfGetAccessTokenFailed(t *testing.T) {
254254

255255
assert.Equal(t, testGetAccessTokenErr, fetchUserInfoErr)
256256
}
257+
258+
func TestNewLogtoClientShouldUseDefaultHttpClientWhenNoOptionsProvided(t *testing.T) {
259+
logtoClient := NewLogtoClient(&LogtoConfig{}, &TestStorage{})
260+
261+
assert.NotNil(t, logtoClient.httpClient)
262+
// The default HTTP client should be a basic http.Client
263+
assert.IsType(t, &http.Client{}, logtoClient.httpClient)
264+
}
265+
266+
func TestNewLogtoClientShouldUseCustomHttpClientWhenWithHttpClientOptionProvided(t *testing.T) {
267+
customClient := &http.Client{
268+
Timeout: 10 * time.Second,
269+
}
270+
271+
logtoClient := NewLogtoClient(&LogtoConfig{}, &TestStorage{}, WithHttpClient(customClient))
272+
273+
assert.NotNil(t, logtoClient.httpClient)
274+
assert.Equal(t, customClient, logtoClient.httpClient)
275+
assert.Equal(t, 10*time.Second, logtoClient.httpClient.Timeout)
276+
}
277+
278+
func TestNewLogtoClientShouldApplyMultipleOptions(t *testing.T) {
279+
customClient := &http.Client{
280+
Timeout: 5 * time.Second,
281+
}
282+
283+
// Create a mock second option for demonstration
284+
var appliedSecondOption bool
285+
mockSecondOption := func(client *LogtoClient) {
286+
appliedSecondOption = true
287+
}
288+
289+
logtoClient := NewLogtoClient(&LogtoConfig{}, &TestStorage{},
290+
WithHttpClient(customClient),
291+
mockSecondOption,
292+
)
293+
294+
// Verify first option (WithHttpClient) was applied
295+
assert.NotNil(t, logtoClient.httpClient)
296+
assert.Equal(t, customClient, logtoClient.httpClient)
297+
assert.Equal(t, 5*time.Second, logtoClient.httpClient.Timeout)
298+
299+
// Verify second option was applied
300+
assert.True(t, appliedSecondOption, "Second option should have been applied")
301+
}
302+
303+
func TestWithHttpClientShouldReturnValidOption(t *testing.T) {
304+
customClient := &http.Client{
305+
Timeout: 30 * time.Second,
306+
}
307+
308+
option := WithHttpClient(customClient)
309+
310+
assert.NotNil(t, option)
311+
312+
// Test that the option function works correctly
313+
logtoClient := &LogtoClient{
314+
httpClient: &http.Client{}, // Default client
315+
}
316+
317+
// Apply the option
318+
option(logtoClient)
319+
320+
assert.Equal(t, customClient, logtoClient.httpClient)
321+
assert.Equal(t, 30*time.Second, logtoClient.httpClient.Timeout)
322+
}
323+
324+
func TestLogtoClientShouldUseCustomHttpClientForFetchUserInfo(t *testing.T) {
325+
customClient := &http.Client{
326+
Timeout: 15 * time.Second,
327+
}
328+
329+
logtoClient := NewLogtoClient(&LogtoConfig{}, &TestStorage{}, WithHttpClient(customClient))
330+
331+
// Verify that the client is using the custom HTTP client
332+
assert.Equal(t, customClient, logtoClient.httpClient)
333+
assert.Equal(t, 15*time.Second, logtoClient.httpClient.Timeout)
334+
335+
// This test verifies that the LogtoClient is configured with the custom HTTP client
336+
// The actual FetchUserInfo will use this client when making HTTP requests
337+
assert.NotNil(t, logtoClient.httpClient)
338+
}

core/fetch_user_info.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package core
22

33
import "net/http"
44

5-
func FetchUserInfo(userInfoEndpoint, accessToken string) (UserInfoResponse, error) {
6-
client := &http.Client{}
7-
5+
// FetchUserInfoWithClient fetches user info using a custom HTTP client.
6+
// This function allows you to use a custom HTTP client for observability,
7+
// tracing, or other custom configurations.
8+
func FetchUserInfoWithClient(client *http.Client, userInfoEndpoint, accessToken string) (UserInfoResponse, error) {
89
request, createRequestErr := http.NewRequest("GET", userInfoEndpoint, nil)
910

1011
if createRequestErr != nil {
@@ -30,3 +31,9 @@ func FetchUserInfo(userInfoEndpoint, accessToken string) (UserInfoResponse, erro
3031

3132
return userInfoResponse, nil
3233
}
34+
35+
// FetchUserInfo fetches user info using the default HTTP client.
36+
// Deprecated: Use FetchUserInfoWithClient instead for better flexibility and observability support.
37+
func FetchUserInfo(userInfoEndpoint, accessToken string) (UserInfoResponse, error) {
38+
return FetchUserInfoWithClient(&http.Client{}, userInfoEndpoint, accessToken)
39+
}

core/fetch_user_info_test.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package core
22

33
import (
44
"encoding/json"
5+
"net/http"
56
"testing"
7+
"time"
68

79
"github.com/jarcoal/httpmock"
810
"github.com/stretchr/testify/assert"
911
)
1012

11-
func TestFetchUserInfo(t *testing.T) {
13+
func TestFetchUserInfoWithClient(t *testing.T) {
1214
httpmock.Activate()
1315
defer httpmock.DeactivateAndReset()
1416

@@ -36,7 +38,12 @@ func TestFetchUserInfo(t *testing.T) {
3638
httpmock.NewStringResponder(200, mockResponse),
3739
)
3840

39-
userInfoResponse, fetchError := FetchUserInfo(userInfoEndpoint, "accessToken")
41+
// Test with custom HTTP client
42+
customClient := &http.Client{
43+
Timeout: 10 * time.Second,
44+
}
45+
46+
userInfoResponse, fetchError := FetchUserInfoWithClient(customClient, userInfoEndpoint, "accessToken")
4047
assert.Nil(t, fetchError)
4148

4249
var testUserInfoResponse UserInfoResponse
@@ -45,3 +52,29 @@ func TestFetchUserInfo(t *testing.T) {
4552

4653
assert.Equal(t, testUserInfoResponse, userInfoResponse)
4754
}
55+
56+
// TestFetchUserInfoDeprecated tests that the deprecated function delegates to the new one
57+
func TestFetchUserInfoDeprecated(t *testing.T) {
58+
httpmock.Activate()
59+
defer httpmock.DeactivateAndReset()
60+
61+
userInfoEndpoint := "http://example.com/oidc/jwks"
62+
mockResponse := `{` +
63+
`"sub": "sub",` +
64+
`"name": "name",` +
65+
`"username": "username"` +
66+
`}`
67+
68+
httpmock.RegisterResponder(
69+
"GET",
70+
userInfoEndpoint,
71+
httpmock.NewStringResponder(200, mockResponse),
72+
)
73+
74+
// Test that deprecated function still works by calling the new implementation
75+
userInfoResponse, fetchError := FetchUserInfo(userInfoEndpoint, "accessToken")
76+
assert.Nil(t, fetchError)
77+
assert.Equal(t, "sub", userInfoResponse.Sub)
78+
assert.Equal(t, "name", userInfoResponse.Name)
79+
assert.Equal(t, "username", userInfoResponse.Username)
80+
}

0 commit comments

Comments
 (0)