From f0e9a45c3aa7852705b45937f5bd1a452e265bdc Mon Sep 17 00:00:00 2001 From: dawkaka Date: Wed, 10 Jul 2024 09:41:40 +0000 Subject: [PATCH] sub packages --- echo.go | 110 ---------- echo/echo.go | 136 +++++++++++++ echo_test.go => echo/echo_test.go | 57 +++--- errors_test.go | 116 ----------- fiber.go | 67 ------ fiber/fiber.go | 93 +++++++++ fiber_test.go => fiber/fiber_test.go | 80 +++----- gin.go | 88 -------- gin/gin.go | 116 +++++++++++ gin/gin_test.go | 291 +++++++++++++++++++++++++++ gin_test.go | 186 ----------------- native.go | 6 +- native_test.go | 34 ++-- outgoing.go | 4 +- outgoing_test.go | 8 +- sdk.go | 43 ++-- sdk_test.go | 87 +------- test_utils.go | 70 +++++++ 18 files changed, 824 insertions(+), 768 deletions(-) delete mode 100644 echo.go create mode 100644 echo/echo.go rename echo_test.go => echo/echo_test.go (68%) delete mode 100644 errors_test.go delete mode 100644 fiber.go create mode 100644 fiber/fiber.go rename fiber_test.go => fiber/fiber_test.go (63%) delete mode 100644 gin.go create mode 100644 gin/gin.go create mode 100644 gin/gin_test.go delete mode 100644 gin_test.go create mode 100644 test_utils.go diff --git a/echo.go b/echo.go deleted file mode 100644 index 829a23d..0000000 --- a/echo.go +++ /dev/null @@ -1,110 +0,0 @@ -package apitoolkit - -import ( - "bufio" - "bytes" - "context" - "errors" - "io" - "net" - "net/http" - "time" - - "github.com/google/uuid" - "github.com/labstack/echo/v4" -) - -// bodyDumpResponseWriter use to preserve the http response body during request processing -type echoBodyLogWriter struct { - io.Writer - http.ResponseWriter -} - -func (w *echoBodyLogWriter) WriteHeader(code int) { - w.ResponseWriter.WriteHeader(code) -} - -func (w *echoBodyLogWriter) Write(b []byte) (int, error) { - return w.Writer.Write(b) -} - -func (w *echoBodyLogWriter) Flush() { - w.ResponseWriter.(http.Flusher).Flush() -} - -func (w *echoBodyLogWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return w.ResponseWriter.(http.Hijacker).Hijack() -} - -// EchoMiddleware middleware for echo framework, collects requests, response and publishes the payload -func (c *Client) EchoMiddleware(next echo.HandlerFunc) echo.HandlerFunc { - return func(ctx echo.Context) (err error) { - // Register the client in the context, - // so it can be used for outgoing requests with little ceremony - ctx.Set(string(CurrentClient), c) - - msgID := uuid.Must(uuid.NewRandom()) - ctx.Set(string(CurrentRequestMessageID), msgID) - - errorList := []ATError{} - ctx.Set(string(ErrorListCtxKey), &errorList) - newCtx := context.WithValue(ctx.Request().Context(), ErrorListCtxKey, &errorList) - newCtx = context.WithValue(newCtx, CurrentRequestMessageID, msgID) - newCtx = context.WithValue(newCtx, CurrentClient, c) - ctx.SetRequest(ctx.Request().WithContext(newCtx)) - - var reqBuf []byte - // safely read request body - if ctx.Request().Body != nil { - reqBuf, _ = io.ReadAll(ctx.Request().Body) - } - ctx.Request().Body = io.NopCloser(bytes.NewBuffer(reqBuf)) - startTime := time.Now() - - // create a MultiWriter that streams the response body into resBody - resBody := new(bytes.Buffer) - mw := io.MultiWriter(ctx.Response().Writer, resBody) - writer := &echoBodyLogWriter{Writer: mw, ResponseWriter: ctx.Response().Writer} - ctx.Response().Writer = writer - pathParams := map[string]string{} - for _, paramName := range ctx.ParamNames() { - pathParams[paramName] = ctx.Param(paramName) - } - - defer func() { - if err := recover(); err != nil { - if _, ok := err.(error); !ok { - err = errors.New(err.(string)) - } - ReportError(ctx.Request().Context(), err.(error)) - payload := c.buildPayload(GoDefaultSDKType, startTime, - ctx.Request(), 500, - reqBuf, resBody.Bytes(), ctx.Response().Header().Clone(), - pathParams, ctx.Path(), - c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, - errorList, - msgID, - nil, - ) - c.PublishMessage(ctx.Request().Context(), payload) - panic(err) - } - }() - - // pass on request handling - err = next(ctx) - - // proceed post-response processing - payload := c.buildPayload(GoDefaultSDKType, startTime, - ctx.Request(), ctx.Response().Status, - reqBuf, resBody.Bytes(), ctx.Response().Header().Clone(), - pathParams, ctx.Path(), - c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, - errorList, - msgID, - nil, - ) - c.PublishMessage(ctx.Request().Context(), payload) - return err - } -} diff --git a/echo/echo.go b/echo/echo.go new file mode 100644 index 0000000..081f21b --- /dev/null +++ b/echo/echo.go @@ -0,0 +1,136 @@ +package apitoolkitecho + +import ( + "bufio" + "bytes" + "context" + "errors" + "io" + "net" + "net/http" + "time" + + apt "github.com/apitoolkit/apitoolkit-go" + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +// bodyDumpResponseWriter use to preserve the http response body during request processing +type echoBodyLogWriter struct { + io.Writer + http.ResponseWriter +} + +func (w *echoBodyLogWriter) WriteHeader(code int) { + w.ResponseWriter.WriteHeader(code) +} + +func (w *echoBodyLogWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +func (w *echoBodyLogWriter) Flush() { + w.ResponseWriter.(http.Flusher).Flush() +} + +func (w *echoBodyLogWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ResponseWriter.(http.Hijacker).Hijack() +} + +func NewClient(ctx context.Context, conf apt.Config) (*apt.Client, error) { + return apt.NewClient(ctx, conf) +} + +func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client { + return apt.HTTPClient(ctx, opts...) +} +func WithRedactHeaders(headers ...string) apt.RoundTripperOption { + return apt.WithRedactHeaders(headers...) +} +func WithRedactRequestBody(paths ...string) apt.RoundTripperOption { + return apt.WithRedactRequestBody(paths...) +} +func WithRedactResponseBody(paths ...string) apt.RoundTripperOption { + return apt.WithRedactResponseBody(paths...) +} + +func ReportError(ctx context.Context, err error) { + apt.ReportError(ctx, err) +} + +// EchoMiddleware middleware for echo framework, collects requests, response and publishes the payload +func EchoMiddleware(c *apt.Client) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) (err error) { + // Register the client in the context, + // so it can be used for outgoing requests with little ceremony + ctx.Set(string(apt.CurrentClient), c) + + msgID := uuid.Must(uuid.NewRandom()) + ctx.Set(string(apt.CurrentRequestMessageID), msgID) + + errorList := []apt.ATError{} + ctx.Set(string(apt.ErrorListCtxKey), &errorList) + newCtx := context.WithValue(ctx.Request().Context(), apt.ErrorListCtxKey, &errorList) + newCtx = context.WithValue(newCtx, apt.CurrentRequestMessageID, msgID) + newCtx = context.WithValue(newCtx, apt.CurrentClient, c) + ctx.SetRequest(ctx.Request().WithContext(newCtx)) + + var reqBuf []byte + // safely read request body + if ctx.Request().Body != nil { + reqBuf, _ = io.ReadAll(ctx.Request().Body) + } + ctx.Request().Body = io.NopCloser(bytes.NewBuffer(reqBuf)) + startTime := time.Now() + + // create a MultiWriter that streams the response body into resBody + resBody := new(bytes.Buffer) + mw := io.MultiWriter(ctx.Response().Writer, resBody) + writer := &echoBodyLogWriter{Writer: mw, ResponseWriter: ctx.Response().Writer} + ctx.Response().Writer = writer + pathParams := map[string]string{} + for _, paramName := range ctx.ParamNames() { + pathParams[paramName] = ctx.Param(paramName) + } + config := c.GetConfig() + + defer func() { + if err := recover(); err != nil { + if _, ok := err.(error); !ok { + err = errors.New(err.(string)) + } + apt.ReportError(ctx.Request().Context(), err.(error)) + payload := c.BuildPayload(apt.GoDefaultSDKType, startTime, + ctx.Request(), 500, + reqBuf, resBody.Bytes(), ctx.Response().Header().Clone(), + pathParams, ctx.Path(), + config.RedactHeaders, config.RedactRequestBody, config.RedactResponseBody, + errorList, + msgID, + nil, + ) + c.PublishMessage(ctx.Request().Context(), payload) + panic(err) + } + }() + + // pass on request handling + err = next(ctx) + + // proceed post-response processing + payload := c.BuildPayload(apt.GoDefaultSDKType, startTime, + ctx.Request(), ctx.Response().Status, + reqBuf, resBody.Bytes(), ctx.Response().Header().Clone(), + pathParams, ctx.Path(), + config.RedactHeaders, config.RedactRequestBody, config.RedactResponseBody, + errorList, + msgID, + nil, + ) + c.PublishMessage(ctx.Request().Context(), payload) + return err + } + + } +} diff --git a/echo_test.go b/echo/echo_test.go similarity index 68% rename from echo_test.go rename to echo/echo_test.go index 244cd5d..c5d62a8 100644 --- a/echo_test.go +++ b/echo/echo_test.go @@ -1,4 +1,4 @@ -package apitoolkit +package apitoolkitecho import ( "context" @@ -9,21 +9,21 @@ import ( "testing" "time" + apt "github.com/apitoolkit/apitoolkit-go" "github.com/imroc/req" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ) func TestEchoServerMiddleware(t *testing.T) { - client := &Client{ - config: &Config{ - RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, - }, - } + client := &apt.Client{} + client.SetConfig(&apt.Config{ + RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, + RedactResponseBody: apt.ExampleDataRedaction, + }) var publishCalled bool - client.PublishMessage = func(ctx context.Context, payload Payload) error { + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { assert.Equal(t, "POST", payload.Method) assert.Equal(t, "/:slug/test", payload.URLPath) assert.Equal(t, map[string]string{ @@ -48,10 +48,10 @@ func TestEchoServerMiddleware(t *testing.T) { assert.Equal(t, "/slug-value/test?param1=abc¶m2=123", payload.RawURL) assert.Equal(t, http.StatusAccepted, payload.StatusCode) assert.Greater(t, payload.Duration, 1000*time.Nanosecond) - assert.Equal(t, GoDefaultSDKType, payload.SdkType) + assert.Equal(t, apt.GoDefaultSDKType, payload.SdkType) - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) + reqData, _ := json.Marshal(apt.ExampleData2) + respData, _ := json.Marshal(apt.ExampleDataRedacted) assert.Equal(t, reqData, payload.RequestBody) assert.Equal(t, respData, payload.ResponseBody) @@ -60,30 +60,30 @@ func TestEchoServerMiddleware(t *testing.T) { } e := echo.New() - e.Use(client.EchoMiddleware) + e.Use(EchoMiddleware(client)) e.POST("/:slug/test", func(c echo.Context) (err error) { body, err := io.ReadAll(c.Request().Body) assert.NoError(t, err) assert.NotEmpty(t, body) - reqData, _ := json.Marshal(exampleData2) + reqData, _ := json.Marshal(apt.ExampleData2) assert.Equal(t, reqData, body) c.Response().Header().Set("Content-Type", "application/json") c.Response().Header().Set("X-API-KEY", "applicationKey") - c.JSON(http.StatusAccepted, exampleData) + c.JSON(http.StatusAccepted, apt.ExampleData) return }) ts := httptest.NewServer(e) defer ts.Close() - respData, _ := json.Marshal(exampleData) + respData, _ := json.Marshal(apt.ExampleData) resp, err := req.Post(ts.URL+"/slug-value/test", req.Param{"param1": "abc", "param2": 123}, req.Header{ "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(apt.ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) @@ -93,12 +93,11 @@ func TestEchoServerMiddleware(t *testing.T) { } func TestOutgoingRequestEcho(t *testing.T) { - client := &Client{ - config: &Config{}, - } + client := &apt.Client{} + client.SetConfig(&apt.Config{}) publishCalled := false var parentId *string - client.PublishMessage = func(ctx context.Context, payload Payload) error { + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { if payload.RawURL == "/from-gorilla" { assert.NotNil(t, payload.ParentID) parentId = payload.ParentID @@ -109,19 +108,21 @@ func TestOutgoingRequestEcho(t *testing.T) { return nil } router := echo.New() - router.Use(client.EchoMiddleware) + client.PublishMessage(context.Background(), apt.Payload{}) + router.Use(EchoMiddleware(client)) router.POST("/:slug/test", func(c echo.Context) (err error) { body, err := io.ReadAll(c.Request().Body) assert.NotEmpty(t, body) - reqData, _ := json.Marshal(exampleData2) + reqData, _ := json.Marshal(apt.ExampleData2) assert.Equal(t, reqData, body) - HTTPClient := http.DefaultClient - HTTPClient.Transport = client.WrapRoundTripper( - c.Request().Context(), HTTPClient.Transport, + hClient := HTTPClient(c.Request().Context(), + WithRedactHeaders("content-type", "Authorization", "HOST"), + WithRedactRequestBody("$.user.email", "$.user.addresses"), + WithRedactResponseBody("$.users[*].email", "$.users[*].credit_card"), ) - _, _ = HTTPClient.Get("http://localhost:3000/from-gorilla") + _, _ = hClient.Get("http://localhost:3000/from-gorilla") - c.JSON(http.StatusAccepted, exampleData) + c.JSON(http.StatusAccepted, apt.ExampleData) return }) ts := httptest.NewServer(router) @@ -133,7 +134,7 @@ func TestOutgoingRequestEcho(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(apt.ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) diff --git a/errors_test.go b/errors_test.go deleted file mode 100644 index 6350849..0000000 --- a/errors_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package apitoolkit - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gin-gonic/gin" - "github.com/imroc/req" - "github.com/stretchr/testify/assert" -) - -func TestErrorReporting(t *testing.T) { - client := &Client{ - config: &Config{ - RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, - }, - } - var publishCalled bool - client.PublishMessage = func(ctx context.Context, payload Payload) error { - // x, _ := json.MarshalIndent(payload, "", "\t") - // fmt.Println(string(x)) - assert.NotEmpty(t, payload.Errors) - assert.Equal(t, "wrapper from err2 Example Error value", payload.Errors[0].Message) - assert.Equal(t, "Example Error value", payload.Errors[0].RootErrorMessage) - assert.Equal(t, "*fmt.wrapError", payload.Errors[0].ErrorType) - assert.Equal(t, "*errors.errorString", payload.Errors[0].RootErrorType) - - assert.Equal(t, "POST", payload.Method) - assert.Equal(t, "/test", payload.URLPath) - publishCalled = true - return nil - } - - handlerFn := func(w http.ResponseWriter, r *http.Request) { - err1 := fmt.Errorf("Example Error %v", "value") - - w.WriteHeader(http.StatusAccepted) - w.Write([]byte(`{"key":"value"}`)) - - err2 := fmt.Errorf("wrapper from err2 %w", err1) - ReportError(r.Context(), err2) - } - - ts := httptest.NewServer(client.Middleware(http.HandlerFunc(handlerFn))) - defer ts.Close() - - outClient := &Client{ - config: &Config{}, - } - - outClient.PublishMessage = func(ctx context.Context, payload Payload) error { - assert.Equal(t, "/test?param1=abc¶m2=123", payload.RawURL) - assert.Equal(t, http.StatusAccepted, payload.StatusCode) - assert.Greater(t, payload.Duration, 1000*time.Nanosecond) - assert.Equal(t, GoOutgoing, payload.SdkType) - return nil - } - - _, err := req.Post(ts.URL+"/test", - req.Param{"param1": "abc", "param2": 123}, - req.Header{ - "Content-Type": "application/json", - "X-API-KEY": "past-3", - }, - req.BodyJSON(exampleData2), - ) - assert.NoError(t, err) - assert.True(t, publishCalled) -} - -func TestGinMiddlewareGETError(t *testing.T) { - gin.SetMode(gin.TestMode) - client := &Client{ - config: &Config{}, - } - var publishCalled bool - respData, _ := json.Marshal(exampleData) - client.PublishMessage = func(ctx context.Context, payload Payload) error { - publishCalled = true - return nil - } - router := gin.New() - router.Use(client.GinMiddleware) - - router.GET("/:slug/test", func(c *gin.Context) { - body, err := io.ReadAll(c.Request.Body) - assert.NoError(t, err) - assert.Equal(t, []byte{}, body) - - ReportError(c.Request.Context(), errors.New("Test Error")) - - c.Header("Content-Type", "application/json") - c.JSON(http.StatusAccepted, exampleData) - }) - - ts := httptest.NewServer(router) - defer ts.Close() - - resp, err := req.Get(ts.URL+"/slug-value/test", - req.QueryParam{"param1": "abc", "param2": 123}, - req.Header{ - "X-API-KEY": "past-3", - }, - ) - assert.NoError(t, err) - assert.True(t, publishCalled) - assert.Equal(t, respData, resp.Bytes()) -} diff --git a/fiber.go b/fiber.go deleted file mode 100644 index b99ec7a..0000000 --- a/fiber.go +++ /dev/null @@ -1,67 +0,0 @@ -package apitoolkit - -import ( - "context" - "errors" - "time" - - fiber "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -func (c *Client) FiberMiddleware(ctx *fiber.Ctx) error { - // Register the client in the context, - // so it can be used for outgoing requests with little ceremony - ctx.Locals(string(CurrentClient), c) - - msgID := uuid.Must(uuid.NewRandom()) - ctx.Locals(string(CurrentRequestMessageID), msgID) - errorList := []ATError{} - ctx.Locals(string(ErrorListCtxKey), &errorList) - ctx.Locals(CurrentClient, c) - newCtx := context.WithValue(ctx.Context(), ErrorListCtxKey, &errorList) - newCtx = context.WithValue(newCtx, CurrentClient, c) - newCtx = context.WithValue(newCtx, CurrentRequestMessageID, msgID) - ctx.SetUserContext(newCtx) - respHeaders := map[string][]string{} - for k, v := range ctx.GetRespHeaders() { - respHeaders[k] = v - } - - start := time.Now() - defer func() { - if err := recover(); err != nil { - if _, ok := err.(error); !ok { - err = errors.New(err.(string)) - } - ReportError(ctx.UserContext(), err.(error)) - payload := c.buildFastHTTPPayload(GoFiberSDKType, start, - ctx.Context(), 500, - ctx.Request().Body(), ctx.Response().Body(), respHeaders, - ctx.AllParams(), ctx.Route().Path, - c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, - errorList, - msgID, - nil, - string(ctx.Context().Referer()), - ) - c.PublishMessage(ctx.Context(), payload) - panic(err) - } - }() - - err := ctx.Next() - payload := c.buildFastHTTPPayload(GoFiberSDKType, start, - ctx.Context(), ctx.Response().StatusCode(), - ctx.Request().Body(), ctx.Response().Body(), respHeaders, - ctx.AllParams(), ctx.Route().Path, - c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, - errorList, - msgID, - nil, - string(ctx.Context().Referer()), - ) - - c.PublishMessage(ctx.Context(), payload) - return err -} diff --git a/fiber/fiber.go b/fiber/fiber.go new file mode 100644 index 0000000..4b0d2a8 --- /dev/null +++ b/fiber/fiber.go @@ -0,0 +1,93 @@ +package apitoolkitfiber + +import ( + "context" + "errors" + "net/http" + "time" + + apt "github.com/apitoolkit/apitoolkit-go" + fiber "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +func NewClient(ctx context.Context, conf apt.Config) (*apt.Client, error) { + return apt.NewClient(ctx, conf) +} + +func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client { + return apt.HTTPClient(ctx, opts...) +} + +func ReportError(ctx context.Context, err error) { + apt.ReportError(ctx, err) +} + +func WithRedactHeaders(headers ...string) apt.RoundTripperOption { + return apt.WithRedactHeaders(headers...) +} +func WithRedactRequestBody(paths ...string) apt.RoundTripperOption { + return apt.WithRedactRequestBody(paths...) +} +func WithRedactResponseBody(paths ...string) apt.RoundTripperOption { + return apt.WithRedactResponseBody(paths...) +} + +func FiberMiddleware(c *apt.Client) fiber.Handler { + return func(ctx *fiber.Ctx) error { + // Register the client in the context, + // so it can be used for outgoing requests with little ceremony + ctx.Locals(string(apt.CurrentClient), c) + + msgID := uuid.Must(uuid.NewRandom()) + ctx.Locals(string(apt.CurrentRequestMessageID), msgID) + errorList := []apt.ATError{} + ctx.Locals(string(apt.ErrorListCtxKey), &errorList) + ctx.Locals(apt.CurrentClient, c) + newCtx := context.WithValue(ctx.Context(), apt.ErrorListCtxKey, &errorList) + newCtx = context.WithValue(newCtx, apt.CurrentClient, c) + newCtx = context.WithValue(newCtx, apt.CurrentRequestMessageID, msgID) + ctx.SetUserContext(newCtx) + respHeaders := map[string][]string{} + for k, v := range ctx.GetRespHeaders() { + respHeaders[k] = v + } + start := time.Now() + config := c.GetConfig() + defer func() { + if err := recover(); err != nil { + if _, ok := err.(error); !ok { + err = errors.New(err.(string)) + } + apt.ReportError(ctx.UserContext(), err.(error)) + payload := c.BuildFastHTTPPayload(apt.GoFiberSDKType, start, + ctx.Context(), 500, + ctx.Request().Body(), ctx.Response().Body(), respHeaders, + ctx.AllParams(), ctx.Route().Path, + config.RedactHeaders, config.RedactRequestBody, config.RedactResponseBody, + errorList, + msgID, + nil, + string(ctx.Context().Referer()), + ) + c.PublishMessage(ctx.Context(), payload) + panic(err) + } + }() + + err := ctx.Next() + payload := c.BuildFastHTTPPayload(apt.GoFiberSDKType, start, + ctx.Context(), ctx.Response().StatusCode(), + ctx.Request().Body(), ctx.Response().Body(), respHeaders, + ctx.AllParams(), ctx.Route().Path, + config.RedactHeaders, config.RedactRequestBody, config.RedactResponseBody, + errorList, + msgID, + nil, + string(ctx.Context().Referer()), + ) + + c.PublishMessage(ctx.Context(), payload) + return err + } +} diff --git a/fiber_test.go b/fiber/fiber_test.go similarity index 63% rename from fiber_test.go rename to fiber/fiber_test.go index 9b6bca1..2211272 100644 --- a/fiber_test.go +++ b/fiber/fiber_test.go @@ -1,4 +1,4 @@ -package apitoolkit +package apitoolkitfiber import ( "bytes" @@ -10,20 +10,20 @@ import ( "testing" "time" + apt "github.com/apitoolkit/apitoolkit-go" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" ) func TestFiberMiddleware(t *testing.T) { - client := &Client{ - config: &Config{ - RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, - }, - } + client := &apt.Client{} + client.SetConfig(&apt.Config{ + RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, + RedactResponseBody: apt.ExampleDataRedaction, + }) var publishCalled bool - client.PublishMessage = func(ctx context.Context, payload Payload) error { + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { assert.Equal(t, "POST", payload.Method) assert.Equal(t, "/:slug/test", payload.URLPath) assert.Equal(t, map[string]string{ @@ -46,10 +46,10 @@ func TestFiberMiddleware(t *testing.T) { assert.Equal(t, "/slug-value/test?param1=abc¶m2=123", payload.RawURL) assert.Equal(t, http.StatusAccepted, payload.StatusCode) assert.Greater(t, payload.Duration, 1000*time.Nanosecond) - assert.Equal(t, GoFiberSDKType, payload.SdkType) + assert.Equal(t, apt.GoFiberSDKType, payload.SdkType) - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) + reqData, _ := json.Marshal(apt.ExampleData2) + respData, _ := json.Marshal(apt.ExampleDataRedacted) assert.Equal(t, reqData, payload.RequestBody) assert.Equal(t, respData, payload.ResponseBody) @@ -58,33 +58,33 @@ func TestFiberMiddleware(t *testing.T) { } router := fiber.New() - router.Use(client.FiberMiddleware) + router.Use(FiberMiddleware(client)) router.Post("/:slug/test", func(c *fiber.Ctx) error { body := c.Request().Body() assert.NotEmpty(t, body) - reqData, _ := json.Marshal(exampleData2) + reqData, _ := json.Marshal(apt.ExampleData2) assert.Equal(t, reqData, body) c.Set("Content-Type", "application/json") c.Set("X-API-KEY", "applicationKey") - return c.Status(http.StatusAccepted).JSON(exampleData) + return c.Status(http.StatusAccepted).JSON(apt.ExampleData) }) // ts := httptest.NewServer(router.Server()) // defer ts.Close() - respData, _ := json.Marshal(exampleData) + respData, _ := json.Marshal(apt.ExampleData) // resp, err := req.Post(ts.URL+"/slug-value/test", // req.Param{"param1": "abc", "param2": 123}, // req.Header{ // "Content-Type": "application/json", // "X-API-KEY": "past-3", // }, - // req.BodyJSON(exampleData2), + // req.BodyJSON(apt.ExampleData2), // ) - reqData, _ := json.Marshal(exampleData2) + reqData, _ := json.Marshal(apt.ExampleData2) req := httptest.NewRequest("POST", "/slug-value/test?param1=abc¶m2=123", bytes.NewReader(reqData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-API-KEY", "past-3") @@ -97,12 +97,11 @@ func TestFiberMiddleware(t *testing.T) { } func TestOutgoingRequestFiber(t *testing.T) { - client := &Client{ - config: &Config{}, - } + client := &apt.Client{} + client.SetConfig(&apt.Config{}) publishCalled := false var parentId *string - client.PublishMessage = func(ctx context.Context, payload Payload) error { + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { if payload.RawURL == "/from-gorilla" { assert.NotNil(t, payload.ParentID) parentId = payload.ParentID @@ -113,11 +112,11 @@ func TestOutgoingRequestFiber(t *testing.T) { return nil } router := fiber.New() - router.Use(client.FiberMiddleware) + router.Use(FiberMiddleware(client)) router.Post("/:slug/test", func(c *fiber.Ctx) error { body := c.Request().Body() assert.NotEmpty(t, body) - reqData, _ := json.Marshal(exampleData2) + reqData, _ := json.Marshal(apt.ExampleData2) assert.Equal(t, reqData, body) HTTPClient := http.DefaultClient HTTPClient.Transport = client.WrapRoundTripper( @@ -128,10 +127,10 @@ func TestOutgoingRequestFiber(t *testing.T) { c.Append("Content-Type", "application/json") c.Append("X-API-KEY", "applicationKey") - return c.Status(http.StatusAccepted).JSON(exampleData) + return c.Status(http.StatusAccepted).JSON(apt.ExampleData) }) - reqData, _ := json.Marshal(exampleData2) + reqData, _ := json.Marshal(apt.ExampleData2) ts := httptest.NewRequest("POST", "/slug-value/test?param1=abc¶m2=123", bytes.NewReader(reqData)) ts.Header.Set("Content-Type", "application/json") ts.Header.Set("X-API-KEY", "past-3") @@ -140,34 +139,3 @@ func TestOutgoingRequestFiber(t *testing.T) { assert.NoError(t, err) assert.True(t, publishCalled) } - -// func TestFiberMiddlewareGET(t *testing.T) { -// client := &Client{ -// config: &Config{}, -// } -// var publishCalled bool -// respData, _ := json.Marshal(exampleData) - -// client.PublishMessage = func(ctx context.Context, payload Payload) error { -// // ... rest of the assertions remain same as in Gin test -// publishCalled = true -// return nil -// } - -// app := fiber.New() -// app.Use(client.FiberMiddleware) -// app.Get("/:slug/test", func(ctx *fiber.Ctx) error { -// body := ctx.Body() -// assert.Empty(t, body) -// ctx.Set("Content-Type", "application/json") -// return ctx.Status(http.StatusAccepted).JSON(exampleData) -// }) - -// ts := httptest.NewRequest("GET", "/slug-value/test", nil) -// ts.Header.Set("X-API-KEY", "past-3") - -// resp, err := app.Test(ts) -// assert.NoError(t, err) -// assert.True(t, publishCalled) -// assert.Equal(t, respData, utils.GetBody(resp)) -// } diff --git a/gin.go b/gin.go deleted file mode 100644 index b8408d5..0000000 --- a/gin.go +++ /dev/null @@ -1,88 +0,0 @@ -package apitoolkit - -import ( - "bytes" - "context" - "errors" - "io" - "time" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -type ginBodyLogWriter struct { - gin.ResponseWriter - body *bytes.Buffer -} - -func (w *ginBodyLogWriter) Write(b []byte) (int, error) { - w.body.Write(b) - return w.ResponseWriter.Write(b) -} - -func (w *ginBodyLogWriter) WriteString(s string) (int, error) { - w.body.WriteString(s) - return w.ResponseWriter.WriteString(s) -} - -func (c *Client) GinMiddleware(ctx *gin.Context) { - // Register the client in the context, - // so it can be used for outgoing requests with little ceremony - ctx.Set(string(CurrentClient), c) - - msgID := uuid.Must(uuid.NewRandom()) - ctx.Set(string(CurrentRequestMessageID), msgID) - errorList := []ATError{} - ctx.Set(string(ErrorListCtxKey), &errorList) - newCtx := context.WithValue(ctx.Request.Context(), ErrorListCtxKey, &errorList) - newCtx = context.WithValue(newCtx, CurrentClient, c) - newCtx = context.WithValue(newCtx, CurrentRequestMessageID, msgID) - ctx.Request = ctx.Request.WithContext(newCtx) - - start := time.Now() - reqByteBody, _ := io.ReadAll(ctx.Request.Body) - ctx.Request.Body = io.NopCloser(bytes.NewBuffer(reqByteBody)) - - blw := &ginBodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: ctx.Writer} - ctx.Writer = blw - - pathParams := map[string]string{} - for _, param := range ctx.Params { - pathParams[param.Key] = param.Value - } - - defer func() { - if err := recover(); err != nil { - if _, ok := err.(error); !ok { - err = errors.New(err.(string)) - } - ReportError(ctx.Request.Context(), err.(error)) - payload := c.buildPayload(GoGinSDKType, start, - ctx.Request, 500, - reqByteBody, blw.body.Bytes(), ctx.Writer.Header().Clone(), - pathParams, ctx.FullPath(), - c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, - errorList, - msgID, - nil, - ) - c.PublishMessage(ctx, payload) - panic(err) - } - }() - - ctx.Next() - - payload := c.buildPayload(GoGinSDKType, start, - ctx.Request, ctx.Writer.Status(), - reqByteBody, blw.body.Bytes(), ctx.Writer.Header().Clone(), - pathParams, ctx.FullPath(), - c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, - errorList, - msgID, - nil, - ) - - c.PublishMessage(ctx, payload) -} diff --git a/gin/gin.go b/gin/gin.go new file mode 100644 index 0000000..e6e801b --- /dev/null +++ b/gin/gin.go @@ -0,0 +1,116 @@ +package apitoolkitgin + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "time" + + apt "github.com/apitoolkit/apitoolkit-go" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type ginBodyLogWriter struct { + gin.ResponseWriter + body *bytes.Buffer +} + +func (w *ginBodyLogWriter) Write(b []byte) (int, error) { + w.body.Write(b) + return w.ResponseWriter.Write(b) +} + +func (w *ginBodyLogWriter) WriteString(s string) (int, error) { + w.body.WriteString(s) + return w.ResponseWriter.WriteString(s) +} + +func NewClient(ctx context.Context, conf apt.Config) (*apt.Client, error) { + return apt.NewClient(ctx, conf) +} + +func HTTPClient(ctx context.Context, opts ...apt.RoundTripperOption) *http.Client { + return apt.HTTPClient(ctx, opts...) +} + +func ReportError(ctx context.Context, err error) { + apt.ReportError(ctx, err) +} + +func WithRedactHeaders(headers ...string) apt.RoundTripperOption { + return apt.WithRedactHeaders(headers...) +} +func WithRedactRequestBody(paths ...string) apt.RoundTripperOption { + return apt.WithRedactRequestBody(paths...) +} +func WithRedactResponseBody(paths ...string) apt.RoundTripperOption { + return apt.WithRedactResponseBody(paths...) +} + +func GinMiddleware(c *apt.Client) gin.HandlerFunc { + return func(ctx *gin.Context) { + // Register the client in the context, + // so it can be used for outgoing requests with little ceremony + ctx.Set(string(apt.CurrentClient), c) + + msgID := uuid.Must(uuid.NewRandom()) + ctx.Set(string(apt.CurrentRequestMessageID), msgID) + errorList := []apt.ATError{} + ctx.Set(string(apt.ErrorListCtxKey), &errorList) + newCtx := context.WithValue(ctx.Request.Context(), apt.ErrorListCtxKey, &errorList) + newCtx = context.WithValue(newCtx, apt.CurrentClient, c) + newCtx = context.WithValue(newCtx, apt.CurrentRequestMessageID, msgID) + ctx.Request = ctx.Request.WithContext(newCtx) + + start := time.Now() + reqByteBody, _ := io.ReadAll(ctx.Request.Body) + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(reqByteBody)) + + blw := &ginBodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: ctx.Writer} + ctx.Writer = blw + + pathParams := map[string]string{} + for _, param := range ctx.Params { + pathParams[param.Key] = param.Value + } + config := c.GetConfig() + + defer func() { + if err := recover(); err != nil { + if _, ok := err.(error); !ok { + err = errors.New(err.(string)) + } + apt.ReportError(ctx.Request.Context(), err.(error)) + payload := c.BuildPayload(apt.GoGinSDKType, start, + ctx.Request, 500, + reqByteBody, blw.body.Bytes(), ctx.Writer.Header().Clone(), + pathParams, ctx.FullPath(), + config.RedactHeaders, config.RedactRequestBody, config.RedactResponseBody, + errorList, + msgID, + nil, + ) + c.PublishMessage(ctx, payload) + panic(err) + } + }() + + ctx.Next() + + payload := c.BuildPayload(apt.GoGinSDKType, start, + ctx.Request, ctx.Writer.Status(), + reqByteBody, blw.body.Bytes(), ctx.Writer.Header().Clone(), + pathParams, ctx.FullPath(), + config.RedactHeaders, config.RedactRequestBody, config.RedactResponseBody, + errorList, + msgID, + nil, + ) + + c.PublishMessage(ctx, payload) + + } +} diff --git a/gin/gin_test.go b/gin/gin_test.go new file mode 100644 index 0000000..641753d --- /dev/null +++ b/gin/gin_test.go @@ -0,0 +1,291 @@ +package apitoolkitgin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + apt "github.com/apitoolkit/apitoolkit-go" + "github.com/gin-gonic/gin" + "github.com/imroc/req" + "github.com/stretchr/testify/assert" +) + +func TestGinMiddleware(t *testing.T) { + gin.SetMode(gin.TestMode) + + client := &apt.Client{} + client.SetConfig(&apt.Config{ + RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, + RedactResponseBody: apt.ExampleDataRedaction, + }) + + var publishCalled bool + + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { + assert.Equal(t, "POST", payload.Method) + assert.Equal(t, "/:slug/test", payload.URLPath) + assert.Equal(t, map[string]string{ + "slug": "slug-value", + }, payload.PathParams) + assert.Equal(t, map[string][]string{ + "param1": {"abc"}, + "param2": {"123"}, + }, payload.QueryParams) + + assert.Equal(t, map[string][]string{ + "Accept-Encoding": {"gzip"}, + "Content-Length": {"437"}, + "Content-Type": {"application/json"}, + "User-Agent": {"Go-http-client/1.1"}, + "X-Api-Key": {"past-3"}, + }, payload.RequestHeaders) + assert.Equal(t, map[string][]string{ + "Content-Type": {"application/json"}, + "X-Api-Key": {"applicationKey"}, + }, payload.ResponseHeaders) + assert.Equal(t, "/slug-value/test?param1=abc¶m2=123", payload.RawURL) + assert.Equal(t, http.StatusAccepted, payload.StatusCode) + assert.Greater(t, payload.Duration, 1000*time.Nanosecond) + assert.Equal(t, apt.GoGinSDKType, payload.SdkType) + + reqData, _ := json.Marshal(apt.ExampleData2) + respData, _ := json.Marshal(apt.ExampleDataRedacted) + assert.Equal(t, reqData, payload.RequestBody) + assert.Equal(t, respData, payload.ResponseBody) + + publishCalled = true + return nil + } + + router := gin.New() + router.Use(GinMiddleware(client)) + router.POST("/:slug/test", func(c *gin.Context) { + body, err := io.ReadAll(c.Request.Body) + assert.NoError(t, err) + assert.NotEmpty(t, body) + reqData, _ := json.Marshal(apt.ExampleData2) + assert.Equal(t, reqData, body) + c.Header("Content-Type", "application/json") + c.Header("X-API-KEY", "applicationKey") + c.JSON(http.StatusAccepted, apt.ExampleData) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + respData, _ := json.Marshal(apt.ExampleData) + resp, err := req.Post(ts.URL+"/slug-value/test", + req.Param{"param1": "abc", "param2": 123}, + req.Header{ + "Content-Type": "application/json", + "X-API-KEY": "past-3", + }, + req.BodyJSON(apt.ExampleData2), + ) + assert.NoError(t, err) + assert.True(t, publishCalled) + assert.Equal(t, respData, resp.Bytes()) +} + +func TestGinMiddlewareGET(t *testing.T) { + gin.SetMode(gin.TestMode) + client := &apt.Client{} + client.SetConfig(&apt.Config{}) + + var publishCalled bool + respData, _ := json.Marshal(apt.ExampleData) + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { + assert.Equal(t, "GET", payload.Method) + assert.Equal(t, "/:slug/test", payload.URLPath) + assert.Equal(t, map[string]string{ + "slug": "slug-value", + }, payload.PathParams) + assert.Equal(t, map[string][]string{ + "param1": {"abc"}, + "param2": {"123"}, + }, payload.QueryParams) + assert.Equal(t, map[string][]string{ + "Accept-Encoding": {"gzip"}, + "User-Agent": {"Go-http-client/1.1"}, + "X-Api-Key": {"past-3"}, + }, payload.RequestHeaders) + assert.Equal(t, map[string][]string{ + "Content-Type": {"application/json"}, + }, payload.ResponseHeaders) + assert.Equal(t, "/slug-value/test?param1=abc¶m2=123", payload.RawURL) + assert.Equal(t, http.StatusAccepted, payload.StatusCode) + assert.Greater(t, payload.Duration, 1000*time.Nanosecond) + assert.Equal(t, []byte{0x6e, 0x75, 0x6c, 0x6c}, payload.RequestBody) + assert.Equal(t, respData, payload.ResponseBody) + assert.Equal(t, apt.GoGinSDKType, payload.SdkType) + publishCalled = true + return nil + } + router := gin.New() + router.Use(GinMiddleware(client)) + + router.GET("/:slug/test", func(c *gin.Context) { + body, err := io.ReadAll(c.Request.Body) + assert.NoError(t, err) + assert.Equal(t, []byte{}, body) + + c.Header("Content-Type", "application/json") + c.JSON(http.StatusAccepted, apt.ExampleData) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + resp, err := req.Get(ts.URL+"/slug-value/test", + req.QueryParam{"param1": "abc", "param2": 123}, + req.Header{ + "X-API-KEY": "past-3", + }, + ) + assert.NoError(t, err) + assert.True(t, publishCalled) + assert.Equal(t, respData, resp.Bytes()) +} + +func TestOutgoingRequestGin(t *testing.T) { + gin.SetMode(gin.TestMode) + client := &apt.Client{} + client.SetConfig(&apt.Config{}) + + var publishCalled bool + router := gin.New() + router.Use(GinMiddleware(client)) + var parentId *string + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { + if payload.RawURL == "/from-gorilla" { + assert.NotNil(t, payload.ParentID) + parentId = payload.ParentID + } else if payload.URLPath == "/:slug/test" { + assert.Equal(t, *parentId, payload.MsgID) + } + publishCalled = true + return nil + } + router.GET("/:slug/test", func(c *gin.Context) { + hClient := apt.HTTPClient(c.Request.Context(), + WithRedactHeaders("X-API-KEY"), + WithRedactRequestBody("$.password"), + WithRedactResponseBody("$.account_data.account_id"), + ) + _, _ = hClient.Get("http://localhost:3000/from-gorilla") + + c.JSON(http.StatusAccepted, gin.H{"hello": "world"}) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + _, err := req.Get(ts.URL + "/slug-value/test") + assert.NoError(t, err) + assert.True(t, publishCalled) +} + +func TestErrorReporting(t *testing.T) { + client := &apt.Client{} + client.SetConfig(&apt.Config{ + RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, + RedactResponseBody: apt.ExampleDataRedaction, + }) + + var publishCalled bool + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { + // x, _ := json.MarshalIndent(payload, "", "\t") + // fmt.Println(string(x)) + assert.NotEmpty(t, payload.Errors) + assert.Equal(t, "wrapper from err2 Example Error value", payload.Errors[0].Message) + assert.Equal(t, "Example Error value", payload.Errors[0].RootErrorMessage) + assert.Equal(t, "*fmt.wrapError", payload.Errors[0].ErrorType) + assert.Equal(t, "*errors.errorString", payload.Errors[0].RootErrorType) + + assert.Equal(t, "POST", payload.Method) + assert.Equal(t, "/test", payload.URLPath) + publishCalled = true + return nil + } + + handlerFn := func(w http.ResponseWriter, r *http.Request) { + err1 := fmt.Errorf("Example Error %v", "value") + + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(`{"key":"value"}`)) + + err2 := fmt.Errorf("wrapper from err2 %w", err1) + ReportError(r.Context(), err2) + } + + ts := httptest.NewServer(client.Middleware(http.HandlerFunc(handlerFn))) + defer ts.Close() + + outClient := &apt.Client{} + outClient.SetConfig(&apt.Config{}) + + outClient.PublishMessage = func(ctx context.Context, payload apt.Payload) error { + assert.Equal(t, "/test?param1=abc¶m2=123", payload.RawURL) + assert.Equal(t, http.StatusAccepted, payload.StatusCode) + assert.Greater(t, payload.Duration, 1000*time.Nanosecond) + assert.Equal(t, apt.GoOutgoing, payload.SdkType) + return nil + } + + _, err := req.Post(ts.URL+"/test", + req.Param{"param1": "abc", "param2": 123}, + req.Header{ + "Content-Type": "application/json", + "X-API-KEY": "past-3", + }, + req.BodyJSON(apt.ExampleData2), + ) + assert.NoError(t, err) + assert.True(t, publishCalled) +} + +func TestGinMiddlewareGETError(t *testing.T) { + gin.SetMode(gin.TestMode) + client := &apt.Client{} + client.SetConfig(&apt.Config{}) + + var publishCalled bool + respData, _ := json.Marshal(apt.ExampleData) + client.PublishMessage = func(ctx context.Context, payload apt.Payload) error { + publishCalled = true + return nil + } + router := gin.New() + router.Use(GinMiddleware(client)) + + router.GET("/:slug/test", func(c *gin.Context) { + body, err := io.ReadAll(c.Request.Body) + assert.NoError(t, err) + assert.Equal(t, []byte{}, body) + + ReportError(c.Request.Context(), errors.New("Test Error")) + + c.Header("Content-Type", "application/json") + c.JSON(http.StatusAccepted, apt.ExampleData) + }) + + ts := httptest.NewServer(router) + defer ts.Close() + + resp, err := req.Get(ts.URL+"/slug-value/test", + req.QueryParam{"param1": "abc", "param2": 123}, + req.Header{ + "X-API-KEY": "past-3", + }, + ) + assert.NoError(t, err) + assert.True(t, publishCalled) + assert.Equal(t, respData, resp.Bytes()) +} diff --git a/gin_test.go b/gin_test.go deleted file mode 100644 index 627478b..0000000 --- a/gin_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package apitoolkit - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gin-gonic/gin" - "github.com/imroc/req" - "github.com/stretchr/testify/assert" -) - -func TestGinMiddleware(t *testing.T) { - gin.SetMode(gin.TestMode) - - client := &Client{ - config: &Config{ - RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, - }, - } - var publishCalled bool - - client.PublishMessage = func(ctx context.Context, payload Payload) error { - assert.Equal(t, "POST", payload.Method) - assert.Equal(t, "/:slug/test", payload.URLPath) - assert.Equal(t, map[string]string{ - "slug": "slug-value", - }, payload.PathParams) - assert.Equal(t, map[string][]string{ - "param1": {"abc"}, - "param2": {"123"}, - }, payload.QueryParams) - - assert.Equal(t, map[string][]string{ - "Accept-Encoding": {"gzip"}, - "Content-Length": {"437"}, - "Content-Type": {"application/json"}, - "User-Agent": {"Go-http-client/1.1"}, - "X-Api-Key": {"past-3"}, - }, payload.RequestHeaders) - assert.Equal(t, map[string][]string{ - "Content-Type": {"application/json"}, - "X-Api-Key": {"applicationKey"}, - }, payload.ResponseHeaders) - assert.Equal(t, "/slug-value/test?param1=abc¶m2=123", payload.RawURL) - assert.Equal(t, http.StatusAccepted, payload.StatusCode) - assert.Greater(t, payload.Duration, 1000*time.Nanosecond) - assert.Equal(t, GoGinSDKType, payload.SdkType) - - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) - assert.Equal(t, reqData, payload.RequestBody) - assert.Equal(t, respData, payload.ResponseBody) - - publishCalled = true - return nil - } - - router := gin.New() - router.Use(client.GinMiddleware) - router.POST("/:slug/test", func(c *gin.Context) { - body, err := io.ReadAll(c.Request.Body) - assert.NoError(t, err) - assert.NotEmpty(t, body) - reqData, _ := json.Marshal(exampleData2) - assert.Equal(t, reqData, body) - c.Header("Content-Type", "application/json") - c.Header("X-API-KEY", "applicationKey") - c.JSON(http.StatusAccepted, exampleData) - }) - - ts := httptest.NewServer(router) - defer ts.Close() - - respData, _ := json.Marshal(exampleData) - resp, err := req.Post(ts.URL+"/slug-value/test", - req.Param{"param1": "abc", "param2": 123}, - req.Header{ - "Content-Type": "application/json", - "X-API-KEY": "past-3", - }, - req.BodyJSON(exampleData2), - ) - assert.NoError(t, err) - assert.True(t, publishCalled) - assert.Equal(t, respData, resp.Bytes()) -} - -func TestGinMiddlewareGET(t *testing.T) { - gin.SetMode(gin.TestMode) - client := &Client{ - config: &Config{}, - } - var publishCalled bool - respData, _ := json.Marshal(exampleData) - client.PublishMessage = func(ctx context.Context, payload Payload) error { - assert.Equal(t, "GET", payload.Method) - assert.Equal(t, "/:slug/test", payload.URLPath) - assert.Equal(t, map[string]string{ - "slug": "slug-value", - }, payload.PathParams) - assert.Equal(t, map[string][]string{ - "param1": {"abc"}, - "param2": {"123"}, - }, payload.QueryParams) - assert.Equal(t, map[string][]string{ - "Accept-Encoding": {"gzip"}, - "User-Agent": {"Go-http-client/1.1"}, - "X-Api-Key": {"past-3"}, - }, payload.RequestHeaders) - assert.Equal(t, map[string][]string{ - "Content-Type": {"application/json"}, - }, payload.ResponseHeaders) - assert.Equal(t, "/slug-value/test?param1=abc¶m2=123", payload.RawURL) - assert.Equal(t, http.StatusAccepted, payload.StatusCode) - assert.Greater(t, payload.Duration, 1000*time.Nanosecond) - assert.Equal(t, []byte{0x6e, 0x75, 0x6c, 0x6c}, payload.RequestBody) - assert.Equal(t, respData, payload.ResponseBody) - assert.Equal(t, GoGinSDKType, payload.SdkType) - publishCalled = true - return nil - } - router := gin.New() - router.Use(client.GinMiddleware) - - router.GET("/:slug/test", func(c *gin.Context) { - body, err := io.ReadAll(c.Request.Body) - assert.NoError(t, err) - assert.Equal(t, []byte{}, body) - - c.Header("Content-Type", "application/json") - c.JSON(http.StatusAccepted, exampleData) - }) - - ts := httptest.NewServer(router) - defer ts.Close() - - resp, err := req.Get(ts.URL+"/slug-value/test", - req.QueryParam{"param1": "abc", "param2": 123}, - req.Header{ - "X-API-KEY": "past-3", - }, - ) - assert.NoError(t, err) - assert.True(t, publishCalled) - assert.Equal(t, respData, resp.Bytes()) -} - -func TestOutgoingRequestGin(t *testing.T) { - gin.SetMode(gin.TestMode) - client := &Client{ - config: &Config{}, - } - var publishCalled bool - router := gin.New() - router.Use(client.GinMiddleware) - var parentId *string - client.PublishMessage = func(ctx context.Context, payload Payload) error { - if payload.RawURL == "/from-gorilla" { - assert.NotNil(t, payload.ParentID) - parentId = payload.ParentID - } else if payload.URLPath == "/:slug/test" { - assert.Equal(t, *parentId, payload.MsgID) - } - publishCalled = true - return nil - } - router.GET("/:slug/test", func(c *gin.Context) { - hClient := HTTPClient(c.Request.Context()) - _, _ = hClient.Get("http://localhost:3000/from-gorilla") - - c.JSON(http.StatusAccepted, gin.H{"hello": "world"}) - }) - - ts := httptest.NewServer(router) - defer ts.Close() - - _, err := req.Get(ts.URL + "/slug-value/test") - assert.NoError(t, err) - assert.True(t, publishCalled) -} diff --git a/native.go b/native.go index 8d90815..a18a450 100644 --- a/native.go +++ b/native.go @@ -44,7 +44,7 @@ func (c *Client) Middleware(next http.Handler) http.Handler { res.WriteHeader(recRes.StatusCode) res.Write(resBody) - payload := c.buildPayload(GoDefaultSDKType, start, + payload := c.BuildPayload(GoDefaultSDKType, start, req, recRes.StatusCode, reqBuf, resBody, recRes.Header, nil, req.URL.Path, c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, @@ -90,7 +90,7 @@ func (c *Client) GorillaMuxMiddleware(next http.Handler) http.Handler { pathTmpl, _ := route.GetPathTemplate() vars := mux.Vars(req) - payload := c.buildPayload(GoGorillaMux, start, + payload := c.BuildPayload(GoGorillaMux, start, req, recRes.StatusCode, reqBuf, resBody, recRes.Header, vars, pathTmpl, c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, @@ -144,7 +144,7 @@ func (c *Client) ChiMiddleware(next http.Handler) http.Handler { } } - payload := c.buildPayload(GoGorillaMux, start, + payload := c.BuildPayload(GoGorillaMux, start, req, recRes.StatusCode, reqBuf, resBody, recRes.Header, vars, chiCtx.RoutePattern(), c.config.RedactHeaders, c.config.RedactRequestBody, c.config.RedactResponseBody, diff --git a/native_test.go b/native_test.go index dcab19e..c10fd03 100644 --- a/native_test.go +++ b/native_test.go @@ -20,7 +20,7 @@ func TestNativeGoMiddleware(t *testing.T) { client := &Client{ config: &Config{ RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, }, } var publishCalled bool @@ -49,8 +49,8 @@ func TestNativeGoMiddleware(t *testing.T) { assert.Greater(t, payload.Duration, 1000*time.Nanosecond) assert.Equal(t, GoDefaultSDKType, payload.SdkType) - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) + reqData, _ := json.Marshal(ExampleData2) + respData, _ := json.Marshal(ExampleDataRedacted) assert.Equal(t, reqData, payload.RequestBody) assert.Equal(t, respData, payload.ResponseBody) @@ -64,7 +64,7 @@ func TestNativeGoMiddleware(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, body) - jsonByte, err := json.Marshal(exampleData) + jsonByte, err := json.Marshal(ExampleData) assert.NoError(t, err) w.Header().Add("Content-Type", "application/json") @@ -82,7 +82,7 @@ func TestNativeGoMiddleware(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) @@ -94,7 +94,7 @@ func TestGorillaGoMiddleware(t *testing.T) { Debug: true, VerboseDebug: true, RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, }, } var publishCalled bool @@ -123,8 +123,8 @@ func TestGorillaGoMiddleware(t *testing.T) { assert.Greater(t, payload.Duration, 1000*time.Nanosecond) assert.Equal(t, GoGorillaMux, payload.SdkType) - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) + reqData, _ := json.Marshal(ExampleData2) + respData, _ := json.Marshal(ExampleDataRedacted) assert.Equal(t, reqData, payload.RequestBody) assert.Equal(t, respData, payload.ResponseBody) @@ -138,7 +138,7 @@ func TestGorillaGoMiddleware(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, body) - jsonByte, err := json.Marshal(exampleData) + jsonByte, err := json.Marshal(ExampleData) assert.NoError(t, err) w.Header().Add("Content-Type", "application/json") @@ -160,7 +160,7 @@ func TestGorillaGoMiddleware(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) @@ -172,7 +172,7 @@ func TestOutgoingRequestGorilla(t *testing.T) { Debug: true, VerboseDebug: true, RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, }, } var publishCalled bool @@ -211,7 +211,7 @@ func TestOutgoingRequestGorilla(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) @@ -223,7 +223,7 @@ func TestChiMiddleware(t *testing.T) { Debug: true, VerboseDebug: true, RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, }, } var publishCalled bool @@ -252,8 +252,8 @@ func TestChiMiddleware(t *testing.T) { assert.Greater(t, payload.Duration, 1000*time.Nanosecond) assert.Equal(t, GoGorillaMux, payload.SdkType) - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) + reqData, _ := json.Marshal(ExampleData2) + respData, _ := json.Marshal(ExampleDataRedacted) assert.Equal(t, reqData, payload.RequestBody) assert.Equal(t, respData, payload.ResponseBody) @@ -267,7 +267,7 @@ func TestChiMiddleware(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, body) - jsonByte, err := json.Marshal(exampleData) + jsonByte, err := json.Marshal(ExampleData) assert.NoError(t, err) w.Header().Add("Content-Type", "application/json") @@ -289,7 +289,7 @@ func TestChiMiddleware(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) diff --git a/outgoing.go b/outgoing.go index 2bc963f..edf0bf7 100644 --- a/outgoing.go +++ b/outgoing.go @@ -57,7 +57,7 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er if res != nil { respBodyBytes, _ := io.ReadAll(res.Body) res.Body = io.NopCloser(bytes.NewBuffer(respBodyBytes)) - payload = rt.client.buildPayload( + payload = rt.client.BuildPayload( GoOutgoing, start, req, res.StatusCode, reqBodyBytes, respBodyBytes, res.Header, nil, @@ -68,7 +68,7 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er parentMsgIDPtr, ) } else { - payload = rt.client.buildPayload( + payload = rt.client.BuildPayload( GoOutgoing, start, req, 503, reqBodyBytes, nil, nil, nil, diff --git a/outgoing_test.go b/outgoing_test.go index d7015dd..cfff368 100644 --- a/outgoing_test.go +++ b/outgoing_test.go @@ -18,7 +18,7 @@ func TestReporting(t *testing.T) { APIKey: os.Getenv("APITOOLKIT_KEY"), RootURL: "", RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, Tags: []string{"staging"}, } client, err := NewClient(ctx, cfg) @@ -51,7 +51,7 @@ func TestReporting(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) @@ -63,7 +63,7 @@ func TestSugaredReporting(t *testing.T) { APIKey: os.Getenv("APITOOLKIT_KEY"), RootURL: "", RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, Tags: []string{"staging"}, } client, err := NewClient(ctx, cfg) @@ -91,7 +91,7 @@ func TestSugaredReporting(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) diff --git a/sdk.go b/sdk.go index 9043572..5a8d81b 100644 --- a/sdk.go +++ b/sdk.go @@ -168,6 +168,18 @@ func (c *Client) Close() error { return nil } +func (c *Client) GetConfig() *Config { + return c.config +} + +func (c *Client) SetConfig(cfg *Config) { + c.config = cfg +} + +func (c *Client) GetMetadata() *ClientMetadata { + return c.metadata +} + // PublishMessage publishes payload to a gcp cloud console func (c *Client) publishMessage(ctx context.Context, payload Payload) error { if c.goReqsTopic == nil { @@ -187,17 +199,18 @@ func (c *Client) publishMessage(ctx context.Context, payload Payload) error { PublishTime: time.Now(), } - c.goReqsTopic.Publish(ctx, msgg) + res := c.goReqsTopic.Publish(ctx, msgg) if c.config.Debug { log.Println("APIToolkit: message published to pubsub topic") if c.config.VerboseDebug { log.Println("APIToolkit: ", pretty.Sprint(data)) } } + _, err = res.Get(ctx) return err } -func (c *Client) buildPayload(SDKType string, trackingStart time.Time, req *http.Request, +func (c *Client) BuildPayload(SDKType string, trackingStart time.Time, req *http.Request, statusCode int, reqBody []byte, respBody []byte, respHeader map[string][]string, pathParams map[string]string, urlPath string, redactHeadersList, @@ -245,10 +258,10 @@ func (c *Client) buildPayload(SDKType string, trackingStart time.Time, req *http QueryParams: req.URL.Query(), RawURL: req.URL.RequestURI(), Referer: req.Referer(), - RequestBody: redact(reqBody, redactRequestBodyList), - RequestHeaders: redactHeaders(req.Header, redactedHeaders), - ResponseBody: redact(respBody, redactResponseBodyList), - ResponseHeaders: redactHeaders(respHeader, redactedHeaders), + RequestBody: RedactJSON(reqBody, redactRequestBodyList), + RequestHeaders: RedactHeaders(req.Header, redactedHeaders), + ResponseBody: RedactJSON(respBody, redactResponseBodyList), + ResponseHeaders: RedactHeaders(respHeader, redactedHeaders), SdkType: SDKType, StatusCode: statusCode, Timestamp: time.Now(), @@ -261,7 +274,7 @@ func (c *Client) buildPayload(SDKType string, trackingStart time.Time, req *http } } -func (c *Client) buildFastHTTPPayload(SDKType string, trackingStart time.Time, req *fasthttp.RequestCtx, +func (c *Client) BuildFastHTTPPayload(SDKType string, trackingStart time.Time, req *fasthttp.RequestCtx, statusCode int, reqBody []byte, respBody []byte, respHeader map[string][]string, pathParams map[string]string, urlPath string, redactHeadersList, @@ -320,10 +333,10 @@ func (c *Client) buildFastHTTPPayload(SDKType string, trackingStart time.Time, r QueryParams: queryParams, RawURL: string(req.RequestURI()), Referer: referer, - RequestBody: redact(reqBody, redactRequestBodyList), - RequestHeaders: redactHeaders(reqHeaders, redactedHeaders), - ResponseBody: redact(respBody, redactResponseBodyList), - ResponseHeaders: redactHeaders(respHeader, redactedHeaders), + RequestBody: RedactJSON(reqBody, redactRequestBodyList), + RequestHeaders: RedactHeaders(reqHeaders, redactedHeaders), + ResponseBody: RedactJSON(respBody, redactResponseBodyList), + ResponseHeaders: RedactHeaders(respHeader, redactedHeaders), SdkType: SDKType, StatusCode: statusCode, Timestamp: time.Now(), @@ -336,7 +349,11 @@ func (c *Client) buildFastHTTPPayload(SDKType string, trackingStart time.Time, r } } -func redact(data []byte, redactList []string) []byte { +func (c *Client) ReportError(ctx context.Context, err error) { + ReportError(ctx, err) +} + +func RedactJSON(data []byte, redactList []string) []byte { config := jsonpath.Config{} config.SetAccessorMode() @@ -356,7 +373,7 @@ func redact(data []byte, redactList []string) []byte { return dataJSON } -func redactHeaders(headers map[string][]string, redactList []string) map[string][]string { +func RedactHeaders(headers map[string][]string, redactList []string) map[string][]string { for k := range headers { if find(redactList, k) { headers[k] = []string{"[CLIENT_REDACTED]"} diff --git a/sdk_test.go b/sdk_test.go index e80ddbe..83e3762 100644 --- a/sdk_test.go +++ b/sdk_test.go @@ -20,17 +20,17 @@ func TestMain(m *testing.M) { func TestRedactFunc(t *testing.T) { t.Run("redact json", func(t *testing.T) { - exampleJSON, err := json.Marshal(exampleData) + exampleJSON, err := json.Marshal(ExampleData) if err != nil { t.Error(err) } - res := redact(exampleJSON, exampleDataRedaction) - expected, _ := json.Marshal(exampleDataRedacted) + res := RedactJSON(exampleJSON, ExampleDataRedaction) + expected, _ := json.Marshal(ExampleDataRedacted) assert.JSONEq(t, string(expected), string(res)) }) t.Run("redactHeaders", func(t *testing.T) { - result := redactHeaders(map[string][]string{ + result := RedactHeaders(map[string][]string{ "Content-Type": {"application/json"}, "X-API-KEY": {"test"}, "X-rando": {"test 2"}, @@ -47,7 +47,7 @@ func TestOutgoingMiddleware(t *testing.T) { client := &Client{ config: &Config{ RedactHeaders: []string{"X-Api-Key", "Accept-Encoding"}, - RedactResponseBody: exampleDataRedaction, + RedactResponseBody: ExampleDataRedaction, }, } var publishCalled bool @@ -77,8 +77,8 @@ func TestOutgoingMiddleware(t *testing.T) { assert.Greater(t, payload.Duration, 1000*time.Nanosecond) assert.Equal(t, GoDefaultSDKType, payload.SdkType) - reqData, _ := json.Marshal(exampleData2) - respData, _ := json.Marshal(exampleDataRedacted) + reqData, _ := json.Marshal(ExampleData2) + respData, _ := json.Marshal(ExampleDataRedacted) assert.Equal(t, reqData, payload.RequestBody) assert.Equal(t, respData, payload.ResponseBody) @@ -110,7 +110,7 @@ func TestOutgoingMiddleware(t *testing.T) { assert.NotEmpty(t, body) atHTTPClient := HTTPClient(r.Context()) _, _ = atHTTPClient.Get("http://localhost:3000/from-gorilla?param1=abc¶m2=123") - jsonByte, err := json.Marshal(exampleData) + jsonByte, err := json.Marshal(ExampleData) assert.NoError(t, err) w.Header().Add("Content-Type", "application/json") @@ -127,77 +127,8 @@ func TestOutgoingMiddleware(t *testing.T) { "Content-Type": "application/json", "X-API-KEY": "past-3", }, - req.BodyJSON(exampleData2), + req.BodyJSON(ExampleData2), ) assert.NoError(t, err) assert.True(t, publishCalled) } - -var exampleData = map[string]interface{}{ - "status": "success", - "data": map[string]interface{}{ - "message": "hello world", - "account_data": map[string]interface{}{ - "batch_number": 12345, - "account_id": "123456789", - "account_name": "test account", - "account_type": "test", - "account_status": "active", - "account_balance": "100.00", - "account_currency": "USD", - "account_created_at": "2020-01-01T00:00:00Z", - "account_updated_at": "2020-01-01T00:00:00Z", - "account_deleted_at": "2020-01-01T00:00:00Z", - "possible_account_types": []string{"test", "staging", "production"}, - "possible_account_types2": []string{"test", "staging", "production"}, - }, - }, -} - -var exampleDataRedaction = []string{ - "$.status", "$.data.account_data.account_type", - "$.data.account_data.possible_account_types", - "$.data.account_data.possible_account_types2[*]", - "$.non_existent", -} - -var exampleDataRedacted = map[string]interface{}{ - "status": "[CLIENT_REDACTED]", - "data": map[string]interface{}{ - "message": "hello world", - "account_data": map[string]interface{}{ - "batch_number": 12345, - "account_id": "123456789", - "account_name": "test account", - "account_type": "[CLIENT_REDACTED]", - "account_status": "active", - "account_balance": "100.00", - "account_currency": "USD", - "account_created_at": "2020-01-01T00:00:00Z", - "account_updated_at": "2020-01-01T00:00:00Z", - "account_deleted_at": "2020-01-01T00:00:00Z", - "possible_account_types": "[CLIENT_REDACTED]", - "possible_account_types2": []string{"[CLIENT_REDACTED]", "[CLIENT_REDACTED]", "[CLIENT_REDACTED]"}, - }, - }, -} - -var exampleData2 = map[string]interface{}{ - "status": "request", - "send": map[string]interface{}{ - "message": "hello world", - "account_data": []map[string]interface{}{{ - "batch_number": 12345, - "account_id": "123456789", - "account_name": "test account", - "account_type": "test", - "account_status": "active", - "account_balance": "100.00", - "account_currency": "USD", - "account_created_at": "2020-01-01T00:00:00Z", - "account_updated_at": "2020-01-01T00:00:00Z", - "account_deleted_at": "2020-01-01T00:00:00Z", - "possible_account_types": []string{"test", "staging", "production"}, - }}, - }, -} diff --git a/test_utils.go b/test_utils.go new file mode 100644 index 0000000..48b9cb7 --- /dev/null +++ b/test_utils.go @@ -0,0 +1,70 @@ +package apitoolkit + +var ExampleData = map[string]interface{}{ + "status": "success", + "data": map[string]interface{}{ + "message": "hello world", + "account_data": map[string]interface{}{ + "batch_number": 12345, + "account_id": "123456789", + "account_name": "test account", + "account_type": "test", + "account_status": "active", + "account_balance": "100.00", + "account_currency": "USD", + "account_created_at": "2020-01-01T00:00:00Z", + "account_updated_at": "2020-01-01T00:00:00Z", + "account_deleted_at": "2020-01-01T00:00:00Z", + "possible_account_types": []string{"test", "staging", "production"}, + "possible_account_types2": []string{"test", "staging", "production"}, + }, + }, +} + +var ExampleDataRedaction = []string{ + "$.status", "$.data.account_data.account_type", + "$.data.account_data.possible_account_types", + "$.data.account_data.possible_account_types2[*]", + "$.non_existent", +} + +var ExampleDataRedacted = map[string]interface{}{ + "status": "[CLIENT_REDACTED]", + "data": map[string]interface{}{ + "message": "hello world", + "account_data": map[string]interface{}{ + "batch_number": 12345, + "account_id": "123456789", + "account_name": "test account", + "account_type": "[CLIENT_REDACTED]", + "account_status": "active", + "account_balance": "100.00", + "account_currency": "USD", + "account_created_at": "2020-01-01T00:00:00Z", + "account_updated_at": "2020-01-01T00:00:00Z", + "account_deleted_at": "2020-01-01T00:00:00Z", + "possible_account_types": "[CLIENT_REDACTED]", + "possible_account_types2": []string{"[CLIENT_REDACTED]", "[CLIENT_REDACTED]", "[CLIENT_REDACTED]"}, + }, + }, +} + +var ExampleData2 = map[string]interface{}{ + "status": "request", + "send": map[string]interface{}{ + "message": "hello world", + "account_data": []map[string]interface{}{{ + "batch_number": 12345, + "account_id": "123456789", + "account_name": "test account", + "account_type": "test", + "account_status": "active", + "account_balance": "100.00", + "account_currency": "USD", + "account_created_at": "2020-01-01T00:00:00Z", + "account_updated_at": "2020-01-01T00:00:00Z", + "account_deleted_at": "2020-01-01T00:00:00Z", + "possible_account_types": []string{"test", "staging", "production"}, + }}, + }, +}