Skip to content

Commit

Permalink
feat: improved validation and added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Ekep-Obasi committed Jan 3, 2025
1 parent cc5fdfe commit 297884e
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 7 deletions.
10 changes: 5 additions & 5 deletions db/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1131,16 +1131,16 @@ type FeatureFlag struct {
Description string `gorm:"type:text" json:"description"`
Enabled bool `gorm:"type:boolean;default:false" json:"enabled"`
Endpoints []Endpoint `gorm:"foreignKey:FeatureFlagUUID" json:"endpoints,omitempty"`
CreatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"updated_at"`
CreatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"-"`
UpdatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"-"`
}

type Endpoint struct {
UUID uuid.UUID `gorm:"type:uuid;primaryKey" json:"uuid"`
Path string `gorm:"type:varchar(255);not null" json:"path"`
FeatureFlagUUID uuid.UUID `gorm:"type:uuid;not null" json:"feature_flag_uuid"`
CreatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"created_at"`
UpdatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"updated_at"`
FeatureFlagUUID uuid.UUID `gorm:"type:uuid;not null" json:"-"`
CreatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"-"`
UpdatedAt time.Time `gorm:"type:timestamp;default:current_timestamp" json:"-"`
}

func (Person) TableName() string {
Expand Down
20 changes: 18 additions & 2 deletions handlers/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package handlers

import (
"encoding/json"
"fmt"
"net/http"

"github.com/go-chi/chi"
Expand Down Expand Up @@ -155,6 +154,15 @@ func (fh *FeatureFlagHandler) UpdateFeatureFlag(w http.ResponseWriter, r *http.R

updatedFlag, err := fh.db.UpdateFeatureFlag(flag)
if err != nil {
if err.Error() == "feature flag not found" {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(FeatureFlagResponse{
Success: false,
Message: "Feature flag not found",
})
return
}

w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(FeatureFlagResponse{
Success: false,
Expand Down Expand Up @@ -193,7 +201,15 @@ func (fh *FeatureFlagHandler) DeleteFeatureFlag(w http.ResponseWriter, r *http.R
}

if err := fh.db.DeleteFeatureFlag(flagUUID); err != nil {
fmt.Print(err)
if err.Error() == "feature flag not found" {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(FeatureFlagResponse{
Success: false,
Message: "Feature flag not found",
})
return
}

w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(FeatureFlagResponse{
Success: false,
Expand Down
167 changes: 167 additions & 0 deletions middlewares/feature_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package middleware

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/google/uuid"
"github.com/stakwork/sphinx-tribes/db"
dbmocks "github.com/stakwork/sphinx-tribes/mocks"
"github.com/stretchr/testify/assert"
)

func TestFeatureFlag(t *testing.T) {
tests := []struct {
name string
path string
setupMock func(*dbmocks.Database)
expectedStatus int
expectedBody map[string]interface{}
}{
{
name: "Feature enabled - should allow request",
path: "/api/test",
setupMock: func(mockDb *dbmocks.Database) {
flagUUID := uuid.New()
mockDb.On("GetAllEndpoints").Return([]db.Endpoint{
{
UUID: uuid.New(),
Path: "/api/test",
FeatureFlagUUID: flagUUID,
},
}, nil)
mockDb.On("GetFeatureFlagByUUID", flagUUID).Return(db.FeatureFlag{
UUID: flagUUID,
Enabled: true,
}, nil)
},
expectedStatus: http.StatusOK,
},
{
name: "Feature disabled - should block request",
path: "/api/test",
setupMock: func(mockDb *dbmocks.Database) {
flagUUID := uuid.New()
mockDb.On("GetAllEndpoints").Return([]db.Endpoint{
{
UUID: uuid.New(),
Path: "/api/test",
FeatureFlagUUID: flagUUID,
},
}, nil)
mockDb.On("GetFeatureFlagByUUID", flagUUID).Return(db.FeatureFlag{
UUID: flagUUID,
Enabled: false,
}, nil)
},
expectedStatus: http.StatusForbidden,
expectedBody: map[string]interface{}{
"success": false,
"message": "This feature is currently unavailable.",
},
},
{
name: "Path not feature flagged - should allow request",
path: "/api/unrestricted",
setupMock: func(mockDb *dbmocks.Database) {
mockDb.On("GetAllEndpoints").Return([]db.Endpoint{
{
UUID: uuid.New(),
Path: "/api/test",
FeatureFlagUUID: uuid.New(),
},
}, nil)
},
expectedStatus: http.StatusOK,
},
{
name: "Path with parameters - should match correctly",
path: "/api/users/123",
setupMock: func(mockDb *dbmocks.Database) {
flagUUID := uuid.New()
mockDb.On("GetAllEndpoints").Return([]db.Endpoint{
{
UUID: uuid.New(),
Path: "/api/users/:id",
FeatureFlagUUID: flagUUID,
},
}, nil)
mockDb.On("GetFeatureFlagByUUID", flagUUID).Return(db.FeatureFlag{
UUID: flagUUID,
Enabled: true,
}, nil)
},
expectedStatus: http.StatusOK,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockDb := dbmocks.NewDatabase(t)

tt.setupMock(mockDb)

nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

req := httptest.NewRequest("GET", tt.path, nil)
w := httptest.NewRecorder()

middleware := FeatureFlag(mockDb)
middleware(nextHandler).ServeHTTP(w, req)

assert.Equal(t, tt.expectedStatus, w.Code)

if tt.expectedBody != nil {
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, tt.expectedBody, response)
}
})
}
}

func TestMatchPath(t *testing.T) {
tests := []struct {
name string
pattern string
requestPath string
expected bool
}{
{
name: "Exact match",
pattern: "/api/test",
requestPath: "/api/test",
expected: true,
},
{
name: "Parameter match",
pattern: "/api/users/:id",
requestPath: "/api/users/123",
expected: true,
},
{
name: "No match - different path",
pattern: "/api/test",
requestPath: "/api/other",
expected: false,
},
{
name: "Multiple parameters",
pattern: "/api/:resource/:id",
requestPath: "/api/users/123",
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := matchPath(tt.pattern, tt.requestPath)
assert.Equal(t, tt.expected, result)
})
}
}

0 comments on commit 297884e

Please sign in to comment.