Repository: cloudflare/cloudflare-go (go)

Issue:
[Improvement] Add support for uploading Access Mutual TLS Root Certificates
## Improvement Description

As far as I can tell (and please do tell me if I've missed something) there is no support for uploading Mutual TLS Root Certificates even though there seem to be [API endpoints](https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-properties) for doing so.

It would be great if the Cloudflare Go library would support these endpoints. :)

## Context

I've just opened [this issue](https://github.com/cloudflare/terraform-provider-cloudflare/issues/878) over on the Cloudflare Terraform provider repo, requesting support for uploading Mutual TLS Root Certificates there. In order for them to support it, it would first need to be implemented in this library.

## References

- https://github.com/cloudflare/terraform-provider-cloudflare/issues/878

Pull request:
Add support for Access Mutual TLS certificates
Introduces support for Access Mutual TLS certificates at the account and
zone level following similar patterns to the previous implementations.

### API documentation

List all certificates

- https://api.cloudflare.com/#access-mutual-tls-authentication-properties
- https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-properties

List single certificate

- https://api.cloudflare.com/#access-mutual-tls-authentication-access-certificate-details
- https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-access-certificate-details

Create certificate

- https://api.cloudflare.com/#access-mutual-tls-authentication-create-access-certificate
- https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-create-access-certificate

Update certificate

- https://api.cloudflare.com/#access-mutual-tls-authentication-update-access-certificate
- https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-update-access-certificate

Delete certificate

- https://api.cloudflare.com/#access-mutual-tls-authentication-update-access-certificate
- https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-update-access-certificate

Closes #559


Patch:
diff --git a/access_mutual_tls_certificates.go b/access_mutual_tls_certificates.go
new file mode 100644
index 000000000..b747648bb
--- /dev/null
+++ b/access_mutual_tls_certificates.go
@@ -0,0 +1,225 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// AccessMutualTLSCertificate is the structure of a single Access Mutual TLS
+// certificate.
+type AccessMutualTLSCertificate struct {
+	ID                  string    `json:"id,omitempty"`
+	CreatedAt           time.Time `json:"created_at,omitempty"`
+	UpdatedAt           time.Time `json:"updated_at,omitempty"`
+	ExpiresOn           time.Time `json:"expires_on,omitempty"`
+	Name                string    `json:"name,omitempty"`
+	Fingerprint         string    `json:"fingerprint,omitempty"`
+	Certificate         string    `json:"certificate,omitempty"`
+	AssociatedHostnames []string  `json:"associated_hostnames,omitempty"`
+}
+
+// AccessMutualTLSCertificateListResponse is the API response for all Access
+// Mutual TLS certificates.
+type AccessMutualTLSCertificateListResponse struct {
+	Response
+	Result []AccessMutualTLSCertificate `json:"result"`
+}
+
+// AccessMutualTLSCertificateDetailResponse is the API response for a single
+// Access Mutual TLS certificate.
+type AccessMutualTLSCertificateDetailResponse struct {
+	Response
+	Result AccessMutualTLSCertificate `json:"result"`
+}
+
+// AccessMutualTLSCertificates returns all Access TLS certificates for the account
+// level.
+//
+// API reference: https://api.cloudflare.com/#access-mutual-tls-authentication-properties
+func (api *API) AccessMutualTLSCertificates(accountID string) ([]AccessMutualTLSCertificate, error) {
+	return api.accessMutualTLSCertificates(accountID, AccountRouteRoot)
+}
+
+// ZoneAccessMutualTLSCertificates returns all Access TLS certificates for the
+// zone level.
+//
+// API reference: https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-properties
+func (api *API) ZoneAccessMutualTLSCertificates(zoneID string) ([]AccessMutualTLSCertificate, error) {
+	return api.accessMutualTLSCertificates(zoneID, ZoneRouteRoot)
+}
+
+func (api *API) accessMutualTLSCertificates(id string, routeRoot RouteRoot) ([]AccessMutualTLSCertificate, error) {
+	uri := fmt.Sprintf(
+		"/%s/%s/access/certificates",
+		routeRoot,
+		id,
+	)
+
+	res, err := api.makeRequestContext(context.Background(), "GET", uri, nil)
+	if err != nil {
+		return []AccessMutualTLSCertificate{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessMutualTLSCertificateListResponse AccessMutualTLSCertificateListResponse
+	err = json.Unmarshal(res, &accessMutualTLSCertificateListResponse)
+	if err != nil {
+		return []AccessMutualTLSCertificate{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessMutualTLSCertificateListResponse.Result, nil
+}
+
+// AccessMutualTLSCertificate returns a single account level Access Mutual TLS
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#access-mutual-tls-authentication-access-certificate-details
+func (api *API) AccessMutualTLSCertificate(accountID, certificateID string) (AccessMutualTLSCertificate, error) {
+	return api.accessMutualTLSCertificate(accountID, certificateID, AccountRouteRoot)
+}
+
+// ZoneAccessMutualTLSCertificate returns a single zone level Access Mutual TLS
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-access-certificate-details
+func (api *API) ZoneAccessMutualTLSCertificate(zoneID, certificateID string) (AccessMutualTLSCertificate, error) {
+	return api.accessMutualTLSCertificate(zoneID, certificateID, ZoneRouteRoot)
+}
+
+func (api *API) accessMutualTLSCertificate(id, certificateID string, routeRoot RouteRoot) (AccessMutualTLSCertificate, error) {
+	uri := fmt.Sprintf(
+		"/%s/%s/access/certificates/%s",
+		routeRoot,
+		id,
+		certificateID,
+	)
+
+	res, err := api.makeRequestContext(context.Background(), "GET", uri, nil)
+	if err != nil {
+		return AccessMutualTLSCertificate{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessMutualTLSCertificateDetailResponse AccessMutualTLSCertificateDetailResponse
+	err = json.Unmarshal(res, &accessMutualTLSCertificateDetailResponse)
+	if err != nil {
+		return AccessMutualTLSCertificate{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessMutualTLSCertificateDetailResponse.Result, nil
+}
+
+// CreateAccessMutualTLSCertificate creates an account level Access TLS Mutual
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#access-mutual-tls-authentication-create-access-certificate
+func (api *API) CreateAccessMutualTLSCertificate(accountID string, certificate AccessMutualTLSCertificate) (AccessMutualTLSCertificate, error) {
+	return api.createAccessMutualTLSCertificate(accountID, certificate, AccountRouteRoot)
+}
+
+// CreateZoneAccessMutualTLSCertificate creates a zone level Access TLS Mutual
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-create-access-certificate
+func (api *API) CreateZoneAccessMutualTLSCertificate(zoneID string, certificate AccessMutualTLSCertificate) (AccessMutualTLSCertificate, error) {
+	return api.createAccessMutualTLSCertificate(zoneID, certificate, ZoneRouteRoot)
+}
+
+func (api *API) createAccessMutualTLSCertificate(id string, certificate AccessMutualTLSCertificate, routeRoot RouteRoot) (AccessMutualTLSCertificate, error) {
+	uri := fmt.Sprintf(
+		"/%s/%s/access/certificates",
+		routeRoot,
+		id,
+	)
+
+	res, err := api.makeRequestContext(context.Background(), "POST", uri, certificate)
+	if err != nil {
+		return AccessMutualTLSCertificate{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessMutualTLSCertificateDetailResponse AccessMutualTLSCertificateDetailResponse
+	err = json.Unmarshal(res, &accessMutualTLSCertificateDetailResponse)
+	if err != nil {
+		return AccessMutualTLSCertificate{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessMutualTLSCertificateDetailResponse.Result, nil
+}
+
+// UpdateAccessMutualTLSCertificate updates an account level Access TLS Mutual
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#access-mutual-tls-authentication-update-access-certificate
+func (api *API) UpdateAccessMutualTLSCertificate(accountID, certificateID string, certificate AccessMutualTLSCertificate) (AccessMutualTLSCertificate, error) {
+	return api.updateAccessMutualTLSCertificate(accountID, certificateID, certificate, AccountRouteRoot)
+}
+
+// UpdateZoneAccessMutualTLSCertificate updates a zone level Access TLS Mutual
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-update-access-certificate
+func (api *API) UpdateZoneAccessMutualTLSCertificate(zoneID, certificateID string, certificate AccessMutualTLSCertificate) (AccessMutualTLSCertificate, error) {
+	return api.updateAccessMutualTLSCertificate(zoneID, certificateID, certificate, ZoneRouteRoot)
+}
+
+func (api *API) updateAccessMutualTLSCertificate(id string, certificateID string, certificate AccessMutualTLSCertificate, routeRoot RouteRoot) (AccessMutualTLSCertificate, error) {
+	uri := fmt.Sprintf(
+		"/%s/%s/access/certificates/%s",
+		routeRoot,
+		id,
+		certificateID,
+	)
+
+	res, err := api.makeRequestContext(context.Background(), "PUT", uri, certificate)
+	if err != nil {
+		return AccessMutualTLSCertificate{}, errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessMutualTLSCertificateDetailResponse AccessMutualTLSCertificateDetailResponse
+	err = json.Unmarshal(res, &accessMutualTLSCertificateDetailResponse)
+	if err != nil {
+		return AccessMutualTLSCertificate{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return accessMutualTLSCertificateDetailResponse.Result, nil
+}
+
+// DeleteAccessMutualTLSCertificate destroys an account level Access Mutual
+// TLS certificate.
+//
+// API reference: https://api.cloudflare.com/#access-mutual-tls-authentication-update-access-certificate
+func (api *API) DeleteAccessMutualTLSCertificate(accountID, certificateID string) error {
+	return api.deleteAccessMutualTLSCertificate(accountID, certificateID, AccountRouteRoot)
+}
+
+// DeleteZoneAccessMutualTLSCertificate destroys a zone level Access Mutual TLS
+// certificate.
+//
+// API reference: https://api.cloudflare.com/#zone-level-access-mutual-tls-authentication-update-access-certificate
+func (api *API) DeleteZoneAccessMutualTLSCertificate(zoneID, certificateID string) error {
+	return api.deleteAccessMutualTLSCertificate(zoneID, certificateID, ZoneRouteRoot)
+}
+
+func (api *API) deleteAccessMutualTLSCertificate(id, certificateID string, routeRoot RouteRoot) error {
+	uri := fmt.Sprintf(
+		"/%s/%s/access/certificates/%s",
+		routeRoot,
+		id,
+		certificateID,
+	)
+
+	res, err := api.makeRequestContext(context.Background(), "DELETE", uri, nil)
+	if err != nil {
+		return errors.Wrap(err, errMakeRequestError)
+	}
+
+	var accessMutualTLSCertificateDetailResponse AccessMutualTLSCertificateDetailResponse
+	err = json.Unmarshal(res, &accessMutualTLSCertificateDetailResponse)
+	if err != nil {
+		return errors.Wrap(err, errUnmarshalError)
+	}
+
+	return nil
+}
diff --git a/argo_tunnel.go b/argo_tunnel.go
index dc31f0319..0318ef670 100644
--- a/argo_tunnel.go
+++ b/argo_tunnel.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"net/http"
 	"time"
 
 	"github.com/pkg/errors"
@@ -47,7 +46,7 @@ type ArgoTunnelDetailResponse struct {
 func (api *API) ArgoTunnels(ctx context.Context, accountID string) ([]ArgoTunnel, error) {
 	uri := "/accounts/" + accountID + "/tunnels"
 
-	res, err := api.makeRequestContextWithHeaders(ctx, "GET", uri, nil, argoV1Header())
+	res, err := api.makeRequestContext(ctx, "GET", uri, nil)
 	if err != nil {
 		return []ArgoTunnel{}, errors.Wrap(err, errMakeRequestError)
 	}
@@ -66,7 +65,7 @@ func (api *API) ArgoTunnels(ctx context.Context, accountID string) ([]ArgoTunnel
 func (api *API) ArgoTunnel(ctx context.Context, accountID, tunnelUUID string) (ArgoTunnel, error) {
 	uri := fmt.Sprintf("/accounts/%s/tunnels/%s", accountID, tunnelUUID)
 
-	res, err := api.makeRequestContextWithHeaders(ctx, "GET", uri, nil, argoV1Header())
+	res, err := api.makeRequestContext(ctx, "GET", uri, nil)
 	if err != nil {
 		return ArgoTunnel{}, errors.Wrap(err, errMakeRequestError)
 	}
@@ -87,7 +86,7 @@ func (api *API) CreateArgoTunnel(ctx context.Context, accountID, name, secret st
 
 	tunnel := ArgoTunnel{Name: name, Secret: secret}
 
-	res, err := api.makeRequestContextWithHeaders(ctx, "POST", uri, tunnel, argoV1Header())
+	res, err := api.makeRequestContext(ctx, "POST", uri, tunnel)
 	if err != nil {
 		return ArgoTunnel{}, errors.Wrap(err, errMakeRequestError)
 	}
@@ -97,7 +96,6 @@ func (api *API) CreateArgoTunnel(ctx context.Context, accountID, name, secret st
 	if err != nil {
 		return ArgoTunnel{}, errors.Wrap(err, errUnmarshalError)
 	}
-
 	return argoDetailsResponse.Result, nil
 }
 
@@ -107,7 +105,7 @@ func (api *API) CreateArgoTunnel(ctx context.Context, accountID, name, secret st
 func (api *API) DeleteArgoTunnel(ctx context.Context, accountID, tunnelUUID string) error {
 	uri := fmt.Sprintf("/accounts/%s/tunnels/%s", accountID, tunnelUUID)
 
-	res, err := api.makeRequestContextWithHeaders(ctx, "DELETE", uri, nil, argoV1Header())
+	res, err := api.makeRequestContext(ctx, "DELETE", uri, nil)
 	if err != nil {
 		return errors.Wrap(err, errMakeRequestError)
 	}
@@ -127,7 +125,7 @@ func (api *API) DeleteArgoTunnel(ctx context.Context, accountID, tunnelUUID stri
 func (api *API) CleanupArgoTunnelConnections(ctx context.Context, accountID, tunnelUUID string) error {
 	uri := fmt.Sprintf("/accounts/%s/tunnels/%s/connections", accountID, tunnelUUID)
 
-	res, err := api.makeRequestContextWithHeaders(ctx, "DELETE", uri, nil, argoV1Header())
+	res, err := api.makeRequestContext(ctx, "DELETE", uri, nil)
 	if err != nil {
 		return errors.Wrap(err, errMakeRequestError)
 	}
@@ -140,14 +138,3 @@ func (api *API) CleanupArgoTunnelConnections(ctx context.Context, accountID, tun
 
 	return nil
 }
-
-// The early implementation of Argo Tunnel endpoints didn't conform to the V4
-// API standard response structure. This has been remedied going forward however
-// to support older clients this isn't yet the default. An explicit `Accept`
-// header is used to get the V4 compatible version.
-func argoV1Header() http.Header {
-	header := make(http.Header)
-	header.Set("Accept", "application/json;version=1")
-
-	return header
-}
diff --git a/cloudflare.go b/cloudflare.go
index 1cdd6a9a0..77a7df085 100644
--- a/cloudflare.go
+++ b/cloudflare.go
@@ -172,10 +172,6 @@ func (api *API) makeRequestContext(ctx context.Context, method, uri string, para
 	return api.makeRequestWithAuthType(ctx, method, uri, params, api.authType)
 }
 
-func (api *API) makeRequestContextWithHeaders(ctx context.Context, method, uri string, params interface{}, headers http.Header) ([]byte, error) {
-	return api.makeRequestWithAuthTypeAndHeaders(ctx, method, uri, params, api.authType, headers)
-}
-
 func (api *API) makeRequestWithHeaders(method, uri string, params interface{}, headers http.Header) ([]byte, error) {
 	return api.makeRequestWithAuthTypeAndHeaders(context.TODO(), method, uri, params, api.authType, headers)
 }
diff --git a/workers_cron_triggers.go b/workers_cron_triggers.go
deleted file mode 100644
index 222ebc319..000000000
--- a/workers_cron_triggers.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"time"
-
-	"github.com/pkg/errors"
-)
-
-// WorkerCronTriggerResponse represents the response from the Worker cron trigger
-// API endpoint.
-type WorkerCronTriggerResponse struct {
-	Response
-	Result WorkerCronTriggerSchedules `json:"result"`
-}
-
-// WorkerCronTriggerSchedules contains the schedule of Worker cron triggers.
-type WorkerCronTriggerSchedules struct {
-	Schedules []WorkerCronTrigger `json:"schedules"`
-}
-
-// WorkerCronTrigger holds an individual cron schedule for a worker.
-type WorkerCronTrigger struct {
-	Cron       string     `json:"cron"`
-	CreatedOn  *time.Time `json:"created_on,omitempty"`
-	ModifiedOn *time.Time `json:"modified_on,omitempty"`
-}
-
-// ListWorkerCronTriggers fetches all available cron triggers for a single Worker
-// script.
-//
-// API reference: https://api.cloudflare.com/#worker-cron-trigger-get-cron-triggers
-func (api *API) ListWorkerCronTriggers(ctx context.Context, scriptName string) ([]WorkerCronTrigger, error) {
-	if err := api.checkAccountID(); err != nil {
-		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
-	}
-
-	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", api.AccountID, scriptName)
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
-	}
-
-	result := WorkerCronTriggerResponse{}
-	if err := json.Unmarshal(res, &result); err != nil {
-		return []WorkerCronTrigger{}, errors.Wrap(err, errUnmarshalError)
-	}
-
-	return result.Result.Schedules, err
-}
-
-// UpdateWorkerCronTriggers updates a single schedule for a Worker cron trigger.
-//
-// API reference: https://api.cloudflare.com/#worker-cron-trigger-update-cron-triggers
-func (api *API) UpdateWorkerCronTriggers(ctx context.Context, scriptName string, crons []WorkerCronTrigger) ([]WorkerCronTrigger, error) {
-	if err := api.checkAccountID(); err != nil {
-		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
-	}
-
-	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/schedules", api.AccountID, scriptName)
-	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, crons)
-	if err != nil {
-		return []WorkerCronTrigger{}, errors.Wrap(err, errMakeRequestError)
-	}
-
-	result := WorkerCronTriggerResponse{}
-	if err := json.Unmarshal(res, &result); err != nil {
-		return []WorkerCronTrigger{}, errors.Wrap(err, errUnmarshalError)
-	}
-
-	return result.Result.Schedules, err
-}


Test patch:
diff --git a/access_mutual_tls_certificates_test.go b/access_mutual_tls_certificates_test.go
new file mode 100644
index 000000000..6b1c1f87d
--- /dev/null
+++ b/access_mutual_tls_certificates_test.go
@@ -0,0 +1,276 @@
+package cloudflare
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAccessMutualTLSCertificates(t *testing.T) {
+	setup()
+	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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+					"created_at": "2014-01-01T05:20:00.12345Z",
+					"updated_at": "2014-01-01T05:20:00.12345Z",
+					"expires_on": "2014-01-01T05:20:00.12345Z",
+					"name": "Allow devs",
+					"fingerprint": "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+					"associated_hostnames": [
+						"admin.example.com"
+					]
+				}
+			]
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	expiresOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	want := []AccessMutualTLSCertificate{{
+		ID:                  "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		CreatedAt:           createdAt,
+		UpdatedAt:           updatedAt,
+		ExpiresOn:           expiresOn,
+		Name:                "Allow devs",
+		Fingerprint:         "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+		AssociatedHostnames: []string{"admin.example.com"},
+	}}
+
+	mux.HandleFunc("/accounts/"+accountID+"/access/certificates", handler)
+
+	actual, err := client.AccessMutualTLSCertificates(accountID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	mux.HandleFunc("/zones/"+zoneID+"/access/certificates", handler)
+
+	actual, err = client.ZoneAccessMutualTLSCertificates(zoneID)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestAccessMutualTLSCertificate(t *testing.T) {
+	setup()
+	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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"expires_on": "2014-01-01T05:20:00.12345Z",
+				"name": "Allow devs",
+				"fingerprint": "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+				"associated_hostnames": [
+					"admin.example.com"
+				]
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	expiresOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	want := AccessMutualTLSCertificate{
+		ID:                  "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		CreatedAt:           createdAt,
+		UpdatedAt:           updatedAt,
+		ExpiresOn:           expiresOn,
+		Name:                "Allow devs",
+		Fingerprint:         "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+		AssociatedHostnames: []string{"admin.example.com"},
+	}
+
+	mux.HandleFunc("/accounts/"+accountID+"/access/certificates/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	actual, err := client.AccessMutualTLSCertificate(accountID, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	mux.HandleFunc("/zones/"+zoneID+"/access/certificates/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	actual, err = client.ZoneAccessMutualTLSCertificate(zoneID, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestCreateAccessMutualTLSCertificate(t *testing.T) {
+	setup()
+	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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"expires_on": "2014-01-01T05:20:00.12345Z",
+				"name": "Allow devs",
+				"fingerprint": "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+				"associated_hostnames": [
+					"admin.example.com"
+				]
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	expiresOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	certificate := AccessMutualTLSCertificate{
+		Name:        "Allow devs",
+		Certificate: "-----BEGIN CERTIFICATE-----\nMIIGAjCCA+qgAwIBAgIJAI7kymlF7CWT...N4RI7KKB7nikiuUf8vhULKy5IX10\nDrUtmu/B\n-----END CERTIFICATE-----",
+	}
+
+	want := AccessMutualTLSCertificate{
+		ID:                  "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		CreatedAt:           createdAt,
+		UpdatedAt:           updatedAt,
+		ExpiresOn:           expiresOn,
+		Name:                "Allow devs",
+		Fingerprint:         "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+		AssociatedHostnames: []string{"admin.example.com"},
+	}
+
+	mux.HandleFunc("/accounts/"+accountID+"/access/certificates", handler)
+
+	actual, err := client.CreateAccessMutualTLSCertificate(accountID, certificate)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	mux.HandleFunc("/zones/"+zoneID+"/access/certificates", handler)
+
+	actual, err = client.CreateZoneAccessMutualTLSCertificate(zoneID, certificate)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestUpdateAccessMutualTLSCertificate(t *testing.T) {
+	setup()
+	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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"updated_at": "2014-01-01T05:20:00.12345Z",
+				"expires_on": "2014-01-01T05:20:00.12345Z",
+				"name": "Allow devs",
+				"fingerprint": "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+				"associated_hostnames": [
+					"admin.example.com"
+				]
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	expiresOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+
+	certificate := AccessMutualTLSCertificate{
+		Name:        "Allow devs",
+		Certificate: "-----BEGIN CERTIFICATE-----\nMIIGAjCCA+qgAwIBAgIJAI7kymlF7CWT...N4RI7KKB7nikiuUf8vhULKy5IX10\nDrUtmu/B\n-----END CERTIFICATE-----",
+	}
+
+	want := AccessMutualTLSCertificate{
+		ID:                  "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+		CreatedAt:           createdAt,
+		UpdatedAt:           updatedAt,
+		ExpiresOn:           expiresOn,
+		Name:                "Allow devs",
+		Fingerprint:         "MD5 Fingerprint=1E:80:0F:7A:FD:31:55:96:DE:D5:CB:E2:F0:91:F6:91",
+		AssociatedHostnames: []string{"admin.example.com"},
+	}
+
+	mux.HandleFunc("/accounts/"+accountID+"/access/certificates/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	actual, err := client.UpdateAccessMutualTLSCertificate(accountID, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", certificate)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	mux.HandleFunc("/zones/"+zoneID+"/access/certificates/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	actual, err = client.UpdateZoneAccessMutualTLSCertificate(zoneID, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415", certificate)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestDeleteAccessMutualTLSCertificate(t *testing.T) {
+	setup()
+	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.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415"
+			}
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/"+accountID+"/access/certificates/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	err := client.DeleteAccessMutualTLSCertificate(accountID, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415")
+
+	assert.NoError(t, err)
+
+	mux.HandleFunc("/zones/"+zoneID+"/access/certificates/f174e90a-fafe-4643-bbbc-4a0ed4fc8415", handler)
+
+	err = client.DeleteZoneAccessMutualTLSCertificate(zoneID, "f174e90a-fafe-4643-bbbc-4a0ed4fc8415")
+
+	assert.NoError(t, err)
+}
diff --git a/workers_cron_triggers_test.go b/workers_cron_triggers_test.go
deleted file mode 100644
index 8f910b612..000000000
--- a/workers_cron_triggers_test.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestListWorkerCronTriggers(t *testing.T) {
-	setup(UsingAccount("9a7806061c88ada191ed06f989cc3dac"))
-	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, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": {
-				"schedules": [
-					{
-						"cron": "*/30 * * * *",
-						"created_on": "2017-01-01T00:00:00Z",
-						"modified_on": "2017-01-01T00:00:00Z"
-					}
-				]
-			}
-		}`)
-	}
-
-	mux.HandleFunc("/accounts/9a7806061c88ada191ed06f989cc3dac/workers/scripts/example-script/schedules", handler)
-	createdOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
-	modifiedOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
-	want := []WorkerCronTrigger{{
-		Cron:       "*/30 * * * *",
-		ModifiedOn: &modifiedOn,
-		CreatedOn:  &createdOn,
-	}}
-
-	actual, err := client.ListWorkerCronTriggers(context.Background(), "example-script")
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
-
-func TestUpdateWorkerCronTriggers(t *testing.T) {
-	setup(UsingAccount("9a7806061c88ada191ed06f989cc3dac"))
-	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, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": {
-				"schedules": [
-					{
-						"cron": "*/30 * * * *",
-						"created_on": "2017-01-01T00:00:00Z",
-						"modified_on": "2017-01-01T00:00:00Z"
-					}
-				]
-			}
-		}`)
-	}
-
-	mux.HandleFunc("/accounts/9a7806061c88ada191ed06f989cc3dac/workers/scripts/example-script/schedules", handler)
-	createdOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
-	modifiedOn, _ := time.Parse(time.RFC3339, "2017-01-01T00:00:00Z")
-	want := []WorkerCronTrigger{{
-		Cron:       "*/30 * * * *",
-		ModifiedOn: &modifiedOn,
-		CreatedOn:  &createdOn,
-	}}
-
-	actual, err := client.UpdateWorkerCronTriggers(context.Background(), "example-script", want)
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}