Skip to content

Commit 80aeaf1

Browse files
authored
test(cassettes): add custom body matcher (#1540)
1 parent 49f02a8 commit 80aeaf1

File tree

2 files changed

+97
-1
lines changed

2 files changed

+97
-1
lines changed

scaleway/provider_test.go

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package scaleway
22

33
import (
44
"context"
5+
"encoding/json"
56
"flag"
67
"fmt"
8+
"io"
79
"net/http"
810
"net/url"
911
"os"
@@ -31,6 +33,13 @@ var QueryMatcherIgnore = []string{
3133
"organization_id",
3234
}
3335

36+
// BodyMatcherIgnore contains the list of json body keys that should be ignored when matching requests with cassettes
37+
var BodyMatcherIgnore = []string{
38+
"organization_id",
39+
"project_id",
40+
"project", // like project_id but should be deprecated
41+
}
42+
3443
func testAccPreCheck(_ *testing.T) {}
3544

3645
// getTestFilePath returns a valid filename path based on the go test name and suffix. (Take care of non fs friendly char)
@@ -52,6 +61,85 @@ func getTestFilePath(t *testing.T, suffix string) string {
5261
return filepath.Join(".", "testdata", fileName)
5362
}
5463

64+
// compareJSONBodies compare two given maps that represent json bodies
65+
// returns true if both json are equivalent
66+
func compareJSONBodies(expected, actual map[string]interface{}) bool {
67+
// Check for each key in actual requests
68+
// Compare its value to cassette content if marshal-able to string
69+
for key := range actual {
70+
expectedValue, exists := expected[key]
71+
if !exists {
72+
// Actual request may contain a field that does not exist in cassette
73+
// New fields can appear in requests with new api features
74+
// We do not want to generate new cassettes for each new features
75+
continue
76+
}
77+
if actualValue, isStringer := actual[key].(fmt.Stringer); isStringer {
78+
if actualValue.String() != expectedValue.(fmt.Stringer).String() {
79+
return false
80+
}
81+
}
82+
}
83+
84+
for key := range expected {
85+
_, exists := actual[key]
86+
if !exists && expected[key] != nil {
87+
// Fails match if cassettes contains a field not in actual requests
88+
// Fields should not disappear from requests unless a sdk breaking change
89+
// We ignore if field is nil in cassette as it could be an old deprecated and unused field
90+
return false
91+
}
92+
}
93+
return true
94+
}
95+
96+
// cassetteMatcher is a custom matcher that will juste check equivalence of request bodies
97+
func cassetteBodyMatcher(actual *http.Request, expected cassette.Request) bool {
98+
if actual.Body == nil || actual.ContentLength == 0 {
99+
if expected.Body == "" {
100+
return true // Body match if both are empty
101+
} else if _, isFile := actual.Body.(*os.File); isFile {
102+
return true // Body match if request is sending a file, maybe do more check here
103+
}
104+
return false
105+
}
106+
107+
actualBody, err := actual.GetBody()
108+
if err != nil {
109+
panic(fmt.Errorf("cassette body matcher: failed to copy actual body: %w", err))
110+
}
111+
actualRawBody, err := io.ReadAll(actualBody)
112+
if err != nil {
113+
panic(fmt.Errorf("cassette body matcher: failed to read actual body: %w", err))
114+
}
115+
116+
// Try to match raw bodies if they are not JSON (ex: cloud-init config)
117+
if string(actualRawBody) == expected.Body {
118+
return true
119+
}
120+
121+
actualJSON := make(map[string]interface{})
122+
expectedJSON := make(map[string]interface{})
123+
124+
err = json.Unmarshal(actualRawBody, &actualJSON)
125+
if err != nil {
126+
panic(fmt.Errorf("cassette body matcher: failed to parse json body: %w", err))
127+
}
128+
129+
err = json.Unmarshal([]byte(expected.Body), &expectedJSON)
130+
if err != nil {
131+
panic(fmt.Errorf("cassette body matcher: failed to parse cassette json body: %w", err))
132+
}
133+
134+
// Remove keys that should be ignored during compare
135+
for _, key := range BodyMatcherIgnore {
136+
delete(actualJSON, key)
137+
delete(expectedJSON, key)
138+
}
139+
140+
return compareJSONBodies(expectedJSON, actualJSON)
141+
}
142+
55143
// cassetteMatcher is a custom matcher that check equivalence of a played request against a recorded one
56144
// It compares method, path and query but will remove unwanted values from query
57145
func cassetteMatcher(actual *http.Request, expected cassette.Request) bool {
@@ -68,7 +156,8 @@ func cassetteMatcher(actual *http.Request, expected cassette.Request) bool {
68156

69157
return actual.Method == expected.Method &&
70158
actual.URL.Path == expectedURL.Path &&
71-
actualURL.RawQuery == expectedURL.RawQuery
159+
actualURL.RawQuery == expectedURL.RawQuery &&
160+
cassetteBodyMatcher(actual, expected)
72161
}
73162

74163
// getHTTPRecoder creates a new httpClient that records all HTTP requests in a cassette.

scaleway/retryable_transport.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,12 @@ func (c *retryableTransport) RoundTrip(r *http.Request) (*http.Response, error)
5757
for key, val := range r.Header {
5858
req.Header.Set(key, val[0])
5959
}
60+
req.GetBody = func() (io.ReadCloser, error) {
61+
b, err := req.BodyBytes()
62+
if err != nil {
63+
return nil, err
64+
}
65+
return io.NopCloser(bytes.NewReader(b)), err
66+
}
6067
return c.Client.Do(req)
6168
}

0 commit comments

Comments
 (0)