Repository: amp-labs/connectors
Pull request #1097 (2024-10-23 17:07:03)

feat: CustomerJourneysApp (Write)
# Description

There is a difference in support among objects between Create/Update/Delete. 
2 objects are made up mapping to URL path:
`customer_exports`  refers to "/exports/customers";
`deliveries_exports` refers to "/exports/deliveries".

Response key holding record of interest differes among objects and such exceptions are also recorded, for more refer to `objectNames.go`.

# Test results

![image](https://github.com/user-attachments/assets/7a460189-20c0-4045-9249-ffc87423cf21)


Cobalt0s: > [!WARNING]
> <b>This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1097?utm_source=stack-comment-downstack-mergeability-warning" >on Graphite</a>.</b>
> <a href="https://graphite.dev/docs/merge-pull-requests">Learn more</a>

* **#1097** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1097?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈
* **#1095** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1095?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1094** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1094?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1078** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1078?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#1077** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/1077?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `main`

This stack of pull requests is managed by Graphite. <a href="https://stacking.dev/?utm_source=stack-comment">Learn more about stacking.</a>
<h2></h2>

 Join @Cobalt0s and the rest of your teammates on <a href="https://graphite.dev?utm-source=stack-comment"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="11px" height="11px"/> <b>Graphite</b></a>
  <!-- Current dependencies on/for this PR: -->

Patch:
diff --git a/providers/customerapp/connector.go b/providers/customerapp/connector.go
index 57829913..e8177de5 100644
--- a/providers/customerapp/connector.go
+++ b/providers/customerapp/connector.go
@@ -43,7 +43,7 @@ func NewConnector(opts ...Option) (conn *Connector, outErr error) {
 	// connector and its client must mirror base url and provide its own error parser
 	conn.setBaseURL(providerInfo.BaseURL)
 	conn.Client.HTTPClient.ErrorHandler = interpreter.ErrorHandler{
-		JSON: interpreter.NewFaultyResponder(errorFormats, nil),
+		JSON: interpreter.NewFaultyResponder(errorFormats, statusCodeMapping),
 	}.Handle
 
 	return conn, nil
diff --git a/providers/customerapp/delete.go b/providers/customerapp/delete.go
new file mode 100644
index 00000000..4959633c
--- /dev/null
+++ b/providers/customerapp/delete.go
@@ -0,0 +1,34 @@
+package customerapp
+
+import (
+	"context"
+
+	"github.com/amp-labs/connectors/common"
+)
+
+func (c *Connector) Delete(ctx context.Context, config common.DeleteParams) (*common.DeleteResult, error) {
+	if err := config.ValidateParams(); err != nil {
+		return nil, err
+	}
+
+	if !supportedObjectsByDelete.Has(config.ObjectName) {
+		return nil, common.ErrOperationNotSupportedForObject
+	}
+
+	url, err := c.getURL(config.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	url.AddPath(config.RecordId)
+
+	// 200 OK is expected
+	_, err = c.Client.Delete(ctx, url.String())
+	if err != nil {
+		return nil, err
+	}
+
+	return &common.DeleteResult{
+		Success: true,
+	}, nil
+}
diff --git a/providers/customerapp/errors.go b/providers/customerapp/errors.go
index c863cd3c..9cb04201 100644
--- a/providers/customerapp/errors.go
+++ b/providers/customerapp/errors.go
@@ -2,8 +2,10 @@ package customerapp
 
 import (
 	"fmt"
+	"net/http"
 	"strings"
 
+	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/interpreter"
 )
 
@@ -16,6 +18,10 @@ var errorFormats = interpreter.NewFormatSwitch( // nolint:gochecknoglobals
 	}...,
 )
 
+var statusCodeMapping = map[int]error{ // nolint:gochecknoglobals
+	http.StatusUnprocessableEntity: common.ErrBadRequest,
+}
+
 type ResponseError struct {
 	Errors []ErrorDetails `json:"errors"`
 }
diff --git a/providers/customerapp/objectNames.go b/providers/customerapp/objectNames.go
index 709102a4..95267c93 100644
--- a/providers/customerapp/objectNames.go
+++ b/providers/customerapp/objectNames.go
@@ -1,20 +1,71 @@
 package customerapp
 
 import (
+	"github.com/amp-labs/connectors/common/naming"
 	"github.com/amp-labs/connectors/internal/datautils"
 	"github.com/amp-labs/connectors/providers/customerapp/metadata"
 )
 
+const (
+	objectNameCollections       = "collections"
+	objectNameImports           = "imports"
+	objectNameReportingWebhooks = "reporting_webhooks"
+	objectNameSegments          = "segments"
+	objectNameSnippets          = "snippets"
+	objectNameCustomerExports   = "customer_exports"
+	objectNameDeliveriesExports = "deliveries_exports"
+	objectNameNewsletters       = "newsletters"
+)
+
 // Supported object names can be found under schemas.json.
 var supportedObjectsByRead = metadata.Schemas.ObjectNames() //nolint:gochecknoglobals
 
-// ObjectNameToResponseField maps ObjectName to the response field name which contains that object.
-var ObjectNameToResponseField = datautils.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
-	"object_types":        "types",
-	"transactional":       "messages",
-	"subscription_topics": "topics",
+var supportedObjectsByCreate = datautils.NewSet( //nolint:gochecknoglobals
+	objectNameCollections,
+	objectNameImports,
+	objectNameReportingWebhooks,
+	objectNameSegments,
+	objectNameSnippets, // create via PUT, which is also an update
+	objectNameCustomerExports,
+	objectNameDeliveriesExports,
+)
+
+var supportedObjectsByUpdate = datautils.NewSet( //nolint:gochecknoglobals
+	objectNameCollections,
+	objectNameReportingWebhooks,
+)
+
+var supportedObjectsByDelete = datautils.NewSet( //nolint:gochecknoglobals
+	objectNameCollections,
+	objectNameNewsletters,
+	objectNameReportingWebhooks,
+	objectNameSegments,
+	objectNameSnippets,
+)
+
+// ObjectNameToWritePath maps ObjectName to URL path used for Write operation.
+var ObjectNameToWritePath = datautils.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
+	objectNameCustomerExports:   "exports/customers",
+	objectNameDeliveriesExports: "exports/deliveries",
+},
+	func(objectName string) (jsonPath string) {
+		return objectName
+	},
+)
+
+// ObjectNameToWriteResponseField maps ObjectName to the write response field names that hold the object.
+var ObjectNameToWriteResponseField = datautils.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
+	objectNameCollections:       "collection",
+	objectNameImports:           "import",
+	objectNameReportingWebhooks: "", // object is not nested, response body is a webhook object
+	objectNameSegments:          "segment",
+	objectNameSnippets:          "snippet",
+	objectNameCustomerExports:   "export",
+	objectNameDeliveriesExports: "export",
 },
-	func(key string) string {
-		return key
+	func(objectName string) string {
+		// The general pattern is response is stored under object name turned into singular form.
+		// This is a fallback, although the list above is exhaustive.
+		return naming.NewSingularString(objectName).String()
 	},
 )
diff --git a/providers/customerapp/read.go b/providers/customerapp/read.go
index 4d13536b..ca32d8c7 100644
--- a/providers/customerapp/read.go
+++ b/providers/customerapp/read.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/providers/customerapp/metadata"
 )
 
 func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common.ReadResult, error) {
@@ -25,7 +26,7 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		return nil, err
 	}
 
-	responseFieldName := ObjectNameToResponseField.Get(config.ObjectName)
+	responseFieldName := metadata.Schemas.LookupArrayFieldName(c.Module.ID, config.ObjectName)
 
 	return common.ParseResult(res,
 		common.GetOptionalRecordsUnderJSONPath(responseFieldName),
diff --git a/providers/customerapp/write.go b/providers/customerapp/write.go
new file mode 100644
index 00000000..60bbb45e
--- /dev/null
+++ b/providers/customerapp/write.go
@@ -0,0 +1,86 @@
+package customerapp
+
+import (
+	"context"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/jsonquery"
+	"github.com/spyzhov/ajson"
+)
+
+func (c *Connector) Write(ctx context.Context, config common.WriteParams) (*common.WriteResult, error) {
+	if err := config.ValidateParams(); err != nil {
+		return nil, err
+	}
+
+	path := ObjectNameToWritePath.Get(config.ObjectName)
+
+	url, err := c.getURL(path)
+	if err != nil {
+		return nil, err
+	}
+
+	var write common.WriteMethod
+
+	if len(config.RecordId) == 0 {
+		if !supportedObjectsByCreate.Has(config.ObjectName) {
+			return nil, common.ErrOperationNotSupportedForObject
+		}
+
+		write = c.Client.Post
+		if config.ObjectName == objectNameSnippets {
+			// https://docs.customer.io/api/app/#operation/listSnippets
+			// Snippets are create and updated via PUT.
+			write = c.Client.Put
+		}
+	} else {
+		if !supportedObjectsByUpdate.Has(config.ObjectName) {
+			return nil, common.ErrOperationNotSupportedForObject
+		}
+
+		write = c.Client.Put
+
+		url.AddPath(config.RecordId)
+	}
+
+	res, err := write(ctx, url.String(), config.RecordData)
+	if err != nil {
+		return nil, err
+	}
+
+	body, ok := res.Body()
+	if !ok {
+		return &common.WriteResult{
+			Success: true,
+		}, nil
+	}
+
+	// write response was with payload
+	return constructWriteResult(config, body)
+}
+
+func constructWriteResult(config common.WriteParams, body *ajson.Node) (*common.WriteResult, error) {
+	fieldName := ObjectNameToWriteResponseField.Get(config.ObjectName)
+
+	nested, err := jsonquery.New(body).Object(fieldName, false)
+	if err != nil {
+		return nil, err
+	}
+
+	recordID, err := jsonquery.New(nested).TextWithDefault("id", "")
+	if err != nil {
+		return nil, err
+	}
+
+	data, err := jsonquery.Convertor.ObjectToMap(nested)
+	if err != nil {
+		return nil, err
+	}
+
+	return &common.WriteResult{
+		Success:  true,
+		RecordId: recordID,
+		Errors:   nil,
+		Data:     data,
+	}, nil
+}
diff --git a/scripts/openapi/customerio/journeysapp/metadata/main.go b/scripts/openapi/customerio/journeysapp/metadata/main.go
index 793e8d41..ed5877cc 100644
--- a/scripts/openapi/customerio/journeysapp/metadata/main.go
+++ b/scripts/openapi/customerio/journeysapp/metadata/main.go
@@ -7,7 +7,6 @@ import (
 	"github.com/amp-labs/connectors/internal/datautils"
 	"github.com/amp-labs/connectors/internal/goutils"
 	"github.com/amp-labs/connectors/internal/staticschema"
-	"github.com/amp-labs/connectors/providers/customerapp"
 	"github.com/amp-labs/connectors/providers/customerapp/metadata"
 	"github.com/amp-labs/connectors/providers/customerapp/openapi"
 	"github.com/amp-labs/connectors/tools/fileconv/api3"
@@ -22,6 +21,15 @@ var (
 	displayNameOverride = map[string]string{ // nolint:gochecknoglobals
 
 	}
+	objectNameToReadResponseField = datautils.NewDefaultMap(map[string]string{ //nolint:gochecknoglobals
+		"object_types":        "types",
+		"transactional":       "messages",
+		"subscription_topics": "topics",
+	},
+		func(objectName string) (fieldName string) {
+			return objectName
+		},
+	)
 )
 
 func main() {
@@ -35,7 +43,7 @@ func main() {
 	objects, err := explorer.ReadObjectsGet(
 		api3.NewDenyPathStrategy(ignoreEndpoints),
 		nil, displayNameOverride,
-		api3.CustomMappingObjectCheck(customerapp.ObjectNameToResponseField),
+		api3.CustomMappingObjectCheck(objectNameToReadResponseField),
 	)
 	goutils.MustBeNil(err)
 


Test patch:
diff --git a/providers/customerapp/delete_test.go b/providers/customerapp/delete_test.go
new file mode 100644
index 00000000..2a90ad1f
--- /dev/null
+++ b/providers/customerapp/delete_test.go
@@ -0,0 +1,80 @@
+package customerapp
+
+import (
+	"errors"
+	"net/http"
+	"testing"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockcond"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
+	"github.com/amp-labs/connectors/test/utils/testroutines"
+	"github.com/amp-labs/connectors/test/utils/testutils"
+)
+
+func TestDelete(t *testing.T) { // nolint:funlen,cyclop
+	t.Parallel()
+
+	errorNotFound := testutils.DataFromFile(t, "delete-not-found.json")
+
+	tests := []testroutines.Delete{
+		{
+			Name:         "Delete object must be included",
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingObjects},
+		},
+		{
+			Name:         "Write object and its ID must be included",
+			Input:        common.DeleteParams{ObjectName: "reporting_webhooks"},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingRecordID},
+		},
+		{
+			Name:   "Object name is not supported",
+			Input:  common.DeleteParams{ObjectName: "customer_exports", RecordId: "95049"},
+			Server: mockserver.Dummy(),
+			ExpectedErrs: []error{
+				common.ErrOperationNotSupportedForObject,
+			},
+		},
+		{
+			Name:  "Successful delete",
+			Input: common.DeleteParams{ObjectName: "reporting_webhooks", RecordId: "95049"},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If: mockcond.And{
+					mockcond.PathSuffix("/v1/reporting_webhooks/95049"),
+					mockcond.MethodDELETE(),
+				},
+				Then: mockserver.Response(http.StatusNoContent),
+			}.Server(),
+			Expected:     &common.DeleteResult{Success: true},
+			ExpectedErrs: nil,
+		},
+		{
+			Name:  "Error on deleting missing record",
+			Input: common.DeleteParams{ObjectName: "reporting_webhooks", RecordId: "95049"},
+			Server: mockserver.Fixed{
+				Setup:  mockserver.ContentJSON(),
+				Always: mockserver.Response(http.StatusNotFound, errorNotFound),
+			}.Server(),
+			ExpectedErrs: []error{
+				common.ErrBadRequest,
+				errors.New( // nolint:goerr113
+					"not found (reference 01JCGC85CF663RT1V3FA04ZBNK)",
+				),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.Name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.Run(t, func() (connectors.DeleteConnector, error) {
+				return constructTestConnector(tt.Server.URL)
+			})
+		})
+	}
+}
diff --git a/providers/customerapp/test/README.md b/providers/customerapp/test/README.md
new file mode 100644
index 00000000..40637a8d
--- /dev/null
+++ b/providers/customerapp/test/README.md
@@ -0,0 +1,51 @@
+# Description
+
+This is a list of operations that can be performed on the objects using non-GET REST methods.
+
+# Write
+
+[collections POST](https://docs.customer.io/api/app/#operation/addCollection)
+
+[collection DELETE](https://docs.customer.io/api/app/#operation/deleteCollection)
+
+[collection PUT](https://docs.customer.io/api/app/#operation/updateCollection)
+
+-------------------
+
+[imports POST](https://docs.customer.io/api/app/#operation/import)
+
+-------------------
+
+[newsletter DELETE](https://docs.customer.io/api/app/#operation/deletetNewsletters)
+
+-------------------
+
+[reporting_webhooks POST](https://docs.customer.io/api/app/#operation/createWebhook)
+
+[reporting_webhooks PUT](https://docs.customer.io/api/app/#operation/updateWebhook)
+
+[reporting_webhooks DELETE](https://docs.customer.io/api/app/#operation/deleteWebhook)
+
+-------------------
+
+[segments POST](https://docs.customer.io/api/app/#operation/createManSegment)
+
+[segments DELETE](https://docs.customer.io/api/app/#operation/deleteManSegment)
+
+-------------------
+
+[snippets (create/update) PUT](https://docs.customer.io/api/app/#operation/updateSnippets)
+
+[snippet DELETE](https://docs.customer.io/api/app/#operation/deleteSnippet)
+
+
+## Self made objects
+
+[customer_exports POST](https://docs.customer.io/api/app/#operation/exportPeopleData)
+
+[deliveries_exports POST](https://docs.customer.io/api/app/#operation/exportDeliveriesData)
+
+## READ
+
+Special GET:
+[Search customers](https://docs.customer.io/api/app/#operation/getPeopleFilter)
diff --git a/providers/customerapp/test/delete-not-found.json b/providers/customerapp/test/delete-not-found.json
new file mode 100644
index 00000000..650f04da
--- /dev/null
+++ b/providers/customerapp/test/delete-not-found.json
@@ -0,0 +1,8 @@
+{
+  "errors": [
+    {
+      "detail": "not found (reference 01JCGC85CF663RT1V3FA04ZBNK)",
+      "status": "404"
+    }
+  ]
+}
diff --git a/providers/customerapp/test/write-segment-bad-request.json b/providers/customerapp/test/write-segment-bad-request.json
new file mode 100644
index 00000000..b95f6f3b
--- /dev/null
+++ b/providers/customerapp/test/write-segment-bad-request.json
@@ -0,0 +1,11 @@
+{
+  "errors": [
+    {
+      "detail": "Name can't be blank",
+      "source": {
+        "pointer": "/data/attributes/name"
+      },
+      "status": "422"
+    }
+  ]
+}
diff --git a/providers/customerapp/test/write-segment.json b/providers/customerapp/test/write-segment.json
new file mode 100644
index 00000000..83cf27f9
--- /dev/null
+++ b/providers/customerapp/test/write-segment.json
@@ -0,0 +1,12 @@
+{
+  "segment": {
+    "id": 16,
+    "deduplicate_id": "16:1731421925",
+    "name": "finished",
+    "description": "a segment that will be removed",
+    "state": "finished",
+    "progress": null,
+    "type": "manual",
+    "tags": null
+  }
+}
diff --git a/providers/customerapp/write_test.go b/providers/customerapp/write_test.go
new file mode 100644
index 00000000..422decf1
--- /dev/null
+++ b/providers/customerapp/write_test.go
@@ -0,0 +1,99 @@
+package customerapp
+
+import (
+	"errors"
+	"net/http"
+	"testing"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockcond"
+	"github.com/amp-labs/connectors/test/utils/mockutils/mockserver"
+	"github.com/amp-labs/connectors/test/utils/testroutines"
+	"github.com/amp-labs/connectors/test/utils/testutils"
+)
+
+func TestWrite(t *testing.T) { // nolint:funlen,cyclop
+	t.Parallel()
+
+	errorSegmentBadRequest := testutils.DataFromFile(t, "write-segment-bad-request.json")
+	responseSegment := testutils.DataFromFile(t, "write-segment.json")
+
+	tests := []testroutines.Write{
+		{
+			Name:         "Write object must be included",
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingObjects},
+		},
+		{
+			Name:         "Write needs data payload",
+			Input:        common.WriteParams{ObjectName: "segments"},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingRecordData},
+		},
+		{
+			Name:  "Error on invalid create payload",
+			Input: common.WriteParams{ObjectName: "segments", RecordData: "dummy"},
+			Server: mockserver.Fixed{
+				Setup:  mockserver.ContentJSON(),
+				Always: mockserver.Response(http.StatusUnprocessableEntity, errorSegmentBadRequest),
+			}.Server(),
+			ExpectedErrs: []error{
+				common.ErrBadRequest,
+				errors.New( // nolint:goerr113
+					"Name can't be blank",
+				),
+			},
+		},
+		{
+			Name: "Write must act as an Update",
+			Input: common.WriteParams{
+				ObjectName: "collections",
+				RecordId:   "123",
+				RecordData: "dummy",
+			},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPUT(),
+				Then:  mockserver.Response(http.StatusOK),
+			}.Server(),
+			Expected:     &common.WriteResult{Success: true},
+			ExpectedErrs: nil,
+		},
+		{
+			Name:  "Valid creation of a segment",
+			Input: common.WriteParams{ObjectName: "segments", RecordData: "dummy"},
+			Server: mockserver.Conditional{
+				Setup: mockserver.ContentJSON(),
+				If:    mockcond.MethodPOST(),
+				Then:  mockserver.Response(http.StatusOK, responseSegment),
+			}.Server(),
+			Expected: &common.WriteResult{
+				Success:  true,
+				RecordId: "16",
+				Errors:   nil,
+				Data: map[string]any{
+					"id":             float64(16),
+					"deduplicate_id": "16:1731421925",
+					"name":           "finished",
+					"description":    "a segment that will be removed",
+					"state":          "finished",
+					"progress":       nil,
+					"type":           "manual",
+					"tags":           nil,
+				},
+			},
+			ExpectedErrs: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.Name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.Run(t, func() (connectors.WriteConnector, error) {
+				return constructTestConnector(tt.Server.URL)
+			})
+		})
+	}
+}
diff --git a/test/customerio/journeysapp/write-delete/main.go b/test/customerio/journeysapp/write-delete/main.go
new file mode 100644
index 00000000..0b331048
--- /dev/null
+++ b/test/customerio/journeysapp/write-delete/main.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"os/signal"
+	"syscall"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/providers/customerapp"
+	connTest "github.com/amp-labs/connectors/test/customerio/journeysapp"
+	"github.com/amp-labs/connectors/test/utils"
+	"github.com/amp-labs/connectors/test/utils/mockutils"
+)
+
+var objectName = "segments"
+
+type SegmentCreatePayload struct {
+	Segment Segment `json:"segment"`
+}
+
+type Segment struct {
+	Name        string `json:"name"`
+	Description string `json:"description"`
+}
+
+func main() {
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
+
+	// Set up slog logging.
+	utils.SetupLogging()
+
+	conn := connTest.GetCustomerJourneysAppConnector(ctx)
+
+	slog.Info("> TEST Create/Delete segments")
+	slog.Info("Creating segment")
+
+	description := "created from test, will be removed"
+	createSegment(ctx, conn, &SegmentCreatePayload{
+		Segment: Segment{
+			Name:        "finished",
+			Description: description,
+		},
+	})
+
+	slog.Info("Reading segments")
+
+	res := readSegments(ctx, conn)
+
+	slog.Info("Finding recently created segment")
+
+	segment := searchSegments(res, "description", description)
+	segmentID := fmt.Sprintf("%v", segment["id"])
+
+	slog.Info("Removing this segment")
+	removeSegment(ctx, conn, segmentID)
+	slog.Info("> Successful test completion")
+}
+
+func searchSegments(res *common.ReadResult, key, value string) map[string]any {
+	for _, data := range res.Data {
+		if mockutils.DoesObjectCorrespondToString(data.Fields[key], value) {
+			return data.Fields
+		}
+	}
+
+	utils.Fail("error finding segment")
+
+	return nil
+}
+
+func readSegments(ctx context.Context, conn *customerapp.Connector) *common.ReadResult {
+	res, err := conn.Read(ctx, common.ReadParams{
+		ObjectName: objectName,
+		Fields: connectors.Fields(
+			"id", "description",
+		),
+	})
+	if err != nil {
+		utils.Fail("error reading from Customer App", "error", err)
+	}
+
+	return res
+}
+
+func createSegment(ctx context.Context, conn *customerapp.Connector, payload *SegmentCreatePayload) {
+	res, err := conn.Write(ctx, common.WriteParams{
+		ObjectName: objectName,
+		RecordId:   "",
+		RecordData: payload,
+	})
+	if err != nil {
+		utils.Fail("error writing to Customer App", "error", err)
+	}
+
+	if !res.Success {
+		utils.Fail("failed to create a segment")
+	}
+}
+
+func removeSegment(ctx context.Context, conn *customerapp.Connector, segmentID string) {
+	res, err := conn.Delete(ctx, common.DeleteParams{
+		ObjectName: objectName,
+		RecordId:   segmentID,
+	})
+	if err != nil {
+		utils.Fail("error deleting for Customer App", "error", err)
+	}
+
+	if !res.Success {
+		utils.Fail("failed to remove a segment")
+	}
+}