From a6b2ce7008264e53f9ea1211094d07f9d906d311 Mon Sep 17 00:00:00 2001 From: "Oyewole S. Abayomi" Date: Mon, 27 Jun 2022 08:13:12 +0100 Subject: [PATCH] feat: implement go novu endpoint --- .gitignore | 1 + Makefile | 9 ++ README.md | 85 +++++++++++- cmd/default.go | 85 ++++++++++++ go.mod | 14 ++ go.sum | 17 +++ lib/event.go | 42 ++++++ lib/event_test.go | 80 +++++++++++ lib/model.go | 79 +++++++++++ lib/novu.go | 107 +++++++++++++++ lib/subscribers.go | 80 +++++++++++ lib/subscribers_test.go | 167 +++++++++++++++++++++++ testdata/identify_subscriber.json | 18 +++ testdata/novu_send_trigger.json | 14 ++ testdata/novu_send_trigger_response.json | 6 + testdata/subscriber_response.json | 6 + testdata/update_subscriber.json | 9 ++ 17 files changed, 818 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 cmd/default.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lib/event.go create mode 100644 lib/event_test.go create mode 100644 lib/model.go create mode 100644 lib/novu.go create mode 100644 lib/subscribers.go create mode 100644 lib/subscribers_test.go create mode 100644 testdata/identify_subscriber.json create mode 100644 testdata/novu_send_trigger.json create mode 100644 testdata/novu_send_trigger_response.json create mode 100644 testdata/subscriber_response.json create mode 100644 testdata/update_subscriber.json diff --git a/.gitignore b/.gitignore index 66fd13c..0ca2a00 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +.idea # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..41edde4 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +OUTPUT = main + +.PHONY: test +test: + go test ./... + +.PHONY: clean +clean: + rm -f $(OUTPUT) \ No newline at end of file diff --git a/README.md b/README.md index 40e162f..f931085 100644 --- a/README.md +++ b/README.md @@ -1 +1,84 @@ -# go \ No newline at end of file +# Novu's API v1 Go Library + +Novu's API exposes the entire Novu features via a standardized programmatic interface. Please refer to the full [documentation](https://docs.novu.co/docs/overview/introduction) to learn more. + +## Installation & Usage +Install the package to your GoLang project. +```golang +go get github.com/novuhq/go-novu +``` + +## Getting Started + +Please follow the [installation procedure](#installation--usage) and then run the following: + +```golang +package main + +import ( + "context" + "fmt" + novu "github.com/novuhq/go-novu/lib" + "log" +) + +func main() { + apiKey := "ee35a7412bc654b3ac3b5cf649daa319" + eventId := "gs-cooperative" + + ctx := context.Background() + to := map[string]interface{}{ + "lastName": "Doe", + "firstName": "John", + "subscriberId": "john@doemail.com", + "email": "john@doemail.com", + } + + payload := map[string]interface{}{ + "name": "Hello World", + "organization": map[string]interface{}{ + "logo": "https://happycorp.com/logo.png", + }, + } + + data := novu.ITriggerPayloadOptions{To: to, Payload: payload} + novuClient := novu.NewAPIClient(apiKey, &novu.Config{}) + + resp, err := novuClient.EventApi.Trigger(ctx, eventId, data) + if err != nil { + log.Fatal("novu error", err.Error()) + return + } + + fmt.Println(resp) +} +``` + +## Documentation for API Endpoints + +Class | Method | HTTP request | Description +------------ |------------------------------------------------|---------------------------------------| ------------- +*EventApi* | [**Trigger**](docs/SubscriberApi.md#identify) | **Post** /events/trigger | Get your account information, plan and credits details +*SubscriberApi* | [**Identify**](docs/SubscriberApi.md#identify) | **Post** /subscribers | Get your account information, plan and credits details +*SubscriberApi* | [**Update**](docs/SubscriberApi.md#update) | **Put** /subscribers/:subscriberID | Get your account information, plan and credits details +*SubscriberApi* | [**Delete**](docs/SubscriberApi.md#delete) | **Delete** /subscribers/:subscriberID | Get your account information, plan and credits details + +## Authorization (api-key) + +- **Type**: API key +- **API key parameter name**: ApiKey +- **Location**: HTTP header + +## Support and Feedback + +Be sure to visit the Novu official [documentation website](https://docs.novu.co/docs) for additional information about our API. + +If you find a bug, please post the issue on [Github](https://github.com/novuhq/go-novu/issues). + +As always, if you need additional assistance, join our Discord us a note [here](https://discord.gg/TT6TttXjRe). + +## Contributors + +Name | +------------ | +[Oyewole Samuel](https://github.com/samsoft00) | \ No newline at end of file diff --git a/cmd/default.go b/cmd/default.go new file mode 100644 index 0000000..4b039d9 --- /dev/null +++ b/cmd/default.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "fmt" + novu "github.com/novuhq/go-novu/lib" + "log" +) + +func main() { + subscriberID := "3ac3b5cf649daa319ee35a7412bc654b" + apiKey := "ee35a7412bc654b3ac3b5cf649daa319" + eventId := "gs-cooperative" + + ctx := context.Background() + to := map[string]interface{}{ + "lastName": "Doe", + "firstName": "John", + "subscriberId": "john@doemail.com", + "email": "john@doemail.com", + } + + payload := map[string]interface{}{ + "name": "Hello World", + "organization": map[string]interface{}{ + "logo": "https://happycorp.com/logo.png", + }, + } + + novuClient := novu.NewAPIClient(apiKey, &novu.Config{}) + + // Trigger + triggerResp, err := novuClient.EventApi.Trigger(ctx, eventId, novu.ITriggerPayloadOptions{ + To: to, + Payload: payload, + }) + if err != nil { + log.Fatal("Novu error", err.Error()) + return + } + + fmt.Println(triggerResp) + + // Subscriber + subscriber := novu.SubscriberPayload{ + LastName: "Skjæveland", + Email: "benedicte.skjaeveland@example.com", + Avatar: "https://randomuser.me/api/portraits/thumb/women/79.jpg", + Data: map[string]interface{}{ + "location": map[string]interface{}{ + "city": "Ballangen", + "state": "Aust-Agder", + "country": "Norway", + "postcode": "7481", + }, + }, + } + + resp, err := novuClient.SubscriberApi.Identify(ctx, subscriberID, subscriber) + if err != nil { + log.Fatal("Subscriber error: ", err.Error()) + return + } + + fmt.Println(resp) + + // update subscriber + updateSubscriber := novu.SubscriberPayload{FirstName: "Susan"} + + updateResp, err := novuClient.SubscriberApi.Update(ctx, subscriberID, updateSubscriber) + if err != nil { + log.Fatal("Update subscriber error: ", err.Error()) + return + } + + fmt.Println(updateResp) + + // delete subscriber + deleteResp, err := novuClient.SubscriberApi.Delete(ctx, subscriberID) + if err != nil { + log.Fatal("Update subscriber error: ", err.Error()) + return + } + fmt.Println(deleteResp) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9d8ed37 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/novuhq/go-novu + +go 1.18 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.7.5 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..291419b --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/event.go b/lib/event.go new file mode 100644 index 0000000..d2bb905 --- /dev/null +++ b/lib/event.go @@ -0,0 +1,42 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" +) + +type IEvent interface { + Trigger(ctx context.Context, eventId string, data ITriggerPayloadOptions) (EventResponse, error) +} + +type EventService service + +func (e *EventService) Trigger(ctx context.Context, eventId string, data ITriggerPayloadOptions) (EventResponse, error) { + var resp EventResponse + URL := fmt.Sprintf(e.client.config.BackendURL+"/%s", "events/trigger") + + reqBody := EventRequest{ + Name: eventId, + To: data.To, + Payload: data.Payload, + } + + jsonBody, _ := json.Marshal(reqBody) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(jsonBody)) + if err != nil { + return resp, err + } + + err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +var _ IEvent = &EventService{} diff --git a/lib/event_test.go b/lib/event_test.go new file mode 100644 index 0000000..71c721c --- /dev/null +++ b/lib/event_test.go @@ -0,0 +1,80 @@ +package lib_test + +import ( + "bytes" + "context" + "encoding/json" + "github.com/novuhq/go-novu/lib" + "github.com/stretchr/testify/require" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + novuApiKey = "test-API-key" + novuEventId = "test-novu" +) + +func fileToStruct(filepath string, s interface{}) io.Reader { + bb, _ := ioutil.ReadFile(filepath) + json.Unmarshal(bb, s) + return bytes.NewReader(bb) +} + +func TestEventServiceTrigger_Success(t *testing.T) { + var ( + receivedBody lib.ITriggerPayloadOptions + expectedTokenRequest lib.ITriggerPayloadOptions + triggerPayload lib.ITriggerPayloadOptions + ) + + eventService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if err := json.NewDecoder(req.Body).Decode(&receivedBody); err != nil { + log.Printf("error in unmarshalling %+v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + t.Run("Header must contain ApiKey", func(t *testing.T) { + authKey := req.Header.Get("Authorization") + assert.True(t, strings.Contains(authKey, novuApiKey)) + assert.True(t, strings.HasPrefix(authKey, "ApiKey")) + }) + + t.Run("URL and request method is as expected", func(t *testing.T) { + expectedURL := "/v1/events/trigger" + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + t.Run("Request is as expected", func(t *testing.T) { + fileToStruct(filepath.Join("../testdata", "novu_send_trigger.json"), &expectedTokenRequest) + assert.Equal(t, expectedTokenRequest, receivedBody) + }) + + var resp lib.EventResponse + fileToStruct(filepath.Join("../testdata", "novu_send_trigger_response.json"), &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer eventService.Close() + + ctx := context.Background() + fileToStruct(filepath.Join("../testdata", "novu_send_trigger.json"), &triggerPayload) + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: eventService.URL}) + _, err := c.EventApi.Trigger(ctx, novuEventId, triggerPayload) + + require.Nil(t, err) +} diff --git a/lib/model.go b/lib/model.go new file mode 100644 index 0000000..6421656 --- /dev/null +++ b/lib/model.go @@ -0,0 +1,79 @@ +package lib + +import "io" + +type ChannelType string +type GeneralError error + +const Version = "v1" + +const ( + HTTPStatusOk = 200 + HTTPStatusCreated = 201 + HTTPRedirectOk = 300 +) + +const ( + EMAIL ChannelType = "EMAIL" + SMS = "SMS" + DIRECT = "DIRECT" +) + +type Data struct { + Acknowledged bool `json:"acknowledged"` + Status string `json:"status"` +} + +type Response struct { + Data Data `json:"data"` +} + +type ITriggerPayloadOptions struct { + To interface{} `json:"to,omitempty"` + Payload interface{} `json:"payload,omitempty"` +} + +type TriggerRecipientsTypeArray interface { + []string | []SubscriberPayload +} +type TriggerRecipientsTypeSingle interface { + string | SubscriberPayload +} + +type SubscriberPayload struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + Avatar string `json:"avatar,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` +} + +type TriggerRecipientsType interface { + TriggerRecipientsTypeSingle | TriggerRecipientsTypeArray +} + +type ITriggerPayload interface { + string | []string | bool | int64 | IAttachmentOptions | []IAttachmentOptions +} + +type IAttachmentOptions struct { + Mime string `json:"mime,omitempty"` + File io.Reader `json:"file,omitempty"` + Name string `json:"name,omitempty"` + Channels []ChannelType `json:"channels,omitempty"` +} + +type EventResponse struct { + Data interface{} `json:"data"` +} + +type EventRequest struct { + Name string `json:"name"` + To interface{} `json:"to"` + Payload interface{} `json:"payload"` +} + +type SubscriberResponse struct { + Data interface{} `json:"data"` +} diff --git a/lib/novu.go b/lib/novu.go new file mode 100644 index 0000000..eee483e --- /dev/null +++ b/lib/novu.go @@ -0,0 +1,107 @@ +package lib + +import ( + "encoding/json" + "fmt" + "github.com/pkg/errors" + "io" + "net/http" + "strings" + "time" +) + +type Config struct { + BackendURL string + HttpClient *http.Client +} + +type APIClient struct { + apiKey string + config *Config + common service + + // Api Service + SubscriberApi *SubscriberService + EventApi *EventService +} + +type service struct { + client *APIClient +} + +func NewAPIClient(apiKey string, cfg *Config) *APIClient { + cfg.BackendURL = buildBackendURL(cfg) + + if cfg.HttpClient == nil { + cfg.HttpClient = &http.Client{Timeout: 20 * time.Second} + } + + c := &APIClient{apiKey: apiKey} + c.config = cfg + c.common.client = c + + // API Services + c.EventApi = (*EventService)(&c.common) + c.SubscriberApi = (*SubscriberService)(&c.common) + + return c +} + +func (c APIClient) sendRequest(req *http.Request, resp interface{}) error { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("ApiKey %s", c.apiKey)) + + res, err := c.config.HttpClient.Do(req) + if err != nil { + return errors.Wrap(err, "failed to execute request") + } + + body, _ := io.ReadAll(res.Body) + defer res.Body.Close() + + if res.StatusCode >= http.StatusMultipleChoices { + return errors.Errorf( + `request was not successful, status code %d, %s`, res.StatusCode, + string(body), + ) + } + + err = c.decode(&resp, body) + if err != nil { + return errors.Wrap(err, "unable to unmarshal response body") + } + + return nil +} + +func (c APIClient) mergeStruct(target, patch interface{}) (interface{}, error) { + var m map[string]interface{} + + targetPayload, _ := json.Marshal(target) + patchPayload, _ := json.Marshal(patch) + + _ = json.Unmarshal(targetPayload, &m) + _ = json.Unmarshal(patchPayload, &m) + + return m, nil +} + +func (c APIClient) decode(v interface{}, b []byte) (err error) { + if err = json.Unmarshal(b, v); err != nil { + return err + } + return nil +} + +func buildBackendURL(cfg *Config) string { + novuVersion := "v1" + + if cfg.BackendURL == "" { + return fmt.Sprintf("https://api.novu.co/%s", novuVersion) + } + + if strings.Contains(cfg.BackendURL, "novu.co/v") { + return cfg.BackendURL + } + return fmt.Sprintf(cfg.BackendURL+"/%s", novuVersion) +} diff --git a/lib/subscribers.go b/lib/subscribers.go new file mode 100644 index 0000000..471292a --- /dev/null +++ b/lib/subscribers.go @@ -0,0 +1,80 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/pkg/errors" + "net/http" +) + +type ISubscribers interface { + Identify(ctx context.Context, subscriberID string, data interface{}) (SubscriberResponse, error) + Update(ctx context.Context, subscriberID string, data interface{}) (SubscriberResponse, error) + Delete(ctx context.Context, subscriberID string) (SubscriberResponse, error) +} + +type SubscriberService service + +func (s *SubscriberService) Identify(ctx context.Context, subscriberID string, data interface{}) (SubscriberResponse, error) { + var resp SubscriberResponse + URL := fmt.Sprintf(s.client.config.BackendURL+"/%s", "subscribers") + + reqBody, err := s.client.mergeStruct(data, map[string]interface{}{"subscriberId": subscriberID}) + if err != nil { + return resp, errors.Wrap(err, "unable to merge struct") + } + + jsonBody, _ := json.Marshal(reqBody) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, bytes.NewBuffer(jsonBody)) + if err != nil { + return resp, err + } + + err = s.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (s *SubscriberService) Update(ctx context.Context, subscriberID string, data interface{}) (SubscriberResponse, error) { + var resp SubscriberResponse + URL := fmt.Sprintf(s.client.config.BackendURL+"/subscribers/%s", subscriberID) + + jsonBody, _ := json.Marshal(data) + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, URL, bytes.NewBuffer(jsonBody)) + if err != nil { + return resp, err + } + + err = s.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (s *SubscriberService) Delete(ctx context.Context, subscriberID string) (SubscriberResponse, error) { + var resp SubscriberResponse + URL := fmt.Sprintf(s.client.config.BackendURL+"/subscribers/%s", subscriberID) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, URL, http.NoBody) + if err != nil { + return resp, err + } + + err = s.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +var _ ISubscribers = &SubscriberService{} diff --git a/lib/subscribers_test.go b/lib/subscribers_test.go new file mode 100644 index 0000000..dbfc433 --- /dev/null +++ b/lib/subscribers_test.go @@ -0,0 +1,167 @@ +package lib_test + +import ( + "context" + "encoding/json" + "github.com/novuhq/go-novu/lib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "log" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" +) + +const subscriberID = "62b51a44da1af31d109f5da7" + +func TestSubscriberService_Identify_Success(t *testing.T) { + var ( + subscriberPayload lib.SubscriberPayload + receivedBody lib.SubscriberPayload + expectedRequest lib.SubscriberPayload + expectedResponse lib.SubscriberResponse + ) + + subscriberService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if err := json.NewDecoder(req.Body).Decode(&receivedBody); err != nil { + log.Printf("error in unmarshalling %+v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + t.Run("Header must contain ApiKey", func(t *testing.T) { + authKey := req.Header.Get("Authorization") + assert.True(t, strings.Contains(authKey, novuApiKey)) + assert.True(t, strings.HasPrefix(authKey, "ApiKey")) + }) + + t.Run("URL and request method is as expected", func(t *testing.T) { + expectedURL := "/v1/subscribers" + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + t.Run("Request is as expected", func(t *testing.T) { + fileToStruct(filepath.Join("../testdata", "identify_subscriber.json"), &expectedRequest) + assert.Equal(t, expectedRequest, receivedBody) + }) + + var resp lib.SubscriberResponse + fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer subscriberService.Close() + + ctx := context.Background() + fileToStruct(filepath.Join("../testdata", "identify_subscriber.json"), &subscriberPayload) + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: subscriberService.URL}) + + resp, err := c.SubscriberApi.Identify(ctx, subscriberID, subscriberPayload) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestSubscriberService_Update_Success(t *testing.T) { + var ( + updateSubscriber lib.SubscriberPayload + receivedBody lib.SubscriberPayload + expectedRequest lib.SubscriberPayload + expectedResponse lib.SubscriberResponse + ) + + subscriberService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if err := json.NewDecoder(req.Body).Decode(&receivedBody); err != nil { + log.Printf("error in unmarshalling %+v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + t.Run("Header must contain ApiKey", func(t *testing.T) { + authKey := req.Header.Get("Authorization") + assert.True(t, strings.Contains(authKey, novuApiKey)) + assert.True(t, strings.HasPrefix(authKey, "ApiKey")) + }) + + t.Run("URL and request method is as expected", func(t *testing.T) { + expectedURL := "/v1/subscribers/" + subscriberID + assert.Equal(t, http.MethodPut, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + t.Run("Request is as expected", func(t *testing.T) { + fileToStruct(filepath.Join("../testdata", "update_subscriber.json"), &expectedRequest) + assert.Equal(t, expectedRequest, receivedBody) + }) + + var resp lib.SubscriberResponse + fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + ctx := context.Background() + fileToStruct(filepath.Join("../testdata", "update_subscriber.json"), &updateSubscriber) + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: subscriberService.URL}) + + resp, err := c.SubscriberApi.Update(ctx, subscriberID, updateSubscriber) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestSubscriberService_Delete_Success(t *testing.T) { + var expectedResponse lib.SubscriberResponse + + ctx := context.Background() + + subscriberService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + t.Run("Header must contain ApiKey", func(t *testing.T) { + authKey := req.Header.Get("Authorization") + assert.True(t, strings.Contains(authKey, novuApiKey)) + assert.True(t, strings.HasPrefix(authKey, "ApiKey")) + }) + + t.Run("URL and request method is as expected", func(t *testing.T) { + expectedURL := "/v1/subscribers/" + subscriberID + assert.Equal(t, http.MethodDelete, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + var resp lib.SubscriberResponse + fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: subscriberService.URL}) + + resp, err := c.SubscriberApi.Delete(ctx, subscriberID) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + fileToStruct(filepath.Join("../testdata", "subscriber_response.json"), &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} diff --git a/testdata/identify_subscriber.json b/testdata/identify_subscriber.json new file mode 100644 index 0000000..8f08852 --- /dev/null +++ b/testdata/identify_subscriber.json @@ -0,0 +1,18 @@ +{ + "title": "Mrs", + "first_name": "Junilvana", + "last_name": "Souza", + "email": "junilvana.souza@example.com", + "phone": "(86) 5889-7095", + "avatar": "https://randomuser.me/api/portraits/women/39.jpg", + "location": { + "street": { + "number": 97, + "name": "Rua Santo Antônio" + }, + "city": "Itapipoca", + "state": "Mato Grosso", + "country": "Brazil", + "postcode": 92448 + } +} \ No newline at end of file diff --git a/testdata/novu_send_trigger.json b/testdata/novu_send_trigger.json new file mode 100644 index 0000000..770240b --- /dev/null +++ b/testdata/novu_send_trigger.json @@ -0,0 +1,14 @@ +{ + "to": { + "subscriberId": "john@doemail.com", + "lastName": "Doe", + "firstName": "John", + "email": "john@doemail.com" + }, + "payload": { + "name": "Hello World", + "organization": { + "logo": "https://happycorp.com/logo.png" + } + } +} \ No newline at end of file diff --git a/testdata/novu_send_trigger_response.json b/testdata/novu_send_trigger_response.json new file mode 100644 index 0000000..9abb2b1 --- /dev/null +++ b/testdata/novu_send_trigger_response.json @@ -0,0 +1,6 @@ +{ + "data": { + "acknowledged": true, + "status": "processed" + } +} \ No newline at end of file diff --git a/testdata/subscriber_response.json b/testdata/subscriber_response.json new file mode 100644 index 0000000..9abb2b1 --- /dev/null +++ b/testdata/subscriber_response.json @@ -0,0 +1,6 @@ +{ + "data": { + "acknowledged": true, + "status": "processed" + } +} \ No newline at end of file diff --git a/testdata/update_subscriber.json b/testdata/update_subscriber.json new file mode 100644 index 0000000..f1043fb --- /dev/null +++ b/testdata/update_subscriber.json @@ -0,0 +1,9 @@ +{ + "title": "Mrs", + "first_name": "Junilvana", + "last_name": "Souza", + "age": 31, + "email": "junilvana.souza@example.com", + "phone": "(86) 5889-7095", + "avatar": "https://randomuser.me/api/portraits/women/39.jpg" +} \ No newline at end of file