@@ -2,8 +2,10 @@ package scaleway
22
33import (
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+
3443func 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
57145func 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.
0 commit comments