Skip to content

Commit 85a1d13

Browse files
chore(test): Add test for API implementation (#10)
* chore(test): Add test for dao_error * chore(test): Add test for UUID generation in rules * chore(test): Add test for parsing ID parsing error in rules * chore(test): Add test to postgres implementation * Add postgres implem test
1 parent efca05a commit 85a1d13

17 files changed

+1560
-55
lines changed

dao/dbmodel/feature_flag.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dbmodel
22

33
import (
4+
"errors"
45
"time"
56

67
"github.com/go-feature-flag/app-api/model"
@@ -51,17 +52,23 @@ func FromModelFeatureFlag(mff model.FeatureFlag) (FeatureFlag, error) {
5152
return ff, nil
5253
}
5354

54-
func (ff *FeatureFlag) ToModelFeatureFlag(rules []Rule) model.FeatureFlag {
55+
func (ff *FeatureFlag) ToModelFeatureFlag(rules []Rule) (model.FeatureFlag, error) {
5556
var apiRules = make([]model.Rule, 0)
5657
var defaultRule *model.Rule
58+
hasDefaultRule := false
5759
for _, rule := range rules {
5860
convertedRule := rule.ToModelRule()
5961
if rule.IsDefault {
62+
hasDefaultRule = true
6063
defaultRule = &convertedRule
6164
continue
6265
}
6366
apiRules = append(apiRules, convertedRule)
6467
}
68+
if !hasDefaultRule {
69+
return model.FeatureFlag{}, errors.New("default rule is required")
70+
}
71+
6572
variations := make(map[string]interface{})
6673
if ff.Variations != nil {
6774
variations = ff.Variations
@@ -85,5 +92,6 @@ func (ff *FeatureFlag) ToModelFeatureFlag(rules []Rule) model.FeatureFlag {
8592
LastUpdatedDate: ff.LastUpdatedDate,
8693
Rules: &apiRules,
8794
DefaultRule: defaultRule,
88-
}
95+
LastModifiedBy: ff.LastModifiedBy,
96+
}, nil
8997
}

dao/dbmodel/feature_flag_test.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,47 @@ func TestToModelFeatureFlag(t *testing.T) {
204204
ruleId2, _ := uuid.Parse("123e4567-e89b-12d3-a456-426614174222")
205205

206206
tests := []struct {
207-
name string
208-
dbFF dbmodel.FeatureFlag
209-
dbRule []dbmodel.Rule
210-
want model.FeatureFlag
207+
name string
208+
dbFF dbmodel.FeatureFlag
209+
dbRule []dbmodel.Rule
210+
want model.FeatureFlag
211+
wantErr assert.ErrorAssertionFunc
211212
}{
213+
{
214+
name: "should error if no default rule",
215+
dbFF: dbmodel.FeatureFlag{
216+
ID: flagID,
217+
Name: "my-flag",
218+
Description: testutils.String("my flag description"),
219+
Variations: dbmodel.JSONB(map[string]interface{}{
220+
"A": "a",
221+
"B": "b",
222+
}),
223+
Type: model.FlagTypeString,
224+
BucketingKey: testutils.String("teamID"),
225+
Metadata: dbmodel.JSONB(map[string]interface{}{
226+
"key": "value",
227+
}),
228+
TrackEvents: testutils.Bool(true),
229+
Disable: testutils.Bool(false),
230+
Version: testutils.String("1.0.0"),
231+
CreatedDate: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
232+
LastUpdatedDate: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
233+
},
234+
dbRule: []dbmodel.Rule{
235+
{
236+
ID: ruleId1,
237+
Name: "Rule 1",
238+
FeatureFlagID: flagID,
239+
Disable: false,
240+
Percentages: testutils.JSONB(dbmodel.JSONB(map[string]interface{}{"A": float64(50), "B": float64(50)})),
241+
IsDefault: false,
242+
Query: testutils.String(`targetingKey eq "foo"`),
243+
OrderIndex: 6,
244+
},
245+
},
246+
wantErr: assert.Error,
247+
},
212248
{
213249
name: "should convert model.FeatureFlag to dbmodel.FeatureFlag",
214250
dbFF: dbmodel.FeatureFlag{
@@ -236,16 +272,16 @@ func TestToModelFeatureFlag(t *testing.T) {
236272
Name: "Rule 1",
237273
FeatureFlagID: flagID,
238274
Disable: false,
239-
Percentages: dbmodel.JSONB(map[string]interface{}{"A": float64(50), "B": float64(50)}),
275+
Percentages: testutils.JSONB(dbmodel.JSONB(map[string]interface{}{"A": float64(50), "B": float64(50)})),
240276
IsDefault: false,
241-
Query: `targetingKey eq "foo"`,
277+
Query: testutils.String(`targetingKey eq "foo"`),
242278
OrderIndex: 6,
243279
},
244280
{
245281
ID: ruleId2,
246282
Name: "rule 2",
247283
FeatureFlagID: flagID,
248-
Query: `targetingKey eq "bar"`,
284+
Query: testutils.String(`targetingKey eq "bar"`),
249285
Disable: true,
250286
OrderIndex: 10,
251287
IsDefault: false,
@@ -265,6 +301,7 @@ func TestToModelFeatureFlag(t *testing.T) {
265301
IsDefault: true,
266302
},
267303
},
304+
wantErr: assert.NoError,
268305
want: model.FeatureFlag{
269306
ID: flagID.String(),
270307
Name: "my-flag",
@@ -320,7 +357,8 @@ func TestToModelFeatureFlag(t *testing.T) {
320357
}
321358
for _, tt := range tests {
322359
t.Run(tt.name, func(t *testing.T) {
323-
got := tt.dbFF.ToModelFeatureFlag(tt.dbRule)
360+
got, err := tt.dbFF.ToModelFeatureFlag(tt.dbRule)
361+
tt.wantErr(t, err)
324362
assert.Equalf(t, tt.want, got, "FromModelFeatureFlag")
325363
})
326364
}

dao/dbmodel/rule.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ type Rule struct {
1212
FeatureFlagID uuid.UUID `db:"feature_flag_id"`
1313
IsDefault bool `db:"is_default"`
1414
Name string `db:"name"`
15-
Query string `db:"query"`
15+
Query *string `db:"query"`
1616
VariationResult *string `db:"variation_result"`
17-
Percentages JSONB `db:"percentages"` // JSONB is stored as string
17+
Percentages *JSONB `db:"percentages"` // JSONB is stored as string
1818
Disable bool `db:"disable"`
1919
ProgressiveRolloutInitialVariation *string `db:"progressive_rollout_initial_variation"`
2020
ProgressiveRolloutEndVariation *string `db:"progressive_rollout_end_variation"`
@@ -48,7 +48,7 @@ func FromModelRule(mr model.Rule, featureFlagID uuid.UUID, isDefault bool, order
4848
FeatureFlagID: featureFlagID,
4949
IsDefault: isDefault,
5050
Name: mr.Name,
51-
Query: mr.Query,
51+
Query: &mr.Query,
5252
Disable: mr.Disable,
5353
OrderIndex: orderIndex,
5454
}
@@ -62,7 +62,8 @@ func FromModelRule(mr model.Rule, featureFlagID uuid.UUID, isDefault bool, order
6262
for k, v := range *mr.Percentages {
6363
percentages[k] = v
6464
}
65-
dbr.Percentages = JSONB(percentages)
65+
jsonbPercentages := JSONB(percentages)
66+
dbr.Percentages = &jsonbPercentages
6667
}
6768

6869
if mr.ProgressiveRollout != nil {
@@ -80,16 +81,16 @@ func (rule *Rule) ToModelRule() model.Rule {
8081
apiRule := model.Rule{
8182
ID: rule.ID.String(),
8283
Name: rule.Name,
83-
Query: rule.Query,
8484
Disable: rule.Disable,
8585
}
86-
86+
if rule.Query != nil {
87+
apiRule.Query = *rule.Query
88+
}
8789
if rule.VariationResult != nil {
8890
apiRule.VariationResult = rule.VariationResult
8991
}
90-
9192
if rule.Percentages != nil {
92-
for k, v := range rule.Percentages {
93+
for k, v := range *rule.Percentages {
9394
if apiRule.Percentages == nil {
9495
apiRule.Percentages = &map[string]float64{}
9596
}

dao/dbmodel/rule_test.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/go-feature-flag/app-api/testutils"
1010
"github.com/google/uuid"
1111
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
1213
)
1314

1415
func TestFromModelRule(t *testing.T) {
@@ -46,7 +47,7 @@ func TestFromModelRule(t *testing.T) {
4647
ID: ruleID,
4748
Name: "defaultRule",
4849
FeatureFlagID: flagID,
49-
Query: "",
50+
Query: testutils.String(""),
5051
Disable: false,
5152
OrderIndex: -1,
5253
VariationResult: testutils.String("A"),
@@ -73,7 +74,7 @@ func TestFromModelRule(t *testing.T) {
7374
ID: ruleID,
7475
Name: "rule 1",
7576
FeatureFlagID: flagID,
76-
Query: `targetingKey eq "foo"`,
77+
Query: testutils.String(`targetingKey eq "foo"`),
7778
Disable: true,
7879
OrderIndex: 10,
7980
VariationResult: testutils.String("A"),
@@ -103,11 +104,11 @@ func TestFromModelRule(t *testing.T) {
103104
ID: ruleID,
104105
Name: "rule 1",
105106
FeatureFlagID: flagID,
106-
Query: `targetingKey eq "foo"`,
107+
Query: testutils.String(`targetingKey eq "foo"`),
107108
Disable: true,
108109
OrderIndex: 10,
109110
IsDefault: false,
110-
Percentages: dbmodel.JSONB(map[string]interface{}{
111+
Percentages: testutils.JSONB(map[string]interface{}{
111112
"A": float64(50),
112113
"B": float64(50),
113114
}),
@@ -144,7 +145,7 @@ func TestFromModelRule(t *testing.T) {
144145
ID: ruleID,
145146
Name: "rule 1",
146147
FeatureFlagID: flagID,
147-
Query: `targetingKey eq "foo"`,
148+
Query: testutils.String(`targetingKey eq "foo"`),
148149
Disable: true,
149150
OrderIndex: 10,
150151
IsDefault: false,
@@ -167,3 +168,53 @@ func TestFromModelRule(t *testing.T) {
167168
})
168169
}
169170
}
171+
172+
func TestFromModelRuleCreateUUID(t *testing.T) {
173+
rule := model.Rule{
174+
Name: "rule 1",
175+
ProgressiveRollout: &model.ProgressiveRollout{
176+
Initial: &model.ProgressiveRolloutStep{
177+
Variation: testutils.String("A"),
178+
Percentage: testutils.Float64(0),
179+
Date: testutils.Time(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
180+
},
181+
End: &model.ProgressiveRolloutStep{
182+
Variation: testutils.String("B"),
183+
Percentage: testutils.Float64(100),
184+
Date: testutils.Time(time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)),
185+
},
186+
},
187+
Query: `targetingKey eq "foo"`,
188+
Disable: true,
189+
}
190+
191+
got, err := dbmodel.FromModelRule(rule, uuid.New(), false, 1)
192+
require.NoError(t, err)
193+
assert.NotNil(t, got.ID)
194+
assert.NotEqual(t, uuid.Nil, got.ID)
195+
}
196+
197+
func TestFromModelRuleErrorParsingUUID(t *testing.T) {
198+
rule := model.Rule{
199+
Name: "rule 1",
200+
ID: "invalid-uuid",
201+
ProgressiveRollout: &model.ProgressiveRollout{
202+
Initial: &model.ProgressiveRolloutStep{
203+
Variation: testutils.String("A"),
204+
Percentage: testutils.Float64(0),
205+
Date: testutils.Time(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
206+
},
207+
End: &model.ProgressiveRolloutStep{
208+
Variation: testutils.String("B"),
209+
Percentage: testutils.Float64(100),
210+
Date: testutils.Time(time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)),
211+
},
212+
},
213+
Query: `targetingKey eq "foo"`,
214+
Disable: true,
215+
}
216+
217+
_, err := dbmodel.FromModelRule(rule, uuid.New(), false, 1)
218+
require.Error(t, err)
219+
assert.Equal(t, "invalid UUID length: 12", err.Error())
220+
}

dao/err/dao_error.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package daoerr
22

3+
import "errors"
4+
35
type DaoErrorCode string
46

57
const (
@@ -17,6 +19,10 @@ type DaoError interface {
1719
}
1820

1921
func NewDaoError(code DaoErrorCode, err error) DaoError {
22+
if err == nil {
23+
err = errors.New("unknown error")
24+
}
25+
2026
return daoError{
2127
error: err,
2228
code: code,

dao/err/dao_error_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package daoerr_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
daoerr "github.com/go-feature-flag/app-api/dao/err"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestNewDaoError(t *testing.T) {
12+
type args struct {
13+
code daoerr.DaoErrorCode
14+
err error
15+
}
16+
tests := []struct {
17+
name string
18+
args args
19+
wantErr error
20+
wantCode daoerr.DaoErrorCode
21+
}{
22+
{
23+
name: "Should be able to create a new DaoError",
24+
args: args{
25+
code: daoerr.NotFound,
26+
err: fmt.Errorf("not found"),
27+
},
28+
wantErr: fmt.Errorf("not found"),
29+
wantCode: daoerr.NotFound,
30+
},
31+
{
32+
name: "Should not fail with a nil error",
33+
args: args{
34+
code: daoerr.NotFound,
35+
err: nil,
36+
},
37+
wantErr: fmt.Errorf("unknown error"),
38+
wantCode: daoerr.NotFound,
39+
},
40+
}
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
err := daoerr.NewDaoError(tt.args.code, tt.args.err)
44+
assert.Equal(t, tt.wantErr.Error(), err.Error())
45+
assert.Equal(t, tt.wantCode, err.Code())
46+
})
47+
}
48+
}

dao/err/postgres_error_wrapper.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ import (
44
"database/sql"
55
"errors"
66

7+
"github.com/google/uuid"
78
"github.com/lib/pq"
89
)
910

1011
// WrapPostgresError wraps a postgres error into a DaoError to have a DB agnostic error handling in the handlers
1112
func WrapPostgresError(err error) DaoError {
12-
if errors.Is(err, sql.ErrNoRows) {
13-
return NewDaoError(NotFound, err)
14-
}
1513
var pqErr *pq.Error
16-
if errors.As(err, &pqErr) && pqErr.Code == "22P02" {
14+
switch {
15+
case errors.Is(err, sql.ErrNoRows):
16+
return NewDaoError(NotFound, err)
17+
case uuid.IsInvalidLengthError(err), errors.As(err, &pqErr) && pqErr.Code == "22P02":
1718
return NewDaoError(InvalidUUID, err)
19+
default:
20+
return NewDaoError(UnknownError, err)
21+
1822
}
19-
return NewDaoError(UnknownError, err)
2023
}
File renamed without changes.

0 commit comments

Comments
 (0)