Skip to content

Commit f3dbeda

Browse files
committed
feat(testroutines): Subscribe and ReadByIds
1 parent 578816c commit f3dbeda

13 files changed

Lines changed: 451 additions & 25 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package mockutils
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/amp-labs/connectors/test/utils/testutils"
7+
)
8+
9+
// JSONComparator offers helpers for structural equality checks between
10+
// arbitrary Go values and JSON documents.
11+
//
12+
// It provides a robust comparison mechanism for verifying JSON
13+
// equivalence in tests, ignoring superficial differences such as
14+
// whitespace, field order, or formatting.
15+
//
16+
// Example:
17+
//
18+
// type response struct {
19+
// ID string `json:"id"`
20+
// Name string `json:"name"`
21+
// }
22+
//
23+
// actual := response{ID: "123", Name: "Alice"}
24+
// expected := mockutils.JSONErrorWrapper(`{"name":"Alice","id":"123"}`)
25+
//
26+
// ok := mockutils.JSONComparator.Equals(actual, expected)
27+
// // ok == true (field order does not matter)
28+
//
29+
// This utility is primarily used by ErrorNormalizedComparator
30+
// to compare JSON-encoded error objects but can be reused
31+
// directly in tests where normalized JSON equality is needed.
32+
var JSONComparator = jsonComparator{}
33+
34+
type jsonComparator struct{}
35+
36+
// Equals reports whether the given Go value and a JSON-encoded
37+
// expected representation are structurally equivalent.
38+
//
39+
// It returns true if both JSON documents represent identical key-value
40+
// structures, ignoring field order and insignificant formatting.
41+
//
42+
// Any marshal or unmarshal failure results in false.
43+
func (jsonComparator) Equals(expected string, actual any) *testutils.CompareResult {
44+
result := testutils.NewCompareResult()
45+
46+
actualBytes, err := json.Marshal(actual)
47+
if err != nil {
48+
result.AddDiff("jsonComparator.Equals: couldn't marshall: %v", actual)
49+
return result
50+
}
51+
52+
actualJSON := make(map[string]any)
53+
if err = json.Unmarshal(actualBytes, &actualJSON); err != nil {
54+
result.AddDiff("jsonComparator.Equals: couldn't unmarshall 'actual' into map[string]any")
55+
return result
56+
}
57+
58+
expectedJSON := make(map[string]any)
59+
if err = json.Unmarshal([]byte(expected), &expectedJSON); err != nil {
60+
result.AddDiff("jsonComparator.Equals: couldn't unmarshall 'expected' into map[string]any")
61+
return result
62+
}
63+
64+
result.Assert("jsonComparator.Equals", actualJSON, expectedJSON)
65+
66+
return result
67+
}
68+
69+
func (jsonComparator) ListsEqual(expected []string, actual []any) (result *testutils.CompareResult) {
70+
result = testutils.NewCompareResult()
71+
72+
if !result.Assert("jsonComparator.ListsEqual lists are of different sizes", len(expected), len(actual)) {
73+
return result
74+
}
75+
76+
for index, e := range expected {
77+
a := actual[index]
78+
result.Merge(JSONComparator.Equals(e, a))
79+
}
80+
81+
return result
82+
}

test/utils/mockutils/mockserver/switch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (c Switch) Server() *httptest.Server {
4343
return
4444
}
4545

46-
// Default fail behaviour.
46+
// Default fail behavior.
4747
w.WriteHeader(http.StatusInternalServerError)
4848
_, _ = w.Write([]byte(`{"error": {"message": "condition failed"}}`))
4949
})

test/utils/mockutils/normalizedErrors.go

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package mockutils
22

33
import (
4-
"encoding/json"
54
"errors"
65
"fmt"
76
"reflect"
@@ -51,17 +50,10 @@ func (errorNormalizedComparator) ErrorEquals(actualErr, expectedErr any) *testut
5150

5251
// 3. Handle JSON case if expected is a JSON string.
5352
if expectedJSON, ok := expectedErr.(JSONErrorWrapper); ok {
54-
aJSON, err := json.Marshal(actualErr)
55-
if err != nil {
56-
result.AddDiff("failed to marshal actual error to JSON: %v", err)
57-
return result
53+
if equals := JSONComparator.Equals(string(expectedJSON), actualErr); !equals.OK {
54+
result.Merge(equals)
5855
}
5956

60-
if jsonBodyMatch(aJSON, string(expectedJSON)) {
61-
return result // good
62-
}
63-
64-
result.Assert("JSON error", string(expectedJSON), string(aJSON))
6557
return result
6658
}
6759

@@ -96,17 +88,3 @@ func (c errorNormalizedComparator) EachErrorEquals(actual, expected []any) *test
9688

9789
return result
9890
}
99-
100-
func jsonBodyMatch(actual []byte, expected string) bool {
101-
first := make(map[string]any)
102-
if err := json.Unmarshal(actual, &first); err != nil {
103-
return false
104-
}
105-
106-
second := make(map[string]any)
107-
if err := json.Unmarshal([]byte(expected), &second); err != nil {
108-
return false
109-
}
110-
111-
return reflect.DeepEqual(first, second)
112-
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package mockutils
2+
3+
import (
4+
"github.com/amp-labs/connectors/common"
5+
"github.com/amp-labs/connectors/test/utils/testutils"
6+
)
7+
8+
var SubscriptionResultComparator = subscriptionResultComparator{}
9+
10+
type subscriptionResultComparator struct{}
11+
12+
func (subscriptionResultComparator) CompareWithoutResultArg(
13+
actual, expected *common.SubscriptionResult,
14+
) *testutils.CompareResult {
15+
result := testutils.NewCompareResult()
16+
17+
result.Assert("ObjectEvents", expected.ObjectEvents, actual.ObjectEvents)
18+
result.Assert("Status", expected.Status, actual.Status)
19+
result.Assert("Objects", expected.Objects, actual.Objects)
20+
result.Assert("Events", expected.Events, actual.Events)
21+
result.Assert("UpdateFields", expected.UpdateFields, actual.UpdateFields)
22+
result.Assert("PassThroughEvents", expected.PassThroughEvents, actual.PassThroughEvents)
23+
24+
return result
25+
}

test/utils/testroutines/comparator.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ func ComparatorSubsetRead(serverURL string, actual, expected *common.ReadResult)
3636
return result
3737
}
3838

39+
// ComparatorSubsetReadByIds compares two slices of ReadResultRow as a subset,
40+
// ignoring order and focusing only on relevant fields, raw data, associations, and identifiers.
41+
func ComparatorSubsetReadByIds(serverURL string, actual, expected []common.ReadResultRow) *testutils.CompareResult {
42+
return ComparatorSubsetRead(serverURL,
43+
&common.ReadResult{
44+
Rows: int64(len(actual)),
45+
Data: actual,
46+
},
47+
&common.ReadResult{
48+
Rows: int64(len(expected)),
49+
Data: expected,
50+
},
51+
)
52+
}
53+
3954
// ComparatorPagination will check pagination related fields.
4055
// Note: you may use an alias for Mock-Server-URL which will be dynamically resolved at runtime.
4156
// Example:
@@ -215,6 +230,24 @@ func ComparatorSubsetUpsertMetadata(_ string, actual, expected *common.UpsertMet
215230
return result
216231
}
217232

233+
func ComparatorSubscriptionWithResult(
234+
resultComparator func(expectedResult, actualResult any) *testutils.CompareResult,
235+
) Comparator[*common.SubscriptionResult] {
236+
return func(_ string, actual, expected *common.SubscriptionResult) *testutils.CompareResult {
237+
result := testutils.NewCompareResult()
238+
result.Merge(mockutils.SubscriptionResultComparator.CompareWithoutResultArg(actual, expected))
239+
result.Merge(resultComparator(expected.Result, actual.Result))
240+
241+
return result
242+
}
243+
}
244+
245+
func ComparatorSubscriptionWithoutResult(
246+
_ string, actual, expected *common.SubscriptionResult,
247+
) *testutils.CompareResult {
248+
return mockutils.SubscriptionResultComparator.CompareWithoutResultArg(actual, expected)
249+
}
250+
218251
func mapIsSubsetMap(subset, superset map[string]any) bool {
219252
for key, expected := range subset {
220253
actual, ok := superset[key]

test/utils/testroutines/outline.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ type TestCase[Input any, Output any] struct {
2727
ExpectedErrs []error
2828
}
2929

30+
// None can be used to indicate no Input or no Output type.
31+
type None struct{}
32+
3033
func (c TestCase[Input, Output]) Close() {
3134
c.Server.Close()
3235
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package testroutines
2+
3+
import (
4+
"testing"
5+
6+
"github.com/amp-labs/connectors"
7+
"github.com/amp-labs/connectors/common"
8+
)
9+
10+
type (
11+
ReadByIdsType = TestCase[ReadByIdsParams, []common.ReadResultRow]
12+
// ReadByIds is a test suite useful for testing connectors.BatchRecordReaderConnector interface.
13+
ReadByIds ReadByIdsType
14+
)
15+
16+
type ReadByIdsParams struct {
17+
ObjectName string
18+
RecordIds []string
19+
Fields []string
20+
Associations []string
21+
}
22+
23+
// Run provides a procedure to test connectors.BatchRecordReaderConnector
24+
func (r ReadByIds) Run(t *testing.T, builder ConnectorBuilder[connectors.BatchRecordReaderConnector]) {
25+
t.Helper()
26+
t.Cleanup(func() {
27+
ReadByIdsType(r).Close()
28+
})
29+
30+
conn := builder.Build(t, r.Name)
31+
output, err := conn.GetRecordsByIds(t.Context(),
32+
r.Input.ObjectName, r.Input.RecordIds,
33+
r.Input.Fields, r.Input.Associations,
34+
)
35+
ReadByIdsType(r).Validate(t, err, output)
36+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package testroutines
2+
3+
import (
4+
"testing"
5+
6+
"github.com/amp-labs/connectors/common"
7+
"github.com/amp-labs/connectors/internal/components"
8+
)
9+
10+
type (
11+
DeleteSubscriptionType = TestCase[common.SubscriptionResult, None]
12+
// DeleteSubscription is a test suite useful for testing part of connectors.SubscribeConnector interface.
13+
DeleteSubscription DeleteSubscriptionType
14+
)
15+
16+
type DeleteSubscriptionParams struct {
17+
Params common.SubscribeParams
18+
PreviousResult *common.SubscriptionResult
19+
}
20+
21+
// Run provides a procedure to test connectors.SubscribeConnector
22+
func (s DeleteSubscription) Run(t *testing.T, builder ConnectorBuilder[components.SubscriptionRemover]) {
23+
t.Helper()
24+
t.Cleanup(func() {
25+
DeleteSubscriptionType(s).Close()
26+
})
27+
28+
s.Expected = None{}
29+
30+
conn := builder.Build(t, s.Name)
31+
err := conn.DeleteSubscription(t.Context(), s.Input)
32+
DeleteSubscriptionType(s).Validate(t, err, None{})
33+
}

0 commit comments

Comments
 (0)