Skip to content

Commit 0417565

Browse files
committed
fix(acctest): compare in depth request body with cassette body
1 parent 13e1fd9 commit 0417565

File tree

4 files changed

+574
-156
lines changed

4 files changed

+574
-156
lines changed

internal/acctest/acctest.go

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -102,65 +102,6 @@ func extractGeneratedNamePrefix(name string) string {
102102
return name
103103
}
104104

105-
// compareJSONFieldsStrings compare two strings from request JSON bodies
106-
// has special case when string are terraform generated names
107-
func compareJSONFieldsStrings(expected, actual string) bool {
108-
expectedHandled := expected
109-
actualHandled := actual
110-
111-
// Remove s3 url suffix to allow comparison
112-
if strings.HasSuffix(actual, ".s3-website.fr-par.scw.cloud") {
113-
actual = strings.TrimSuffix(actual, ".s3-website.fr-par.scw.cloud")
114-
expected = strings.TrimSuffix(expected, ".s3-website.fr-par.scw.cloud")
115-
}
116-
117-
// Try to parse test generated name
118-
if strings.Contains(actual, "-") {
119-
expectedHandled = extractTestGeneratedNamePrefix(expected)
120-
actualHandled = extractTestGeneratedNamePrefix(actual)
121-
}
122-
123-
// Try provider generated name
124-
if actualHandled == actual && strings.HasPrefix(actual, "tf-") {
125-
expectedHandled = extractGeneratedNamePrefix(expected)
126-
actualHandled = extractGeneratedNamePrefix(actual)
127-
}
128-
129-
return expectedHandled == actualHandled
130-
}
131-
132-
// compareJSONBodies compare two given maps that represent json bodies
133-
// returns true if both json are equivalent
134-
func compareJSONBodies(expected, actual map[string]interface{}) bool {
135-
// Check for each key in actual requests
136-
// Compare its value to cassette content if marshal-able to string
137-
for key := range actual {
138-
expectedValue, exists := expected[key]
139-
if !exists {
140-
// Actual request may contain a field that does not exist in cassette
141-
// New fields can appear in requests with new api features
142-
// We do not want to generate new cassettes for each new features
143-
continue
144-
}
145-
146-
if !compareJSONFields(expectedValue, actual[key]) {
147-
return false
148-
}
149-
}
150-
151-
for key := range expected {
152-
_, exists := actual[key]
153-
if !exists && expected[key] != nil {
154-
// Fails match if cassettes contains a field not in actual requests
155-
// Fields should not disappear from requests unless a sdk breaking change
156-
// We ignore if field is nil in cassette as it could be an old deprecated and unused field
157-
return false
158-
}
159-
}
160-
161-
return true
162-
}
163-
164105
// IsTestResource returns true if given resource identifier is from terraform test
165106
// identifier should be resource name but some resource don't have names
166107
// return true if identifier match regex "tf[-_]test"

internal/acctest/acctest_test.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package acctest_test
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
10+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest"
11+
"gopkg.in/dnaeon/go-vcr.v3/cassette"
12+
)
13+
14+
var barMemberCreationBody = `{
15+
"organization_id": "6867048b-fe12-4e96-835e-41c79a39604b",
16+
"tags": [
17+
"bar"
18+
],
19+
"member": {
20+
"email": "[email protected]",
21+
"send_password_email": false,
22+
"send_welcome_email": false,
23+
"username": "bar",
24+
"password": "",
25+
"first_name": "",
26+
"last_name": "",
27+
"phone_number": "",
28+
"locale": ""
29+
}
30+
}
31+
`
32+
33+
var fooMemberCreationBody = `{
34+
"organization_id": "6867048b-fe12-4e96-835e-41c79a39604b",
35+
"tags": [
36+
"foo"
37+
],
38+
"member": {
39+
"email": "[email protected]",
40+
"send_password_email": false,
41+
"send_welcome_email": false,
42+
"username": "foo",
43+
"password": "",
44+
"first_name": "",
45+
"last_name": "",
46+
"phone_number": "",
47+
"locale": ""
48+
}
49+
}
50+
`
51+
52+
var secretPatchBodyCassette = `{
53+
"environment_variables": {
54+
"foo": "bar"
55+
},
56+
"privacy": "unknown_privacy",
57+
"protocol": "unknown_protocol",
58+
"secret_environment_variables": [
59+
{
60+
"key": "foo_secret",
61+
"value": "bar_secret"
62+
},
63+
{
64+
"key": "test_secret",
65+
"value": "updated_secret"
66+
},
67+
{
68+
"key": "first_secret",
69+
"value": null
70+
}
71+
],
72+
"http_option": "unknown_http_option",
73+
"sandbox": "unknown_sandbox"
74+
}
75+
`
76+
77+
var secretPatchBodyRequest = `{
78+
"environment_variables": {
79+
"foo": "bar"
80+
},
81+
"privacy": "unknown_privacy",
82+
"protocol": "unknown_protocol",
83+
"secret_environment_variables": [
84+
{
85+
"key": "first_secret",
86+
"value": null
87+
},
88+
{
89+
"key": "foo_secret",
90+
"value": "bar_secret"
91+
},
92+
{
93+
"key": "test_secret",
94+
"value": "updated_secret"
95+
}
96+
],
97+
"http_option": "unknown_http_option",
98+
"sandbox": "unknown_sandbox"
99+
}
100+
`
101+
102+
var integertestBodyRequest = `{
103+
"akey": "avalue",
104+
"integers": [
105+
1,
106+
2,
107+
3
108+
]
109+
}
110+
`
111+
112+
var integertestBodyCassette = `{
113+
"akey": "avalue",
114+
"integers": [
115+
4,
116+
5,
117+
6
118+
]
119+
}
120+
`
121+
122+
var integerBodyRequestOutOfOrder = `{
123+
"akey": "avalue",
124+
"integers": [
125+
2,
126+
1,
127+
3
128+
]
129+
}
130+
`
131+
132+
var nestedSliceOfSlicesRequest = `{
133+
"akey": "avalue",
134+
"nested_lists": [
135+
[
136+
"1",
137+
"2",
138+
"3"
139+
],
140+
[
141+
"4",
142+
"5",
143+
"6"
144+
]
145+
}
146+
}
147+
`
148+
149+
var nestedSliceOfSlicesCassette = `{
150+
"akey": "avalue",
151+
"nested_slice_of_slices": {
152+
"integers_array": [
153+
[
154+
"4",
155+
"5",
156+
"6"
157+
],
158+
[
159+
"1",
160+
"2",
161+
"3"
162+
]
163+
]
164+
},
165+
}
166+
`
167+
168+
var simpleSliceOfStringsRequest = `{
169+
"strings": [
170+
"1",
171+
"2",
172+
"3"
173+
]
174+
}
175+
`
176+
var simpleSliceOfStringsCassette = `{
177+
"strings": [
178+
"3",
179+
"2",
180+
"1"
181+
]
182+
}
183+
`
184+
185+
// we don't use httptest.NewRequest because it does not set the GetBody func
186+
func newRequest(method, url string, body io.Reader) *http.Request {
187+
req, err := http.NewRequestWithContext(context.Background(), method, url, body)
188+
if err != nil {
189+
panic(err) // lintignore: R009
190+
}
191+
192+
return req
193+
}
194+
195+
var testBodyMatcherCases = []struct {
196+
requestBody *http.Request
197+
cassetteBody *cassette.Request
198+
shouldMatch bool
199+
}{
200+
// create bar compare with foo
201+
{
202+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(barMemberCreationBody)),
203+
cassetteBody: &cassette.Request{
204+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
205+
Method: http.MethodPost,
206+
Body: fooMemberCreationBody,
207+
ContentLength: int64(len(fooMemberCreationBody)),
208+
},
209+
shouldMatch: false,
210+
},
211+
// create bar compare with bar
212+
{
213+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(barMemberCreationBody)),
214+
cassetteBody: &cassette.Request{
215+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
216+
Method: http.MethodPost,
217+
Body: barMemberCreationBody,
218+
ContentLength: int64(len(barMemberCreationBody)),
219+
},
220+
shouldMatch: true,
221+
},
222+
// simple http get
223+
{
224+
requestBody: newRequest(http.MethodGet, "https://api.scaleway.com/iam/v1alpha1/users/6867048b-fe12-4e96-835e-41c79a39604b", nil),
225+
cassetteBody: &cassette.Request{
226+
URL: "https://api.scaleway.com/iam/v1alpha1/users/6867048b-fe12-4e96-835e-41c79a39604b",
227+
Method: http.MethodGet,
228+
Body: "",
229+
ContentLength: 0,
230+
},
231+
shouldMatch: true,
232+
},
233+
// patch secret with nested slices of map[string]interface{} in different order
234+
// we cannot user deep equal because the order of the slices is different although the values are the same
235+
// it is not possible to sort them because they are not comparable (map[string]interface{})
236+
{
237+
requestBody: newRequest(http.MethodPatch, "https://api.scaleway.com/secrets/v1/secrets/123", strings.NewReader(secretPatchBodyRequest)),
238+
cassetteBody: &cassette.Request{
239+
URL: "https://api.scaleway.com/secrets/v1/secrets/123",
240+
Method: http.MethodPatch,
241+
Body: secretPatchBodyCassette,
242+
ContentLength: int64(len(secretPatchBodyCassette)),
243+
},
244+
shouldMatch: true,
245+
},
246+
// compare nested slices of different integers
247+
{
248+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(integertestBodyRequest)),
249+
cassetteBody: &cassette.Request{
250+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
251+
Method: http.MethodPost,
252+
Body: integertestBodyCassette,
253+
ContentLength: int64(len(integertestBodyCassette)),
254+
},
255+
shouldMatch: false,
256+
},
257+
// compare nested slices of same integers in different order
258+
{
259+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(integerBodyRequestOutOfOrder)),
260+
cassetteBody: &cassette.Request{
261+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
262+
Method: http.MethodPost,
263+
Body: integertestBodyRequest,
264+
ContentLength: int64(len(integertestBodyRequest)),
265+
},
266+
shouldMatch: true,
267+
},
268+
// compare nested slices of slices of strings
269+
{
270+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(nestedSliceOfSlicesRequest)),
271+
cassetteBody: &cassette.Request{
272+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
273+
Method: http.MethodPost,
274+
Body: nestedSliceOfSlicesCassette,
275+
ContentLength: int64(len(nestedSliceOfSlicesCassette)),
276+
},
277+
shouldMatch: false,
278+
},
279+
{
280+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(nestedSliceOfSlicesRequest)),
281+
cassetteBody: &cassette.Request{
282+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
283+
Method: http.MethodPost,
284+
Body: nestedSliceOfSlicesRequest,
285+
ContentLength: int64(len(nestedSliceOfSlicesRequest)),
286+
},
287+
shouldMatch: true,
288+
},
289+
// compare simple slice of strings
290+
{
291+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(simpleSliceOfStringsRequest)),
292+
cassetteBody: &cassette.Request{
293+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
294+
Method: http.MethodPost,
295+
Body: simpleSliceOfStringsCassette,
296+
ContentLength: int64(len(simpleSliceOfStringsCassette)),
297+
},
298+
shouldMatch: true,
299+
},
300+
}
301+
302+
func TestCassetteMatcher(t *testing.T) {
303+
for i, test := range testBodyMatcherCases {
304+
shouldMatch := acctest.CassetteMatcher(test.requestBody, *test.cassetteBody)
305+
if shouldMatch != test.shouldMatch {
306+
t.Errorf("test %d: expected %v, got %v", i, test.shouldMatch, shouldMatch)
307+
}
308+
}
309+
}

0 commit comments

Comments
 (0)