Repository: cloudflare/cloudflare-go
Pull request #1267 (2023-04-14 21:09:54)

Add support for Turnstile
## Description
Adds support for turnstile widgets

## Has your change been tested?

New tests added

## 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.
- [ ] 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.

[1]: https://help.github.com/articles/closing-issues-using-keywords/



Patch:
diff --git a/.changelog/1267.txt b/.changelog/1267.txt
new file mode 100644
index 000000000..33e3526ec
--- /dev/null
+++ b/.changelog/1267.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+turnstile: add support for turnstile
+```
diff --git a/cloudflare.go b/cloudflare.go
index e8d09ff69..8e5afe369 100644
--- a/cloudflare.go
+++ b/cloudflare.go
@@ -596,3 +596,10 @@ func checkResultInfo(perPage, page, count int, info *ResultInfo) bool {
 		panic("checkResultInfo: impossible")
 	}
 }
+
+type OrderDirection string
+
+const (
+	OrderDirectionAsc  OrderDirection = "asc"
+	OrderDirectionDesc OrderDirection = "desc"
+)
diff --git a/turnstile.go b/turnstile.go
new file mode 100644
index 000000000..565a36e6e
--- /dev/null
+++ b/turnstile.go
@@ -0,0 +1,245 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+var ErrMissingSiteKey = errors.New("required site key missing")
+
+type TurnstileWidget struct {
+	SiteKey      string     `json:"sitekey,omitempty"`
+	Secret       string     `json:"secret,omitempty"`
+	CreatedOn    *time.Time `json:"created_on,omitempty"`
+	ModifiedOn   *time.Time `json:"modified_on,omitempty"`
+	Name         string     `json:"name,omitempty"`
+	Domains      []string   `json:"domains,omitempty"`
+	Mode         string     `json:"mode,omitempty"`
+	BotFightMode bool       `json:"bot_fight_mode,omitempty"`
+	Region       string     `json:"region,omitempty"`
+	OffLabel     bool       `json:"offlabel,omitempty"`
+}
+
+type CreateTurnstileWidgetParams struct {
+	Name         string   `json:"name,omitempty"`
+	Domains      []string `json:"domains,omitempty"`
+	Mode         string   `json:"mode,omitempty"`
+	BotFightMode bool     `json:"bot_fight_mode,omitempty"`
+	Region       string   `json:"region,omitempty"`
+	OffLabel     bool     `json:"offlabel,omitempty"`
+}
+
+type UpdateTurnstileWidgetParams struct {
+	SiteKey      string   `json:"sitekey,omitempty"`
+	Secret       string   `json:"secret,omitempty"`
+	Name         string   `json:"name,omitempty"`
+	Domains      []string `json:"domains,omitempty"`
+	Mode         string   `json:"mode,omitempty"`
+	BotFightMode bool     `json:"bot_fight_mode,omitempty"`
+	Region       string   `json:"region,omitempty"`
+	OffLabel     bool     `json:"offlabel,omitempty"`
+}
+
+type TurnstileWidgetResponse struct {
+	Response
+	Result TurnstileWidget `json:"result"`
+}
+
+type ListTurnstileWidgetParams struct {
+	ResultInfo
+	Direction string         `url:"direction,omitempty"`
+	Order     OrderDirection `url:"order,omitempty"`
+}
+
+type ListTurnstileWidgetResponse struct {
+	Response
+	ResultInfo `json:"result_info"`
+	Result     []TurnstileWidget `json:"result"`
+}
+
+type RotateTurnstileWidgetParams struct {
+	SiteKey               string
+	InvalidateImmediately bool `json:"invalidate_immediately,omitempty"`
+}
+
+// CreateTurnstileWidget creates a new challenge widgets.
+//
+// API reference: https://api.cloudflare.com/#challenge-widgets-properties
+func (api *API) CreateTurnstileWidget(ctx context.Context, rc *ResourceContainer, params CreateTurnstileWidgetParams) (TurnstileWidget, error) {
+	if rc.Identifier == "" {
+		return TurnstileWidget{}, ErrMissingAccountID
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/challenges/widgets", rc.Identifier)
+	res, err := api.makeRequestContext(ctx, "POST", uri, params)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r TurnstileWidgetResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return r.Result, nil
+}
+
+// ListTurnstileWidgets lists challenge widgets.
+//
+// API reference: https://api.cloudflare.com/#challenge-widgets-list-challenge-widgets
+func (api *API) ListTurnstileWidgets(ctx context.Context, rc *ResourceContainer, params ListTurnstileWidgetParams) ([]TurnstileWidget, *ResultInfo, error) {
+	if rc.Identifier == "" {
+		return []TurnstileWidget{}, &ResultInfo{}, ErrMissingAccountID
+	}
+	autoPaginate := true
+	if params.PerPage >= 1 || params.Page >= 1 {
+		autoPaginate = false
+	}
+
+	if params.PerPage < 1 {
+		params.PerPage = 25
+	}
+
+	if params.Page < 1 {
+		params.Page = 1
+	}
+
+	var widgets []TurnstileWidget
+	var r ListTurnstileWidgetResponse
+	for {
+		uri := buildURI(fmt.Sprintf("/accounts/%s/challenges/widgets", rc.Identifier), params)
+		res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+
+		if err != nil {
+			return []TurnstileWidget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+		}
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []TurnstileWidget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+		}
+
+		widgets = append(widgets, r.Result...)
+		params.ResultInfo = r.ResultInfo.Next()
+		if params.ResultInfo.Done() || !autoPaginate {
+			break
+		}
+	}
+
+	return widgets, &r.ResultInfo, nil
+}
+
+// GetTurnstileWidget shows a single challenge widget configuration.
+//
+// API reference: https://api.cloudflare.com/#challenge-widgets-challenge-widget-details
+func (api *API) GetTurnstileWidget(ctx context.Context, rc *ResourceContainer, siteKey string) (TurnstileWidget, error) {
+	if rc.Identifier == "" {
+		return TurnstileWidget{}, ErrMissingAccountID
+	}
+
+	if siteKey == "" {
+		return TurnstileWidget{}, ErrMissingSiteKey
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s", rc.Identifier, siteKey)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r TurnstileWidgetResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return r.Result, nil
+}
+
+// UpdateTurnstileWidget update the configuration of a widget.
+//
+// API reference: https://api.cloudflare.com/#challenge-widgets-update-a-challenge-widget
+func (api *API) UpdateTurnstileWidget(ctx context.Context, rc *ResourceContainer, params UpdateTurnstileWidgetParams) (TurnstileWidget, error) {
+	if rc.Identifier == "" {
+		return TurnstileWidget{}, ErrMissingAccountID
+	}
+
+	if params.SiteKey == "" {
+		return TurnstileWidget{}, ErrMissingSiteKey
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s", rc.Identifier, params.SiteKey)
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r TurnstileWidgetResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// RotateTurnstileWidget generates a new secret key for this widget. If
+// invalidate_immediately is set to false, the previous secret remains valid for
+// 2 hours.
+//
+// Note that secrets cannot be rotated again during the grace period.
+//
+// API reference: https://api.cloudflare.com/#challenge-widgets-rotate-secret-for-a-challenge-widget
+func (api *API) RotateTurnstileWidget(ctx context.Context, rc *ResourceContainer, param RotateTurnstileWidgetParams) (TurnstileWidget, error) {
+	if rc.Identifier == "" {
+		return TurnstileWidget{}, ErrMissingAccountID
+	}
+	if param.SiteKey == "" {
+		return TurnstileWidget{}, ErrMissingSiteKey
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s/rotate_secret", rc.Identifier, param.SiteKey)
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, param)
+
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r TurnstileWidgetResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return TurnstileWidget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return r.Result, nil
+}
+
+// DeleteTurnstileWidget delete a challenge widget.
+//
+// API reference: https://api.cloudflare.com/#challenge-widgets-delete-a-challenge-widget
+func (api *API) DeleteTurnstileWidget(ctx context.Context, rc *ResourceContainer, siteKey string) error {
+	if rc.Identifier == "" {
+		return ErrMissingAccountID
+	}
+
+	if siteKey == "" {
+		return ErrMissingSiteKey
+	}
+	uri := fmt.Sprintf("/accounts/%s/challenges/widgets/%s", rc.Identifier, siteKey)
+
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var r TurnstileWidgetResponse
+	err = json.Unmarshal(res, &r)
+	if err != nil {
+		return fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return nil
+}


Test patch:
diff --git a/turnstile_test.go b/turnstile_test.go
new file mode 100644
index 000000000..f2193431f
--- /dev/null
+++ b/turnstile_test.go
@@ -0,0 +1,334 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const testTurnstileWidgetSiteKey = "0x4AAF00AAAABn0R22HWm-YUc"
+
+var (
+	turnstileWidgetCreatedOn, _  = time.Parse(time.RFC3339, "2014-01-01T05:20:00.123123Z")
+	turnstileWidgetModifiedOn, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.123123Z")
+	expectedTurnstileWidget      = TurnstileWidget{
+		SiteKey:    "0x4AAF00AAAABn0R22HWm-YUc",
+		Secret:     "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+		CreatedOn:  &turnstileWidgetCreatedOn,
+		ModifiedOn: &turnstileWidgetModifiedOn,
+		Name:       "blog.cloudflare.com login form",
+		Domains: []string{
+			"203.0.113.1",
+			"cloudflare.com",
+			"blog.example.com",
+		},
+		Mode:         "invisible",
+		BotFightMode: true,
+		Region:       "world",
+		OffLabel:     false,
+	}
+)
+
+func TestTurnstileWidget_Create(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets", 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": {
+				"sitekey": "0x4AAF00AAAABn0R22HWm-YUc",
+				"secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+				"created_on": "2014-01-01T05:20:00.123123Z",
+				"modified_on": "2014-01-01T05:20:00.123123Z",
+				"name": "blog.cloudflare.com login form",
+				"domains": [
+				  "203.0.113.1",
+				  "cloudflare.com",
+				  "blog.example.com"
+				],
+				"mode": "invisible",
+				"bot_fight_mode": true,
+				"region": "world",
+				"offlabel": false
+			  }
+			}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.CreateTurnstileWidget(context.Background(), AccountIdentifier(""), CreateTurnstileWidgetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	out, err := client.CreateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), CreateTurnstileWidgetParams{
+		Name:         "blog.cloudflare.com login form",
+		Mode:         "invisible",
+		BotFightMode: true,
+		Domains: []string{
+			"203.0.113.1",
+			"cloudflare.com",
+			"blog.example.com",
+		},
+		Region:   "world",
+		OffLabel: false,
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedTurnstileWidget, out, "create challenge_widgets structs not equal")
+	}
+}
+
+func TestTurnstileWidget_List(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		assert.Equal(t, "asc", r.URL.Query().Get("order"))
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "sitekey": "0x4AAF00AAAABn0R22HWm-YUc",
+      "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+      "created_on": "2014-01-01T05:20:00.123123Z",
+      "modified_on": "2014-01-01T05:20:00.123123Z",
+      "name": "blog.cloudflare.com login form",
+      "domains": [
+        "203.0.113.1",
+        "cloudflare.com",
+        "blog.example.com"
+      ],
+      "mode": "invisible",
+      "bot_fight_mode": true,
+	  "region": "world",
+	  "offlabel": false
+    }
+  ],
+  "result_info": {
+    "page": 1,
+    "per_page": 20,
+    "count": 1,
+    "total_count": 2000
+  }
+}`)
+	})
+
+	_, _, err := client.ListTurnstileWidgets(context.Background(), AccountIdentifier(""), ListTurnstileWidgetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	out, results, err := client.ListTurnstileWidgets(context.Background(), AccountIdentifier(testAccountID), ListTurnstileWidgetParams{
+		Order: OrderDirectionAsc,
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, 1, len(out), "expected 1 challenge_widgets")
+		assert.Equal(t, 20, results.PerPage, "expected 20 per page")
+		assert.Equal(t, expectedTurnstileWidget, out[0], "list challenge_widgets structs not equal")
+	}
+}
+
+func TestTurnstileWidget_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey, 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": {
+    "sitekey": "0x4AAF00AAAABn0R22HWm-YUc",
+    "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+    "created_on": "2014-01-01T05:20:00.123123Z",
+    "modified_on": "2014-01-01T05:20:00.123123Z",
+    "name": "blog.cloudflare.com login form",
+    "domains": [
+      "203.0.113.1",
+      "cloudflare.com",
+      "blog.example.com"
+    ],
+    "mode": "invisible",
+	"bot_fight_mode": true,
+	"region": "world",
+	"offlabel": false
+  }
+}`)
+	})
+
+	_, err := client.GetTurnstileWidget(context.Background(), AccountIdentifier(""), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	_, err = client.GetTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingSiteKey, err)
+	}
+
+	out, err := client.GetTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), testTurnstileWidgetSiteKey)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedTurnstileWidget, out, "get challenge_widgets structs not equal")
+	}
+}
+
+func TestTurnstileWidgets_Update(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey, 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, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+    "sitekey": "0x4AAF00AAAABn0R22HWm-YUc",
+    "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+    "created_on": "2014-01-01T05:20:00.123123Z",
+    "modified_on": "2014-01-01T05:20:00.123123Z",
+    "name": "blog.cloudflare.com login form",
+    "domains": [
+      "203.0.113.1",
+      "cloudflare.com",
+      "blog.example.com"
+    ],
+    "mode": "invisible",
+	"bot_fight_mode": true,
+	"region": "world",
+	"offlabel": false
+  }
+}`)
+	})
+
+	_, err := client.UpdateTurnstileWidget(context.Background(), AccountIdentifier(""), UpdateTurnstileWidgetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	_, err = client.UpdateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), UpdateTurnstileWidgetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingSiteKey, err)
+	}
+
+	out, err := client.UpdateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), UpdateTurnstileWidgetParams{
+		SiteKey: testTurnstileWidgetSiteKey,
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedTurnstileWidget, out, "update challenge_widgets structs not equal")
+	}
+}
+
+func TestTurnstileWidgets_RotateSecret(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey+"/rotate_secret", 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": {
+    "sitekey": "0x4AAF00AAAABn0R22HWm-YUc",
+    "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+    "created_on": "2014-01-01T05:20:00.123123Z",
+    "modified_on": "2014-01-01T05:20:00.123123Z",
+    "name": "blog.cloudflare.com login form",
+    "domains": [
+      "203.0.113.1",
+      "cloudflare.com",
+      "blog.example.com"
+    ],
+    "mode": "invisible",
+	"bot_fight_mode": true,
+	"region": "world",
+	"offlabel": false
+  }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.RotateTurnstileWidget(context.Background(), AccountIdentifier(""), RotateTurnstileWidgetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	_, err = client.RotateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), RotateTurnstileWidgetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingSiteKey, err)
+	}
+
+	out, err := client.RotateTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), RotateTurnstileWidgetParams{SiteKey: testTurnstileWidgetSiteKey})
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedTurnstileWidget, out, "rotate challenge_widgets structs not equal")
+	}
+}
+
+func TestTurnstileWidgets_Delete(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/challenges/widgets/"+testTurnstileWidgetSiteKey, 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": {
+    "sitekey": "0x4AAF00AAAABn0R22HWm-YUc",
+    "secret": "0x4AAF00AAAABn0R22HWm098HVBjhdsYUc",
+    "created_on": "2014-01-01T05:20:00.123123Z",
+    "modified_on": "2014-01-01T05:20:00.123123Z",
+    "name": "blog.cloudflare.com login form",
+    "domains": [
+      "203.0.113.1",
+      "cloudflare.com",
+      "blog.example.com"
+    ],
+    "mode": "invisible",
+	"bot_fight_mode": true,
+	"region": "world",
+	"offlabel": false
+  }
+}`)
+	})
+
+	// Make sure missing account ID is thrown
+	err := client.DeleteTurnstileWidget(context.Background(), AccountIdentifier(""), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	err = client.DeleteTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingSiteKey, err)
+	}
+
+	err = client.DeleteTurnstileWidget(context.Background(), AccountIdentifier(testAccountID), testTurnstileWidgetSiteKey)
+	assert.NoError(t, err)
+}