diff --git a/.changelog/1288.txt b/.changelog/1288.txt new file mode 100644 index 00000000000..74018234412 --- /dev/null +++ b/.changelog/1288.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +lists: add support for hostname and ASN lists. +``` \ No newline at end of file diff --git a/list.go b/list.go index 75e802fe1d9..6cd9fe986b7 100644 --- a/list.go +++ b/list.go @@ -14,6 +14,10 @@ const ( ListTypeIP = "ip" // ListTypeRedirect specifies a list containing redirects. ListTypeRedirect = "redirect" + // ListTypeHostname specifies a list containing hostnames. + ListTypeHostname = "hostname" + // ListTypeHostname specifies a list containing autonomous system numbers (ASNs). + ListTypeASN = "asn" ) // ListBulkOperation contains information about a Bulk Operation. @@ -47,11 +51,17 @@ type Redirect struct { PreservePathSuffix *bool `json:"preserve_path_suffix,omitempty"` } +type Hostname struct { + UrlHostname string `json:"url_hostname"` +} + // ListItem contains information about a single List Item. type ListItem struct { ID string `json:"id"` IP *string `json:"ip,omitempty"` Redirect *Redirect `json:"redirect,omitempty"` + Hostname *Hostname `json:"hostname,omitempty"` + ASN *uint32 `json:"asn,omitempty"` Comment string `json:"comment"` CreatedOn *time.Time `json:"created_on"` ModifiedOn *time.Time `json:"modified_on"` @@ -68,6 +78,8 @@ type ListCreateRequest struct { type ListItemCreateRequest struct { IP *string `json:"ip,omitempty"` Redirect *Redirect `json:"redirect,omitempty"` + Hostname *Hostname `json:"hostname,omitempty"` + ASN *uint32 `json:"asn,omitempty"` Comment string `json:"comment"` } diff --git a/list_test.go b/list_test.go index 9a73a0a8fd3..0a622f8d1ea 100644 --- a/list_test.go +++ b/list_test.go @@ -423,6 +423,117 @@ func TestListsItemsRedirect(t *testing.T) { } } +func TestListsItemsHostname(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": [ + { + "id": "0c0fc9fa937b11eaa1b71c4d701ab86e", + "hostname": { + "url_hostname": "cloudflare.com" + }, + "comment": "CF hostname", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + } + ], + "result_info": { + "cursors": { + "before": "xxx" + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := []ListItem{ + { + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + Hostname: &Hostname{ + UrlHostname: "cloudflare.com", + }, + Comment: "CF hostname", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + } + + actual, err := client.ListListItems( + context.Background(), + AccountIdentifier(testAccountID), + ListListItemsParams{ID: "0c0fc9fa937b11eaa1b71c4d701ab86e"}, + ) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestListsItemsASN(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + + fmt.Fprint(w, `{ + "result": [ + { + "id": "0c0fc9fa937b11eaa1b71c4d701ab86e", + "asn": 3456, + "comment": "ASN", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + } + ], + "result_info": { + "cursors": { + "before": "xxx" + } + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := []ListItem{ + { + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + ASN: Uint32Ptr(3456), + Comment: "ASN", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + }, + } + + actual, err := client.ListListItems( + context.Background(), + AccountIdentifier(testAccountID), + ListListItemsParams{ID: "0c0fc9fa937b11eaa1b71c4d701ab86e"}, + ) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestCreateListItemsIP(t *testing.T) { setup() defer teardown() @@ -507,6 +618,93 @@ func TestCreateListItemsRedirect(t *testing.T) { } } +func TestCreateListItemsHostname(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method, "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/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.CreateListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListCreateItemsParams{ + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + Hostname: &Hostname{ + UrlHostname: "3fonteinen.be", // ie. only match 3fonteinen.be + }, + Comment: "hostname 3F", + }, { + Hostname: &Hostname{ + UrlHostname: "*.cf.com", // ie. match all subdomains of cf.com but not cf.com + }, + Comment: "Hostname cf", + }, { + Hostname: &Hostname{ + UrlHostname: "*.abc.com", // ie. equivalent to match all subdomains of abc.com excluding abc.com + }, + Comment: "Hostname abc", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestCreateListItemsASN(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method, "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/"+testAccountID+"/rules/lists/0c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.CreateListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListCreateItemsParams{ + ID: "0c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + ASN: Uint32Ptr(458), + Comment: "ASN 458", + }, { + ASN: Uint32Ptr(789), + Comment: "ASN 789", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestReplaceListItemsIP(t *testing.T) { setup() defer teardown() @@ -591,6 +789,88 @@ func TestReplaceListItemsRedirect(t *testing.T) { } } +func TestReplaceListItemsHostname(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "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/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.ReplaceListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListReplaceItemsParams{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + Hostname: &Hostname{ + UrlHostname: "3fonteinen.be", + }, + Comment: "hostname 3F", + }, { + Hostname: &Hostname{ + UrlHostname: "cf.com", + }, + Comment: "Hostname cf", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestReplaceListItemsASN(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method, "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/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items", handler) + + want := ListItemCreateResponse{} + want.Success = true + want.Errors = []ResponseInfo{} + want.Messages = []ResponseInfo{} + want.Result.OperationID = "4da8780eeb215e6cb7f48dd981c4ea02" + + actual, err := client.ReplaceListItemsAsync(context.Background(), AccountIdentifier(testAccountID), ListReplaceItemsParams{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Items: []ListItemCreateRequest{{ + ASN: Uint32Ptr(4567), + Comment: "ASN 4567", + }, { + ASN: Uint32Ptr(8901), + Comment: "ASN 8901", + }}}) + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestDeleteListItems(t *testing.T) { setup() defer teardown() @@ -667,6 +947,92 @@ func TestGetListItemIP(t *testing.T) { } } +func TestGetListItemHostname(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "hostname": { + "url_hostname": "cloudflare.com" + }, + "comment": "CF Hostname", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items/"+ + "34b12448945f11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := ListItem{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + Hostname: &Hostname{ + UrlHostname: "cloudflare.com", + }, + Comment: "CF Hostname", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.GetListItem(context.Background(), AccountIdentifier(testAccountID), "2c0fc9fa937b11eaa1b71c4d701ab86e", "34b12448945f11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + +func TestGetListItemASN(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) + w.Header().Set("content-type", "application/json") + fmt.Fprint(w, `{ + "result": { + "id": "2c0fc9fa937b11eaa1b71c4d701ab86e", + "asn": 5555, + "comment": "asn 5555", + "created_on": "2023-01-01T08:00:00Z", + "modified_on": "2023-01-10T14:00:00Z" + }, + "success": true, + "errors": [], + "messages": [] + }`) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/rules/lists/2c0fc9fa937b11eaa1b71c4d701ab86e/items/"+ + "34b12448945f11eaa1b71c4d701ab86e", handler) + + createdOn, _ := time.Parse(time.RFC3339, "2023-01-01T08:00:00Z") + modifiedOn, _ := time.Parse(time.RFC3339, "2023-01-10T14:00:00Z") + + want := ListItem{ + ID: "2c0fc9fa937b11eaa1b71c4d701ab86e", + ASN: Uint32Ptr(5555), + Comment: "asn 5555", + CreatedOn: &createdOn, + ModifiedOn: &modifiedOn, + } + + actual, err := client.GetListItem(context.Background(), AccountIdentifier(testAccountID), "2c0fc9fa937b11eaa1b71c4d701ab86e", "34b12448945f11eaa1b71c4d701ab86e") + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestPollListTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 0) defer cancel()