Skip to content

Commit 07a189c

Browse files
chore: Add tests for flags handlers (#8)
* Add test for GetAllFeatureFlags * add test for createFlags * Bump go * Add test for flags handlers
1 parent f38e84e commit 07a189c

File tree

12 files changed

+1076
-41
lines changed

12 files changed

+1076
-41
lines changed

dao/err/postgres_error_wrapper.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package daoErr
33
import (
44
"database/sql"
55
"errors"
6+
67
"github.com/lib/pq"
78
)
89

dao/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dao
22

33
import (
44
"context"
5+
56
daoErr "github.com/go-feature-flag/app-api/dao/err"
67
"github.com/go-feature-flag/app-api/model"
78
)

dao/inmemory_impl_mock.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dao
33
import (
44
"context"
55
"fmt"
6+
67
daoErr "github.com/go-feature-flag/app-api/dao/err"
78
"github.com/go-feature-flag/app-api/model"
89
_ "github.com/lib/pq" // we import the driver used by sqlx
@@ -22,11 +23,23 @@ type InMemoryMockDao struct {
2223

2324
// GetFlags return all the flags
2425
func (m *InMemoryMockDao) GetFlags(ctx context.Context) ([]model.FeatureFlag, daoErr.DaoError) {
26+
if ctx.Value("error") != nil {
27+
if err, ok := ctx.Value("error").(daoErr.DaoErrorCode); ok {
28+
return nil, daoErr.NewDaoError(err, fmt.Errorf("error on get flags"))
29+
}
30+
return nil, daoErr.NewDaoError(daoErr.UnknownError, fmt.Errorf("error on get flags"))
31+
}
2532
return m.flags, nil
2633
}
2734

2835
// GetFlagByID return a flag by its ID
2936
func (m *InMemoryMockDao) GetFlagByID(ctx context.Context, id string) (model.FeatureFlag, daoErr.DaoError) {
37+
if ctx.Value("error") != nil {
38+
if err, ok := ctx.Value("error").(daoErr.DaoErrorCode); ok {
39+
return model.FeatureFlag{}, daoErr.NewDaoError(err, fmt.Errorf("error on get flag by id"))
40+
}
41+
return model.FeatureFlag{}, daoErr.NewDaoError(daoErr.UnknownError, fmt.Errorf("error on get flag by id"))
42+
}
3043
for _, flag := range m.flags {
3144
if flag.ID == id {
3245
return flag, nil
@@ -37,6 +50,12 @@ func (m *InMemoryMockDao) GetFlagByID(ctx context.Context, id string) (model.Fea
3750

3851
// GetFlagByName return a flag by its name
3952
func (m *InMemoryMockDao) GetFlagByName(ctx context.Context, name string) (model.FeatureFlag, daoErr.DaoError) {
53+
if ctx.Value("error") != nil {
54+
if err, ok := ctx.Value("error").(daoErr.DaoErrorCode); ok {
55+
return model.FeatureFlag{}, daoErr.NewDaoError(err, fmt.Errorf("error on get flag by name"))
56+
}
57+
return model.FeatureFlag{}, daoErr.NewDaoError(daoErr.UnknownError, fmt.Errorf("error on get flag by name"))
58+
}
4059
for _, flag := range m.flags {
4160
if flag.Name == name {
4261
return flag, nil
@@ -47,11 +66,24 @@ func (m *InMemoryMockDao) GetFlagByName(ctx context.Context, name string) (model
4766

4867
// CreateFlag create a new flag, return the id of the flag
4968
func (m *InMemoryMockDao) CreateFlag(ctx context.Context, flag model.FeatureFlag) (string, daoErr.DaoError) {
69+
if ctx.Value("error_create") != nil {
70+
if err, ok := ctx.Value("error_create").(daoErr.DaoErrorCode); ok {
71+
return "", daoErr.NewDaoError(err, fmt.Errorf("error creating flag"))
72+
}
73+
return "", daoErr.NewDaoError(daoErr.UnknownError, fmt.Errorf("error creating flag"))
74+
}
75+
5076
m.flags = append(m.flags, flag)
5177
return flag.ID, nil
5278
}
5379

5480
func (m *InMemoryMockDao) UpdateFlag(ctx context.Context, flag model.FeatureFlag) daoErr.DaoError {
81+
if ctx.Value("error_update") != nil {
82+
if err, ok := ctx.Value("error_update").(daoErr.DaoErrorCode); ok {
83+
return daoErr.NewDaoError(err, fmt.Errorf("error on update flags"))
84+
}
85+
return daoErr.NewDaoError(daoErr.UnknownError, fmt.Errorf("error on update flags"))
86+
}
5587
for index, f := range m.flags {
5688
if f.ID == flag.ID {
5789
m.flags[index] = flag
@@ -62,6 +94,13 @@ func (m *InMemoryMockDao) UpdateFlag(ctx context.Context, flag model.FeatureFlag
6294
}
6395

6496
func (m *InMemoryMockDao) DeleteFlagByID(ctx context.Context, id string) daoErr.DaoError {
97+
if ctx.Value("error_delete") != nil {
98+
if err, ok := ctx.Value("error_delete").(daoErr.DaoErrorCode); ok {
99+
return daoErr.NewDaoError(err, fmt.Errorf("error on get flags"))
100+
}
101+
return daoErr.NewDaoError(daoErr.UnknownError, fmt.Errorf("error on get flags"))
102+
}
103+
65104
newInmemoryFlagList := []model.FeatureFlag{}
66105
for _, f := range m.flags {
67106
if f.ID != id {
@@ -82,3 +121,7 @@ func (m *InMemoryMockDao) Ping() daoErr.DaoError {
82121
func (m *InMemoryMockDao) OnPingReturnError(v bool) {
83122
m.errorOnPing = v
84123
}
124+
125+
func (m *InMemoryMockDao) SetFlags(flags []model.FeatureFlag) {
126+
m.flags = flags
127+
}

dao/postgres_impl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
78
"github.com/go-feature-flag/app-api/dao/dbmodel"
89
daoErr "github.com/go-feature-flag/app-api/dao/err"
910
"github.com/go-feature-flag/app-api/model"

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/go-feature-flag/app-api
22

3-
go 1.22.7
3+
go 1.23.2
44

55
require (
66
github.com/google/uuid v1.6.0

handler/flags.go

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
11
package handler
22

33
import (
4-
"database/sql"
54
"errors"
65
"fmt"
7-
daoErr "github.com/go-feature-flag/app-api/dao/err"
8-
"github.com/labstack/echo/v4"
96
"net/http"
10-
"time"
117

128
"github.com/go-feature-flag/app-api/dao"
9+
daoErr "github.com/go-feature-flag/app-api/dao/err"
1310
"github.com/go-feature-flag/app-api/model"
11+
"github.com/go-feature-flag/app-api/util"
1412
"github.com/google/uuid"
15-
"github.com/lib/pq"
13+
"github.com/labstack/echo/v4"
1614
)
1715

16+
type FlagAPIHandlerOptions struct {
17+
Clock util.Clock
18+
}
19+
1820
type FlagAPIHandler struct {
19-
dao dao.Flags
21+
dao dao.Flags
22+
options *FlagAPIHandlerOptions
2023
}
2124

2225
// NewFlagAPIHandler creates a new instance of the FlagAPIHandler handler
2326
// It is a controller class to handle the feature flag configuration logic
24-
func NewFlagAPIHandler(dao dao.Flags) FlagAPIHandler {
25-
return FlagAPIHandler{dao: dao}
27+
func NewFlagAPIHandler(dao dao.Flags, options *FlagAPIHandlerOptions) FlagAPIHandler {
28+
if options == nil {
29+
options = &FlagAPIHandlerOptions{}
30+
}
31+
if options.Clock == nil {
32+
options.Clock = util.DefaultClock{}
33+
}
34+
return FlagAPIHandler{dao: dao, options: options}
2635
}
2736

2837
// GetAllFeatureFlags is returning the list of all the flags
@@ -87,8 +96,8 @@ func (f FlagAPIHandler) CreateNewFlag(c echo.Context) error {
8796
if flag.ID == "" {
8897
flag.ID = uuid.NewString()
8998
}
90-
flag.CreatedDate = time.Now()
91-
flag.LastUpdatedDate = time.Now()
99+
flag.CreatedDate = f.options.Clock.Now()
100+
flag.LastUpdatedDate = f.options.Clock.Now()
92101
// TODO: remove this line and extract the information from the token
93102
flag.LastModifiedBy = "toto"
94103

@@ -105,6 +114,9 @@ func (f FlagAPIHandler) CreateNewFlag(c echo.Context) error {
105114

106115
id, err := f.dao.CreateFlag(c.Request().Context(), flag)
107116
if err != nil {
117+
if err.Code() == daoErr.ConversionError {
118+
return c.JSON(model.NewHTTPError(http.StatusBadRequest, err))
119+
}
108120
return c.JSON(model.NewHTTPError(http.StatusInternalServerError, err))
109121
}
110122
flag.ID = id
@@ -123,8 +135,17 @@ func validateFlag(flag model.FeatureFlag) (int, error) {
123135
return status, err
124136
}
125137

126-
if flag.VariationType == "" {
138+
switch flag.VariationType {
139+
case model.FlagTypeBoolean,
140+
model.FlagTypeDouble,
141+
model.FlagTypeInteger,
142+
model.FlagTypeString,
143+
model.FlagTypeJSON:
144+
break
145+
case "":
127146
return http.StatusBadRequest, errors.New("flag type is required")
147+
default:
148+
return http.StatusBadRequest, fmt.Errorf("flag type %s not supported", flag.VariationType)
128149
}
129150

130151
for _, rule := range flag.GetRules() {
@@ -137,10 +158,15 @@ func validateFlag(flag model.FeatureFlag) (int, error) {
137158
}
138159

139160
func validateRule(rule *model.Rule, isDefault bool) (int, error) {
140-
if rule == nil ||
141-
(rule.ProgressiveRollout == nil &&
142-
rule.Percentages == nil &&
143-
(rule.VariationResult == nil || *rule.VariationResult == "")) {
161+
if rule == nil || *rule == (model.Rule{}) {
162+
if isDefault {
163+
return http.StatusBadRequest, errors.New("flag default rule is required")
164+
}
165+
return http.StatusBadRequest, errors.New("targeting rule is nil")
166+
}
167+
if rule.ProgressiveRollout == nil &&
168+
rule.Percentages == nil &&
169+
(rule.VariationResult == nil || *rule.VariationResult == "") {
144170
err := fmt.Errorf("invalid rule %s", rule.Name)
145171
if isDefault {
146172
err = errors.New("flag default rule is invalid")
@@ -150,7 +176,7 @@ func validateRule(rule *model.Rule, isDefault bool) (int, error) {
150176

151177
if !isDefault {
152178
if rule.Query == "" {
153-
return http.StatusBadRequest, errors.New("rule query is required")
179+
return http.StatusBadRequest, errors.New("query is required for targeting rules")
154180
}
155181
}
156182
return http.StatusOK, nil
@@ -168,8 +194,7 @@ func validateRule(rule *model.Rule, isDefault bool) (int, error) {
168194
// @Failure 500 {object} model.HTTPError "Internal server error"
169195
// @Router /v1/flags/{id} [put]
170196
func (f FlagAPIHandler) UpdateFlagByID(c echo.Context) error {
171-
// check if the flag exists
172-
_, err := f.dao.GetFlagByID(c.Request().Context(), c.Param("id"))
197+
retrievedFlag, err := f.dao.GetFlagByID(c.Request().Context(), c.Param("id"))
173198
if err != nil {
174199
return f.handleDaoError(c, err)
175200
}
@@ -187,11 +212,12 @@ func (f FlagAPIHandler) UpdateFlagByID(c echo.Context) error {
187212
if flag.ID == "" {
188213
flag.ID = c.Param("id")
189214
}
190-
flag.LastUpdatedDate = time.Now()
215+
flag.LastUpdatedDate = f.options.Clock.Now()
216+
flag.CreatedDate = retrievedFlag.CreatedDate
191217

192218
err = f.dao.UpdateFlag(c.Request().Context(), flag)
193219
if err != nil {
194-
return c.JSON(model.NewHTTPError(http.StatusInternalServerError, err))
220+
return f.handleDaoError(c, err)
195221
}
196222
return c.JSON(http.StatusOK, flag)
197223
}
@@ -210,19 +236,7 @@ func (f FlagAPIHandler) DeleteFlagByID(c echo.Context) error {
210236
idParam := c.Param("id")
211237
err := f.dao.DeleteFlagByID(c.Request().Context(), idParam)
212238
if err != nil {
213-
if errors.Is(err, sql.ErrNoRows) {
214-
return c.JSON(model.NewHTTPError(http.StatusNotFound, fmt.Errorf("flag with id %s not found", idParam)))
215-
}
216-
var pgErr *pq.Error
217-
if errors.As(err, &pgErr) {
218-
switch pgErr.Code {
219-
case "22P02":
220-
return c.JSON(model.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid UUID format")))
221-
default:
222-
return c.JSON(model.NewHTTPError(http.StatusInternalServerError, err))
223-
}
224-
}
225-
return c.JSON(model.NewHTTPError(http.StatusInternalServerError, err))
239+
return f.handleDaoError(c, err)
226240
}
227241
return c.JSON(http.StatusNoContent, nil)
228242
}
@@ -251,10 +265,10 @@ func (f FlagAPIHandler) UpdateFeatureFlagStatus(c echo.Context) error {
251265
}
252266

253267
flag.Disable = &statusUpdate.Disable
254-
flag.LastUpdatedDate = time.Now()
268+
flag.LastUpdatedDate = f.options.Clock.Now()
255269
err = f.dao.UpdateFlag(c.Request().Context(), flag)
256270
if err != nil {
257-
return c.JSON(model.NewHTTPError(http.StatusInternalServerError, err))
271+
return f.handleDaoError(c, err)
258272
}
259273
return c.JSON(http.StatusOK, flag)
260274
}
@@ -263,7 +277,7 @@ func (f FlagAPIHandler) UpdateFeatureFlagStatus(c echo.Context) error {
263277
func (f FlagAPIHandler) handleDaoError(c echo.Context, err daoErr.DaoError) error {
264278
switch err.Code() {
265279
case daoErr.NotFound:
266-
return c.JSON(model.NewHTTPError(http.StatusNotFound, fmt.Errorf("flag with id %s not found", c.Param("id"))))
280+
return c.JSON(model.NewHTTPError(http.StatusNotFound, fmt.Errorf("flag not found")))
267281
case daoErr.InvalidUUID:
268282
return c.JSON(model.NewHTTPError(http.StatusBadRequest, fmt.Errorf("invalid UUID format")))
269283
default:

0 commit comments

Comments
 (0)