diff --git a/providers/hubspot/batch-write_test.go b/providers/hubspot/batch-write_test.go index 978b1e3dde..8bcf1f5b88 100644 --- a/providers/hubspot/batch-write_test.go +++ b/providers/hubspot/batch-write_test.go @@ -60,8 +60,11 @@ func TestBatchCreate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/create"), - Then: mockserver.Response(http.StatusConflict, errConflictExisting), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/create"), + }, + Then: mockserver.Response(http.StatusConflict, errConflictExisting), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -82,8 +85,11 @@ func TestBatchCreate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/create"), - Then: mockserver.Response(http.StatusBadRequest, nil), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/create"), + }, + Then: mockserver.Response(http.StatusBadRequest, nil), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -104,8 +110,11 @@ func TestBatchCreate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/create"), - Then: mockserver.Response(http.StatusBadRequest, errManyInvalidFields), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/create"), + }, + Then: mockserver.Response(http.StatusBadRequest, errManyInvalidFields), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -142,8 +151,11 @@ func TestBatchCreate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/create"), - Then: mockserver.Response(http.StatusOK, responseCreateContacts), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/create"), + }, + Then: mockserver.Response(http.StatusOK, responseCreateContacts), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -219,8 +231,11 @@ func TestBatchUpdate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/update"), - Then: mockserver.Response(http.StatusBadRequest, errDuplicateIDs), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/update"), + }, + Then: mockserver.Response(http.StatusBadRequest, errDuplicateIDs), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -241,8 +256,11 @@ func TestBatchUpdate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/update"), - Then: mockserver.Response(http.StatusBadRequest, errManyInvalidFields), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/update"), + }, + Then: mockserver.Response(http.StatusBadRequest, errManyInvalidFields), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -293,8 +311,11 @@ func TestBatchUpdate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/update"), - Then: mockserver.Response(http.StatusMultiStatus, errUpdatePartial), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/update"), + }, + Then: mockserver.Response(http.StatusMultiStatus, errUpdatePartial), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ @@ -333,8 +354,11 @@ func TestBatchUpdate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx }, Server: mockserver.Conditional{ Setup: mockserver.ContentJSON(), - If: mockcond.Path("/crm/v3/objects/contacts/batch/update"), - Then: mockserver.Response(http.StatusOK, responseUpdateContacts), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/crm/v3/objects/contacts/batch/update"), + }, + Then: mockserver.Response(http.StatusOK, responseUpdateContacts), }.Server(), Comparator: testroutines.ComparatorSubsetBatchWrite, Expected: &common.BatchWriteResult{ diff --git a/providers/salesforce/batch-write_test.go b/providers/salesforce/batch-write_test.go new file mode 100644 index 0000000000..bd0b3ae563 --- /dev/null +++ b/providers/salesforce/batch-write_test.go @@ -0,0 +1,364 @@ +package salesforce + +import ( + "net/http" + "testing" + + "github.com/amp-labs/connectors" + "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/test/utils/mockutils" + "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 TestBatchCreate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx + t.Parallel() + + errBadRequest := testutils.DataFromFile(t, "batch/create/contacts/err-bad-request.json") + errCreatePartial := testutils.DataFromFile(t, "batch/create/contacts/partial-but-allOrNone.json") + createPayload := testutils.DataFromFile(t, "batch/create/contacts/payload.json") + responseCreateContacts := testutils.DataFromFile(t, "batch/create/contacts/success.json") + + type record = common.Record + + createRecords := common.BatchItems{{ + Record: record{ + "FirstName": "Siena", + "LastName": "Dyer", + }, + }, { + Record: record{ + "FirstName": "Markus", + "LastName": "Blevins", + }, + }} + + tests := []testroutines.BatchWrite{ + { + Name: "At least one object name must be queried", + Input: &common.BatchWriteParam{ + ObjectName: "", + }, + Server: mockserver.Dummy(), + ExpectedErrs: []error{common.ErrMissingObjects}, + }, + { + Name: "Batch write type is missing", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + }, + Server: mockserver.Dummy(), + ExpectedErrs: []error{common.ErrUnknownBatchWriteType}, + }, + { + Name: "General high level error not tied to any record", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeCreate, + Batch: createRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + }, + Then: mockserver.Response(http.StatusBadRequest, errBadRequest), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusFailure, + Errors: []any{"record was not processed due to other records failures: " + + "error REQUIRED_FIELD_MISSING: At least 1 record is required"}, + Results: []common.WriteResult{{ + Success: false, + RecordId: "", + Errors: []any{common.ErrBatchUnprocessedRecord}, + Data: nil, + }}, + SuccessCount: 0, + FailureCount: 2, + }, + ExpectedErrs: nil, + }, + { + Name: "Many errors not traceable to any record", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeCreate, + Batch: createRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + }, + Then: mockserver.Response(http.StatusBadRequest, errCreatePartial), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusFailure, + Errors: nil, + Results: []common.WriteResult{{ + Success: false, + RecordId: "", + Errors: []any{mockutils.JSONErrorWrapper(`{ + "statusCode": "REQUIRED_FIELD_MISSING", + "message": "Required fields are missing: [LastName]", + "fields": ["LastName"] + }`)}, + Data: nil, + }, { + Success: false, + RecordId: "", + // nolint:lll + Errors: []any{mockutils.JSONErrorWrapper(`{ + "statusCode": "ALL_OR_NONE_OPERATION_ROLLED_BACK", + "message": "Record rolled back because not all records were valid and the request was using AllOrNone header", + "fields": [] + }`)}, + Data: nil, + }}, + SuccessCount: 0, + FailureCount: 2, + }, + ExpectedErrs: nil, + }, + { + Name: "Bad request without the body", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeCreate, + Batch: createRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + }, + Then: mockserver.Response(http.StatusBadRequest, nil), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusFailure, + Errors: []any{common.ErrEmptyJSONHTTPResponse}, + Results: nil, + SuccessCount: 0, + FailureCount: 2, + }, + ExpectedErrs: nil, + }, + { + Name: "Successful write with valid payload construction", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeCreate, + Batch: createRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPOST(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + mockcond.BodyBytes(createPayload), // validate that connector knows how to create payload. + }, + Then: mockserver.Response(http.StatusOK, responseCreateContacts), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusSuccess, + Errors: []any{}, + Results: []common.WriteResult{{ + Success: true, + RecordId: "003ak00000luKU1AAM", + Errors: nil, + Data: nil, + }, { + Success: true, + RecordId: "003ak00000luKU2AAM", + Errors: nil, + Data: nil, + }}, + SuccessCount: 2, + FailureCount: 0, + }, + ExpectedErrs: nil, + }, + } + + for _, tt := range tests { + // nolint:varnamelen + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + tt.Run(t, func() (connectors.BatchWriteConnector, error) { + return constructTestConnector(tt.Server.URL) + }) + }) + } +} + +func TestBatchUpdate(t *testing.T) { // nolint:funlen,gocognit,cyclop,maintidx + t.Parallel() + + errNoIDs := testutils.DataFromFile(t, "batch/update/contacts/err-each-no-ids.json") + updatePayload := testutils.DataFromFile(t, "batch/update/contacts/payload.json") + errUpdatePartial := testutils.DataFromFile(t, "batch/update/contacts/partial-but-allOrNone.json") + responseUpdateContacts := testutils.DataFromFile(t, "batch/update/contacts/success.json") + + type record = common.Record + + updateRecords := common.BatchItems{{ + Record: record{ + "id": "003ak00000jvIfpAAE", + "FirstName": "Siena", + "LastName": "Dyer", + }, + }, { + Record: record{ + "id": "003ak00000jvIfqAAE", + "FirstName": "Markus", + "LastName": "Blevins", + }, + }} + + tests := []testroutines.BatchWrite{ + { + Name: "General high level error not tied to any record", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeUpdate, + Batch: updateRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPATCH(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + }, + Then: mockserver.Response(http.StatusBadRequest, errNoIDs), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusFailure, + Errors: nil, + Results: []common.WriteResult{{ + Success: false, + RecordId: "", + Errors: []any{mockutils.JSONErrorWrapper(`{ + "statusCode": "MISSING_ARGUMENT", + "message": "Id not specified in an update call", + "fields": []}`)}, + Data: nil, + }, { + Success: false, + RecordId: "", + Errors: []any{mockutils.JSONErrorWrapper(`{ + "statusCode": "MISSING_ARGUMENT", + "message": "Id not specified in an update call", + "fields": []}`)}, + Data: nil, + }}, + SuccessCount: 0, + FailureCount: 2, + }, + ExpectedErrs: nil, + }, + { + Name: "Partial result where one contact did not have an id", + // For right now no partial response is supported. + // As of right now, connector always sets payload with AllOrNone=true. + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeUpdate, + Batch: updateRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPATCH(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + mockcond.BodyBytes(updatePayload), + }, + Then: mockserver.Response(http.StatusOK, errUpdatePartial), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusFailure, + Errors: nil, + Results: []common.WriteResult{{ + Success: false, + RecordId: "", + Errors: []any{mockutils.JSONErrorWrapper(`{ + "statusCode": "ALL_OR_NONE_OPERATION_ROLLED_BACK", + "message": "Record rolled back because not all records were valid and the request was using AllOrNone header", + "fields": [] + }`)}, + Data: nil, + }, { + Success: false, + RecordId: "", + Errors: []any{mockutils.JSONErrorWrapper(`{ + "statusCode": "INVALID_FIELD", + "message": "No such column 'unknownField_LastName' on sobject of type Contact", + "fields": [] + }`)}, + Data: nil, + }}, + SuccessCount: 0, + FailureCount: 2, + }, + ExpectedErrs: nil, + }, + { + Name: "Successful write", + Input: &common.BatchWriteParam{ + ObjectName: "Contact", + Type: common.BatchWriteTypeUpdate, + Batch: updateRecords, + }, + Server: mockserver.Conditional{ + Setup: mockserver.ContentJSON(), + If: mockcond.And{ + mockcond.MethodPATCH(), + mockcond.Path("/services/data/v60.0/composite/sobjects"), + }, + Then: mockserver.Response(http.StatusOK, responseUpdateContacts), + }.Server(), + Comparator: testroutines.ComparatorSubsetBatchWrite, + Expected: &common.BatchWriteResult{ + Status: common.BatchStatusSuccess, + Errors: []any{}, + Results: []common.WriteResult{{ + Success: true, + RecordId: "003ak00000jvIfpAAE", + Errors: nil, + Data: nil, + }, { + Success: true, + RecordId: "003ak00000jvIfqAAE", + Errors: nil, + Data: nil, + }}, + SuccessCount: 2, + FailureCount: 0, + }, + ExpectedErrs: nil, + }, + } + + for _, tt := range tests { + // nolint:varnamelen + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + tt.Run(t, func() (connectors.BatchWriteConnector, error) { + return constructTestConnector(tt.Server.URL) + }) + }) + } +} diff --git a/providers/salesforce/internal/crm/batch/write.go b/providers/salesforce/internal/crm/batch/write.go index 5301d31787..9e3a95628a 100644 --- a/providers/salesforce/internal/crm/batch/write.go +++ b/providers/salesforce/internal/crm/batch/write.go @@ -2,6 +2,7 @@ package batch import ( "context" + "fmt" "net/http" "github.com/amp-labs/connectors/common" @@ -57,7 +58,7 @@ func (a *Adapter) BatchWrite(ctx context.Context, params *common.BatchWriteParam } if response == nil || len(*response) == 0 { - return a.handleEmptyResponse(rsp) + return a.handleEmptyResponse(rsp, len(payload.Records)) } // nolint:lll @@ -105,23 +106,17 @@ func resultBuilder(_ PayloadItem, respItem *Item) (*common.WriteResult, error) { return respItem.ToWriteResult() } -func (a *Adapter) handleEmptyResponse(rsp *common.JSONHTTPResponse) (*common.BatchWriteResult, error) { - status := common.BatchStatusSuccess - errors := make([]any, 0) - +func (a *Adapter) handleEmptyResponse( + rsp *common.JSONHTTPResponse, totalNumRecords int, +) (*common.BatchWriteResult, error) { if rsp.Code == http.StatusBadRequest { // A 400 Bad Request is allowed by implementation, but we always expect a response body. // Since there is no data, and non-2xx response we cannot determine per-record results, // so the batch is treated as failed. - status = common.BatchStatusFailure - errors = append(errors, common.ErrEmptyJSONHTTPResponse) + return common.NewBatchWriteResultFailed(nil, totalNumRecords, []any{common.ErrEmptyJSONHTTPResponse}) } - return &common.BatchWriteResult{ - Status: status, - Errors: errors, - Results: nil, - }, nil + return common.NewBatchWriteResult(nil, totalNumRecords, totalNumRecords, nil) } func (a *Adapter) buildBatchWriteURL(params *common.BatchWriteParam) (*urlbuilder.URL, error) { @@ -191,6 +186,11 @@ type Item struct { Success bool `json:"success"` ID string `json:"id,omitempty"` Errors []ItemError `json:"errors"` + + // These properties can come up during 400 BadRequest. + // Ex: no records sent to the endpoint. + Message *string `json:"message,omitempty"` + ErrorCode *string `json:"errorCode,omitempty"` } type ItemError struct { @@ -202,6 +202,22 @@ type ItemError struct { func (i Item) ToWriteResult() (*common.WriteResult, error) { success := len(i.Errors) == 0 + if success && !i.Success { + // Success status is missing in response. + // At the same time there are no error objects. + // This means the format and structure we expected is not present. + switch { + case i.Message != nil && i.ErrorCode != nil: + return nil, fmt.Errorf("%w: error %s: %s", common.ErrBatchUnprocessedRecord, *i.ErrorCode, *i.Message) + case i.Message != nil: + return nil, fmt.Errorf("%w: %v", common.ErrBatchUnprocessedRecord, *i.Message) + case i.ErrorCode != nil: + return nil, fmt.Errorf("%w: error code: %s", common.ErrBatchUnprocessedRecord, *i.ErrorCode) + default: + return nil, fmt.Errorf("%w: unexpected response format", common.ErrBatchUnprocessedRecord) + } + } + if success { return &common.WriteResult{ Success: true, diff --git a/providers/salesforce/test/batch/create/contacts/err-bad-request.json b/providers/salesforce/test/batch/create/contacts/err-bad-request.json new file mode 100644 index 0000000000..52cd1fdc16 --- /dev/null +++ b/providers/salesforce/test/batch/create/contacts/err-bad-request.json @@ -0,0 +1,6 @@ +[ + { + "message": "At least 1 record is required", + "errorCode": "REQUIRED_FIELD_MISSING" + } +] diff --git a/providers/salesforce/test/batch/create/contacts/partial-but-allOrNone.json b/providers/salesforce/test/batch/create/contacts/partial-but-allOrNone.json new file mode 100644 index 0000000000..e97e355ea5 --- /dev/null +++ b/providers/salesforce/test/batch/create/contacts/partial-but-allOrNone.json @@ -0,0 +1,24 @@ +[ + { + "success": false, + "errors": [ + { + "statusCode": "REQUIRED_FIELD_MISSING", + "message": "Required fields are missing: [LastName]", + "fields": [ + "LastName" + ] + } + ] + }, + { + "success": false, + "errors": [ + { + "statusCode": "ALL_OR_NONE_OPERATION_ROLLED_BACK", + "message": "Record rolled back because not all records were valid and the request was using AllOrNone header", + "fields": [] + } + ] + } +] diff --git a/providers/salesforce/test/batch/create/contacts/payload.json b/providers/salesforce/test/batch/create/contacts/payload.json new file mode 100644 index 0000000000..263985f776 --- /dev/null +++ b/providers/salesforce/test/batch/create/contacts/payload.json @@ -0,0 +1,19 @@ +{ + "allOrNone": true, + "records": [ + { + "FirstName": "Siena", + "LastName": "Dyer", + "attributes": { + "type": "Contact" + } + }, + { + "FirstName": "Markus", + "LastName": "Blevins", + "attributes": { + "type": "Contact" + } + } + ] +} \ No newline at end of file diff --git a/providers/salesforce/test/batch/create/contacts/success.json b/providers/salesforce/test/batch/create/contacts/success.json new file mode 100644 index 0000000000..ebf3a5731a --- /dev/null +++ b/providers/salesforce/test/batch/create/contacts/success.json @@ -0,0 +1,12 @@ +[ + { + "id": "003ak00000luKU1AAM", + "success": true, + "errors": [] + }, + { + "id": "003ak00000luKU2AAM", + "success": true, + "errors": [] + } +] diff --git a/providers/salesforce/test/batch/update/contacts/err-each-no-ids.json b/providers/salesforce/test/batch/update/contacts/err-each-no-ids.json new file mode 100644 index 0000000000..055e981182 --- /dev/null +++ b/providers/salesforce/test/batch/update/contacts/err-each-no-ids.json @@ -0,0 +1,22 @@ +[ + { + "success": false, + "errors": [ + { + "statusCode": "MISSING_ARGUMENT", + "message": "Id not specified in an update call", + "fields": [] + } + ] + }, + { + "success": false, + "errors": [ + { + "statusCode": "MISSING_ARGUMENT", + "message": "Id not specified in an update call", + "fields": [] + } + ] + } +] diff --git a/providers/salesforce/test/batch/update/contacts/partial-but-allOrNone.json b/providers/salesforce/test/batch/update/contacts/partial-but-allOrNone.json new file mode 100644 index 0000000000..7231b0bd65 --- /dev/null +++ b/providers/salesforce/test/batch/update/contacts/partial-but-allOrNone.json @@ -0,0 +1,22 @@ +[ + { + "success": false, + "errors": [ + { + "statusCode": "ALL_OR_NONE_OPERATION_ROLLED_BACK", + "message": "Record rolled back because not all records were valid and the request was using AllOrNone header", + "fields": [] + } + ] + }, + { + "success": false, + "errors": [ + { + "statusCode": "INVALID_FIELD", + "message": "No such column 'unknownField_LastName' on sobject of type Contact", + "fields": [] + } + ] + } +] diff --git a/providers/salesforce/test/batch/update/contacts/payload.json b/providers/salesforce/test/batch/update/contacts/payload.json new file mode 100644 index 0000000000..ba4381c55e --- /dev/null +++ b/providers/salesforce/test/batch/update/contacts/payload.json @@ -0,0 +1,21 @@ +{ + "allOrNone": true, + "records": [ + { + "id": "003ak00000jvIfpAAE", + "FirstName": "Siena", + "LastName": "Dyer", + "attributes": { + "type": "Contact" + } + }, + { + "id": "003ak00000jvIfqAAE", + "FirstName": "Markus", + "LastName": "Blevins", + "attributes": { + "type": "Contact" + } + } + ] +} \ No newline at end of file diff --git a/providers/salesforce/test/batch/update/contacts/success.json b/providers/salesforce/test/batch/update/contacts/success.json new file mode 100644 index 0000000000..04ba03d579 --- /dev/null +++ b/providers/salesforce/test/batch/update/contacts/success.json @@ -0,0 +1,12 @@ +[ + { + "id": "003ak00000jvIfpAAE", + "success": true, + "errors": [] + }, + { + "id": "003ak00000jvIfqAAE", + "success": true, + "errors": [] + } +] diff --git a/test/utils/mockutils/writeResult.go b/test/utils/mockutils/writeResult.go index 43ffed858d..5949ffef67 100644 --- a/test/utils/mockutils/writeResult.go +++ b/test/utils/mockutils/writeResult.go @@ -13,10 +13,16 @@ type writeResultComparator struct{} // SubsetData checks that expected WriteResult.Data is a subset of actual WriteResult.Data // other fields are strictly compared. func (writeResultComparator) SubsetData(actual, expected *common.WriteResult) bool { + // We are expecting more fields than there in the existence. if len(actual.Data) < len(expected.Data) { return false } + // At least one field should be mentioned. + if len(actual.Data) > 0 && len(expected.Data) == 0 { + return false + } + for k, expectedValue := range expected.Data { actualValue, ok := actual.Data[k] if !ok {