Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
27 changes: 19 additions & 8 deletions eppoclient/configurationrequestor.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package eppoclient

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
)
Expand All @@ -13,23 +15,19 @@ type iConfigRequestor interface {
}

type configurationRequestor struct {
httpClient httpClient
configStore *configurationStore
httpClient HttpClientInterface
configStore *configurationStore
storedFlagConfigsHash string
}

func newConfigurationRequestor(httpClient httpClient, configStore *configurationStore) *configurationRequestor {
func newConfigurationRequestor(httpClient HttpClientInterface, configStore *configurationStore) *configurationRequestor {
return &configurationRequestor{
httpClient: httpClient,
configStore: configStore,
}
}

func (ecr *configurationRequestor) GetConfiguration(experimentKey string) (flagConfiguration, error) {
if ecr.httpClient.isUnauthorized {
// should we panic here or return an error?
panic("Unauthorized: please check your SDK key")
}

result, err := ecr.configStore.GetConfiguration(experimentKey)

return result, err
Expand All @@ -42,6 +40,19 @@ func (ecr *configurationRequestor) FetchAndStoreConfigurations() {
return
}

// Calculate the hash of the current response
hash := sha256.New()
hash.Write([]byte(result))
receivedFlagConfigsHash := hex.EncodeToString(hash.Sum(nil))

// Compare the current hash with the last saved hash
if receivedFlagConfigsHash == ecr.storedFlagConfigsHash {
fmt.Println("[EppoSDK] Response has not changed, skipping deserialization and cache update.")
return
}

ecr.storedFlagConfigsHash = receivedFlagConfigsHash

var wrapper ufcResponse
err = json.Unmarshal([]byte(result), &wrapper)
if err != nil {
Expand Down
106 changes: 106 additions & 0 deletions eppoclient/configurationrequestor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package eppoclient

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

type mockHttpClient struct {
mock.Mock
}

func (m *mockHttpClient) get(url string) (string, error) {
args := m.Called(url)
return args.String(0), args.Error(1)
}

func Test_FetchAndStoreConfigurations_DoesNotDeserializeTwice(t *testing.T) {
mockHttpClient := new(mockHttpClient)
mockConfigStore := newConfigurationStore()
configRequestor := newConfigurationRequestor(mockHttpClient, mockConfigStore)

// Mock the HTTP client to return a fixed response
mockResponse1 := `
{
"createdAt": "2024-04-17T19:40:53.716Z",
"environment": {
"name": "Test"
},
"flags": {
"empty_flag": {
"key": "empty_flag",
"enabled": true,
"variationType": "STRING",
"variations": {},
"allocations": [],
"totalShards": 10000
}
}
}
`
mockCall := mockHttpClient.On("get", UFC_ENDPOINT).Return(mockResponse1, nil)

// First fetch and store configurations
configRequestor.FetchAndStoreConfigurations()

// Store the current hash
firstHash := configRequestor.storedFlagConfigsHash

// Fetch and store configurations again
configRequestor.FetchAndStoreConfigurations()

// Store the hash after the second fetch
secondHash := configRequestor.storedFlagConfigsHash

// Assert that the hash has not changed
assert.Equal(t, firstHash, secondHash)

// Assert that the configurations were only deserialized once
assert.Equal(t, 1, len(mockConfigStore.configs))
assert.Equal(t, "empty_flag", mockConfigStore.configs["empty_flag"].Key)
mockHttpClient.AssertNumberOfCalls(t, "get", 2)
mockCall.Unset()

// change the remote config
mockResponse2 := `
{
"createdAt": "2024-04-17T19:40:53.716Z",
"environment": {
"name": "Test"
},
"flags": {
"empty_flag_2": {
"key": "empty_flag_2",
"enabled": true,
"variationType": "STRING",
"variations": {},
"allocations": [],
"totalShards": 10000
}
}
}
`
mockCall = mockHttpClient.On("get", UFC_ENDPOINT).Return(mockResponse2, nil)

// fetch and store again
configRequestor.FetchAndStoreConfigurations()

// assert that the new config is stored
assert.Equal(t, 1, len(mockConfigStore.configs))
assert.Equal(t, "empty_flag_2", mockConfigStore.configs["empty_flag_2"].Key)
mockHttpClient.AssertNumberOfCalls(t, "get", 3)
mockCall.Unset()

// change remote config back to original
mockCall = mockHttpClient.On("get", UFC_ENDPOINT).Return(mockResponse1, nil)

// fetch and store again
configRequestor.FetchAndStoreConfigurations()

assert.Equal(t, 1, len(mockConfigStore.configs))
assert.Equal(t, "empty_flag", mockConfigStore.configs["empty_flag"].Key)
mockHttpClient.AssertNumberOfCalls(t, "get", 4)
mockCall.Unset()
}
21 changes: 11 additions & 10 deletions eppoclient/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (

const REQUEST_TIMEOUT_SECONDS = time.Duration(10 * time.Second)

type HttpClientInterface interface {
get(resource string) (string, error)
}

type httpClient struct {
baseUrl string
sdkParams SDKParams
isUnauthorized bool
client *http.Client
baseUrl string
sdkParams SDKParams
client *http.Client
}

type SDKParams struct {
Expand All @@ -23,12 +26,11 @@ type SDKParams struct {
sdkVersion string
}

func newHttpClient(baseUrl string, client *http.Client, sdkParams SDKParams) *httpClient {
func newHttpClient(baseUrl string, client *http.Client, sdkParams SDKParams) HttpClientInterface {
var hc = &httpClient{
baseUrl: baseUrl,
sdkParams: sdkParams,
isUnauthorized: false,
client: client,
baseUrl: baseUrl,
sdkParams: sdkParams,
client: client,
}
return hc
}
Expand Down Expand Up @@ -64,7 +66,6 @@ func (hc *httpClient) get(resource string) (string, error) {
defer resp.Body.Close() // Ensure the response body is closed

if resp.StatusCode == 401 {
hc.isUnauthorized = true
return "", fmt.Errorf("unauthorized access") // Return an error indicating unauthorized access
}

Expand Down
4 changes: 2 additions & 2 deletions eppoclient/initclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package eppoclient

import "net/http"

var __version__ = "4.0.1"
var __version__ = "4.0.2"

// InitClient is required to start polling of experiments configurations and create
// an instance of EppoClient, which could be used to get assignments information.
Expand All @@ -14,7 +14,7 @@ func InitClient(config Config) *EppoClient {

httpClient := newHttpClient(config.BaseUrl, &http.Client{Timeout: REQUEST_TIMEOUT_SECONDS}, sdkParams)
configStore := newConfigurationStore()
requestor := newConfigurationRequestor(*httpClient, configStore)
requestor := newConfigurationRequestor(httpClient, configStore)
assignmentLogger := config.AssignmentLogger

poller := newPoller(config.PollerInterval, requestor.FetchAndStoreConfigurations)
Expand Down