Skip to content

Commit 1036f73

Browse files
authored
fix: return invalid argument for update feature validation errors (#2765)
1 parent 0516403 commit 1036f73

5 files changed

Lines changed: 93 additions & 4 deletions

File tree

pkg/feature/api/feature.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,10 @@ func (s *FeatureService) UpdateFeature(
605605
return err
606606
})
607607
if err != nil {
608-
return nil, err
608+
// Convert domain errors to gRPC statuses; otherwise validation errors
609+
// (e.g. variation value schema violations) surface as codes.Unknown
610+
// (HTTP 500) without the structured details the console relies on.
611+
return nil, s.convUpdateFeatureError(err)
609612
}
610613
if errs := s.publishDomainEvents(ctx, []*eventproto.Event{event}); len(errs) > 0 {
611614
s.logger.Error(

pkg/feature/api/feature_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
"github.com/stretchr/testify/assert"
2424
"github.com/stretchr/testify/require"
2525
"go.uber.org/mock/gomock"
26+
"google.golang.org/grpc/codes"
2627
"google.golang.org/grpc/metadata"
28+
"google.golang.org/grpc/status"
2729
"google.golang.org/protobuf/types/known/wrapperspb"
2830

2931
"github.com/bucketeer-io/bucketeer/v2/pkg/api/api"
@@ -619,6 +621,14 @@ func TestConvUpdateFeatureError(t *testing.T) {
619621
err := fs.convUpdateFeatureError(p.input)
620622
assert.Equal(t, p.expectedErr, err)
621623
}
624+
625+
// UpdateFeature relies on this conversion to surface domain validation
626+
// errors (e.g. variation value schema violations) as InvalidArgument
627+
// with structured details instead of Unknown without details.
628+
fs := &FeatureService{}
629+
schemaErr := fs.convUpdateFeatureError(pkgErr.NewErrorInvalidArgNotMatchFormat(
630+
pkgErr.FeaturePackageName, "feature: variation value does not match schema", "VariationValueSchema"))
631+
assert.Equal(t, codes.InvalidArgument, status.Code(schemaErr))
622632
}
623633

624634
func TestEvaluateFeatures(t *testing.T) {
@@ -3012,6 +3022,80 @@ func TestUpdateFeature(t *testing.T) {
30123022
},
30133023
expectedErr: statusInvalidArchive.Err(),
30143024
},
3025+
{
3026+
// Regression test: domain validation errors from the update
3027+
// transaction must surface as InvalidArgument with structured
3028+
// details, not as codes.Unknown (HTTP 500).
3029+
desc: "fail: variation value schema violation returns InvalidArgument",
3030+
setup: func(s *FeatureService) {
3031+
vID1 := newUUID(t)
3032+
vID2 := newUUID(t)
3033+
s.experimentClient.(*exprclientmock.MockClient).EXPECT().ListExperiments(gomock.Any(), gomock.Any()).Return(
3034+
&exprproto.ListExperimentsResponse{},
3035+
nil,
3036+
)
3037+
s.environmentClient.(*envclientmock.MockClient).EXPECT().GetEnvironmentV2(
3038+
gomock.Any(),
3039+
&envproto.GetEnvironmentV2Request{Id: "eid"},
3040+
).Return(
3041+
&envproto.GetEnvironmentV2Response{Environment: &envproto.EnvironmentV2{RequireComment: true}},
3042+
nil,
3043+
)
3044+
// Run the real transaction callback so the domain error
3045+
// propagates through UpdateFeature's error conversion.
3046+
s.dbClient.(*databasemock.MockClient).EXPECT().RunInTransactionV2(
3047+
gomock.Any(), gomock.Any(),
3048+
).DoAndReturn(func(ctx context.Context, fn func(ctx context.Context) error) error {
3049+
return fn(ctx)
3050+
})
3051+
s.featureStorage.(*mock.MockFeatureStorage).EXPECT().ListFeatures(
3052+
gomock.Any(), gomock.Any(),
3053+
).Return([]*featureproto.Feature{
3054+
{
3055+
Id: "fid",
3056+
VariationType: featureproto.Feature_STRING,
3057+
Variations: []*featureproto.Variation{
3058+
{
3059+
Id: vID1,
3060+
Value: "true",
3061+
Name: "true",
3062+
},
3063+
{
3064+
Id: vID2,
3065+
Value: "false",
3066+
Name: "false",
3067+
},
3068+
},
3069+
OffVariation: vID2,
3070+
DefaultStrategy: &featureproto.Strategy{
3071+
Type: featureproto.Strategy_FIXED,
3072+
FixedStrategy: &featureproto.FixedStrategy{
3073+
Variation: vID1,
3074+
},
3075+
},
3076+
Tags: []string{"test"},
3077+
},
3078+
}, 0, int64(0), nil)
3079+
},
3080+
ctx: createContextWithToken(),
3081+
input: &featureproto.UpdateFeatureRequest{
3082+
EnvironmentId: "eid",
3083+
Comment: "comment",
3084+
Id: "fid",
3085+
// The existing variation values (true/false) are not in the
3086+
// enum, so the schema update must be rejected.
3087+
VariationValueSchema: &featureproto.VariationValueSchema{
3088+
Type: featureproto.VariationValueSchema_ENUM,
3089+
Validator: &featureproto.VariationValueSchema_EnumValidator_{
3090+
EnumValidator: &featureproto.VariationValueSchema_EnumValidator{
3091+
Values: []string{"a", "b"},
3092+
},
3093+
},
3094+
},
3095+
},
3096+
expectedErr: api.NewGRPCStatus(pkgErr.NewErrorInvalidArgNotMatchFormat(
3097+
pkgErr.FeaturePackageName, "feature: variation value does not match schema", "VariationValueSchema")).Err(),
3098+
},
30153099
{
30163100
desc: "success",
30173101
setup: func(s *FeatureService) {

pkg/feature/domain/feature.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ var (
9797
errVariationTypeUnmatched = pkgErr.NewErrorInvalidArgNotMatchFormat(
9898
pkgErr.FeaturePackageName, "feature: variation value and type are unmatched", "variation")
9999
errVariationValueSchemaInvalid = pkgErr.NewErrorInvalidArgNotMatchFormat(
100-
pkgErr.FeaturePackageName, "feature: variation value schema is invalid", "variation_value_schema")
100+
pkgErr.FeaturePackageName, "feature: variation value schema is invalid", "VariationValueSchema")
101101
errVariationValueSchemaTypeUnmatched = pkgErr.NewErrorInvalidArgNotMatchFormat(
102-
pkgErr.FeaturePackageName, "feature: variation value schema and type are unmatched", "variation_value_schema")
102+
pkgErr.FeaturePackageName, "feature: variation value schema and type are unmatched", "VariationValueSchema")
103103
errVariationValueSchemaViolation = pkgErr.NewErrorInvalidArgNotMatchFormat(
104-
pkgErr.FeaturePackageName, "feature: variation value does not match schema", "variation_value_schema")
104+
pkgErr.FeaturePackageName, "feature: variation value does not match schema", "VariationValueSchema")
105105
errStrategyRequired = pkgErr.NewErrorInvalidArgEmpty(
106106
pkgErr.FeaturePackageName, "feature: strategy required", "strategy")
107107
errUnsupportedStrategy = pkgErr.NewErrorInvalidArgNotMatchFormat(

ui/dashboard/src/@locales/en/backend.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"TimeRange": "Time range",
111111
"TrialProject": "Trial project",
112112
"VariationId": "Variation ID",
113+
"VariationValueSchema": "Variation value schema",
113114
"VariationWeight": "Variation weight",
114115
"Webhook": "Webhook",
115116
"WebhookURL": "Webhook URL",

ui/dashboard/src/@locales/ja/backend.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
"UserId": "ユーザーID",
129129
"Variation": "バリエーション",
130130
"VariationId": "バリエーションID",
131+
"VariationValueSchema": "バリエーション値スキーマ",
131132
"VariationWeight": "バリエーションの重み",
132133
"Webhook": "Webhook",
133134
"WebhookURL": "Webhook URL",

0 commit comments

Comments
 (0)