Repository: cloudflare/cloudflare-go
Pull request #2754 (2024-07-17 17:39:36)

Teams / Gateway: Add TeamsCertificate service
## Description

Adds TeamsCertificate resource and service to generate, activate, and delete certificates. Also renames TeamsCertificate in TeamsAccountConfiguration to TeamsCertificateSetting to avoid mixup -- this is a breaking change.

## Has your change been tested?

Unit tests added in teams_certificate_test.go.

## 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)
- [x] 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.
- [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/



Patch:
diff --git a/.changelog/2754.txt b/.changelog/2754.txt
new file mode 100644
index 000000000..c0ec7321c
--- /dev/null
+++ b/.changelog/2754.txt
@@ -0,0 +1,7 @@
+```release-note:breaking-change
+teams_accounts: rename `TeamsCertificate` in `TeamsAccountConfiguration` to `TeamsCertificateSetting`
+```
+
+```release-note:enhancement
+teams_certificates: add `TeamsCertificate` resource to manage gateway certificates
+```
diff --git a/teams_accounts.go b/teams_accounts.go
index f426c68ba..dbded0953 100644
--- a/teams_accounts.go
+++ b/teams_accounts.go
@@ -47,7 +47,7 @@ type TeamsAccountSettings struct {
 	BodyScanning          *TeamsBodyScanning          `json:"body_scanning,omitempty"`
 	ExtendedEmailMatching *TeamsExtendedEmailMatching `json:"extended_email_matching,omitempty"`
 	CustomCertificate     *TeamsCustomCertificate     `json:"custom_certificate,omitempty"`
-	Certificate           *TeamsCertificate           `json:"certificate,omitempty"`
+	Certificate           *TeamsCertificateSetting    `json:"certificate,omitempty"`
 }
 
 type BrowserIsolation struct {
@@ -113,7 +113,7 @@ type TeamsCustomCertificate struct {
 	UpdatedAt     *time.Time `json:"updated_at,omitempty"`
 }
 
-type TeamsCertificate struct {
+type TeamsCertificateSetting struct {
 	ID string `json:"id"`
 }
 
diff --git a/teams_certificates.go b/teams_certificates.go
new file mode 100644
index 000000000..742678669
--- /dev/null
+++ b/teams_certificates.go
@@ -0,0 +1,158 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+type TeamsCertificate struct {
+	Enabled       *bool      `json:"enabled"`
+	ID            string     `json:"id"`
+	BindingStatus string     `json:"binding_status"`
+	QsPackId      string     `json:"qs_pack_id"`
+	Type          string     `json:"type"`
+	UpdatedAt     *time.Time `json:"updated_at"`
+	UploadedOn    *time.Time `json:"uploaded_on"`
+	CreatedAt     *time.Time `json:"created_at"`
+	ExpiresOn     *time.Time `json:"expires_on"`
+}
+
+type TeamsCertificateCreateRequest struct {
+	ValidityPeriodDays int `json:"validity_period_days,omitempty"`
+}
+
+const DEFAULT_VALIDITY_PERIOD_DAYS = 1826
+
+// TeamsCertificateResponse is the API response, containing a single certificate.
+type TeamsCertificateResponse struct {
+	Response
+	Result TeamsCertificate `json:"result"`
+}
+
+// TeamsCertificatesResponse is the API response, containing an array of certificates.
+type TeamsCertificatesResponse struct {
+	Response
+	Result []TeamsCertificate `json:"result"`
+}
+
+// TeamsCertificates returns all certificates in an account
+//
+// API reference: https://developers.cloudflare.com/api/operations/zero-trust-certificates-list-zero-trust-certificates
+func (api *API) TeamsCertificates(ctx context.Context, accountID string) ([]TeamsCertificate, error) {
+	uri := fmt.Sprintf("/accounts/%s/gateway/certificates", accountID)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []TeamsCertificate{}, err
+	}
+
+	var teamsCertificatesResponse TeamsCertificatesResponse
+	err = json.Unmarshal(res, &teamsCertificatesResponse)
+	if err != nil {
+		return []TeamsCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return teamsCertificatesResponse.Result, nil
+}
+
+// TeamsCertificate returns teams account certificate.
+//
+// API reference: https://developers.cloudflare.com/api/operations/zero-trust-certificates-zero-trust-certificate-details
+func (api *API) TeamsCertificate(ctx context.Context, accountID string, certificateId string) (TeamsCertificate, error) {
+	uri := fmt.Sprintf("/accounts/%s/gateway/certificates/%s", accountID, certificateId)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return TeamsCertificate{}, err
+	}
+
+	var teamsCertificateResponse TeamsCertificateResponse
+	err = json.Unmarshal(res, &teamsCertificateResponse)
+	if err != nil {
+		return TeamsCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return teamsCertificateResponse.Result, nil
+}
+
+// TeamsGenerateCertificate generates a new gateway managed certificate
+//
+// API reference: https://developers.cloudflare.com/api/operations/zero-trust-certificates-create-zero-trust-certificate
+func (api *API) TeamsGenerateCertificate(ctx context.Context, accountID string, certificateRequest TeamsCertificateCreateRequest) (TeamsCertificate, error) {
+	uri := fmt.Sprintf("/accounts/%s/gateway/certificates", accountID)
+
+	if certificateRequest.ValidityPeriodDays == 0 {
+		certificateRequest.ValidityPeriodDays = DEFAULT_VALIDITY_PERIOD_DAYS
+	}
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, certificateRequest)
+	if err != nil {
+		return TeamsCertificate{}, err
+	}
+
+	var teamsCertResponse TeamsCertificateResponse
+	err = json.Unmarshal(res, &teamsCertResponse)
+	if err != nil {
+		return TeamsCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return teamsCertResponse.Result, nil
+}
+
+// TeamsActivateCertificate activates a certificate
+//
+// API reference: https://developers.cloudflare.com/api/operations/zero-trust-certificates-activate-zero-trust-certificate
+func (api *API) TeamsActivateCertificate(ctx context.Context, accountID string, certificateId string) (TeamsCertificate, error) {
+	uri := fmt.Sprintf("/accounts/%s/gateway/certificates/%s/activate", accountID, certificateId)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil)
+	if err != nil {
+		return TeamsCertificate{}, err
+	}
+
+	var teamsCertResponse TeamsCertificateResponse
+	err = json.Unmarshal(res, &teamsCertResponse)
+	if err != nil {
+		return TeamsCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return teamsCertResponse.Result, nil
+}
+
+// TeamsDectivateCertificate deactivates a certificate
+//
+// API reference: https://developers.cloudflare.com/api/operations/zero-trust-certificates-deactivate-zero-trust-certificate
+func (api *API) TeamsDeactivateCertificate(ctx context.Context, accountID string, certificateId string) (TeamsCertificate, error) {
+	uri := fmt.Sprintf("/accounts/%s/gateway/certificates/%s/deactivate", accountID, certificateId)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil)
+	if err != nil {
+		return TeamsCertificate{}, err
+	}
+
+	var teamsCertResponse TeamsCertificateResponse
+	err = json.Unmarshal(res, &teamsCertResponse)
+	if err != nil {
+		return TeamsCertificate{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return teamsCertResponse.Result, nil
+}
+
+// TeamsDeleteCertificate deletes a certificate.
+//
+// API reference: https://developers.cloudflare.com/api/operations/zero-trust-certificates-delete-zero-trust-certificate
+func (api *API) TeamsDeleteCertificate(ctx context.Context, accountID string, certificateId string) error {
+	uri := fmt.Sprintf("/accounts/%s/gateway/certificates/%s", accountID, certificateId)
+
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}


Test patch:
diff --git a/teams_accounts_test.go b/teams_accounts_test.go
index 52873da6c..569d35254 100644
--- a/teams_accounts_test.go
+++ b/teams_accounts_test.go
@@ -141,7 +141,7 @@ func TestTeamsAccountConfiguration(t *testing.T) {
 			ExtendedEmailMatching: &TeamsExtendedEmailMatching{
 				Enabled: BoolPtr(true),
 			},
-			Certificate: &TeamsCertificate{
+			Certificate: &TeamsCertificateSetting{
 				ID: "7559a944-3dd7-41bf-b183-360a814a8c36",
 			},
 		})
diff --git a/teams_certificates_test.go b/teams_certificates_test.go
new file mode 100644
index 000000000..8fc7155bb
--- /dev/null
+++ b/teams_certificates_test.go
@@ -0,0 +1,229 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTeamsCertificate(t *testing.T) {
+	setup()
+	defer teardown()
+
+	testCertID := "80c8a54e-d55c-46c6-86bb-e8a3c90472f4"
+
+	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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"enabled": false,
+				"id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4",
+				"binding_status": "inactive",
+				"qs_pack_id": "00000000-0000-0000-0000-000000000000",
+				"type": "gateway_managed",
+				"updated_at": "0001-01-01T00:00:00Z",
+				"uploaded_on": "0001-01-01T00:00:00Z",
+				"created_at": "2024-06-19T08:28:34.235742Z",
+				"expires_on": "2029-06-19T08:24:00Z"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/gateway/certificates/"+testCertID, handler)
+
+	actual, err := client.TeamsCertificate(context.Background(), testAccountID, testCertID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, *actual.Enabled, false)
+		assert.Equal(t, actual.ID, testCertID)
+		assert.Equal(t, actual.BindingStatus, "inactive")
+		assert.Equal(t, actual.Type, "gateway_managed")
+	}
+}
+
+func TestTeamsCertificatesList(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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"enabled": false,
+					"id": "43a36083-987b-4321-95ab-4052771c4e6f",
+					"binding_status": "inactive",
+					"qs_pack_id": "00000000-0000-0000-0000-000000000000",
+					"type": "custom",
+					"updated_at": "0001-01-01T00:00:00Z",
+					"uploaded_on": "2022-12-08T22:58:13.646596Z",
+					"created_at": "0001-01-01T00:00:00Z",
+					"expires_on": "2122-10-29T16:59:47Z"
+				},
+				{
+					"enabled": false,
+					"id": "4a9d6ecf-0fdd-4676-818a-ee6b45f17f9b",
+					"binding_status": "inactive",
+					"qs_pack_id": "00000000-0000-0000-0000-000000000000",
+					"type": "gateway_managed",
+					"updated_at": "0001-01-01T00:00:00Z",
+					"uploaded_on": "0001-01-01T00:00:00Z",
+					"created_at": "2024-05-29T01:32:21.133597Z",
+					"expires_on": "2029-05-29T01:27:00Z"
+				}
+			]
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/gateway/certificates", handler)
+
+	actual, err := client.TeamsCertificates(context.Background(), testAccountID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, len(actual), 2)
+		assert.Equal(t, actual[0].ID, "43a36083-987b-4321-95ab-4052771c4e6f")
+		assert.Equal(t, actual[1].ID, "4a9d6ecf-0fdd-4676-818a-ee6b45f17f9b")
+	}
+}
+
+func TestTeamsAccountGenerateCertificate(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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"enabled": false,
+				"id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4",
+				"binding_status": "inactive",
+				"qs_pack_id": "00000000-0000-0000-0000-000000000000",
+				"type": "gateway_managed",
+				"updated_at": "0001-01-01T00:00:00Z",
+				"uploaded_on": "0001-01-01T00:00:00Z",
+				"created_at": "2024-06-19T08:28:34.235742Z",
+				"expires_on": "2029-06-19T08:24:00Z"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/gateway/certificates", handler)
+
+	createRequest := TeamsCertificateCreateRequest{
+		ValidityPeriodDays: 1826,
+	}
+	actual, err := client.TeamsGenerateCertificate(context.Background(), testAccountID, createRequest)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, actual.ID, "80c8a54e-d55c-46c6-86bb-e8a3c90472f4")
+	}
+}
+
+func TestTeamsActivateCertificate(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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"enabled": false,
+				"id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4",
+				"binding_status": "pending_deployment",
+				"qs_pack_id": "00000000-0000-0000-0000-000000000000",
+				"type": "gateway_managed",
+				"updated_at": "0001-01-01T00:00:00Z",
+				"uploaded_on": "0001-01-01T00:00:00Z",
+				"created_at": "2024-06-19T08:28:34.235742Z",
+				"expires_on": "2029-06-19T08:24:00Z"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/gateway/certificates/80c8a54e-d55c-46c6-86bb-e8a3c90472f4/activate", handler)
+
+	cert, err := client.TeamsActivateCertificate(context.Background(), testAccountID, "80c8a54e-d55c-46c6-86bb-e8a3c90472f4")
+
+	assert.NoError(t, err)
+	assert.Equal(t, cert.BindingStatus, "pending_deployment")
+}
+
+func TestTeamsDeactivateCertificate(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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"enabled": false,
+				"id": "80c8a54e-d55c-46c6-86bb-e8a3c90472f4",
+				"binding_status": "pending_deletion",
+				"qs_pack_id": "00000000-0000-0000-0000-000000000000",
+				"type": "gateway_managed",
+				"updated_at": "0001-01-01T00:00:00Z",
+				"uploaded_on": "0001-01-01T00:00:00Z",
+				"created_at": "2024-06-19T08:28:34.235742Z",
+				"expires_on": "2029-06-19T08:24:00Z"
+			}
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/gateway/certificates/80c8a54e-d55c-46c6-86bb-e8a3c90472f4/deactivate", handler)
+
+	cert, err := client.TeamsDeactivateCertificate(context.Background(), testAccountID, "80c8a54e-d55c-46c6-86bb-e8a3c90472f4")
+
+	assert.NoError(t, err)
+	assert.Equal(t, cert.BindingStatus, "pending_deletion")
+}
+
+func TestTeamsDeleteCertificate(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := 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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": nil
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/gateway/certificates/80c8a54e-d55c-46c6-86bb-e8a3c90472f4", handler)
+
+	err := client.TeamsDeleteCertificate(context.Background(), testAccountID, "80c8a54e-d55c-46c6-86bb-e8a3c90472f4")
+
+	assert.NoError(t, err)
+}