diff --git a/file_internal_test.go b/file_internal_test.go index c8af811..547abeb 100644 --- a/file_internal_test.go +++ b/file_internal_test.go @@ -1,6 +1,7 @@ package fdk import ( + "fmt" "testing" "time" ) @@ -246,12 +247,24 @@ func TestNormalizeFile(t *testing.T) { } } -func EqualVals[T comparable](t testing.TB, want, got T) bool { +func EqualVals[T comparable](t testing.TB, want, got T, args ...any) bool { t.Helper() + var errMsg string + if len(args) > 0 { + format, ok := args[0].(string) + if ok { + errMsg = fmt.Sprintf(format, args[1:]...) + } + } + match := want == got if !match { - t.Errorf("values not equal:\n\t\twant:\t%#v\n\t\tgot:\t%#v", want, got) + msg := "values not equal:\n\twant:\t%#v\n\tgot:\t%#v" + if errMsg != "" { + msg += "\n\n\t" + errMsg + } + t.Errorf(msg, want, got) } return match } diff --git a/handler_fns.go b/handler_fns.go index 12457bf..0441db0 100644 --- a/handler_fns.go +++ b/handler_fns.go @@ -17,7 +17,7 @@ func (h HandlerFn) Handle(ctx context.Context, r Request) Response { // HandleFnOf provides a means to translate the incoming requests to the destination body type. // This normalizes the sad path and provides the caller with a zero fuss request to work with. Reducing // json boilerplate for what is essentially the same operation on different types. -func HandleFnOf[T any](fn func(context.Context, RequestOf[T]) Response) Handler { +func HandleFnOf[T any](fn func(ctx context.Context, r RequestOf[T]) Response) Handler { return HandlerFn(func(ctx context.Context, r Request) Response { var v T if err := json.NewDecoder(r.Body).Decode(&v); err != nil { @@ -25,6 +25,8 @@ func HandleFnOf[T any](fn func(context.Context, RequestOf[T]) Response) Handler } return fn(ctx, RequestOf[T]{ + FnID: r.FnID, + FnVersion: r.FnVersion, Body: v, Context: r.Context, Params: r.Params, @@ -39,7 +41,7 @@ func HandleFnOf[T any](fn func(context.Context, RequestOf[T]) Response) Handler // HandlerFnOfOK provides a means to translate the incoming requests to the destination body type // and execute validation on that type. This normalizes the sad path for both the unmarshalling of // the request body and the validation of that request type using its OK() method. -func HandlerFnOfOK[T interface{ OK() []APIError }](fn func(context.Context, RequestOf[T]) Response) Handler { +func HandlerFnOfOK[T interface{ OK() []APIError }](fn func(ctx context.Context, r RequestOf[T]) Response) Handler { return HandleFnOf(func(ctx context.Context, r RequestOf[T]) Response { if errs := r.Body.OK(); len(errs) > 0 { return ErrResp(errs...) @@ -71,7 +73,7 @@ type WorkflowCtx struct { // HandleWorkflow provides a means to create a handler with workflow integration. This function // does not have an opinion on the request body but does expect a workflow integration. Typically, // this is useful for DELETE/GET handlers. -func HandleWorkflow(fn func(context.Context, Request, WorkflowCtx) Response) Handler { +func HandleWorkflow(fn func(ctx context.Context, r Request, wrkCtx WorkflowCtx) Response) Handler { return HandlerFn(func(ctx context.Context, r Request) Response { var w WorkflowCtx if err := json.Unmarshal(r.Context, &w); err != nil { @@ -85,7 +87,7 @@ func HandleWorkflow(fn func(context.Context, Request, WorkflowCtx) Response) Han // HandleWorkflowOf provides a means to create a handler with Workflow integration. This // function is useful when you expect a request body and have workflow integrations. Typically, this // is with PATCH/POST/PUT handlers. -func HandleWorkflowOf[T any](fn func(context.Context, RequestOf[T], WorkflowCtx) Response) Handler { +func HandleWorkflowOf[T any](fn func(ctx context.Context, r RequestOf[T], wrkCtx WorkflowCtx) Response) Handler { return HandleWorkflow(func(ctx context.Context, r Request, workflowCtx WorkflowCtx) Response { next := HandleFnOf(func(ctx context.Context, r RequestOf[T]) Response { return fn(ctx, r, workflowCtx) diff --git a/handler_fns_test.go b/handler_fns_test.go new file mode 100644 index 0000000..a7cacc0 --- /dev/null +++ b/handler_fns_test.go @@ -0,0 +1,217 @@ +package fdk_test + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "strings" + "testing" + + fdk "github.com/CrowdStrike/foundry-fn-go" +) + +type testBody struct { + Name string `json:"name"` +} + +func (t testBody) OK() []fdk.APIError { + if t.Name == "fail" { + return []fdk.APIError{{Code: http.StatusBadRequest, Message: "got a fail"}} + } + return nil +} + +func TestHandlers(t *testing.T) { + mux := fdk.NewMux() + mux.Post("/handler-fn-of", fdk.HandleFnOf(func(ctx context.Context, r fdk.RequestOf[testBody]) fdk.Response { + return fdk.Response{Code: 200, Body: fdk.JSON(r)} + })) + mux.Post("/handle-fn-of-ok", fdk.HandlerFnOfOK(func(ctx context.Context, r fdk.RequestOf[testBody]) fdk.Response { + return fdk.Response{Code: 200, Body: fdk.JSON(r)} + })) + mux.Get("/handle-workflow", fdk.HandleWorkflow(func(ctx context.Context, r fdk.Request, wrkCtx fdk.WorkflowCtx) fdk.Response { + return fdk.Response{Code: 200, Body: fdk.JSON(r)} + })) + mux.Put("/handle-workflow-of", fdk.HandleWorkflowOf(func(ctx context.Context, r fdk.RequestOf[testBody], wrkCtx fdk.WorkflowCtx) fdk.Response { + return fdk.Response{Code: 200, Body: fdk.JSON(r)} + })) + + params := struct { + Header http.Header + Query url.Values + }{ + Header: http.Header{"X-Cs-Foo": []string{"header"}}, + Query: url.Values{"key": []string{"value"}}, + } + + wrkCtxVal, err := json.Marshal(fdk.WorkflowCtx{ + ActivityExecID: "act-exec-id", + AppID: "app-id", + CID: "cid", + OwnerCID: "owner-cid", + DefinitionID: "def-id", + DefinitionVersion: 9000, + ExecutionID: "exec-id", + }) + mustNoErr(t, err) + + t.Run("HandleFnOf", func(t *testing.T) { + resp := mux.Handle(context.TODO(), fdk.Request{ + FnID: "id1", + FnVersion: 1, + Body: strings.NewReader(`{"name":"frodo"}`), + Context: json.RawMessage(`{"some":"ctx"}`), + URL: "/handler-fn-of", + Params: params, + Method: "POST", + AccessToken: "access", + TraceID: "trace-id", + }) + gotStatusOK(t, resp) + + b, err := resp.Body.MarshalJSON() + mustNoErr(t, err) + + var got fdk.RequestOf[testBody] + mustNoErr(t, json.Unmarshal(b, &got)) + + fdk.EqualVals(t, "id1", got.FnID) + fdk.EqualVals(t, 1, got.FnVersion) + fdk.EqualVals(t, "/handler-fn-of", got.URL) + fdk.EqualVals(t, "POST", got.Method) + fdk.EqualVals(t, `{"some":"ctx"}`, string(got.Context)) + fdk.EqualVals(t, "header", got.Params.Header.Get("X-Cs-Foo")) + fdk.EqualVals(t, "value", got.Params.Query.Get("key")) + fdk.EqualVals(t, "access", got.AccessToken) + fdk.EqualVals(t, "trace-id", got.TraceID) + + wantFoo := testBody{Name: "frodo"} + fdk.EqualVals(t, wantFoo, got.Body) + }) + + t.Run("HandlerFnOfOK", func(t *testing.T) { + t.Run("with valid name", func(t *testing.T) { + resp := mux.Handle(context.TODO(), fdk.Request{ + FnID: "id1", + FnVersion: 1, + Body: strings.NewReader(`{"name":"frodo"}`), + Context: json.RawMessage(`{"some":"ctx"}`), + URL: "/handle-fn-of-ok", + Params: params, + Method: "POST", + AccessToken: "access", + TraceID: "trace-id", + }) + gotStatusOK(t, resp) + + b, err := resp.Body.MarshalJSON() + mustNoErr(t, err) + + var got fdk.RequestOf[testBody] + mustNoErr(t, json.Unmarshal(b, &got)) + + fdk.EqualVals(t, "id1", got.FnID) + fdk.EqualVals(t, 1, got.FnVersion) + fdk.EqualVals(t, "/handle-fn-of-ok", got.URL) + fdk.EqualVals(t, "POST", got.Method) + fdk.EqualVals(t, `{"some":"ctx"}`, string(got.Context)) + fdk.EqualVals(t, "header", got.Params.Header.Get("X-Cs-Foo")) + fdk.EqualVals(t, "value", got.Params.Query.Get("key")) + fdk.EqualVals(t, "access", got.AccessToken) + fdk.EqualVals(t, "trace-id", got.TraceID) + + wantFoo := testBody{Name: "frodo"} + fdk.EqualVals(t, wantFoo, got.Body) + }) + + t.Run("with invalid name", func(t *testing.T) { + resp := mux.Handle(context.TODO(), fdk.Request{ + FnID: "id1", + FnVersion: 1, + Body: strings.NewReader(`{"name":"fail"}`), + URL: "/handle-fn-of-ok", + Method: "POST", + }) + fdk.EqualVals(t, http.StatusBadRequest, resp.Code) + fdk.EqualVals(t, 1, len(resp.Errors), "got invalid errors: %s", resp.Errors) + fdk.EqualVals(t, fdk.APIError{Code: http.StatusBadRequest, Message: "got a fail"}, resp.Errors[0]) + }) + }) + + t.Run("HandleWorkflow", func(t *testing.T) { + resp := mux.Handle(context.TODO(), fdk.Request{ + FnID: "id1", + FnVersion: 1, + Context: wrkCtxVal, + URL: "/handle-workflow", + Params: params, + Method: "GET", + AccessToken: "access", + TraceID: "trace-id", + }) + gotStatusOK(t, resp) + + b, err := resp.Body.MarshalJSON() + mustNoErr(t, err) + + var got fdk.Request + mustNoErr(t, json.Unmarshal(b, &got)) + + fdk.EqualVals(t, "id1", got.FnID) + fdk.EqualVals(t, 1, got.FnVersion) + fdk.EqualVals(t, "/handle-workflow", got.URL) + fdk.EqualVals(t, "GET", got.Method) + fdk.EqualVals(t, string(wrkCtxVal), string(got.Context)) + fdk.EqualVals(t, "header", got.Params.Header.Get("X-Cs-Foo")) + fdk.EqualVals(t, "value", got.Params.Query.Get("key")) + fdk.EqualVals(t, "access", got.AccessToken) + fdk.EqualVals(t, "trace-id", got.TraceID) + }) + + t.Run("HandleWorkflowOf", func(t *testing.T) { + resp := mux.Handle(context.TODO(), fdk.Request{ + FnID: "id1", + FnVersion: 1, + Body: strings.NewReader(`{"name":"frodo"}`), + Context: wrkCtxVal, + URL: "/handle-workflow-of", + Params: params, + Method: "PUT", + AccessToken: "access", + TraceID: "trace-id", + }) + gotStatusOK(t, resp) + + b, err := resp.Body.MarshalJSON() + mustNoErr(t, err) + + var got fdk.RequestOf[testBody] + mustNoErr(t, json.Unmarshal(b, &got)) + + fdk.EqualVals(t, "id1", got.FnID) + fdk.EqualVals(t, 1, got.FnVersion) + fdk.EqualVals(t, "/handle-workflow-of", got.URL) + fdk.EqualVals(t, "PUT", got.Method) + fdk.EqualVals(t, string(wrkCtxVal), string(got.Context)) + fdk.EqualVals(t, "header", got.Params.Header.Get("X-Cs-Foo")) + fdk.EqualVals(t, "value", got.Params.Query.Get("key")) + fdk.EqualVals(t, "access", got.AccessToken) + fdk.EqualVals(t, "trace-id", got.TraceID) + + wantFoo := testBody{Name: "frodo"} + fdk.EqualVals(t, wantFoo, got.Body) + }) +} + +func gotStatusOK(t *testing.T, resp fdk.Response) { + t.Helper() + + b, _ := json.MarshalIndent(resp.Errors, "", " ") + equals := fdk.EqualVals(t, 0, len(resp.Errors), "errors encountered:\n"+string(b)) + equals = fdk.EqualVals(t, http.StatusOK, resp.Code, "status code is invalid") && equals + + if !equals { + t.FailNow() + } +}