Repository: cloudflare/cloudflare-go
Pull request #1317 (2023-06-22 18:30:54)

feat(workers): configure tail consumers
Hello from the Workers team :wave: 

## Description

This adds the necessary functionality to the sdk to allow customers to set [Tail Workers](https://developers.cloudflare.com/workers/platform/tail-workers/) as consumers on their scripts. Tail Consumers are also included in the response metadata, and the updates the response to reflect that.

Additionally, this does a fairly large refactor to the Workers test suite, moving script response to a variadic parameters setup. Many of the existing hardcoded responses were out of date or half complete, so this ensures an easy way to make changes in 1 spot going forward for consistent & up to date api shapes.

For reviewing purposes these have been broken into separate commits.

## Has your change been tested?

A test has been added to cover uploading a Tail Consumer.

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [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/



Patch:
diff --git a/.changelog/1317.txt b/.changelog/1317.txt
new file mode 100644
index 000000000..722e04c20
--- /dev/null
+++ b/.changelog/1317.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+workers: Add ability to specify tail Workers in script metadata
+```
\ No newline at end of file
diff --git a/workers.go b/workers.go
index b934218e6..53711f906 100644
--- a/workers.go
+++ b/workers.go
@@ -28,10 +28,17 @@ type CreateWorkerParams struct {
 	// ES Module syntax script.
 	Module bool
 
-	// Logpush opts the worker into Workers Logpush logging. A nil value leaves the current setting unchanged.
-	//  https://developers.cloudflare.com/workers/platform/logpush/
+	// Logpush opts the worker into Workers Logpush logging. A nil value leaves
+	// the current setting unchanged.
+	//
+	// Documentation: https://developers.cloudflare.com/workers/platform/logpush/
 	Logpush *bool
 
+	// TailConsumers specifies a list of Workers that will consume the logs of
+	// the attached Worker.
+	// Documentation: https://developers.cloudflare.com/workers/platform/tail-workers/
+	TailConsumers *[]WorkersTailConsumer
+
 	// Bindings should be a map where the keys are the binding name, and the
 	// values are the binding content
 	Bindings map[string]WorkerBinding
@@ -90,17 +97,23 @@ type WorkerScript struct {
 	UsageModel string `json:"usage_model,omitempty"`
 }
 
+type WorkersTailConsumer struct {
+	Service     string  `json:"service"`
+	Environment *string `json:"environment,omitempty"`
+}
+
 // WorkerMetaData contains worker script information such as size, creation & modification dates.
 type WorkerMetaData struct {
-	ID               string         `json:"id,omitempty"`
-	ETAG             string         `json:"etag,omitempty"`
-	Size             int            `json:"size,omitempty"`
-	CreatedOn        time.Time      `json:"created_on,omitempty"`
-	ModifiedOn       time.Time      `json:"modified_on,omitempty"`
-	Logpush          *bool          `json:"logpush,omitempty"`
-	LastDeployedFrom *string        `json:"last_deployed_from,omitempty"`
-	DeploymentId     *string        `json:"deployment_id,omitempty"`
-	PlacementMode    *PlacementMode `json:"placement_mode,omitempty"`
+	ID               string                 `json:"id,omitempty"`
+	ETAG             string                 `json:"etag,omitempty"`
+	Size             int                    `json:"size,omitempty"`
+	CreatedOn        time.Time              `json:"created_on,omitempty"`
+	ModifiedOn       time.Time              `json:"modified_on,omitempty"`
+	Logpush          *bool                  `json:"logpush,omitempty"`
+	TailConsumers    *[]WorkersTailConsumer `json:"tail_consumers,omitempty"`
+	LastDeployedFrom *string                `json:"last_deployed_from,omitempty"`
+	DeploymentId     *string                `json:"deployment_id,omitempty"`
+	PlacementMode    *PlacementMode         `json:"placement_mode,omitempty"`
 }
 
 // WorkerListResponse wrapper struct for API response to worker script list API call.
@@ -251,7 +264,7 @@ func (api *API) UploadWorker(ctx context.Context, rc *ResourceContainer, params
 		err         error
 	)
 
-	if params.Module || params.Logpush != nil || params.Placement != nil || len(params.Bindings) > 0 || params.CompatibilityDate != "" || len(params.CompatibilityFlags) > 0 {
+	if params.Module || params.Logpush != nil || params.Placement != nil || len(params.Bindings) > 0 || params.CompatibilityDate != "" || len(params.CompatibilityFlags) > 0 || params.TailConsumers != nil {
 		contentType, body, err = formatMultipartBody(params)
 		if err != nil {
 			return WorkerScriptResponse{}, err
@@ -285,16 +298,18 @@ func formatMultipartBody(params CreateWorkerParams) (string, []byte, error) {
 	// Write metadata part
 	var scriptPartName string
 	meta := struct {
-		BodyPart           string              `json:"body_part,omitempty"`
-		MainModule         string              `json:"main_module,omitempty"`
-		Bindings           []workerBindingMeta `json:"bindings"`
-		Logpush            *bool               `json:"logpush,omitempty"`
-		CompatibilityDate  string              `json:"compatibility_date,omitempty"`
-		CompatibilityFlags []string            `json:"compatibility_flags,omitempty"`
-		Placement          *Placement          `json:"placement,omitempty"`
+		BodyPart           string                 `json:"body_part,omitempty"`
+		MainModule         string                 `json:"main_module,omitempty"`
+		Bindings           []workerBindingMeta    `json:"bindings"`
+		Logpush            *bool                  `json:"logpush,omitempty"`
+		TailConsumers      *[]WorkersTailConsumer `json:"tail_consumers,omitempty"`
+		CompatibilityDate  string                 `json:"compatibility_date,omitempty"`
+		CompatibilityFlags []string               `json:"compatibility_flags,omitempty"`
+		Placement          *Placement             `json:"placement,omitempty"`
 	}{
 		Bindings:           make([]workerBindingMeta, 0, len(params.Bindings)),
 		Logpush:            params.Logpush,
+		TailConsumers:      params.TailConsumers,
 		CompatibilityDate:  params.CompatibilityDate,
 		CompatibilityFlags: params.CompatibilityFlags,
 		Placement:          params.Placement,


Test patch:
diff --git a/workers_test.go b/workers_test.go
index 8492cdee0..1a48bf4b0 100644
--- a/workers_test.go
+++ b/workers_test.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -21,29 +22,7 @@ const (
     "errors": [],
     "messages": []
 }`
-	uploadWorkerResponseData = `{
-    "result": {
-        "script": "addEventListener('fetch', event => {\n  event.passThroughOnException()\n  event.respondWith(handleRequest(event.request))\n})\n\nasync function handleRequest(request) {\n  return fetch(request)\n}",
-        "etag": "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-        "size": 191,
-        "modified_on": "2018-06-09T15:17:01.989141Z"
-    },
-    "success": true,
-    "errors": [],
-    "messages": []
-}`
 
-	uploadWorkerModuleResponseData = `{
-    "result": {
-        "script": "export default {\n  async fetch(request, env, event) {\n    event.passThroughOnException()\n    return fetch(request)\n  }\n}",
-        "etag": "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-        "size": 191,
-        "modified_on": "2018-06-09T15:17:01.989141Z"
-    },
-    "success": true,
-    "errors": [],
-    "messages": []
-}`
 	updateWorkerRouteResponse = `{
     "result": {
         "id": "e7a57d8746e74ae49c25994dadb421b1",
@@ -211,18 +190,6 @@ export default {
 }
 --workermodulescriptdownload--
 `
-	uploadModuleWorkerSmartPlacement = `{
-    "result": {
-        "script": "export default {\n  async fetch(request, env, event) {\n    event.passThroughOnException()\n    return fetch(request)\n  }\n}",
-        "etag": "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-        "size": 191,
-        "modified_on": "2018-06-09T15:17:01.989141Z",
-		"placement_mode": "smart"
-    },
-    "success": true,
-    "errors": [],
-    "messages": []
-}`
 )
 
 var (
@@ -242,6 +209,125 @@ var (
 }`, testZoneID)
 )
 
+type (
+	WorkersTestScriptResponse struct {
+		Script            string                 `json:"script"`
+		UsageModel        string                 `json:"usage_model,omitempty"`
+		Handlers          []string               `json:"handlers"`
+		ID                string                 `json:"id,omitempty"`
+		ETAG              string                 `json:"etag,omitempty"`
+		Size              uint                   `json:"size,omitempty"`
+		CreatedOn         string                 `json:"created_on,omitempty"`
+		ModifiedOn        string                 `json:"modified_on,omitempty"`
+		LastDeployedFrom  *string                `json:"last_deployed_from,omitempty"`
+		DeploymentId      *string                `json:"deployment_id,omitempty"`
+		CompatibilityDate *string                `json:"compatibility_date,omitempty"`
+		Logpush           *bool                  `json:"logpush,omitempty"`
+		TailConsumers     *[]WorkersTailConsumer `json:"tail_consumers,omitempty"`
+		PlacementMode     *string                `json:"placement_mode,omitempty"`
+	}
+	workersTestResponseOpt func(r *WorkersTestScriptResponse)
+)
+
+var (
+	expectedWorkersServiceWorkerScript = "addEventListener('fetch', event => {\n  event.passThroughOnException()\n  event.respondWith(handleRequest(event.request))\n})\n\nasync function handleRequest(request) {\n  return fetch(request)\n}"
+	expectedWorkersModuleWorkerScript  = "export default {\n  async fetch(request, env, event) {\n    event.passThroughOnException()\n    return fetch(request)\n  }\n}"
+	WorkersDefaultTestResponse         = WorkersTestScriptResponse{
+		Script:            expectedWorkersServiceWorkerScript,
+		Handlers:          []string{"fetch"},
+		UsageModel:        "unbound",
+		ID:                "e7a57d8746e74ae49c25994dadb421b1",
+		ETAG:              "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
+		Size:              191,
+		LastDeployedFrom:  StringPtr("dash"),
+		Logpush:           BoolPtr(false),
+		CompatibilityDate: StringPtr("2022-07-12"),
+	}
+)
+
+//nolint:unused
+func withWorkerScript(content string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.Script = content }
+}
+
+//nolint:unused
+func withWorkerUsageModel(um string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.UsageModel = um }
+}
+
+//nolint:unused
+func withWorkerHandlers(h []string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.Handlers = h }
+}
+
+//nolint:unused
+func withWorkerID(id string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.ID = id }
+}
+
+//nolint:unused
+func withWorkerEtag(etag string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.ETAG = etag }
+}
+
+//nolint:unused
+func withWorkerSize(size uint) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.Size = size }
+}
+
+//nolint:unused
+func withWorkerCreatedOn(co time.Time) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.CreatedOn = co.Format(time.RFC3339Nano) }
+}
+
+//nolint:unused
+func withWorkerModifiedOn(mo time.Time) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.ModifiedOn = mo.Format(time.RFC3339Nano) }
+}
+
+//nolint:unused
+func withWorkerLogpush(logpush *bool) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.Logpush = logpush }
+}
+
+//nolint:unused
+func withWorkerPlacementMode(mode *string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.PlacementMode = mode }
+}
+
+//nolint:unused
+func withWorkerTailConsumers(consumers ...WorkersTailConsumer) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.TailConsumers = &consumers }
+}
+
+//nolint:unused
+func withWorkerLastDeployedFrom(from *string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.LastDeployedFrom = from }
+}
+
+//nolint:unused
+func withWorkerDeploymentId(dID *string) workersTestResponseOpt {
+	return func(r *WorkersTestScriptResponse) { r.DeploymentId = dID }
+}
+
+func workersScriptResponse(t testing.TB, opts ...workersTestResponseOpt) string {
+	var responseConfig = WorkersDefaultTestResponse
+	for _, opt := range opts {
+		opt(&responseConfig)
+	}
+
+	bytes, err := json.Marshal(struct {
+		Response
+		Result WorkersTestScriptResponse `json:"result"`
+	}{
+		Response: Response{Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}},
+		Result:   responseConfig,
+	})
+	require.NoError(t, err)
+
+	return string(bytes)
+}
+
 func getFormValue(r *http.Request, key string) ([]byte, error) {
 	err := r.ParseMultipartForm(1024 * 1024)
 	if err != nil {
@@ -439,24 +525,28 @@ func TestUploadWorker_Basic(t *testing.T) {
 	setup()
 	defer teardown()
 
+	formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
 		contentTypeHeader := r.Header.Get("content-type")
 		assert.Equal(t, "application/javascript", contentTypeHeader, "Expected content-type request header to be 'application/javascript', got %s", contentTypeHeader)
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t, withWorkerModifiedOn(formattedTime)))
 	})
 	res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerScript})
-	formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
 	want := WorkerScriptResponse{
 		successResponse,
 		false,
 		WorkerScript{
-			Script: workerScript,
+			Script:     workerScript,
+			UsageModel: "unbound",
 			WorkerMetaData: WorkerMetaData{
-				ETAG:       "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-				Size:       191,
-				ModifiedOn: formattedTime,
+				ID:               "e7a57d8746e74ae49c25994dadb421b1",
+				ETAG:             "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
+				Size:             191,
+				ModifiedOn:       formattedTime,
+				Logpush:          BoolPtr(false),
+				LastDeployedFrom: StringPtr("dash"),
 			},
 		}}
 	if assert.NoError(t, err) {
@@ -468,6 +558,7 @@ func TestUploadWorker_Module(t *testing.T) {
 	setup()
 	defer teardown()
 
+	formattedCreatedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", func(w http.ResponseWriter, r *http.Request) {
 		mpUpload, err := parseMultipartUpload(r)
 		assert.NoError(t, err)
@@ -483,19 +574,22 @@ func TestUploadWorker_Module(t *testing.T) {
 		assert.Equal(t, expectedContentType, contentTypeHeader, "Expected content-type request header to be %s, got %s", expectedContentType, contentTypeHeader)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerModuleResponseData)
+		fmt.Fprint(w, workersScriptResponse(t, withWorkerScript(expectedWorkersModuleWorkerScript), withWorkerCreatedOn(formattedCreatedTime)))
 	})
 	res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerModuleScript, Module: true})
-	formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
 	want := WorkerScriptResponse{
-		successResponse,
-		false,
-		WorkerScript{
-			Script: workerModuleScript,
+		Response: successResponse,
+		Module:   false,
+		WorkerScript: WorkerScript{
+			Script:     workerModuleScript,
+			UsageModel: "unbound",
 			WorkerMetaData: WorkerMetaData{
-				ETAG:       "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-				Size:       191,
-				ModifiedOn: formattedTime,
+				ID:               "e7a57d8746e74ae49c25994dadb421b1",
+				ETAG:             "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
+				Size:             191,
+				CreatedOn:        formattedCreatedTime,
+				Logpush:          BoolPtr(false),
+				LastDeployedFrom: StringPtr("dash"),
 			},
 		}}
 	if assert.NoError(t, err) {
@@ -525,7 +619,7 @@ func TestUploadWorker_WithDurableObjectBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
@@ -548,6 +642,7 @@ func TestUploadWorker_WithInheritBinding(t *testing.T) {
 	setup()
 	defer teardown()
 
+	formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
 	// Setup route handler for both single-script and multi-script
 	handler := func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
@@ -570,20 +665,23 @@ func TestUploadWorker_WithInheritBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t, withWorkerModifiedOn(formattedTime)))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
-	formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
 	want := WorkerScriptResponse{
-		successResponse,
-		false,
-		WorkerScript{
-			Script: workerScript,
+		Response: successResponse,
+		Module:   false,
+		WorkerScript: WorkerScript{
+			Script:     workerScript,
+			UsageModel: "unbound",
 			WorkerMetaData: WorkerMetaData{
-				ETAG:       "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-				Size:       191,
-				ModifiedOn: formattedTime,
+				ID:               "e7a57d8746e74ae49c25994dadb421b1",
+				ETAG:             "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
+				Size:             191,
+				ModifiedOn:       formattedTime,
+				Logpush:          BoolPtr(false),
+				LastDeployedFrom: StringPtr("dash"),
 			},
 		}}
 
@@ -622,7 +720,7 @@ func TestUploadWorker_WithKVBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -663,7 +761,7 @@ func TestUploadWorker_WithWasmBinding(t *testing.T) {
 		assert.Equal(t, []byte("fake-wasm"), wasmContent)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -701,7 +799,7 @@ func TestUploadWorker_WithPlainTextBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -747,7 +845,7 @@ func TestUploadWorker_ModuleWithPlainTextBinding(t *testing.T) {
 		assert.Equal(t, expectedContentDisposition, contentDispositonHeader, "Expected content-disposition request header to be %s, got %s", expectedContentDisposition, contentDispositonHeader)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerModuleResponseData)
+		fmt.Fprint(w, workersScriptResponse(t, withWorkerScript(expectedWorkersModuleWorkerScript)))
 	})
 
 	_, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
@@ -785,7 +883,7 @@ func TestUploadWorker_WithSecretTextBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -828,7 +926,7 @@ func TestUploadWorker_WithServiceBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -852,6 +950,10 @@ func TestUploadWorker_WithLogpush(t *testing.T) {
 	setup()
 	defer teardown()
 
+	var (
+		formattedTime, _ = time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
+		logpush          = BoolPtr(true)
+	)
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/foo", func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
 		mpUpload, err := parseMultipartUpload(r)
@@ -861,19 +963,22 @@ func TestUploadWorker_WithLogpush(t *testing.T) {
 		assert.Equal(t, &expected, mpUpload.Logpush)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t, withWorkerScript(expectedWorkersModuleWorkerScript), withWorkerLogpush(logpush), withWorkerModifiedOn(formattedTime)))
 	})
-	res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerScript, Logpush: BoolPtr(true)})
-	formattedTime, _ := time.Parse(time.RFC3339Nano, "2018-06-09T15:17:01.989141Z")
+	res, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{ScriptName: "foo", Script: workerScript, Logpush: logpush})
 	want := WorkerScriptResponse{
-		successResponse,
-		false,
-		WorkerScript{
-			Script: workerScript,
+		Response: successResponse,
+		Module:   false,
+		WorkerScript: WorkerScript{
+			Script:     expectedWorkersModuleWorkerScript,
+			UsageModel: "unbound",
 			WorkerMetaData: WorkerMetaData{
-				ETAG:       "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
-				Size:       191,
-				ModifiedOn: formattedTime,
+				ID:               "e7a57d8746e74ae49c25994dadb421b1",
+				ETAG:             "279cf40d86d70b82f6cd3ba90a646b3ad995912da446836d7371c21c6a43977a",
+				Size:             191,
+				ModifiedOn:       formattedTime,
+				Logpush:          logpush,
+				LastDeployedFrom: StringPtr("dash"),
 			},
 		}}
 	if assert.NoError(t, err) {
@@ -899,7 +1004,7 @@ func TestUploadWorker_WithCompatibilityFlags(t *testing.T) {
 		assert.Equal(t, compatibilityFlags, mpUpload.CompatibilityFlags)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -933,7 +1038,7 @@ func TestUploadWorker_WithQueueBinding(t *testing.T) {
 		assert.Equal(t, expectedBindings, mpUpload.BindingMeta)
 
 		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, uploadWorkerResponseData)
+		fmt.Fprint(w, workersScriptResponse(t))
 	}
 	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
 
@@ -954,7 +1059,7 @@ func TestUploadWorker_WithSmartPlacementEnabled(t *testing.T) {
 	defer teardown()
 
 	placementMode := PlacementModeSmart
-	response := uploadModuleWorkerSmartPlacement
+	response := workersScriptResponse(t, withWorkerScript(expectedWorkersModuleWorkerScript), withWorkerPlacementMode(StringPtr("smart")))
 
 	handler := func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
@@ -983,7 +1088,7 @@ func TestUploadWorker_WithSmartPlacementEnabled(t *testing.T) {
 
 	t.Run("Test disabling placement", func(t *testing.T) {
 		placementMode = PlacementModeOff
-		response = uploadWorkerModuleResponseData
+		response = workersScriptResponse(t, withWorkerScript(expectedWorkersModuleWorkerScript))
 
 		worker, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
 			ScriptName: "bar",
@@ -996,3 +1101,42 @@ func TestUploadWorker_WithSmartPlacementEnabled(t *testing.T) {
 		assert.Nil(t, worker.PlacementMode)
 	})
 }
+
+func TestUploadWorker_WithTailConsumers(t *testing.T) {
+	setup()
+	defer teardown()
+
+	response := workersScriptResponse(t,
+		withWorkerScript(expectedWorkersModuleWorkerScript))
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
+
+		mpUpload, err := parseMultipartUpload(r)
+		assert.NoError(t, err)
+
+		assert.Equal(t, workerScript, mpUpload.Script)
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, response)
+	}
+	mux.HandleFunc("/accounts/"+testAccountID+"/workers/scripts/bar", handler)
+
+	t.Run("adds tail consumers", func(t *testing.T) {
+		tailConsumers := []WorkersTailConsumer{
+			{Service: "my-service-a"},
+			{Service: "my-service-b", Environment: StringPtr("production")},
+		}
+		response = workersScriptResponse(t,
+			withWorkerScript(expectedWorkersModuleWorkerScript),
+			withWorkerTailConsumers(tailConsumers...))
+
+		worker, err := client.UploadWorker(context.Background(), AccountIdentifier(testAccountID), CreateWorkerParams{
+			ScriptName:    "bar",
+			Script:        workerScript,
+			TailConsumers: &tailConsumers,
+		})
+		assert.NoError(t, err)
+		require.NotNil(t, worker.TailConsumers)
+		assert.Len(t, *worker.TailConsumers, 2)
+	})
+}