Repository: cloudflare/cloudflare-go
Pull request #1413 (2023-09-29 17:14:22)

Add support for Get/Patch API Shield API Discovery Operations
This change adds support for the following API Shield related endpoints related to API Discovery:

- Retrieve discovered operations on a zone (https://developers.cloudflare.com/api/operations/api-shield-api-discovery-retrieve-discovered-operations-on-a-zone)
- Patch discovered operations (https://developers.cloudflare.com/api/operations/api-shield-api-patch-discovered-operations)
- Patch discovered operation (https://developers.cloudflare.com/api/operations/api-shield-api-patch-discovered-operation)


## Description

This change adds support for API Shield endpoints related to API Shield API Discovery. 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


## 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. (see [API Shield API Discovery](https://developers.cloudflare.com/api/operations/api-shield-api-discovery-retrieve-discovered-operations-on-a-zone))

[1]: https://help.github.com/articles/closing-issues-using-keywords/


djhworld: Draft PR as API docs require updating
github-actions[bot]: changelog detected :white_check_mark:

Patch:
diff --git a/.changelog/1406.txt b/.changelog/1406.txt
deleted file mode 100644
index 248b681c5..000000000
--- a/.changelog/1406.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-api_shield_schema: Add support for managing schemas for API Shield Schema Validation 2.0
-```
diff --git a/.changelog/1413.txt b/.changelog/1413.txt
new file mode 100644
index 000000000..9030b0c29
--- /dev/null
+++ b/.changelog/1413.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+api_shield_discovery: Add support for Get/Patch API Shield API Discovery Operations
+```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01eeaba80..f8df004c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,5 @@
 ## 0.79.0 (Unreleased)
 
-ENHANCEMENTS:
-
-* api_shield_schema: Add support for managing schemas for API Shield Schema Validation 2.0 ([#1406](https://github.com/cloudflare/cloudflare-go/issues/1406))
-
 BUG FIXES:
 
 * custom_nameservers: change `NSSet` from string to int to match API response ([#1410](https://github.com/cloudflare/cloudflare-go/issues/1410))
diff --git a/api_shield_api_discovery.go b/api_shield_api_discovery.go
new file mode 100644
index 000000000..69b3ad243
--- /dev/null
+++ b/api_shield_api_discovery.go
@@ -0,0 +1,190 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+// APIShieldDiscoveryOrigin is an enumeration on what discovery engine an operation was discovered by.
+type APIShieldDiscoveryOrigin string
+
+const (
+
+	// APIShieldDiscoveryOriginML discovered operations that were sourced using ML API Discovery.
+	APIShieldDiscoveryOriginML APIShieldDiscoveryOrigin = "ML"
+	// APIShieldDiscoveryOriginSessionIdentifier discovered operations that were sourced using Session Identifier
+	// API Discovery.
+	APIShieldDiscoveryOriginSessionIdentifier APIShieldDiscoveryOrigin = "SessionIdentifier"
+)
+
+// APIShieldDiscoveryState is an enumeration on states a discovery operation can be in.
+type APIShieldDiscoveryState string
+
+const (
+	// APIShieldDiscoveryStateReview discovered operations that are not saved into API Shield Endpoint Management.
+	APIShieldDiscoveryStateReview APIShieldDiscoveryState = "review"
+	// APIShieldDiscoveryStateSaved discovered operations that are already saved into API Shield Endpoint Management.
+	APIShieldDiscoveryStateSaved APIShieldDiscoveryState = "saved"
+	// APIShieldDiscoveryStateIgnored discovered operations that have been marked as ignored.
+	APIShieldDiscoveryStateIgnored APIShieldDiscoveryState = "ignored"
+)
+
+// APIShieldDiscoveryOperation is an operation that was discovered by API Discovery.
+type APIShieldDiscoveryOperation struct {
+	// ID represents the ID of the operation, formatted as UUID
+	ID string `json:"id"`
+	// Origin represents the API discovery engine(s) that discovered this operation
+	Origin []APIShieldDiscoveryOrigin `json:"origin"`
+	// State represents the state of operation in API Discovery
+	State APIShieldDiscoveryState `json:"state"`
+	// LastUpdated timestamp of when this operation was last updated
+	LastUpdated *time.Time `json:"last_updated"`
+	// Features are additional data about the operation
+	Features map[string]any `json:"features,omitempty"`
+
+	Method   string `json:"method"`
+	Host     string `json:"host"`
+	Endpoint string `json:"endpoint"`
+}
+
+// ListAPIShieldDiscoveryOperationsParams represents the parameters to pass when retrieving discovered operations.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-api-discovery-retrieve-discovered-operations-on-a-zone
+type ListAPIShieldDiscoveryOperationsParams struct {
+	// 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"`
+	// 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 filters results to only include endpoints containing this pattern.
+	Endpoint string `url:"endpoint,omitempty"`
+	// Diff when true, only return API Discovery results that are not saved into API Shield Endpoint Management
+	Diff bool `url:"diff,omitempty"`
+	// Origin filters results to only include discovery results sourced from a particular discovery engine
+	// See APIShieldDiscoveryOrigin for valid values.
+	Origin APIShieldDiscoveryOrigin `url:"origin,omitempty"`
+	// State filters results to only include discovery results in a particular state
+	// See APIShieldDiscoveryState for valid values.
+	State APIShieldDiscoveryState `url:"state,omitempty"`
+
+	// Pagination options to apply to the request.
+	PaginationOptions
+}
+
+// UpdateAPIShieldDiscoveryOperationParams represents the parameters to pass to patch a discovery operation
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-api-patch-discovered-operation
+type UpdateAPIShieldDiscoveryOperationParams struct {
+	// OperationID is the ID, formatted as UUID, of the operation to be updated
+	OperationID string                  `json:"-" url:"-"`
+	State       APIShieldDiscoveryState `json:"state" url:"-"`
+}
+
+// UpdateAPIShieldDiscoveryOperationsParams maps discovery operation IDs to PatchAPIShieldDiscoveryOperation structs
+//
+// Example:
+//
+//	UpdateAPIShieldDiscoveryOperations{
+//			"99522293-a505-45e5-bbad-bbc339f5dc40": PatchAPIShieldDiscoveryOperation{ State: "review" },
+//	}
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-api-patch-discovered-operations
+type UpdateAPIShieldDiscoveryOperationsParams map[string]UpdateAPIShieldDiscoveryOperation
+
+// UpdateAPIShieldDiscoveryOperation represents the state to set on a discovery operation.
+type UpdateAPIShieldDiscoveryOperation struct {
+	// State is the state to set on the operation
+	State APIShieldDiscoveryState `json:"state" url:"-"`
+}
+
+// APIShieldListDiscoveryOperationsResponse represents the response from the api_gateway/discovery/operations endpoint.
+type APIShieldListDiscoveryOperationsResponse struct {
+	Result     []APIShieldDiscoveryOperation `json:"result"`
+	ResultInfo `json:"result_info"`
+	Response
+}
+
+// APIShieldPatchDiscoveryOperationResponse represents the response from the PATCH api_gateway/discovery/operations/{id} endpoint.
+type APIShieldPatchDiscoveryOperationResponse struct {
+	Result UpdateAPIShieldDiscoveryOperation `json:"result"`
+	Response
+}
+
+// APIShieldPatchDiscoveryOperationsResponse represents the response from the PATCH api_gateway/discovery/operations endpoint.
+type APIShieldPatchDiscoveryOperationsResponse struct {
+	Result UpdateAPIShieldDiscoveryOperationsParams `json:"result"`
+	Response
+}
+
+// ListAPIShieldDiscoveryOperations retrieve the most up to date view of discovered operations.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-api-discovery-retrieve-discovered-operations-on-a-zone
+func (api *API) ListAPIShieldDiscoveryOperations(ctx context.Context, rc *ResourceContainer, params ListAPIShieldDiscoveryOperationsParams) ([]APIShieldDiscoveryOperation, ResultInfo, error) {
+	uri := buildURI(fmt.Sprintf("/zones/%s/api_gateway/discovery/operations", rc.Identifier), params)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, ResultInfo{}, err
+	}
+
+	var asResponse APIShieldListDiscoveryOperationsResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return nil, ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return asResponse.Result, asResponse.ResultInfo, nil
+}
+
+// UpdateAPIShieldDiscoveryOperation updates certain fields on a discovered operation.
+//
+// API Documentation: https://developers.cloudflare.com/api/operations/api-shield-api-patch-discovered-operation
+func (api *API) UpdateAPIShieldDiscoveryOperation(ctx context.Context, rc *ResourceContainer, params UpdateAPIShieldDiscoveryOperationParams) (*UpdateAPIShieldDiscoveryOperation, error) {
+	if params.OperationID == "" {
+		return nil, fmt.Errorf("operation ID must be provided")
+	}
+
+	uri := fmt.Sprintf("/zones/%s/api_gateway/discovery/operations/%s", rc.Identifier, params.OperationID)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+	if err != nil {
+		return nil, err
+	}
+
+	// Result should be the updated schema that was patched
+	var asResponse APIShieldPatchDiscoveryOperationResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return &asResponse.Result, nil
+}
+
+// UpdateAPIShieldDiscoveryOperations bulk updates certain fields on multiple discovered operations
+//
+// API documentation: https://developers.cloudflare.com/api/operations/api-shield-api-patch-discovered-operations
+func (api *API) UpdateAPIShieldDiscoveryOperations(ctx context.Context, rc *ResourceContainer, params UpdateAPIShieldDiscoveryOperationsParams) (*UpdateAPIShieldDiscoveryOperationsParams, error) {
+	uri := fmt.Sprintf("/zones/%s/api_gateway/discovery/operations", rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+	if err != nil {
+		return nil, err
+	}
+
+	// Result should be the updated schema that was patched
+	var asResponse APIShieldPatchDiscoveryOperationsResponse
+	err = json.Unmarshal(res, &asResponse)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return &asResponse.Result, nil
+}
diff --git a/api_shield_schemas.go b/api_shield_schemas.go
deleted file mode 100644
index 45e557cd5..000000000
--- a/api_shield_schemas.go
+++ /dev/null
@@ -1,335 +0,0 @@
-package cloudflare
-
-import (
-	"bytes"
-	"context"
-	"fmt"
-	"io"
-	"mime/multipart"
-	"net/http"
-	"strconv"
-	"time"
-
-	"github.com/goccy/go-json"
-)
-
-// APIShieldSchema represents a schema stored in API Shield Schema Validation 2.0.
-type APIShieldSchema struct {
-	// ID represents the ID of the schema
-	ID string `json:"schema_id"`
-	// Name represents the name of the schema
-	Name string `json:"name"`
-	// Kind of the schema
-	Kind string `json:"kind"`
-	// Source is the contents of the schema
-	Source string `json:"source,omitempty"`
-	// CreatedAt is the time the schema was created
-	CreatedAt *time.Time `json:"created_at,omitempty"`
-	// ValidationEnabled controls if schema is used for validation
-	ValidationEnabled bool `json:"validation_enabled,omitempty"`
-}
-
-// CreateAPIShieldSchemaParams represents the parameters to pass when creating a schema in Schema Validation 2.0.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-post-schema
-type CreateAPIShieldSchemaParams struct {
-	// Source is a io.Reader containing the contents of the schema
-	Source io.Reader
-	// Name represents the name of the schema.
-	Name string
-	// Kind of the schema. This is always set to openapi_v3.
-	Kind string
-	// ValidationEnabled controls if schema is used for validation
-	ValidationEnabled *bool
-}
-
-// GetAPIShieldSchemaParams represents the parameters to pass when retrieving a schema with a given schema ID.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-retrieve-information-about-specific-schema
-type GetAPIShieldSchemaParams struct {
-	// SchemaID is the ID of the schema to retrieve
-	SchemaID string `url:"-"`
-
-	// OmitSource specifies whether the contents of the schema should be returned in the "Source" field.
-	OmitSource *bool `url:"omit_source,omitempty"`
-}
-
-// ListAPIShieldSchemasParams represents the parameters to pass when retrieving all schemas.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-retrieve-information-about-all-schemas
-type ListAPIShieldSchemasParams struct {
-	// OmitSource specifies whether the contents of the schema should be returned in the "Source" field.
-	OmitSource *bool `url:"omit_source,omitempty"`
-
-	// ValidationEnabled specifies whether to return only schemas that have validation enabled.
-	ValidationEnabled *bool `url:"validation_enabled,omitempty"`
-
-	// PaginationOptions to apply to the request.
-	PaginationOptions
-}
-
-// DeleteAPIShieldSchemaParams represents the parameters to pass to delete a schema.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-delete-a-schema
-type DeleteAPIShieldSchemaParams struct {
-	// SchemaID is the schema to be deleted
-	SchemaID string `url:"-"`
-}
-
-// UpdateAPIShieldSchemaParams represents the parameters to pass to patch certain fields on an existing schema
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-enable-validation-for-a-schema
-type UpdateAPIShieldSchemaParams struct {
-	// SchemaID is the schema to be patched
-	SchemaID string `json:"-" url:"-"`
-
-	// ValidationEnabled controls if schema is used for validation
-	ValidationEnabled *bool `json:"validation_enabled" url:"-"`
-}
-
-// APIShieldGetSchemaResponse represents the response from the GET api_gateway/user_schemas/{id} endpoint.
-type APIShieldGetSchemaResponse struct {
-	Result APIShieldSchema `json:"result"`
-	Response
-}
-
-// APIShieldListSchemasResponse represents the response from the GET api_gateway/user_schemas endpoint.
-type APIShieldListSchemasResponse struct {
-	Result     []APIShieldSchema `json:"result"`
-	ResultInfo `json:"result_info"`
-	Response
-}
-
-// APIShieldCreateSchemaResponse represents the response from the POST api_gateway/user_schemas endpoint.
-type APIShieldCreateSchemaResponse struct {
-	Result APIShieldCreateSchemaResult `json:"result"`
-	Response
-}
-
-// APIShieldDeleteSchemaResponse represents the response from the DELETE api_gateway/user_schemas/{id} endpoint.
-type APIShieldDeleteSchemaResponse struct {
-	Result interface{} `json:"result"`
-	Response
-}
-
-// APIShieldPatchSchemaResponse represents the response from the PATCH api_gateway/user_schemas/{id} endpoint.
-type APIShieldPatchSchemaResponse struct {
-	Result APIShieldSchema `json:"result"`
-	Response
-}
-
-// APIShieldCreateSchemaResult represents the successful result of creating a schema in Schema Validation 2.0.
-type APIShieldCreateSchemaResult struct {
-	// APIShieldSchema is the schema that was created
-	Schema APIShieldSchema `json:"schema"`
-	// APIShieldCreateSchemaEvents are non-critical event logs that occurred during processing.
-	Events APIShieldCreateSchemaEvents `json:"upload_details"`
-}
-
-// APIShieldCreateSchemaEvents are event logs that occurred during processing.
-//
-// The logs are separated into levels of severity.
-type APIShieldCreateSchemaEvents struct {
-	Critical *APIShieldCreateSchemaEventWithLocation   `json:"critical,omitempty"`
-	Errors   []APIShieldCreateSchemaEventWithLocations `json:"errors,omitempty"`
-	Warnings []APIShieldCreateSchemaEventWithLocations `json:"warnings,omitempty"`
-}
-
-// APIShieldCreateSchemaEvent is an event log that occurred during processing.
-type APIShieldCreateSchemaEvent struct {
-	// Code identifies the event that occurred
-	Code uint `json:"code"`
-	// Message describes the event that occurred
-	Message string `json:"message"`
-}
-
-// APIShieldCreateSchemaEventWithLocation is an event log that occurred during processing, with the location
-// in the schema where the event occurred.
-type APIShieldCreateSchemaEventWithLocation struct {
-	APIShieldCreateSchemaEvent
-
-	// Location is where the event occurred
-	// See https://goessner.net/articles/JsonPath/ for JSONPath specification.
-	Location string `json:"location,omitempty"`
-}
-
-// APIShieldCreateSchemaEventWithLocations is an event log that occurred during processing, with the location(s)
-// in the schema where the event occurred.
-type APIShieldCreateSchemaEventWithLocations struct {
-	APIShieldCreateSchemaEvent
-
-	// Locations lists JSONPath locations where the event occurred
-	// See https://goessner.net/articles/JsonPath/ for JSONPath specification
-	Locations []string `json:"locations"`
-}
-
-func (cse APIShieldCreateSchemaEventWithLocations) String() string {
-	var s string
-	s += cse.Message
-
-	if len(cse.Locations) == 0 || (len(cse.Locations) == 1 && cse.Locations[0] == "") {
-		// append nothing
-	} else if len(cse.Locations) == 1 {
-		s += fmt.Sprintf(" (%s)", cse.Locations[0])
-	} else {
-		s += " (multiple locations)"
-	}
-
-	return s
-}
-
-// GetAPIShieldSchema retrieves information about a specific schema on a zone
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-retrieve-information-about-specific-schema
-func (api *API) GetAPIShieldSchema(ctx context.Context, rc *ResourceContainer, params GetAPIShieldSchemaParams) (*APIShieldSchema, error) {
-	if params.SchemaID == "" {
-		return nil, fmt.Errorf("schema ID must be provided")
-	}
-
-	path := fmt.Sprintf("/zones/%s/api_gateway/user_schemas/%s", rc.Identifier, params.SchemaID)
-
-	uri := buildURI(path, params)
-
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var asResponse APIShieldGetSchemaResponse
-	err = json.Unmarshal(res, &asResponse)
-	if err != nil {
-		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return &asResponse.Result, nil
-}
-
-// ListAPIShieldSchemas retrieves all schemas for a zone
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-retrieve-information-about-all-schemas
-func (api *API) ListAPIShieldSchemas(ctx context.Context, rc *ResourceContainer, params ListAPIShieldSchemasParams) ([]APIShieldSchema, ResultInfo, error) {
-	path := fmt.Sprintf("/zones/%s/api_gateway/user_schemas", rc.Identifier)
-
-	uri := buildURI(path, params)
-
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return nil, ResultInfo{}, err
-	}
-
-	var asResponse APIShieldListSchemasResponse
-	err = json.Unmarshal(res, &asResponse)
-	if err != nil {
-		return nil, ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return asResponse.Result, asResponse.ResultInfo, nil
-}
-
-// CreateAPIShieldSchema uploads a schema to a zone
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-post-schema
-func (api *API) CreateAPIShieldSchema(ctx context.Context, rc *ResourceContainer, params CreateAPIShieldSchemaParams) (*APIShieldCreateSchemaResult, error) {
-	uri := fmt.Sprintf("/zones/%s/api_gateway/user_schemas", rc.Identifier)
-
-	if params.Name == "" {
-		return nil, fmt.Errorf("name must not be empty")
-	}
-
-	if params.Source == nil {
-		return nil, fmt.Errorf("source must not be nil")
-	}
-
-	// Prepare the form to be submitted
-	var b bytes.Buffer
-	w := multipart.NewWriter(&b)
-	// write fields
-	if err := w.WriteField("name", params.Name); err != nil {
-		return nil, fmt.Errorf("error during multi-part form construction: %w", err)
-	}
-	if err := w.WriteField("kind", params.Kind); err != nil {
-		return nil, fmt.Errorf("error during multi-part form construction: %w", err)
-	}
-
-	if params.ValidationEnabled != nil {
-		if err := w.WriteField("validation_enabled", strconv.FormatBool(*params.ValidationEnabled)); err != nil {
-			return nil, fmt.Errorf("error during multi-part form construction: %w", err)
-		}
-	}
-
-	// write schema contents
-	part, err := w.CreateFormFile("file", params.Name)
-	if err != nil {
-		return nil, fmt.Errorf("error during multi-part form construction: %w", err)
-	}
-	if _, err := io.Copy(part, params.Source); err != nil {
-		return nil, fmt.Errorf("error during multi-part form construction: %w", err)
-	}
-	if err := w.Close(); err != nil {
-		return nil, fmt.Errorf("error during multi-part form construction: %w", err)
-	}
-
-	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPost, uri, &b, http.Header{
-		"Content-Type": []string{w.FormDataContentType()},
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	var asResponse APIShieldCreateSchemaResponse
-	err = json.Unmarshal(res, &asResponse)
-	if err != nil {
-		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return &asResponse.Result, nil
-}
-
-// DeleteAPIShieldSchema deletes a single schema
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-delete-a-schema
-func (api *API) DeleteAPIShieldSchema(ctx context.Context, rc *ResourceContainer, params DeleteAPIShieldSchemaParams) error {
-	if params.SchemaID == "" {
-		return fmt.Errorf("schema ID must be provided")
-	}
-
-	uri := fmt.Sprintf("/zones/%s/api_gateway/user_schemas/%s", rc.Identifier, params.SchemaID)
-
-	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
-	if err != nil {
-		return err
-	}
-
-	var asResponse APIShieldDeleteSchemaResponse
-	err = json.Unmarshal(res, &asResponse)
-	if err != nil {
-		return fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return nil
-}
-
-// UpdateAPIShieldSchema updates certain fields on an existing schema.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/api-shield-schema-validation-enable-validation-for-a-schema
-func (api *API) UpdateAPIShieldSchema(ctx context.Context, rc *ResourceContainer, params UpdateAPIShieldSchemaParams) (*APIShieldSchema, error) {
-	if params.SchemaID == "" {
-		return nil, fmt.Errorf("schema ID must be provided")
-	}
-
-	uri := fmt.Sprintf("/zones/%s/api_gateway/user_schemas/%s", rc.Identifier, params.SchemaID)
-
-	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
-	if err != nil {
-		return nil, err
-	}
-
-	// Result should be the updated schema that was patched
-	var asResponse APIShieldPatchSchemaResponse
-	err = json.Unmarshal(res, &asResponse)
-	if err != nil {
-		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return &asResponse.Result, nil
-}


Test patch:
diff --git a/api_shield_api_discovery_test.go b/api_shield_api_discovery_test.go
new file mode 100644
index 000000000..3344c4934
--- /dev/null
+++ b/api_shield_api_discovery_test.go
@@ -0,0 +1,383 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const testAPIShieldDiscoveryOperationID = "d3f614c0-7d73-4e4f-8d17-4215e7d78b77"
+
+func TestListAPIShieldDiscoveryOperations(t *testing.T) {
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/discovery/operations", testZoneID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": [
+			{
+				"id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+				"method": "POST",
+				"host": "api.cloudflare.com",
+				"endpoint": "/client/v4/zones",
+				"last_updated": "2023-03-02T15:46:06.000000Z",
+				"origin": ["ML"],
+				"state": "review"
+			}
+		],
+		"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.ListAPIShieldDiscoveryOperations(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		ListAPIShieldDiscoveryOperationsParams{},
+	)
+
+	lastUpdated := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+	expectedOps := []APIShieldDiscoveryOperation{
+		{
+			Method:      "POST",
+			Host:        "api.cloudflare.com",
+			Endpoint:    "/client/v4/zones",
+			ID:          "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+			LastUpdated: &lastUpdated,
+			Origin:      []APIShieldDiscoveryOrigin{APIShieldDiscoveryOriginML},
+			State:       APIShieldDiscoveryStateReview,
+		},
+	}
+
+	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 TestListAPIShieldDiscoveryOperationsWithParams(t *testing.T) {
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/discovery/operations", testZoneID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": [
+			{
+				"id": "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+				"method": "POST",
+				"host": "api.cloudflare.com",
+				"endpoint": "/client/v4/zones",
+				"last_updated": "2023-03-02T15:46:06.000000Z",
+				"origin": ["SessionIdentifier"],
+				"state": "saved",
+				"features": {
+					"traffic_stats": {}
+				}
+			}
+		],
+		"result_info": {
+			"page": 3,
+			"per_page": 20,
+			"count": 1,
+			"total_count": 2000
+		}
+	}`
+
+	tests := []struct {
+		name           string
+		params         ListAPIShieldDiscoveryOperationsParams
+		expectedParams url.Values
+	}{
+		{
+			name: "all params",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Direction: "desc",
+				OrderBy:   "host",
+				Hosts:     []string{"api.cloudflare.com", "developers.cloudflare.com"},
+				Methods:   []string{"GET", "PUT"},
+				Endpoint:  "/client",
+				Origin:    APIShieldDiscoveryOriginSessionIdentifier,
+				State:     APIShieldDiscoveryStateSaved,
+				Diff:      true,
+				PaginationOptions: PaginationOptions{
+					Page:    1,
+					PerPage: 25,
+				},
+			},
+			expectedParams: url.Values{
+				"direction": []string{"desc"},
+				"order":     []string{"host"},
+				"host":      []string{"api.cloudflare.com", "developers.cloudflare.com"},
+				"method":    []string{"GET", "PUT"},
+				"endpoint":  []string{"/client"},
+				"origin":    []string{"SessionIdentifier"},
+				"state":     []string{"saved"},
+				"diff":      []string{"true"},
+				"page":      []string{"1"},
+				"per_page":  []string{"25"},
+			},
+		},
+		{
+			name: "direction only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Direction: "desc",
+			},
+			expectedParams: url.Values{
+				"direction": []string{"desc"},
+			},
+		},
+		{
+			name: "order only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				OrderBy: "host",
+			},
+			expectedParams: url.Values{
+				"order": []string{"host"},
+			},
+		},
+		{
+			name: "hosts only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Hosts: []string{"api.cloudflare.com", "developers.cloudflare.com"},
+			},
+			expectedParams: url.Values{
+				"host": []string{"api.cloudflare.com", "developers.cloudflare.com"},
+			},
+		},
+		{
+			name: "methods only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Methods: []string{"GET", "PUT"},
+			},
+			expectedParams: url.Values{
+				"method": []string{"GET", "PUT"},
+			},
+		},
+		{
+			name: "endpoint only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Endpoint: "/client",
+			},
+			expectedParams: url.Values{
+				"endpoint": []string{"/client"},
+			},
+		},
+		{
+			name: "origin only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Origin: APIShieldDiscoveryOriginSessionIdentifier,
+			},
+			expectedParams: url.Values{
+				"origin": []string{"SessionIdentifier"},
+			},
+		},
+		{
+			name: "state only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				State: APIShieldDiscoveryStateSaved,
+			},
+			expectedParams: url.Values{
+				"state": []string{"saved"},
+			},
+		},
+		{
+			name: "diff only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				Diff: true,
+			},
+			expectedParams: url.Values{
+				"diff": []string{"true"},
+			},
+		},
+		{
+			name: "pagination only",
+			params: ListAPIShieldDiscoveryOperationsParams{
+				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.ListAPIShieldDiscoveryOperations(
+				context.Background(),
+				ZoneIdentifier(testZoneID),
+				test.params,
+			)
+
+			lastUpdated := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
+			expected := []APIShieldDiscoveryOperation{
+				{
+					Method:      "POST",
+					Host:        "api.cloudflare.com",
+					Endpoint:    "/client/v4/zones",
+					ID:          "9def2cb0-3ed0-4737-92ca-f09efa4718fd",
+					LastUpdated: &lastUpdated,
+					Origin:      []APIShieldDiscoveryOrigin{APIShieldDiscoveryOriginSessionIdentifier},
+					State:       APIShieldDiscoveryStateSaved,
+					Features: map[string]any{
+						"traffic_stats": map[string]any{},
+					},
+				},
+			}
+
+			if assert.NoError(t, err) {
+				assert.Equal(t, expected, actual)
+			}
+		})
+	}
+}
+
+func TestUpdateAPIShieldDiscoveryOperation(t *testing.T) {
+	setup()
+	t.Cleanup(teardown)
+
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/discovery/operations/%s", testZoneID, testAPIShieldDiscoveryOperationID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": {
+			"state": "ignored"
+		}
+	}`
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		require.Empty(t, r.URL.Query())
+
+		body, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		require.Equal(t, `{"state":"ignored"}`, string(body))
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+
+	mux.HandleFunc(endpoint, handler)
+
+	actual, err := client.UpdateAPIShieldDiscoveryOperation(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		UpdateAPIShieldDiscoveryOperationParams{
+			OperationID: testAPIShieldDiscoveryOperationID,
+			State:       APIShieldDiscoveryStateIgnored,
+		},
+	)
+
+	// patch result is a cut down representation of the schema
+	// so metadata like created date is not populated
+	expected := &UpdateAPIShieldDiscoveryOperation{
+		State: APIShieldDiscoveryStateIgnored,
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+
+	assert.NoError(t, err)
+}
+
+func TestUpdateAPIShieldDiscoveryOperations(t *testing.T) {
+	setup()
+	t.Cleanup(teardown)
+
+	endpoint := fmt.Sprintf("/zones/%s/api_gateway/discovery/operations", testZoneID)
+	response := `{
+		"success" : true,
+		"errors": [],
+		"messages": [],
+		"result": {
+			"9b16ce22-d1bf-425d-869f-a11f8240fafb": { "state": "ignored" },
+			"c51c2ea1-a690-48fd-8e3f-7fc79b269947": { "state": "review" }
+		}
+	}`
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		require.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		require.Empty(t, r.URL.Query())
+
+		body, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		require.Equal(t, `{"9b16ce22-d1bf-425d-869f-a11f8240fafb":{"state":"ignored"},"c51c2ea1-a690-48fd-8e3f-7fc79b269947":{"state":"review"}}`, string(body))
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+
+	mux.HandleFunc(endpoint, handler)
+
+	actual, err := client.UpdateAPIShieldDiscoveryOperations(
+		context.Background(),
+		ZoneIdentifier(testZoneID),
+		UpdateAPIShieldDiscoveryOperationsParams{
+			"9b16ce22-d1bf-425d-869f-a11f8240fafb": UpdateAPIShieldDiscoveryOperation{State: APIShieldDiscoveryStateIgnored},
+			"c51c2ea1-a690-48fd-8e3f-7fc79b269947": UpdateAPIShieldDiscoveryOperation{State: APIShieldDiscoveryStateReview},
+		},
+	)
+
+	expected := &UpdateAPIShieldDiscoveryOperationsParams{
+		"9b16ce22-d1bf-425d-869f-a11f8240fafb": UpdateAPIShieldDiscoveryOperation{State: APIShieldDiscoveryStateIgnored},
+		"c51c2ea1-a690-48fd-8e3f-7fc79b269947": UpdateAPIShieldDiscoveryOperation{State: APIShieldDiscoveryStateReview},
+	}
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expected, actual)
+	}
+
+	assert.NoError(t, err)
+}
+
+func TestMustProvideDiscoveryOperationID(t *testing.T) {
+	setup()
+	t.Cleanup(teardown)
+
+	_, err := client.UpdateAPIShieldDiscoveryOperation(context.Background(), ZoneIdentifier(testZoneID), UpdateAPIShieldDiscoveryOperationParams{})
+	require.ErrorContains(t, err, "operation ID must be provided")
+}
diff --git a/api_shield_schemas_test.go b/api_shield_schemas_test.go
deleted file mode 100644
index 5d2f21990..000000000
--- a/api_shield_schemas_test.go
+++ /dev/null
@@ -1,504 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"io"
-	"mime"
-	"mime/multipart"
-	"net/http"
-	"net/url"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-const testAPIShieldSchemaId = "0f05e4fc-1c36-4bd2-a5e7-b29d32d3dc8e"
-
-func TestGetAPIShieldSchema(t *testing.T) {
-	endpoint := fmt.Sprintf("/zones/%s/api_gateway/user_schemas/%s", testZoneID, testAPIShieldSchemaId)
-	response := `{
-		"success" : true,
-		"errors": [],
-		"messages": [],
-		"result": {
-			"schema_id": "0f05e4fc-1c36-4bd2-a5e7-b29d32d3dc8e",
-			"name": "petstore.json",
-			"kind": "openapi_v3",
-			"created_at": "2023-03-02T15:46:06.000000Z",
-			"validation_enabled": true,
-			"source": "{}"
-		}
-	}`
-
-	tests := []struct {
-		name           string
-		getParams      GetAPIShieldSchemaParams
-		expectedParams url.Values
-	}{
-		{
-			name: "without omit source",
-			getParams: GetAPIShieldSchemaParams{
-				SchemaID: testAPIShieldSchemaId,
-			},
-			expectedParams: url.Values{},
-		},
-		{
-			name: "with omit source=true",
-			getParams: GetAPIShieldSchemaParams{
-				SchemaID:   testAPIShieldSchemaId,
-				OmitSource: BoolPtr(true),
-			},
-			expectedParams: url.Values{
-				"omit_source": []string{"true"},
-			},
-		},
-		{
-			name: "with omit source=false",
-			getParams: GetAPIShieldSchemaParams{
-				SchemaID:   testAPIShieldSchemaId,
-				OmitSource: BoolPtr(false),
-			},
-			expectedParams: url.Values{
-				"omit_source": []string{"false"},
-			},
-		},
-	}
-
-	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.GetAPIShieldSchema(
-				context.Background(),
-				ZoneIdentifier(testZoneID),
-				test.getParams,
-			)
-
-			createdAt := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
-			expected := &APIShieldSchema{
-				ID:                testAPIShieldSchemaId,
-				Name:              "petstore.json",
-				Kind:              "openapi_v3",
-				Source:            "{}",
-				CreatedAt:         &createdAt,
-				ValidationEnabled: true,
-			}
-
-			if assert.NoError(t, err) {
-				assert.Equal(t, expected, actual)
-			}
-		})
-	}
-}
-
-func TestListAPIShieldSchemas(t *testing.T) {
-	endpoint := fmt.Sprintf("/zones/%s/api_gateway/user_schemas", testZoneID)
-	response := `{
-		"success" : true,
-		"errors": [],
-		"messages": [],
-		"result": [
-			{
-				"schema_id": "0f05e4fc-1c36-4bd2-a5e7-b29d32d3dc8e",
-				"name": "petstore.json",
-				"kind": "openapi_v3",
-				"created_at": "2023-03-02T15:46:06.000000Z",
-				"validation_enabled": true,
-				"source": "{}"
-			}
-		],
-		"result_info": {
-			"page": 3,
-			"per_page": 20,
-			"count": 1,
-			"total_count": 2000
-		}
-	}`
-
-	tests := []struct {
-		name           string
-		getParams      ListAPIShieldSchemasParams
-		expectedParams url.Values
-	}{
-		{
-			name:           "with empty params",
-			getParams:      ListAPIShieldSchemasParams{},
-			expectedParams: url.Values{},
-		},
-		{
-			name: "all params",
-			getParams: ListAPIShieldSchemasParams{
-				OmitSource:        BoolPtr(true),
-				ValidationEnabled: BoolPtr(false),
-				PaginationOptions: PaginationOptions{
-					Page:    1,
-					PerPage: 25,
-				},
-			},
-			expectedParams: url.Values{
-				"omit_source":        []string{"true"},
-				"validation_enabled": []string{"false"},
-				"page":               []string{"1"},
-				"per_page":           []string{"25"},
-			},
-		},
-		{
-			name: "with omit source",
-			getParams: ListAPIShieldSchemasParams{
-				OmitSource: BoolPtr(true),
-			},
-			expectedParams: url.Values{
-				"omit_source": []string{"true"},
-			},
-		},
-		{
-			name: "with validation enabled",
-			getParams: ListAPIShieldSchemasParams{
-				ValidationEnabled: BoolPtr(true),
-			},
-			expectedParams: url.Values{
-				"validation_enabled": []string{"true"},
-			},
-		},
-	}
-
-	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, resultInfo, err := client.ListAPIShieldSchemas(
-				context.Background(),
-				ZoneIdentifier(testZoneID),
-				test.getParams,
-			)
-
-			createdAt := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
-			expected := []APIShieldSchema{
-				{
-					ID:                testAPIShieldSchemaId,
-					Name:              "petstore.json",
-					Kind:              "openapi_v3",
-					Source:            "{}",
-					CreatedAt:         &createdAt,
-					ValidationEnabled: true,
-				},
-			}
-
-			if assert.NoError(t, err) {
-				assert.Equal(t, expected, actual)
-
-				expectedResultInfo := ResultInfo{
-					Page:    3,
-					PerPage: 20,
-					Count:   1,
-					Total:   2000,
-				}
-				assert.Equal(t, expectedResultInfo, resultInfo)
-			}
-		})
-	}
-}
-
-func TestDeleteAPIShieldSchema(t *testing.T) {
-	setup()
-	t.Cleanup(teardown)
-
-	endpoint := fmt.Sprintf("/zones/%s/api_gateway/user_schemas/%s", testZoneID, testAPIShieldSchemaId)
-	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.DeleteAPIShieldSchema(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		DeleteAPIShieldSchemaParams{
-			SchemaID: testAPIShieldSchemaId,
-		},
-	)
-
-	assert.NoError(t, err)
-}
-
-func TestUpdateAPIShieldSchema(t *testing.T) {
-	setup()
-	t.Cleanup(teardown)
-
-	endpoint := fmt.Sprintf("/zones/%s/api_gateway/user_schemas/%s", testZoneID, testAPIShieldSchemaId)
-	response := `{
-		"success" : true,
-		"errors": [],
-		"messages": [],
-		"result": {
-			"schema_id": "0f05e4fc-1c36-4bd2-a5e7-b29d32d3dc8e",
-			"name": "petstore.json",
-			"kind": "openapi_v3",
-			"validation_enabled": true
-		}
-	}`
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		require.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
-		require.Empty(t, r.URL.Query())
-
-		body, err := io.ReadAll(r.Body)
-		require.NoError(t, err)
-		require.Equal(t, `{"validation_enabled":true}`, string(body))
-
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, response)
-	}
-
-	mux.HandleFunc(endpoint, handler)
-
-	actual, err := client.UpdateAPIShieldSchema(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		UpdateAPIShieldSchemaParams{
-			SchemaID:          testAPIShieldSchemaId,
-			ValidationEnabled: BoolPtr(true),
-		},
-	)
-
-	// patch result is a cut down representation of the schema
-	// so metadata like created date is not populated
-	expected := &APIShieldSchema{
-		ID:                testAPIShieldSchemaId,
-		Name:              "petstore.json",
-		Kind:              "openapi_v3",
-		ValidationEnabled: true,
-	}
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, expected, actual)
-	}
-
-	assert.NoError(t, err)
-}
-
-func TestCreateAPIShieldSchema(t *testing.T) {
-	setup()
-	t.Cleanup(teardown)
-
-	endpoint := fmt.Sprintf("/zones/%s/api_gateway/user_schemas", testZoneID)
-	response := `{
-		"success" : true,
-		"errors": [],
-		"messages": [],
-		"result": {
-			"schema": {
-				"schema_id": "0f05e4fc-1c36-4bd2-a5e7-b29d32d3dc8e",
-				"name": "petstore.json",
-				"kind": "openapi_v3",
-				"created_at": "2023-03-02T15:46:06.000000Z",
-				"validation_enabled": true,
-				"source": "{}"
-			},
-			"upload_details": {
-				"warnings": [
-					{
-						"code": 28,
-						"message": "unsupported media type: application/octet-stream",
-						"locations": [
-							".paths[\"/user/{username}\"].put"
-						]
-					}
-				]
-			}
-		}
-	}`
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		require.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
-
-		// Check that form data has been sent correctly.
-		mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
-		require.NoError(t, err)
-		require.True(t, strings.HasPrefix(mediaType, "multipart/form-data"))
-
-		expectedParts := []struct {
-			partKeys map[string]string
-			data     []byte
-		}{
-			{
-				partKeys: map[string]string{"name": "name"},
-				data:     []byte("petstore.json"),
-			},
-			{
-				partKeys: map[string]string{"name": "kind"},
-				data:     []byte("openapi_v3"),
-			},
-			{
-				partKeys: map[string]string{"name": "validation_enabled"},
-				data:     []byte("true"),
-			},
-			{
-				partKeys: map[string]string{"filename": "petstore.json", "name": "file"},
-				data:     []byte("{}"),
-			},
-		}
-
-		multiPartReader := multipart.NewReader(r.Body, params["boundary"])
-		partNum := 0
-		for {
-			part, err := multiPartReader.NextPart()
-			if errors.Is(err, io.EOF) {
-				break
-			}
-			require.NoError(t, err)
-
-			// more parts found - this is unexpected
-			require.Less(t, partNum, len(expectedParts))
-
-			value, err := io.ReadAll(part)
-			require.NoError(t, err)
-
-			_, dParams, err := mime.ParseMediaType(part.Header.Get("Content-Disposition"))
-			require.NoError(t, err)
-
-			require.Equal(t, expectedParts[partNum].partKeys, dParams)
-			require.Equal(t, expectedParts[partNum].data, value)
-			partNum++
-		}
-
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, response)
-	}
-
-	mux.HandleFunc(endpoint, handler)
-
-	actual, err := client.CreateAPIShieldSchema(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		CreateAPIShieldSchemaParams{
-			Name:              "petstore.json",
-			Kind:              "openapi_v3",
-			Source:            strings.NewReader("{}"),
-			ValidationEnabled: BoolPtr(true),
-		},
-	)
-
-	createdAt := time.Date(2023, time.March, 2, 15, 46, 6, 0, time.UTC)
-	expected := &APIShieldCreateSchemaResult{
-		Schema: APIShieldSchema{
-			ID:                testAPIShieldSchemaId,
-			Name:              "petstore.json",
-			Kind:              "openapi_v3",
-			Source:            "{}",
-			CreatedAt:         &createdAt,
-			ValidationEnabled: true,
-		},
-		Events: APIShieldCreateSchemaEvents{
-			Warnings: []APIShieldCreateSchemaEventWithLocations{
-				{
-					APIShieldCreateSchemaEvent: APIShieldCreateSchemaEvent{
-						Code:    28,
-						Message: "unsupported media type: application/octet-stream",
-					},
-					Locations: []string{
-						".paths[\"/user/{username}\"].put",
-					},
-				},
-			},
-		},
-	}
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, expected, actual)
-	}
-}
-
-func TestCreateAPIShieldSchemaError(t *testing.T) {
-	setup()
-	t.Cleanup(teardown)
-
-	endpoint := fmt.Sprintf("/zones/%s/api_gateway/user_schemas", testZoneID)
-	response := `{
-		"success" : false,
-		"errors": [
-			{
-			  "code": 21400,
-			  "message": "only default parameter styles are supported: path, form (.paths[\"/pet/{petId}\"].get.parameters[0])"
-			}
-		],
-		"messages": [],
-		"result": {
-			"upload_details": {
-				"errors": [
-					{
-						"code": 22,
-						"message": "only default parameter styles are supported: path, form",
-						"locations": [
-							".paths[\"/pet/{petId}\"].get.parameters[0]"
-						]
-					}
-				]
-			}
-		}
-	}`
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		require.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		w.WriteHeader(400)
-		fmt.Fprint(w, response)
-	}
-
-	mux.HandleFunc(endpoint, handler)
-
-	_, err := client.CreateAPIShieldSchema(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		CreateAPIShieldSchemaParams{
-			Name:              "petstore.json",
-			Kind:              "openapi_v3",
-			Source:            strings.NewReader("{}"),
-			ValidationEnabled: BoolPtr(true),
-		},
-	)
-
-	require.ErrorContains(t, err, "only default parameter styles are supported: path, form (.paths[\"/pet/{petId}\"].get.parameters[0]) (21400)")
-}
-
-func TestMustProvideSchemaID(t *testing.T) {
-	setup()
-	t.Cleanup(teardown)
-
-	_, err := client.GetAPIShieldSchema(context.Background(), ZoneIdentifier(testZoneID), GetAPIShieldSchemaParams{})
-	require.ErrorContains(t, err, "schema ID must be provided")
-
-	_, err = client.UpdateAPIShieldSchema(context.Background(), ZoneIdentifier(testZoneID), UpdateAPIShieldSchemaParams{})
-	require.ErrorContains(t, err, "schema ID must be provided")
-
-	err = client.DeleteAPIShieldSchema(context.Background(), ZoneIdentifier(testZoneID), DeleteAPIShieldSchemaParams{})
-	require.ErrorContains(t, err, "schema ID must be provided")
-}