From 7f1902b619d08a01f9db734b3f8c2bf183dff23f Mon Sep 17 00:00:00 2001 From: Alejandro Visiedo Date: Thu, 8 Aug 2024 14:03:39 +0200 Subject: [PATCH] test(HMS-4532): pendo client Add unit tests for pendo client. Signed-off-by: Alejandro Visiedo --- .../builder_setmetadatadetailsrequest.go | 32 ++ .../pendo/builder_setmetadatarequest.go | 26 ++ .../pendo/builder_setmetadataresponse.go | 62 +++ .../usecase/client/pendo/pendo_client_test.go | 365 ++++++++++++++++++ 4 files changed, 485 insertions(+) create mode 100644 internal/test/builder/clients/pendo/builder_setmetadatadetailsrequest.go create mode 100644 internal/test/builder/clients/pendo/builder_setmetadatarequest.go create mode 100644 internal/test/builder/clients/pendo/builder_setmetadataresponse.go create mode 100644 internal/usecase/client/pendo/pendo_client_test.go diff --git a/internal/test/builder/clients/pendo/builder_setmetadatadetailsrequest.go b/internal/test/builder/clients/pendo/builder_setmetadatadetailsrequest.go new file mode 100644 index 00000000..c94bf4ba --- /dev/null +++ b/internal/test/builder/clients/pendo/builder_setmetadatadetailsrequest.go @@ -0,0 +1,32 @@ +package pendo + +import ( + "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" + pendo_api "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" +) + +type SetMetadataDetailsRequest interface { + Build() *pendo.SetMetadataDetailsRequest + SetVisitorID(value string) SetMetadataDetailsRequest + AddValue(fieldName string, value any) SetMetadataDetailsRequest +} + +type setMetadataDetailsRequest pendo_api.SetMetadataDetailsRequest + +func NewSetMetadataDetailsRequest() SetMetadataDetailsRequest { + return (*setMetadataDetailsRequest)(&pendo_api.SetMetadataDetailsRequest{}) +} + +func (b *setMetadataDetailsRequest) Build() *pendo.SetMetadataDetailsRequest { + return (*pendo.SetMetadataDetailsRequest)(b) +} + +func (b *setMetadataDetailsRequest) SetVisitorID(value string) SetMetadataDetailsRequest { + b.VisitorID = value + return b +} + +func (b *setMetadataDetailsRequest) AddValue(fieldName string, value any) SetMetadataDetailsRequest { + b.Values[fieldName] = value + return b +} diff --git a/internal/test/builder/clients/pendo/builder_setmetadatarequest.go b/internal/test/builder/clients/pendo/builder_setmetadatarequest.go new file mode 100644 index 00000000..09898394 --- /dev/null +++ b/internal/test/builder/clients/pendo/builder_setmetadatarequest.go @@ -0,0 +1,26 @@ +package pendo + +import ( + "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" + pendo_api "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" +) + +type SetMetadataRequest interface { + Add(item pendo.SetMetadataDetailsRequest) SetMetadataRequest + Build() *pendo.SetMetadataRequest +} + +type setMetadataRequest pendo_api.SetMetadataRequest + +func NewSetMetadataRequest() SetMetadataRequest { + return (*setMetadataRequest)(&pendo_api.SetMetadataRequest{}) +} + +func (b *setMetadataRequest) Build() *pendo.SetMetadataRequest { + return (*pendo.SetMetadataRequest)(b) +} + +func (b *setMetadataRequest) Add(item pendo.SetMetadataDetailsRequest) SetMetadataRequest { + *b = append(*b, item) + return b +} diff --git a/internal/test/builder/clients/pendo/builder_setmetadataresponse.go b/internal/test/builder/clients/pendo/builder_setmetadataresponse.go new file mode 100644 index 00000000..b7272484 --- /dev/null +++ b/internal/test/builder/clients/pendo/builder_setmetadataresponse.go @@ -0,0 +1,62 @@ +package pendo + +import ( + "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" + pendo_api "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" +) + +type SetMetadataResponse interface { + Build() *pendo.SetMetadataResponse + WithTotal(value int64) SetMetadataResponse + WithUpdated(value int64) SetMetadataResponse + IncUpdated() SetMetadataResponse + WithFailed(value int64) SetMetadataResponse + IncFailed() SetMetadataResponse + AddMissing(value string) SetMetadataResponse + WithKind(value pendo.Kind) SetMetadataResponse +} + +type setMetadataResponse pendo_api.SetMetadataResponse + +func NewSetMetadataResponse() SetMetadataResponse { + return (*setMetadataResponse)(&pendo_api.SetMetadataResponse{}) +} + +func (b *setMetadataResponse) Build() *pendo.SetMetadataResponse { + return (*pendo.SetMetadataResponse)(b) +} + +func (b *setMetadataResponse) WithTotal(value int64) SetMetadataResponse { + b.Total = value + return b +} + +func (b *setMetadataResponse) WithUpdated(value int64) SetMetadataResponse { + b.Updated = value + return b +} + +func (b *setMetadataResponse) IncUpdated() SetMetadataResponse { + b.Updated++ + return b +} + +func (b *setMetadataResponse) WithFailed(value int64) SetMetadataResponse { + b.Failed = value + return b +} + +func (b *setMetadataResponse) IncFailed() SetMetadataResponse { + b.Failed++ + return b +} + +func (b *setMetadataResponse) AddMissing(value string) SetMetadataResponse { + b.Missing = append(b.Missing, value) + return b +} + +func (b *setMetadataResponse) WithKind(value pendo.Kind) SetMetadataResponse { + b.Kind = value + return b +} diff --git a/internal/usecase/client/pendo/pendo_client_test.go b/internal/usecase/client/pendo/pendo_client_test.go new file mode 100644 index 00000000..a2581aa8 --- /dev/null +++ b/internal/usecase/client/pendo/pendo_client_test.go @@ -0,0 +1,365 @@ +package pendo + +import ( + "bytes" + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + "testing" + "time" + + "github.com/podengo-project/idmsvc-backend/internal/config" + app_context "github.com/podengo-project/idmsvc-backend/internal/infrastructure/context" + "github.com/podengo-project/idmsvc-backend/internal/interface/client/pendo" + builder_pendo "github.com/podengo-project/idmsvc-backend/internal/test/builder/clients/pendo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const baseURL = "http://localhost:8031/pendo/v1" + +// RoundTripFunc . +type RoundTripFunc func(req *http.Request) *http.Response + +// RoundTrip . +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +// PendoHash kind group visitorId fieldName => value +type PendoHash map[string]map[string]map[string]map[string]any + +func helperPendoConfig() *config.Config { + return &config.Config{ + Clients: config.Clients{ + PendoBaseURL: baseURL, + PendoAPIKey: "test-api-key", + PendoRequestTimeoutSecs: 1, + }, + } +} + +func helperNewPendo(cfg *config.Config, fn RoundTripFunc) pendo.Pendo { + client := newClient(cfg) + client.Client.Transport = RoundTripFunc(fn) + return client +} + +func TestNewPendo(t *testing.T) { + assert.PanicsWithValue(t, "'cfg' is nil", func() { + newClient(nil) + }) + + assert.PanicsWithValue(t, "'PendoBaseURL' is empty", func() { + newClient(&config.Config{ + Clients: config.Clients{ + PendoBaseURL: "", + PendoAPIKey: "", + PendoRequestTimeoutSecs: 0, + }, + }) + }) + + assert.PanicsWithValue(t, "'PendoAPIKey' is empty", func() { + newClient(&config.Config{ + Clients: config.Clients{ + PendoBaseURL: baseURL, + PendoAPIKey: "", + PendoRequestTimeoutSecs: 0, + }, + }) + }) + + client := newClient(&config.Config{ + Clients: config.Clients{ + PendoBaseURL: baseURL, + PendoAPIKey: "kiudsahfq84radihfa", + PendoRequestTimeoutSecs: 0, + }, + }) + require.NotNil(t, client) +} + +func TestGuardSetMetadata(t *testing.T) { + cfg := helperPendoConfig() + client := newClient(cfg) + + require.EqualError(t, client.guardSetMetadata("", "", nil), "'kind' is an empty string") + require.EqualError(t, client.guardSetMetadata("mykind", "", nil), "'group' is an empty string") + require.EqualError(t, client.guardSetMetadata("mykind", "mygroup", nil), "'metrics' is nil") + pendoMetrics := make(pendo.SetMetadataRequest, 0, 1) + require.NoError(t, client.guardSetMetadata("mykind", "mygroup", pendoMetrics), "an empty slice does not report error") + pendoMetrics = append(pendoMetrics, pendo.SetMetadataDetailsRequest{}) + require.EqualError(t, client.guardSetMetadata("mykind", "mygroup", pendoMetrics), "'metrics[0].VisitorID' is an empty string") + pendoMetrics[0].VisitorID = "my-test-visitor-id" + + // Success case + require.NoError(t, client.guardSetMetadata("mykind", "mygroup", pendoMetrics)) +} + +func helperSetMetadataPrepareRequest(t *testing.T, req *http.Request, kind pendo.Kind, group pendo.Group) pendo.SetMetadataRequest { + metrics := make(pendo.SetMetadataRequest, 0, 10) + // Check request + assert.Equal(t, baseURL+"/metadata/"+string(kind)+"/"+string(group)+"/value", req.URL.String()) + reqBytes, err := io.ReadAll(req.Body) + require.NoError(t, err) + require.NotNil(t, reqBytes) + err = json.Unmarshal(reqBytes, &metrics) + require.NoError(t, err) + require.NotNil(t, metrics) + return metrics +} + +func helperSetMetadataPrepareResponse(t *testing.T, metrics pendo.SetMetadataRequest, kind pendo.Kind, group pendo.Group, store PendoHash) *pendo.SetMetadataResponse { + var ( + ok bool + groups map[string]map[string]map[string]any + visitors map[string]map[string]any + storeMetrics map[string]any + ) + respBuilder := builder_pendo.NewSetMetadataResponse(). + WithTotal(int64(len(metrics))). + WithKind(kind) + require.NotNil(t, respBuilder) + for i := range metrics { + if groups, ok = store[string(kind)]; !ok { + respBuilder.AddMissing(metrics[i].VisitorID) + respBuilder.IncFailed() + continue + } + if visitors, ok = groups[string(group)]; !ok { + respBuilder.AddMissing(metrics[i].VisitorID) + respBuilder.IncFailed() + continue + } + if storeMetrics, ok = visitors[metrics[i].VisitorID]; !ok { + respBuilder.AddMissing(metrics[i].VisitorID) + respBuilder.IncFailed() + continue + } + respBuilder.IncUpdated() + for k, v := range metrics[i].Values { + storeMetrics[k] = v + } + } + + resp := respBuilder.Build() + require.NotNil(t, resp) + return resp +} + +func TestSetMetadata(t *testing.T) { + kind := pendo.KindAccount + group := pendo.Group("custom") + var store PendoHash = PendoHash{ + string(kind): { + string(group) /* group */ : { + "test-visitor-id" /* visitorId */ : { + "test-field-1" /* fieldName */ : 4, /* value */ + "test-field-2" /* fieldName */ : "someStringValue", /* value */ + }, + }, + }, + } + // https://hassansin.github.io/Unit-Testing-http-client-in-Go#2-by-replacing-httptransport + cfg := helperPendoConfig() + client := helperNewPendo(cfg, func(req *http.Request) *http.Response { + metrics := helperSetMetadataPrepareRequest(t, req, kind, group) + resp := helperSetMetadataPrepareResponse(t, metrics, kind, group, store) + bytesResp, err := json.Marshal(resp) + require.NoError(t, err) + require.NotNil(t, bytesResp) + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(bytesResp)), + Header: make(http.Header), + } + }) + + // Panic when context is nil + assert.PanicsWithValue(t, "'ctx' is nil", func() { + client.SetMetadata(nil, "", "", nil) + }) + + // Error when wrong argument + ctx := app_context.CtxWithLog(context.TODO(), slog.Default()) + resp, err := client.SetMetadata(ctx, "", "", nil) + require.EqualError(t, err, "bad arguments: 'kind' is an empty string") + require.Nil(t, resp) + + // Success + metrics := make(pendo.SetMetadataRequest, 0, 1) + metrics = append(metrics, pendo.SetMetadataDetailsRequest{ + VisitorID: "test-visitor-id", + Values: map[string]any{ + "test-field-1": 2, + "test-field-2": "anotherString", + }, + }) + metrics = append(metrics, pendo.SetMetadataDetailsRequest{ + VisitorID: "thisVisitorDoesNotExist", + }) + + // Call the data + resp, err = client.SetMetadata(ctx, kind, group, metrics) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, int64(2), resp.Total) + assert.Equal(t, int64(1), resp.Failed) + assert.Equal(t, int64(1), resp.Updated) + assert.Equal(t, []string{"thisVisitorDoesNotExist"}, resp.Missing) +} + +func TestSetMetadataForceFailureOnDoingRequestToPendo(t *testing.T) { + // This forces the path when c.Client.Do(req) returns nil at 'SetMetadata' + kind := pendo.KindAccount + group := pendo.Group("custom") + // https://hassansin.github.io/Unit-Testing-http-client-in-Go#2-by-replacing-httptransport + cfg := helperPendoConfig() + client := helperNewPendo(cfg, func(req *http.Request) *http.Response { + return nil + }) + + // Error path 1 + ctx := app_context.CtxWithLog(context.TODO(), slog.Default()) + metrics := make(pendo.SetMetadataRequest, 0, 1) + metrics = append(metrics, pendo.SetMetadataDetailsRequest{ + VisitorID: "test-visitor-id", + Values: map[string]any{ + "test-field-1": 2, + "test-field-2": "anotherString", + }, + }) + metrics = append(metrics, pendo.SetMetadataDetailsRequest{ + VisitorID: "thisVisitorDoesNotExist", + }) + resp, err := client.SetMetadata(ctx, kind, group, metrics) + require.EqualError(t, err, "Post \"http://localhost:8031/pendo/v1/metadata/account/custom/value\": http: RoundTripper implementation (pendo.RoundTripFunc) returned a nil *Response with a nil error") + require.Nil(t, resp) +} + +func TestSetMetadataErrorHttp(t *testing.T) { + kind := pendo.KindAccount + group := pendo.Group("custom") + // https://hassansin.github.io/Unit-Testing-http-client-in-Go#2-by-replacing-httptransport + cfg := helperPendoConfig() + client := helperNewPendo(cfg, func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: http.StatusBadGateway, + Body: http.NoBody, + Header: make(http.Header), + } + }) + + // Error path 1 + ctx := app_context.CtxWithLog(context.TODO(), slog.Default()) + metrics := make(pendo.SetMetadataRequest, 0, 1) + metrics = append(metrics, pendo.SetMetadataDetailsRequest{ + VisitorID: "test-visitor-id", + Values: map[string]any{ + "test-field-1": 2, + "test-field-2": "anotherString", + }, + }) + metrics = append(metrics, pendo.SetMetadataDetailsRequest{ + VisitorID: "thisVisitorDoesNotExist", + }) + resp, err := client.SetMetadata(ctx, kind, group, metrics) + require.EqualError(t, err, "unexpected StatusCode on SetMetadata response") + require.Nil(t, resp) +} + +func TestSetMetadataForceErrorUnmarshalling(t *testing.T) { + kind := pendo.KindAccount + group := pendo.Group("custom") + // https://hassansin.github.io/Unit-Testing-http-client-in-Go#2-by-replacing-httptransport + cfg := helperPendoConfig() + client := helperNewPendo(cfg, func(req *http.Request) *http.Response { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString("{")), + Header: make(http.Header), + } + }) + + // Error path 1 + ctx := app_context.CtxWithLog(context.TODO(), slog.Default()) + metrics := make(pendo.SetMetadataRequest, 0, 1) + resp, err := client.SetMetadata(ctx, kind, group, metrics) + require.EqualError(t, err, "error parsing SetMetadata response: unexpected end of JSON input") + require.Nil(t, resp) +} + +func TestGuardSetTrack(t *testing.T) { + cfg := helperPendoConfig() + client := newClient(cfg) + + client.guardSendTrackEvent(nil) + require.EqualError(t, client.guardSendTrackEvent(nil), "'track' is nil") + track := pendo.TrackRequest{} + require.EqualError(t, client.guardSendTrackEvent(&track), "'track.AccountID' is an empty string") + track.AccountID = "my-account-id" + require.EqualError(t, client.guardSendTrackEvent(&track), "'track.Type' is '', but 'track' was expected") + track.Type = "track" + require.EqualError(t, client.guardSendTrackEvent(&track), "'track.Event' is an empty string") + track.Event = "guard-set-track-tested" + require.EqualError(t, client.guardSendTrackEvent(&track), "'track.VisitorID' is an empty string") + track.VisitorID = "my-visitor-id" + require.EqualError(t, client.guardSendTrackEvent(&track), "'track.Timestamp' is invalid") + track.Timestamp = time.Now().UTC().Unix() + require.NoError(t, client.guardSendTrackEvent(&track)) +} + +func TestSetTrackEvent(t *testing.T) { + cfg := helperPendoConfig() + client := helperNewPendo(cfg, func(req *http.Request) *http.Response { + assert.Equal(t, baseURL+"/track", req.URL.String()) + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(`OK`)), + Header: make(http.Header), + } + }) + + assert.PanicsWithValue(t, "'ctx' is nil", func() { + client.SendTrackEvent(nil, nil) + }) + ctx := app_context.CtxWithLog(context.TODO(), slog.Default()) + require.EqualError(t, client.SendTrackEvent(ctx, nil), "bad argument for SendTrackEvent: 'track' is nil") + track := pendo.TrackRequest{ + AccountID: "my-account-id", + Type: "track", + Event: "my-event", + VisitorID: "my-visitor-id", + Timestamp: time.Now().UTC().Unix(), + } + require.NoError(t, client.SendTrackEvent(ctx, &track)) +} + +func TestSetTrackEventErrorHttpStatus(t *testing.T) { + cfg := helperPendoConfig() + client := helperNewPendo(cfg, func(req *http.Request) *http.Response { + assert.Equal(t, baseURL+"/track", req.URL.String()) + return &http.Response{ + StatusCode: http.StatusBadGateway, + Body: http.NoBody, + Header: make(http.Header), + } + }) + + assert.PanicsWithValue(t, "'ctx' is nil", func() { + client.SendTrackEvent(nil, nil) + }) + ctx := app_context.CtxWithLog(context.TODO(), slog.Default()) + track := pendo.TrackRequest{ + AccountID: "my-account-id", + Type: "track", + Event: "my-event", + VisitorID: "my-visitor-id", + Timestamp: time.Now().UTC().Unix(), + } + err := client.SendTrackEvent(ctx, &track) + require.EqualError(t, err, "unexpected StatusCode on SendTrackEvent response") +}