Skip to content

Commit 8b3af27

Browse files
committed
feat(acctest): compare slice of map[string]interface{}
1 parent a8d278a commit 8b3af27

File tree

3 files changed

+251
-37
lines changed

3 files changed

+251
-37
lines changed

internal/acctest/acctest_test.go

Lines changed: 197 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ var simpleSliceOfStringsRequest = `{
173173
]
174174
}
175175
`
176+
176177
var simpleSliceOfStringsCassette = `{
177178
"strings": [
178179
"3",
@@ -182,6 +183,162 @@ var simpleSliceOfStringsCassette = `{
182183
}
183184
`
184185

186+
var ludacrisBodyRequest = `{
187+
"payload": {
188+
"artists": [
189+
{
190+
"name": "Ludacris",
191+
"age": 45,
192+
"songs": ["Ludacris", "Ludacris", "Ludacris"]
193+
}
194+
}
195+
}
196+
}
197+
`
198+
199+
var jdillaBodyCassette = `{
200+
"payload": {
201+
"artists": [
202+
{
203+
"name": "Jdilla",
204+
"age": 54,
205+
"songs": ["this", "is", "jdilla"]
206+
}
207+
]
208+
}
209+
}
210+
`
211+
212+
var requestInstanceSettings = `{
213+
"settings": [
214+
{
215+
"name": "max_connections",
216+
"value": "200"
217+
},
218+
{
219+
"name": "max_parallel_workers",
220+
"value": "2"
221+
},
222+
{
223+
"name": "effective_cache_size",
224+
"value": "1300"
225+
},
226+
{
227+
"name": "maintenance_work_mem",
228+
"value": "150"
229+
},
230+
{
231+
"name": "max_parallel_workers_per_gather",
232+
"value": "2"
233+
},
234+
{
235+
"name": "work_mem",
236+
"value": "4"
237+
}
238+
]
239+
}
240+
`
241+
242+
var cassetteInstanceSettings = `{
243+
"settings": [
244+
{
245+
"name": "maintenance_work_mem",
246+
"value": "150"
247+
},
248+
{
249+
"name": "effective_cache_size",
250+
"value": "1300"
251+
},
252+
{
253+
"name": "work_mem",
254+
"value": "4"
255+
},
256+
{
257+
"name": "max_parallel_workers",
258+
"value": "2"
259+
},
260+
{
261+
"name": "max_parallel_workers_per_gather",
262+
"value": "2"
263+
},
264+
{
265+
"name": "max_connections",
266+
"value": "200"
267+
}
268+
]
269+
}
270+
`
271+
272+
var objectBodyRequest = `{
273+
"Id": "MyPolicy",
274+
"Statement": [
275+
{
276+
"Action": [
277+
"s3:ListBucket",
278+
"s3:GetObject"
279+
],
280+
"Effect": "Allow",
281+
"Principal": {
282+
"SCW": "*"
283+
},
284+
"Resource": [
285+
"tf-tests-scw-obp-basic-4713290580220176511",
286+
"tf-tests-scw-obp-basic-4713290580220176511/*"
287+
],
288+
"Sid": "GrantToEveryone"
289+
},
290+
{
291+
"Action": [
292+
"s3:ListBucket",
293+
"s3:GetObject"
294+
],
295+
"Effect": "Allow",
296+
"Principal": {
297+
"SCW": "*"
298+
},
299+
"Sid": "GrantToEveryone",
300+
"project_id": "1234567890"
301+
}
302+
],
303+
"Version": "2012-10-17"
304+
}
305+
`
306+
307+
var objectBodyCassette = `{
308+
"Id": "MyPolicy",
309+
"Statement": [
310+
{
311+
"Action": [
312+
"s3:ListBucket",
313+
"s3:GetObject"
314+
],
315+
"Effect": "Allow",
316+
"Principal": {
317+
"SCW": "*"
318+
},
319+
"Sid": "GrantToEveryone",
320+
"project_id": "9876543210"
321+
},
322+
{
323+
"Action": [
324+
"s3:ListBucket",
325+
"s3:GetObject"
326+
],
327+
"Effect": "Allow",
328+
"Principal": {
329+
"SCW": "*"
330+
},
331+
"Sid": "GrantToEveryone",
332+
"Resource": [
333+
"tf-tests-scw-obp-basic-1234567890",
334+
"tf-tests-scw-obp-basic-1234567890/*"
335+
]
336+
}
337+
],
338+
"Version": "2012-10-17"
339+
}
340+
`
341+
185342
// we don't use httptest.NewRequest because it does not set the GetBody func
186343
func newRequest(method, url string, body io.Reader) *http.Request {
187344
req, err := http.NewRequestWithContext(context.Background(), method, url, body)
@@ -197,7 +354,7 @@ var testBodyMatcherCases = []struct {
197354
cassetteBody *cassette.Request
198355
shouldMatch bool
199356
}{
200-
// create bar compare with foo
357+
// bar does not match foo
201358
{
202359
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(barMemberCreationBody)),
203360
cassetteBody: &cassette.Request{
@@ -208,7 +365,7 @@ var testBodyMatcherCases = []struct {
208365
},
209366
shouldMatch: false,
210367
},
211-
// create bar compare with bar
368+
// bar matches bar
212369
{
213370
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(barMemberCreationBody)),
214371
cassetteBody: &cassette.Request{
@@ -231,8 +388,6 @@ var testBodyMatcherCases = []struct {
231388
shouldMatch: true,
232389
},
233390
// 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{})
236391
{
237392
requestBody: newRequest(http.MethodPatch, "https://api.scaleway.com/secrets/v1/secrets/123", strings.NewReader(secretPatchBodyRequest)),
238393
cassetteBody: &cassette.Request{
@@ -243,7 +398,28 @@ var testBodyMatcherCases = []struct {
243398
},
244399
shouldMatch: true,
245400
},
246-
// compare nested slices of different integers
401+
{
402+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(requestInstanceSettings)),
403+
cassetteBody: &cassette.Request{
404+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
405+
Method: http.MethodPost,
406+
Body: cassetteInstanceSettings,
407+
ContentLength: int64(len(cassetteInstanceSettings)),
408+
},
409+
shouldMatch: true,
410+
},
411+
// complex slice of maps case
412+
{
413+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/policies", strings.NewReader(objectBodyRequest)),
414+
cassetteBody: &cassette.Request{
415+
URL: "https://api.scaleway.com/iam/v1alpha1/policies",
416+
Method: http.MethodPost,
417+
Body: objectBodyCassette,
418+
ContentLength: int64(len(objectBodyCassette)),
419+
},
420+
shouldMatch: true,
421+
},
422+
// compare slices of different integers
247423
{
248424
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(integertestBodyRequest)),
249425
cassetteBody: &cassette.Request{
@@ -254,7 +430,7 @@ var testBodyMatcherCases = []struct {
254430
},
255431
shouldMatch: false,
256432
},
257-
// compare nested slices of same integers in different order
433+
// compare slices of same integers in different order
258434
{
259435
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(integerBodyRequestOutOfOrder)),
260436
cassetteBody: &cassette.Request{
@@ -265,7 +441,7 @@ var testBodyMatcherCases = []struct {
265441
},
266442
shouldMatch: true,
267443
},
268-
// compare nested slices of slices of strings
444+
// compare slices of slices of strings in different order
269445
{
270446
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(nestedSliceOfSlicesRequest)),
271447
cassetteBody: &cassette.Request{
@@ -286,7 +462,7 @@ var testBodyMatcherCases = []struct {
286462
},
287463
shouldMatch: true,
288464
},
289-
// compare simple slice of strings
465+
// compare slices of strings in different order
290466
{
291467
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(simpleSliceOfStringsRequest)),
292468
cassetteBody: &cassette.Request{
@@ -297,13 +473,26 @@ var testBodyMatcherCases = []struct {
297473
},
298474
shouldMatch: true,
299475
},
476+
// ludacris does not match jdilla
477+
{
478+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(ludacrisBodyRequest)),
479+
cassetteBody: &cassette.Request{
480+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
481+
Method: http.MethodPost,
482+
Body: jdillaBodyCassette,
483+
ContentLength: int64(len(jdillaBodyCassette)),
484+
},
485+
shouldMatch: false,
486+
},
300487
}
301488

302489
func TestCassetteMatcher(t *testing.T) {
303490
for i, test := range testBodyMatcherCases {
304491
shouldMatch := acctest.CassetteMatcher(test.requestBody, *test.cassetteBody)
305492
if shouldMatch != test.shouldMatch {
306493
t.Errorf("test %d: expected %v, got %v", i, test.shouldMatch, shouldMatch)
494+
t.Errorf("requestBody: %s", test.requestBody.Body)
495+
t.Errorf("cassetteBody: %s", test.cassetteBody.Body)
307496
}
308497
}
309498
}

internal/acctest/compare.go

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"strings"
88
)
99

10-
// compareJSONFields compare two given json fields
11-
// it will recurse on map[string]interface{} and []interface{}
10+
// compareJSONFields is the entry point for comparing two interface values
11+
// handle string with special cases, map[string]interface{} and []interface{} or any other primitive type
1212
func compareJSONFields(requestValue, cassetteValue interface{}) bool {
1313
switch requestValue := requestValue.(type) {
1414
case string:
@@ -43,17 +43,6 @@ func compareJSONBodies(request, cassette map[string]interface{}) bool {
4343
}
4444
}
4545

46-
// TODO: record back ipam/TestAccIPAMIPReverseDNS_Basic
47-
48-
// for key, cassetteValue := range cassette {
49-
// if _, ok := request[key]; !ok && cassetteValue != nil {
50-
// // Fails match if cassettes contains a field not in actual requests
51-
// // Fields should not disappear from requests unless a sdk breaking change
52-
// // We ignore if field is nil in cassette as it could be an old deprecated and unused field
53-
// return false
54-
// }
55-
// }
56-
5746
return true
5847
}
5948

@@ -150,9 +139,8 @@ func compareStringSlices(request, cassette []string) bool {
150139
return true
151140
}
152141

153-
// compareJSONSlices compares two slices of interface{}
154-
// if the slices are comparable (string or float64), it will sort them and compare them
155-
// it returns true in case of slice of map[string]interface{} because it is impossible to sort
142+
// compareSlices compares two slices of interface{}
143+
// in case of slice of map[string]interface{}, it will attempt to find a match in the other slice without taking into account the order
156144
func compareSlices(request, cassette []interface{}) bool {
157145
if len(request) != len(cassette) {
158146
return false
@@ -168,6 +156,7 @@ func compareSlices(request, cassette []interface{}) bool {
168156
for i, v := range request {
169157
requestStrings[i] = v.(string)
170158
}
159+
171160
cassetteStrings := make([]string, len(cassette))
172161
for i, v := range cassette {
173162
cassetteStrings[i] = v.(string)
@@ -190,7 +179,43 @@ func compareSlices(request, cassette []interface{}) bool {
190179

191180
return true
192181
case map[string]interface{}:
193-
return true
182+
// compare list of maps[string]interface{} without order and without ignored keys
183+
matched := 0
184+
185+
reqVisited := make([]bool, len(request))
186+
casVisited := make([]bool, len(cassette))
187+
188+
for i := range request {
189+
if reqVisited[i] {
190+
continue
191+
}
192+
193+
// cleanup ignored keys
194+
for _, key := range BodyMatcherIgnore {
195+
removeKeyRecursive(request[i].(map[string]interface{}), key)
196+
}
197+
198+
for j := range cassette {
199+
if casVisited[j] {
200+
continue
201+
}
202+
203+
// cleanup ignored keys
204+
for _, key := range BodyMatcherIgnore {
205+
removeKeyRecursive(cassette[j].(map[string]interface{}), key)
206+
}
207+
208+
if compareJSONFields(request[i], cassette[j]) {
209+
matched++
210+
reqVisited[i] = true
211+
casVisited[j] = true
212+
213+
break
214+
}
215+
}
216+
}
217+
218+
return matched == len(request)
194219
default:
195220
return reflect.DeepEqual(request, cassette)
196221
}

0 commit comments

Comments
 (0)