1 package growthbook
2
3 import (
4 "reflect"
5 "testing"
6 )
7
8 func TestContextMalformedJSON(t *testing.T) {
9 SetLogger(&testLog)
10
11 contextJSON := []string{
12 `{"enabled": 1}`,
13 `{"attributes": 1}`,
14 `{"url": 1}`,
15 `{"features": 1}`,
16 `{"forcedVariations": 1}`,
17 `{"forcedVariations": {"abc": 1, "def": "bad"}}`,
18 `{"qaMode": 1}`,
19 `{"devMode": 1}`,
20 `{"userAttributes": 1}`,
21 `{"groups": 1}`,
22 `{"groups": {"abc": true, "def": "bad"}}`,
23 `{"apiHost": 1}`,
24 `{"clientKey": 1}`,
25 `{"decryptionKey": 1}`,
26 `{"overrides": 1}`,
27 `{"unknownKey": "some data"}`,
28 }
29
30 for _, json := range contextJSON {
31 testLog.reset()
32 ParseContext([]byte(json))
33 if len(testLog.warnings) != 1 {
34 t.Errorf("expected warning from Context JSON parser for: %s", json)
35 }
36 }
37 }
38
39 func TestFeaturesCanSetFeatures(t *testing.T) {
40 context := NewContext().
41 WithAttributes(Attributes{"id": "123"})
42 gb := New(context).
43 WithFeatures(FeatureMap{"feature": &Feature{DefaultValue: 0}})
44
45 result := gb.Feature("feature")
46 expected := FeatureResult{
47 Value: 0,
48 On: false,
49 Off: true,
50 Source: DefaultValueResultSource,
51 }
52
53 if result == nil || !reflect.DeepEqual(*result, expected) {
54 t.Errorf("unexpected result: %v", result)
55 }
56 }
57
58 func TestFeaturesCanSetEncryptedFeatures(t *testing.T) {
59 gb := New(nil)
60
61 keyString := "Ns04T5n9+59rl2x3SlNHtQ=="
62 encrypedFeatures :=
63 "vMSg2Bj/IurObDsWVmvkUg==.L6qtQkIzKDoE2Dix6IAKDcVel8PHUnzJ7JjmLjFZFQDqidRIoCxKmvxvUj2kTuHFTQ3/NJ3D6XhxhXXv2+dsXpw5woQf0eAgqrcxHrbtFORs18tRXRZza7zqgzwvcznx"
64
65 _, err := gb.WithEncryptedFeatures(encrypedFeatures, keyString)
66 if err != nil {
67 t.Error("unexpected error: ", err)
68 }
69
70 expectedJson := `{
71 "testfeature1": {
72 "defaultValue": true,
73 "rules": [{"condition": { "id": "1234" }, "force": false}]
74 }
75 }`
76 expected := ParseFeatureMap([]byte(expectedJson))
77 actual := gb.Features()
78
79 if !reflect.DeepEqual(actual, expected) {
80 t.Error("unexpected features value: ", actual)
81 }
82 }
83
84 func TestFeaturesDecryptFeaturesWithInvalidKey(t *testing.T) {
85 gb := New(nil)
86
87 keyString := "fakeT5n9+59rl2x3SlNHtQ=="
88 encrypedFeatures :=
89 "vMSg2Bj/IurObDsWVmvkUg==.L6qtQkIzKDoE2Dix6IAKDcVel8PHUnzJ7JjmLjFZFQDqidRIoCxKmvxvUj2kTuHFTQ3/NJ3D6XhxhXXv2+dsXpw5woQf0eAgqrcxHrbtFORs18tRXRZza7zqgzwvcznx"
90
91 _, err := gb.WithEncryptedFeatures(encrypedFeatures, keyString)
92 if err == nil {
93 t.Error("unexpected lack of error")
94 }
95 }
96
97 func TestFeaturesDecryptFeaturesWithInvalidCiphertext(t *testing.T) {
98 gb := New(nil)
99
100 keyString := "Ns04T5n9+59rl2x3SlNHtQ=="
101 encrypedFeatures :=
102 "FAKE2Bj/IurObDsWVmvkUg==.L6qtQkIzKDoE2Dix6IAKDcVel8PHUnzJ7JjmLjFZFQDqidRIoCxKmvxvUj2kTuHFTQ3/NJ3D6XhxhXXv2+dsXpw5woQf0eAgqrcxHrbtFORs18tRXRZza7zqgzwvcznx"
103
104 _, err := gb.WithEncryptedFeatures(encrypedFeatures, keyString)
105 if err == nil {
106 t.Error("unexpected lack of error")
107 }
108 }
109
110 func TestFeaturesReturnsRuleID(t *testing.T) {
111 featuresJson := `{
112 "feature": {"defaultValue": 0, "rules": [{"force": 1, "id": "foo"}]}
113 }`
114 gb := New(nil).
115 WithFeatures(ParseFeatureMap([]byte(featuresJson)))
116 result := gb.EvalFeature("feature")
117 if result.RuleID != "foo" {
118 t.Errorf("expected rule ID to be foo, got: %v", result.RuleID)
119 }
120 }
121
122 func TestFeaturesUpdatesAttributes(t *testing.T) {
123 context := NewContext().
124 WithAttributes(Attributes{"foo": 1, "bar": 2})
125 gb := New(context).
126 WithAttributes(Attributes{"foo": 2, "baz": 3})
127
128 result := context.Attributes
129 expected := Attributes{"foo": 2, "baz": 3}
130
131 if !reflect.DeepEqual(result, expected) {
132 t.Errorf("unexpected result: %v", result)
133 }
134 if !reflect.DeepEqual(gb.Attributes(), expected) {
135 t.Errorf("unexpected result: %v", result)
136 }
137 }
138
139 func TestFeaturesUsesAttributeOverrides(t *testing.T) {
140 context := NewContext().
141 WithAttributes(Attributes{"id": "123", "foo": "bar"})
142 gb := New(context).
143 WithAttributeOverrides(Attributes{"foo": "baz"})
144
145 if !reflect.DeepEqual(gb.Attributes(),
146 Attributes{"id": "123", "foo": "baz"}) {
147 t.Errorf("unexpected value for gb.Attributes(): %v\n",
148 gb.Attributes())
149 }
150
151 exp1 := NewExperiment("my-test").WithVariations(0, 1).WithHashAttribute("foo")
152 result := gb.Run(exp1)
153 if result.HashValue != "baz" {
154 t.Errorf("unexpected experiment result: %v\n", result.HashValue)
155 }
156
157 gb = gb.WithAttributeOverrides(nil)
158
159 if !reflect.DeepEqual(gb.Attributes(),
160 Attributes{"id": "123", "foo": "bar"}) {
161 t.Errorf("unexpected value for gb.Attributes(): %v\n",
162 gb.Attributes())
163 }
164
165 result = gb.Run(exp1)
166 if result.HashValue != "bar" {
167 t.Errorf("unexpected experiment result: %v\n", result.HashValue)
168 }
169 }
170
171 func TestFeaturesUsesForcedFeatureValues(t *testing.T) {
172 featuresJson := `{
173 "feature1": {"defaultValue": 0},
174 "feature2": {"defaultValue": 0}
175 }`
176 gb := New(nil).
177 WithFeatures(ParseFeatureMap([]byte(featuresJson))).
178 WithForcedFeatures(map[string]interface{}{
179 "feature2": 1.0,
180 "feature3": 1.0,
181 })
182
183 check := func(icase int, feature string, value interface{}) {
184 result := gb.EvalFeature(feature)
185 if !reflect.DeepEqual(result.Value, value) {
186 t.Errorf("%d: result from EvalFeature: expected %v, got %v",
187 icase, value, result.Value)
188 }
189 }
190
191 check(1, "feature1", 0.0)
192 check(2, "feature2", 1.0)
193 check(3, "feature3", 1.0)
194
195 gb = gb.WithForcedFeatures(nil)
196
197 check(4, "feature1", 0.0)
198 check(5, "feature2", 0.0)
199 check(6, "feature3", nil)
200 }
201
202 func TestFeaturesGetsFeatures(t *testing.T) {
203 featuresJson := `{ "feature1": { "defaultValue": 0 } }`
204 features := ParseFeatureMap([]byte(featuresJson))
205 gb := New(nil).WithFeatures(features)
206
207 if !reflect.DeepEqual(gb.Features(), features) {
208 t.Error("expected features to match")
209 }
210 }
211
212 func TestFeaturesFeatureUsageWhenAssignedValueChanges(t *testing.T) {
213 featuresJson := `{
214 "feature": {
215 "defaultValue": 0,
216 "rules": [{"condition": {"color": "blue"}, "force": 1}]
217 }
218 }`
219 context := NewContext().
220 WithAttributes(Attributes{"color": "green"}).
221 WithFeatures(ParseFeatureMap([]byte(featuresJson)))
222
223 type featureCall struct {
224 key string
225 result *FeatureResult
226 }
227 calls := []featureCall{}
228 callback := func(key string, result *FeatureResult) {
229 calls = append(calls, featureCall{key, result})
230 }
231 gb := New(context).WithFeatureUsageCallback(callback)
232
233
234 res1 := gb.EvalFeature("feature")
235 if res1.Value != 0.0 {
236 t.Errorf("expected value 0, got %#v", res1.Value)
237 }
238
239
240 gb = gb.WithAttributes(Attributes{"color": "blue"})
241 res2 := gb.EvalFeature("feature")
242 if res2.Value != 1.0 {
243 t.Errorf("expected value 1, got %#v", res2.Value)
244 }
245
246 if len(calls) != 2 {
247 t.Errorf("expected 2 calls to feature usage callback, got %d", len(calls))
248 } else {
249 if !reflect.DeepEqual(calls[0], featureCall{"feature", res1}) {
250 t.Errorf("unexpected callback result")
251 }
252 if !reflect.DeepEqual(calls[1], featureCall{"feature", res2}) {
253 t.Errorf("unexpected callback result")
254 }
255 }
256 }
257
258 func TestFeaturesUsesFallbacksForGetFeatureValue(t *testing.T) {
259 gb := New(nil).WithFeatures(ParseFeatureMap(
260 []byte(`{"feature": {"defaultValue": "blue"}}`)))
261
262 res := gb.GetFeatureValue("feature", "green")
263 if res != "blue" {
264 t.Error("1: unexpected return from GetFeatureValue: ", res)
265 }
266 res = gb.GetFeatureValue("unknown", "green")
267 if res != "green" {
268 t.Error("2: unexpected return from GetFeatureValue: ", res)
269 }
270 res = gb.GetFeatureValue("testing", nil)
271 if res != nil {
272 t.Error("3: unexpected return from GetFeatureValue: ", res)
273 }
274 }
275
276 func TestFeaturesUsageTracking(t *testing.T) {
277 called := false
278 cb := func(key string, result *FeatureResult) {
279 called = true
280 }
281
282 context := NewContext().
283 WithAttributes(Attributes{"id": "123"}).
284 WithFeatureUsageCallback(cb)
285 gb := New(context).
286 WithFeatures(FeatureMap{"feature": &Feature{DefaultValue: 0}})
287
288 result := gb.Feature("feature")
289 expected := FeatureResult{
290 Value: 0,
291 On: false,
292 Off: true,
293 Source: DefaultValueResultSource,
294 }
295
296 if result == nil || !reflect.DeepEqual(*result, expected) {
297 t.Errorf("unexpected result: %v", result)
298 }
299 if !called {
300 t.Errorf("expected feature tracking callback to be called")
301 }
302 }
303
View as plain text