Repository: cloudflare/cloudflare-go
Pull request #1288 (2023-05-22 20:06:01)

FW-5588: Add hostname and ASN lists
## Description

This adds support for Hostname and ASN lists

## Has your change been tested?

Integration tests were written to ensure the requests are parsed successfully.

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


github-actions[bot]: changelog detected :white_check_mark:

Patch:
diff --git a/.changelog/1288.txt b/.changelog/1288.txt
new file mode 100644
index 000000000..740182344
--- /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 75e802fe1..6cd9fe986 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"`
 }
 


Test patch:
diff --git a/list_test.go b/list_test.go
index 9a73a0a8f..0a622f8d1 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()