Repository: cloudflare/cloudflare-go (go)

Issue:
Missing Add Custom Domain to CF Pages
### Current cloudflare-go version

latest

### Description

I cant find the Add a custom Domain for CF Pages.
It seems also missing in the API Reference, but the Endpoint seems to work: 
https://community.cloudflare.com/t/add-pages-domain-via-api/303510/4

Updating and Deleting would be awesome too

### Use cases

It would be really awesome to add custom domains. 

### Potential cloudflare-go usage

```go
api.CreatePagesDomain(...)
```


### References

_No response_

Pull request description:
Add Pages domains support


## Description

Add API calls for pages domains

Would resolve #966 

## Has your change been tested?

Unit tests have been added for all new functions

## 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.
- [ ] My change requires a change to the documentation.
- [ ] 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 (api.cloudflare.com or developers.cloudflare.com) and stable APIs.

Patch:
diff --git a/pages_domain.go b/pages_domain.go
new file mode 100644
index 000000000..9aaf998c9
--- /dev/null
+++ b/pages_domain.go
@@ -0,0 +1,193 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// PagesDomain represents a pages domain.
+type PagesDomain struct {
+	ID               string           `json:"id"`
+	Name             string           `json:"name"`
+	Status           string           `json:"status"`
+	VerificationData VerificationData `json:"verification_data"`
+	ValidationData   ValidationData   `json:"validation_data"`
+	ZoneTag          string           `json:"zone_tag"`
+	CreatedOn        *time.Time       `json:"created_on"`
+}
+
+// VerificationData represents verification data for a domain.
+type VerificationData struct {
+	Status string `json:"status"`
+}
+
+// ValidationData represents validation data for a domain.
+type ValidationData struct {
+	Status string `json:"status"`
+	Method string `json:"method"`
+}
+
+// PagesDomainsParameters represents parameters for a pages domains request.
+type PagesDomainsParameters struct {
+	AccountID   string
+	ProjectName string
+}
+
+// PagesDomainsResponse represents an API response for a pages domains request.
+type PagesDomainsResponse struct {
+	Response
+	Result []PagesDomain `json:"result,omitempty"`
+}
+
+// PagesDomainParameters represents parameters for a pages domain request.
+type PagesDomainParameters struct {
+	AccountID   string
+	ProjectName string
+	DomainName  string `json:"name,omitempty"`
+}
+
+// PagesDomainResponse represents an API response for a pages domain request.
+type PagesDomainResponse struct {
+	Response
+	Result PagesDomain `json:"result,omitempty"`
+}
+
+// GetPagesDomains gets all domains for a pages project.
+//
+// API Reference: https://api.cloudflare.com/#pages-domains-get-domains
+func (api *API) GetPagesDomains(ctx context.Context, params PagesDomainsParameters) ([]PagesDomain, error) {
+	if params.AccountID == "" {
+		return []PagesDomain{}, ErrMissingAccountID
+	}
+
+	if params.ProjectName == "" {
+		return []PagesDomain{}, ErrMissingProjectName
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/pages/projects/%s/domains", params.AccountID, params.ProjectName)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []PagesDomain{}, err
+	}
+
+	var pageDomainResponse PagesDomainsResponse
+	if err := json.Unmarshal(res, &pageDomainResponse); err != nil {
+		return []PagesDomain{}, err
+	}
+	return pageDomainResponse.Result, nil
+}
+
+// GetPagesDomain gets a single domain.
+//
+// API Reference: https://api.cloudflare.com/#pages-domains-get-domains
+func (api *API) GetPagesDomain(ctx context.Context, params PagesDomainParameters) (PagesDomain, error) {
+	if params.AccountID == "" {
+		return PagesDomain{}, ErrMissingAccountID
+	}
+
+	if params.ProjectName == "" {
+		return PagesDomain{}, ErrMissingProjectName
+	}
+
+	if params.DomainName == "" {
+		return PagesDomain{}, ErrMissingDomain
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/pages/projects/%s/domains/%s", params.AccountID, params.ProjectName, params.DomainName)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return PagesDomain{}, err
+	}
+
+	var pagesDomainResponse PagesDomainResponse
+	if err := json.Unmarshal(res, &pagesDomainResponse); err != nil {
+		return PagesDomain{}, err
+	}
+	return pagesDomainResponse.Result, nil
+}
+
+// PagesPatchDomain retries the validation status of a single domain.
+//
+// API Reference: https://api.cloudflare.com/#pages-domains-patch-domain
+func (api *API) PagesPatchDomain(ctx context.Context, params PagesDomainParameters) (PagesDomain, error) {
+	if params.AccountID == "" {
+		return PagesDomain{}, ErrMissingAccountID
+	}
+
+	if params.ProjectName == "" {
+		return PagesDomain{}, ErrMissingProjectName
+	}
+
+	if params.DomainName == "" {
+		return PagesDomain{}, ErrMissingDomain
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/pages/projects/%s/domains/%s", params.AccountID, params.ProjectName, params.DomainName)
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, nil)
+	if err != nil {
+		return PagesDomain{}, err
+	}
+
+	var pagesDomainResponse PagesDomainResponse
+	if err := json.Unmarshal(res, &pagesDomainResponse); err != nil {
+		return PagesDomain{}, err
+	}
+	return pagesDomainResponse.Result, nil
+}
+
+// PagesAddDomain adds a domain to a pages project.
+//
+// API Reference: https://api.cloudflare.com/#pages-domains-add-domain
+func (api *API) PagesAddDomain(ctx context.Context, params PagesDomainParameters) (PagesDomain, error) {
+	if params.AccountID == "" {
+		return PagesDomain{}, ErrMissingAccountID
+	}
+
+	if params.ProjectName == "" {
+		return PagesDomain{}, ErrMissingProjectName
+	}
+
+	if params.DomainName == "" {
+		return PagesDomain{}, ErrMissingDomain
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/pages/projects/%s/domains", params.AccountID, params.ProjectName)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return PagesDomain{}, err
+	}
+
+	var pagesDomainResponse PagesDomainResponse
+	if err := json.Unmarshal(res, &pagesDomainResponse); err != nil {
+		return PagesDomain{}, err
+	}
+	return pagesDomainResponse.Result, nil
+}
+
+// PagesDeleteDomain removes a domain from a pages project.
+//
+// API Reference: https://api.cloudflare.com/#pages-domains-delete-domain
+func (api *API) PagesDeleteDomain(ctx context.Context, params PagesDomainParameters) error {
+	if params.AccountID == "" {
+		return ErrMissingAccountID
+	}
+
+	if params.ProjectName == "" {
+		return ErrMissingProjectName
+	}
+
+	if params.DomainName == "" {
+		return ErrMissingDomain
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/pages/projects/%s/domains/%s", params.AccountID, params.ProjectName, params.DomainName)
+
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, params)
+	if err != nil {
+		return err
+	}
+	return nil
+}


Test patch:
diff --git a/pages_domain_test.go b/pages_domain_test.go
new file mode 100644
index 000000000..9e1e9d39e
--- /dev/null
+++ b/pages_domain_test.go
@@ -0,0 +1,276 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+	"time"
+)
+
+const testPagesProjectName = "page-project"
+
+var testCreatedOn, _ = time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
+
+var testPagesDomain = PagesDomain{
+	ID:     "8232210c-6818-4e34-8d95-cc386874b8d2",
+	Name:   "example.com",
+	Status: "pending",
+	VerificationData: VerificationData{
+		Status: "active",
+	},
+	ValidationData: ValidationData{
+		Status: "active",
+		Method: "http",
+	},
+	ZoneTag:   "023e105f4ecef8ad9ca31a8372d0c353",
+	CreatedOn: &testCreatedOn,
+}
+
+func TestPages_GetDomains(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/pages/projects/"+testPagesProjectName+"/domains", 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, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "id": "8232210c-6818-4e34-8d95-cc386874b8d2",
+      "name": "example.com",
+      "status": "pending",
+      "verification_data": {
+        "status": "active"
+      },
+      "validation_data": {
+        "status": "active",
+        "method": "http"
+      },
+      "zone_tag": "023e105f4ecef8ad9ca31a8372d0c353",
+      "created_on": "2017-01-01T00:00:00Z"
+    }
+  ]
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.GetPagesDomains(context.Background(), PagesDomainsParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing project name is thrown
+	_, err = client.GetPagesDomains(context.Background(), PagesDomainsParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingProjectName, err)
+	}
+
+	out, err := client.GetPagesDomains(context.Background(), PagesDomainsParameters{AccountID: testAccountID, ProjectName: testPagesProjectName})
+	if assert.NoError(t, err) {
+		assert.Equal(t, 1, len(out), "Domains length not correct")
+		assert.Equal(t, out[0], testPagesDomain, "structs not equal")
+	}
+}
+
+func TestPages_GetDomain(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/pages/projects/"+testPagesProjectName+"/domains/example.com", 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, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+      "id": "8232210c-6818-4e34-8d95-cc386874b8d2",
+      "name": "example.com",
+      "status": "pending",
+      "verification_data": {
+        "status": "active"
+      },
+      "validation_data": {
+        "status": "active",
+        "method": "http"
+      },
+      "zone_tag": "023e105f4ecef8ad9ca31a8372d0c353",
+      "created_on": "2017-01-01T00:00:00Z"
+    }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.GetPagesDomain(context.Background(), PagesDomainParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing project name is thrown
+	_, err = client.GetPagesDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingProjectName, err)
+	}
+
+	// Make sure missing domain is thrown
+	_, err = client.GetPagesDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+
+	out, err := client.GetPagesDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName, DomainName: "example.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, testPagesDomain, "structs not equal")
+	}
+}
+
+func TestPages_PatchDomain(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/pages/projects/"+testPagesProjectName+"/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+      "id": "8232210c-6818-4e34-8d95-cc386874b8d2",
+      "name": "example.com",
+      "status": "pending",
+      "verification_data": {
+        "status": "active"
+      },
+      "validation_data": {
+        "status": "active",
+        "method": "http"
+      },
+      "zone_tag": "023e105f4ecef8ad9ca31a8372d0c353",
+      "created_on": "2017-01-01T00:00:00Z"
+    }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.PagesPatchDomain(context.Background(), PagesDomainParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing project name is thrown
+	_, err = client.PagesPatchDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingProjectName, err)
+	}
+
+	// Make sure missing domain is thrown
+	_, err = client.PagesPatchDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+
+	out, err := client.PagesPatchDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName, DomainName: "example.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, testPagesDomain, "structs not equal")
+	}
+}
+
+func TestPages_AddDomain(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/pages/projects/"+testPagesProjectName+"/domains", 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, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+      "id": "8232210c-6818-4e34-8d95-cc386874b8d2",
+      "name": "example.com",
+      "status": "pending",
+      "verification_data": {
+        "status": "active"
+      },
+      "validation_data": {
+        "status": "active",
+        "method": "http"
+      },
+      "zone_tag": "023e105f4ecef8ad9ca31a8372d0c353",
+      "created_on": "2017-01-01T00:00:00Z"
+    }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.PagesAddDomain(context.Background(), PagesDomainParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing project name is thrown
+	_, err = client.PagesAddDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingProjectName, err)
+	}
+
+	// Make sure missing domain is thrown
+	_, err = client.PagesAddDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+
+	out, err := client.PagesAddDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName, DomainName: "example.com"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, out, testPagesDomain, "structs not equal")
+	}
+}
+
+func TestPages_DeleteDomain(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/pages/projects/"+testPagesProjectName+"/domains/example.com", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": null
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	err := client.PagesDeleteDomain(context.Background(), PagesDomainParameters{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	// Make sure missing project name is thrown
+	err = client.PagesDeleteDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingProjectName, err)
+	}
+
+	// Make sure missing domain is thrown
+	err = client.PagesDeleteDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingDomain, err)
+	}
+
+	err = client.PagesDeleteDomain(context.Background(), PagesDomainParameters{AccountID: testAccountID, ProjectName: testPagesProjectName, DomainName: "example.com"})
+	assert.NoError(t, err)
+}