diff --git a/cmd/relayproxy/controller/all_flags_test.go b/cmd/relayproxy/controller/all_flags_test.go index 3c61236e91d..de0cfce648d 100644 --- a/cmd/relayproxy/controller/all_flags_test.go +++ b/cmd/relayproxy/controller/all_flags_test.go @@ -65,15 +65,17 @@ func Test_all_flag_Handler_DefaultMode(t *testing.T) { }, }, { - name: "No user key in payload", + name: "No user key in payload - should try to evaluate flags individually", + // the API should return targeting key missing for all flags that require bucketing + // and it should also perform all the static evaluations for non-bucketing flags args: args{ bodyFile: "../testdata/controller/all_flags/no_user_key_request.json", configFlagsLocation: configFlagsLocation, }, want: want{ - handlerErr: true, - errorMsg: "empty key for evaluation context, impossible to retrieve flags", - errorCode: http.StatusBadRequest, + handlerErr: false, + httpCode: http.StatusOK, + bodyFile: "../testdata/controller/all_flags/no_user_key_response_updated.json", }, }, { @@ -194,15 +196,17 @@ func Test_all_flag_Handler_FlagsetMode(t *testing.T) { }, }, { - name: "No user key in payload", + name: "No user key in payload - should now pass and evaluate flags individually", + // the API should return targeting key missing for all flags that require bucketing + // and it should also perform all the static evaluations for non-bucketing flags args: args{ bodyFile: "../testdata/controller/all_flags/no_user_key_request.json", configFlagsLocation: configFlagsLocation, }, want: want{ - handlerErr: true, - errorMsg: "empty key for evaluation context, impossible to retrieve flags", - errorCode: http.StatusBadRequest, + handlerErr: false, // No longer fails at API validation level + httpCode: http.StatusOK, + bodyFile: "../testdata/controller/all_flags/no_user_key_response_updated.json", }, }, { diff --git a/cmd/relayproxy/controller/flag_eval_test.go b/cmd/relayproxy/controller/flag_eval_test.go index 90effe342c4..0b8d11647fa 100644 --- a/cmd/relayproxy/controller/flag_eval_test.go +++ b/cmd/relayproxy/controller/flag_eval_test.go @@ -2,7 +2,6 @@ package controller_test import ( "io" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -126,9 +125,9 @@ func Test_flag_eval_Handler(t *testing.T) { bodyFile: "../testdata/controller/flag_eval/no_user_key_request.json", }, want: want{ - handlerErr: true, - errorMsg: "empty key for evaluation context, impossible to retrieve flags", - errorCode: http.StatusBadRequest, + handlerErr: false, + bodyFile: "../testdata/controller/flag_eval/no_user_key_response.json", + httpCode: http.StatusOK, }, }, { @@ -169,7 +168,7 @@ func Test_flag_eval_Handler(t *testing.T) { // read wantBody request file var bodyReq io.Reader if tt.args.bodyFile != "" { - bodyReqContent, err := ioutil.ReadFile(tt.args.bodyFile) + bodyReqContent, err := os.ReadFile(tt.args.bodyFile) assert.NoError(t, err, "request wantBody file missing %s", tt.args.bodyFile) bodyReq = strings.NewReader(string(bodyReqContent)) } diff --git a/cmd/relayproxy/controller/utils.go b/cmd/relayproxy/controller/utils.go index 9f80e02c194..d7499a5b857 100644 --- a/cmd/relayproxy/controller/utils.go +++ b/cmd/relayproxy/controller/utils.go @@ -19,21 +19,6 @@ func assertRequest(u *model.AllFlagRequest) *echo.HTTPError { "assertRequest: impossible to find user in request", ) } - - if u.EvaluationContext != nil { - return assertContextKey(u.EvaluationContext.Key) - } - return assertContextKey(u.User.Key) // nolint: staticcheck -} - -// assertContextKey is checking that the user key is valid, if not an echo.HTTPError is return. -func assertContextKey(key string) *echo.HTTPError { - if len(key) == 0 { - return &echo.HTTPError{ - Code: http.StatusBadRequest, - Message: "empty key for evaluation context, impossible to retrieve flags", - } - } return nil } diff --git a/cmd/relayproxy/controller/utils_test.go b/cmd/relayproxy/controller/utils_test.go index 9372087fce4..cab223a0dfd 100644 --- a/cmd/relayproxy/controller/utils_test.go +++ b/cmd/relayproxy/controller/utils_test.go @@ -41,13 +41,14 @@ func Test_assertRequest(t *testing.T) { }, { name: "user with User and EvaluationContext, empty key for evaluation context", + // in this case, since we have a targetingKey set for the evaluation context we take the one from the + // evaluation context key not from the user key. + // In that case the targetingKey is empty, but this is allowed by the core evaluation logic. req: &model.AllFlagRequest{ User: &model.UserRequest{Key: "my-key"}, EvaluationContext: &model.EvaluationContextRequest{Key: ""}, }, - wantErr: echo.NewHTTPError( - http.StatusBadRequest, - "empty key for evaluation context, impossible to retrieve flags"), + wantErr: nil, }, { name: "invalid user but valid evaluation context should pass", diff --git a/cmd/relayproxy/ofrep/evaluate.go b/cmd/relayproxy/ofrep/evaluate.go index 2eb505f67e2..0129dbb7c2a 100644 --- a/cmd/relayproxy/ofrep/evaluate.go +++ b/cmd/relayproxy/ofrep/evaluate.go @@ -70,17 +70,11 @@ func (h *EvaluateCtrl) Evaluate(c echo.Context) error { Key: flagKey, }) } - evalCtx, err := evaluationContextFromOFREPRequest(reqBody.Context) + evalCtx, err := evaluationContextFromOFREPRequest(reqBody) if err != nil { return c.JSON( http.StatusBadRequest, - model.OFREPEvaluateResponseError{ - OFREPCommonResponseError: model.OFREPCommonResponseError{ - ErrorCode: flag.ErrorCodeInvalidContext, - ErrorDetails: err.Error(), - }, - Key: flagKey, - }) + err) } tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName) @@ -167,7 +161,7 @@ func (h *EvaluateCtrl) BulkEvaluate(c echo.Context) error { if err := assertOFREPEvaluateRequest(request); err != nil { return c.JSON(http.StatusBadRequest, err) } - evalCtx, err := evaluationContextFromOFREPRequest(request.Context) + evalCtx, err := evaluationContextFromOFREPRequest(request) if err != nil { return c.JSON( http.StatusBadRequest, @@ -227,19 +221,36 @@ func (h *EvaluateCtrl) BulkEvaluate(c echo.Context) error { func assertOFREPEvaluateRequest( ofrepEvalReq *model.OFREPEvalFlagRequest, ) *model.OFREPCommonResponseError { - if ofrepEvalReq.Context == nil || ofrepEvalReq.Context["targetingKey"] == "" { - return NewOFREPCommonError(flag.ErrorCodeTargetingKeyMissing, - "GO Feature Flag MUST have a targeting key in the request.") + if ofrepEvalReq.Context == nil { + return NewOFREPCommonError(flag.ErrorCodeInvalidContext, + "GO Feature Flag requires an evaluation context in the request.") } + + // An empty context object is allowed since the evaluation context is optional. + // If the context does not have any targetingKey, this is fine since the core + // evaluation logic will handle if it is required or not. + return nil } -func evaluationContextFromOFREPRequest(ctx map[string]any) (ffcontext.Context, error) { - if targetingKey, ok := ctx["targetingKey"].(string); ok { - evalCtx := utils.ConvertEvaluationCtxFromRequest(targetingKey, ctx) - return evalCtx, nil +func evaluationContextFromOFREPRequest(req *model.OFREPEvalFlagRequest) (ffcontext.Context, error) { + if req == nil || req.Context == nil { + return ffcontext.EvaluationContext{}, NewOFREPCommonError( + flag.ErrorCodeInvalidContext, + "GO Feature Flag has received an invalid context.") } - return ffcontext.EvaluationContext{}, NewOFREPCommonError( - flag.ErrorCodeTargetingKeyMissing, - "GO Feature Flag has received no targetingKey or a none string value that is not a string.") + + ctx := req.Context + + // targetingKey is optional, it is only required if the flag needs bucketing and + // the check is done in the core evaluation logic. + // If we don't have a targetingKey, we return an empty string. + targetingKey := "" + if key, ok := ctx["targetingKey"].(string); ok { + targetingKey = key + } + + // Create evaluation context (empty targeting key is allowed) + evalCtx := utils.ConvertEvaluationCtxFromRequest(targetingKey, ctx) + return evalCtx, nil } diff --git a/cmd/relayproxy/ofrep/evaluate_test.go b/cmd/relayproxy/ofrep/evaluate_test.go index 7581e2f5bea..8bbd19fcf77 100644 --- a/cmd/relayproxy/ofrep/evaluate_test.go +++ b/cmd/relayproxy/ofrep/evaluate_test.go @@ -72,6 +72,17 @@ func Test_Bulk_Evaluation(t *testing.T) { bodyFile: "../testdata/ofrep/responses/invalid_context.json", }, }, + { + name: "Empty body", + args: args{ + bodyFile: "../testdata/ofrep/empty_body.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/empty_body.json", + }, + }, { name: "Nil context", args: args{ @@ -85,12 +96,14 @@ func Test_Bulk_Evaluation(t *testing.T) { }, { name: "No Targeting Key in context", + // in this case we don't have a targetingKey, so we will evaluate the flags individually + // if the flag requires bucketing, we will return a targeting key missing error args: args{ bodyFile: "../testdata/ofrep/no_targeting_key_context.json", configFlagsLocation: configFlagsLocation, }, want: want{ - httpCode: http.StatusBadRequest, + httpCode: http.StatusOK, bodyFile: "../testdata/ofrep/responses/no_targeting_key_context.json", }, }, @@ -209,15 +222,39 @@ func Test_Evaluate(t *testing.T) { }, }, { - name: "No Targeting Key in context", + name: "No Targeting Key for bucketing-required flag - should return 400 from core evaluation", args: args{ bodyFile: "../testdata/ofrep/no_targeting_key_context.json", configFlagsLocation: configFlagsLocation, - flagKey: "number-flag", + flagKey: "number-flag", // This flag has percentage rules, requires bucketing }, want: want{ httpCode: http.StatusBadRequest, - bodyFile: "../testdata/ofrep/responses/no_targeting_key_context_with_key.json", + bodyFile: "../testdata/ofrep/responses/no_targeting_key_bucketing_flag.json", + }, + }, + { + name: "No Targeting Key for non-bucketing flag - should succeed", + args: args{ + bodyFile: "../testdata/ofrep/no_targeting_key_context.json", + configFlagsLocation: configFlagsLocation, + flagKey: "targeting-key-rule", // This flag has no percentages, doesn't require bucketing + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/ofrep/responses/no_targeting_key_static_flag.json", + }, + }, + { + name: "Percentage-based rule in flag without targeting key should return 400 error", + args: args{ + bodyFile: "../testdata/ofrep/no_targeting_key_context.json", + configFlagsLocation: configFlagsLocation, + flagKey: "flag-only-for-admin", // This flag has percentage rules, requires bucketing + }, + want: want{ + httpCode: http.StatusBadRequest, // Core evaluation returns 400 for missing targeting key + bodyFile: "../testdata/ofrep/responses/percentage_flag_no_key_error.json", }, }, { diff --git a/cmd/relayproxy/testdata/controller/all_flags/no_user_key_response_updated.json b/cmd/relayproxy/testdata/controller/all_flags/no_user_key_response_updated.json new file mode 100644 index 00000000000..aa7bf39a029 --- /dev/null +++ b/cmd/relayproxy/testdata/controller/all_flags/no_user_key_response_updated.json @@ -0,0 +1,85 @@ +{ + "flags": { + "array-flag": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "disable-flag": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "flag-only-for-admin": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "new-admin-access": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "number-flag": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "targeting-key-rule": { + "value": false, + "timestamp": 1652273630, + "variationType": "false_var", + "trackEvents": true, + "errorCode": "", + "reason": "DEFAULT" + }, + "test-flag-rule-apply": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "test-flag-rule-apply-false": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + }, + "test-flag-rule-not-apply": { + "value": null, + "timestamp": 1652273630, + "variationType": "", + "trackEvents": true, + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key", + "reason": "ERROR" + } + }, + "valid": false +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/disable_flag_request.json b/cmd/relayproxy/testdata/controller/flag_eval/disable_flag_request.json index 04fcd87794d..2d01bf4ac96 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/disable_flag_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/disable_flag_request.json @@ -1,11 +1,10 @@ { - "user": { + "evaluationContext": { "key": "random-key", - "anonymous": false, "custom": { "custom1": "value1", "custom2": "value2" } }, "defaultValue": "mydefaultFlagValue" -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/flag_not_exist_request.json b/cmd/relayproxy/testdata/controller/flag_eval/flag_not_exist_request.json index beba1c0e472..c83ca436f26 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/flag_not_exist_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/flag_not_exist_request.json @@ -1,11 +1,10 @@ { - "user": { + "evaluationContext": { "key": "random-key", - "anonymous": false, "custom": { "custom1": "value1", "custom2": "value2" } }, "defaultValue": "not exist flag value" -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/invalid_json_request.json b/cmd/relayproxy/testdata/controller/flag_eval/invalid_json_request.json index 4a229856510..e85ef68bef9 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/invalid_json_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/invalid_json_request.json @@ -1,11 +1,9 @@ { - "user": { + "evaluationContext": { "key": "random-key", - "anonymous": false, "custom": { "custom1": "value1", "custom2": "value2" } }, - "defaultValue": "mydefaultFlagValue" - + "defaultValue": "mydefaultFlagValue" \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_request.json b/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_request.json index 633390cecd0..00abc1ed8a7 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_request.json @@ -1,10 +1,9 @@ { - "user": { - "anonymous": false, + "evaluationContext": { "custom": { "custom1": "value1", "custom2": "value2" } }, "defaultValue": "mydefaultFlagValue" -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_response.json b/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_response.json new file mode 100644 index 00000000000..163ccacb6fd --- /dev/null +++ b/cmd/relayproxy/testdata/controller/flag_eval/no_user_key_response.json @@ -0,0 +1,11 @@ +{ + "variationType": "SdkDefault", + "failed": true, + "version": "", + "reason": "ERROR", + "errorCode": "TARGETING_KEY_MISSING", + "value": "mydefaultFlagValue", + "errorDetails": "Error: Empty targeting key", + "cacheable": false, + "trackEvents": true +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_request.json b/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_request.json index 8d36874f596..15c7365bea3 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_request.json @@ -1,8 +1,8 @@ { - "user": { + "evaluationContext": { "key": "random-key-ssss1", - "anonymous": true, "custom": { + "anonymous": true, "custom1": "value1", "custom2": "value2" } @@ -10,4 +10,4 @@ "defaultValue": { "test123": "test" } -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_response.json b/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_response.json index fdc0a20d152..5e05c1a3514 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_response.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_false_response.json @@ -12,4 +12,4 @@ "metadata": { "evaluatedRuleName": "rule1" } -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_request.json b/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_request.json index a7b37465a03..2394282ab8c 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/rule_apply_request.json @@ -1,7 +1,6 @@ { - "user": { + "evaluationContext": { "key": "random-key", - "anonymous": false, "custom": { "custom1": "value1", "custom2": "value2" @@ -10,4 +9,4 @@ "defaultValue": { "test4": "test" } -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/rule_not_apply_request.json b/cmd/relayproxy/testdata/controller/flag_eval/rule_not_apply_request.json index a7b37465a03..2394282ab8c 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/rule_not_apply_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/rule_not_apply_request.json @@ -1,7 +1,6 @@ { - "user": { + "evaluationContext": { "key": "random-key", - "anonymous": false, "custom": { "custom1": "value1", "custom2": "value2" @@ -10,4 +9,4 @@ "defaultValue": { "test4": "test" } -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/controller/flag_eval/valid_request.json b/cmd/relayproxy/testdata/controller/flag_eval/valid_request.json index 884d22e87d6..6f2bee801de 100644 --- a/cmd/relayproxy/testdata/controller/flag_eval/valid_request.json +++ b/cmd/relayproxy/testdata/controller/flag_eval/valid_request.json @@ -1,7 +1,6 @@ { - "user": { + "evaluationContext": { "key": "a20b1cd5-7165-4e02-a279-c0c8b90a8912", - "anonymous": false, "custom": { "custom1": "value1", "custom2": "value2" diff --git a/cmd/relayproxy/testdata/ofrep/empty_body.json b/cmd/relayproxy/testdata/ofrep/empty_body.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cmd/relayproxy/testdata/ofrep/nil_context.json b/cmd/relayproxy/testdata/ofrep/nil_context.json index e69de29bb2d..1d83a5de9af 100644 --- a/cmd/relayproxy/testdata/ofrep/nil_context.json +++ b/cmd/relayproxy/testdata/ofrep/nil_context.json @@ -0,0 +1,3 @@ +{ + "context": null +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/empty_body.json b/cmd/relayproxy/testdata/ofrep/responses/empty_body.json new file mode 100644 index 00000000000..4e6e35f6b1d --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/empty_body.json @@ -0,0 +1,4 @@ +{ + "errorCode": "INVALID_CONTEXT", + "errorDetails": "GO Feature Flag requires an evaluation context in the request." +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/nil_context.json b/cmd/relayproxy/testdata/ofrep/responses/nil_context.json index efe38676815..4e6e35f6b1d 100644 --- a/cmd/relayproxy/testdata/ofrep/responses/nil_context.json +++ b/cmd/relayproxy/testdata/ofrep/responses/nil_context.json @@ -1,4 +1,4 @@ { - "errorCode": "TARGETING_KEY_MISSING", - "errorDetails": "GO Feature Flag MUST have a targeting key in the request." + "errorCode": "INVALID_CONTEXT", + "errorDetails": "GO Feature Flag requires an evaluation context in the request." } \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/nil_context_with_key.json b/cmd/relayproxy/testdata/ofrep/responses/nil_context_with_key.json index ed146f22c22..9b1ff178bf7 100644 --- a/cmd/relayproxy/testdata/ofrep/responses/nil_context_with_key.json +++ b/cmd/relayproxy/testdata/ofrep/responses/nil_context_with_key.json @@ -1,5 +1,5 @@ { - "errorCode": "TARGETING_KEY_MISSING", - "errorDetails": "GO Feature Flag MUST have a targeting key in the request.", + "errorCode": "INVALID_CONTEXT", + "errorDetails": "GO Feature Flag requires an evaluation context in the request.", "key": "number-flag" -} +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_bucketing_flag.json b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_bucketing_flag.json new file mode 100644 index 00000000000..a92a609ec4c --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_bucketing_flag.json @@ -0,0 +1,5 @@ +{ + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error while evaluating the flag: number-flag", + "key": "number-flag" +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json index 9cfc8111722..42269a35790 100644 --- a/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json +++ b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json @@ -1,4 +1,74 @@ { - "errorCode": "TARGETING_KEY_MISSING", - "errorDetails": "GO Feature Flag has received no targetingKey or a none string value that is not a string." + "flags": [ + { + "key": "array-flag", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "disable-flag", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "flag-only-for-admin", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "new-admin-access", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "number-flag", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "targeting-key-rule", + "value": false, + "reason": "DEFAULT", + "variant": "false_var" + }, + { + "key": "test-flag-rule-apply", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "test-flag-rule-apply-false", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + }, + { + "key": "test-flag-rule-not-apply", + "value": null, + "reason": "ERROR", + "variant": "", + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error: Empty targeting key" + } + ] } \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_static_flag.json b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_static_flag.json new file mode 100644 index 00000000000..041f87ea237 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_static_flag.json @@ -0,0 +1,9 @@ +{ + "key": "targeting-key-rule", + "value": false, + "reason": "DEFAULT", + "variant": "false_var", + "metadata": { + "gofeatureflag_cacheable": true + } +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/percentage_flag_no_key_error.json b/cmd/relayproxy/testdata/ofrep/responses/percentage_flag_no_key_error.json new file mode 100644 index 00000000000..9c162169673 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/percentage_flag_no_key_error.json @@ -0,0 +1,5 @@ +{ + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "Error while evaluating the flag: flag-only-for-admin", + "key": "flag-only-for-admin" +} \ No newline at end of file diff --git a/ffcontext/context_builder_test.go b/ffcontext/context_builder_test.go index 3b69dbf4440..7403dc8b92d 100644 --- a/ffcontext/context_builder_test.go +++ b/ffcontext/context_builder_test.go @@ -92,3 +92,21 @@ func TestNewUser(t *testing.T) { }) } } + +func TestNewEvaluationContextWithoutTargetingKey(t *testing.T) { + ctx := NewEvaluationContext("") + assert.Equal(t, "", ctx.GetKey(), "Should have empty targeting key") + assert.Equal(t, map[string]interface{}{}, ctx.GetCustom(), "Should have empty custom attributes") + assert.False(t, ctx.IsAnonymous(), "Should not be anonymous by default") +} + +func TestNewEvaluationContextBuilderWithoutTargetingKey(t *testing.T) { + ctx := NewEvaluationContextBuilder(""). + AddCustom("role", "admin"). + AddCustom("anonymous", true). + Build() + + assert.Equal(t, "", ctx.GetKey(), "Should have empty targeting key") + assert.Equal(t, "admin", ctx.GetCustom()["role"], "Should have custom attributes") + assert.True(t, ctx.IsAnonymous(), "Should be anonymous when set") +} diff --git a/internal/flag/internal_flag.go b/internal/flag/internal_flag.go index cc94fb2ffb4..2ed57abc3df 100644 --- a/internal/flag/internal_flag.go +++ b/internal/flag/internal_flag.go @@ -66,13 +66,20 @@ func (f *InternalFlag) Value( evaluationCtx ffcontext.Context, flagContext Context, ) (interface{}, ResolutionDetails) { + // if the evaluation context is nil, we create a new one with an empty key + // this is to avoid any nil pointer exception. + if evaluationCtx == nil { + evaluationCtx = ffcontext.NewEvaluationContext("") + } + evaluationDate := DateFromContextOrDefault(evaluationCtx, time.Now()) flag, err := f.applyScheduledRolloutSteps(evaluationDate) if err != nil { return flagContext.DefaultSdkValue, ResolutionDetails{ - Variant: VariationSDKDefault, - Reason: ReasonError, - ErrorCode: ErrorCodeGeneral, + Variant: VariationSDKDefault, + Reason: ReasonError, + ErrorCode: ErrorCodeGeneral, + ErrorMessage: err.Error(), } } @@ -104,10 +111,11 @@ func (f *InternalFlag) Value( if err != nil { return flagContext.DefaultSdkValue, ResolutionDetails{ - Variant: VariationSDKDefault, - Reason: ReasonError, - ErrorCode: ErrorFlagConfiguration, - Metadata: flag.GetMetadata(), + Variant: VariationSDKDefault, + Reason: ReasonError, + ErrorCode: ErrorFlagConfiguration, + ErrorMessage: err.Error(), + Metadata: flag.GetMetadata(), } } @@ -247,7 +255,7 @@ func (f *InternalFlag) applyScheduledRolloutSteps(evaluationDate time.Time) (*In flagCopy.Experimentation = &ExperimentationRollout{} } if steps.Experimentation.Start != nil { - flagCopy.Experimentation.End = steps.Experimentation.End + flagCopy.Experimentation.Start = steps.Experimentation.Start } if steps.Experimentation.End != nil { flagCopy.Experimentation.End = steps.Experimentation.End @@ -377,10 +385,8 @@ func (f *InternalFlag) GetVersion() string { // GetVariationValue return the value of variation from his name func (f *InternalFlag) GetVariationValue(name string) interface{} { - for k, v := range f.GetVariations() { - if k == name && v != nil { - return *v - } + if v, exists := f.GetVariations()[name]; exists && v != nil { + return *v } return nil } @@ -393,8 +399,49 @@ func (f *InternalFlag) GetBucketingKey() string { return *f.BucketingKey } +// RequiresBucketing checks if the flag requires a bucketing key for evaluation +// A flag requires bucketing if it has percentage-based rules or progressive rollouts, +// including those introduced by scheduled rollout steps +func (f *InternalFlag) RequiresBucketing() bool { + // Check if default rule requires bucketing + if f.DefaultRule != nil && f.DefaultRule.RequiresBucketing() { + return true + } + + // Check if any targeting rule requires bucketing + for _, rule := range f.GetRules() { + if rule.RequiresBucketing() { + return true + } + } + + // Check if any scheduled rollout steps introduce bucketing requirements + if f.Scheduled != nil { + for _, step := range *f.Scheduled { + // Check if the scheduled step's default rule requires bucketing + if step.DefaultRule != nil && step.DefaultRule.RequiresBucketing() { + return true + } + + // Check if any of the scheduled step's rules require bucketing + for _, rule := range step.GetRules() { + if rule.RequiresBucketing() { + return true + } + } + } + } + + return false +} + // GetBucketingKeyValue return the value of the bucketing key from the context +// If requiresBucketing is false, it allows empty keys for flags that don't need them func (f *InternalFlag) GetBucketingKeyValue(ctx ffcontext.Context) (string, error) { + // Cache the bucketing requirement check to avoid multiple calls + requiresBucketing := f.RequiresBucketing() + + // Check if custom bucketing key is provided if f.BucketingKey != nil { key := f.GetBucketingKey() if key == "" { @@ -404,7 +451,11 @@ func (f *InternalFlag) GetBucketingKeyValue(ctx ffcontext.Context) (string, erro switch v := value.(type) { case string: if v == "" { - return "", &gofferror.EmptyBucketingKeyError{Message: "Empty bucketing key"} + if requiresBucketing { + return "", &gofferror.EmptyBucketingKeyError{Message: "Empty bucketing key"} + } + // Return empty key if bucketing not required + return "", nil } return v, nil default: @@ -412,8 +463,13 @@ func (f *InternalFlag) GetBucketingKeyValue(ctx ffcontext.Context) (string, erro } } + // Check if targeting key is required for this flag if ctx.GetKey() == "" { - return "", &gofferror.EmptyBucketingKeyError{Message: "Empty targeting key"} + if requiresBucketing { + return "", &gofferror.EmptyBucketingKeyError{Message: "Empty targeting key"} + } + // Return empty key if bucketing not required + return "", nil } return ctx.GetKey(), nil diff --git a/internal/flag/internal_flag_empty_context_test.go b/internal/flag/internal_flag_empty_context_test.go new file mode 100644 index 00000000000..4f350d54ccb --- /dev/null +++ b/internal/flag/internal_flag_empty_context_test.go @@ -0,0 +1,549 @@ +package flag_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" +) + +func TestInternalFlag_RequiresBucketing(t *testing.T) { + tests := []struct { + name string + flag flag.InternalFlag + expected bool + }{ + { + name: "Should not require bucketing - static variation only", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + expected: false, + }, + { + name: "Should require bucketing - default rule has percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 20, + "disabled": 80, + }, + }, + }, + expected: true, + }, + { + name: "Should require bucketing - targeting rule has percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("key eq \"admin\""), + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + expected: true, + }, + { + name: "Should require bucketing - has progressive rollout", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + ProgressiveRollout: &flag.ProgressiveRollout{ + Initial: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("disabled"), + Percentage: testconvert.Float64(0), + }, + End: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("enabled"), + Percentage: testconvert.Float64(100), + }, + }, + }, + }, + expected: true, + }, + { + name: "Should not require bucketing - only targeting queries without percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("key eq \"admin\""), + VariationResult: testconvert.String("enabled"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + expected: false, + }, + { + name: "Should require bucketing - scheduled rollout with percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("key eq \"admin\""), + VariationResult: testconvert.String("enabled"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + Scheduled: &[]flag.ScheduledStep{ + { + InternalFlag: flag.InternalFlag{ + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + }, + Date: testconvert.Time(time.Now()), + }, + }, + }, + expected: true, + }, + { + name: "Should require bucketing - scheduled rollout with progressive rollout", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + Scheduled: &[]flag.ScheduledStep{ + { + InternalFlag: flag.InternalFlag{ + DefaultRule: &flag.Rule{ + ProgressiveRollout: &flag.ProgressiveRollout{ + Initial: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("disabled"), + Percentage: testconvert.Float64(0), + Date: testconvert.Time(time.Now()), + }, + End: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("enabled"), + Percentage: testconvert.Float64(100), + Date: testconvert.Time(time.Now().Add(time.Hour)), + }, + }, + }, + }, + Date: testconvert.Time(time.Now()), + }, + }, + }, + expected: true, + }, + { + name: "Should require bucketing - scheduled rollout with targeting rule percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + Scheduled: &[]flag.ScheduledStep{ + { + InternalFlag: flag.InternalFlag{ + Rules: &[]flag.Rule{ + { + Query: testconvert.String("beta eq true"), + Percentages: &map[string]float64{ + "enabled": 25, + "disabled": 75, + }, + }, + }, + }, + Date: testconvert.Time(time.Now()), + }, + }, + }, + expected: true, + }, + { + name: "Should not require bucketing - scheduled rollout with only static variations", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + Scheduled: &[]flag.ScheduledStep{ + { + InternalFlag: flag.InternalFlag{ + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("enabled"), + }, + }, + Date: testconvert.Time(time.Now()), + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.flag.RequiresBucketing() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestInternalFlag_Value_EmptyContext(t *testing.T) { + tests := []struct { + name string + flag flag.InternalFlag + evaluationCtx ffcontext.Context + expectedValue interface{} + expectedErrorCode string + expectedReason string + shouldSucceed bool + description string + }{ + { + name: "Should succeed with empty context for static flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + evaluationCtx: ffcontext.NewEvaluationContext(""), + expectedValue: false, + expectedReason: flag.ReasonStatic, + shouldSucceed: true, + description: "Flag without bucketing requirements should work with empty context", + }, + { + name: "Should fail with empty context for percentage-based flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 20, + "disabled": 80, + }, + }, + }, + evaluationCtx: ffcontext.NewEvaluationContext(""), + expectedErrorCode: flag.ErrorCodeTargetingKeyMissing, + expectedReason: flag.ReasonError, + shouldSucceed: false, + description: "Flag with percentage-based rollout should fail with empty context", + }, + { + name: "Should succeed with targeting key for percentage-based flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 20, + "disabled": 80, + }, + }, + }, + evaluationCtx: ffcontext.NewEvaluationContext("user-123"), + expectedReason: flag.ReasonSplit, + shouldSucceed: true, + description: "Flag with percentage-based rollout should work with targeting key", + }, + { + name: "Should succeed with empty context for targeting rule without percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("anonymous eq true"), + VariationResult: testconvert.String("enabled"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + evaluationCtx: func() ffcontext.Context { + ctx := ffcontext.NewEvaluationContext("") + ctx.AddCustomAttribute("anonymous", true) + return ctx + }(), + expectedValue: true, + expectedReason: flag.ReasonTargetingMatch, + shouldSucceed: true, + description: "Flag with targeting rule but no bucketing should work with empty context", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flagContext := flag.Context{DefaultSdkValue: false} + value, resolutionDetails := tt.flag.Value("test-flag", tt.evaluationCtx, flagContext) + + if tt.shouldSucceed { + assert.Equal(t, "", resolutionDetails.ErrorCode, tt.description) + assert.Equal(t, tt.expectedReason, resolutionDetails.Reason, tt.description) + if tt.expectedValue != nil { + assert.Equal(t, tt.expectedValue, value, tt.description) + } + } else { + assert.Equal(t, tt.expectedErrorCode, resolutionDetails.ErrorCode, tt.description) + assert.Equal(t, tt.expectedReason, resolutionDetails.Reason, tt.description) + } + }) + } +} + +func TestInternalFlag_Value_NilContext(t *testing.T) { + tests := []struct { + name string + flag flag.InternalFlag + evaluationCtx ffcontext.Context + expectedValue interface{} + expectedErrorCode string + expectedReason string + shouldSucceed bool + description string + }{ + { + name: "Should succeed with nil context for static flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + evaluationCtx: nil, + expectedValue: false, + expectedReason: flag.ReasonStatic, + shouldSucceed: true, + description: "Flag without bucketing requirements should work with nil context", + }, + { + name: "Should fail with nil context for percentage-based flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 20, + "disabled": 80, + }, + }, + }, + evaluationCtx: nil, + expectedErrorCode: flag.ErrorCodeTargetingKeyMissing, + expectedReason: flag.ReasonError, + shouldSucceed: false, + description: "Flag with percentage-based rollout should fail with nil context", + }, + { + name: "Should succeed with nil context for flag with targeting rule without percentages", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + Rules: &[]flag.Rule{ + { + Query: testconvert.String("key eq \"admin\""), + VariationResult: testconvert.String("enabled"), + }, + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + evaluationCtx: nil, + expectedValue: false, + expectedReason: flag.ReasonDefault, + shouldSucceed: true, + description: "Flag with targeting rule but no bucketing should work with nil context (falls back to default)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + flagContext := flag.Context{DefaultSdkValue: false} + value, resolutionDetails := tt.flag.Value("test-flag", tt.evaluationCtx, flagContext) + + if tt.shouldSucceed { + assert.Equal(t, "", resolutionDetails.ErrorCode, tt.description) + assert.Equal(t, tt.expectedReason, resolutionDetails.Reason, tt.description) + if tt.expectedValue != nil { + assert.Equal(t, tt.expectedValue, value, tt.description) + } + } else { + assert.Equal(t, tt.expectedErrorCode, resolutionDetails.ErrorCode, tt.description) + assert.Equal(t, tt.expectedReason, resolutionDetails.Reason, tt.description) + } + }) + } +} + +func TestInternalFlag_GetBucketingKeyValue_EmptyContext(t *testing.T) { + tests := []struct { + name string + flag flag.InternalFlag + evaluationCtx ffcontext.Context + expectedKey string + expectedError bool + description string + }{ + { + name: "Should allow empty key for non-bucketing flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + evaluationCtx: ffcontext.NewEvaluationContext(""), + expectedKey: "", + expectedError: false, + description: "Non-bucketing flag should allow empty targeting key", + }, + { + name: "Should require key for bucketing flag", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + }, + evaluationCtx: ffcontext.NewEvaluationContext(""), + expectedKey: "", + expectedError: true, + description: "Bucketing flag should require targeting key", + }, + { + name: "Should use custom bucketing key when provided", + flag: flag.InternalFlag{ + BucketingKey: testconvert.String("teamId"), + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + }, + evaluationCtx: func() ffcontext.Context { + ctx := ffcontext.NewEvaluationContext("") + ctx.AddCustomAttribute("teamId", "team-123") + return ctx + }(), + expectedKey: "team-123", + expectedError: false, + description: "Should use custom bucketing key when available", + }, + { + name: "Should allow empty custom bucketing key for non-bucketing flag", + flag: flag.InternalFlag{ + BucketingKey: testconvert.String("teamId"), + Variations: &map[string]*interface{}{ + "enabled": testconvert.Interface(true), + "disabled": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("disabled"), + }, + }, + evaluationCtx: func() ffcontext.Context { + ctx := ffcontext.NewEvaluationContext("") + ctx.AddCustomAttribute("teamId", "") + return ctx + }(), + expectedKey: "", + expectedError: false, + description: "Non-bucketing flag should allow empty custom bucketing key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := tt.flag.GetBucketingKeyValue(tt.evaluationCtx) + + if tt.expectedError { + assert.Error(t, err, tt.description) + } else { + assert.NoError(t, err, tt.description) + assert.Equal(t, tt.expectedKey, key, tt.description) + } + }) + } +} diff --git a/internal/flag/internal_flag_test.go b/internal/flag/internal_flag_test.go index dd3b00a0a05..44ae57769e3 100644 --- a/internal/flag/internal_flag_test.go +++ b/internal/flag/internal_flag_test.go @@ -691,6 +691,7 @@ func TestInternalFlag_Value(t *testing.T) { "description": "this is a flag", "issue-link": "https://issue.link/GOFF-1", }, + ErrorMessage: "error in the configuration, no variation available for this rule", }, }, { @@ -732,6 +733,7 @@ func TestInternalFlag_Value(t *testing.T) { "description": "this is a flag", "issue-link": "https://issue.link/GOFF-1", }, + ErrorMessage: "error in the configuration, no variation available for this rule", }, }, { @@ -773,6 +775,7 @@ func TestInternalFlag_Value(t *testing.T) { "description": "this is a flag", "issue-link": "https://issue.link/GOFF-1", }, + ErrorMessage: "no default targeting for the flag", }, }, { @@ -808,6 +811,7 @@ func TestInternalFlag_Value(t *testing.T) { "description": "this is a flag", "issue-link": "https://issue.link/GOFF-1", }, + ErrorMessage: "error in the configuration, no variation available for this rule", }, }, { @@ -1953,13 +1957,14 @@ func TestInternalFlag_Value(t *testing.T) { }, want: "default-sdk", want1: flag.ResolutionDetails{ - Variant: "SdkDefault", - Reason: flag.ReasonError, - ErrorCode: flag.ErrorCodeGeneral, + Variant: "SdkDefault", + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeGeneral, + ErrorMessage: "json: unsupported type: chan int", }, }, { - name: "Empty targeting key", + name: "Empty targeting key for static flag should succeed", flag: flag.InternalFlag{ Variations: &map[string]*interface{}{ "variation_A": testconvert.Interface(true), @@ -1980,13 +1985,11 @@ func TestInternalFlag_Value(t *testing.T) { DefaultSdkValue: false, }, }, - want: false, + want: true, want1: flag.ResolutionDetails{ - Variant: "SdkDefault", - Reason: flag.ReasonError, - ErrorCode: flag.ErrorCodeTargetingKeyMissing, - ErrorMessage: "Error: Empty targeting key", - Cacheable: false, + Variant: "variation_A", + Reason: flag.ReasonStatic, + Cacheable: true, Metadata: map[string]interface{}{ "description": "this is a flag", "issue-link": "https://issue.link/GOFF-1", @@ -1994,7 +1997,7 @@ func TestInternalFlag_Value(t *testing.T) { }, }, { - name: "Empty bucketing key", + name: "Empty bucketing key for static flag should succeed", flag: flag.InternalFlag{ Variations: &map[string]*interface{}{ "variation_A": testconvert.Interface(true), @@ -2018,12 +2021,48 @@ func TestInternalFlag_Value(t *testing.T) { DefaultSdkValue: false, }, }, + want: true, + want1: flag.ResolutionDetails{ + Variant: "variation_A", + Reason: flag.ReasonStatic, + Cacheable: true, + Metadata: map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + }, + { + name: "Empty targeting key for percentage flag should fail", + flag: flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "variation_A": testconvert.Interface(true), + "variation_B": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "variation_A": 50, + "variation_B": 50, + }, + }, + Metadata: &map[string]interface{}{ + "description": "this is a flag", + "issue-link": "https://issue.link/GOFF-1", + }, + }, + args: args{ + flagName: "my-flag", + user: ffcontext.NewEvaluationContext(""), + flagContext: flag.Context{ + DefaultSdkValue: false, + }, + }, want: false, want1: flag.ResolutionDetails{ Variant: "SdkDefault", Reason: flag.ReasonError, ErrorCode: flag.ErrorCodeTargetingKeyMissing, - ErrorMessage: "Error: Empty bucketing key", + ErrorMessage: "Error: Empty targeting key", Cacheable: false, Metadata: map[string]interface{}{ "description": "this is a flag", diff --git a/internal/flag/rule.go b/internal/flag/rule.go index 6796aaf5305..d7d49e88b1c 100644 --- a/internal/flag/rule.go +++ b/internal/flag/rule.go @@ -49,12 +49,28 @@ type Rule struct { Disable *bool `json:"disable,omitempty" yaml:"disable,omitempty" toml:"disable,omitempty" jsonschema:"title=disable,description=Indicates that this rule is disabled."` // nolint: lll } +// RequiresBucketing checks if this rule requires a bucketing key for evaluation +func (r *Rule) RequiresBucketing() bool { + // Rules with percentages need bucketing for distribution + if r.Percentages != nil && len(r.GetPercentages()) > 0 { + return true + } + + // Rules with progressive rollout need bucketing + if r.ProgressiveRollout != nil { + return true + } + + return false +} + // Evaluate is checking if the rule applies to for the user. // If yes, it returns the variation you should use for this rule. func (r *Rule) Evaluate(key string, ctx ffcontext.Context, flagName string, isDefault bool, ) (string, error) { - if key == "" { - return "", fmt.Errorf("evaluate Rule: no key") + // Only require key if this rule needs bucketing + if key == "" && r.RequiresBucketing() { + return "", fmt.Errorf("evaluate Rule: no key for bucketing-required rule") } evaluationDate := DateFromContextOrDefault(ctx, time.Now()) @@ -69,9 +85,15 @@ func (r *Rule) Evaluate(key string, ctx ffcontext.Context, flagName string, isDe return "", &internalerror.RuleNotApplyError{Context: ctx} } if r.ProgressiveRollout != nil { + if key == "" { + return "", fmt.Errorf("progressive rollout requires a bucketing key") + } return r.EvaluateProgressiveRollout(key, flagName, evaluationDate) } if r.Percentages != nil && len(r.GetPercentages()) > 0 { + if key == "" { + return "", fmt.Errorf("percentage rollout requires a bucketing key") + } return r.EvaluatePercentageRollout(key, flagName) } if r.VariationResult != nil { diff --git a/internal/flag/rule_empty_context_test.go b/internal/flag/rule_empty_context_test.go new file mode 100644 index 00000000000..61630fb956b --- /dev/null +++ b/internal/flag/rule_empty_context_test.go @@ -0,0 +1,171 @@ +package flag_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" +) + +func TestRule_RequiresBucketing(t *testing.T) { + tests := []struct { + name string + rule flag.Rule + expected bool + }{ + { + name: "Should not require bucketing - static variation", + rule: flag.Rule{ + VariationResult: testconvert.String("enabled"), + }, + expected: false, + }, + { + name: "Should require bucketing - has percentages", + rule: flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + expected: true, + }, + { + name: "Should not require bucketing - empty percentages", + rule: flag.Rule{ + Percentages: &map[string]float64{}, + }, + expected: false, + }, + { + name: "Should require bucketing - has progressive rollout", + rule: flag.Rule{ + ProgressiveRollout: &flag.ProgressiveRollout{ + Initial: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("disabled"), + Percentage: testconvert.Float64(0), + Date: testconvert.Time(time.Now()), + }, + End: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("enabled"), + Percentage: testconvert.Float64(100), + Date: testconvert.Time(time.Now().Add(time.Hour)), + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.rule.RequiresBucketing() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestRule_Evaluate_EmptyKey(t *testing.T) { + tests := []struct { + name string + rule flag.Rule + key string + ctx ffcontext.Context + expectedError bool + expectedValue string + description string + }{ + { + name: "Should succeed with empty key for static variation", + rule: flag.Rule{ + VariationResult: testconvert.String("enabled"), + }, + key: "", + ctx: ffcontext.NewEvaluationContext(""), + expectedError: false, + expectedValue: "enabled", + description: "Static variation should work without key", + }, + { + name: "Should fail with empty key for percentage rule", + rule: flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + key: "", + ctx: ffcontext.NewEvaluationContext(""), + expectedError: true, + description: "Percentage rule should fail without key", + }, + { + name: "Should succeed with key for percentage rule", + rule: flag.Rule{ + Percentages: &map[string]float64{ + "enabled": 50, + "disabled": 50, + }, + }, + key: "user-123", + ctx: ffcontext.NewEvaluationContext("user-123"), + expectedError: false, + description: "Percentage rule should work with key", + }, + { + name: "Should fail with empty key for progressive rollout", + rule: flag.Rule{ + ProgressiveRollout: &flag.ProgressiveRollout{ + Initial: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("disabled"), + Percentage: testconvert.Float64(0), + Date: testconvert.Time(time.Now().Add(-time.Hour)), + }, + End: &flag.ProgressiveRolloutStep{ + Variation: testconvert.String("enabled"), + Percentage: testconvert.Float64(100), + Date: testconvert.Time(time.Now().Add(time.Hour)), + }, + }, + }, + key: "", + ctx: ffcontext.NewEvaluationContext(""), + expectedError: true, + description: "Progressive rollout should fail without key", + }, + { + name: "Should succeed with empty key for query-only rule", + rule: flag.Rule{ + Query: testconvert.String("anonymous eq true"), + VariationResult: testconvert.String("enabled"), + }, + key: "", + ctx: func() ffcontext.Context { + ctx := ffcontext.NewEvaluationContext("") + ctx.AddCustomAttribute("anonymous", true) + return ctx + }(), + expectedError: false, + expectedValue: "enabled", + description: "Query-only rule should work without key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.rule.Evaluate(tt.key, tt.ctx, "test-flag", false) + + if tt.expectedError { + assert.Error(t, err, tt.description) + } else { + assert.NoError(t, err, tt.description) + if tt.expectedValue != "" { + assert.Equal(t, tt.expectedValue, result, tt.description) + } + } + }) + } +} diff --git a/internal/flagstate/flag_state_test.go b/internal/flagstate/flag_state_test.go index 53b52f87690..0d4fcb48a7f 100644 --- a/internal/flagstate/flag_state_test.go +++ b/internal/flagstate/flag_state_test.go @@ -50,7 +50,10 @@ func TestFromFlagEvaluation(t *testing.T) { "var2": testconvert.Interface(2), }, DefaultRule: &flag.Rule{ - VariationResult: testconvert.String("var1"), + Percentages: &map[string]float64{ + "var1": 50, + "var2": 50, + }, }, }, expected: flagstate.FlagState{ diff --git a/variation_test.go b/variation_test.go index 90b06fbfb59..409d9cd7ae7 100644 --- a/variation_test.go +++ b/variation_test.go @@ -57,12 +57,12 @@ func (c *cacheMock) AllFlags() (map[string]flag.Flag, error) { return nil, nil } func TestBoolVariation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue bool - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue bool + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -74,9 +74,9 @@ func TestBoolVariation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: false, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: false, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -99,9 +99,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -113,9 +113,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -127,9 +127,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -142,9 +142,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -174,9 +174,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -206,9 +206,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -238,9 +238,9 @@ func TestBoolVariation(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -270,9 +270,9 @@ func TestBoolVariation(t *testing.T) { { name: "No exported log", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -303,10 +303,10 @@ func TestBoolVariation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: false, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: false, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -358,7 +358,7 @@ func TestBoolVariation(t *testing.T) { } } - got, err := BoolVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := BoolVariation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -391,12 +391,12 @@ func TestBoolVariation(t *testing.T) { func TestBoolVariationDetails(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue bool - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue bool + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -408,9 +408,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: false, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: false, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -432,9 +432,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -453,9 +453,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -466,9 +466,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -480,9 +480,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -519,9 +519,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -561,9 +561,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get rule name on metadata, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -603,9 +603,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get no rule name on metadata, rule apply has not name", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -641,9 +641,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -683,9 +683,9 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -722,10 +722,10 @@ func TestBoolVariationDetails(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: false, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: false, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -740,6 +740,65 @@ func TestBoolVariationDetails(t *testing.T) { wantErr: false, expectedLog: "", }, + { + name: "Evaluation without targetingKey: success", + args: args{ + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext(""), + defaultValue: true, + cacheMock: NewCacheMock(&flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "on": testconvert.Interface(true), + "off": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + VariationResult: testconvert.String("on"), + }, + }, nil), + }, + want: model.VariationResult[bool]{ + VariationType: "on", + Failed: false, + Reason: flag.ReasonStatic, + Value: true, + TrackEvents: true, + Cacheable: true, + }, + wantErr: false, + expectedLog: `user="", flag="test-flag", value="true", variation="on"`, + }, + { + name: "Evaluation without targetingKey: fail because flag requires bucketing", + args: args{ + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext(""), + defaultValue: false, + cacheMock: NewCacheMock(&flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "on": testconvert.Interface(true), + "off": testconvert.Interface(false), + }, + DefaultRule: &flag.Rule{ + Percentages: &map[string]float64{ + "on": 100, + "off": 0, + }, + }, + }, nil), + }, + want: model.VariationResult[bool]{ + VariationType: "SdkDefault", + Failed: true, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTargetingKeyMissing, + ErrorDetails: "Error: Empty targeting key", + Value: false, + TrackEvents: true, + Cacheable: false, + }, + wantErr: false, + expectedLog: `user="", flag="test-flag", value="false", variation="SdkDefault"`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -780,7 +839,7 @@ func TestBoolVariationDetails(t *testing.T) { } } - got, err := BoolVariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := BoolVariationDetails(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -819,12 +878,12 @@ func TestBoolVariationDetails(t *testing.T) { func TestFloat64Variation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue float64 - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue float64 + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -836,9 +895,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 123.3, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 123.3, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -861,9 +920,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 120.12, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 120.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -875,9 +934,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -889,9 +948,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -904,9 +963,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -936,9 +995,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -968,9 +1027,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1000,9 +1059,9 @@ func TestFloat64Variation(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1032,9 +1091,9 @@ func TestFloat64Variation(t *testing.T) { { name: "No exported log", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1065,10 +1124,10 @@ func TestFloat64Variation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -1117,7 +1176,7 @@ func TestFloat64Variation(t *testing.T) { } } - got, err := Float64Variation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := Float64Variation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -1149,12 +1208,12 @@ func TestFloat64Variation(t *testing.T) { func TestFloat64VariationDetails(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue float64 - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue float64 + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -1166,9 +1225,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 123.3, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 123.3, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -1190,9 +1249,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 120.12, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 120.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -1211,9 +1270,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -1224,9 +1283,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -1238,9 +1297,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1277,9 +1336,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1319,9 +1378,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1361,9 +1420,9 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: 118.12, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1399,10 +1458,10 @@ func TestFloat64VariationDetails(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118.12, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118.12, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -1457,7 +1516,7 @@ func TestFloat64VariationDetails(t *testing.T) { } } - got, err := Float64VariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := Float64VariationDetails(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -1489,12 +1548,12 @@ func TestFloat64VariationDetails(t *testing.T) { func TestJSONArrayVariation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue []interface{} - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue []interface{} + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -1506,9 +1565,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{}, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -1531,9 +1590,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -1545,9 +1604,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -1559,9 +1618,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -1574,9 +1633,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1606,9 +1665,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1638,9 +1697,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "Default": testconvert.Interface([]interface{}{"default"}), @@ -1663,9 +1722,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1695,9 +1754,9 @@ func TestJSONArrayVariation(t *testing.T) { { name: "No exported log", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "Default": testconvert.Interface([]interface{}{"default"}), @@ -1721,10 +1780,10 @@ func TestJSONArrayVariation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -1773,7 +1832,7 @@ func TestJSONArrayVariation(t *testing.T) { } } - got, err := JSONArrayVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := JSONArrayVariation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.wantErr { assert.Error(t, err, "JSONArrayVariation() error = %v, wantErr %v", err, tt.wantErr) @@ -1804,12 +1863,12 @@ func TestJSONArrayVariation(t *testing.T) { func TestJSONArrayVariationDetails(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue []interface{} - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue []interface{} + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -1821,9 +1880,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{}, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -1845,9 +1904,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -1866,9 +1925,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -1879,9 +1938,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -1893,9 +1952,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1932,9 +1991,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -1974,9 +2033,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "Default": testconvert.Interface([]interface{}{"default"}), @@ -2006,9 +2065,9 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: []interface{}{"toto"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2044,10 +2103,10 @@ func TestJSONArrayVariationDetails(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: []interface{}{"toto"}, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: []interface{}{"toto"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2104,7 +2163,7 @@ func TestJSONArrayVariationDetails(t *testing.T) { got, err := JSONArrayVariationDetails( tt.args.flagKey, - tt.args.user, + tt.args.evaluationCtx, tt.args.defaultValue, ) @@ -2137,12 +2196,12 @@ func TestJSONArrayVariationDetails(t *testing.T) { func TestJSONVariation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue map[string]interface{} - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue map[string]interface{} + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -2154,9 +2213,9 @@ func TestJSONVariation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{}, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -2179,9 +2238,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2193,9 +2252,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -2207,9 +2266,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -2222,9 +2281,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2254,9 +2313,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2286,9 +2345,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2318,9 +2377,9 @@ func TestJSONVariation(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2350,10 +2409,10 @@ func TestJSONVariation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2402,7 +2461,7 @@ func TestJSONVariation(t *testing.T) { } } - got, err := JSONVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := JSONVariation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -2435,12 +2494,12 @@ func TestJSONVariation(t *testing.T) { func TestJSONVariationDetails(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue map[string]interface{} - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue map[string]interface{} + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -2452,9 +2511,9 @@ func TestJSONVariationDetails(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2473,9 +2532,9 @@ func TestJSONVariationDetails(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2512,9 +2571,9 @@ func TestJSONVariationDetails(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2554,9 +2613,9 @@ func TestJSONVariationDetails(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: map[string]interface{}{"default-notkey": true}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2596,10 +2655,10 @@ func TestJSONVariationDetails(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"default-notkey": true}, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"default-notkey": true}, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2654,7 +2713,7 @@ func TestJSONVariationDetails(t *testing.T) { } } - got, err := JSONVariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := JSONVariationDetails(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -2687,12 +2746,12 @@ func TestJSONVariationDetails(t *testing.T) { func TestStringVariation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue string - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue string + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -2704,9 +2763,9 @@ func TestStringVariation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "", + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "", cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -2729,9 +2788,9 @@ func TestStringVariation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2743,9 +2802,9 @@ func TestStringVariation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -2757,9 +2816,9 @@ func TestStringVariation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -2773,9 +2832,9 @@ func TestStringVariation(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2805,9 +2864,9 @@ func TestStringVariation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2837,9 +2896,9 @@ func TestStringVariation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2869,9 +2928,9 @@ func TestStringVariation(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -2901,10 +2960,10 @@ func TestStringVariation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -2952,7 +3011,7 @@ func TestStringVariation(t *testing.T) { ), } } - got, err := StringVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := StringVariation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -2985,12 +3044,12 @@ func TestStringVariation(t *testing.T) { func TestStringVariationDetails(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue string - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue string + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -3002,9 +3061,9 @@ func TestStringVariationDetails(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3023,9 +3082,9 @@ func TestStringVariationDetails(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3062,9 +3121,9 @@ func TestStringVariationDetails(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3104,9 +3163,9 @@ func TestStringVariationDetails(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: "default-notkey", + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3146,10 +3205,10 @@ func TestStringVariationDetails(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "default-notkey", + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "default-notkey", cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3203,7 +3262,7 @@ func TestStringVariationDetails(t *testing.T) { ), } } - got, err := StringVariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := StringVariationDetails(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -3236,12 +3295,12 @@ func TestStringVariationDetails(t *testing.T) { func TestIntVariation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue int - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue int + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -3253,9 +3312,9 @@ func TestIntVariation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 1, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 1, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -3278,9 +3337,9 @@ func TestIntVariation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 125, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 125, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3292,9 +3351,9 @@ func TestIntVariation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -3306,9 +3365,9 @@ func TestIntVariation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -3321,9 +3380,9 @@ func TestIntVariation(t *testing.T) { { name: "Get default value rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3353,9 +3412,9 @@ func TestIntVariation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3385,9 +3444,9 @@ func TestIntVariation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3417,9 +3476,9 @@ func TestIntVariation(t *testing.T) { { name: "Get default value, when rule apply and not right type", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key-ssss1"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key-ssss1"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3449,9 +3508,9 @@ func TestIntVariation(t *testing.T) { { name: "Convert float to Int", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3481,10 +3540,10 @@ func TestIntVariation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 125, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 125, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3532,7 +3591,7 @@ func TestIntVariation(t *testing.T) { ), } } - got, err := IntVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := IntVariation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -3565,12 +3624,12 @@ func TestIntVariation(t *testing.T) { func TestIntVariationDetails(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue int - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue int + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -3582,9 +3641,9 @@ func TestIntVariationDetails(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 125, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 125, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3603,9 +3662,9 @@ func TestIntVariationDetails(t *testing.T) { { name: "Get default value rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3642,9 +3701,9 @@ func TestIntVariationDetails(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3684,9 +3743,9 @@ func TestIntVariationDetails(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3726,9 +3785,9 @@ func TestIntVariationDetails(t *testing.T) { { name: "Convert float to Int", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: 118, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: 118, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -3768,10 +3827,10 @@ func TestIntVariationDetails(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 125, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 125, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3825,7 +3884,7 @@ func TestIntVariationDetails(t *testing.T) { ), } } - got, err := IntVariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := IntVariationDetails(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( @@ -3858,12 +3917,12 @@ func TestIntVariationDetails(t *testing.T) { func TestRawVariation(t *testing.T) { type args struct { - flagKey string - user ffcontext.Context - defaultValue interface{} - cacheMock cache.Manager - offline bool - disableInit bool + flagKey string + evaluationCtx ffcontext.Context + defaultValue interface{} + cacheMock cache.Manager + offline bool + disableInit bool } tests := []struct { name string @@ -3875,9 +3934,9 @@ func TestRawVariation(t *testing.T) { { name: "Call variation before init of SDK", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "", + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "", cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "var_true": testconvert.Interface(true), @@ -3906,9 +3965,9 @@ func TestRawVariation(t *testing.T) { { name: "Get default value if flag disable", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -3927,9 +3986,9 @@ func TestRawVariation(t *testing.T) { { name: "Get error when cache not init", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: "defaultValue", + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: "defaultValue", cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("impossible to read the toggle before the initialisation")), @@ -3949,9 +4008,9 @@ func TestRawVariation(t *testing.T) { { name: "Get default value with key not exist", args: args{ - flagKey: "key-not-exist", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: 123456, + flagKey: "key-not-exist", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: 123456, cacheMock: NewCacheMock( &flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists"), @@ -3971,9 +4030,9 @@ func TestRawVariation(t *testing.T) { { name: "Get default value, rule not apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"test123": "test"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"test123": "test"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -4010,9 +4069,9 @@ func TestRawVariation(t *testing.T) { { name: "Get true value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: map[string]interface{}{"test123": "test"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: map[string]interface{}{"test123": "test"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -4052,9 +4111,9 @@ func TestRawVariation(t *testing.T) { { name: "Get false value, rule apply", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), - defaultValue: map[string]interface{}{"test123": "test"}, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key-ssss1"), + defaultValue: map[string]interface{}{"test123": "test"}, cacheMock: NewCacheMock(&flag.InternalFlag{ Rules: &[]flag.Rule{ { @@ -4094,9 +4153,9 @@ func TestRawVariation(t *testing.T) { { name: "No exported log", args: args{ - flagKey: "test-flag", - user: ffcontext.NewAnonymousEvaluationContext("random-key"), - defaultValue: true, + flagKey: "test-flag", + evaluationCtx: ffcontext.NewAnonymousEvaluationContext("random-key"), + defaultValue: true, cacheMock: NewCacheMock(&flag.InternalFlag{ Variations: &map[string]*interface{}{ "Default": testconvert.Interface(false), @@ -4137,10 +4196,10 @@ func TestRawVariation(t *testing.T) { { name: "Get sdk default value if offline", args: args{ - offline: true, - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: false, + offline: true, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: false, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -4158,9 +4217,9 @@ func TestRawVariation(t *testing.T) { { name: "should use interface default value if the flag is disabled", args: args{ - flagKey: "disable-flag", - user: ffcontext.NewEvaluationContext("random-key"), - defaultValue: nil, + flagKey: "disable-flag", + evaluationCtx: ffcontext.NewEvaluationContext("random-key"), + defaultValue: nil, cacheMock: NewCacheMock(&flag.InternalFlag{ Disable: testconvert.Bool(true), }, nil), @@ -4216,7 +4275,7 @@ func TestRawVariation(t *testing.T) { } } - got, err := ff.RawVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) + got, err := ff.RawVariation(tt.args.flagKey, tt.args.evaluationCtx, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep( diff --git a/wasm/main.go b/wasm/main.go index e4e719728ba..bbbe3af7213 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "fmt" "github.com/thomaspoignant/go-feature-flag/evaluation" "github.com/thomaspoignant/go-feature-flag/ffcontext" @@ -45,14 +44,10 @@ func localEvaluation(input string) string { }.ToJsonStr() } - evalCtx, err := convertEvaluationCtx(evaluateInput.EvaluationCtx) - if err != nil { - return model.VariationResult[interface{}]{ - ErrorCode: flag.ErrorCodeTargetingKeyMissing, - ErrorDetails: err.Error(), - }.ToJsonStr() - } + evalCtx := convertEvaluationCtx(evaluateInput.EvaluationCtx) + // we don't care about the error here because the errorCode and errorDetails + // contains information about the type of the error directly, no need to check the Go error. c, _ := evaluation.Evaluate[interface{}]( &evaluateInput.Flag, evaluateInput.FlagKey, @@ -65,11 +60,16 @@ func localEvaluation(input string) string { } // convertEvaluationCtx converts the evaluation context from the input to a ffcontext.Context. -func convertEvaluationCtx(ctx map[string]any) (ffcontext.Context, error) { - if targetingKey, ok := ctx["targetingKey"].(string); ok { - evalCtx := utils.ConvertEvaluationCtxFromRequest(targetingKey, ctx) - return evalCtx, nil +// Note: Empty targeting keys are now allowed - the core evaluation logic will determine +// if a targeting key is required based on whether the flag needs bucketing. +func convertEvaluationCtx(ctx map[string]any) ffcontext.Context { + // Allow empty or missing targeting keys - core evaluation logic will handle requirements + targetingKey := "" + if key, ok := ctx["targetingKey"].(string); ok { + targetingKey = key } - return ffcontext.NewEvaluationContextBuilder("").Build(), - fmt.Errorf("targetingKey not found in context") + + // Create evaluation context (empty targeting key is allowed) + evalCtx := utils.ConvertEvaluationCtxFromRequest(targetingKey, ctx) + return evalCtx } diff --git a/wasm/testdata/local_evaluation_outputs/missing-targeting-key.json b/wasm/testdata/local_evaluation_outputs/missing-targeting-key.json index 1f84c137811..4c89f4a40bd 100644 --- a/wasm/testdata/local_evaluation_outputs/missing-targeting-key.json +++ b/wasm/testdata/local_evaluation_outputs/missing-targeting-key.json @@ -1,11 +1,15 @@ { - "trackEvents": false, - "variationType": "", - "failed": false, + "trackEvents": true, + "variationType": "SdkDefault", + "failed": true, "version": "", - "reason": "", + "reason": "ERROR", "errorCode": "TARGETING_KEY_MISSING", - "errorDetails": "targetingKey not found in context", - "value": null, - "cacheable": false + "errorDetails": "Error: Empty targeting key", + "value": false, + "cacheable": false, + "metadata": { + "description": "test flag", + "type": "boolean" + } } \ No newline at end of file