From 02081437091a0bce2073de679a64fdaedb910c1d Mon Sep 17 00:00:00 2001 From: srikanth597 Date: Sun, 8 Oct 2023 19:31:05 +0530 Subject: [PATCH 1/2] 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 2/2] 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") + } +}