Repository: cloudflare/cloudflare-go
Pull request #1397 (2023-09-12 16:24:07)

Add support for Get/Post/Delete operations in API Shield Endpoint Management
This change adds support for the following API Shield related endpoints related to endpoint management:

- [Add operations to a zone](https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-add-operations-to-a-zone)
- [Delete an operation](https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-delete-an-operation)
- [Retrieve information about an operation](https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-retrieve-information-about-an-operation)


## Description

This change adds support for API Shield related endpoints related to managing operations in API Shield Endpoint Management. This is so they can be used from the library and will enable this feature to be added to terraform in the future. 

## Has your change been tested?

- Unit tests
- Local testing using API


## Types of changes

What sort of change does your code introduce/modify?

- [x] New feature (non-breaking change which adds functionality)


## 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.

Supersedes #1054

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) Report
> Merging [#1397](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) (bc05376) into [master](https://app.codecov.io/gh/cloudflare/cloudflare-go/commit/b9ac80404771c061ccb53da6a2e13c41728a1d82?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) (b9ac804) will **increase** coverage by `0.23%`.
> Report is 265 commits behind head on master.
> The diff coverage is `52.38%`.

```diff
@@            Coverage Diff             @@
##           master    #1397      +/-   ##
==========================================
+ Coverage   48.33%   48.57%   +0.23%     
==========================================
  Files         133      140       +7     
  Lines       13023    13898     +875     
==========================================
+ Hits         6295     6751     +456     
- Misses       5201     5482     +281     
- Partials     1527     1665     +138     
```


| [Files Changed](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) | Coverage Δ | |
|---|---|---|
| [access\_audit\_log.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX2F1ZGl0X2xvZy5nbw==) | `79.31% <ø> (ø)` | |
| [access\_bookmark.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX2Jvb2ttYXJrLmdv) | `72.44% <ø> (ø)` | |
| [access\_keys.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX2tleXMuZ28=) | `71.42% <ø> (ø)` | |
| [access\_organization.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX29yZ2FuaXphdGlvbi5nbw==) | `53.84% <ø> (ø)` | |
| [access\_service\_tokens.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX3NlcnZpY2VfdG9rZW5zLmdv) | `51.85% <ø> (ø)` | |
| [account\_members.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjb3VudF9tZW1iZXJzLmdv) | `65.54% <ø> (ø)` | |
| [account\_roles.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjb3VudF9yb2xlcy5nbw==) | `53.84% <ø> (ø)` | |
| [accounts.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjb3VudHMuZ28=) | `50.81% <ø> (ø)` | |
| [addressing\_address\_map.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWRkcmVzc2luZ19hZGRyZXNzX21hcC5nbw==) | `39.04% <ø> (ø)` | |
| [addressing\_ip\_prefix.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWRkcmVzc2luZ19pcF9wcmVmaXguZ28=) | `45.45% <ø> (ø)` | |
| ... and [122 more](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) | |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1397/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare)



Patch:
diff --git a/.changelog/1397.txt b/.changelog/1397.txt
new file mode 100644
index 000000000..a3954f447
--- /dev/null
+++ b/.changelog/1397.txt
@@ -0,0 +1,3 @@
+```release_note:enhancement
+api_shield: Add support for Get/Post/Delete operations in API Shield Endpoint Management
+```
\ No newline at end of file
diff --git a/api_shield_operations.go b/api_shield_operations.go
new file mode 100644
index 000000000..1d89a1754
--- /dev/null
+++ b/api_shield_operations.go
@@ -0,0 +1,185 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+// APIShieldOperation represents an operation stored in API Shield Endpoint Management.
+type APIShieldOperation struct {
+	APIShieldBasicOperation
+	ID          string         `json:"operation_id"`
+	LastUpdated *time.Time     `json:"last_updated"`
+	Features    map[string]any `json:"features,omitempty"`
+}
+
+// GetAPIShieldOperationParams represents the parameters to pass when retrieving an operation.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-retrieve-information-about-an-operation
+type GetAPIShieldOperationParams struct {
+	// The Operation ID to retrieve
+	OperationID string `url:"-"`
+	// Features represents a set of features to return in `features` object when
+	// performing making read requests against an Operation or listing operations.
+	Features []string `url:"feature,omitempty"`
+}
+
+// CreateAPIShieldOperationsParams represents the parameters to pass when adding one or more operations.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-add-operations-to-a-zone
+type CreateAPIShieldOperationsParams struct {
+	// Operations are a slice of operations to be created in API Shield Endpoint Management
+	Operations []APIShieldBasicOperation `url:"-"`
+}
+
+// APIShieldBasicOperation should be used when creating an operation in API Shield Endpoint Management.
+type APIShieldBasicOperation struct {
+	Method   string `json:"method"`
+	Host     string `json:"host"`
+	Endpoint string `json:"endpoint"`
+}
+
+// DeleteAPIShieldOperationParams represents the parameters to pass to delete an operation.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-delete-an-operation
+type DeleteAPIShieldOperationParams struct {
+	// OperationID is the operation to be deleted
+	OperationID string `url:"-"`
+}
+
+// ListAPIShieldOperationsParams represents the parameters to pass when retrieving operations
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-retrieve-information-about-all-operations-on-a-zone
+type ListAPIShieldOperationsParams struct {
+	// Features represents a set of features to return in `features` object when
+	// performing making read requests against an Operation or listing operations.
+	Features []string `url:"feature,omitempty"`
+	// Direction to order results.
+	Direction string `url:"direction,omitempty"`
+	// OrderBy when requesting a feature, the feature keys are available for ordering as well, e.g., thresholds.suggested_threshold.
+	OrderBy string `url:"order,omitempty"`
+	// Filters to only return operations that match filtering criteria, see APIShieldGetOperationsFilters
+	APIShieldListOperationsFilters
+	// Pagination options to apply to the request.
+	PaginationOptions
+}
+
+// APIShieldListOperationsFilters represents the filtering query parameters to set when retrieving operations
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-retrieve-information-about-all-operations-on-a-zone
+type APIShieldListOperationsFilters struct {
+	// Hosts filters results to only include the specified hosts.
+	Hosts []string `url:"host,omitempty"`
+	// Methods filters results to only include the specified methods.
+	Methods []string `url:"method,omitempty"`
+	// Endpoint filter results to only include endpoints containing this pattern.
+	Endpoint string `url:"endpoint,omitempty"`
+}
+
+// APIShieldGetOperationResponse represents the response from the api_gateway/operations/{id} endpoint.
+type APIShieldGetOperationResponse struct {
+	Result APIShieldOperation `json:"result"`
+	Response
+}
+
+// APIShieldGetOperationsResponse represents the response from the api_gateway/operations endpoint.
+type APIShieldGetOperationsResponse struct {
+	Result     []APIShieldOperation `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+// APIShieldDeleteOperationResponse represents the response from the api_gateway/operations/{id} endpoint (DELETE).
+type APIShieldDeleteOperationResponse struct {
+	Result interface{} `json:"result"`
+	Response
+}
+
+// GetAPIShieldOperation returns information about an operation
+//
+// API documentation https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-retrieve-information-about-an-operation
+func (api *API) GetAPIShieldOperation(ctx context.Context, rc *ResourceContainer, params GetAPIShieldOperationParams) (*APIShieldOperation, error) {
+	path := fmt.Sprintf("/zones/%s/api_gateway/operations/%s", rc.Identifier, params.OperationID)
+
+	uri := buildURI(path, params)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var asResponse APIShieldGetOperationResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return &asResponse.Result, nil
+}
+
+// ListAPIShieldOperations retrieve information about all operations on a zone
+//
+// API documentation https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-retrieve-information-about-all-operations-on-a-zone
+func (api *API) ListAPIShieldOperations(ctx context.Context, rc *ResourceContainer, params ListAPIShieldOperationsParams) ([]APIShieldOperation, ResultInfo, error) {
+	path := fmt.Sprintf("/zones/%s/api_gateway/operations", rc.Identifier)
+
+	uri := buildURI(path, params)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, ResultInfo{}, err
+	}
+
+	var asResponse APIShieldGetOperationsResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return nil, ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return asResponse.Result, asResponse.ResultInfo, nil
+}
+
+// CreateAPIShieldOperations add one or more operations to a zone.
+//
+// API documentation https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-add-operations-to-a-zone
+func (api *API) CreateAPIShieldOperations(ctx context.Context, rc *ResourceContainer, params CreateAPIShieldOperationsParams) ([]APIShieldOperation, error) {
+	uri := fmt.Sprintf("/zones/%s/api_gateway/operations", rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params.Operations)
+	if err != nil {
+		return nil, err
+	}
+
+	// Result should be all the operations added to the zone, similar to doing GetAPIShieldOperations
+	var asResponse APIShieldGetOperationsResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return asResponse.Result, nil
+}
+
+// DeleteAPIShieldOperation deletes a single operation
+//
+// API documentation https://developers.cloudflare.com/api/operations/api-shield-endpoint-management-delete-an-operation
+func (api *API) DeleteAPIShieldOperation(ctx context.Context, rc *ResourceContainer, params DeleteAPIShieldOperationParams) error {
+	uri := fmt.Sprintf("/zones/%s/api_gateway/operations/%s", rc.Identifier, params.OperationID)
+
+	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return err
+	}
+
+	var asResponse APIShieldDeleteOperationResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return nil
+}


Test patch:
diff --git a/api_shield_operations_test.go b/api_shield_operations_test.go
new file mode 100644
index 000000000..fdcc25cc8
--- /dev/null
+++ b/api_shield_operations_test.go
@@ -0,0 +1,491 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const testAPIShieldOperationId = "9def2cb0-3ed0-4737-92ca-f09efa4718fd"
+
+func TestGetAPIShieldOperation(t *testing.T) {
+	setup()
+	t.Cleanup(teardown)
+
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/operations/%s", testZoneID, testAPIShieldOperationId)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": {
+			"operation_id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+			"method": "POST",
+			"host": "api.cloudflare.com",
+			"endpoint": "/client/v4/zones",
+			"last_updated": "2023-03-02T15:46:06.000000Z"
+		}
+	}`
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		require.Empty(t, r.URL.Query())
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+
+	mux.HandleFunc(endpoint, handler)
+
+	actual, err := client.GetAPIShieldOperation(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		GetAPIShieldOperationParams{
+			OperationID: testAPIShieldOperationId,
+		},
+	)
+
+	time := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+	expected := &APIShieldOperation{
+		APIShieldBasicOperation: APIShieldBasicOperation{
+			Method:   "POST",
+			Host:     "api.cloudflare.com",
+			Endpoint: "/client/v4/zones",
+		},
+		ID:          testAPIShieldOperationId,
+		LastUpdated: &time,
+		Features:    nil,
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+}
+
+func TestGetAPIShieldOperationWithParams(t *testing.T) {
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/operations/%s", testZoneID, testAPIShieldOperationId)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": {
+			"operation_id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+			"method": "POST",
+			"host": "api.cloudflare.com",
+			"endpoint": "/client/v4/zones",
+			"last_updated": "2023-03-02T15:46:06.000000Z",
+			"features":{
+				"thresholds":{},
+				"parameter_schemas":{}
+			}
+		}
+	}`
+
+	tests := []struct {
+		name           string
+		getParams      GetAPIShieldOperationParams
+		expectedParams url.Values
+	}{
+		{
+			name: "one feature",
+			getParams: GetAPIShieldOperationParams{
+				OperationID: testAPIShieldOperationId,
+				Features:    []string{"thresholds"},
+			},
+			expectedParams: url.Values{
+				"feature": []string{"thresholds"},
+			},
+		},
+		{
+			name: "more than one feature",
+			getParams: GetAPIShieldOperationParams{
+				OperationID: testAPIShieldOperationId,
+				Features:    []string{"thresholds", "parameter_schemas"},
+			},
+			expectedParams: url.Values{
+				"feature": []string{"thresholds", "parameter_schemas"},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			setup()
+			t.Cleanup(teardown)
+			handler := func(w http.ResponseWriter, r *http.Request) {
+				require.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+				require.Equal(t, test.expectedParams, r.URL.Query())
+				w.Header().Set("content-type", "application/json")
+				fmt.Fprint(w, response)
+			}
+
+			mux.HandleFunc(endpoint, handler)
+
+			actual, err := client.GetAPIShieldOperation(
+				context.Background(),
+				ZoneIdentifier(testZoneID),
+				test.getParams,
+			)
+
+			time := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+			expected := &APIShieldOperation{
+				APIShieldBasicOperation: APIShieldBasicOperation{
+					Method:   "POST",
+					Host:     "api.cloudflare.com",
+					Endpoint: "/client/v4/zones",
+				},
+				ID:          "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+				LastUpdated: &time,
+				Features: map[string]any{
+					"thresholds":        map[string]any{},
+					"parameter_schemas": map[string]any{},
+				},
+			}
+
+			if assert.NoError(t, err) {
+				assert.Equal(t, expected, actual)
+			}
+		})
+	}
+}
+
+func TestListAPIShieldOperations(t *testing.T) {
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/operations", testZoneID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": [
+			{
+				"operation_id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+				"method": "POST",
+				"host": "api.cloudflare.com",
+				"endpoint": "/client/v4/zones",
+				"last_updated": "2023-03-02T15:46:06.000000Z"
+			}
+
+		],
+		"result_info": {
+			"page": 3,
+			"per_page": 20,
+			"count": 1,
+			"total_count": 2000
+		}
+	}`
+
+	setup()
+	t.Cleanup(teardown)
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		require.Empty(t, r.URL.Query())
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+
+	mux.HandleFunc(endpoint, handler)
+
+	actual, actualResultInfo, err := client.ListAPIShieldOperations(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		ListAPIShieldOperationsParams{},
+	)
+
+	time := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+	expectedOps := []APIShieldOperation{
+		{
+			APIShieldBasicOperation: APIShieldBasicOperation{
+				Method:   "POST",
+				Host:     "api.cloudflare.com",
+				Endpoint: "/client/v4/zones",
+			},
+			ID:          "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+			LastUpdated: &time,
+			Features:    nil,
+		},
+	}
+
+	expectedResultInfo := ResultInfo{
+		Page:    3,
+		PerPage: 20,
+		Count:   1,
+		Total:   2000,
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedOps, actual)
+		assert.Equal(t, expectedResultInfo, actualResultInfo)
+	}
+}
+
+func TestListAPIShieldOperationsWithParams(t *testing.T) {
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/operations", testZoneID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": [
+			{
+				"operation_id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+				"method": "POST",
+				"host": "api.cloudflare.com",
+				"endpoint": "/client/v4/zones",
+				"last_updated": "2023-03-02T15:46:06.000000Z",
+				"features": {
+					"thresholds": {}
+                }
+			}
+		],
+		"result_info": {
+			"page": 3,
+			"per_page": 20,
+			"count": 1,
+			"total_count": 2000
+		}
+	}`
+
+	tests := []struct {
+		name           string
+		params         ListAPIShieldOperationsParams
+		expectedParams url.Values
+	}{
+		{
+			name: "all params",
+			params: ListAPIShieldOperationsParams{
+				Features:  []string{"thresholds", "parameter_schemas"},
+				Direction: "desc",
+				OrderBy:   "host",
+				APIShieldListOperationsFilters: APIShieldListOperationsFilters{
+					Hosts:    []string{"api.cloudflare.com", "developers.cloudflare.com"},
+					Methods:  []string{"GET", "PUT"},
+					Endpoint: "/client",
+				},
+				PaginationOptions: PaginationOptions{
+					Page:    1,
+					PerPage: 25,
+				},
+			},
+			expectedParams: url.Values{
+				"feature":   []string{"thresholds", "parameter_schemas"},
+				"direction": []string{"desc"},
+				"order":     []string{"host"},
+				"host":      []string{"api.cloudflare.com", "developers.cloudflare.com"},
+				"method":    []string{"GET", "PUT"},
+				"endpoint":  []string{"/client"},
+				"page":      []string{"1"},
+				"per_page":  []string{"25"},
+			},
+		},
+		{
+			name: "features only",
+			params: ListAPIShieldOperationsParams{
+				Features: []string{"thresholds", "parameter_schemas"},
+			},
+			expectedParams: url.Values{
+				"feature": []string{"thresholds", "parameter_schemas"},
+			},
+		},
+		{
+			name: "direction only",
+			params: ListAPIShieldOperationsParams{
+				Direction: "desc",
+			},
+			expectedParams: url.Values{
+				"direction": []string{"desc"},
+			},
+		},
+		{
+			name: "order only",
+			params: ListAPIShieldOperationsParams{
+				OrderBy: "host",
+			},
+			expectedParams: url.Values{
+				"order": []string{"host"},
+			},
+		},
+		{
+			name: "hosts only",
+			params: ListAPIShieldOperationsParams{
+				APIShieldListOperationsFilters: APIShieldListOperationsFilters{
+					Hosts: []string{"api.cloudflare.com", "developers.cloudflare.com"},
+				},
+			},
+			expectedParams: url.Values{
+				"host": []string{"api.cloudflare.com", "developers.cloudflare.com"},
+			},
+		},
+		{
+			name: "methods only",
+			params: ListAPIShieldOperationsParams{
+				APIShieldListOperationsFilters: APIShieldListOperationsFilters{
+					Methods: []string{"GET", "PUT"},
+				},
+			},
+			expectedParams: url.Values{
+				"method": []string{"GET", "PUT"},
+			},
+		},
+		{
+			name: "endpoint only",
+			params: ListAPIShieldOperationsParams{
+				APIShieldListOperationsFilters: APIShieldListOperationsFilters{
+					Endpoint: "/client",
+				},
+			},
+			expectedParams: url.Values{
+				"endpoint": []string{"/client"},
+			},
+		},
+		{
+			name: "pagination only",
+			params: ListAPIShieldOperationsParams{
+				PaginationOptions: PaginationOptions{
+					Page:    1,
+					PerPage: 25,
+				},
+			},
+			expectedParams: url.Values{
+				"page":     []string{"1"},
+				"per_page": []string{"25"},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			setup()
+			t.Cleanup(teardown)
+			handler := func(w http.ResponseWriter, r *http.Request) {
+				require.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+				require.Equal(t, test.expectedParams, r.URL.Query())
+				w.Header().Set("content-type", "application/json")
+				fmt.Fprint(w, response)
+			}
+
+			mux.HandleFunc(endpoint, handler)
+
+			actual, _, err := client.ListAPIShieldOperations(
+				context.Background(),
+				ZoneIdentifier(testZoneID),
+				test.params,
+			)
+
+			time := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+			expected := []APIShieldOperation{
+				{
+					APIShieldBasicOperation: APIShieldBasicOperation{
+						Method:   "POST",
+						Host:     "api.cloudflare.com",
+						Endpoint: "/client/v4/zones",
+					},
+					ID:          "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+					LastUpdated: &time,
+					Features: map[string]any{
+						"thresholds": map[string]any{},
+					},
+				},
+			}
+
+			if assert.NoError(t, err) {
+				assert.Equal(t, expected, actual)
+			}
+		})
+	}
+}
+
+func TestCreateAPIShieldOperations(t *testing.T) {
+	setup()
+	t.Cleanup(teardown)
+
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/operations", testZoneID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": [
+			{
+				"operation_id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+				"method": "POST",
+				"host": "api.cloudflare.com",
+				"endpoint": "/client/v4/zones",
+				"last_updated": "2023-03-02T15:46:06.000000Z"
+			}
+		]
+	}`
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+
+		body, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		require.Equal(t, []byte(`[{"method":"POST","host":"api.cloudflare.com","endpoint":"/client/v4/zones"}]`), body)
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+
+	mux.HandleFunc(endpoint, handler)
+
+	actual, err := client.CreateAPIShieldOperations(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		CreateAPIShieldOperationsParams{
+			Operations: []APIShieldBasicOperation{
+				{
+					Method:   "POST",
+					Host:     "api.cloudflare.com",
+					Endpoint: "/client/v4/zones",
+				},
+			},
+		},
+	)
+
+	time := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+	expected := []APIShieldOperation{
+		{
+			APIShieldBasicOperation: APIShieldBasicOperation{
+				Method:   "POST",
+				Host:     "api.cloudflare.com",
+				Endpoint: "/client/v4/zones",
+			},
+			ID:          "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+			LastUpdated: &time,
+			Features:    nil,
+		},
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+}
+
+func TestDeleteAPIShieldOperation(t *testing.T) {
+	setup()
+	t.Cleanup(teardown)
+
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/operations/%s", testZoneID, testAPIShieldOperationId)
+	response := `{"result":{},"success":true,"errors":[],"messages":[]}`
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		require.Empty(t, r.URL.Query())
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+
+	mux.HandleFunc(endpoint, handler)
+
+	err := client.DeleteAPIShieldOperation(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		DeleteAPIShieldOperationParams{
+			OperationID: testAPIShieldOperationId,
+		},
+	)
+
+	assert.NoError(t, err)
+}