From f4953178c19d4d4baf8803dc3e44479c68dec8de Mon Sep 17 00:00:00 2001 From: Chang Sheng Date: Tue, 5 Sep 2023 13:56:22 +0800 Subject: [PATCH 01/13] chore: adds topic endpoint to readme --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59dba39..7f1b8e8 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,18 @@ Class | Method *SubscriberApi* | [**Post**](https://docs.novu.co/api/mark-a-subscriber-feed-message-as-seen) | **Post** /v1/subscribers/:subscriberId/messages/markAs | Mark a subscriber feed message as seen *SubscriberApi* | [**Get**](https://docs.novu.co/api/get-subscriber-preferences/) | **Get** /subscribers/:subscriberId/preferences | Get subscriber preferences *SubscriberApi* | [**Patch**](https://docs.novu.co/api/update-subscriber-preference/) | **Patch** /subscribers/:subscriberId/preferences/:templateId | Update subscriber preference +*TopicsApi* | [**Get**](https://docs.novu.co/api/filter-topics/) | **Get** /topics | Get a list of topics +*TopicsApi* | [**Get**](https://docs.novu.co/api/get-topic/) | **Get** /topics/:topicKey | Get a topic by its topic key +*TopicsApi* | [**Post**](https://docs.novu.co/api/topic-creation/) | **Post** /topics | Create a topic +*TopicsApi* | [**Patch**](https://docs.novu.co/api/rename-a-topic/) | **Patch** /topics/:topicKey | Rename a topic +*TopicsApi* | [**Delete**](https://docs.novu.co/api/delete-topic/) | **Delete** /topics/:topicKey | Delete a topic +*TopicsApi* | [**Post**](https://docs.novu.co/api/subscribers-addition/) | **Post** /topics/:topicKey/subscribers | Add subscribers to a topic by key +*TopicsApi* | [**Post**](https://docs.novu.co/api/subscribers-removal/) | **Post** /topics/:topicKey/subscribers/removal |Remove subscribers from a topic *IntegrationsApi* | [**Create**](https://docs.novu.co/platform/integrations) | **Post** /integrations | Create an integration *IntegrationsApi* | [**Update**](https://docs.novu.co/platform/integrations) | **Put** /integrations/:integrationId | Update an integration *IntegrationsApi* | [**Delete**](https://docs.novu.co/platform/integrations) | **Delete** /integrations/:integrationId | Delete an integration *IntegrationsApi* | [**Get**](https://docs.novu.co/platform/integrations) | **Get** /integrations | Get all integrations -*IntegrationsApi* | [**GetActive**](https://docs.novu.co/platform/intergations) | **Get** /integrations/active | Get all active integrations +*IntegrationsApi* | [**GetActive**](https://docs.novu.co/platform/integrations) | **Get** /integrations/active | Get all active integrations ## Authorization (api-key) @@ -103,7 +110,7 @@ As always, if you need additional assistance, join our Discord us a note [here]( ## Contributors -Name | +Name | ------------ | [Oyewole Samuel](https://github.com/samsoft00) | [Dima Grossman](https://github.com/scopsy) | From 689641b60567ec4f2a6c1eb0b9af8e0027fc78f3 Mon Sep 17 00:00:00 2001 From: Talabi Opemipo Date: Fri, 6 Oct 2023 18:18:26 +0100 Subject: [PATCH 02/13] changes endpoints implementation --- lib/changes.go | 91 +++++++++++++++ lib/changes_test.go | 267 ++++++++++++++++++++++++++++++++++++++++++++ lib/model.go | 38 +++++++ lib/novu.go | 2 + 4 files changed, 398 insertions(+) create mode 100644 lib/changes.go create mode 100644 lib/changes_test.go diff --git a/lib/changes.go b/lib/changes.go new file mode 100644 index 0000000..d7dd610 --- /dev/null +++ b/lib/changes.go @@ -0,0 +1,91 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" +) + +type IChanges interface { + ApplyChange(ctx context.Context, changeId string) (ChangesApplyResponse, error) + ApplyBulkChanges(ctx context.Context, payload ChangesBulkApplyPayload) (JsonResponse, error) + GetChanges(ctx context.Context, query ChangesGetQuery) (ChangesGetResponse, error) + GetChangesCount(ctx context.Context) (ChangesCountResponse, error) +} + +type ChangesService service + +func (c *ChangesService) GetChangesCount(ctx context.Context) (ChangesCountResponse, error) { + var resp ChangesCountResponse + URL := c.client.config.BackendURL.JoinPath("changes", "count") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesService) GetChanges(ctx context.Context, q ChangesGetQuery) (ChangesGetResponse, error) { + var resp ChangesGetResponse + URL := c.client.config.BackendURL.JoinPath("changes") + URL.RawQuery = fmt.Sprintf("promoted=%s&page=%s&limit=%s", q.Promoted, strconv.Itoa(q.Page), strconv.Itoa(q.Limit)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesService) ApplyChange(ctx context.Context, changeId string) (ChangesApplyResponse, error) { + var resp ChangesApplyResponse + URL := c.client.config.BackendURL.JoinPath("changes", changeId, "apply") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesService) ApplyBulkChanges(ctx context.Context, payload ChangesBulkApplyPayload) (ChangesApplyResponse, error) { + var resp ChangesApplyResponse + URL := c.client.config.BackendURL.JoinPath("changes", "bulk/apply") + jsonBody, err := json.Marshal(payload) + if err != nil { + return resp, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), bytes.NewBuffer(jsonBody)) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/lib/changes_test.go b/lib/changes_test.go new file mode 100644 index 0000000..68db13f --- /dev/null +++ b/lib/changes_test.go @@ -0,0 +1,267 @@ +package lib_test + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/novuhq/go-novu/lib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var bulkApplyPayload = `{ + "changeIds": [ + "string" + ] +}` + +var applyResponse = `{ + "data": [ + { + "_id": "string", + "_creatorId": "string", + "_environmentId": "string", + "_organizationId": "string", + "_entityId": "string", + "enabled": true, + "type": "Feed", + "change": {}, + "createdAt": "string", + "_parentId": "string" + } + ] +}` + +var getResponse = `{ + "totalCount": 0, + "data": [ + { + "_id": "string", + "_creatorId": "string", + "_environmentId": "string", + "_organizationId": "string", + "_entityId": "string", + "enabled": true, + "type": "Feed", + "change": {}, + "createdAt": "string", + "_parentId": "string" + } + ], + "pageSize": 0, + "page": 0 +} +` + +var getCountResponse = `{ + "data": 0 +}` + +func payloadStringToStruct(str string, s interface{}) error { + bb := []byte(str) + err := json.Unmarshal(bb, s) + if err != nil { + return err + } + return nil +} + +func TestChangesService_GetCount_Success(t *testing.T) { + var ( + expectedResponse lib.ChangesCountResponse + ) + + ChangesService := 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 := fmt.Sprintf("/v1/changes/count") + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + var resp lib.ChangesCountResponse + err := payloadStringToStruct(getCountResponse, &resp) + require.Nil(t, err) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + resp, err := c.ChangesApi.GetChangesCount(ctx) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + err := payloadStringToStruct(getCountResponse, &expectedResponse) + require.Nil(t, err) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestChangesService_Get_Success(t *testing.T) { + var ( + expectedResponse lib.ChangesGetResponse + ) + promoted := "yes" + page := 1 + limit := 10 + + ChangesService := 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 := fmt.Sprintf("/v1/changes?promoted=%s&page=%s&limit=%s", promoted, strconv.Itoa(page), strconv.Itoa(limit)) + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + var resp lib.ChangesGetResponse + err := payloadStringToStruct(getResponse, &resp) + require.Nil(t, err) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + q := lib.ChangesGetQuery{Page: 1, Limit: 10, Promoted: "yes"} + resp, err := c.ChangesApi.GetChanges(ctx, q) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + err := payloadStringToStruct(getResponse, &expectedResponse) + require.Nil(t, err) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestChangesService_Apply_Success(t *testing.T) { + const changeID = "62b51a44da1af31d109f5da7" + var ( + expectedResponse lib.ChangesApplyResponse + ) + + ChangesService := 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/changes/" + changeID + "/apply" + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + var resp lib.ChangesApplyResponse + // fileToStruct(filepath.Join("../testdata", "changes_apply_response.json"), &resp) + payloadStringToStruct(applyResponse, &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + + resp, err := c.ChangesApi.ApplyChange(ctx, changeID) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + // fileToStruct(filepath.Join("../testdata", "changes_apply_response.json"), &expectedResponse) + payloadStringToStruct(applyResponse, &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestChangesService_BulkApply_Success(t *testing.T) { + var ( + changesBulkApplyPayload lib.ChangesBulkApplyPayload + receivedBody lib.ChangesBulkApplyPayload + expectedRequest lib.ChangesBulkApplyPayload + expectedResponse lib.ChangesApplyResponse + ) + + ChangesService := 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/changes/bulk/apply" + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + t.Run("Request is as expected", func(t *testing.T) { + payloadStringToStruct(bulkApplyPayload, &expectedRequest) + assert.Equal(t, expectedRequest, receivedBody) + }) + + var resp lib.ChangesApplyResponse + payloadStringToStruct(applyResponse, &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + // fileToStruct(filepath.Join("../testdata", "changes_apply_payload.json"), &changesBulkApplyPayload) + payloadStringToStruct(bulkApplyPayload, &changesBulkApplyPayload) + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + + resp, err := c.ChangesApi.ApplyBulkChanges(ctx, changesBulkApplyPayload) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + payloadStringToStruct(applyResponse, &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} diff --git a/lib/model.go b/lib/model.go index 3c137d8..3f3171d 100644 --- a/lib/model.go +++ b/lib/model.go @@ -408,3 +408,41 @@ type MxRecordConfiguredStatus struct { type InboundParserResponse struct { Data MxRecordConfiguredStatus `json:"data"` } + +type ChangesGetQuery struct { + Page int `json:"page,omitempty"` + Limit int `json:"limit,omitempty"` + Promoted string `json:"promoted,omitempty"` +} + +type ChangesGetResponseData struct { + Id string `json:"_id,omitempty"` + CreatorId string `json:"_creatorId,omitempty"` + EnvironmentId string `json:"_environmentId,omitempty"` + OrganizationId string `json:"_organizationId,omitempty"` + EntityId string `json:"_entityId,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Type string `json:"type,omitempty"` + Change interface{} `json:"change,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + ParentId string `json:"_parentId,omitempty"` +} + +type ChangesGetResponse struct { + TotalCount int `json:"totalCount,omitempty"` + Data []ChangesGetResponseData `json:"data"` + PageSize int `json:"pageSize,omitempty"` + Page int `json:"page,omitempty"` +} + +type ChangesCountResponse struct { + Data int `json:"data"` +} + +type ChangesBulkApplyPayload struct { + ChangeIds []string `json:"changeIds"` +} + +type ChangesApplyResponse struct { + Data []ChangesGetResponseData `json:"data,omitempty"` +} diff --git a/lib/novu.go b/lib/novu.go index ce1b424..b58a696 100644 --- a/lib/novu.go +++ b/lib/novu.go @@ -28,6 +28,7 @@ type APIClient struct { common service // Api Service + ChangesApi *ChangesService SubscriberApi *SubscriberService EventApi *EventService ExecutionsApi *ExecutionsService @@ -54,6 +55,7 @@ func NewAPIClient(apiKey string, cfg *Config) *APIClient { c.common.client = c // API Services + c.ChangesApi = (*ChangesService)(&c.common) c.EventApi = (*EventService)(&c.common) c.ExecutionsApi = (*ExecutionsService)(&c.common) c.FeedsApi = (*FeedsService)(&c.common) From 2e3b47b4021c9e778c9e740bf20353cb448c94bc Mon Sep 17 00:00:00 2001 From: Talabi Opemipo Date: Fri, 6 Oct 2023 18:22:56 +0100 Subject: [PATCH 03/13] remove interface definition and comments --- lib/changes.go | 7 ------- lib/changes_test.go | 2 -- 2 files changed, 9 deletions(-) diff --git a/lib/changes.go b/lib/changes.go index d7dd610..2b6e830 100644 --- a/lib/changes.go +++ b/lib/changes.go @@ -9,13 +9,6 @@ import ( "strconv" ) -type IChanges interface { - ApplyChange(ctx context.Context, changeId string) (ChangesApplyResponse, error) - ApplyBulkChanges(ctx context.Context, payload ChangesBulkApplyPayload) (JsonResponse, error) - GetChanges(ctx context.Context, query ChangesGetQuery) (ChangesGetResponse, error) - GetChangesCount(ctx context.Context) (ChangesCountResponse, error) -} - type ChangesService service func (c *ChangesService) GetChangesCount(ctx context.Context) (ChangesCountResponse, error) { diff --git a/lib/changes_test.go b/lib/changes_test.go index 68db13f..651c2de 100644 --- a/lib/changes_test.go +++ b/lib/changes_test.go @@ -183,7 +183,6 @@ func TestChangesService_Apply_Success(t *testing.T) { }) var resp lib.ChangesApplyResponse - // fileToStruct(filepath.Join("../testdata", "changes_apply_response.json"), &resp) payloadStringToStruct(applyResponse, &resp) w.WriteHeader(http.StatusOK) @@ -251,7 +250,6 @@ func TestChangesService_BulkApply_Success(t *testing.T) { defer ChangesService.Close() ctx := context.Background() - // fileToStruct(filepath.Join("../testdata", "changes_apply_payload.json"), &changesBulkApplyPayload) payloadStringToStruct(bulkApplyPayload, &changesBulkApplyPayload) c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) From fb369d1f704c3a820a9692893469159f78f933c4 Mon Sep 17 00:00:00 2001 From: Partik Singh Date: Sun, 8 Oct 2023 00:54:43 +0530 Subject: [PATCH 04/13] Added Tenants API Signed-off-by: Partik Singh --- lib/model.go | 7 +++ lib/novu.go | 2 + lib/tenants.go | 90 +++++++++++++++++++++++++++++ lib/tenants_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 lib/tenants.go create mode 100644 lib/tenants_test.go diff --git a/lib/model.go b/lib/model.go index cf03654..6e23f9d 100644 --- a/lib/model.go +++ b/lib/model.go @@ -439,3 +439,10 @@ type BlueprintGroupByCategoryResponse struct { General []interface{} `json:"general,omitempty"` Popular interface{} `json:"popular,omitempty"` } + + +type UpdateTenantRequest struct { + Name string `json:"name"` + Data map[string]interface{} `json:"data"` + Identifier string `json:"identifier"` +} diff --git a/lib/novu.go b/lib/novu.go index 353566c..9b17aea 100644 --- a/lib/novu.go +++ b/lib/novu.go @@ -37,6 +37,7 @@ type APIClient struct { TopicsApi *TopicService IntegrationsApi *IntegrationService InboundParserApi *InboundParserService + TenantApi *TenantService } type service struct { @@ -64,6 +65,7 @@ func NewAPIClient(apiKey string, cfg *Config) *APIClient { c.IntegrationsApi = (*IntegrationService)(&c.common) c.InboundParserApi = (*InboundParserService)(&c.common) c.BlueprintApi = (*BlueprintService)(&c.common) + c.TenantApi = (*TenantService)(&c.common) return c } diff --git a/lib/tenants.go b/lib/tenants.go new file mode 100644 index 0000000..dc8f803 --- /dev/null +++ b/lib/tenants.go @@ -0,0 +1,90 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +type TenantService service + +func (e *TenantService) CreateTenant(ctx context.Context, name string,identifier string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants") + n := map[string]string{"name": name,"identifier":identifier} + jsonBody, _ := json.Marshal(n) + b := bytes.NewBuffer(jsonBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), b) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + +func (e *TenantService) GetTenants(ctx context.Context,page string,limit string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants") + v := URL.Query(); + v.Set("page",page) + v.Set("limit",limit) + URL.RawQuery = v.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + +func (e *TenantService) GetTenant(ctx context.Context,identifier string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants",identifier) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + +func (e *TenantService) DeleteTenant(ctx context.Context, identifier string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants", identifier) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + + +func (e *TenantService) UpdateTenant(ctx context.Context, identifier string,updateTenantObject *UpdateTenantRequest) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants", identifier) + jsonBody, _ := json.Marshal(updateTenantObject) + b := bytes.NewBuffer(jsonBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, URL.String(), b) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} diff --git a/lib/tenants_test.go b/lib/tenants_test.go new file mode 100644 index 0000000..d664313 --- /dev/null +++ b/lib/tenants_test.go @@ -0,0 +1,136 @@ +package lib_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/novuhq/go-novu/lib" +) + +var tenantsApiResponse = `{ + "data": { + "_environmentId": "string", + "_id": "string", + "createdAt": "string", + "data": "object", + "identifier": "string", + "name": "string", + "updatedAt": "string" + } + } + +` + +func TestCreateTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Want POST, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants" { + t.Errorf("Want /v1/tenants, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.CreateTenant(context.Background(), "Tenant", "TenantId") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestGetTenants(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("Want GET, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants" { + t.Errorf("Want /v1/tenants, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.GetTenants(context.Background(), "1", "10") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestGetTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("Want GET, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants/TenantId" { + t.Errorf("Want /v1/feeds, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.GetTenant(context.Background(), "TenantId") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestDeleteTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("Want DELETE, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants/TenantId" { + t.Errorf("Want /v1/tenants/TenantId, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.DeleteTenant(context.Background(), "TenantId") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestUpdateTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("Want PATCH, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants/TenantId" { + t.Errorf("Want /v1/tenants/TenantId, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.UpdateTenant(context.Background(), "TenantId", &lib.UpdateTenantRequest{ + Name: "Tenant2", + }) + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} From 9a7639508878c237bde46ccbd0ee45e66889a4e6 Mon Sep 17 00:00:00 2001 From: Talabi Opemipo Date: Sun, 8 Oct 2023 00:05:28 +0100 Subject: [PATCH 05/13] fix: changes query params and adjust test case --- lib/changes.go | 26 +++++++++++++++++++++++--- lib/changes_test.go | 8 ++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/changes.go b/lib/changes.go index 2b6e830..56dedf7 100644 --- a/lib/changes.go +++ b/lib/changes.go @@ -4,8 +4,8 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" + "net/url" "strconv" ) @@ -31,7 +31,7 @@ func (c *ChangesService) GetChangesCount(ctx context.Context) (ChangesCountRespo func (c *ChangesService) GetChanges(ctx context.Context, q ChangesGetQuery) (ChangesGetResponse, error) { var resp ChangesGetResponse URL := c.client.config.BackendURL.JoinPath("changes") - URL.RawQuery = fmt.Sprintf("promoted=%s&page=%s&limit=%s", q.Promoted, strconv.Itoa(q.Page), strconv.Itoa(q.Limit)) + URL.RawQuery = q.BuildQuery() req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) if err != nil { @@ -65,7 +65,7 @@ func (c *ChangesService) ApplyChange(ctx context.Context, changeId string) (Chan func (c *ChangesService) ApplyBulkChanges(ctx context.Context, payload ChangesBulkApplyPayload) (ChangesApplyResponse, error) { var resp ChangesApplyResponse - URL := c.client.config.BackendURL.JoinPath("changes", "bulk/apply") + URL := c.client.config.BackendURL.JoinPath("changes", "bulk", "apply") jsonBody, err := json.Marshal(payload) if err != nil { return resp, err @@ -82,3 +82,23 @@ func (c *ChangesService) ApplyBulkChanges(ctx context.Context, payload ChangesBu return resp, nil } + +func (c *ChangesGetQuery) BuildQuery() string { + params := url.Values{} + + if c.Page == 0 { + c.Page = 1 + } + + if c.Limit == 0 { + c.Limit = 10 + } + + if c.Promoted == "" { + c.Promoted = "false" + } + params.Add("page", strconv.Itoa(c.Page)) + params.Add("limit", strconv.Itoa(c.Limit)) + params.Add("promoted", c.Promoted) + return params.Encode() +} diff --git a/lib/changes_test.go b/lib/changes_test.go index 651c2de..c198183 100644 --- a/lib/changes_test.go +++ b/lib/changes_test.go @@ -120,7 +120,7 @@ func TestChangesService_Get_Success(t *testing.T) { var ( expectedResponse lib.ChangesGetResponse ) - promoted := "yes" + promoted := "false" page := 1 limit := 10 @@ -132,9 +132,9 @@ func TestChangesService_Get_Success(t *testing.T) { }) t.Run("URL and request method is as expected", func(t *testing.T) { - expectedURL := fmt.Sprintf("/v1/changes?promoted=%s&page=%s&limit=%s", promoted, strconv.Itoa(page), strconv.Itoa(limit)) + expectedURL := fmt.Sprintf("/v1/changes?limit=%s&page=%s&promoted=%s", strconv.Itoa(limit), strconv.Itoa(page), promoted) assert.Equal(t, http.MethodGet, req.Method) - assert.Equal(t, expectedURL, req.RequestURI) + assert.Equal(t, expectedURL, req.URL.String()) }) var resp lib.ChangesGetResponse @@ -151,7 +151,7 @@ func TestChangesService_Get_Success(t *testing.T) { ctx := context.Background() c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) - q := lib.ChangesGetQuery{Page: 1, Limit: 10, Promoted: "yes"} + q := lib.ChangesGetQuery{Page: 1, Limit: 10, Promoted: "false"} resp, err := c.ChangesApi.GetChanges(ctx, q) require.Nil(t, err) assert.NotNil(t, resp) From f60f6d4992512d36bb45d7dfb30b79c2b08e43a4 Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Sun, 8 Oct 2023 13:18:13 +0530 Subject: [PATCH 06/13] Add the new methods for integration endpoints --- lib/integration.go | 38 ++++++++++++++ lib/integration_test.go | 52 +++++++++++++++++++ lib/model.go | 26 +++++++++- .../integration_channel_limit_response.json | 6 +++ .../set_integration_as_primary_response.json | 43 +++++++++++++++ 5 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 testdata/integration_channel_limit_response.json create mode 100644 testdata/set_integration_as_primary_response.json diff --git a/lib/integration.go b/lib/integration.go index c783b44..2a77d10 100644 --- a/lib/integration.go +++ b/lib/integration.go @@ -14,6 +14,8 @@ type IIntegration interface { GetWebhookSupportStatus(ctx context.Context, providerId string) (bool, error) Update(ctx context.Context, integrationId string, request UpdateIntegrationRequest) (*IntegrationResponse, error) Delete(ctx context.Context, integrationId string) (*IntegrationResponse, error) + SetIntegrationAsPrimary(ctx context.Context, integrationId string) (*SetIntegrationAsPrimaryResponse, error) + GetChannelLimit(ctx context.Context, channelType string) (*IntegrationChannelLimitResponse, error) } type IntegrationService service @@ -148,3 +150,39 @@ func (i IntegrationService) Delete(ctx context.Context, integrationId string) (* return &response, nil } + +func (i IntegrationService) SetIntegrationAsPrimary(ctx context.Context, integrationId string) (*SetIntegrationAsPrimaryResponse, error) { + var response SetIntegrationAsPrimaryResponse + + URL := i.client.config.BackendURL.JoinPath("integrations", integrationId, "set-primary") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), http.NoBody) + if err != nil { + return nil, err + } + + _, err = i.client.sendRequest(req, &response) + if err != nil { + return nil, err + } + + return &response, nil +} + +func (i IntegrationService) GetChannelLimit(ctx context.Context, channelType string) (*IntegrationChannelLimitResponse, error) { + var response IntegrationChannelLimitResponse + + URL := i.client.config.BackendURL.JoinPath("integrations", channelType, "limit") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return nil, err + } + + _, err = i.client.sendRequest(req, &response) + if err != nil { + return nil, err + } + + return &response, nil +} diff --git a/lib/integration_test.go b/lib/integration_test.go index bcf1066..95ea33a 100644 --- a/lib/integration_test.go +++ b/lib/integration_test.go @@ -254,3 +254,55 @@ func TestDeleteActiveIntegration_Success(t *testing.T) { require.NoError(t, err) } + +func TestSetIntegrationAsPrimary_Success(t *testing.T) { + const integrationId = "IntegrationId" + + var response *lib.SetIntegrationAsPrimaryResponse + fileToStruct(filepath.Join("../testdata", "set_integration_as_primary_response.json"), &response) + + httpServer := IntegrationTestServer(t, IntegrationServerOptions[interface{}]{ + ExpectedRequest: IntegrationRequestDetails[interface{}]{ + Url: fmt.Sprintf("/v1/integrations/%s/set-primary", integrationId), + Method: http.MethodPost, + }, + ExpectedResponse: IntegrationResponseDetails{ + StatusCode: http.StatusOK, + Body: response, + }, + }) + + ctx := context.Background() + novuClient := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + + res, err := novuClient.IntegrationsApi.SetIntegrationAsPrimary(ctx, integrationId) + + assert.Equal(t, response, res) + require.NoError(t, err) +} + +func TestGetChannelLimit_Success(t *testing.T) { + const channelType = "ChannelType" + + var response *lib.IntegrationChannelLimitResponse + fileToStruct(filepath.Join("../testdata", "integration_channel_limit_response.json"), &response) + + httpServer := IntegrationTestServer(t, IntegrationServerOptions[interface{}]{ + ExpectedRequest: IntegrationRequestDetails[interface{}]{ + Url: fmt.Sprintf("/v1/integrations/%s/limit", channelType), + Method: http.MethodGet, + }, + ExpectedResponse: IntegrationResponseDetails{ + StatusCode: http.StatusOK, + Body: response, + }, + }) + + ctx := context.Background() + novuClient := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + + res, err := novuClient.IntegrationsApi.GetChannelLimit(ctx, channelType) + + assert.Equal(t, response, res) + require.NoError(t, err) +} diff --git a/lib/model.go b/lib/model.go index cf03654..9d36374 100644 --- a/lib/model.go +++ b/lib/model.go @@ -382,6 +382,30 @@ type GetIntegrationsResponse struct { Data []Integration `json:"data"` } +type IntegrationChannelLimitResponse struct { + Data struct { + Limit int `json:"limit"` + Count int `json:"count"` + } `json:"data"` +} + +type SetIntegrationAsPrimaryResponse struct { + Data struct { + ID string `json:"_id"` + EnvironmentID string `json:"_environmentId"` + OrganizationID string `json:"_organizationId"` + Name string `json:"name"` + Identifier string `json:"identifier"` + ProviderID string `json:"providerId"` + Channel string `json:"channel"` + Credentials IntegrationCredentials `json:"credentials"` + Active bool `json:"active"` + Deleted bool `json:"deleted"` + DeletedAt string `json:"deletedAt"` + DeletedBy string `json:"deletedBy"` + Primary bool `json:"primary"` + } `json:"data"` +} type BulkTriggerOptions struct { Name interface{} `json:"name,omitempty"` To interface{} `json:"to,omitempty"` @@ -437,5 +461,5 @@ type BlueprintByTemplateIdResponse struct { type BlueprintGroupByCategoryResponse struct { General []interface{} `json:"general,omitempty"` - Popular interface{} `json:"popular,omitempty"` + Popular interface{} `json:"popular,omitempty"` } diff --git a/testdata/integration_channel_limit_response.json b/testdata/integration_channel_limit_response.json new file mode 100644 index 0000000..7f67af3 --- /dev/null +++ b/testdata/integration_channel_limit_response.json @@ -0,0 +1,6 @@ +{ + "data": { + "limit": 0, + "count": 0 + } +} diff --git a/testdata/set_integration_as_primary_response.json b/testdata/set_integration_as_primary_response.json new file mode 100644 index 0000000..b0eeb0f --- /dev/null +++ b/testdata/set_integration_as_primary_response.json @@ -0,0 +1,43 @@ +{ + "data": { + "_id": "string", + "_environmentId": "string", + "_organizationId": "string", + "name": "string", + "identifier": "string", + "providerId": "string", + "channel": "in_app", + "credentials": { + "apiKey": "string", + "user": "string", + "secretKey": "string", + "domain": "string", + "password": "string", + "host": "string", + "port": "string", + "secure": true, + "region": "string", + "accountSid": "string", + "messageProfileId": "string", + "token": "string", + "from": "string", + "senderName": "string", + "projectName": "string", + "applicationId": "string", + "clientId": "string", + "requireTls": true, + "ignoreTls": true, + "baseUrl": "string", + "webhookUrl": "string", + "redirectUrl": "string", + "hmac": true, + "serviceAccount": "string", + "ipPoolName": "string" + }, + "active": true, + "deleted": true, + "deletedAt": "string", + "deletedBy": "string", + "primary": true + } +} From 3193cf36c1762d03d61566f6b271a8f75defbd1a Mon Sep 17 00:00:00 2001 From: Eshaan Aggarwal <96648934+EshaanAgg@users.noreply.github.com> Date: Sun, 8 Oct 2023 13:26:40 +0530 Subject: [PATCH 07/13] Add documentation of the endpoints in README.md --- README.md | 58 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c387200..459e45a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ 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 ``` @@ -61,35 +63,39 @@ func main() { fmt.Println(integrations) } ``` + **NOTE** Check the `cmd` directory to see a sample implementation and test files to see sample tests ## Documentation for API Endpoints -Class | Method | HTTP request | Description ------------- |----------------------------------------------------------------------------------|-----------------------------------------| ------------- -*EventApi* | [**Trigger**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Post** /events/trigger | Trigger -*EventApi* | [**TriggerBulk**](https://docs.novu.co/api/trigger-event/) | **Post** /v1/events/trigger/bulk | Bulk trigger event -*EventApi* | [**BroadcastToAll**](https://docs.novu.co/api/broadcast-event-to-all/) | **Post** /v1/events/trigger/broadcast | Broadcast event to all -*EventApi* | [**CancelTrigger**](https://docs.novu.co/api/cancel-triggered-event/) | **Delete** /v1/events/trigger/:transactionId | Cancel triggered event -*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-subscriber/) | **Get** /subscribers/:subscriberId | Get a subscriber -*SubscriberApi* | [**Identify**](https://docs.novu.co/platform/subscribers#creating-a-subscriber) | **Post** /subscribers | Create a subscriber -*SubscriberApi* | [**Update**](https://docs.novu.co/platform/subscribers#updating-subscriber-data) | **Put** /subscribers/:subscriberID | Update subscriber data -*SubscriberApi* | [**Delete**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Delete** /subscribers/:subscriberID | Removing a subscriber -*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-a-notification-feed-for-a-particular-subscriber) | **Get** /subscribers/:subscriberId/notifications/feed | Get a notification feed for a particular subscriber -*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-the-unseen-notification-count-for-subscribers-feed) | **Get** /subscribers/:subscriberId/notifications/feed | Get the unseen notification count for subscribers feed -*SubscriberApi* | [**Post**](https://docs.novu.co/api/mark-a-subscriber-feed-message-as-seen) | **Post** /v1/subscribers/:subscriberId/messages/markAs | Mark a subscriber feed message as seen -*SubscriberApi* | [**Get**](https://docs.novu.co/api/get-subscriber-preferences/) | **Get** /subscribers/:subscriberId/preferences | Get subscriber preferences -*SubscriberApi* | [**Patch**](https://docs.novu.co/api/update-subscriber-preference/) | **Patch** /subscribers/:subscriberId/preferences/:templateId | Update subscriber preference -*IntegrationsApi* | [**Create**](https://docs.novu.co/platform/integrations) | **Post** /integrations | Create an integration -*IntegrationsApi* | [**Update**](https://docs.novu.co/platform/integrations) | **Put** /integrations/:integrationId | Update an integration -*IntegrationsApi* | [**Delete**](https://docs.novu.co/platform/integrations) | **Delete** /integrations/:integrationId | Delete an integration -*IntegrationsApi* | [**Get**](https://docs.novu.co/platform/integrations) | **Get** /integrations | Get all integrations -*IntegrationsApi* | [**GetActive**](https://docs.novu.co/platform/intergations) | **Get** /integrations/active | Get all active integrations - -*InboundParserApi* | [**Get**](https://docs.novu.co/platform/inbound-parse-webhook/) | **Get** /inbound-parse/mx/status | Validate the mx record setup for the inbound parse functionality +| Class | Method | HTTP request | Description | +| ----------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------ | +| _EventApi_ | [**Trigger**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Post** /events/trigger | Trigger | +| _EventApi_ | [**TriggerBulk**](https://docs.novu.co/api/trigger-event/) | **Post** /v1/events/trigger/bulk | Bulk trigger event | +| _EventApi_ | [**BroadcastToAll**](https://docs.novu.co/api/broadcast-event-to-all/) | **Post** /v1/events/trigger/broadcast | Broadcast event to all | +| _EventApi_ | [**CancelTrigger**](https://docs.novu.co/api/cancel-triggered-event/) | **Delete** /v1/events/trigger/:transactionId | Cancel triggered event | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-subscriber/) | **Get** /subscribers/:subscriberId | Get a subscriber | +| _SubscriberApi_ | [**Identify**](https://docs.novu.co/platform/subscribers#creating-a-subscriber) | **Post** /subscribers | Create a subscriber | +| _SubscriberApi_ | [**Update**](https://docs.novu.co/platform/subscribers#updating-subscriber-data) | **Put** /subscribers/:subscriberID | Update subscriber data | +| _SubscriberApi_ | [**Delete**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Delete** /subscribers/:subscriberID | Removing a subscriber | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-a-notification-feed-for-a-particular-subscriber) | **Get** /subscribers/:subscriberId/notifications/feed | Get a notification feed for a particular subscriber | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-the-unseen-notification-count-for-subscribers-feed) | **Get** /subscribers/:subscriberId/notifications/feed | Get the unseen notification count for subscribers feed | +| _SubscriberApi_ | [**Post**](https://docs.novu.co/api/mark-a-subscriber-feed-message-as-seen) | **Post** /v1/subscribers/:subscriberId/messages/markAs | Mark a subscriber feed message as seen | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-subscriber-preferences/) | **Get** /subscribers/:subscriberId/preferences | Get subscriber preferences | +| _SubscriberApi_ | [**Patch**](https://docs.novu.co/api/update-subscriber-preference/) | **Patch** /subscribers/:subscriberId/preferences/:templateId | Update subscriber preference | +| _IntegrationsApi_ | [**Create**](https://docs.novu.co/platform/integrations) | **Post** /integrations | Create an integration | +| _IntegrationsApi_ | [**Update**](https://docs.novu.co/platform/integrations) | **Put** /integrations/:integrationId | Update an integration | +| _IntegrationsApi_ | [**Delete**](https://docs.novu.co/platform/integrations) | **Delete** /integrations/:integrationId | Delete an integration | +| _IntegrationsApi_ | [**Get**](https://docs.novu.co/platform/integrations) | **Get** /integrations | Get all integrations | +| _IntegrationsApi_ | [**GetActive**](https://docs.novu.co/platform/intergations) | **Get** /integrations/active | Get all active integrations | +| _IntegrationsApi_ | [**SetIntegrationAsPrimary**](https://docs.novu.co/platform/intergations) | **Post** /integrations/{integrationId}/set-primary | Set the integration as primary | +| _IntegrationsApi_ | [**GetChannelLimit**](https://docs.novu.co/platform/intergations) | **Get** /integrations/{channelType}/limit | Get the limits of the channel | + +_InboundParserApi_ | [**Get**](https://docs.novu.co/platform/inbound-parse-webhook/) | **Get** /inbound-parse/mx/status | Validate the mx record setup for the inbound parse functionality ## Authorization (api-key) + ## Authorization (api-key) - **Type**: API key @@ -106,7 +112,7 @@ As always, if you need additional assistance, join our Discord us a note [here]( ## Contributors -Name | ------------- | -[Oyewole Samuel](https://github.com/samsoft00) | -[Dima Grossman](https://github.com/scopsy) | +| Name | +| ---------------------------------------------- | +| [Oyewole Samuel](https://github.com/samsoft00) | +| [Dima Grossman](https://github.com/scopsy) | From 02081437091a0bce2073de679a64fdaedb910c1d Mon Sep 17 00:00:00 2001 From: srikanth597 Date: Sun, 8 Oct 2023 19:31:05 +0530 Subject: [PATCH 08/13] feat: support Layout API Methods --- lib/layout.go | 133 +++++++++++++++++++++++++++++ lib/layout_test.go | 207 +++++++++++++++++++++++++++++++++++++++++++++ lib/model.go | 45 ++++++++++ lib/novu.go | 2 + 4 files changed, 387 insertions(+) create mode 100644 lib/layout.go create mode 100644 lib/layout_test.go diff --git a/lib/layout.go b/lib/layout.go new file mode 100644 index 0000000..4f6672f --- /dev/null +++ b/lib/layout.go @@ -0,0 +1,133 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +type LayoutService service + +func (l *LayoutService) Create(ctx context.Context, request CreateLayoutRequest) (*CreateLayoutResponse, error) { + var resp CreateLayoutResponse + URL := l.client.config.BackendURL.JoinPath("layouts") + + requestBody := CreateLayoutRequest{ + Name: request.Name, + Identifier: request.Identifier, + Description: request.Description, + Content: request.Content, + Variables: request.Variables, + IsDefault: request.IsDefault, + } + + jsonBody, _ := json.Marshal(requestBody) + b := bytes.NewBuffer(jsonBody) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), b) + if err != nil { + return nil, err + } + _, err = l.client.sendRequest(req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (l *LayoutService) List(ctx context.Context, options *LayoutRequestOptions) (*LayoutsResponse, error) { + var resp LayoutsResponse + URL := l.client.config.BackendURL.JoinPath("layouts") + if options == nil { + options = &LayoutRequestOptions{} + } + queryParams, _ := json.Marshal(options) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), bytes.NewBuffer(queryParams)) + if err != nil { + return nil, err + } + + _, err = l.client.sendRequest(req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (l *LayoutService) Get(ctx context.Context, key string) (*LayoutResponse, error) { + var resp LayoutResponse + URL := l.client.config.BackendURL.JoinPath("layouts", key) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), bytes.NewBuffer([]byte{})) + if err != nil { + return nil, err + } + + _, err = l.client.sendRequest(req, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +func (l *LayoutService) Delete(ctx context.Context, key string) error { + var resp interface{} + URL := l.client.config.BackendURL.JoinPath("layouts", key) + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, URL.String(), http.NoBody) + if err != nil { + return err + } + _, err = l.client.sendRequest(req, &resp) + if err != nil { + return err + } + return nil +} + +func (l *LayoutService) Update(ctx context.Context, key string, request CreateLayoutRequest) (*LayoutResponse, error) { + var resp LayoutResponse + URL := l.client.config.BackendURL.JoinPath("layouts", key) + + requestBody := CreateLayoutRequest{ + Name: request.Name, + Identifier: request.Identifier, + Description: request.Description, + Content: request.Content, + Variables: request.Variables, + IsDefault: request.IsDefault, + } + + jsonBody, _ := json.Marshal(requestBody) + + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, URL.String(), bytes.NewBuffer(jsonBody)) + if err != nil { + return nil, err + } + + _, err = l.client.sendRequest(req, &resp) + if err != nil { + return nil, err + } + + return &resp, nil +} + +func (l *LayoutService) SetDefault(ctx context.Context, key string) error { + var resp interface{} + URL := l.client.config.BackendURL.JoinPath("layouts", key, "default") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), http.NoBody) + if err != nil { + return err + } + + _, err = l.client.sendRequest(req, &resp) + if err != nil { + return err + } + + return nil +} diff --git a/lib/layout_test.go b/lib/layout_test.go new file mode 100644 index 0000000..9b6e8b7 --- /dev/null +++ b/lib/layout_test.go @@ -0,0 +1,207 @@ +package lib_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/novuhq/go-novu/lib" + "github.com/stretchr/testify/require" +) + +const LayoutId = "2222" + +func TestLayoutService_Create_Layout_Success(t *testing.T) { + var createLayoutRequest *lib.CreateLayoutRequest = &lib.CreateLayoutRequest{ + Name: "layoutName", + Identifier: "layoutIdentifier", + Description: "layoutDescription", + Content: "layoutContent", + Variables: []interface{}(nil), + IsDefault: true, + } + res, _ := json.Marshal(createLayoutRequest) + fmt.Println(string(res)) + var expectedResponse *lib.CreateLayoutResponse = &lib.CreateLayoutResponse{ + Data: struct { + Id string `json:"_id"` + }{ + Id: "2222", + }, + } + + httpServer := createTestServer(t, TestServerOptions[lib.CreateLayoutRequest, lib.CreateLayoutResponse]{ + expectedURLPath: "/v1/layouts", + expectedSentBody: *createLayoutRequest, + expectedSentMethod: http.MethodPost, + responseStatusCode: http.StatusCreated, + responseBody: *expectedResponse, + }) + + ctx := context.Background() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + resp, err := c.LayoutApi.Create(ctx, *createLayoutRequest) + + require.NoError(t, err) + require.Equal(t, expectedResponse, resp) +} + +func TestLayoutService_List_Layouts_Success(t *testing.T) { + body := map[string]string{} + var expectedResponse *lib.LayoutsResponse = &lib.LayoutsResponse{ + Page: 0, + PageSize: 20, + TotalCount: 1, + Data: []lib.LayoutResponse{{ + Id: "id", + OrganizationId: "orgId", + EnvironmentId: "envId", + CreatorId: "creatorId", + Name: "layoutName", + Identifier: "layoutIdentifier", + Description: "layoutDescription", + Channel: "in_app", + Content: "layoutContent", + ContentType: "layoutContentType", + Variables: []interface{}{}, + IsDefault: true, + IsDeleted: false, + CreatedAt: "createdAt", + UpdatedAt: "updatedAt", + ParentId: "parentId", + }}, + } + httpServer := createTestServer(t, TestServerOptions[map[string]string, lib.LayoutsResponse]{ + expectedURLPath: "/v1/layouts", + expectedSentMethod: http.MethodGet, + expectedSentBody: body, + responseStatusCode: http.StatusCreated, + responseBody: *expectedResponse, + }) + + ctx := context.Background() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + resp, err := c.LayoutApi.List(ctx, nil) + + require.NoError(t, err) + require.Equal(t, expectedResponse, resp) +} + +func TestLayoutService_Get_Layout_Success(t *testing.T) { + + var expectedResponse *lib.LayoutResponse = &lib.LayoutResponse{ + Id: "id", + OrganizationId: "orgId", + EnvironmentId: "envId", + CreatorId: "creatorId", + Name: "layoutName", + Identifier: "layoutIdentifier", + Description: "layoutDescription", + Channel: "in_app", + Content: "layoutContent", + ContentType: "layoutContentType", + Variables: []interface{}{}, + IsDefault: true, + IsDeleted: false, + CreatedAt: "createdAt", + UpdatedAt: "updatedAt", + ParentId: "parentId", + } + + httpServer := createTestServer(t, TestServerOptions[map[string]string, lib.LayoutResponse]{ + expectedURLPath: fmt.Sprintf("/v1/layouts/%s", LayoutId), + expectedSentMethod: http.MethodGet, + responseStatusCode: http.StatusOK, + responseBody: *expectedResponse, + }) + + ctx := context.Background() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + resp, err := c.LayoutApi.Get(ctx, "2222") + + require.NoError(t, err) + require.Equal(t, expectedResponse, resp) +} + +func TestLayoutService_Delete_Layout_Success(t *testing.T) { + + body := map[string]string{} + + httpServer := createTestServer(t, TestServerOptions[map[string]string, map[string]string]{ + expectedURLPath: fmt.Sprintf("/v1/layouts/%s", LayoutId), + expectedSentMethod: http.MethodDelete, + responseStatusCode: http.StatusOK, + responseBody: body, + }) + + ctx := context.Background() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + err := c.LayoutApi.Delete(ctx, "2222") + + require.NoError(t, err) +} + +func TestLayoutService_Update_Layout_Success(t *testing.T) { + + var updateLayoutRequest *lib.CreateLayoutRequest = &lib.CreateLayoutRequest{ + Name: "layoutName", + Identifier: "layoutIdentifier", + Description: "layoutDescription", + Content: "layoutContent", + Variables: []interface{}(nil), + IsDefault: false, + } + res, _ := json.Marshal(updateLayoutRequest) + fmt.Println(string(res)) + var expectedResponse *lib.LayoutResponse = &lib.LayoutResponse{ + Id: "id", + OrganizationId: "orgId", + EnvironmentId: "envId", + CreatorId: "creatorId", + Name: "layoutName", + Identifier: "layoutIdentifier", + Description: "layoutDescription", + Channel: "in_app", + Content: "layoutContent", + ContentType: "layoutContentType", + Variables: []interface{}{}, + IsDefault: true, + IsDeleted: false, + CreatedAt: "createdAt", + UpdatedAt: "updatedAt", + ParentId: "parentId", + } + httpServer := createTestServer[lib.CreateLayoutRequest, lib.LayoutResponse](t, TestServerOptions[lib.CreateLayoutRequest, lib.LayoutResponse]{ + expectedURLPath: fmt.Sprintf("/v1/layouts/%s", LayoutId), + expectedSentBody: *updateLayoutRequest, + expectedSentMethod: http.MethodPatch, + responseStatusCode: http.StatusOK, + responseBody: *expectedResponse, + }) + ctx := context.Background() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + resp, err := c.LayoutApi.Update(ctx, "2222", *updateLayoutRequest) + require.NoError(t, err) + require.Equal(t, expectedResponse, resp) +} + +func TestLayoutService_Layout_SetDefault(t *testing.T) { + + body := map[string]string{} + + httpServer := createTestServer(t, TestServerOptions[map[string]string, map[string]string]{ + expectedURLPath: fmt.Sprintf("/v1/layouts/%s/default", LayoutId), + expectedSentBody: body, + expectedSentMethod: http.MethodPost, + responseStatusCode: http.StatusNoContent, + responseBody: body, + }) + + ctx := context.Background() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(httpServer.URL)}) + err := c.LayoutApi.SetDefault(ctx, LayoutId) + + require.NoError(t, err) +} diff --git a/lib/model.go b/lib/model.go index 3c137d8..95fa3af 100644 --- a/lib/model.go +++ b/lib/model.go @@ -408,3 +408,48 @@ type MxRecordConfiguredStatus struct { type InboundParserResponse struct { Data MxRecordConfiguredStatus `json:"data"` } + +type CreateLayoutRequest struct { + Name string `json:"name"` + Identifier string `json:"identifier"` + Description string `json:"description"` + Content string `json:"content"` + Variables []interface{} `json:"variables,omitempty"` + IsDefault bool `json:"isDefault,omitempty"` +} + +type CreateLayoutResponse struct { + Data struct { + Id string `json:"_id"` + } `json:"data"` +} +type LayoutRequestOptions struct { + Page *int `json:"page,omitempty"` + PageSize *int `json:"pageSize,omitempty"` + Key *string `json:"key,omitempty"` + OrderBy *int `json:"orderBy,omitempty"` +} +type LayoutResponse struct { + Id string `json:"_id"` + OrganizationId string `json:"_organizationId"` + EnvironmentId string `json:"_environmentId"` + CreatorId string `json:"_creatorId"` + Name string `json:"name"` + Identifier string `json:"identifier"` + Description string `json:"description"` + Channel string `json:"channel"` + Content string `json:"content"` + ContentType string `json:"contentType"` + Variables []interface{} `json:"variables"` + IsDefault bool `json:"isDefault"` + IsDeleted bool `json:"isDeleted"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + ParentId string `json:"_parentId"` +} +type LayoutsResponse struct { + TotalCount int `json:"totalCount"` + Data []LayoutResponse `json:"data"` + PageSize int `json:"pageSize"` + Page int `json:"page"` +} diff --git a/lib/novu.go b/lib/novu.go index ce1b424..85cf5c4 100644 --- a/lib/novu.go +++ b/lib/novu.go @@ -36,6 +36,7 @@ type APIClient struct { TopicsApi *TopicService IntegrationsApi *IntegrationService InboundParserApi *InboundParserService + LayoutApi *LayoutService } type service struct { @@ -62,6 +63,7 @@ func NewAPIClient(apiKey string, cfg *Config) *APIClient { c.TopicsApi = (*TopicService)(&c.common) c.IntegrationsApi = (*IntegrationService)(&c.common) c.InboundParserApi = (*InboundParserService)(&c.common) + c.LayoutApi = (*LayoutService)(&c.common) return c } From 60c635901f54793b7606b7a92968ae91fcfb0bb5 Mon Sep 17 00:00:00 2001 From: srikanth597 Date: Wed, 11 Oct 2023 22:44:02 +0530 Subject: [PATCH 09/13] Resolve Conflicts --- lib/changes.go | 104 +++++++++++++++++ lib/changes_test.go | 265 ++++++++++++++++++++++++++++++++++++++++++++ lib/model.go | 45 ++++++++ lib/novu.go | 4 + lib/tenants.go | 90 +++++++++++++++ lib/tenants_test.go | 136 +++++++++++++++++++++++ 6 files changed, 644 insertions(+) create mode 100644 lib/changes.go create mode 100644 lib/changes_test.go create mode 100644 lib/tenants.go create mode 100644 lib/tenants_test.go diff --git a/lib/changes.go b/lib/changes.go new file mode 100644 index 0000000..56dedf7 --- /dev/null +++ b/lib/changes.go @@ -0,0 +1,104 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/url" + "strconv" +) + +type ChangesService service + +func (c *ChangesService) GetChangesCount(ctx context.Context) (ChangesCountResponse, error) { + var resp ChangesCountResponse + URL := c.client.config.BackendURL.JoinPath("changes", "count") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesService) GetChanges(ctx context.Context, q ChangesGetQuery) (ChangesGetResponse, error) { + var resp ChangesGetResponse + URL := c.client.config.BackendURL.JoinPath("changes") + URL.RawQuery = q.BuildQuery() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesService) ApplyChange(ctx context.Context, changeId string) (ChangesApplyResponse, error) { + var resp ChangesApplyResponse + URL := c.client.config.BackendURL.JoinPath("changes", changeId, "apply") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesService) ApplyBulkChanges(ctx context.Context, payload ChangesBulkApplyPayload) (ChangesApplyResponse, error) { + var resp ChangesApplyResponse + URL := c.client.config.BackendURL.JoinPath("changes", "bulk", "apply") + jsonBody, err := json.Marshal(payload) + if err != nil { + return resp, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), bytes.NewBuffer(jsonBody)) + if err != nil { + return resp, err + } + + _, err = c.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + + return resp, nil +} + +func (c *ChangesGetQuery) BuildQuery() string { + params := url.Values{} + + if c.Page == 0 { + c.Page = 1 + } + + if c.Limit == 0 { + c.Limit = 10 + } + + if c.Promoted == "" { + c.Promoted = "false" + } + params.Add("page", strconv.Itoa(c.Page)) + params.Add("limit", strconv.Itoa(c.Limit)) + params.Add("promoted", c.Promoted) + return params.Encode() +} diff --git a/lib/changes_test.go b/lib/changes_test.go new file mode 100644 index 0000000..c198183 --- /dev/null +++ b/lib/changes_test.go @@ -0,0 +1,265 @@ +package lib_test + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/novuhq/go-novu/lib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var bulkApplyPayload = `{ + "changeIds": [ + "string" + ] +}` + +var applyResponse = `{ + "data": [ + { + "_id": "string", + "_creatorId": "string", + "_environmentId": "string", + "_organizationId": "string", + "_entityId": "string", + "enabled": true, + "type": "Feed", + "change": {}, + "createdAt": "string", + "_parentId": "string" + } + ] +}` + +var getResponse = `{ + "totalCount": 0, + "data": [ + { + "_id": "string", + "_creatorId": "string", + "_environmentId": "string", + "_organizationId": "string", + "_entityId": "string", + "enabled": true, + "type": "Feed", + "change": {}, + "createdAt": "string", + "_parentId": "string" + } + ], + "pageSize": 0, + "page": 0 +} +` + +var getCountResponse = `{ + "data": 0 +}` + +func payloadStringToStruct(str string, s interface{}) error { + bb := []byte(str) + err := json.Unmarshal(bb, s) + if err != nil { + return err + } + return nil +} + +func TestChangesService_GetCount_Success(t *testing.T) { + var ( + expectedResponse lib.ChangesCountResponse + ) + + ChangesService := 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 := fmt.Sprintf("/v1/changes/count") + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + var resp lib.ChangesCountResponse + err := payloadStringToStruct(getCountResponse, &resp) + require.Nil(t, err) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + resp, err := c.ChangesApi.GetChangesCount(ctx) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + err := payloadStringToStruct(getCountResponse, &expectedResponse) + require.Nil(t, err) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestChangesService_Get_Success(t *testing.T) { + var ( + expectedResponse lib.ChangesGetResponse + ) + promoted := "false" + page := 1 + limit := 10 + + ChangesService := 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 := fmt.Sprintf("/v1/changes?limit=%s&page=%s&promoted=%s", strconv.Itoa(limit), strconv.Itoa(page), promoted) + assert.Equal(t, http.MethodGet, req.Method) + assert.Equal(t, expectedURL, req.URL.String()) + }) + + var resp lib.ChangesGetResponse + err := payloadStringToStruct(getResponse, &resp) + require.Nil(t, err) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + q := lib.ChangesGetQuery{Page: 1, Limit: 10, Promoted: "false"} + resp, err := c.ChangesApi.GetChanges(ctx, q) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + err := payloadStringToStruct(getResponse, &expectedResponse) + require.Nil(t, err) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestChangesService_Apply_Success(t *testing.T) { + const changeID = "62b51a44da1af31d109f5da7" + var ( + expectedResponse lib.ChangesApplyResponse + ) + + ChangesService := 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/changes/" + changeID + "/apply" + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + var resp lib.ChangesApplyResponse + payloadStringToStruct(applyResponse, &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + + resp, err := c.ChangesApi.ApplyChange(ctx, changeID) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + // fileToStruct(filepath.Join("../testdata", "changes_apply_response.json"), &expectedResponse) + payloadStringToStruct(applyResponse, &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} + +func TestChangesService_BulkApply_Success(t *testing.T) { + var ( + changesBulkApplyPayload lib.ChangesBulkApplyPayload + receivedBody lib.ChangesBulkApplyPayload + expectedRequest lib.ChangesBulkApplyPayload + expectedResponse lib.ChangesApplyResponse + ) + + ChangesService := 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/changes/bulk/apply" + assert.Equal(t, http.MethodPost, req.Method) + assert.Equal(t, expectedURL, req.RequestURI) + }) + + t.Run("Request is as expected", func(t *testing.T) { + payloadStringToStruct(bulkApplyPayload, &expectedRequest) + assert.Equal(t, expectedRequest, receivedBody) + }) + + var resp lib.ChangesApplyResponse + payloadStringToStruct(applyResponse, &resp) + + w.WriteHeader(http.StatusOK) + bb, _ := json.Marshal(resp) + w.Write(bb) + })) + + defer ChangesService.Close() + + ctx := context.Background() + payloadStringToStruct(bulkApplyPayload, &changesBulkApplyPayload) + + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(ChangesService.URL)}) + + resp, err := c.ChangesApi.ApplyBulkChanges(ctx, changesBulkApplyPayload) + require.Nil(t, err) + assert.NotNil(t, resp) + + t.Run("Response is as expected", func(t *testing.T) { + payloadStringToStruct(applyResponse, &expectedResponse) + assert.Equal(t, expectedResponse, resp) + }) +} diff --git a/lib/model.go b/lib/model.go index 906a057..1c35906 100644 --- a/lib/model.go +++ b/lib/model.go @@ -483,3 +483,48 @@ type BlueprintGroupByCategoryResponse struct { General []interface{} `json:"general,omitempty"` Popular interface{} `json:"popular,omitempty"` } + +type ChangesGetQuery struct { + Page int `json:"page,omitempty"` + Limit int `json:"limit,omitempty"` + Promoted string `json:"promoted,omitempty"` +} + +type ChangesGetResponseData struct { + Id string `json:"_id,omitempty"` + CreatorId string `json:"_creatorId,omitempty"` + EnvironmentId string `json:"_environmentId,omitempty"` + OrganizationId string `json:"_organizationId,omitempty"` + EntityId string `json:"_entityId,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Type string `json:"type,omitempty"` + Change interface{} `json:"change,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + ParentId string `json:"_parentId,omitempty"` +} + +type ChangesGetResponse struct { + TotalCount int `json:"totalCount,omitempty"` + Data []ChangesGetResponseData `json:"data"` + PageSize int `json:"pageSize,omitempty"` + Page int `json:"page,omitempty"` +} + +type ChangesCountResponse struct { + Data int `json:"data"` +} + +type ChangesBulkApplyPayload struct { + ChangeIds []string `json:"changeIds"` +} + +type ChangesApplyResponse struct { + Data []ChangesGetResponseData `json:"data,omitempty"` +} + + +type UpdateTenantRequest struct { + Name string `json:"name"` + Data map[string]interface{} `json:"data"` + Identifier string `json:"identifier"` +} diff --git a/lib/novu.go b/lib/novu.go index 6f433a2..312460f 100644 --- a/lib/novu.go +++ b/lib/novu.go @@ -41,6 +41,7 @@ type APIClient struct { // Api Service BlueprintApi *BlueprintService + ChangesApi *ChangesService SubscriberApi *SubscriberService EventApi *EventService ExecutionsApi *ExecutionsService @@ -50,6 +51,7 @@ type APIClient struct { IntegrationsApi *IntegrationService InboundParserApi *InboundParserService LayoutApi *LayoutService + TenantApi *TenantService } type service struct { @@ -98,6 +100,7 @@ func NewAPIClient(apiKey string, cfg *Config) *APIClient { c.common.client = c // API Services + c.ChangesApi = (*ChangesService)(&c.common) c.EventApi = (*EventService)(&c.common) c.ExecutionsApi = (*ExecutionsService)(&c.common) c.FeedsApi = (*FeedsService)(&c.common) @@ -108,6 +111,7 @@ func NewAPIClient(apiKey string, cfg *Config) *APIClient { c.InboundParserApi = (*InboundParserService)(&c.common) c.LayoutApi = (*LayoutService)(&c.common) c.BlueprintApi = (*BlueprintService)(&c.common) + c.TenantApi = (*TenantService)(&c.common) return c } diff --git a/lib/tenants.go b/lib/tenants.go new file mode 100644 index 0000000..dc8f803 --- /dev/null +++ b/lib/tenants.go @@ -0,0 +1,90 @@ +package lib + +import ( + "bytes" + "context" + "encoding/json" + "net/http" +) + +type TenantService service + +func (e *TenantService) CreateTenant(ctx context.Context, name string,identifier string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants") + n := map[string]string{"name": name,"identifier":identifier} + jsonBody, _ := json.Marshal(n) + b := bytes.NewBuffer(jsonBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, URL.String(), b) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + +func (e *TenantService) GetTenants(ctx context.Context,page string,limit string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants") + v := URL.Query(); + v.Set("page",page) + v.Set("limit",limit) + URL.RawQuery = v.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + +func (e *TenantService) GetTenant(ctx context.Context,identifier string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants",identifier) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + +func (e *TenantService) DeleteTenant(ctx context.Context, identifier string) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants", identifier) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, URL.String(), http.NoBody) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} + + +func (e *TenantService) UpdateTenant(ctx context.Context, identifier string,updateTenantObject *UpdateTenantRequest) (JsonResponse, error) { + var resp JsonResponse + URL := e.client.config.BackendURL.JoinPath("tenants", identifier) + jsonBody, _ := json.Marshal(updateTenantObject) + b := bytes.NewBuffer(jsonBody) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, URL.String(), b) + if err != nil { + return resp, err + } + _, err = e.client.sendRequest(req, &resp) + if err != nil { + return resp, err + } + return resp, nil +} diff --git a/lib/tenants_test.go b/lib/tenants_test.go new file mode 100644 index 0000000..d664313 --- /dev/null +++ b/lib/tenants_test.go @@ -0,0 +1,136 @@ +package lib_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/novuhq/go-novu/lib" +) + +var tenantsApiResponse = `{ + "data": { + "_environmentId": "string", + "_id": "string", + "createdAt": "string", + "data": "object", + "identifier": "string", + "name": "string", + "updatedAt": "string" + } + } + +` + +func TestCreateTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Want POST, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants" { + t.Errorf("Want /v1/tenants, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.CreateTenant(context.Background(), "Tenant", "TenantId") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestGetTenants(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("Want GET, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants" { + t.Errorf("Want /v1/tenants, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.GetTenants(context.Background(), "1", "10") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestGetTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("Want GET, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants/TenantId" { + t.Errorf("Want /v1/feeds, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.GetTenant(context.Background(), "TenantId") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestDeleteTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("Want DELETE, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants/TenantId" { + t.Errorf("Want /v1/tenants/TenantId, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.DeleteTenant(context.Background(), "TenantId") + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} + +func TestUpdateTenant(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("Want PATCH, got %s", r.Method) + } + if r.URL.Path != "/v1/tenants/TenantId" { + t.Errorf("Want /v1/tenants/TenantId, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(tenantsApiResponse)) + })) + defer server.Close() + c := lib.NewAPIClient(novuApiKey, &lib.Config{BackendURL: lib.MustParseURL(server.URL)}) + resp, err := c.TenantApi.UpdateTenant(context.Background(), "TenantId", &lib.UpdateTenantRequest{ + Name: "Tenant2", + }) + if err != nil { + t.Errorf("Error should be nil, got %v", err) + } + if resp.Data == nil || resp.Data == "" { + t.Error("Expected response, got none") + } +} From 448028e372519dbb7fd452e865a946684ddf542a Mon Sep 17 00:00:00 2001 From: Atharva Kulkarni Date: Wed, 8 Nov 2023 19:28:42 +0530 Subject: [PATCH 10/13] Removed Redundant Headings in README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 459e45a..6a27f15 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,6 @@ _InboundParserApi_ | [**Get**](https://docs.novu.co/platform/inbound-parse-webho ## Authorization (api-key) -## Authorization (api-key) - - **Type**: API key - **API key parameter name**: ApiKey - **Location**: HTTP header From 81a93a5821ccd9a386a3f27080338357ad963d3d Mon Sep 17 00:00:00 2001 From: Atharva Kulkarni Date: Thu, 9 Nov 2023 16:35:37 +0530 Subject: [PATCH 11/13] Update discord invite link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a27f15..8f36a48 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Be sure to visit the Novu official [documentation website](https://docs.novu.co/ 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). +As always, if you need additional assistance, join our Discord us a note [here](https://discord.gg/novu). ## Contributors From 4c1ced68ccafd22a649bad632a67700c7747a5ae Mon Sep 17 00:00:00 2001 From: Atharva Kulkarni Date: Thu, 9 Nov 2023 20:31:31 +0530 Subject: [PATCH 12/13] Added link to the api documentation --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8f36a48..6f61d3c 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ _InboundParserApi_ | [**Get**](https://docs.novu.co/platform/inbound-parse-webho - **API key parameter name**: ApiKey - **Location**: HTTP header +### For more information about these methods and their parameters, see the [API documentation](https://docs.novu.co/api-reference/overview). + ## Support and Feedback Be sure to visit the Novu official [documentation website](https://docs.novu.co/docs) for additional information about our API. From 24dd25fbadc6452af3bb2c6958a8c777c1e5e262 Mon Sep 17 00:00:00 2001 From: Atharva Kulkarni Date: Mon, 20 Nov 2023 10:00:38 +0530 Subject: [PATCH 13/13] Updated the links to api methods documentation --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6f61d3c..339abfd 100644 --- a/README.md +++ b/README.md @@ -71,25 +71,25 @@ Check the `cmd` directory to see a sample implementation and test files to see s | Class | Method | HTTP request | Description | | ----------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------ | -| _EventApi_ | [**Trigger**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Post** /events/trigger | Trigger | -| _EventApi_ | [**TriggerBulk**](https://docs.novu.co/api/trigger-event/) | **Post** /v1/events/trigger/bulk | Bulk trigger event | -| _EventApi_ | [**BroadcastToAll**](https://docs.novu.co/api/broadcast-event-to-all/) | **Post** /v1/events/trigger/broadcast | Broadcast event to all | -| _EventApi_ | [**CancelTrigger**](https://docs.novu.co/api/cancel-triggered-event/) | **Delete** /v1/events/trigger/:transactionId | Cancel triggered event | -| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-subscriber/) | **Get** /subscribers/:subscriberId | Get a subscriber | -| _SubscriberApi_ | [**Identify**](https://docs.novu.co/platform/subscribers#creating-a-subscriber) | **Post** /subscribers | Create a subscriber | -| _SubscriberApi_ | [**Update**](https://docs.novu.co/platform/subscribers#updating-subscriber-data) | **Put** /subscribers/:subscriberID | Update subscriber data | -| _SubscriberApi_ | [**Delete**](https://docs.novu.co/platform/subscribers#removing-a-subscriber) | **Delete** /subscribers/:subscriberID | Removing a subscriber | -| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-a-notification-feed-for-a-particular-subscriber) | **Get** /subscribers/:subscriberId/notifications/feed | Get a notification feed for a particular subscriber | -| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-the-unseen-notification-count-for-subscribers-feed) | **Get** /subscribers/:subscriberId/notifications/feed | Get the unseen notification count for subscribers feed | -| _SubscriberApi_ | [**Post**](https://docs.novu.co/api/mark-a-subscriber-feed-message-as-seen) | **Post** /v1/subscribers/:subscriberId/messages/markAs | Mark a subscriber feed message as seen | -| _SubscriberApi_ | [**Get**](https://docs.novu.co/api/get-subscriber-preferences/) | **Get** /subscribers/:subscriberId/preferences | Get subscriber preferences | -| _SubscriberApi_ | [**Patch**](https://docs.novu.co/api/update-subscriber-preference/) | **Patch** /subscribers/:subscriberId/preferences/:templateId | Update subscriber preference | -| _IntegrationsApi_ | [**Create**](https://docs.novu.co/platform/integrations) | **Post** /integrations | Create an integration | -| _IntegrationsApi_ | [**Update**](https://docs.novu.co/platform/integrations) | **Put** /integrations/:integrationId | Update an integration | -| _IntegrationsApi_ | [**Delete**](https://docs.novu.co/platform/integrations) | **Delete** /integrations/:integrationId | Delete an integration | -| _IntegrationsApi_ | [**Get**](https://docs.novu.co/platform/integrations) | **Get** /integrations | Get all integrations | -| _IntegrationsApi_ | [**GetActive**](https://docs.novu.co/platform/intergations) | **Get** /integrations/active | Get all active integrations | -| _IntegrationsApi_ | [**SetIntegrationAsPrimary**](https://docs.novu.co/platform/intergations) | **Post** /integrations/{integrationId}/set-primary | Set the integration as primary | +| _EventApi_ | [**Trigger**](https://docs.novu.co/api-reference/events/trigger-event) | **Post** /events/trigger | Trigger | +| _EventApi_ | [**TriggerBulk**](https://docs.novu.co/api-reference/events/bulk-trigger-event) | **Post** /v1/events/trigger/bulk | Bulk trigger event | +| _EventApi_ | [**BroadcastToAll**](https://docs.novu.co/api-reference/events/broadcast-event-to-all) | **Post** /v1/events/trigger/broadcast | Broadcast event to all | +| _EventApi_ | [**CancelTrigger**](https://docs.novu.co/api-reference/events/cancel-triggered-event) | **Delete** /v1/events/trigger/:transactionId | Cancel triggered event | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api-reference/subscribers/get-subscribers) | **Get** /subscribers/:subscriberId | Get a subscriber | +| _SubscriberApi_ | [**Identify**](https://docs.novu.co/api-reference/subscribers/create-subscriber) | **Post** /subscribers | Create a subscriber | +| _SubscriberApi_ | [**Update**](https://docs.novu.co/api-reference/subscribers/update-subscriber) | **Put** /subscribers/:subscriberID | Update subscriber data | +| _SubscriberApi_ | [**Delete**](https://docs.novu.co/api-reference/subscribers/delete-subscriber) | **Delete** /subscribers/:subscriberID | Removing a subscriber | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api-reference/subscribers/get-subscriber) | **Get** /subscribers/:subscriberId/notifications/feed | Get a notification feed for a particular subscriber | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api-reference/subscribers/get-the-unseen-in-app-notifications-count-for-subscribers-feed) | **Get** /subscribers/:subscriberId/notifications/feed | Get the unseen notification count for subscribers feed | +| _SubscriberApi_ | [**Post**](https://docs.novu.co/api-reference/subscribers/mark-a-subscriber-feed-message-as-seen) | **Post** /v1/subscribers/:subscriberId/messages/markAs | Mark a subscriber feed message as seen | +| _SubscriberApi_ | [**Get**](https://docs.novu.co/api-reference/subscribers/get-subscriber-preferences) | **Get** /subscribers/:subscriberId/preferences | Get subscriber preferences | +| _SubscriberApi_ | [**Patch**](https://docs.novu.co/api-reference/subscribers/update-subscriber-preference) | **Patch** /subscribers/:subscriberId/preferences/:templateId | Update subscriber preference | +| _IntegrationsApi_ | [**Create**](https://docs.novu.co/api-reference/integrations/create-integration) | **Post** /integrations | Create an integration | +| _IntegrationsApi_ | [**Update**](https://docs.novu.co/api-reference/integrations/update-integration) | **Put** /integrations/:integrationId | Update an integration | +| _IntegrationsApi_ | [**Delete**](https://docs.novu.co/api-reference/integrations/delete-integration) | **Delete** /integrations/:integrationId | Delete an integration | +| _IntegrationsApi_ | [**Get**](https://docs.novu.co/api-reference/integrations/get-integrations) | **Get** /integrations | Get all integrations | +| _IntegrationsApi_ | [**GetActive**](https://docs.novu.co/api-reference/integrations/get-active-integrations) | **Get** /integrations/active | Get all active integrations | +| _IntegrationsApi_ | [**SetIntegrationAsPrimary**](https://docs.novu.co/api-reference/integrations/set-integration-as-primary) | **Post** /integrations/{integrationId}/set-primary | Set the integration as primary | | _IntegrationsApi_ | [**GetChannelLimit**](https://docs.novu.co/platform/intergations) | **Get** /integrations/{channelType}/limit | Get the limits of the channel | _InboundParserApi_ | [**Get**](https://docs.novu.co/platform/inbound-parse-webhook/) | **Get** /inbound-parse/mx/status | Validate the mx record setup for the inbound parse functionality