Skip to content

Commit 9339567

Browse files
YuanCMichael Ng
authored andcommitted
feat(getenabledfeatures): Add GetEnabledFeatures API (#63)
1 parent da8ec79 commit 9339567

File tree

4 files changed

+172
-0
lines changed

4 files changed

+172
-0
lines changed

optimizely/client/client.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,41 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit
9393
return result, nil
9494
}
9595

96+
// GetEnabledFeatures returns an array containing the keys of all features in the project that are enabled for the given user.
97+
func (o *OptimizelyClient) GetEnabledFeatures(userContext entities.UserContext) (enabledFeatures []string, err error) {
98+
if !o.isValid {
99+
errorMessage := "Optimizely instance is not valid. Failing GetEnabledFeatures."
100+
err := errors.New(errorMessage)
101+
logger.Error(errorMessage, nil)
102+
return enabledFeatures, err
103+
}
104+
105+
defer func() {
106+
if r := recover(); r != nil {
107+
errorMessage := fmt.Sprintf(`Optimizely SDK is panicking with the error "%s"`, string(debug.Stack()))
108+
err = errors.New(errorMessage)
109+
logger.Error(errorMessage, err)
110+
}
111+
}()
112+
113+
projectConfig := o.configManager.GetConfig()
114+
115+
if reflect.ValueOf(projectConfig).IsNil() {
116+
return enabledFeatures, fmt.Errorf("project config is null")
117+
}
118+
119+
featureList := projectConfig.GetFeatureList()
120+
for _, feature := range featureList {
121+
isEnabled, _ := o.IsFeatureEnabled(feature.Key, userContext)
122+
123+
if isEnabled {
124+
enabledFeatures = append(enabledFeatures, feature.Key)
125+
}
126+
}
127+
128+
return enabledFeatures, nil
129+
}
130+
96131
// Close closes the Optimizely instance and stops any ongoing tasks from its children components
97132
func (o *OptimizelyClient) Close() {
98133
o.cancelFunc()

optimizely/client/client_test.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ func (c *MockProjectConfig) GetFeatureByKey(featureKey string) (entities.Feature
3838
return args.Get(0).(entities.Feature), args.Error(1)
3939
}
4040

41+
func (c *MockProjectConfig) GetFeatureList() []entities.Feature {
42+
args := c.Called()
43+
return args.Get(0).([]entities.Feature)
44+
}
45+
4146
type MockProjectConfigManager struct {
4247
mock.Mock
4348
}
@@ -169,3 +174,126 @@ func TestIsFeatureEnabledPanic(t *testing.T) {
169174
assert.False(t, result)
170175
assert.True(t, assert.Error(t, err))
171176
}
177+
178+
func TestGetEnabledFeatures(t *testing.T) {
179+
testUserContext := entities.UserContext{ID: "test_user_1"}
180+
testVariationEnabled := entities.Variation{
181+
ID: "22222",
182+
Key: "22222",
183+
FeatureEnabled: true,
184+
}
185+
testVariationDisabled := entities.Variation{
186+
ID: "22222",
187+
Key: "22222",
188+
FeatureEnabled: false,
189+
}
190+
testExperimentEnabled := entities.Experiment{
191+
ID: "111111",
192+
Variations: map[string]entities.Variation{"22222": testVariationEnabled},
193+
}
194+
testExperimentDisabled := entities.Experiment{
195+
ID: "111111",
196+
Variations: map[string]entities.Variation{"22222": testVariationDisabled},
197+
}
198+
testFeatureEnabledKey := "test_feature_enabled_key"
199+
testFeatureEnabled := entities.Feature{
200+
ID: "22222",
201+
Key: testFeatureEnabledKey,
202+
FeatureExperiments: []entities.Experiment{testExperimentEnabled},
203+
}
204+
testFeatureDisabledKey := "test_feature_disabled_key"
205+
testFeatureDisabled := entities.Feature{
206+
ID: "22222",
207+
Key: testFeatureDisabledKey,
208+
FeatureExperiments: []entities.Experiment{testExperimentDisabled},
209+
}
210+
featureList := []entities.Feature{testFeatureEnabled, testFeatureDisabled}
211+
// Test happy path
212+
mockConfig := new(MockProjectConfig)
213+
mockConfig.On("GetFeatureByKey", testFeatureEnabledKey).Return(testFeatureEnabled, nil)
214+
mockConfig.On("GetFeatureByKey", testFeatureDisabledKey).Return(testFeatureDisabled, nil)
215+
mockConfig.On("GetFeatureList").Return(featureList)
216+
mockConfigManager := new(MockProjectConfigManager)
217+
mockConfigManager.On("GetConfig").Return(mockConfig)
218+
// Set up the mock decision service and its return value
219+
testDecisionContextEnabled := decision.FeatureDecisionContext{
220+
Feature: &testFeatureEnabled,
221+
ProjectConfig: mockConfig,
222+
}
223+
testDecisionContextDisabled := decision.FeatureDecisionContext{
224+
Feature: &testFeatureDisabled,
225+
ProjectConfig: mockConfig,
226+
}
227+
228+
expectedFeatureDecisionEnabled := decision.FeatureDecision{
229+
Experiment: testExperimentEnabled,
230+
Variation: testVariationEnabled,
231+
Decision: decision.Decision{
232+
DecisionMade: true,
233+
},
234+
}
235+
expectedFeatureDecisionDisabled := decision.FeatureDecision{
236+
Experiment: testExperimentDisabled,
237+
Variation: testVariationDisabled,
238+
Decision: decision.Decision{
239+
DecisionMade: true,
240+
},
241+
}
242+
243+
mockDecisionService := new(MockDecisionService)
244+
mockDecisionService.On("GetFeatureDecision", testDecisionContextEnabled, testUserContext).Return(expectedFeatureDecisionEnabled, nil)
245+
mockDecisionService.On("GetFeatureDecision", testDecisionContextDisabled, testUserContext).Return(expectedFeatureDecisionDisabled, nil)
246+
247+
client := OptimizelyClient{
248+
configManager: mockConfigManager,
249+
decisionService: mockDecisionService,
250+
isValid: true,
251+
}
252+
result, err := client.GetEnabledFeatures(testUserContext)
253+
assert.NoError(t, err)
254+
assert.ElementsMatch(t, result, []string{testFeatureEnabledKey})
255+
mockConfig.AssertExpectations(t)
256+
mockConfigManager.AssertExpectations(t)
257+
mockDecisionService.AssertExpectations(t)
258+
}
259+
260+
func TestGetEnabledFeaturesErrorCases(t *testing.T) {
261+
testUserContext := entities.UserContext{ID: "test_user_1"}
262+
263+
// Test instance invalid
264+
mockConfigManager := new(MockProjectConfigManager)
265+
mockDecisionService := new(MockDecisionService)
266+
267+
client := OptimizelyClient{
268+
configManager: mockConfigManager,
269+
decisionService: mockDecisionService,
270+
isValid: false,
271+
}
272+
result, err := client.GetEnabledFeatures(testUserContext)
273+
assert.Error(t, err)
274+
assert.Empty(t, result)
275+
mockConfigManager.AssertNotCalled(t, "GetFeatureByKey")
276+
mockDecisionService.AssertNotCalled(t, "GetFeatureDecision")
277+
}
278+
279+
func TestGetEnabledFeaturesPanic(t *testing.T) {
280+
testUserContext := entities.UserContext{ID: "test_user_1"}
281+
testFeatureKey := "test_feature_key"
282+
283+
mockConfigManager := new(MockProjectConfigManager)
284+
mockDecisionService := new(MockDecisionService)
285+
286+
client := OptimizelyClient{
287+
configManager: mockConfigManager,
288+
decisionService: mockDecisionService,
289+
isValid: true,
290+
}
291+
292+
// returning an error object will cause the Client to panic
293+
mockConfigManager.On("GetFeatureByKey", testFeatureKey, testUserContext).Return(errors.New("failure"))
294+
295+
// ensure that the client calms back down and recovers
296+
result, err := client.GetEnabledFeatures(testUserContext)
297+
assert.Empty(t, result)
298+
assert.True(t, assert.Error(t, err))
299+
}

optimizely/config/datafileprojectconfig/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ func (c DatafileProjectConfig) GetFeatureByKey(featureKey string) (entities.Feat
117117
return entities.Feature{}, errors.New(errMessage)
118118
}
119119

120+
// GetFeatureList returns an array of all the features
121+
func (c DatafileProjectConfig) GetFeatureList() (featureList []entities.Feature) {
122+
for _, feature := range c.featureMap {
123+
featureList = append(featureList, feature)
124+
}
125+
return featureList
126+
}
127+
120128
// GetAudienceByID returns the audience with the given ID
121129
func (c DatafileProjectConfig) GetAudienceByID(audienceID string) (entities.Audience, error) {
122130
if audience, ok := c.audienceMap[audienceID]; ok {

optimizely/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type ProjectConfig interface {
3131
GetEventByKey(string) (entities.Event, error)
3232
GetExperimentByKey(string) (entities.Experiment, error)
3333
GetFeatureByKey(string) (entities.Feature, error)
34+
GetFeatureList() []entities.Feature
3435
GetGroupByID(string) (entities.Group, error)
3536
GetProjectID() string
3637
GetRevision() string

0 commit comments

Comments
 (0)