Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit aecf848

Browse files
authoredMar 4, 2024
Merge branch 'main' into feature/statsig
2 parents e872d17 + a8ee306 commit aecf848

File tree

8 files changed

+529
-166
lines changed

8 files changed

+529
-166
lines changed
 

‎.github/workflows/ci.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
- 8013:8013
5252
# sync-testbed for flagd-provider e2e tests
5353
sync:
54-
image: ghcr.io/open-feature/sync-testbed:v0.5.1
54+
image: ghcr.io/open-feature/sync-testbed:v0.5.2
5555
ports:
5656
- 9090:9090
5757
steps:

‎providers/unleash/go.mod

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ go 1.21
44

55
require (
66
github.com/Unleash/unleash-client-go/v3 v3.9.2
7-
github.com/open-feature/go-sdk v1.9.0
7+
github.com/open-feature/go-sdk v1.10.0
88
github.com/stretchr/testify v1.8.4
99
)
1010

1111
require (
12-
github.com/Masterminds/semver/v3 v3.1.1 // indirect
12+
github.com/Masterminds/semver/v3 v3.2.1 // indirect
1313
github.com/davecgh/go-spew v1.1.1 // indirect
14-
github.com/go-logr/logr v1.3.0 // indirect
14+
github.com/go-logr/logr v1.4.1 // indirect
1515
github.com/kr/pretty v0.3.1 // indirect
1616
github.com/pmezard/go-difflib v1.0.0 // indirect
17-
github.com/stretchr/objx v0.5.0 // indirect
18-
github.com/twmb/murmur3 v1.1.5 // indirect
19-
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect
17+
github.com/stretchr/objx v0.5.1 // indirect
18+
github.com/twmb/murmur3 v1.1.8 // indirect
19+
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
2020
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2121
gopkg.in/yaml.v3 v3.0.1 // indirect
2222
)

‎providers/unleash/go.sum

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
21
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
2+
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
3+
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
34
github.com/Unleash/unleash-client-go/v3 v3.9.2 h1:/Jl61G/kOx+1+MqPuMnC/JvJxdsf52ZDdJvCmXoA2ck=
45
github.com/Unleash/unleash-client-go/v3 v3.9.2/go.mod h1:jAf7F2WWpfJbfn1n8bZ74p7hkAhijrqH4TpWoT7kWLc=
56
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
67
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
78
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
89
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9-
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
10-
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
10+
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
11+
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
1112
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
1213
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
1314
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@@ -18,8 +19,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
1819
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1920
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
2021
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
21-
github.com/open-feature/go-sdk v1.9.0 h1:1Nyj+XNHfL0rRGZgGCbZ29CHDD57PQJL7Q/2ZbW/E8c=
22-
github.com/open-feature/go-sdk v1.9.0/go.mod h1:n5BM4DfvIiKaWWquZnL/yVihcGM5aLsz7rNYE3BkXAM=
22+
github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM=
23+
github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
2324
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
2425
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2526
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -28,17 +29,20 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
2829
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
2930
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
3031
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
31-
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
3232
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
33+
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
34+
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
3335
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
3436
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
3537
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
38+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
3639
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
3740
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
38-
github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk=
3941
github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
40-
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
41-
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
42+
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
43+
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
44+
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
45+
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
4246
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
4347
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
4448
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+238-105
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,243 @@
11
{
22
"version": 1,
33
"features": [
4-
{
5-
"name": "variant-flag",
6-
"type": "experiment",
7-
"enabled": true,
8-
"stale": false,
9-
"strategies": [
10-
{ "name": "default", "parameters": {}, "constraints": [] }
11-
],
12-
"variants": [
13-
{
14-
"name": "v1",
15-
"weight": 1000,
16-
"weightType": "fix",
17-
"payload": { "type": "string", "value": "v1" },
18-
"overrides": [],
19-
"stickiness": "default"
20-
},
21-
{
22-
"name": "v2",
23-
"weight": 0,
24-
"weightType": "variable",
25-
"payload": { "type": "string", "value": "v2" },
26-
"overrides": [{ "contextName": "userId", "values": ["me"] }],
27-
"stickiness": "default"
28-
}
29-
]
30-
},
31-
{
32-
"name": "users-flag",
33-
"type": "release",
34-
"enabled": true,
35-
"stale": false,
36-
"strategies": [
37-
{ "name": "userWithId", "parameters": { "userIds": "111,234" } }
38-
],
39-
"variants": []
40-
},
41-
{
42-
"name": "json-flag",
43-
"type": "experiment",
44-
"enabled": true,
45-
"stale": false,
46-
"strategies": [{ "name": "default", "parameters": {} }],
47-
"variants": [
48-
{
49-
"name": "aaaa",
50-
"weight": 1000,
51-
"payload": { "type": "json", "value": "{ a: 1 }" },
52-
"overrides": [],
53-
"weightType": "variable",
54-
"stickiness": "default"
55-
}
56-
]
57-
},
58-
{
59-
"name": "csv-flag",
60-
"type": "experiment",
61-
"enabled": true,
62-
"stale": false,
63-
"strategies": [{ "name": "default", "parameters": {} }],
64-
"variants": [
65-
{
66-
"name": "aaaa",
67-
"weight": 1000,
68-
"payload": { "type": "csv", "value": "a,b,c" },
69-
"overrides": [],
70-
"weightType": "variable",
71-
"stickiness": "default"
72-
}
73-
]
74-
},
75-
{
76-
"name": "int-flag",
77-
"type": "experiment",
78-
"enabled": true,
79-
"stale": false,
80-
"strategies": [{ "name": "default", "parameters": {} }],
81-
"variants": [
82-
{
83-
"name": "aaaa",
84-
"weight": 1000,
85-
"payload": { "type": "number", "value": "123" },
86-
"overrides": [],
87-
"weightType": "variable",
88-
"stickiness": "default"
89-
}
90-
]
91-
},
92-
{
93-
"name": "double-flag",
94-
"type": "experiment",
95-
"enabled": true,
96-
"stale": false,
97-
"strategies": [{ "name": "default", "parameters": {} }],
98-
"variants": [
99-
{
100-
"name": "aaaa",
101-
"weight": 1000,
102-
"payload": { "type": "number", "value": "1.23" },
103-
"overrides": [],
104-
"weightType": "variable",
105-
"stickiness": "default"
106-
}
107-
]
4+
{
5+
"name": "variant-flag",
6+
"type": "experiment",
7+
"enabled": true,
8+
"stale": false,
9+
"strategies": [
10+
{
11+
"name": "default",
12+
"parameters": {},
13+
"constraints": []
14+
}
15+
],
16+
"variants": [
17+
{
18+
"name": "v1",
19+
"weight": 1000,
20+
"weightType": "fix",
21+
"payload": {
22+
"type": "string",
23+
"value": "v1"
24+
},
25+
"overrides": [],
26+
"stickiness": "default"
27+
},
28+
{
29+
"name": "v2",
30+
"weight": 0,
31+
"weightType": "variable",
32+
"payload": {
33+
"type": "string",
34+
"value": "v2"
35+
},
36+
"overrides": [
37+
{
38+
"contextName": "userId",
39+
"values": [
40+
"me"
41+
]
42+
}
43+
],
44+
"stickiness": "default"
45+
}
46+
]
47+
},
48+
{
49+
"name": "users-flag",
50+
"type": "release",
51+
"enabled": true,
52+
"stale": false,
53+
"strategies": [
54+
{
55+
"name": "userWithId",
56+
"parameters": {
57+
"userIds": "111,234"
58+
}
59+
}
60+
],
61+
"variants": []
62+
},
63+
{
64+
"name": "json-flag",
65+
"type": "experiment",
66+
"enabled": true,
67+
"stale": false,
68+
"strategies": [
69+
{
70+
"name": "default",
71+
"parameters": {}
72+
}
73+
],
74+
"variants": [
75+
{
76+
"name": "aaaa",
77+
"weight": 1000,
78+
"payload": {
79+
"type": "json",
80+
"value": "{\n \"k1\": \"v1\"\n}"
81+
},
82+
"overrides": [],
83+
"weightType": "variable",
84+
"stickiness": "default"
85+
}
86+
]
87+
},
88+
{
89+
"name": "csv-flag",
90+
"type": "experiment",
91+
"enabled": true,
92+
"stale": false,
93+
"strategies": [
94+
{
95+
"name": "default",
96+
"parameters": {}
97+
}
98+
],
99+
"variants": [
100+
{
101+
"name": "aaaa",
102+
"weight": 1000,
103+
"payload": {
104+
"type": "csv",
105+
"value": "a,b,c"
106+
},
107+
"overrides": [],
108+
"weightType": "variable",
109+
"stickiness": "default"
110+
}
111+
]
112+
},
113+
{
114+
"name": "int-flag",
115+
"type": "experiment",
116+
"enabled": true,
117+
"stale": false,
118+
"strategies": [
119+
{
120+
"name": "default",
121+
"parameters": {}
122+
}
123+
],
124+
"variants": [
125+
{
126+
"name": "aaaa",
127+
"weight": 1000,
128+
"payload": {
129+
"type": "number",
130+
"value": "123"
131+
},
132+
"overrides": [],
133+
"weightType": "variable",
134+
"stickiness": "default"
135+
}
136+
]
137+
},
138+
{
139+
"name": "double-flag",
140+
"type": "experiment",
141+
"enabled": true,
142+
"stale": false,
143+
"strategies": [
144+
{
145+
"name": "default",
146+
"parameters": {}
147+
}
148+
],
149+
"variants": [
150+
{
151+
"name": "aaaa",
152+
"weight": 1000,
153+
"payload": {
154+
"type": "number",
155+
"value": "1.23"
156+
},
157+
"overrides": [],
158+
"weightType": "variable",
159+
"stickiness": "default"
160+
}
161+
]
162+
},
163+
{
164+
"name": "DateExample",
165+
"type": "release",
166+
"enabled": true,
167+
"stale": false,
168+
"strategies": [{ "name": "default", "parameters": {} }],
169+
"variants": []
170+
},
171+
{
172+
"name": "variant-flag-by-date",
173+
"type": "release",
174+
"enabled": true,
175+
"stale": false,
176+
"strategies": [
177+
{
178+
"name": "default",
179+
"constraints": [
180+
{
181+
"contextName": "currentTime",
182+
"operator": "DATE_AFTER",
183+
"values": [],
184+
"caseInsensitive": false,
185+
"inverted": false,
186+
"value": "2024-02-28T07:49:06.825Z"
187+
}
188+
],
189+
"parameters": {
190+
"rollout": "100",
191+
"stickiness": "default",
192+
"groupId": "number1"
193+
},
194+
"variants": [
195+
{
196+
"stickiness": "default",
197+
"name": "var1",
198+
"weight": 1000,
199+
"payload": {
200+
"type": "string",
201+
"value": "v1"
202+
},
203+
"weightType": "variable"
204+
}
205+
],
206+
"segments": [],
207+
"disabled": false
208+
}
209+
],
210+
"variants": [
211+
{
212+
"name": "fallback",
213+
"weight": 1000,
214+
"weightType": "fix",
215+
"payload": {
216+
"type": "string",
217+
"value": "fallback-value"
218+
},
219+
"overrides": [],
220+
"stickiness": "default"
221+
},
222+
{
223+
"name": "v2",
224+
"weight": 0,
225+
"weightType": "variable",
226+
"payload": {
227+
"type": "string",
228+
"value": "v2"
229+
},
230+
"overrides": [
231+
{
232+
"contextName": "userId",
233+
"values": [
234+
"me"
235+
]
236+
}
237+
],
238+
"stickiness": "default"
239+
}
240+
]
108241
}
109242
]
110-
}
243+
}

‎providers/unleash/pkg/provider.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package unleash
33
import (
44
"context"
55
"fmt"
6+
"strconv"
67

78
"github.com/Unleash/unleash-client-go/v3"
89
"github.com/Unleash/unleash-client-go/v3/api"
@@ -106,9 +107,21 @@ func (p *Provider) BooleanEvaluation(ctx context.Context, flag string, defaultVa
106107

107108
func (p *Provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail {
108109
res := p.ObjectEvaluation(ctx, flag, defaultValue, evalCtx)
110+
if strValue, ok := res.Value.(string); ok {
111+
value, err := strconv.ParseFloat(strValue, 64)
112+
if err == nil {
113+
return of.FloatResolutionDetail{
114+
Value: value,
115+
ProviderResolutionDetail: res.ProviderResolutionDetail,
116+
}
117+
}
118+
}
109119
return of.FloatResolutionDetail{
110-
Value: res.Value.(float64),
111-
ProviderResolutionDetail: res.ProviderResolutionDetail,
120+
Value: defaultValue,
121+
ProviderResolutionDetail: of.ProviderResolutionDetail{
122+
Reason: of.ErrorReason,
123+
ResolutionError: of.NewFlagNotFoundResolutionError(fmt.Sprintf("FloatEvaluation type error for %s", flag)),
124+
},
112125
}
113126
}
114127

‎providers/unleash/pkg/provider_test.go

+133-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"reflect"
78
"testing"
89
"time"
910

@@ -14,21 +15,48 @@ import (
1415
)
1516

1617
var provider *unleashProvider.Provider
18+
var ofClient *of.Client
1719

1820
func TestBooleanEvaluation(t *testing.T) {
1921
resolution := provider.BooleanEvaluation(context.Background(), "variant-flag", false, nil)
2022
enabled, _ := resolution.ProviderResolutionDetail.FlagMetadata.GetBool("enabled")
21-
if enabled == false {
23+
if !enabled {
24+
t.Fatalf("Expected feature to be enabled")
25+
}
26+
if !resolution.Value {
27+
t.Fatalf("Expected one of the variant payloads")
28+
}
29+
30+
t.Run("evalCtx empty", func(t *testing.T) {
31+
resolution := provider.BooleanEvaluation(context.Background(), "non-existing-flag", false, nil)
32+
require.Equal(t, false, resolution.Value)
33+
})
34+
35+
t.Run("evalCtx empty fallback to default", func(t *testing.T) {
36+
resolution := provider.BooleanEvaluation(context.Background(), "non-existing-flag", true, nil)
37+
require.Equal(t, true, resolution.Value)
38+
})
39+
}
40+
41+
func TestIntEvaluation(t *testing.T) {
42+
resolution := provider.BooleanEvaluation(context.Background(), "int-flag", false, nil)
43+
enabled, _ := resolution.ProviderResolutionDetail.FlagMetadata.GetBool("enabled")
44+
if !enabled {
2245
t.Fatalf("Expected feature to be enabled")
2346
}
24-
if resolution.Value != true {
47+
if !resolution.Value {
2548
t.Fatalf("Expected one of the variant payloads")
2649
}
2750

2851
t.Run("evalCtx empty", func(t *testing.T) {
2952
resolution := provider.BooleanEvaluation(context.Background(), "non-existing-flag", false, nil)
3053
require.Equal(t, false, resolution.Value)
3154
})
55+
56+
t.Run("evalCtx empty fallback to default", func(t *testing.T) {
57+
resolution := provider.BooleanEvaluation(context.Background(), "non-existing-flag", true, nil)
58+
require.Equal(t, true, resolution.Value)
59+
})
3260
}
3361

3462
func TestStringEvaluation(t *testing.T) {
@@ -44,9 +72,6 @@ func TestStringEvaluation(t *testing.T) {
4472
t.Fatalf("Expected one of the variant payloads")
4573
}
4674

47-
of.SetProvider(provider)
48-
ofClient := of.NewClient("my-app")
49-
5075
evalCtx := of.NewEvaluationContext(
5176
"",
5277
map[string]interface{}{},
@@ -74,13 +99,15 @@ func TestBooleanEvaluationByUser(t *testing.T) {
7499
t.Fatalf("Expected feature to be disabled")
75100
}
76101

77-
of.SetProvider(provider)
78-
ofClient := of.NewClient("my-app")
79-
80102
evalCtx := of.NewEvaluationContext(
81103
"",
82104
map[string]interface{}{
83-
"UserId": "111",
105+
"UserId": "111",
106+
"AppName": "test-app",
107+
"CurrentTime": "2006-01-02T15:04:05Z",
108+
"Environment": "test-env",
109+
"RemoteAddress": "1.2.3.4",
110+
"SessionId": "test-session",
84111
},
85112
)
86113
enabled, _ = ofClient.BooleanValue(context.Background(), "users-flag", false, evalCtx)
@@ -89,6 +116,97 @@ func TestBooleanEvaluationByUser(t *testing.T) {
89116
}
90117
}
91118

119+
func TestStringEvaluationByCurrentTime(t *testing.T) {
120+
resolution := provider.StringEvaluation(context.Background(), "variant-flag-by-date", "fallback", map[string]interface{}{
121+
"UserId": "2",
122+
"CurrentTime": "2025-01-02T15:04:05Z",
123+
})
124+
enabled, _ := resolution.ProviderResolutionDetail.FlagMetadata.GetBool("enabled")
125+
if enabled == false {
126+
t.Fatalf("Expected feature to be enabled")
127+
}
128+
129+
if resolution.ProviderResolutionDetail.Variant != "var1" {
130+
t.Fatalf("Expected variant name")
131+
}
132+
if resolution.Value != "v1" {
133+
t.Fatalf("Expected one of the variant payloads")
134+
}
135+
136+
resolution = provider.StringEvaluation(context.Background(), "variant-flag-by-date", "fallback", map[string]interface{}{
137+
"UserId": "2",
138+
"CurrentTime": "2023-01-02T15:04:05Z",
139+
})
140+
if resolution.Value != "fallback" {
141+
t.Fatalf("Expected fallback value")
142+
}
143+
}
144+
145+
func TestInvalidContextEvaluation(t *testing.T) {
146+
evalCtx := make(of.FlattenedContext)
147+
defaultValue := true
148+
evalCtx["Invalid-key"] = make(chan int)
149+
resolution := provider.BooleanEvaluation(context.Background(), "non-existing-flag", defaultValue, evalCtx)
150+
if resolution.Value != defaultValue {
151+
t.Errorf("Expected value to be %v when evaluation context is invalid, got %v", defaultValue, resolution.Value)
152+
}
153+
if resolution.Reason != of.ErrorReason {
154+
t.Errorf("Expected reason to be %s, got %s", of.ErrorReason, resolution.Reason)
155+
}
156+
}
157+
158+
func TestEvaluationMethods(t *testing.T) {
159+
160+
tests := []struct {
161+
flag string
162+
defaultValue interface{}
163+
evalCtx of.FlattenedContext
164+
expected interface{}
165+
expectedError string
166+
}{
167+
{flag: "DateExample", defaultValue: false, evalCtx: of.FlattenedContext{}, expected: true, expectedError: ""},
168+
{flag: "variant-flag", defaultValue: false, evalCtx: of.FlattenedContext{}, expected: true, expectedError: ""},
169+
{flag: "double-flag", defaultValue: 9.9, evalCtx: of.FlattenedContext{}, expected: 1.23, expectedError: ""},
170+
{flag: "variant-flag", defaultValue: "fallback", evalCtx: of.FlattenedContext{}, expected: "v1", expectedError: ""},
171+
{flag: "json-flag", defaultValue: "fallback", evalCtx: of.FlattenedContext{}, expected: "{\n \"k1\": \"v1\"\n}", expectedError: ""},
172+
{flag: "csv-flag", defaultValue: "fallback", evalCtx: of.FlattenedContext{}, expected: "a,b,c", expectedError: ""},
173+
174+
{flag: "csv-invalid_flag", defaultValue: false, evalCtx: of.FlattenedContext{}, expected: false, expectedError: ""},
175+
{flag: "csv-invalid_flag", defaultValue: true, evalCtx: of.FlattenedContext{}, expected: true, expectedError: ""},
176+
177+
{"float", 1.23, of.FlattenedContext{"UserID": "123"}, 1.23, "flag not found"},
178+
{"number", int64(43), of.FlattenedContext{"UserID": "123"}, int64(43), "flag not found"},
179+
{"object", map[string]interface{}{"key1": "other-value"}, of.FlattenedContext{"UserID": "123"}, map[string]interface{}{"key1": "other-value"}, "flag not found"},
180+
{"string", "value2", of.FlattenedContext{"UserID": "123"}, "value2", "flag not found"},
181+
182+
{"invalid_user_context", false, of.FlattenedContext{"UserID": "123", "invalid": "value"}, false, ""},
183+
{"enriched_user_context", false, of.FlattenedContext{"UserID": "123", "Email": "v", "IpAddress": "v", "UserAgent": "v", "Country": "v", "Locale": "v"}, false, ""},
184+
{"missing_feature_config", int64(43), of.FlattenedContext{"UserID": "123"}, int64(43), ""},
185+
{"empty_context", int64(43), of.FlattenedContext{}, int64(43), ""},
186+
}
187+
188+
for _, test := range tests {
189+
rt := reflect.TypeOf(test.expected)
190+
switch rt.Kind() {
191+
case reflect.Bool:
192+
res := provider.BooleanEvaluation(context.Background(), test.flag, test.defaultValue.(bool), test.evalCtx)
193+
require.Equal(t, test.expected, res.Value, fmt.Errorf("failed for test flag `%s`", test.flag))
194+
case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64:
195+
res := provider.IntEvaluation(context.Background(), test.flag, test.defaultValue.(int64), test.evalCtx)
196+
require.Equal(t, test.expected, res.Value, fmt.Errorf("failed for test flag `%s`", test.flag))
197+
case reflect.Float32, reflect.Float64:
198+
res := provider.FloatEvaluation(context.Background(), test.flag, test.defaultValue.(float64), test.evalCtx)
199+
require.Equal(t, test.expected, res.Value, fmt.Errorf("failed for test flag `%s`", test.flag))
200+
case reflect.String:
201+
res := provider.StringEvaluation(context.Background(), test.flag, test.defaultValue.(string), test.evalCtx)
202+
require.Equal(t, test.expected, res.Value, fmt.Errorf("failed for test flag `%s`", test.flag))
203+
default:
204+
res := provider.ObjectEvaluation(context.Background(), test.flag, test.defaultValue, test.evalCtx)
205+
require.Equal(t, test.expected, res.Value, fmt.Errorf("failed for test flag `%s`", test.flag))
206+
}
207+
}
208+
}
209+
92210
// global cleanup
93211
func cleanup() {
94212
provider.Shutdown()
@@ -100,7 +218,9 @@ func TestMain(m *testing.M) {
100218
demoReader, err := os.Open("demo_app_toggles.json")
101219
if err != nil {
102220
fmt.Printf("Error during features file open: %v\n", err)
221+
os.Exit(1)
103222
}
223+
defer demoReader.Close()
104224

105225
providerOptions := unleashProvider.ProviderConfig{
106226
Options: []unleash.ConfigOption{
@@ -118,10 +238,12 @@ func TestMain(m *testing.M) {
118238
if err != nil {
119239
fmt.Printf("Error during provider open: %v\n", err)
120240
}
121-
err = provider.Init(of.EvaluationContext{})
241+
err = of.SetProviderAndWait(provider)
122242
if err != nil {
123-
fmt.Printf("Error during provider init: %v\n", err)
243+
fmt.Printf("Error during SetProviderAndWait: %v\n", err)
244+
os.Exit(1)
124245
}
246+
ofClient = of.NewClient("my-app")
125247

126248
fmt.Printf("provider: %v\n", provider)
127249

‎tests/flagd/pkg/integration/flagd_json_evaluator.go

+122-31
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import (
99
"github.com/open-feature/go-sdk/openfeature"
1010
)
1111

12-
// ctxKeyKey is the key used to pass flag key across context.Context
12+
// ctxKeyKey is the key used to pass flag key across tests
1313
type ctxKeyKey struct{}
1414

15-
// ctxKeyKey is the key used to pass the default across context.Context
15+
// ctxKeyKey is the key used to pass the default value across tests
1616
type ctxDefaultKey struct{}
1717

18-
// ctxValueKey is the key used to pass the value across context.Context
19-
type ctxValueKey struct{}
18+
// ctxEvaluationCtxKey is the key used to pass openfeature evaluation context across tests
19+
type ctxEvaluationCtxKey struct{}
20+
21+
// ctxReasonKey is the key used to pass the evaluation reason across tests
22+
type ctxReasonKey struct{}
2023

2124
// InitializeFlagdJsonTestSuite register provider supplier and register test steps
2225
func InitializeFlagdJsonTestSuite(providerSupplier func() openfeature.FeatureProvider) func(*godog.TestSuiteContext) {
@@ -28,55 +31,143 @@ func InitializeFlagdJsonTestSuite(providerSupplier func() openfeature.FeaturePro
2831
// InitializeFlagdJsonScenario initializes the flagd json evaluator test scenario
2932
func InitializeFlagdJsonScenario(ctx *godog.ScenarioContext) {
3033
ctx.Step(`^a flagd provider is set$`, aFlagdProviderIsSet)
34+
3135
ctx.Step(`^a string flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aFlagdStringFlagWithKeyIsEvaluatedWithDefaultValue)
32-
ctx.Step(`^a context containing a key "([^"]*)", with value "([^"]*)"$`, aContextContainingAKeyWithValue)
33-
ctx.Step(`^a context containing a nested property with outer key "([^"]*)" and inner key "([^"]*)", with value "([^"]*)"$`, aContextContainingANestedPropertyWithOuterKeyAndInnerKeyWithValue)
34-
ctx.Step(`^the returned value should be "([^"]*)"$`, theReturnedValueShouldBe)
36+
ctx.Step(`^an integer flag with key "([^"]*)" is evaluated with default value (\d+)$`, aFlagdIntegerFlagWithKeyIsEvaluatedWithDefaultValue)
37+
38+
ctx.Step(`^a context containing a key "([^"]*)", with value "([^"]*)"$`, aContextContainingAKeyWitStringValue)
39+
ctx.Step(`^a context containing a key "([^"]*)", with value (\d+)$`, aContextContainingAKeyWithIntValue)
40+
ctx.Step(`^a context containing a targeting key with value "([^"]*)"$`, aContextContainingATargetingKey)
41+
ctx.Step(`^a context containing a nested property with outer key "([^"]*)" and inner key "([^"]*)", with value "([^"]*)"$`, aContextContainingANestedPropertyWithOuterKeyAndInnerKeyWithStringValue)
42+
ctx.Step(`^a context containing a nested property with outer key "([^"]*)" and inner key "([^"]*)", with value (\d+)$`, aContextContainingANestedPropertyWithOuterKeyAndInnerKeyWithIntValue)
43+
44+
ctx.Step(`^the returned value should be "([^"]*)"$`, theReturnedValueShouldBeString)
45+
ctx.Step(`^the returned value should be (\d+)$`, theReturnedValueShouldBeInt)
46+
ctx.Step(`^the returned value should be -(\d+)$`, theReturnedValueShouldBeNegativeInt)
47+
48+
ctx.Step(`^the returned reason should be "([^"]*)"$`, theReturnedReasonShouldBe)
3549
}
3650

51+
// setup
52+
3753
func aFlagdStringFlagWithKeyIsEvaluatedWithDefaultValue(ctx context.Context, key, defaultValue string) (context.Context, error) {
3854
ctx = context.WithValue(ctx, ctxKeyKey{}, key)
3955
ctx = context.WithValue(ctx, ctxDefaultKey{}, defaultValue)
4056
return ctx, nil
4157
}
4258

43-
func aContextContainingAKeyWithValue(ctx context.Context, evalContextKey, evalContextValue string) (context.Context, error) {
59+
func aFlagdIntegerFlagWithKeyIsEvaluatedWithDefaultValue(ctx context.Context, key string, defaultValue int64) (context.Context, error) {
60+
ctx = context.WithValue(ctx, ctxKeyKey{}, key)
61+
ctx = context.WithValue(ctx, ctxDefaultKey{}, defaultValue)
62+
return ctx, nil
63+
}
64+
65+
// set contexts
66+
67+
func aContextContainingAKeyWitStringValue(ctx context.Context, evalContextKey, evalContextValue string) (context.Context, error) {
68+
evalCtx := openfeature.NewEvaluationContext("", map[string]interface{}{
69+
evalContextKey: evalContextValue,
70+
})
71+
72+
return context.WithValue(ctx, ctxEvaluationCtxKey{}, evalCtx), nil
73+
}
74+
75+
func aContextContainingAKeyWithIntValue(ctx context.Context, evalContextKey string, evalContextValue int64) (context.Context, error) {
76+
evalCtx := openfeature.NewEvaluationContext("", map[string]interface{}{
77+
evalContextKey: evalContextValue,
78+
})
79+
80+
return context.WithValue(ctx, ctxEvaluationCtxKey{}, evalCtx), nil
81+
}
82+
83+
func aContextContainingATargetingKey(ctx context.Context, targetingKet string) (context.Context, error) {
84+
evalCtx := openfeature.NewEvaluationContext(targetingKet, map[string]interface{}{})
85+
86+
return context.WithValue(ctx, ctxEvaluationCtxKey{}, evalCtx), nil
87+
}
88+
89+
func aContextContainingANestedPropertyWithOuterKeyAndInnerKeyWithStringValue(ctx context.Context, outerKey, innerKey, value string) (context.Context, error) {
90+
evalCtx := openfeature.NewEvaluationContext("", map[string]interface{}{
91+
outerKey: map[string]interface{}{
92+
innerKey: value,
93+
},
94+
})
95+
96+
return context.WithValue(ctx, ctxEvaluationCtxKey{}, evalCtx), nil
97+
}
98+
99+
func aContextContainingANestedPropertyWithOuterKeyAndInnerKeyWithIntValue(ctx context.Context, outerKey string, innerKey string, value int) (context.Context, error) {
100+
evalCtx := openfeature.NewEvaluationContext("", map[string]interface{}{
101+
outerKey: map[string]interface{}{
102+
innerKey: value,
103+
},
104+
})
105+
106+
return context.WithValue(ctx, ctxEvaluationCtxKey{}, evalCtx), nil
107+
}
108+
109+
// validate
110+
111+
func theReturnedValueShouldBeString(ctx context.Context, expectedValue string) (context.Context, error) {
44112
client := ctx.Value(ctxClientKey{}).(*openfeature.Client)
45113
key := ctx.Value(ctxKeyKey{}).(string)
46114
defaultValue := ctx.Value(ctxDefaultKey{}).(string)
47-
ec := openfeature.NewEvaluationContext("", map[string]interface{}{
48-
evalContextKey: evalContextValue,
49-
})
50-
got, err := client.StringValue(ctx, key, defaultValue, ec)
51-
if err != nil {
52-
return ctx, fmt.Errorf("error: %w", err)
115+
116+
var evalCtx openfeature.EvaluationContext
117+
if ctx.Value(ctxEvaluationCtxKey{}) != nil {
118+
evalCtx = ctx.Value(ctxEvaluationCtxKey{}).(openfeature.EvaluationContext)
53119
}
54-
return context.WithValue(ctx, ctxValueKey{}, got), nil
120+
121+
// error from evaluation are ignored as we only check for detail content
122+
details, _ := client.StringValueDetails(ctx, key, defaultValue, evalCtx)
123+
124+
if details.Value != expectedValue {
125+
return ctx, fmt.Errorf("expected resolved int value to be %s, got %s", expectedValue, details.Value)
126+
}
127+
128+
return context.WithValue(ctx, ctxReasonKey{}, details.Reason), nil
129+
}
130+
131+
func theReturnedValueShouldBeInt(ctx context.Context, expectedValue int64) (context.Context, error) {
132+
return validateInteger(ctx, expectedValue, false)
133+
}
134+
135+
func theReturnedValueShouldBeNegativeInt(ctx context.Context, expectedValue int64) (context.Context, error) {
136+
return validateInteger(ctx, expectedValue, true)
55137
}
56138

57-
func aContextContainingANestedPropertyWithOuterKeyAndInnerKeyWithValue(ctx context.Context, outerKey, innerKey, name string) (context.Context, error) {
139+
func validateInteger(ctx context.Context, expectedValue int64, isNegative bool) (context.Context, error) {
58140
client := ctx.Value(ctxClientKey{}).(*openfeature.Client)
59141
key := ctx.Value(ctxKeyKey{}).(string)
60-
defaultValue := ctx.Value(ctxDefaultKey{}).(string)
61-
ec := openfeature.NewEvaluationContext("", map[string]interface{}{
62-
outerKey: map[string]interface{}{
63-
innerKey: name,
64-
},
65-
})
66-
got, err := client.StringValue(ctx, key, defaultValue, ec)
67-
if err != nil {
68-
return ctx, fmt.Errorf("error: %w", err)
142+
defaultValue := ctx.Value(ctxDefaultKey{}).(int64)
143+
144+
var evalCtx openfeature.EvaluationContext
145+
if ctx.Value(ctxEvaluationCtxKey{}) != nil {
146+
evalCtx = ctx.Value(ctxEvaluationCtxKey{}).(openfeature.EvaluationContext)
69147
}
70-
return context.WithValue(ctx, ctxValueKey{}, got), nil
148+
149+
// error from evaluation are ignored as we only check for detail content
150+
details, _ := client.IntValueDetails(ctx, key, defaultValue, evalCtx)
151+
152+
if isNegative {
153+
expectedValue = -(expectedValue)
154+
}
155+
156+
if details.Value != expectedValue {
157+
return ctx, fmt.Errorf("expected resolved int value to be %d, got %d", expectedValue, details.Value)
158+
}
159+
160+
return context.WithValue(ctx, ctxReasonKey{}, details.Reason), nil
71161
}
72162

73-
func theReturnedValueShouldBe(ctx context.Context, expectedValue string) (context.Context, error) {
74-
got, ok := ctx.Value(ctxValueKey{}).(string)
163+
func theReturnedReasonShouldBe(ctx context.Context, expectedReason string) (context.Context, error) {
164+
evaluatedReason, ok := ctx.Value(ctxReasonKey{}).(openfeature.Reason)
75165
if !ok {
76-
return ctx, errors.New("no flag resolution result")
166+
return ctx, errors.New("no flag resolution reason set")
77167
}
78-
if got != expectedValue {
79-
return ctx, fmt.Errorf("expected resolved int value to be %s, got %s", expectedValue, got)
168+
169+
if string(evaluatedReason) != expectedReason {
170+
return ctx, fmt.Errorf("expected resolved int value to be %s, got %s", expectedReason, evaluatedReason)
80171
}
81172
return ctx, nil
82173
}

0 commit comments

Comments
 (0)
Please sign in to comment.