Repository: amp-labs/connectors
Pull request #1031 (2024-09-10 15:02:38)

test: Salesforce Bulk Delete
# Description
Using real requests recorded Provider API output to create mock tests.

`BulkDelete` is very similiar to `BulkWrite` therefore created common `mock server` which is reused by both **TestSuites**.

Added validation to BulkDelete argument params. Also removed a comment that ExternalIdField is required (that is only true for **upserts**).


Patch:
diff --git a/providers/salesforce/bulk-delete.go b/providers/salesforce/bulk-delete.go
index d73fe45d..89630dad 100644
--- a/providers/salesforce/bulk-delete.go
+++ b/providers/salesforce/bulk-delete.go
@@ -2,6 +2,8 @@ package salesforce
 
 import (
 	"context"
+
+	"github.com/amp-labs/connectors/common"
 )
 
 // BulkDelete launches async Bulk Job to delete records.
@@ -12,6 +14,14 @@ import (
 // * GetJobResults
 // * GetSuccessfulJobResults.
 func (c *Connector) BulkDelete(ctx context.Context, params BulkOperationParams) (*BulkOperationResult, error) {
+	if len(params.ObjectName) == 0 {
+		return nil, common.ErrMissingObjects
+	}
+
+	if params.CSVData == nil {
+		return nil, common.ErrMissingCSVData
+	}
+
 	body := map[string]any{
 		"object":    params.ObjectName,
 		"operation": DeleteMode,
diff --git a/providers/salesforce/bulk.go b/providers/salesforce/bulk.go
index af292096..1b2294d3 100644
--- a/providers/salesforce/bulk.go
+++ b/providers/salesforce/bulk.go
@@ -45,7 +45,7 @@ type BulkOperationParams struct {
 	ObjectName string // required
 
 	// The name of a field on the object which is an External ID. Provided in the case of upserts, not inserts
-	ExternalIdField string // required
+	ExternalIdField string
 
 	// The path to the CSV file we are writing
 	CSVData io.Reader // required


Test patch:
diff --git a/providers/salesforce/bulk-delete_test.go b/providers/salesforce/bulk-delete_test.go
new file mode 100644
index 00000000..19c02aee
--- /dev/null
+++ b/providers/salesforce/bulk-delete_test.go
@@ -0,0 +1,88 @@
+package salesforce
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/interpreter"
+	"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 TestBulkDelete(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
+	t.Parallel()
+
+	responseCreateJob := testutils.DataFromFile(t, "bulk/delete/launch-job-opportunity.json")
+	responseUpdateJob := testutils.DataFromFile(t, "bulk/delete/update-job-opportunity.json")
+
+	bodyRequest := `{"object":"Opportunity","operation":"delete"}`
+
+	tests := []bulkDeleteTestCase{
+		{
+			Name: "Mime response header expected",
+			Input: BulkOperationParams{
+				ObjectName: "Opportunity",
+				CSVData:    strings.NewReader(""),
+			},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{interpreter.ErrMissingContentType},
+		},
+		{
+			Name:  "Read object must be included",
+			Input: BulkOperationParams{},
+			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/json")
+				w.WriteHeader(http.StatusNoContent)
+			})),
+			ExpectedErrs: []error{common.ErrMissingObjects},
+		},
+		{
+			Name: "CSV is required for upload",
+			Input: BulkOperationParams{
+				ObjectName: "Opportunity",
+				CSVData:    nil,
+			},
+			Server:       mockserver.Dummy(),
+			ExpectedErrs: []error{common.ErrMissingCSVData},
+		},
+		{
+			Name: "Successful Job Create, CSV upload, Job Update",
+			Input: BulkOperationParams{
+				ObjectName: "Opportunity",
+				CSVData:    strings.NewReader(""),
+			},
+			Server: createBulkJobServer(bodyRequest, responseCreateJob, responseUpdateJob, "750ak000009BkrxAAC"),
+			Expected: &BulkOperationResult{
+				State: "UploadComplete",
+				JobId: "750ak000009BkrxAAC",
+			},
+			ExpectedErrs: nil,
+		},
+	}
+
+	for _, tt := range tests { // nolint:dupl
+		// nolint:varnamelen
+		tt := tt // rebind, omit loop side effects for parallel goroutine
+		t.Run(tt.Name, func(t *testing.T) {
+			t.Parallel()
+
+			tt.Run(t, func() (*Connector, error) {
+				return constructTestConnector(tt.Server.URL)
+			})
+		})
+	}
+}
+
+type bulkDeleteTestCase bulkWriteTestCase
+
+func (c bulkDeleteTestCase) Run(t *testing.T, builder testroutines.ConnectorBuilder[*Connector]) {
+	t.Helper()
+	conn := builder.Build(t, c.Name)
+	output, err := conn.BulkDelete(context.Background(), c.Input)
+	bulkWriteTestCaseType(c).Validate(t, err, output)
+}
diff --git a/providers/salesforce/bulk-write_test.go b/providers/salesforce/bulk-write_test.go
index f6fe5858..d91a92f6 100644
--- a/providers/salesforce/bulk-write_test.go
+++ b/providers/salesforce/bulk-write_test.go
@@ -2,6 +2,7 @@ package salesforce
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -158,32 +159,7 @@ func TestBulkWrite(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 				CSVData:         strings.NewReader(""),
 				Mode:            UpsertMode,
 			},
-			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // nolint:varnamelen
-				w.Header().Set("Content-Type", "application/json")
-				switch path := r.URL.Path; {
-				case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest"):
-					// Create job if body matches.
-					mockutils.RespondToBody(w, r, bodyRequest, func() {
-						w.WriteHeader(http.StatusOK)
-						_, _ = w.Write(responseCreateJob)
-					})
-				case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest/750ak000009BWKLAA4/batches"):
-					// We expect CSV to be uploaded via this endpoint.
-					w.WriteHeader(http.StatusCreated)
-					_, _ = w.Write([]byte{})
-				case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest/750ak000009BWKLAA4"):
-					// Mark job Completed.
-					mockutils.RespondToMethod(w, r, "PATCH", func() {
-						mockutils.RespondToBody(w, r, `{"state":"UploadComplete"}`, func() {
-							w.WriteHeader(http.StatusOK)
-							mockutils.WriteBody(w, `{...]`) // deliberate failure
-						})
-					})
-				default:
-					w.WriteHeader(http.StatusInternalServerError)
-					_, _ = w.Write([]byte{})
-				}
-			})),
+			Server:       createBulkJobServer(bodyRequest, responseCreateJob, []byte(`{...]`), "750ak000009BWKLAA4"),
 			ExpectedErrs: []error{ErrUpdateJob},
 		},
 		{
@@ -194,32 +170,7 @@ func TestBulkWrite(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 				CSVData:         strings.NewReader(""),
 				Mode:            UpsertMode,
 			},
-			Server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // nolint:varnamelen
-				w.Header().Set("Content-Type", "application/json")
-				switch path := r.URL.Path; {
-				case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest"):
-					// Create job if body matches.
-					mockutils.RespondToBody(w, r, bodyRequest, func() {
-						w.WriteHeader(http.StatusOK)
-						_, _ = w.Write(responseCreateJob)
-					})
-				case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest/750ak000009BWKLAA4/batches"):
-					// We expect CSV to be uploaded via this endpoint.
-					w.WriteHeader(http.StatusCreated)
-					_, _ = w.Write([]byte{})
-				case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest/750ak000009BWKLAA4"):
-					// Mark job Completed.
-					mockutils.RespondToMethod(w, r, "PATCH", func() {
-						mockutils.RespondToBody(w, r, `{"state":"UploadComplete"}`, func() {
-							w.WriteHeader(http.StatusOK)
-							_, _ = w.Write(responseUpdateJob)
-						})
-					})
-				default:
-					w.WriteHeader(http.StatusInternalServerError)
-					_, _ = w.Write([]byte{})
-				}
-			})),
+			Server: createBulkJobServer(bodyRequest, responseCreateJob, responseUpdateJob, "750ak000009BWKLAA4"),
 			Expected: &BulkOperationResult{
 				State: "UploadComplete",
 				JobId: "750ak000009BWKLAA4",
@@ -241,6 +192,49 @@ func TestBulkWrite(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 	}
 }
 
+// This server knows how to respond to the following events:
+// * Job created for certain request body.
+// * CSV was uploaded using JobID.
+// * Updated Job status as upload completed.
+// Otherwise, responds with internal server error that will break the tests which is intended.
+func createBulkJobServer(
+	bodyRequest string,
+	responseCreateJob []byte,
+	responseUpdateJob []byte,
+	jobID string,
+) *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // nolint:varnamelen
+		w.Header().Set("Content-Type", "application/json")
+
+		switch path := r.URL.Path; {
+		case strings.HasSuffix(path, "/services/data/v59.0/jobs/ingest"):
+			// Create job if body matches.
+			mockutils.RespondToBody(w, r, bodyRequest, func() {
+				w.WriteHeader(http.StatusOK)
+				_, _ = w.Write(responseCreateJob)
+			})
+
+		case strings.HasSuffix(path, fmt.Sprintf("/services/data/v59.0/jobs/ingest/%v/batches", jobID)):
+			// We expect CSV to be uploaded via this endpoint.
+			w.WriteHeader(http.StatusCreated)
+			_, _ = w.Write([]byte{})
+
+		case strings.HasSuffix(path, fmt.Sprintf("/services/data/v59.0/jobs/ingest/%v", jobID)):
+			// Mark job Completed.
+			mockutils.RespondToMethod(w, r, "PATCH", func() {
+				mockutils.RespondToBody(w, r, `{"state":"UploadComplete"}`, func() {
+					w.WriteHeader(http.StatusOK)
+					_, _ = w.Write(responseUpdateJob)
+				})
+			})
+
+		default:
+			w.WriteHeader(http.StatusInternalServerError)
+			_, _ = w.Write([]byte{})
+		}
+	}))
+}
+
 type (
 	bulkWriteTestCaseType = testroutines.TestCase[BulkOperationParams, *BulkOperationResult]
 	bulkWriteTestCase     bulkWriteTestCaseType
diff --git a/providers/salesforce/test/bulk/delete/launch-job-opportunity.json b/providers/salesforce/test/bulk/delete/launch-job-opportunity.json
new file mode 100644
index 00000000..26de8d1d
--- /dev/null
+++ b/providers/salesforce/test/bulk/delete/launch-job-opportunity.json
@@ -0,0 +1,15 @@
+{
+  "id": "750ak000009BkrxAAC",
+  "operation": "delete",
+  "object": "Opportunity",
+  "createdById": "005ak000005hvjJAAQ",
+  "createdDate": "2024-09-09T20:41:31.000+0000",
+  "systemModstamp": "2024-09-09T20:41:31.000+0000",
+  "state": "Open",
+  "concurrencyMode": "Parallel",
+  "contentType": "CSV",
+  "apiVersion": 59.0,
+  "contentUrl": "services/data/v59.0/jobs/ingest/750ak000009BkrxAAC/batches",
+  "lineEnding": "LF",
+  "columnDelimiter": "COMMA"
+}
diff --git a/providers/salesforce/test/bulk/delete/update-job-opportunity.json b/providers/salesforce/test/bulk/delete/update-job-opportunity.json
new file mode 100644
index 00000000..d70d6963
--- /dev/null
+++ b/providers/salesforce/test/bulk/delete/update-job-opportunity.json
@@ -0,0 +1,12 @@
+{
+  "id": "750ak000009BkrxAAC",
+  "operation": "delete",
+  "object": "Opportunity",
+  "createdById": "005ak000005hvjJAAQ",
+  "createdDate": "2024-09-09T20:41:31.000+0000",
+  "systemModstamp": "2024-09-09T20:41:31.000+0000",
+  "state": "UploadComplete",
+  "concurrencyMode": "Parallel",
+  "contentType": "CSV",
+  "apiVersion": 59.0
+}