diff --git a/providers/hubspot/internal/crm/associations/create_test.go b/providers/hubspot/internal/crm/associations/create_test.go index 7ce4d8f644..9a792809b0 100644 --- a/providers/hubspot/internal/crm/associations/create_test.go +++ b/providers/hubspot/internal/crm/associations/create_test.go @@ -211,8 +211,8 @@ func (c batchCreateTestCase) Run(t *testing.T, builder testroutines.ConnectorBui testCaseTypeBatchCreate(c).Validate(t, err, output) } -func batchCreateComparator(_ string, actual, expected *BatchCreateResult) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func batchCreateComparator(_ string, actual, expected *BatchCreateResult) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert("Success", expected.Success, actual.Success) result.Merge(mockutils.ErrorNormalizedComparator.EachErrorEquals( goutils.ToAnySlice(expected.Errors), diff --git a/providers/phoneburner/read_test.go b/providers/phoneburner/read_test.go index 9ea8863b6c..77f162ac8a 100644 --- a/providers/phoneburner/read_test.go +++ b/providers/phoneburner/read_test.go @@ -8,7 +8,6 @@ import ( "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" @@ -348,7 +347,7 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop func comparatorSubsetReadOrderByFolderID( serverURL string, actual, expected *common.ReadResult, -) *mockutils.CompareResult { +) *testutils.CompareResult { sort.Slice(actual.Data, func(i, j int) bool { ai, _ := actual.Data[i].Fields["folder_id"].(string) aj, _ := actual.Data[j].Fields["folder_id"].(string) diff --git a/providers/salesforce/bulk-info_test.go b/providers/salesforce/bulk-info_test.go index 7eebc2027d..163474184d 100644 --- a/providers/salesforce/bulk-info_test.go +++ b/providers/salesforce/bulk-info_test.go @@ -5,7 +5,6 @@ import ( "testing" "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" @@ -248,18 +247,18 @@ func TestGetBulkQueryResults(t *testing.T) { // nolint:dupl } } -func statusCodeComparator(serverURL string, actual, expected *http.Response) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func statusCodeComparator(serverURL string, actual, expected *http.Response) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert("StatusCode", expected.StatusCode, actual.StatusCode) return result } -func testJobResultsComparator(serverURL string, actual, expected *JobResults) *mockutils.CompareResult { +func testJobResultsComparator(serverURL string, actual, expected *JobResults) *testutils.CompareResult { actual.JobInfo = nil // ignore JobInfo when comparing - result := mockutils.NewCompareResult() + result := testutils.NewCompareResult() result.Assert("JobResults", expected, actual) @@ -268,8 +267,8 @@ func testJobResultsComparator(serverURL string, actual, expected *JobResults) *m func testConciseJobInfoComparator( serverURL string, actual *GetJobInfoResult, expected *GetJobInfoResult, -) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert("Id", expected.Id, actual.Id) result.Assert("Object", expected.Object, actual.Object) diff --git a/providers/salesforce/metadata_test.go b/providers/salesforce/metadata_test.go index 03aa3d7e5d..5b54b4cf0c 100644 --- a/providers/salesforce/metadata_test.go +++ b/providers/salesforce/metadata_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -358,8 +357,8 @@ func TestListObjectMetadataPardot(t *testing.T) { // nolint:funlen,gocognit,cycl }.Server(), Comparator: func( serverURL string, actual, expected *common.ListObjectMetadataResult, - ) *mockutils.CompareResult { - result := mockutils.NewCompareResult() + ) *testutils.CompareResult { + result := testutils.NewCompareResult() // Usual subset comparison. result.Merge(testroutines.ComparatorSubsetMetadata(serverURL, actual, expected)) diff --git a/test/utils/mockutils/batchWriteResult.go b/test/utils/mockutils/batchWriteResult.go index 9bf8ea207c..782b8207fe 100644 --- a/test/utils/mockutils/batchWriteResult.go +++ b/test/utils/mockutils/batchWriteResult.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/test/utils/testutils" ) // BatchWriteResultComparator provides utility methods for comparing BatchWriteResult structures in tests. @@ -21,10 +22,12 @@ type batchWriteResultComparator struct{} // - Subset equality for the Data field of each WriteResult (only expected keys/values are checked). // - Normalized equality for Errors, supporting struct/JSON, string, or golang error comparison. // - Exact equality for the Success and RecordId fields. -func (batchWriteResultComparator) SubsetWriteResults(actual, expected *common.BatchWriteResult) *CompareResult { - result := NewCompareResult() +func (batchWriteResultComparator) SubsetWriteResults( + actual, expected *common.BatchWriteResult, +) *testutils.CompareResult { + result := testutils.NewCompareResult() if len(actual.Results) != len(expected.Results) { - result.AddDiff(fmt.Sprintf("expected %d batch results, got %d", len(expected.Results), len(actual.Results))) + result.AddDiff("expected %d batch results, got %d", len(expected.Results), len(actual.Results)) return result } @@ -37,11 +40,11 @@ func (batchWriteResultComparator) SubsetWriteResults(actual, expected *common.Ba errorComparison := ErrorNormalizedComparator.EachErrorEquals(actualResult.Errors, expectedResult.Errors) for _, diff := range dataComparison.Diff { - result.AddDiff(fmt.Sprintf("Result[%d] %s", i, diff)) + result.AddDiff("Result[%d] %s", i, diff) } for _, diff := range errorComparison.Diff { - result.AddDiff(fmt.Sprintf("Result[%d] %s", i, diff)) + result.AddDiff("Result[%d] %s", i, diff) } result.Assert(fmt.Sprintf("Result[%d].Success", i), expectedResult.Success, actualResult.Success) diff --git a/test/utils/mockutils/checks.go b/test/utils/mockutils/checks.go index d7815ece7f..f3499deed4 100644 --- a/test/utils/mockutils/checks.go +++ b/test/utils/mockutils/checks.go @@ -3,82 +3,9 @@ package mockutils import ( "fmt" "strconv" - "strings" "testing" - - "github.com/go-test/deep" ) -// CompareResult holds the result of a comparison operation between actual and expected values. -// It tracks whether the comparison passed (OK) and collects detailed failure messages (Diff) -// for precise test failure diagnostics. -type CompareResult struct { - OK bool // True if comparison passed completely, false otherwise - Diff []string // List of human-readable failure descriptions, empty if OK -} - -// NewCompareResult creates a successful comparison result instance. -func NewCompareResult() *CompareResult { - return &CompareResult{OK: true} -} - -// AddDiff marks the comparison as failed and appends a custom failure message. -// -// This is the primary way to report simple failures like row count mismatches -// or pagination URL differences. -func (r *CompareResult) AddDiff(diff string) { - r.OK = false - r.Diff = append(r.Diff, diff) -} - -// Assert compares two data structures using github.com/go-test/deep.Equal -// and records a formatted mismatch report for the specified data name. -// Returns true if mismatch found (and recorded), false if exact match. -// -// Example output: -// -// Data[0].Fields[stagename] mismatch: -// ❌ Prospecting != PROSPECTING -// -// Data[0].Raw[OpportunityContactRoles] mismatch: -// ❌ map[totalSize]: 2 != 3 -// -// No-op (returns false) if structures match exactly. -func (r *CompareResult) Assert(dataName string, expectedData, gotData any) bool { - diff := deep.Equal(gotData, expectedData) - if len(diff) == 0 { - return false - } - - list := make([]string, len(diff)) - for index, text := range diff { - // Tabulated list of mismatches. - list[index] = fmt.Sprintf("\t❌ %v", text) - } - - message := fmt.Sprintf("%v mismatch:\n%v", dataName, strings.Join(list, "\n")) - - r.Diff = append(r.Diff, message) - r.OK = false - - return true -} - -// Merge combines another CompareResult into the receiver. -// -// Updates OK status (true only if both were true) and concatenates all Diff messages. -// Ignores nil other results. Used for chaining multiple sub-comparisons. -func (r *CompareResult) Merge(other *CompareResult) { - if other == nil { - return - } - - // Success requires both to succeed. - // Fail if either failed. - r.OK = r.OK && other.OK - r.Diff = append(r.Diff, other.Diff...) -} - func DoesObjectCorrespondToString(object any, correspondent string) bool { if object == nil && len(correspondent) == 0 { return true diff --git a/test/utils/mockutils/metadataResult.go b/test/utils/mockutils/metadataResult.go index 94e613bcd0..51f4e5dffb 100644 --- a/test/utils/mockutils/metadataResult.go +++ b/test/utils/mockutils/metadataResult.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/test/utils/testutils" ) var MetadataResultComparator = metadataResultComparator{} @@ -12,12 +13,14 @@ var MetadataResultComparator = metadataResultComparator{} type metadataResultComparator struct{} // SubsetFields checks that expected ListObjectMetadataResult fields are a subset of actual metadata result. -func (metadataResultComparator) SubsetFields(actual, expected *common.ListObjectMetadataResult) *CompareResult { - result := NewCompareResult() +func (metadataResultComparator) SubsetFields( + actual, expected *common.ListObjectMetadataResult, +) *testutils.CompareResult { + result := testutils.NewCompareResult() for objectName, expectedMetadata := range expected.Result { actualMetadata, ok := actual.Result[objectName] if !ok { - result.AddDiff(fmt.Sprintf("Result[%s] missing", objectName)) + result.AddDiff("Result[%s] missing", objectName) continue } @@ -27,7 +30,7 @@ func (metadataResultComparator) SubsetFields(actual, expected *common.ListObject for k, expectedValue := range expectedMetadata.Fields { actualValue, ok := actualMetadata.Fields[k] if !ok { - result.AddDiff(fmt.Sprintf("Result[%s].Fields[%s] missing", objectName, k)) + result.AddDiff("Result[%s].Fields[%s] missing", objectName, k) continue } @@ -39,7 +42,7 @@ func (metadataResultComparator) SubsetFields(actual, expected *common.ListObject for k, expectedValue := range expectedMetadata.FieldsMap { actualValue, ok := actualMetadata.FieldsMap[k] if !ok { - result.AddDiff(fmt.Sprintf("Result[%s].FieldsMap[%s] missing", objectName, k)) + result.AddDiff("Result[%s].FieldsMap[%s] missing", objectName, k) continue } @@ -51,12 +54,14 @@ func (metadataResultComparator) SubsetFields(actual, expected *common.ListObject return result } -func (metadataResultComparator) SubsetErrors(actual, expected *common.ListObjectMetadataResult) *CompareResult { - result := NewCompareResult() +func (metadataResultComparator) SubsetErrors( + actual, expected *common.ListObjectMetadataResult, +) *testutils.CompareResult { + result := testutils.NewCompareResult() for objectName, expectedError := range expected.Errors { actualError, ok := actual.Errors[objectName] if !ok { - result.AddDiff(fmt.Sprintf("Errors[%s] missing", objectName)) + result.AddDiff("Errors[%s] missing", objectName) continue } diff --git a/test/utils/mockutils/normalizedErrors.go b/test/utils/mockutils/normalizedErrors.go index 15d46b19da..0aac97961d 100644 --- a/test/utils/mockutils/normalizedErrors.go +++ b/test/utils/mockutils/normalizedErrors.go @@ -6,6 +6,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/amp-labs/connectors/test/utils/testutils" ) // ErrorNormalizedComparator provides helper methods to compare error values @@ -27,8 +29,8 @@ type errorNormalizedComparator struct{} // 4. Fallback: string comparison via fmt.Sprintf("%v"). // // It returns true if the two values are considered equivalent under these rules. -func (errorNormalizedComparator) ErrorEquals(actualErr, expectedErr any) *CompareResult { - result := NewCompareResult() +func (errorNormalizedComparator) ErrorEquals(actualErr, expectedErr any) *testutils.CompareResult { + result := testutils.NewCompareResult() // 1. Direct equality first. if reflect.DeepEqual(actualErr, expectedErr) { return result // good @@ -51,7 +53,7 @@ func (errorNormalizedComparator) ErrorEquals(actualErr, expectedErr any) *Compar if expectedJSON, ok := expectedErr.(JSONErrorWrapper); ok { aJSON, err := json.Marshal(actualErr) if err != nil { - result.AddDiff(fmt.Sprintf("failed to marshal actual error to JSON: %v", err)) + result.AddDiff("failed to marshal actual error to JSON: %v", err) return result } @@ -77,10 +79,10 @@ func (errorNormalizedComparator) ErrorEquals(actualErr, expectedErr any) *Compar // according to ErrorEquals. // // Order and slice length must match exactly. -func (c errorNormalizedComparator) EachErrorEquals(actual, expected []any) *CompareResult { - result := NewCompareResult() +func (c errorNormalizedComparator) EachErrorEquals(actual, expected []any) *testutils.CompareResult { + result := testutils.NewCompareResult() if len(actual) != len(expected) { - result.AddDiff(fmt.Sprintf("expected %d errors, got %d", len(expected), len(actual))) + result.AddDiff("expected %d errors, got %d", len(expected), len(actual)) return result } @@ -88,7 +90,7 @@ func (c errorNormalizedComparator) EachErrorEquals(actual, expected []any) *Comp res := c.ErrorEquals(actual[i], expected[i]) for _, diff := range res.Diff { - result.AddDiff(fmt.Sprintf("Errors[%d] %s", i, diff)) + result.AddDiff("Errors[%d] %s", i, diff) } } diff --git a/test/utils/mockutils/readResult.go b/test/utils/mockutils/readResult.go index b184ef608e..0582c3e36f 100644 --- a/test/utils/mockutils/readResult.go +++ b/test/utils/mockutils/readResult.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/test/utils/testutils" ) var ReadResultComparator = readResultComparator{} @@ -11,10 +12,10 @@ var ReadResultComparator = readResultComparator{} type readResultComparator struct{} // SubsetRaw checks that expected ReadResult.Raw is a subset of actual ReadResult.Raw -func (readResultComparator) SubsetRaw(actual, expected *common.ReadResult) *CompareResult { - result := NewCompareResult() +func (readResultComparator) SubsetRaw(actual, expected *common.ReadResult) *testutils.CompareResult { + result := testutils.NewCompareResult() if len(actual.Data) < len(expected.Data) { - result.AddDiff(fmt.Sprintf("expected at least %d data entries, got %d", len(expected.Data), len(actual.Data))) + result.AddDiff("expected at least %d data entries, got %d", len(expected.Data), len(actual.Data)) return result } @@ -34,10 +35,10 @@ func (readResultComparator) SubsetRaw(actual, expected *common.ReadResult) *Comp } // SubsetFields checks that expected ReadResult.Fields is a subset of actual ReadResult.Fields -func (readResultComparator) SubsetFields(actual, expected *common.ReadResult) *CompareResult { - result := NewCompareResult() +func (readResultComparator) SubsetFields(actual, expected *common.ReadResult) *testutils.CompareResult { + result := testutils.NewCompareResult() if len(actual.Data) < len(expected.Data) { - result.AddDiff(fmt.Sprintf("expected at least %d data entries, got %d", len(expected.Data), len(actual.Data))) + result.AddDiff("expected at least %d data entries, got %d", len(expected.Data), len(actual.Data)) return result } @@ -58,28 +59,28 @@ func (readResultComparator) SubsetFields(actual, expected *common.ReadResult) *C // SubsetAssociationsRaw checks that expected ReadResult.Associations are matching exactly, // but for each Association.Raw it only checks if every mentioned expected field is present in actual raw. -func (readResultComparator) SubsetAssociationsRaw(actual, expected *common.ReadResult) *CompareResult { - result := NewCompareResult() +func (readResultComparator) SubsetAssociationsRaw(actual, expected *common.ReadResult) *testutils.CompareResult { + result := testutils.NewCompareResult() if len(actual.Data) < len(expected.Data) { - result.AddDiff(fmt.Sprintf("expected at least %d data entries, got %d", len(expected.Data), len(actual.Data))) + result.AddDiff("expected at least %d data entries, got %d", len(expected.Data), len(actual.Data)) return result } for i := range expected.Data { message := fmt.Sprintf("Data[%d].Associations length", i) - if result.Assert(message, len(expected.Data[i].Associations), len(actual.Data[i].Associations)) { + if !result.Assert(message, len(expected.Data[i].Associations), len(actual.Data[i].Associations)) { continue } for key, expectedAssociations := range expected.Data[i].Associations { actualAssociations, ok := actual.Data[i].Associations[key] if !ok { - result.AddDiff(fmt.Sprintf("Data[%d].Associations[%s] missing", i, key)) + result.AddDiff("Data[%d].Associations[%s] missing", i, key) continue } message = fmt.Sprintf("Data[%d].Associations[%s] length", i, key) - if result.Assert(message, len(expectedAssociations), len(actualAssociations)) { + if !result.Assert(message, len(expectedAssociations), len(actualAssociations)) { continue } @@ -109,8 +110,8 @@ func (readResultComparator) SubsetAssociationsRaw(actual, expected *common.ReadR // Identifiers checks that actual rows have identifiers matching with expected. // NOTE: Empty strings signify nothing should be compared. -func (c readResultComparator) Identifiers(actual *common.ReadResult, expected *common.ReadResult) *CompareResult { - result := NewCompareResult() +func (c readResultComparator) Identifiers(actual *common.ReadResult, expected *common.ReadResult) *testutils.CompareResult { + result := testutils.NewCompareResult() for index, datum := range expected.Data { expectedID := datum.Id if expectedID != "" { diff --git a/test/utils/mockutils/writeResult.go b/test/utils/mockutils/writeResult.go index f75f0fd04d..5b254cddf9 100644 --- a/test/utils/mockutils/writeResult.go +++ b/test/utils/mockutils/writeResult.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/test/utils/testutils" ) var WriteResultComparator = writeResultComparator{} @@ -12,11 +13,11 @@ 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) *CompareResult { - result := NewCompareResult() +func (writeResultComparator) SubsetData(actual, expected *common.WriteResult) *testutils.CompareResult { + result := testutils.NewCompareResult() // We are expecting more fields than there in the existence. if len(actual.Data) < len(expected.Data) { - result.AddDiff(fmt.Sprintf("expected at least %d data fields, got %d", len(expected.Data), len(actual.Data))) + result.AddDiff("expected at least %d data fields, got %d", len(expected.Data), len(actual.Data)) return result } @@ -29,7 +30,7 @@ func (writeResultComparator) SubsetData(actual, expected *common.WriteResult) *C for key, expectedValue := range expected.Data { actualValue, ok := actual.Data[key] if !ok { - result.AddDiff(fmt.Sprintf("Data[%s] missing", key)) + result.AddDiff("Data[%s] missing", key) continue } diff --git a/test/utils/testroutines/comparator.go b/test/utils/testroutines/comparator.go index 4e4779e1b7..ab1d9d5e29 100644 --- a/test/utils/testroutines/comparator.go +++ b/test/utils/testroutines/comparator.go @@ -8,6 +8,7 @@ import ( "github.com/amp-labs/connectors/common" "github.com/amp-labs/connectors/common/urlbuilder" "github.com/amp-labs/connectors/test/utils/mockutils" + "github.com/amp-labs/connectors/test/utils/testutils" ) // URLTestServer is an alias to mock server BaseURL. @@ -19,13 +20,13 @@ const URLTestServer = "{{testServerURL}}" // // This package provides the most commonly used comparators like ComparatorSubsetRead, ComparatorPagination, // ComparatorSubsetWrite, ComparatorSubsetMetadata for partial field matching in large API responses. -type Comparator[Output any] func(serverURL string, actual, expected Output) *mockutils.CompareResult +type Comparator[Output any] func(serverURL string, actual, expected Output) *testutils.CompareResult // ComparatorSubsetRead ensures that a subset of fields or raw data is present in the response. // This is convenient for cases where the returned data is large, // allowing for a more concise test that still validates the desired behavior. -func ComparatorSubsetRead(serverURL string, actual, expected *common.ReadResult) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func ComparatorSubsetRead(serverURL string, actual, expected *common.ReadResult) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Merge(mockutils.ReadResultComparator.SubsetFields(actual, expected)) result.Merge(mockutils.ReadResultComparator.SubsetRaw(actual, expected)) result.Merge(mockutils.ReadResultComparator.SubsetAssociationsRaw(actual, expected)) @@ -48,8 +49,8 @@ func ComparatorSubsetRead(serverURL string, actual, expected *common.ReadResult) // the check will conclude that pagination matches. func ComparatorPagination( serverURL string, actual *common.ReadResult, expected *common.ReadResult, -) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +) *testutils.CompareResult { + result := testutils.NewCompareResult() expectedNextPage := resolveTestServerURL(expected.NextPage.String(), serverURL) if !compareNextPageToken(actual.NextPage.String(), expectedNextPage) { @@ -98,8 +99,8 @@ func compareNextPageToken(actual, expected string) bool { // // This comparator is typically used when only a subset of Data fields // needs verification rather than a full equality check. -func ComparatorSubsetWrite(_ string, actual, expected *common.WriteResult) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func ComparatorSubsetWrite(_ string, actual, expected *common.WriteResult) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert(fmt.Sprintf("Success mismatch"), expected.Success, actual.Success) result.Assert(fmt.Sprintf("RecordId mismatch"), expected.RecordId, actual.RecordId) result.Merge(mockutils.WriteResultComparator.SubsetData(actual, expected)) @@ -118,8 +119,8 @@ func ComparatorSubsetWrite(_ string, actual, expected *common.WriteResult) *mock // // This enables expressive, stable tests that verify meaningful fields // without enforcing strict structural equality across the entire batch. -func ComparatorSubsetBatchWrite(_ string, actual, expected *common.BatchWriteResult) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func ComparatorSubsetBatchWrite(_ string, actual, expected *common.BatchWriteResult) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert(fmt.Sprintf("Status mismatch"), expected.Status, actual.Status) result.Assert(fmt.Sprintf("SuccessCount mismatch"), expected.SuccessCount, actual.SuccessCount) result.Assert(fmt.Sprintf("FailureCount mismatch"), expected.FailureCount, actual.FailureCount) @@ -144,12 +145,12 @@ func ComparatorSubsetBatchWrite(_ string, actual, expected *common.BatchWriteRes // "arsenal": common.NewHTTPError(http.StatusBadRequest, // Is doing exact match. // headers, body, fmt.Errorf("%w: %s", common.ErrCaller, string(unsupportedResponse))), // }, -func ComparatorSubsetMetadata(_ string, actual, expected *common.ListObjectMetadataResult) *mockutils.CompareResult { +func ComparatorSubsetMetadata(_ string, actual, expected *common.ListObjectMetadataResult) *testutils.CompareResult { if len(expected.Result)+len(expected.Errors) == 0 { panic("please specify expected Result or Errors in Metadata response") } - result := mockutils.NewCompareResult() + result := testutils.NewCompareResult() result.Merge(mockutils.MetadataResultComparator.SubsetFields(actual, expected)) result.Merge(mockutils.MetadataResultComparator.SubsetErrors(actual, expected)) @@ -175,8 +176,8 @@ func resolveTestServerURL(urlTemplate string, serverURL string) string { // - Metadata is compared using subset semantics — all key/value // pairs defined in expected.Metadata must be present in // actual.Metadata, but actual may contain additional entries. -func ComparatorSubsetUpsertMetadata(_ string, actual, expected *common.UpsertMetadataResult) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func ComparatorSubsetUpsertMetadata(_ string, actual, expected *common.UpsertMetadataResult) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert(fmt.Sprintf("Success mismatch"), expected.Success, actual.Success) result.Assert(fmt.Sprintf("Fields length mismatch"), len(expected.Fields), len(actual.Fields)) @@ -185,13 +186,13 @@ func ComparatorSubsetUpsertMetadata(_ string, actual, expected *common.UpsertMet for fieldName, expectedField := range property { actualProperty, ok := actual.Fields[propertyName] if !ok { - result.AddDiff(fmt.Sprintf("Fields[%s] missing", propertyName)) + result.AddDiff("Fields[%s] missing", propertyName) continue } actualField, ok := actualProperty[fieldName] if !ok { - result.AddDiff(fmt.Sprintf("Fields[%s][%s] missing", propertyName, fieldName)) + result.AddDiff("Fields[%s][%s] missing", propertyName, fieldName) continue } diff --git a/test/utils/testroutines/outline.go b/test/utils/testroutines/outline.go index cc27b0440a..d7bcff84b4 100644 --- a/test/utils/testroutines/outline.go +++ b/test/utils/testroutines/outline.go @@ -1,12 +1,10 @@ package testroutines import ( - "fmt" "net/http/httptest" "reflect" "testing" - "github.com/amp-labs/connectors/test/utils/mockutils" "github.com/amp-labs/connectors/test/utils/testutils" "github.com/go-test/deep" ) @@ -47,7 +45,7 @@ func (c TestCase[Input, Output]) checkError(t *testing.T, err error) { func (c TestCase[Input, Output]) checkValue(t *testing.T, output Output) { // compare desired output - var result *mockutils.CompareResult + var result *testutils.CompareResult if c.Comparator == nil { // default comparison is concerned about all fields result = c.defaultDeepCompare(output, c.Expected) @@ -55,22 +53,15 @@ func (c TestCase[Input, Output]) checkValue(t *testing.T, output Output) { result = c.Comparator(c.Server.URL, output, c.Expected) } - if !result.OK { - message := fmt.Sprintf("[%s] some expectations were not satisfied:\n", c.Name) - for index, text := range result.Diff { - message += fmt.Sprintf("(%v) %v\n", index+1, text) - } - - t.Fatal(message) - } + result.Validate(t, c.Name) } -func (c TestCase[Input, Output]) defaultDeepCompare(actual, expected Output) *mockutils.CompareResult { - result := mockutils.NewCompareResult() +func (c TestCase[Input, Output]) defaultDeepCompare(actual, expected Output) *testutils.CompareResult { + result := testutils.NewCompareResult() if !reflect.DeepEqual(actual, expected) { for _, diff := range deep.Equal(actual, expected) { - result.AddDiff(diff) + result.AddDifference(diff) } } diff --git a/test/utils/testroutines/post-auth-info.go b/test/utils/testroutines/post-auth-info.go index aad067c12f..b964f6285d 100644 --- a/test/utils/testroutines/post-auth-info.go +++ b/test/utils/testroutines/post-auth-info.go @@ -5,7 +5,7 @@ import ( "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/testutils" ) type ( @@ -21,8 +21,8 @@ func (r PostAuthInfo) Run(t *testing.T, builder ConnectorBuilder[connectors.Auth PostAuthInfoType(r).Close() }) - r.Comparator = func(serverURL string, actual, expected *common.PostAuthInfo) *mockutils.CompareResult { - result := mockutils.NewCompareResult() + r.Comparator = func(serverURL string, actual, expected *common.PostAuthInfo) *testutils.CompareResult { + result := testutils.NewCompareResult() result.Assert("ProviderWorkspaceRef", expected.ProviderWorkspaceRef, actual.ProviderWorkspaceRef) result.Assert("CatalogVars", expected.CatalogVars, actual.CatalogVars) diff --git a/test/utils/testutils/comparison.go b/test/utils/testutils/comparison.go new file mode 100644 index 0000000000..80ecc93f01 --- /dev/null +++ b/test/utils/testutils/comparison.go @@ -0,0 +1,212 @@ +package testutils + +import ( + "errors" + "fmt" + "strings" + "testing" + + "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" +) + +// CompareResult represents the aggregated result of one or more comparisons +// performed during a test. +// +// It collects multiple assertion failures and defers test termination until +// Validate is called, allowing a single test run to report all mismatches +// instead of failing on the first error. +// +// A CompareResult is successful when OK is true. Any call that records a +// difference (e.g., AddDiff, Assert, AssertErr) sets OK to false and appends +// a human-readable message to Diff. +// +// After performing comparisons, Validate must be called to fail the test if +// any mismatches were recorded. +// +// Example: +// +// main := NewCompareResult() +// +// create := NewCompareResult() +// create.Assert("Create", expectedCreate, actualCreate) +// create.AssertErr("Create.Err", expectedCreateErr, actualCreateErr) +// +// update := NewCompareResult() +// update.Assert("Update", expectedUpdate, actualUpdate) +// update.AssertErr("Update.Err", expectedUpdateErr, actualUpdateErr) +// +// main.Merge(create) +// main.Merge(update) +// +// main.Validate(t, "TestFlow") +type CompareResult struct { + OK bool // True if comparison passed completely, false otherwise + Diff []string // List of human-readable failure descriptions, empty if OK +} + +// NewCompareResult creates a successful comparison result instance. +func NewCompareResult() *CompareResult { + return &CompareResult{OK: true} +} + +// AddDiff marks the comparison as failed and appends a custom failure message. +// +// This is the primary way to report simple failures like row count mismatches +// or pagination URL differences. +func (r *CompareResult) AddDiff(diff string, args ...any) *CompareResult { + if len(args) != 0 { + return r.AddDifference(fmt.Sprintf(diff, args...)) + } + + return r.AddDifference(diff) +} + +func (r *CompareResult) AddDifference(diff string) *CompareResult { + r.OK = false + r.Diff = append(r.Diff, diff) + return r +} + +// Assert compares two data structures using github.com/go-test/deep.Equal +// and records a formatted mismatch report for the specified data name. +// Returns true if structures match exactly. +// +// Example output: +// +// Data[0].Fields[stagename] mismatch: +// ❌ Prospecting != PROSPECTING +// +// Data[0].Raw[OpportunityContactRoles] mismatch: +// ❌ map[totalSize]: 2 != 3 +// +// No-op (returns true) if structures match exactly. +func (r *CompareResult) Assert(dataName string, expectedData, gotData any) bool { + status, isMap := r.tryAssertMap(dataName, expectedData, gotData) + if isMap { + return status + } + + diff := deep.Equal(gotData, expectedData) + if len(diff) == 0 { + return true + } + + list := make([]string, len(diff)) + for index, text := range diff { + // Tabulated list of mismatches. + list[index] = fmt.Sprintf("\t❌ %v", text) + } + + message := fmt.Sprintf("%v mismatch:\n%v", dataName, strings.Join(list, "\n")) + + r.Diff = append(r.Diff, message) + r.OK = false + + return false +} + +func (r *CompareResult) tryAssertMap(dataName string, expectedData, gotData any) (status bool, isMap bool) { + _, firstIsMap := expectedData.(map[string]any) + _, secondIsMap := gotData.(map[string]any) + if !(firstIsMap && secondIsMap) { + return false, false + } + + diff := cmp.Diff(expectedData, gotData) + if diff != "" { + r.AddDiff("%v: mismatch (-expected +got):\n%s", dataName, diff) + return false, true + } + + return true, true +} + +// AssertErr compares expected and actual errors and records a mismatch +// in the result if they do not match. +// +// Matching rules: +// +// - If both expectedErr and actualErr are nil → considered a match. +// - If only one is nil → mismatch. +// - Otherwise, errors are compared using errors.Is (semantic match). +// - If expectedErr is of type StrError, errors are compared as substrings. +// +// Returns true if the errors match, false if a mismatch is found. +func (r *CompareResult) AssertErr(dataName string, expectedErr, actualErr error) bool { + if expectedErr == nil { + if actualErr != nil { + r.AddDiff("%s: expected no error, got: (%v)", dataName, actualErr) + return false + } + + // Both errors are nil. + return true + } + + if actualErr == nil { + r.AddDiff("%s: expected error: (%v), got nil", dataName, expectedErr) + return false + } + + // Default behavior: strict semantic comparison using errors.Is. + if !errors.Is(actualErr, expectedErr) { + // Special marker handling: detect whether the expected error is a StrError marker. + if _, ok := errors.AsType[StrError](expectedErr); ok { + // StrError allows flexible comparison: + // prefer errors.Is, but fall back to message containment. + if !strings.Contains(actualErr.Error(), expectedErr.Error()) { + r.AddDiff("%s: expected error: (%v), got: (%v)", dataName, expectedErr, actualErr) + return false + } + + // Expected substring is found inside the error. + return true + } + + r.AddDiff("%s: expected error: (%v), got: (%v)", dataName, expectedErr, actualErr) + return false + } + + // Both errors match. + return true +} + +// Merge combines another CompareResult into the receiver. +// +// Updates OK status (true only if both were true) and concatenates all Diff messages. +// Ignores nil other results. Used for chaining multiple sub-comparisons. +func (r *CompareResult) Merge(other *CompareResult) { + if other == nil { + return + } + + // Success requires both to succeed. + // Fail if either failed. + r.OK = r.OK && other.OK + r.Diff = append(r.Diff, other.Diff...) +} + +// Validate finalizes the comparison and fails the test if any mismatches +// were recorded. +// +// If the CompareResult is successful (OK == true), Validate is a no-op. +// +// Otherwise, it builds a structured failure message that includes the provided +// testName and all collected Diff entries, then terminates the test using +// t.Fatal. Each diff is numbered in order of occurrence to improve readability. +// +// This method is intended to be called once at the end of a test after all +// assertions have been executed. +func (r *CompareResult) Validate(t *testing.T, testName string) { + if r.OK { + return + } + + message := fmt.Sprintf("[%s] some expectations were not satisfied:\n", testName) + for index, text := range r.Diff { + message += fmt.Sprintf("(%v) %v\n", index+1, text) + } + + t.Fatal(message) +}