diff --git a/cmd/relayproxy/controller/all_flags.go b/cmd/relayproxy/controller/all_flags.go index d0573501586..87d5c9ec46a 100644 --- a/cmd/relayproxy/controller/all_flags.go +++ b/cmd/relayproxy/controller/all_flags.go @@ -26,6 +26,7 @@ func NewAllFlags(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics) Controlle // Handler is the entry point for the allFlags endpoint // @Summary All flags variations for a user +// @Tags GO Feature Flag Evaluation API // @Description Making a **POST** request to the URL `/v1/allflags` will give you the values of all the flags for // @Description this user. // @Description diff --git a/cmd/relayproxy/controller/collect_eval_data.go b/cmd/relayproxy/controller/collect_eval_data.go index 915cdaf9f2e..33fb7b29ad0 100644 --- a/cmd/relayproxy/controller/collect_eval_data.go +++ b/cmd/relayproxy/controller/collect_eval_data.go @@ -28,6 +28,7 @@ func NewCollectEvalData(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics) Co // Handler is the entry point for the data/collector endpoint // @Summary Endpoint to send usage of your flags to be collected +// @Tags GO Feature Flag Evaluation API // @Description This endpoint is receiving the events of your flags usage to send them in the data collector. // @Description // @Description It is used by the different Open Feature providers to send in bulk all the cached events to avoid diff --git a/cmd/relayproxy/controller/flag_eval.go b/cmd/relayproxy/controller/flag_eval.go index 84808cb5c9b..e932b777568 100644 --- a/cmd/relayproxy/controller/flag_eval.go +++ b/cmd/relayproxy/controller/flag_eval.go @@ -27,6 +27,7 @@ func NewFlagEval(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics) Controlle // Handler is the entry point for the flag eval endpoint // @Summary Evaluate a feature flag +// @Tags GO Feature Flag Evaluation API // @Description Making a **POST** request to the URL `/v1/feature//eval` will give you the value of the // @Description flag for this user. // @Description diff --git a/cmd/relayproxy/controller/health.go b/cmd/relayproxy/controller/health.go index d59d28b3a14..45df232a04a 100644 --- a/cmd/relayproxy/controller/health.go +++ b/cmd/relayproxy/controller/health.go @@ -20,6 +20,7 @@ func NewHealth(monitoring service.Monitoring) Controller { // Handler is the entry point for this API // @Summary Health +// @Tags Monitoring // @Description Making a **GET** request to the URL path `/health` will tell you if the relay proxy is ready to serve // @Description traffic. // @Description diff --git a/cmd/relayproxy/controller/info.go b/cmd/relayproxy/controller/info.go index 80fe706e339..47dd9047eba 100644 --- a/cmd/relayproxy/controller/info.go +++ b/cmd/relayproxy/controller/info.go @@ -19,6 +19,7 @@ func NewInfo(monitoring service.Monitoring) Controller { // Handler is the entry point for the Info API // @Summary Info +// @Tags Monitoring // @Description Making a **GET** request to the URL path `/info` will give you information about the actual state // @Description of the relay proxy. // @Description diff --git a/cmd/relayproxy/controller/ws_flag_change.go b/cmd/relayproxy/controller/ws_flag_change.go index 7bfbc337913..3678835a457 100644 --- a/cmd/relayproxy/controller/ws_flag_change.go +++ b/cmd/relayproxy/controller/ws_flag_change.go @@ -32,6 +32,7 @@ type wsFlagChange struct { // Handler is the entry point for the websocket endpoint to get notified when a flag has been edited // @Summary Websocket endpoint to be notified about flag changes +// @Tags GO Feature Flag Evaluation API // @Description This endpoint is a websocket endpoint to be notified about flag changes, every change // @Description will send a request to the client with a model.DiffCache format. // @Description diff --git a/cmd/relayproxy/docs/docs.go b/cmd/relayproxy/docs/docs.go index 68f9cae2fa5..3767a16afdc 100644 --- a/cmd/relayproxy/docs/docs.go +++ b/cmd/relayproxy/docs/docs.go @@ -32,6 +32,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "Monitoring" + ], "summary": "Health", "responses": { "200": { @@ -49,6 +52,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "Monitoring" + ], "summary": "Info", "responses": { "200": { @@ -66,6 +72,9 @@ const docTemplate = `{ "produces": [ "text/plain" ], + "tags": [ + "Monitoring" + ], "summary": "Prometheus endpoint", "responses": { "200": { @@ -91,6 +100,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "OpenFeature Remote Evaluation Protocol (OFREP)" + ], "summary": "Open-Feature Remote Evaluation Protocol bulk evaluation API.", "parameters": [ { @@ -157,6 +169,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "OpenFeature Remote Evaluation Protocol (OFREP)" + ], "summary": "Evaluate a feature flag using the OpenFeature Remote Evaluation Protocol", "parameters": [ { @@ -224,6 +239,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "All flags variations for a user", "parameters": [ { @@ -272,6 +290,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "Endpoint to send usage of your flags to be collected", "parameters": [ { @@ -320,6 +341,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "Evaluate a feature flag", "parameters": [ { @@ -370,6 +394,9 @@ const docTemplate = `{ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "Websocket endpoint to be notified about flag changes", "parameters": [ { diff --git a/cmd/relayproxy/docs/swagger.json b/cmd/relayproxy/docs/swagger.json index cd79f426940..fc3fa36f95e 100644 --- a/cmd/relayproxy/docs/swagger.json +++ b/cmd/relayproxy/docs/swagger.json @@ -24,6 +24,9 @@ "produces": [ "application/json" ], + "tags": [ + "Monitoring" + ], "summary": "Health", "responses": { "200": { @@ -41,6 +44,9 @@ "produces": [ "application/json" ], + "tags": [ + "Monitoring" + ], "summary": "Info", "responses": { "200": { @@ -58,6 +64,9 @@ "produces": [ "text/plain" ], + "tags": [ + "Monitoring" + ], "summary": "Prometheus endpoint", "responses": { "200": { @@ -83,6 +92,9 @@ "produces": [ "application/json" ], + "tags": [ + "OpenFeature Remote Evaluation Protocol (OFREP)" + ], "summary": "Open-Feature Remote Evaluation Protocol bulk evaluation API.", "parameters": [ { @@ -149,6 +161,9 @@ "produces": [ "application/json" ], + "tags": [ + "OpenFeature Remote Evaluation Protocol (OFREP)" + ], "summary": "Evaluate a feature flag using the OpenFeature Remote Evaluation Protocol", "parameters": [ { @@ -216,6 +231,9 @@ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "All flags variations for a user", "parameters": [ { @@ -264,6 +282,9 @@ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "Endpoint to send usage of your flags to be collected", "parameters": [ { @@ -312,6 +333,9 @@ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "Evaluate a feature flag", "parameters": [ { @@ -362,6 +386,9 @@ "produces": [ "application/json" ], + "tags": [ + "GO Feature Flag Evaluation API" + ], "summary": "Websocket endpoint to be notified about flag changes", "parameters": [ { diff --git a/cmd/relayproxy/docs/swagger.yaml b/cmd/relayproxy/docs/swagger.yaml index 6d25bf19ddc..6925d45f56d 100644 --- a/cmd/relayproxy/docs/swagger.yaml +++ b/cmd/relayproxy/docs/swagger.yaml @@ -386,6 +386,8 @@ paths: schema: $ref: '#/definitions/model.HealthResponse' summary: Health + tags: + - Monitoring /info: get: description: |- @@ -402,6 +404,8 @@ paths: schema: $ref: '#/definitions/model.InfoResponse' summary: Info + tags: + - Monitoring /metrics: get: description: This endpoint is providing metrics about the relay proxy in the @@ -414,6 +418,8 @@ paths: schema: type: string summary: Prometheus endpoint + tags: + - Monitoring /ofrep/v1/evaluate/flags: post: consumes: @@ -460,6 +466,8 @@ paths: security: - ApiKeyAuth: [] summary: Open-Feature Remote Evaluation Protocol bulk evaluation API. + tags: + - OpenFeature Remote Evaluation Protocol (OFREP) /ofrep/v1/evaluate/flags/{flag_key}: post: consumes: @@ -505,6 +513,8 @@ paths: security: - ApiKeyAuth: [] summary: Evaluate a feature flag using the OpenFeature Remote Evaluation Protocol + tags: + - OpenFeature Remote Evaluation Protocol (OFREP) /v1/allflags: post: consumes: @@ -540,6 +550,8 @@ paths: security: - ApiKeyAuth: [] summary: All flags variations for a user + tags: + - GO Feature Flag Evaluation API /v1/data/collector: post: consumes: @@ -574,6 +586,8 @@ paths: security: - ApiKeyAuth: [] summary: Endpoint to send usage of your flags to be collected + tags: + - GO Feature Flag Evaluation API /v1/feature/{flag_key}/eval: post: consumes: @@ -619,6 +633,8 @@ paths: security: - ApiKeyAuth: [] summary: Evaluate a feature flag + tags: + - GO Feature Flag Evaluation API /ws/v1/flag/change: post: consumes: @@ -647,6 +663,8 @@ paths: schema: $ref: '#/definitions/modeldocs.HTTPErrorDoc' summary: Websocket endpoint to be notified about flag changes + tags: + - GO Feature Flag Evaluation API securityDefinitions: ApiKeyAuth: description: Use configured APIKeys in yaml config as authorization keys, disabled diff --git a/cmd/relayproxy/model/ofrep_error_response.go b/cmd/relayproxy/model/ofrep_error_response.go index c84ba7f94fe..c7964079e23 100644 --- a/cmd/relayproxy/model/ofrep_error_response.go +++ b/cmd/relayproxy/model/ofrep_error_response.go @@ -1,6 +1,9 @@ package model -import "github.com/thomaspoignant/go-feature-flag/internal/flag" +import ( + "fmt" + "github.com/thomaspoignant/go-feature-flag/internal/flag" +) type OFREPEvaluateErrorResponse struct { OFREPCommonErrorResponse `json:",inline" yaml:",inline" toml:",inline"` @@ -11,3 +14,7 @@ type OFREPCommonErrorResponse struct { ErrorCode flag.ErrorCode `json:"errorCode"` ErrorDetails string `json:"errorDetails"` } + +func (o *OFREPCommonErrorResponse) Error() string { + return fmt.Sprintf("[%s] %s", o.ErrorCode, o.ErrorDetails) +} diff --git a/cmd/relayproxy/model/ofrep_success_response.go b/cmd/relayproxy/model/ofrep_success_response.go index 88942cc3948..73034c46917 100644 --- a/cmd/relayproxy/model/ofrep_success_response.go +++ b/cmd/relayproxy/model/ofrep_success_response.go @@ -5,5 +5,5 @@ type OFREPEvaluateSuccessResponse struct { Value any `json:"value"` Reason string `json:"reason"` Variant string `json:"variant"` - Metadata map[string]any `json:"metadata"` + Metadata map[string]any `json:"metadata,omitempty"` } diff --git a/cmd/relayproxy/modeldocs/metricsController.go b/cmd/relayproxy/modeldocs/metricsController.go index 834f46d39a8..3e34b73d24e 100644 --- a/cmd/relayproxy/modeldocs/metricsController.go +++ b/cmd/relayproxy/modeldocs/metricsController.go @@ -5,6 +5,7 @@ import "github.com/labstack/echo/v4" // FakeMetricsController is the entry point for the allFlags endpoint // // @Summary Prometheus endpoint +// @Tags Monitoring // @Description This endpoint is providing metrics about the relay proxy in the prometheus format. // @Produce plain // @Success 200 {object} string diff --git a/cmd/relayproxy/ofrep/evaluate.go b/cmd/relayproxy/ofrep/evaluate.go index 5d8ea27a455..61e29603dca 100644 --- a/cmd/relayproxy/ofrep/evaluate.go +++ b/cmd/relayproxy/ofrep/evaluate.go @@ -30,6 +30,7 @@ func NewOFREPEvaluate(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics) Eval // Evaluate is the entry point to evaluate a flag using the OpenFeature Remote Evaluation Protocol // @Summary Evaluate a feature flag using the OpenFeature Remote Evaluation Protocol +// @Tags OpenFeature Remote Evaluation Protocol (OFREP) // @Description Making a **POST** request to the URL `/ofrep/v1/evaluate/flags/{your_flag_name}` will give you the // @Description value of the flag for this evaluation context // @Description @@ -49,8 +50,8 @@ func (h *EvaluateCtrl) Evaluate(c echo.Context) error { if flagKey == "" { return c.JSON( http.StatusBadRequest, - NewEvaluateError(flag.ErrorCodeGeneral, - "No key provided in the URL").ToOFRErrorResponse()) + NewEvaluateError(flagKey, flag.ErrorCodeGeneral, + "No key provided in the URL")) } h.metrics.IncFlagEvaluation(flagKey) @@ -58,16 +59,25 @@ func (h *EvaluateCtrl) Evaluate(c echo.Context) error { if err := c.Bind(reqBody); err != nil { return c.JSON( http.StatusBadRequest, - NewEvaluateError(flag.ErrorCodeInvalidContext, err.Error()).ToOFRErrorResponse()) + NewEvaluateError(flagKey, flag.ErrorCodeInvalidContext, err.Error())) } if err := assertOFREPEvaluateRequest(reqBody); err != nil { - return c.JSON(http.StatusBadRequest, err.ToOFRErrorResponse()) + return c.JSON(http.StatusBadRequest, model.OFREPEvaluateErrorResponse{ + OFREPCommonErrorResponse: *err, + Key: flagKey, + }) } evalCtx, err := evaluationContextFromOFREPRequest(reqBody.Context) if err != nil { return c.JSON( http.StatusBadRequest, - err) + model.OFREPEvaluateErrorResponse{ + OFREPCommonErrorResponse: model.OFREPCommonErrorResponse{ + ErrorCode: flag.ErrorCodeInvalidContext, + ErrorDetails: err.Error(), + }, + Key: flagKey, + }) } tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName) @@ -83,8 +93,8 @@ func (h *EvaluateCtrl) Evaluate(c echo.Context) error { } return c.JSON( httpStatus, - NewEvaluateError(flagValue.ErrorCode, - fmt.Sprintf("Error while evaluating the flag: %s", flagKey)).ToOFRErrorResponse()) + NewEvaluateError(flagKey, flagValue.ErrorCode, + fmt.Sprintf("Error while evaluating the flag: %s", flagKey))) } span.SetAttributes( @@ -111,6 +121,7 @@ func (h *EvaluateCtrl) Evaluate(c echo.Context) error { // BulkEvaluate is the entry point to evaluate in bulk flags using the OpenFeature Remote Evaluation Protocol // @Summary Open-Feature Remote Evaluation Protocol bulk evaluation API. +// @Tags OpenFeature Remote Evaluation Protocol (OFREP) // @Description Making a **POST** request to the URL `/ofrep/v1/evaluate/flags` will give you the value of the list // @Description of feature flags for this evaluation context. // @Description @@ -133,10 +144,10 @@ func (h *EvaluateCtrl) BulkEvaluate(c echo.Context) error { if err := c.Bind(request); err != nil { return c.JSON( http.StatusBadRequest, - NewEvaluateError("INVALID_CONTEXT", err.Error()).ToOFRErrorResponse()) + NewOFREPCommonError("INVALID_CONTEXT", err.Error())) } if err := assertOFREPEvaluateRequest(request); err != nil { - return c.JSON(http.StatusBadRequest, err.ToOFRErrorResponse()) + return c.JSON(http.StatusBadRequest, err) } evalCtx, err := evaluationContextFromOFREPRequest(request.Context) if err != nil { @@ -183,9 +194,9 @@ func (h *EvaluateCtrl) BulkEvaluate(c echo.Context) error { return c.JSON(http.StatusOK, response) } -func assertOFREPEvaluateRequest(ofrepEvalReq *model.OFREPEvalFlagRequest) *EvaluateError { +func assertOFREPEvaluateRequest(ofrepEvalReq *model.OFREPEvalFlagRequest) *model.OFREPCommonErrorResponse { if ofrepEvalReq.Context == nil || ofrepEvalReq.Context["targetingKey"] == "" { - return NewEvaluateError(flag.ErrorCodeTargetingKeyMissing, + return NewOFREPCommonError(flag.ErrorCodeTargetingKeyMissing, "GO Feature Flag MUST have a targeting key in the request.") } return nil @@ -197,6 +208,7 @@ func evaluationContextFromOFREPRequest(ctx map[string]any) (ffcontext.Context, e evalCtx := utils.ConvertEvaluationCtxFromRequest(targetingKey, ctx) return evalCtx, nil } - return ffcontext.EvaluationContext{}, NewEvaluateError( - flag.ErrorCodeTargetingKeyMissing, "GO Feature Flag has received a targetingKey that is not a string.") + return ffcontext.EvaluationContext{}, NewOFREPCommonError( + flag.ErrorCodeTargetingKeyMissing, + "GO Feature Flag has received no targetingKey or a none string value that is not a string.") } diff --git a/cmd/relayproxy/ofrep/evaluate_test.go b/cmd/relayproxy/ofrep/evaluate_test.go new file mode 100644 index 00000000000..ba5bdf3dbf1 --- /dev/null +++ b/cmd/relayproxy/ofrep/evaluate_test.go @@ -0,0 +1,272 @@ +package ofrep_test + +import ( + "context" + "fmt" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" + "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/ofrep" + "io" + "log" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + ffclient "github.com/thomaspoignant/go-feature-flag" + "github.com/thomaspoignant/go-feature-flag/exporter/logsexporter" + "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" +) + +const configFlagsLocation = "../testdata/controller/config_flags.yaml" + +func Test_Bulk_Evaluation(t *testing.T) { + type want struct { + httpCode int + bodyFile string + handlerErr bool + errorMsg string + errorCode int + } + + type args struct { + bodyFile string + configFlagsLocation string + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "valid flag", + args: args{ + bodyFile: "../testdata/ofrep/valid_request.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/ofrep/responses/valid_response.json", + }, + }, + { + name: "Invalid context", + args: args{ + bodyFile: "../testdata/ofrep/invalid_context.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/invalid_context.json", + }, + }, + { + name: "Nil context", + args: args{ + bodyFile: "../testdata/ofrep/nil_context.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/nil_context.json", + }, + }, + { + name: "No Targeting Key in context", + args: args{ + bodyFile: "../testdata/ofrep/no_targeting_key_context.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/no_targeting_key_context.json", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // init go-feature-flag + goFF, _ := ffclient.New(ffclient.Config{ + PollingInterval: 10 * time.Second, + Logger: log.New(os.Stdout, "", 0), + Context: context.Background(), + Retriever: &fileretriever.Retriever{ + Path: tt.args.configFlagsLocation, + }, + DataExporter: ffclient.DataExporter{ + FlushInterval: 10 * time.Second, + MaxEventInMemory: 10000, + Exporter: &logsexporter.Exporter{}, + }, + }) + defer goFF.Close() + + ctrl := ofrep.NewOFREPEvaluate(goFF, metric.Metrics{}) + e := echo.New() + rec := httptest.NewRecorder() + + // read wantBody request file + var bodyReq io.Reader + if 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)) + } + + req := httptest.NewRequest(echo.POST, "/ofrep/v1/evaluate/flags", bodyReq) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + c := e.NewContext(req, rec) + + c.SetPath("/ofrep/v1/evaluate/flags") + handlerErr := ctrl.BulkEvaluate(c) + + if tt.want.handlerErr { + assert.Error(t, handlerErr, "handler should return an error") + he, ok := handlerErr.(*echo.HTTPError) + if ok { + assert.Equal(t, tt.want.errorCode, he.Code) + assert.Equal(t, tt.want.errorMsg, he.Message) + } else { + assert.Equal(t, tt.want.errorMsg, handlerErr.Error()) + } + return + } + + wantBody, err := os.ReadFile(tt.want.bodyFile) + + fmt.Println(rec.Header()) + + assert.NoError(t, err, "Impossible the expected wantBody file %s", tt.want.bodyFile) + assert.Equal(t, tt.want.httpCode, rec.Code, "Invalid HTTP Code") + assert.JSONEq(t, string(wantBody), rec.Body.String(), "Invalid response wantBody") + }) + } +} + +func Test_Evaluate(t *testing.T) { + type want struct { + httpCode int + bodyFile string + } + + type args struct { + bodyFile string + configFlagsLocation string + flagKey string + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "valid evaluation", + args: args{ + bodyFile: "../testdata/ofrep/valid_request.json", + configFlagsLocation: configFlagsLocation, + flagKey: "number-flag", + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/ofrep/responses/valid_evaluation.json", + }, + }, + { + name: "Invalid context", + args: args{ + bodyFile: "../testdata/ofrep/invalid_context.json", + configFlagsLocation: configFlagsLocation, + flagKey: "number-flag", + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/invalid_context_with_key.json", + }, + }, + { + name: "Nil context", + args: args{ + bodyFile: "../testdata/ofrep/nil_context.json", + configFlagsLocation: configFlagsLocation, + flagKey: "number-flag", + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/nil_context_with_key.json", + }, + }, + { + name: "No Targeting Key in context", + args: args{ + bodyFile: "../testdata/ofrep/no_targeting_key_context.json", + configFlagsLocation: configFlagsLocation, + flagKey: "number-flag", + }, + want: want{ + httpCode: http.StatusBadRequest, + bodyFile: "../testdata/ofrep/responses/no_targeting_key_context_with_key.json", + }, + }, + { + name: "Empty flag key", + args: args{ + bodyFile: "../testdata/ofrep/valid_request.json", + configFlagsLocation: configFlagsLocation, + flagKey: "", + }, + want: want{ + httpCode: http.StatusNotFound, + bodyFile: "../testdata/ofrep/responses/not_found.json", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // init go-feature-flag + goFF, _ := ffclient.New(ffclient.Config{ + PollingInterval: 10 * time.Second, + Logger: log.New(os.Stdout, "", 0), + Context: context.Background(), + Retriever: &fileretriever.Retriever{ + Path: tt.args.configFlagsLocation, + }, + DataExporter: ffclient.DataExporter{ + FlushInterval: 10 * time.Second, + MaxEventInMemory: 10000, + Exporter: &logsexporter.Exporter{}, + }, + }) + defer goFF.Close() + + ctrl := ofrep.NewOFREPEvaluate(goFF, metric.Metrics{}) + e := echo.New() + e.POST("/ofrep/v1/evaluate/flags/:flagKey", ctrl.Evaluate) + rec := httptest.NewRecorder() + + flagKey := tt.args.flagKey + + // read wantBody request file + var bodyReq io.Reader + if 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)) + } + req := httptest.NewRequest(echo.POST, "/ofrep/v1/evaluate/flags/"+flagKey, bodyReq) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + e.ServeHTTP(rec, req) + wantBody, err := os.ReadFile(tt.want.bodyFile) + fmt.Println(rec.Body.String()) + + assert.NoError(t, err, "Impossible the expected wantBody file %s", tt.want.bodyFile) + assert.Equal(t, tt.want.httpCode, rec.Code, "Invalid HTTP Code") + assert.JSONEq(t, string(wantBody), rec.Body.String(), "Invalid response wantBody") + }) + } +} diff --git a/cmd/relayproxy/ofrep/ofrepEvaluateError.go b/cmd/relayproxy/ofrep/ofrepEvaluateError.go index 0a8ae91981e..4170fcd0272 100644 --- a/cmd/relayproxy/ofrep/ofrepEvaluateError.go +++ b/cmd/relayproxy/ofrep/ofrepEvaluateError.go @@ -1,28 +1,23 @@ package ofrep import ( - "fmt" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" "github.com/thomaspoignant/go-feature-flag/internal/flag" ) -type EvaluateError struct { - err model.OFREPCommonErrorResponse -} - -func NewEvaluateError(errorCode flag.ErrorCode, errorDetails string) *EvaluateError { - return &EvaluateError{ - err: model.OFREPCommonErrorResponse{ +func NewEvaluateError(key string, errorCode flag.ErrorCode, errorDetails string) *model.OFREPEvaluateErrorResponse { + return &model.OFREPEvaluateErrorResponse{ + OFREPCommonErrorResponse: model.OFREPCommonErrorResponse{ ErrorCode: errorCode, ErrorDetails: errorDetails, }, + Key: key, } } -func (m *EvaluateError) Error() string { - return fmt.Sprintf("missing TargetingKey error: %v", m.err) -} - -func (m *EvaluateError) ToOFRErrorResponse() model.OFREPCommonErrorResponse { - return m.err +func NewOFREPCommonError(errorCode flag.ErrorCode, errorDetails string) *model.OFREPCommonErrorResponse { + return &model.OFREPCommonErrorResponse{ + ErrorCode: errorCode, + ErrorDetails: errorDetails, + } } diff --git a/cmd/relayproxy/testdata/ofrep/invalid_context.json b/cmd/relayproxy/testdata/ofrep/invalid_context.json new file mode 100644 index 00000000000..565c5864040 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/invalid_context.json @@ -0,0 +1,7 @@ +{ + "context": { + "company": "GO Feature Flag", + "firstname": "John", + "lastname": "Doe", + "targetingKey": "4f433951-4c8c-42b3-9f18-8c9a5ed8e9eb" +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/nil_context.json b/cmd/relayproxy/testdata/ofrep/nil_context.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cmd/relayproxy/testdata/ofrep/no_targeting_key_context.json b/cmd/relayproxy/testdata/ofrep/no_targeting_key_context.json new file mode 100644 index 00000000000..6a20302de24 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/no_targeting_key_context.json @@ -0,0 +1,7 @@ +{ + "context": { + "company": "GO Feature Flag", + "firstname": "John", + "lastname": "Doe" + } +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/invalid_context.json b/cmd/relayproxy/testdata/ofrep/responses/invalid_context.json new file mode 100644 index 00000000000..520ae82ee2e --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/invalid_context.json @@ -0,0 +1,4 @@ +{ + "errorCode": "INVALID_CONTEXT", + "errorDetails": "code=400, message=unexpected EOF, internal=unexpected EOF" +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/invalid_context_with_key.json b/cmd/relayproxy/testdata/ofrep/responses/invalid_context_with_key.json new file mode 100644 index 00000000000..8086004972d --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/invalid_context_with_key.json @@ -0,0 +1,5 @@ +{ + "errorCode": "INVALID_CONTEXT", + "errorDetails": "code=400, message=unexpected EOF, internal=unexpected EOF", + "key": "number-flag" +} \ 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 new file mode 100644 index 00000000000..efe38676815 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/nil_context.json @@ -0,0 +1,4 @@ +{ + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "GO Feature Flag MUST have a targeting key 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 new file mode 100644 index 00000000000..ed146f22c22 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/nil_context_with_key.json @@ -0,0 +1,5 @@ +{ + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "GO Feature Flag MUST have a targeting key in the request.", + "key": "number-flag" +} diff --git a/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json new file mode 100644 index 00000000000..9cfc8111722 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context.json @@ -0,0 +1,4 @@ +{ + "errorCode": "TARGETING_KEY_MISSING", + "errorDetails": "GO Feature Flag has received no targetingKey or a none string value that is not a string." +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context_with_key.json b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context_with_key.json new file mode 100644 index 00000000000..2f23cc32841 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/no_targeting_key_context_with_key.json @@ -0,0 +1,5 @@ +{ + "errorCode": "INVALID_CONTEXT", + "errorDetails": "[TARGETING_KEY_MISSING] GO Feature Flag has received no targetingKey or a none string value that is not a string.", + "key": "number-flag" +} diff --git a/cmd/relayproxy/testdata/ofrep/responses/not_found.json b/cmd/relayproxy/testdata/ofrep/responses/not_found.json new file mode 100644 index 00000000000..8567929152b --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/not_found.json @@ -0,0 +1 @@ +{"message":"Not Found"} diff --git a/cmd/relayproxy/testdata/ofrep/responses/valid_evaluation.json b/cmd/relayproxy/testdata/ofrep/responses/valid_evaluation.json new file mode 100644 index 00000000000..5a76d2b1f90 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/valid_evaluation.json @@ -0,0 +1,6 @@ +{ + "key": "number-flag", + "value": 1, + "reason": "DEFAULT", + "variant": "Default" +} diff --git a/cmd/relayproxy/testdata/ofrep/responses/valid_response.json b/cmd/relayproxy/testdata/ofrep/responses/valid_response.json new file mode 100644 index 00000000000..03854d11017 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/valid_response.json @@ -0,0 +1,62 @@ +{ + "flags": [ + { + "key": "array-flag", + "value": [ + "batmanDefault", + "supermanDefault", + "superherosDefault" + ], + "reason": "DEFAULT", + "variant": "Default" + }, + { + "key": "disable-flag", + "value": null, + "reason": "DISABLED", + "variant": "" + }, + { + "key": "flag-only-for-admin", + "value": false, + "reason": "DEFAULT", + "variant": "Default" + }, + { + "key": "new-admin-access", + "value": true, + "reason": "SPLIT", + "variant": "True" + }, + { + "key": "number-flag", + "value": 1, + "reason": "DEFAULT", + "variant": "Default" + }, + { + "key": "test-flag-rule-apply", + "value": { + "test": "test" + }, + "reason": "DEFAULT", + "variant": "Default" + }, + { + "key": "test-flag-rule-apply-false", + "value": { + "test": "test" + }, + "reason": "DEFAULT", + "variant": "Default" + }, + { + "key": "test-flag-rule-not-apply", + "value": { + "test": "test" + }, + "reason": "DEFAULT", + "variant": "Default" + } + ] +} diff --git a/cmd/relayproxy/testdata/ofrep/valid_request.json b/cmd/relayproxy/testdata/ofrep/valid_request.json new file mode 100644 index 00000000000..fcbb1bf90e0 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/valid_request.json @@ -0,0 +1,8 @@ +{ + "context": { + "company": "GO Feature Flag", + "firstname": "John", + "lastname": "Doe", + "targetingKey": "4f433951-4c8c-42b3-9f18-8c9a5ed8e9eb" + } +} \ No newline at end of file