diff --git a/acl.go b/acl.go new file mode 100644 index 0000000..02a2b6f --- /dev/null +++ b/acl.go @@ -0,0 +1,163 @@ +package dbaas + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// ACL is the API response for the acls. +type ACL struct { + ID string `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID string `json:"project_id"` + DatastoreID string `json:"datastore_id"` + Pattern string `json:"pattern"` + PatternType string `json:"pattern_type"` + UserID string `json:"user_id"` + Status Status `json:"status"` + AllowRead bool `json:"allow_read"` + AllowWrite bool `json:"allow_write"` +} + +// ACLCreateOpts represents options for the acl Create request. +type ACLCreateOpts struct { + DatastoreID string `json:"datastore_id"` + Pattern string `json:"pattern,omitempty"` + PatternType string `json:"pattern_type"` + UserID string `json:"user_id"` + AllowRead bool `json:"allow_read"` + AllowWrite bool `json:"allow_write"` +} + +// ACLUpdateOpts represents options for the acl Update request. +type ACLUpdateOpts struct { + AllowRead bool `json:"allow_read"` + AllowWrite bool `json:"allow_write"` +} + +// ACLQueryParams represents available query parameters for the acl. +type ACLQueryParams struct { + ID string `json:"id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + DatastoreID string `json:"datastore_id,omitempty"` + Pattern string `json:"pattern,omitempty"` + PatternType string `json:"pattern_type,omitempty"` + UserID string `json:"user_id,omitempty"` + Status Status `json:"status,omitempty"` +} + +// ACLs returns all ACLs. +func (api *API) ACLs(ctx context.Context, params *ACLQueryParams) ([]ACL, error) { + uri, err := setQueryParams("/acls", params) + if err != nil { + return []ACL{}, err + } + + resp, err := api.makeRequest(ctx, http.MethodGet, uri, nil) + if err != nil { + return []ACL{}, err + } + + var result struct { + ACLs []ACL `json:"acls"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return []ACL{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.ACLs, nil +} + +// ACL returns an ACL based on the ID. +func (api *API) ACL(ctx context.Context, aclID string) (ACL, error) { + uri := fmt.Sprintf("/acls/%s", aclID) + + resp, err := api.makeRequest(ctx, http.MethodGet, uri, nil) + if err != nil { + return ACL{}, err + } + + var result struct { + ACL ACL `json:"acl"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return ACL{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.ACL, nil +} + +// CreateACL creates a new acl. +func (api *API) CreateACL(ctx context.Context, opts ACLCreateOpts) (ACL, error) { + uri := "/acls" + createACLOpts := struct { + ACL ACLCreateOpts `json:"acl"` + }{ + ACL: opts, + } + requestBody, err := json.Marshal(createACLOpts) + if err != nil { + return ACL{}, fmt.Errorf("Error marshalling params to JSON, %w", err) + } + + resp, err := api.makeRequest(ctx, http.MethodPost, uri, requestBody) + if err != nil { + return ACL{}, err + } + + var result struct { + ACL ACL `json:"acl"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return ACL{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.ACL, nil +} + +// UpdateACL updates an existing acl. +func (api *API) UpdateACL(ctx context.Context, aclID string, opts ACLUpdateOpts) (ACL, error) { + uri := fmt.Sprintf("/acls/%s", aclID) + updateACLOpts := struct { + ACL ACLUpdateOpts `json:"acl"` + }{ + ACL: opts, + } + requestBody, err := json.Marshal(updateACLOpts) + if err != nil { + return ACL{}, fmt.Errorf("Error marshalling params to JSON, %w", err) + } + + resp, err := api.makeRequest(ctx, http.MethodPut, uri, requestBody) + if err != nil { + return ACL{}, err + } + + var result struct { + ACL ACL `json:"acl"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return ACL{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.ACL, nil +} + +// DeleteACL deletes an existing acl. +func (api *API) DeleteACL(ctx context.Context, aclID string) error { + uri := fmt.Sprintf("/acls/%s", aclID) + + _, err := api.makeRequest(ctx, http.MethodDelete, uri, nil) + if err != nil { + return err + } + + return nil +} diff --git a/acl_test.go b/acl_test.go new file mode 100644 index 0000000..249db76 --- /dev/null +++ b/acl_test.go @@ -0,0 +1,324 @@ +package dbaas + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +const aclID = "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4" + +const testACLNotFoundResponse = `{ + "error": { + "code": 404, + "title": "Not Found", + "message": "acl 123 not found." + } +}` + +const testCreateACLInvalidDatastoreIDResponse = `{ + "error": { + "code": 400, + "title": "Bad Request", + "message": + "Validation failure: {'acl.datastore_id': \"'20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f' is not a 'UUID'\"}" + } +}` + +const testUpdateACLInvalidResponse = `{ + "error": { + "code": 400, + "title": "Bad Request", + "message": + "Validation failure: At least one of these fields (allow_read, allow_write) must be true" + } +}` + +const testACLResponse = `{ + "acl": { + "id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "created_at": "1970-01-01T00:00:00", + "updated_at": "1970-01-01T00:00:00", + "project_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "datastore_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "user_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "pattern": "topic1", + "pattern_type": "literal", + "allow_read": true, + "allow_write": true, + "status": "ACTIVE" + } +}` + +const testACLsResponse = ` +{ + "acls": [ + { + "id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "created_at": "1970-01-01T00:00:00", + "updated_at": "1970-01-01T00:00:00", + "project_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "datastore_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "user_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "pattern": "topic1", + "pattern_type": "literal", + "allow_read": true, + "allow_write": true, + "status": "ACTIVE" + }, + { + "id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f5", + "created_at": "1970-01-01T00:00:00", + "updated_at": "1970-01-01T00:00:00", + "project_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "datastore_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "user_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "pattern": "topic2", + "pattern_type": "literal", + "allow_read": true, + "allow_write": true, + "status": "ACTIVE" + } + ] +}` + +var ACLResponse ACL = ACL{ //nolint + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + UserID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Pattern: "topic1", + PatternType: "literal", + AllowRead: true, + AllowWrite: true, + Status: StatusActive, +} + +var ACLExpected ACL = ACL{ //nolint + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + UserID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Pattern: "topic1", + PatternType: "literal", + AllowRead: true, + AllowWrite: true, + Status: StatusActive, +} + +func TestACLs(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", testClient.Endpoint+"/acls", + httpmock.NewStringResponder(200, testACLsResponse)) + + expected := []ACL{ + { + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + UserID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Pattern: "topic1", + PatternType: "literal", + AllowRead: true, + AllowWrite: true, + Status: StatusActive, + }, + { + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f5", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + UserID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Pattern: "topic2", + PatternType: "literal", + AllowRead: true, + AllowWrite: true, + Status: StatusActive, + }, + } + + actual, err := testClient.ACLs(context.Background(), nil) + + if assert.NoError(t, err) { + assert.Equal(t, expected, actual) + } +} + +func TestACL(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", testClient.Endpoint+"/acls/"+aclID, + httpmock.NewStringResponder(200, testACLResponse)) + + actual, err := testClient.ACL(context.Background(), aclID) + + if assert.NoError(t, err) { + assert.Equal(t, ACLExpected, actual) + } +} + +func TestACLNotFound(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", testClient.Endpoint+"/acls/123", + httpmock.NewStringResponder(404, testACLNotFoundResponse)) + + expected := &DBaaSAPIError{} + expected.APIError.Code = 404 + expected.APIError.Title = ErrorNotFoundTitle + expected.APIError.Message = "acl 123 not found." + + _, err := testClient.ACL(context.Background(), "123") + + assert.ErrorAs(t, err, &expected) +} + +func TestCreateACL(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", testClient.Endpoint+"/acls", + func(req *http.Request) (*http.Response, error) { + if err := json.NewDecoder(req.Body).Decode(&ACLCreateOpts{}); err != nil { + return httpmock.NewStringResponse(400, ""), err + } + + acls := make(map[string]ACL) + ACLCreateResponse := ACLResponse + ACLCreateResponse.Status = StatusPendingCreate + acls["acl"] = ACLCreateResponse + + resp, err := httpmock.NewJsonResponse(200, acls) + if err != nil { + return httpmock.NewStringResponse(500, ""), err + } + + return resp, nil + }) + + createACLOpts := ACLCreateOpts{ + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + UserID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Pattern: "topic1", + PatternType: "literal", + AllowRead: true, + AllowWrite: true, + } + + actual, err := testClient.CreateACL(context.Background(), createACLOpts) + + ACLCreateExpected := ACLExpected + ACLCreateExpected.Status = StatusPendingCreate + if assert.NoError(t, err) { + assert.Equal(t, ACLCreateExpected, actual) + } +} + +func TestCreateACLInvalidDatastoreID(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", testClient.Endpoint+"/acls", + httpmock.NewStringResponder(400, testCreateACLInvalidDatastoreIDResponse)) + + expected := &DBaaSAPIError{} + expected.APIError.Code = 400 + expected.APIError.Title = ErrorBadRequestTitle + expected.APIError.Message = `Validation failure: + {'acl.datastore_id': \"'20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f' is not a 'UUID'\"}` + + createACLOpts := ACLCreateOpts{ + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + UserID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Pattern: "topic1", + PatternType: "literal", + AllowRead: true, + AllowWrite: true, + } + + _, err := testClient.CreateACL(context.Background(), createACLOpts) + + assert.ErrorAs(t, err, &expected) +} + +func TestUpdateACL(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PUT", testClient.Endpoint+"/acls/"+aclID, + func(req *http.Request) (*http.Response, error) { + if err := json.NewDecoder(req.Body).Decode(&ACLUpdateOpts{}); err != nil { + return httpmock.NewStringResponse(400, ""), err + } + + acls := make(map[string]ACL) + ACLUpdateResponse := ACLResponse + ACLUpdateResponse.Status = StatusPendingUpdate + acls["acl"] = ACLUpdateResponse + + resp, err := httpmock.NewJsonResponse(200, acls) + if err != nil { + return httpmock.NewStringResponse(500, ""), err + } + + return resp, nil + }) + + updateACLOpts := ACLUpdateOpts{ + AllowRead: true, + AllowWrite: false, + } + + actual, err := testClient.UpdateACL(context.Background(), aclID, updateACLOpts) + + ACLUpdateExpexted := ACLExpected + ACLUpdateExpexted.Status = StatusPendingUpdate + if assert.NoError(t, err) { + assert.Equal(t, ACLUpdateExpexted, actual) + } +} + +func TestUpdateACLInvalidResponse(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PUT", testClient.Endpoint+"/acls/"+aclID, + httpmock.NewStringResponder(400, testUpdateACLInvalidResponse)) + + expected := &DBaaSAPIError{} + expected.APIError.Code = 400 + expected.APIError.Title = ErrorBadRequestTitle + expected.APIError.Message = `Validation failure: + At least one of these fields (allow_read, allow_write) must be true` + + updateACLOpts := ACLUpdateOpts{ + AllowRead: false, + AllowWrite: false, + } + + _, err := testClient.UpdateACL(context.Background(), aclID, updateACLOpts) + + assert.ErrorAs(t, err, &expected) +} diff --git a/go.mod b/go.mod index 47307c8..e744be7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/selectel/dbaas-go -go 1.17 +go 1.21 require ( github.com/gophercloud/gophercloud v1.0.0 diff --git a/topic.go b/topic.go new file mode 100644 index 0000000..11d4bb3 --- /dev/null +++ b/topic.go @@ -0,0 +1,154 @@ +package dbaas + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +// Topic is the API response for the topics. +type Topic struct { + ID string `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ProjectID string `json:"project_id"` + DatastoreID string `json:"datastore_id"` + Name string `json:"name"` + Status Status `json:"status"` + Partitions uint16 `json:"partitions"` +} + +// TopicCreateOpts represents options for the topic Create request. +type TopicCreateOpts struct { + DatastoreID string `json:"datastore_id"` + Name string `json:"name"` + Partitions uint16 `json:"partitions"` +} + +// TopicUpdateOpts represents options for the topic Update request. +type TopicUpdateOpts struct { + Partitions uint16 `json:"partitions"` +} + +// TopicQueryParams represents available query parameters for the topic. +type TopicQueryParams struct { + ID string `json:"id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + DatastoreID string `json:"datastore_id,omitempty"` + Name string `json:"name,omitempty"` + Status Status `json:"status,omitempty"` +} + +// Topics returns all topics. +func (api *API) Topics(ctx context.Context, params *TopicQueryParams) ([]Topic, error) { + uri, err := setQueryParams("/topics", params) + if err != nil { + return []Topic{}, err + } + + resp, err := api.makeRequest(ctx, http.MethodGet, uri, nil) + if err != nil { + return []Topic{}, err + } + + var result struct { + Topics []Topic `json:"topics"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return []Topic{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.Topics, nil +} + +// Topic returns a topic based on the ID. +func (api *API) Topic(ctx context.Context, topicID string) (Topic, error) { + uri := fmt.Sprintf("/topics/%s", topicID) + + resp, err := api.makeRequest(ctx, http.MethodGet, uri, nil) + if err != nil { + return Topic{}, err + } + + var result struct { + Topic Topic `json:"topic"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return Topic{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.Topic, nil +} + +// CreateTopic creates a new topic. +func (api *API) CreateTopic(ctx context.Context, opts TopicCreateOpts) (Topic, error) { + uri := "/topics" + createTopicOpts := struct { + Topic TopicCreateOpts `json:"topic"` + }{ + Topic: opts, + } + requestBody, err := json.Marshal(createTopicOpts) + if err != nil { + return Topic{}, fmt.Errorf("Error marshalling params to JSON, %w", err) + } + + resp, err := api.makeRequest(ctx, http.MethodPost, uri, requestBody) + if err != nil { + return Topic{}, err + } + + var result struct { + Topic Topic `json:"topic"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return Topic{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.Topic, nil +} + +// UpdateTopic updates an existing topic. +func (api *API) UpdateTopic(ctx context.Context, topicID string, opts TopicUpdateOpts) (Topic, error) { + uri := fmt.Sprintf("/topics/%s", topicID) + updateTopicOpts := struct { + Topic TopicUpdateOpts `json:"topic"` + }{ + Topic: opts, + } + requestBody, err := json.Marshal(updateTopicOpts) + if err != nil { + return Topic{}, fmt.Errorf("Error marshalling params to JSON, %w", err) + } + + resp, err := api.makeRequest(ctx, http.MethodPut, uri, requestBody) + if err != nil { + return Topic{}, err + } + + var result struct { + Topic Topic `json:"topic"` + } + err = json.Unmarshal(resp, &result) + if err != nil { + return Topic{}, fmt.Errorf("Error during Unmarshal, %w", err) + } + + return result.Topic, nil +} + +// DeleteTopic deletes an existing topic. +func (api *API) DeleteTopic(ctx context.Context, topicID string) error { + uri := fmt.Sprintf("/topics/%s", topicID) + + _, err := api.makeRequest(ctx, http.MethodDelete, uri, nil) + if err != nil { + return err + } + + return nil +} diff --git a/topic_test.go b/topic_test.go new file mode 100644 index 0000000..41b94d7 --- /dev/null +++ b/topic_test.go @@ -0,0 +1,295 @@ +package dbaas + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" +) + +const topicID = "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4" + +const testTopicNotFoundResponse = `{ + "error": { + "code": 404, + "title": "Not Found", + "message": "topic 123 not found." + } +}` + +const testCreateTopicInvalidDatastoreIDResponse = `{ + "error": { + "code": 400, + "title": "Bad Request", + "message": + "Validation failure: {'topic.datastore_id': \"'20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f' is not a 'UUID'\"}" + } +}` + +const testUpdateTopicInvalidPartitionsResponse = `{ + "error": { + "code": 400, + "title": "Bad Request", + "message": + "Validation failure: {'topic.partitions': \"'4001 is greater than the maximum of 4000'\"}" + } +}` + +const testTopicResponse = `{ + "topic": { + "id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "created_at": "1970-01-01T00:00:00", + "updated_at": "1970-01-01T00:00:00", + "project_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "datastore_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "name": "topic1", + "partitions": 1, + "status": "ACTIVE" + } +}` + +const testTopicsResponse = ` +{ + "topics": [ + { + "id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "created_at": "1970-01-01T00:00:00", + "updated_at": "1970-01-01T00:00:00", + "project_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "datastore_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "name": "topic1", + "partitions": 1, + "status": "ACTIVE" + }, + { + "id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f5", + "created_at": "1970-01-01T00:00:00", + "updated_at": "1970-01-01T00:00:00", + "project_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "datastore_id": "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + "name": "topic2", + "partitions": 2, + "status": "ACTIVE" + } + ] +}` + +var TopicResponse = Topic{ //nolint + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Name: "topic1", + Partitions: 1, + Status: StatusActive, +} + +var TopicExpected Topic = Topic{ //nolint + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Name: "topic1", + Partitions: 1, + Status: StatusActive, +} + +func TestTopics(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", testClient.Endpoint+"/topics", + httpmock.NewStringResponder(200, testTopicsResponse)) + + expected := []Topic{ + { + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Name: "topic1", + Partitions: 1, + Status: StatusActive, + }, + { + ID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f5", + CreatedAt: "1970-01-01T00:00:00", + UpdatedAt: "1970-01-01T00:00:00", + ProjectID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Name: "topic2", + Partitions: 2, + Status: StatusActive, + }, + } + + actual, err := testClient.Topics(context.Background(), nil) + + if assert.NoError(t, err) { + assert.Equal(t, expected, actual) + } +} + +func TestTopic(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", testClient.Endpoint+"/topics/"+topicID, + httpmock.NewStringResponder(200, testTopicResponse)) + + actual, err := testClient.Topic(context.Background(), topicID) + + if assert.NoError(t, err) { + assert.Equal(t, TopicExpected, actual) + } +} + +func TestTopicNotFound(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("GET", testClient.Endpoint+"/topics/123", + httpmock.NewStringResponder(404, testTopicNotFoundResponse)) + + expected := &DBaaSAPIError{} + expected.APIError.Code = 404 + expected.APIError.Title = ErrorNotFoundTitle + expected.APIError.Message = "topic 123 not found." + + _, err := testClient.Topic(context.Background(), "123") + + assert.ErrorAs(t, err, &expected) +} + +func TestCreateToopic(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", testClient.Endpoint+"/topics", + func(req *http.Request) (*http.Response, error) { + if err := json.NewDecoder(req.Body).Decode(&TopicCreateOpts{}); err != nil { + return httpmock.NewStringResponse(400, ""), err + } + + topics := make(map[string]Topic) + TopicCreateResponse := TopicResponse + TopicCreateResponse.Status = StatusPendingCreate + topics["topic"] = TopicResponse + + resp, err := httpmock.NewJsonResponse(200, topics) + if err != nil { + return httpmock.NewStringResponse(500, ""), err + } + + return resp, nil + }) + + createTopicOpts := TopicCreateOpts{ + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Name: "topic1", + Partitions: 1, + } + + actual, err := testClient.CreateTopic(context.Background(), createTopicOpts) + + TopicCreateExpexted := TopicExpected + TopicCreateExpexted.Status = StatusPendingCreate + if assert.NoError(t, err) { + assert.Equal(t, TopicExpected, actual) + } +} + +func TestCreateTopicInvalidDatastoreID(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("POST", testClient.Endpoint+"/topics", + httpmock.NewStringResponder(400, testCreateTopicInvalidDatastoreIDResponse)) + + expected := &DBaaSAPIError{} + expected.APIError.Code = 400 + expected.APIError.Title = ErrorBadRequestTitle + expected.APIError.Message = `Validation failure: + {'topic.datastore_id': \"'20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f' is not a 'UUID'\"}` + + createTopicOpts := TopicCreateOpts{ + DatastoreID: "20d7bcf4-f8d6-4bf6-b8f6-46cb440a87f4", + Name: "topic1", + Partitions: 1, + } + + _, err := testClient.CreateTopic(context.Background(), createTopicOpts) + + assert.ErrorAs(t, err, &expected) +} + +func TestUpdateTopic(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PUT", testClient.Endpoint+"/topics/"+topicID, + func(req *http.Request) (*http.Response, error) { + if err := json.NewDecoder(req.Body).Decode(&TopicUpdateOpts{}); err != nil { + return httpmock.NewStringResponse(400, ""), err + } + + topics := make(map[string]Topic) + TopicUpdateResponse := TopicResponse + TopicUpdateResponse.Status = StatusPendingUpdate + topics["topic"] = TopicUpdateResponse + + resp, err := httpmock.NewJsonResponse(200, topics) + if err != nil { + return httpmock.NewStringResponse(500, ""), err + } + + return resp, nil + }) + + updateTopicOpts := TopicUpdateOpts{ + Partitions: 2, + } + + actual, err := testClient.UpdateTopic(context.Background(), topicID, updateTopicOpts) + + TopicUpdateExpected := TopicExpected + TopicUpdateExpected.Status = StatusPendingUpdate + if assert.NoError(t, err) { + assert.Equal(t, TopicUpdateExpected, actual) + } +} + +func TestUpdateTopicInvalidPartitions(t *testing.T) { + httpmock.Activate() + testClient := SetupTestClient() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterResponder("PUT", testClient.Endpoint+"/topics/"+topicID, + httpmock.NewStringResponder(400, testUpdateTopicInvalidPartitionsResponse)) + + expected := &DBaaSAPIError{} + expected.APIError.Code = 400 + expected.APIError.Title = ErrorBadRequestTitle + expected.APIError.Message = `Validation failure: + {'topic.partitions': \"'4001 is greater than the maximum of 4000'\"}` + + updateTopicOpts := TopicUpdateOpts{ + Partitions: 4001, + } + + _, err := testClient.UpdateTopic(context.Background(), topicID, updateTopicOpts) + + assert.ErrorAs(t, err, &expected) +}