From 4f8e9939badb42ef97f2936e2456699eb1ff787c Mon Sep 17 00:00:00 2001 From: Christian Ehrig Date: Thu, 30 Jul 2020 17:37:15 +0200 Subject: [PATCH] Adds implementation for (IP) Rules Lists (#507) * Adds implementation for Rules Lists * Apply suggestions from code review Co-authored-by: Jacob Bednarz * Returns an error if BulkOperation returns an unexpected status Co-authored-by: Christian Ehrig Co-authored-by: Jacob Bednarz --- cloudflare.go | 19 +- errors.go | 14 +- ip_list.go | 460 +++++++++++++++++++++++++++++++++++++++++++++++ ip_list_test.go | 464 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 945 insertions(+), 12 deletions(-) create mode 100644 ip_list.go create mode 100644 ip_list_test.go diff --git a/cloudflare.go b/cloudflare.go index 73dba8fcdc5..bbd9745cba9 100644 --- a/cloudflare.go +++ b/cloudflare.go @@ -363,14 +363,21 @@ type Response struct { Messages []ResponseInfo `json:"messages"` } +// ResultInfoCursors contains information about cursors. +type ResultInfoCursors struct { + Before string `json:"before"` + After string `json:"after"` +} + // ResultInfo contains metadata about the Response. type ResultInfo struct { - Page int `json:"page"` - PerPage int `json:"per_page"` - TotalPages int `json:"total_pages"` - Count int `json:"count"` - Total int `json:"total_count"` - Cursor string `json:"cursor"` + Page int `json:"page"` + PerPage int `json:"per_page"` + TotalPages int `json:"total_pages"` + Count int `json:"count"` + Total int `json:"total_count"` + Cursor string `json:"cursor"` + Cursors ResultInfoCursors `json:"cursors"` } // RawResponse keeps the result as JSON form diff --git a/errors.go b/errors.go index 21c38b16801..f2ea83a5dc0 100644 --- a/errors.go +++ b/errors.go @@ -2,12 +2,14 @@ package cloudflare // Error messages const ( - errEmptyCredentials = "invalid credentials: key & email must not be empty" - errEmptyAPIToken = "invalid credentials: API Token must not be empty" - errMakeRequestError = "error from makeRequest" - errUnmarshalError = "error unmarshalling the JSON response" - errRequestNotSuccessful = "error reported by API" - errMissingAccountID = "account ID is empty and must be provided" + errEmptyCredentials = "invalid credentials: key & email must not be empty" + errEmptyAPIToken = "invalid credentials: API Token must not be empty" + errMakeRequestError = "error from makeRequest" + errUnmarshalError = "error unmarshalling the JSON response" + errRequestNotSuccessful = "error reported by API" + errMissingAccountID = "account ID is empty and must be provided" + errOperationStillRunning = "bulk operation did not finish before timeout" + errOperationUnexpectedStatus = "bulk operation returned an unexpected status" ) var _ Error = &UserError{} diff --git a/ip_list.go b/ip_list.go new file mode 100644 index 00000000000..4dcf5072ec1 --- /dev/null +++ b/ip_list.go @@ -0,0 +1,460 @@ +package cloudflare + +import ( + "context" + "encoding/json" + "fmt" + "math" + "net/http" + "time" + + "github.com/pkg/errors" +) + +const ( + // IPListTypeIP specifies a list containing IP addresses + IPListTypeIP = "ip" +) + +// IPListBulkOperation contains information about a Bulk Operation +type IPListBulkOperation struct { + ID string `json:"id"` + Status string `json:"status"` + Error string `json:"error"` + Completed *time.Time `json:"completed"` +} + +// IPList contains information about an IP List +type IPList struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Kind string `json:"kind"` + NumItems int `json:"num_items"` + NumReferencingFilters int `json:"num_referencing_filters"` + CreatedOn *time.Time `json:"created_on"` + ModifiedOn *time.Time `json:"modified_on"` +} + +// IPListItem contains information about a single IP List Item +type IPListItem struct { + ID string `json:"id"` + IP string `json:"ip"` + Comment string `json:"comment"` + CreatedOn *time.Time `json:"created_on"` + ModifiedOn *time.Time `json:"modified_on"` +} + +// IPListCreateRequest contains data for a new IP List +type IPListCreateRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Kind string `json:"kind"` +} + +// IPListItemCreateRequest contains data for a new IP List Item +type IPListItemCreateRequest struct { + IP string `json:"ip"` + Comment string `json:"comment"` +} + +// IPListItemDeleteRequest wraps IP List Items that shall be deleted +type IPListItemDeleteRequest struct { + Items []IPListItemDeleteItemRequest `json:"items"` +} + +// IPListItemDeleteItemRequest contains single IP List Items that shall be deleted +type IPListItemDeleteItemRequest struct { + ID string `json:"id"` +} + +// IPListUpdateRequest contains data for an IP List update +type IPListUpdateRequest struct { + Description string `json:"description"` +} + +// IPListResponse contains a single IP List +type IPListResponse struct { + Response + Result IPList `json:"result"` +} + +// IPListItemCreateResponse contains information about the creation of an IP List Item +type IPListItemCreateResponse struct { + Response + Result struct { + OperationID string `json:"operation_id"` + } `json:"result"` +} + +// IPListListResponse contains a slice of IP Lists +type IPListListResponse struct { + Response + Result []IPList `json:"result"` +} + +// IPListBulkOperationResponse contains information about a Bulk Operation +type IPListBulkOperationResponse struct { + Response + Result IPListBulkOperation `json:"result"` +} + +// IPListDeleteResponse contains information about the deletion of an IP List +type IPListDeleteResponse struct { + Response + Result struct { + ID string `json:"id"` + } `json:"result"` +} + +// IPListItemsListResponse contains information about IP List Items +type IPListItemsListResponse struct { + Response + ResultInfo `json:"result_info"` + Result []IPListItem `json:"result"` +} + +// IPListItemDeleteResponse contains information about the deletion of an IP List Item +type IPListItemDeleteResponse struct { + Response + Result struct { + OperationID string `json:"operation_id"` + } `json:"result"` +} + +// IPListItemsGetResponse contains information about a single IP List Item +type IPListItemsGetResponse struct { + Response + Result IPListItem `json:"result"` +} + +// ListIPLists lists all IP Lists +// +// API reference: https://api.cloudflare.com/#rules-lists-list-lists +func (api *API) ListIPLists(ctx context.Context) ([]IPList, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists", api.AccountID) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []IPList{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListListResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []IPList{}, errors.Wrap(err, errUnmarshalError) + } + + return result.Result, nil +} + +// CreateIPList creates a new IP List +// +// API reference: https://api.cloudflare.com/#rules-lists-create-list +func (api *API) CreateIPList(ctx context.Context, name string, description string, kind string) (IPList, + error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists", api.AccountID) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, + IPListCreateRequest{Name: name, Description: description, Kind: kind}) + if err != nil { + return IPList{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPList{}, errors.Wrap(err, errUnmarshalError) + } + + return result.Result, nil +} + +// GetIPList returns a single IP List +// +// API reference: https://api.cloudflare.com/#rules-lists-get-list +func (api *API) GetIPList(ctx context.Context, id string) (IPList, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return IPList{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPList{}, errors.Wrap(err, errUnmarshalError) + } + + return result.Result, nil +} + +// UpdateIPList updates the description of an existing IP List +// +// API reference: https://api.cloudflare.com/#rules-lists-update-list +func (api *API) UpdateIPList(ctx context.Context, id string, description string) (IPList, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, IPListUpdateRequest{Description: description}) + if err != nil { + return IPList{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPList{}, errors.Wrap(err, errUnmarshalError) + } + + return result.Result, nil +} + +// DeleteIPList deletes an IP List +// +// API reference: https://api.cloudflare.com/#rules-lists-delete-list +func (api *API) DeleteIPList(ctx context.Context, id string) (IPListDeleteResponse, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) + if err != nil { + return IPListDeleteResponse{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListDeleteResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListDeleteResponse{}, errors.Wrap(err, errUnmarshalError) + } + + return result, nil +} + +// ListIPListItems returns a list with all items in an IP List +// +// API reference: https://api.cloudflare.com/#rules-lists-list-list-items +func (api *API) ListIPListItems(ctx context.Context, id string) ([]IPListItem, error) { + var list []IPListItem + var cursor string + var cursorQuery string + + for { + if len(cursor) > 0 { + cursorQuery = fmt.Sprintf("?cursor=%s", cursor) + } + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items%s", api.AccountID, id, cursorQuery) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return []IPListItem{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListItemsListResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return []IPListItem{}, errors.Wrap(err, errUnmarshalError) + } + + list = append(list, result.Result...) + if cursor = result.ResultInfo.Cursors.After; cursor == "" { + break + } + } + + return list, nil +} + +// CreateIPListItemAsync creates a new IP List Item asynchronously. Users have to poll the operation status by +// using the operation_id returned by this function. +// +// API reference: https://api.cloudflare.com/#rules-lists-create-list-items +func (api *API) CreateIPListItemAsync(ctx context.Context, id, ip, comment string) (IPListItemCreateResponse, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, []IPListItemCreateRequest{{IP: ip, Comment: comment}}) + if err != nil { + return IPListItemCreateResponse{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListItemCreateResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListItemCreateResponse{}, errors.Wrap(err, errUnmarshalError) + } + + return result, nil +} + +// CreateIPListItem creates a new IP List Item synchronously and returns the current set of IP List Items +func (api *API) CreateIPListItem(ctx context.Context, id, ip, comment string) ([]IPListItem, error) { + result, err := api.CreateIPListItemAsync(ctx, id, ip, comment) + + if err != nil { + return []IPListItem{}, err + } + + err = api.pollIPListBulkOperation(ctx, result.Result.OperationID) + if err != nil { + return []IPListItem{}, err + } + + return api.ListIPListItems(ctx, id) +} + +// CreateIPListItemsAsync bulk creates many IP List Items asynchronously. Users have to poll the operation status by +// using the operation_id returned by this function. +// +// API reference: https://api.cloudflare.com/#rules-lists-create-list-items +func (api *API) CreateIPListItemsAsync(ctx context.Context, id string, items []IPListItemCreateRequest) ( + IPListItemCreateResponse, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodPost, uri, items) + if err != nil { + return IPListItemCreateResponse{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListItemCreateResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListItemCreateResponse{}, errors.Wrap(err, errUnmarshalError) + } + + return result, nil +} + +// CreateIPListItems bulk creates many IP List Items synchronously and returns the current set of IP List Items +func (api *API) CreateIPListItems(ctx context.Context, id string, items []IPListItemCreateRequest) ( + []IPListItem, error) { + result, err := api.CreateIPListItemsAsync(ctx, id, items) + if err != nil { + return []IPListItem{}, err + } + + err = api.pollIPListBulkOperation(ctx, result.Result.OperationID) + if err != nil { + return []IPListItem{}, err + } + + return api.ListIPListItems(ctx, id) +} + +// ReplaceIPListItemsAsync replaces all IP List Items asynchronously. Users have to poll the operation status by +// using the operation_id returned by this function. +// +// API reference: https://api.cloudflare.com/#rules-lists-replace-list-items +func (api *API) ReplaceIPListItemsAsync(ctx context.Context, id string, items []IPListItemCreateRequest) ( + IPListItemCreateResponse, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodPut, uri, items) + if err != nil { + return IPListItemCreateResponse{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListItemCreateResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListItemCreateResponse{}, errors.Wrap(err, errUnmarshalError) + } + + return result, nil +} + +// ReplaceIPListItems replaces all IP List Items synchronously and returns the current set of IP List Items +func (api *API) ReplaceIPListItems(ctx context.Context, id string, items []IPListItemCreateRequest) ( + []IPListItem, error) { + result, err := api.ReplaceIPListItemsAsync(ctx, id, items) + if err != nil { + return []IPListItem{}, err + } + + err = api.pollIPListBulkOperation(ctx, result.Result.OperationID) + if err != nil { + return []IPListItem{}, err + } + + return api.ListIPListItems(ctx, id) +} + +// DeleteIPListItemsAsync removes specific Items of an IP List by their ID asynchronously. Users have to poll the +// operation status by using the operation_id returned by this function. +// +// API reference: https://api.cloudflare.com/#rules-lists-delete-list-items +func (api *API) DeleteIPListItemsAsync(ctx context.Context, id string, items IPListItemDeleteRequest) ( + IPListItemDeleteResponse, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, items) + if err != nil { + return IPListItemDeleteResponse{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListItemDeleteResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListItemDeleteResponse{}, errors.Wrap(err, errUnmarshalError) + } + + return result, nil +} + +// DeleteIPListItems removes specific Items of an IP List by their ID synchronously and returns the current set +// of IP List Items +func (api *API) DeleteIPListItems(ctx context.Context, id string, items IPListItemDeleteRequest) ( + []IPListItem, error) { + result, err := api.DeleteIPListItemsAsync(ctx, id, items) + if err != nil { + return []IPListItem{}, err + } + + err = api.pollIPListBulkOperation(ctx, result.Result.OperationID) + if err != nil { + return []IPListItem{}, err + } + + return api.ListIPListItems(ctx, id) +} + +// GetIPListItem returns a single IP List Item +// +// API reference: https://api.cloudflare.com/#rules-lists-get-list-item +func (api *API) GetIPListItem(ctx context.Context, listID, id string) (IPListItem, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items/%s", api.AccountID, listID, id) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return IPListItem{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListItemsGetResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListItem{}, errors.Wrap(err, errUnmarshalError) + } + + return result.Result, nil +} + +// GetIPListBulkOperation returns the status of a bulk operation +// +// API reference: https://api.cloudflare.com/#rules-lists-get-bulk-operation +func (api *API) GetIPListBulkOperation(ctx context.Context, id string) (IPListBulkOperation, error) { + uri := fmt.Sprintf("/accounts/%s/rules/lists/bulk_operations/%s", api.AccountID, id) + res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return IPListBulkOperation{}, errors.Wrap(err, errMakeRequestError) + } + + result := IPListBulkOperationResponse{} + if err := json.Unmarshal(res, &result); err != nil { + return IPListBulkOperation{}, errors.Wrap(err, errUnmarshalError) + } + + return result.Result, nil +} + +// pollIPListBulkOperation implements synchronous behaviour for some asynchronous endpoints. +// bulk-operation status can be either pending, running, failed or completed +func (api *API) pollIPListBulkOperation(ctx context.Context, id string) error { + var i uint8 + for i = 0; i < 16; i++ { + time.Sleep(0x1 << uint8(math.Ceil(float64(i/2))) * time.Second) + + bulkResult, err := api.GetIPListBulkOperation(ctx, id) + if err != nil { + return err + } + + switch bulkResult.Status { + case "failed": + return errors.New(bulkResult.Error) + case "pending", "running": + continue + case "completed": + return nil + default: + return errors.New(fmt.Sprintf("%s: %s", errOperationUnexpectedStatus, bulkResult.Status)) + } + } + + return errors.New(errOperationStillRunning) +} diff --git a/ip_list_test.go b/ip_list_test.go new file mode 100644 index 00000000000..c7ab7b257fb --- /dev/null +++ b/ip_list_test.go @@ -0,0 +1,464 @@ +package cloudflare + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestListIPLists(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": [ + { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "name": "list1", + "description": "This is a note.", + "kind": "ip", + "num_items": 10, + "num_referencing_filters": 2, + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + } + ], + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2020-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2020-01-10T14:00:00Z") + + want := []IPList{ + { + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Name: "list1", + Description: "This is a note.", + Kind: "ip", + NumItems: 10, + NumReferencingFilters: 2, + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + } + + actual, err := client.ListIPLists(context.Background()) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateIPList(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "name": "list1", + "description": "This is a note.", + "kind": "ip", + "num_items": 10, + "num_referencing_filters": 2, + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2020-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2020-01-10T14:00:00Z") + + want := IPList{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Name: "list1", + Description: "This is a note.", + Kind: "ip", + NumItems: 10, + NumReferencingFilters: 2, + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.CreateIPList(context.Background(), "list1", "This is a note.", "ip") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetIPList(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "name": "list1", + "description": "This is a note.", + "kind": "ip", + "num_items": 10, + "num_referencing_filters": 2, + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2020-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2020-01-10T14:00:00Z") + + want := IPList{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Name: "list1", + Description: "This is a note.", + Kind: "ip", + NumItems: 10, + NumReferencingFilters: 2, + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.GetIPList(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestUpdateIPList(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PUT", "Expected method 'PUT', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "name": "list1", + "description": "This note was updated.", + "kind": "ip", + "num_items": 10, + "num_referencing_filters": 2, + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2020-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2020-01-10T14:00:00Z") + + want := IPList{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Name: "list1", + Description: "This note was updated.", + Kind: "ip", + NumItems: 10, + NumReferencingFilters: 2, + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.UpdateIPList(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e", + "This note was updated.") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteIPList(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "DELETE", "Expected method 'DELETE', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "34b12448945f11eaa1b71c4d701ab86e" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e", handler) + + want := IPListDeleteResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.ID = "34b12448945f11eaa1b71c4d701ab86e" + + actual, err := client.DeleteIPList(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListIPListsItems(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + + if len(r.URL.Query().Get("cursor")) > 0 && r.URL.Query().Get("cursor") == "yyy" { + fmt.Fprint(w, `{ + "result": [ + { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "ip": "192.0.2.2", + "comment": "Another Private IP address", + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + } + ], + "result_info": { + "cursors": { + "before": "xxx" + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } else { + fmt.Fprint(w, `{ + "result": [ + { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "ip": "192.0.2.1", + "comment": "Private IP address", + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + } + ], + "result_info": { + "cursors": { + "before": "xxx", + "after": "yyy" + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2020-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2020-01-10T14:00:00Z") + + want := []IPListItem{ + { + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + IP: "192.0.2.1", + Comment: "Private IP address", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + { + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + IP: "192.0.2.2", + Comment: "Another Private IP address", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + } + + actual, err := client.ListIPListItems(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateIPListItems(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := IPListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.CreateIPListItemsAsync(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e", + []IPListItemCreateRequest{{ + IP: "192.0.2.1", + Comment: "Private IP", + }, { + IP: "192.0.2.2", + Comment: "Another Private IP", + }}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestReplaceIPListItems(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PUT", "Expected method 'PUT', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := IPListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.ReplaceIPListItemsAsync(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e", + []IPListItemCreateRequest{{ + IP: "192.0.2.1", + Comment: "Private IP", + }, { + IP: "192.0.2.2", + Comment: "Another Private IP", + }}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestDeleteIPListItems(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "DELETE", "Expected method 'DELETE', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "operation_id": "4da8780eeb215e6cb7f48dd981c4ea02" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := IPListItemDeleteResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.DeleteIPListItemsAsync(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e", + IPListItemDeleteRequest{[]IPListItemDeleteItemRequest{{ + ID: "34b12448945f11eaa1b71c4d701ab86e", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetIPListItem(t *testing.T) { + setup(UsingAccount("foo")) + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "GET", "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "ip": "192.0.2.1", + "comment": "Private IP address", + "created_on": "2020-01-01T08:00:00Z", + "modified_on": "2020-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/foo/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items/"+ + "34b12448945f11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2020-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2020-01-10T14:00:00Z") + + want := IPListItem{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + IP: "192.0.2.1", + Comment: "Private IP address", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.GetIPListItem(context.Background(), "2c0fc9fa937b11eaa1b71c4d701ab86e", + "34b12448945f11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +}