Skip to content

Commit 7d3d395

Browse files
committed
feat(acctest): compare slice of map[string]interface{}
1 parent 0417565 commit 7d3d395

File tree

2 files changed

+286
-8
lines changed

2 files changed

+286
-8
lines changed

internal/acctest/acctest_test.go

Lines changed: 175 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,138 @@ 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+
"Version": "2012-10-17"
292+
}
293+
`
294+
295+
var objectBodyCassette = `{
296+
"Id": "MyPolicy",
297+
"Statement": [
298+
{
299+
"Action": [
300+
"s3:ListBucket",
301+
"s3:GetObject"
302+
],
303+
"Effect": "Allow",
304+
"Principal": {
305+
"SCW": "*"
306+
},
307+
"Sid": "GrantToEveryone",
308+
"Resource": [
309+
"tf-tests-scw-obp-basic-1234567890",
310+
"tf-tests-scw-obp-basic-1234567890/*"
311+
]
312+
}
313+
],
314+
"Version": "2012-10-17"
315+
}
316+
`
317+
185318
// we don't use httptest.NewRequest because it does not set the GetBody func
186319
func newRequest(method, url string, body io.Reader) *http.Request {
187320
req, err := http.NewRequestWithContext(context.Background(), method, url, body)
@@ -197,7 +330,7 @@ var testBodyMatcherCases = []struct {
197330
cassetteBody *cassette.Request
198331
shouldMatch bool
199332
}{
200-
// create bar compare with foo
333+
// bar does not match foo
201334
{
202335
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(barMemberCreationBody)),
203336
cassetteBody: &cassette.Request{
@@ -208,7 +341,7 @@ var testBodyMatcherCases = []struct {
208341
},
209342
shouldMatch: false,
210343
},
211-
// create bar compare with bar
344+
// bar matches bar
212345
{
213346
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(barMemberCreationBody)),
214347
cassetteBody: &cassette.Request{
@@ -231,8 +364,7 @@ var testBodyMatcherCases = []struct {
231364
shouldMatch: true,
232365
},
233366
// 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{})
367+
// in such simple cases we flatten the maps and compare them in same order
236368
{
237369
requestBody: newRequest(http.MethodPatch, "https://api.scaleway.com/secrets/v1/secrets/123", strings.NewReader(secretPatchBodyRequest)),
238370
cassetteBody: &cassette.Request{
@@ -243,7 +375,29 @@ var testBodyMatcherCases = []struct {
243375
},
244376
shouldMatch: true,
245377
},
246-
// compare nested slices of different integers
378+
// flatten the maps and compare them in same order
379+
{
380+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(requestInstanceSettings)),
381+
cassetteBody: &cassette.Request{
382+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
383+
Method: http.MethodPost,
384+
Body: cassetteInstanceSettings,
385+
ContentLength: int64(len(cassetteInstanceSettings)),
386+
},
387+
shouldMatch: true,
388+
},
389+
// complex slice of maps case
390+
{
391+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/policies", strings.NewReader(objectBodyRequest)),
392+
cassetteBody: &cassette.Request{
393+
URL: "https://api.scaleway.com/iam/v1alpha1/policies",
394+
Method: http.MethodPost,
395+
Body: objectBodyCassette,
396+
ContentLength: int64(len(objectBodyCassette)),
397+
},
398+
shouldMatch: true,
399+
},
400+
// compare slices of different integers
247401
{
248402
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(integertestBodyRequest)),
249403
cassetteBody: &cassette.Request{
@@ -254,7 +408,7 @@ var testBodyMatcherCases = []struct {
254408
},
255409
shouldMatch: false,
256410
},
257-
// compare nested slices of same integers in different order
411+
// compare slices of same integers in different order
258412
{
259413
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(integerBodyRequestOutOfOrder)),
260414
cassetteBody: &cassette.Request{
@@ -265,7 +419,7 @@ var testBodyMatcherCases = []struct {
265419
},
266420
shouldMatch: true,
267421
},
268-
// compare nested slices of slices of strings
422+
// compare slices of slices of strings in different order
269423
{
270424
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(nestedSliceOfSlicesRequest)),
271425
cassetteBody: &cassette.Request{
@@ -286,7 +440,7 @@ var testBodyMatcherCases = []struct {
286440
},
287441
shouldMatch: true,
288442
},
289-
// compare simple slice of strings
443+
// compare slices of strings in different order
290444
{
291445
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(simpleSliceOfStringsRequest)),
292446
cassetteBody: &cassette.Request{
@@ -297,13 +451,26 @@ var testBodyMatcherCases = []struct {
297451
},
298452
shouldMatch: true,
299453
},
454+
// ludacris does not match jdilla
455+
{
456+
requestBody: newRequest(http.MethodPost, "https://api.scaleway.com/iam/v1alpha1/users", strings.NewReader(ludacrisBodyRequest)),
457+
cassetteBody: &cassette.Request{
458+
URL: "https://api.scaleway.com/iam/v1alpha1/users",
459+
Method: http.MethodPost,
460+
Body: jdillaBodyCassette,
461+
ContentLength: int64(len(jdillaBodyCassette)),
462+
},
463+
shouldMatch: false,
464+
},
300465
}
301466

302467
func TestCassetteMatcher(t *testing.T) {
303468
for i, test := range testBodyMatcherCases {
304469
shouldMatch := acctest.CassetteMatcher(test.requestBody, *test.cassetteBody)
305470
if shouldMatch != test.shouldMatch {
306471
t.Errorf("test %d: expected %v, got %v", i, test.shouldMatch, shouldMatch)
472+
t.Errorf("requestBody: %s", test.requestBody.Body)
473+
t.Errorf("cassetteBody: %s", test.cassetteBody.Body)
307474
}
308475
}
309476
}

internal/acctest/compare.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
package acctest
22

33
import (
4+
"errors"
5+
"fmt"
46
"net/url"
57
"reflect"
68
"sort"
79
"strings"
810
)
911

12+
var (
13+
ErrKeyIndex = errors.New("key index not found")
14+
ErrValueIndex = errors.New("value index not found")
15+
ErrNotComparable = errors.New("not comparable")
16+
supportedMapKeys = []string{"key", "name"}
17+
supportedMapValues = []string{"value", "values"}
18+
)
19+
1020
// compareJSONFields compare two given json fields
1121
// it will recurse on map[string]interface{} and []interface{}
1222
func compareJSONFields(requestValue, cassetteValue interface{}) bool {
@@ -168,6 +178,7 @@ func compareSlices(request, cassette []interface{}) bool {
168178
for i, v := range request {
169179
requestStrings[i] = v.(string)
170180
}
181+
171182
cassetteStrings := make([]string, len(cassette))
172183
for i, v := range cassette {
173184
cassetteStrings[i] = v.(string)
@@ -190,8 +201,108 @@ func compareSlices(request, cassette []interface{}) bool {
190201

191202
return true
192203
case map[string]interface{}:
204+
// potential corner cases:
205+
// - the maps have the same structure to identify key value pairs: we normalize and compare them
206+
// - the maps have different structure: we recurse on compareJSONFields to individually compare them
207+
requestMap, cassetteMap, err := normalizeMaps(request, cassette)
208+
if err != nil {
209+
for i, v := range request {
210+
if !compareJSONFields(v, cassette[i]) {
211+
return false
212+
}
213+
}
214+
215+
return true
216+
}
217+
218+
for k := range requestMap {
219+
if _, ok := cassetteMap[k]; !ok {
220+
return false
221+
}
222+
223+
if !compareJSONFields(requestMap[k], cassetteMap[k]) {
224+
return false
225+
}
226+
}
227+
193228
return true
194229
default:
195230
return reflect.DeepEqual(request, cassette)
196231
}
197232
}
233+
234+
// normalizeMaps normalize 2 lists of simple maps
235+
// from [{ "name": "foo", "value": "bar" }, { "name": "baz", "value": "qux" }] to { "foo": "bar", "baz": "qux" }
236+
// returns KeyIndex and ValueIndex errors if the supported key or value are not found
237+
func normalizeMaps(request, cassette []interface{}) (requestMap, cassetteMap map[string]interface{}, err error) {
238+
requestMap = make(map[string]interface{})
239+
cassetteMap = make(map[string]interface{})
240+
241+
for _, v := range request {
242+
rmap, ok := v.(map[string]interface{})
243+
if !ok {
244+
return nil, nil, ErrNotComparable
245+
}
246+
247+
var keyIndex, key string
248+
for _, key = range supportedMapKeys {
249+
if keyIndex, ok = rmap[key].(string); ok {
250+
break
251+
}
252+
}
253+
254+
if !ok {
255+
return nil, nil, fmt.Errorf("%w: %s", ErrKeyIndex, key)
256+
}
257+
258+
var value interface{}
259+
260+
var valueKey string
261+
for _, valueKey = range supportedMapValues {
262+
if value, ok = rmap[valueKey]; ok {
263+
break
264+
}
265+
}
266+
267+
if !ok {
268+
return nil, nil, fmt.Errorf("%w: %s", ErrValueIndex, valueKey)
269+
}
270+
271+
requestMap[keyIndex] = value
272+
}
273+
274+
for _, v := range cassette {
275+
cmap, ok := v.(map[string]interface{})
276+
if !ok {
277+
return nil, nil, ErrNotComparable
278+
}
279+
280+
var keyIndex, key string
281+
for _, key = range supportedMapKeys {
282+
if keyIndex, ok = cmap[key].(string); ok {
283+
break
284+
}
285+
}
286+
287+
if !ok {
288+
return nil, nil, fmt.Errorf("%w: %s", ErrKeyIndex, key)
289+
}
290+
291+
var value interface{}
292+
293+
var valueKey string
294+
for _, valueKey = range supportedMapValues {
295+
if value, ok = cmap[valueKey]; ok {
296+
break
297+
}
298+
}
299+
300+
if !ok {
301+
return nil, nil, fmt.Errorf("%w: %s", ErrValueIndex, valueKey)
302+
}
303+
304+
cassetteMap[keyIndex] = value
305+
}
306+
307+
return requestMap, cassetteMap, nil
308+
}

0 commit comments

Comments
 (0)