Repository: cloudflare/cloudflare-go
Pull request #1485 (2024-01-23 21:50:12)

DLP-1223: Add support for DLP EDM and CWL datasets
## Description

Adds support for the DLP EDM/CWL dataset APIs to the Go client in anticipation of Terraform support. All publicly-documented routes have an equivalent client method.

## Has your change been tested?

Unit tests added for each new client method

## 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.
- [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.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


github-actions[bot]: changelog detected :white_check_mark:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) Report
Attention: `538 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`b9ac804`)](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) 48.33% compared to head [(`e89e16a`)](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) 53.59%.
> Report is 587 commits behind head on master.

| [Files](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) | Patch % | Lines |
|---|---|---|
| [api\_shield\_schemas.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YXBpX3NoaWVsZF9zY2hlbWFzLmdv) | 54.81% | [35 Missing and 26 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [observatory.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-b2JzZXJ2YXRvcnkuZ28=) | 54.68% | [29 Missing and 29 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [per\_hostname\_tls\_settings.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-cGVyX2hvc3RuYW1lX3Rsc19zZXR0aW5ncy5nbw==) | 41.86% | [25 Missing and 25 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [dlp\_dataset.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-ZGxwX2RhdGFzZXQuZ28=) | 45.45% | [24 Missing and 24 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [logpush.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-bG9ncHVzaC5nbw==) | 48.86% | [43 Missing and 2 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [devices\_policy.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-ZGV2aWNlc19wb2xpY3kuZ28=) | 56.96% | [23 Missing and 11 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [access\_users.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX3VzZXJzLmdv) | 76.84% | [11 Missing and 11 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [d1.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-ZDEuZ28=) | 70.27% | [11 Missing and 11 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [access\_custom\_page.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-YWNjZXNzX2N1c3RvbV9wYWdlLmdv) | 58.33% | [10 Missing and 10 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| [page\_shield\_policies.go](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare#diff-cGFnZV9zaGllbGRfcG9saWNpZXMuZ28=) | 60.86% | [9 Missing and 9 partials :warning: ](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) |
| ... and [19 more](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare) | |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1485      +/-   ##
==========================================
+ Coverage   48.33%   53.59%   +5.25%     
==========================================
  Files         133      156      +23     
  Lines       13023    11903    -1120     
==========================================
+ Hits         6295     6379      +84     
+ Misses       5201     3672    -1529     
- Partials     1527     1852     +325     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cloudflare/cloudflare-go/pull/1485?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudflare).


Patch:
diff --git a/.changelog/1485.txt b/.changelog/1485.txt
new file mode 100644
index 000000000..e8a177e2e
--- /dev/null
+++ b/.changelog/1485.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+dlp: add support for EDM and CWL datasets
+```
diff --git a/dlp_dataset.go b/dlp_dataset.go
new file mode 100644
index 000000000..09cf86416
--- /dev/null
+++ b/dlp_dataset.go
@@ -0,0 +1,278 @@
+package cloudflare
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/goccy/go-json"
+)
+
+var (
+	ErrMissingDatasetID = errors.New("missing required dataset ID")
+)
+
+// DLPDatasetUpload represents a single upload version attached to a DLP dataset.
+type DLPDatasetUpload struct {
+	NumCells int    `json:"num_cells"`
+	Status   string `json:"status,omitempty"`
+	Version  int    `json:"version"`
+}
+
+// DLPDataset represents a DLP Exact Data Match dataset or Custom Word List.
+type DLPDataset struct {
+	CreatedAt   *time.Time         `json:"created_at,omitempty"`
+	Description string             `json:"description,omitempty"`
+	ID          string             `json:"id,omitempty"`
+	Name        string             `json:"name,omitempty"`
+	NumCells    int                `json:"num_cells"`
+	Secret      *bool              `json:"secret,omitempty"`
+	Status      string             `json:"status,omitempty"`
+	UpdatedAt   *time.Time         `json:"updated_at,omitempty"`
+	Uploads     []DLPDatasetUpload `json:"uploads"`
+}
+
+type ListDLPDatasetsParams struct{}
+
+type DLPDatasetListResponse struct {
+	Result []DLPDataset `json:"result"`
+	Response
+}
+
+// ListDLPDatasets returns all the DLP datasets associated with an account.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-read-all
+func (api *API) ListDLPDatasets(ctx context.Context, rc *ResourceContainer, params ListDLPDatasetsParams) ([]DLPDataset, error) {
+	if rc.Identifier == "" {
+		return nil, nil
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets", rc.Level, rc.Identifier), nil)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var dlpDatasetListResponse DLPDatasetListResponse
+	err = json.Unmarshal(res, &dlpDatasetListResponse)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return dlpDatasetListResponse.Result, nil
+}
+
+type DLPDatasetGetResponse struct {
+	Result DLPDataset `json:"result"`
+	Response
+}
+
+// GetDLPDataset returns a DLP dataset based on the dataset ID.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-read
+func (api *API) GetDLPDataset(ctx context.Context, rc *ResourceContainer, datasetID string) (DLPDataset, error) {
+	if rc.Identifier == "" {
+		return DLPDataset{}, nil
+	}
+
+	if datasetID == "" {
+		return DLPDataset{}, ErrMissingDatasetID
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets/%s", rc.Level, rc.Identifier, datasetID), nil)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return DLPDataset{}, err
+	}
+
+	var dlpDatasetGetResponse DLPDatasetGetResponse
+	err = json.Unmarshal(res, &dlpDatasetGetResponse)
+	if err != nil {
+		return DLPDataset{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return dlpDatasetGetResponse.Result, nil
+}
+
+type CreateDLPDatasetParams struct {
+	Description string `json:"description,omitempty"`
+	Name        string `json:"name"`
+	Secret      *bool  `json:"secret,omitempty"`
+}
+
+type CreateDLPDatasetResult struct {
+	MaxCells int        `json:"max_cells"`
+	Secret   string     `json:"secret"`
+	Version  int        `json:"version"`
+	Dataset  DLPDataset `json:"dataset"`
+}
+
+type CreateDLPDatasetResponse struct {
+	Result CreateDLPDatasetResult `json:"result"`
+	Response
+}
+
+// CreateDLPDataset creates a DLP dataset.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-create
+func (api *API) CreateDLPDataset(ctx context.Context, rc *ResourceContainer, params CreateDLPDatasetParams) (CreateDLPDatasetResult, error) {
+	if rc.Identifier == "" {
+		return CreateDLPDatasetResult{}, nil
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets", rc.Level, rc.Identifier), nil)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return CreateDLPDatasetResult{}, err
+	}
+
+	var CreateDLPDatasetResponse CreateDLPDatasetResponse
+	err = json.Unmarshal(res, &CreateDLPDatasetResponse)
+	if err != nil {
+		return CreateDLPDatasetResult{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return CreateDLPDatasetResponse.Result, nil
+}
+
+// DeleteDLPDataset deletes a DLP dataset.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-delete
+func (api *API) DeleteDLPDataset(ctx context.Context, rc *ResourceContainer, datasetID string) error {
+	if rc.Identifier == "" {
+		return ErrMissingResourceIdentifier
+	}
+
+	if datasetID == "" {
+		return ErrMissingDatasetID
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets/%s", rc.Level, rc.Identifier, datasetID), nil)
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	return err
+}
+
+type UpdateDLPDatasetParams struct {
+	DatasetID   string
+	Description *string `json:"description,omitempty"` // nil to leave descrption as-is
+	Name        *string `json:"name,omitempty"`        // nil to leave name as-is
+}
+
+type UpdateDLPDatasetResponse struct {
+	Result DLPDataset `json:"result"`
+	Response
+}
+
+// UpdateDLPDataset updates the details of a DLP dataset.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-update
+func (api *API) UpdateDLPDataset(ctx context.Context, rc *ResourceContainer, params UpdateDLPDatasetParams) (DLPDataset, error) {
+	if rc.Identifier == "" {
+		return DLPDataset{}, nil
+	}
+
+	if params.DatasetID == "" {
+		return DLPDataset{}, ErrMissingDatasetID
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets/%s", rc.Level, rc.Identifier, params.DatasetID), nil)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
+	if err != nil {
+		return DLPDataset{}, err
+	}
+
+	var updateDLPDatasetResponse UpdateDLPDatasetResponse
+	err = json.Unmarshal(res, &updateDLPDatasetResponse)
+	if err != nil {
+		return DLPDataset{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return updateDLPDatasetResponse.Result, nil
+}
+
+type CreateDLPDatasetUploadResult struct {
+	MaxCells int    `json:"max_cells"`
+	Secret   string `json:"secret"`
+	Version  int    `json:"version"`
+}
+
+type CreateDLPDatasetUploadResponse struct {
+	Result CreateDLPDatasetUploadResult `json:"result"`
+	Response
+}
+type CreateDLPDatasetUploadParams struct {
+	DatasetID string
+}
+
+// CreateDLPDatasetUpload creates a new upload version for the specified DLP dataset.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-create-version
+func (api *API) CreateDLPDatasetUpload(ctx context.Context, rc *ResourceContainer, params CreateDLPDatasetUploadParams) (CreateDLPDatasetUploadResult, error) {
+	if rc.Identifier == "" {
+		return CreateDLPDatasetUploadResult{}, nil
+	}
+
+	if params.DatasetID == "" {
+		return CreateDLPDatasetUploadResult{}, ErrMissingDatasetID
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets/%s/upload", rc.Level, rc.Identifier, params.DatasetID), nil)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, nil)
+	if err != nil {
+		return CreateDLPDatasetUploadResult{}, err
+	}
+
+	var dlpDatasetCreateUploadResponse CreateDLPDatasetUploadResponse
+	err = json.Unmarshal(res, &dlpDatasetCreateUploadResponse)
+	if err != nil {
+		return CreateDLPDatasetUploadResult{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return dlpDatasetCreateUploadResponse.Result, nil
+}
+
+type UploadDLPDatasetVersionParams struct {
+	DatasetID string
+	Version   int
+	Body      interface{}
+}
+
+type UploadDLPDatasetVersionResponse struct {
+	Result DLPDataset `json:"result"`
+	Response
+}
+
+// UploadDLPDatasetVersion uploads a new version of the specified DLP dataset.
+//
+// API reference: https://developers.cloudflare.com/api/operations/dlp-datasets-upload-version
+func (api *API) UploadDLPDatasetVersion(ctx context.Context, rc *ResourceContainer, params UploadDLPDatasetVersionParams) (DLPDataset, error) {
+	if rc.Identifier == "" {
+		return DLPDataset{}, nil
+	}
+
+	if params.DatasetID == "" {
+		return DLPDataset{}, ErrMissingDatasetID
+	}
+
+	uri := buildURI(fmt.Sprintf("/%s/%s/dlp/datasets/%s/upload/%d", rc.Level, rc.Identifier, params.DatasetID, params.Version), nil)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params.Body)
+	if err != nil {
+		return DLPDataset{}, err
+	}
+
+	var dlpDatasetUploadVersionResponse UploadDLPDatasetVersionResponse
+	err = json.Unmarshal(res, &dlpDatasetUploadVersionResponse)
+	if err != nil {
+		return DLPDataset{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return dlpDatasetUploadVersionResponse.Result, nil
+}


Test patch:
diff --git a/dlp_dataset_test.go b/dlp_dataset_test.go
new file mode 100644
index 000000000..363163627
--- /dev/null
+++ b/dlp_dataset_test.go
@@ -0,0 +1,422 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/goccy/go-json"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestListDLPDatasets(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": [
+				{
+					"created_at": "2019-08-24T14:15:22Z",
+					"description": "string",
+					"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+					"name": "string",
+					"num_cells": 0,
+					"secret": true,
+					"status": "empty",
+					"updated_at": "2019-08-24T14:15:22Z",
+					"uploads": [
+					  {
+						"num_cells": 0,
+						"status": "empty",
+						"version": 0
+					  }
+					]
+				  }
+			]
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+
+	secret := true
+	want := []DLPDataset{
+		{
+			CreatedAt:   &createdAt,
+			Description: "string",
+			ID:          "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+			Name:        "string",
+			NumCells:    0,
+			Secret:      &secret,
+			Status:      "empty",
+			UpdatedAt:   &updatedAt,
+			Uploads: []DLPDatasetUpload{
+				{
+					NumCells: 0,
+					Status:   "empty",
+					Version:  0,
+				},
+			},
+		},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets", handler)
+
+	actual, err := client.ListDLPDatasets(context.Background(), AccountIdentifier(testAccountID), ListDLPDatasetsParams{})
+	require.NoError(t, err)
+	require.Equal(t, want, actual)
+}
+
+func TestGetDLPDataset(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2019-08-24T14:15:22Z",
+				"description": "string",
+				"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+				"name": "string",
+				"num_cells": 0,
+				"secret": true,
+				"status": "empty",
+				"updated_at": "2019-08-24T14:15:22Z",
+				"uploads": [
+					{
+						"num_cells": 0,
+						"status": "empty",
+						"version": 0
+					}
+				]
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+
+	secret := true
+	want := DLPDataset{
+		CreatedAt:   &createdAt,
+		Description: "string",
+		ID:          "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+		Name:        "string",
+		NumCells:    0,
+		Secret:      &secret,
+		Status:      "empty",
+		UpdatedAt:   &updatedAt,
+		Uploads: []DLPDatasetUpload{
+			{
+				NumCells: 0,
+				Status:   "empty",
+				Version:  0,
+			},
+		},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets/497f6eca-6276-4993-bfeb-53cbbbba6f08", handler)
+
+	actual, err := client.GetDLPDataset(context.Background(), AccountIdentifier(testAccountID), "497f6eca-6276-4993-bfeb-53cbbbba6f08")
+	require.NoError(t, err)
+	require.Equal(t, want, actual)
+}
+
+func TestCreateDLPDataset(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+
+		var reqBody CreateDLPDatasetParams
+		err := json.NewDecoder(r.Body).Decode(&reqBody)
+		require.Nil(t, err)
+
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"max_cells": 0,
+				"secret": "1234",
+				"version": 0,
+				"dataset": {
+					"created_at": "2019-08-24T14:15:22Z",
+					"description": "`+reqBody.Description+`",
+					"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+					"name": "`+reqBody.Name+`",
+					"num_cells": 0,
+					"secret": `+fmt.Sprintf("%t", *reqBody.Secret)+`,
+					"status": "empty",
+					"updated_at": "2019-08-24T14:15:22Z",
+					"uploads": [
+						{
+							"num_cells": 0,
+							"status": "empty",
+							"version": 0
+						}
+					]
+				}
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+
+	secret := true
+	want := CreateDLPDatasetResult{
+		MaxCells: 0,
+		Secret:   "1234",
+		Version:  0,
+		Dataset: DLPDataset{
+			CreatedAt:   &createdAt,
+			Description: "string",
+			ID:          "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+			Name:        "string",
+			NumCells:    0,
+			Secret:      &secret,
+			Status:      "empty",
+			UpdatedAt:   &updatedAt,
+			Uploads: []DLPDatasetUpload{
+				{
+					NumCells: 0,
+					Status:   "empty",
+					Version:  0,
+				},
+			},
+		},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets", handler)
+
+	actual, err := client.CreateDLPDataset(context.Background(), AccountIdentifier(testAccountID), CreateDLPDatasetParams{Description: "string", Name: "string", Secret: &secret})
+	require.NoError(t, err)
+	require.Equal(t, want, actual)
+}
+
+func TestDeleteDLPDataset(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": null
+		}`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets/497f6eca-6276-4993-bfeb-53cbbbba6f08", handler)
+
+	err := client.DeleteDLPDataset(context.Background(), AccountIdentifier(testAccountID), "497f6eca-6276-4993-bfeb-53cbbbba6f08")
+	require.NoError(t, err)
+}
+
+func TestUpdateDLPDataset(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := 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")
+
+		var reqBody UpdateDLPDatasetParams
+		err := json.NewDecoder(r.Body).Decode(&reqBody)
+		require.Nil(t, err)
+
+		var description string
+		if reqBody.Description == nil {
+			description = "string"
+		} else {
+			description = *reqBody.Description
+		}
+
+		var name string
+		if reqBody.Name == nil {
+			name = "string"
+		} else {
+			name = *reqBody.Name
+		}
+
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2019-08-24T14:15:22Z",
+				"description": "`+description+`",
+				"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+				"name": "`+name+`",
+				"num_cells": 0,
+				"secret": true,
+				"status": "empty",
+				"updated_at": "2019-08-24T14:15:22Z",
+				"uploads": [
+					{
+						"num_cells": 0,
+						"status": "empty",
+						"version": 0
+					}
+				]
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+
+	secret := true
+	want := DLPDataset{
+		CreatedAt:   &createdAt,
+		Description: "new_desc",
+		ID:          "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+		Name:        "string",
+		NumCells:    0,
+		Secret:      &secret,
+		Status:      "empty",
+		UpdatedAt:   &updatedAt,
+		Uploads: []DLPDatasetUpload{
+			{
+				NumCells: 0,
+				Status:   "empty",
+				Version:  0,
+			},
+		},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets/497f6eca-6276-4993-bfeb-53cbbbba6f08", handler)
+
+	description := "new_desc"
+	actual, err := client.UpdateDLPDataset(context.Background(), AccountIdentifier(testAccountID), UpdateDLPDatasetParams{Description: &description, Name: nil, DatasetID: "497f6eca-6276-4993-bfeb-53cbbbba6f08"})
+	require.NoError(t, err)
+	require.Equal(t, want, actual)
+}
+
+func TestCreateDLPDatasetUpload(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"max_cells": 0,
+				"secret": "1234",
+				"version": 1
+			}
+		}`)
+	}
+
+	want := CreateDLPDatasetUploadResult{
+		MaxCells: 0,
+		Secret:   "1234",
+		Version:  1,
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets/497f6eca-6276-4993-bfeb-53cbbbba6f08/upload", handler)
+
+	actual, err := client.CreateDLPDatasetUpload(context.Background(), AccountIdentifier(testAccountID), CreateDLPDatasetUploadParams{DatasetID: "497f6eca-6276-4993-bfeb-53cbbbba6f08"})
+	require.NoError(t, err)
+	require.Equal(t, want, actual)
+}
+
+func TestUploadDLPDatasetVersion(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+
+		body, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		require.Equal(t, []byte{1, 2, 3, 4}, body)
+
+		fmt.Fprintf(w, `{
+			"success": true,
+			"errors": [],
+			"messages": [],
+			"result": {
+				"created_at": "2019-08-24T14:15:22Z",
+				"description": "string",
+				"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+				"name": "string",
+				"num_cells": 5,
+				"secret": true,
+				"status": "complete",
+				"updated_at": "2019-08-24T14:15:22Z",
+				"uploads": [
+					{
+						"num_cells": 0,
+						"status": "empty",
+						"version": 0
+					},
+					{
+						"num_cells": 5,
+						"status": "complete",
+						"version": 1
+					}
+				]
+			}
+		}`)
+	}
+
+	createdAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2019-08-24T14:15:22Z")
+
+	secret := true
+	want := DLPDataset{
+		CreatedAt:   &createdAt,
+		Description: "string",
+		ID:          "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+		Name:        "string",
+		NumCells:    5,
+		Secret:      &secret,
+		Status:      "complete",
+		UpdatedAt:   &updatedAt,
+		Uploads: []DLPDatasetUpload{
+			{
+				NumCells: 0,
+				Status:   "empty",
+				Version:  0,
+			},
+			{
+				NumCells: 5,
+				Status:   "complete",
+				Version:  1,
+			},
+		},
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/dlp/datasets/497f6eca-6276-4993-bfeb-53cbbbba6f08/upload/1", handler)
+
+	actual, err := client.UploadDLPDatasetVersion(context.Background(), AccountIdentifier(testAccountID), UploadDLPDatasetVersionParams{DatasetID: "497f6eca-6276-4993-bfeb-53cbbbba6f08", Version: 1, Body: []byte{1, 2, 3, 4}})
+	require.NoError(t, err)
+	require.Equal(t, want, actual)
+}