github.com/webdestroya/awsmocker@v0.2.6/mocked_request.go (about) 1 package awsmocker 2 3 import ( 4 "fmt" 5 "net/url" 6 "regexp" 7 "strings" 8 "sync" 9 10 "golang.org/x/exp/maps" 11 "golang.org/x/exp/slices" 12 ) 13 14 // Describes a request that should be matched 15 type MockedRequest struct { 16 // Require that fields are matched exactly 17 // 18 // Nonstrict (default) means that Params listed are matched against 19 // the request to ensure the ones specified match 20 // 21 // Strict mode requires that the request contain ONLY the params listed 22 // any extra parameters will cause the request to fail to match 23 Strict bool 24 25 // The hostname only. Does not include the port 26 Hostname string 27 28 // The AWS service shortcode 29 Service string 30 31 // The AWS API Action being performed 32 Action string 33 34 // Body to match against 35 Body string 36 37 // Match against specific parameters in the request. 38 // This is only used for XML/Form requests (not the newer JSON ones) 39 Params url.Values 40 41 // Match a specific HTTP method 42 Method string 43 44 // Match the URL path 45 Path string 46 47 // Match the URL path, using a regex 48 PathRegex *regexp.Regexp 49 50 // Is this an instance metadata request? 51 // setting this to true will match against both the IPv4 and IPv6 hostnames 52 IsEc2IMDS bool 53 54 // Matches a JSON request body by resolving the jmespath expression as keys 55 // and comparing the values returned against the value provided in the map 56 JMESPathMatches map[string]any 57 58 // Write a custom matcher function that will be used to match a request. 59 // this runs after checking the other fields, so you can use those as filters. 60 Matcher func(*ReceivedRequest) bool 61 62 // Stop matching this request after it has been matched X times 63 // 64 // 0 (default) means it will live forever 65 MaxMatchCount int 66 67 // number of times this request has matched 68 matchCount int64 69 mu sync.Mutex 70 } 71 72 func (mr *MockedRequest) prep() { 73 74 } 75 76 func (m *MockedRequest) incMatchCount() { 77 m.mu.Lock() 78 defer m.mu.Unlock() 79 m.matchCount += 1 80 } 81 82 // Returns a string to help identify this MockedRequest 83 func (m *MockedRequest) Inspect() string { 84 parts := make([]string, 0, 10) 85 86 if m.Strict { 87 parts = append(parts, "STRICT") 88 } 89 90 if m.Service != "" { 91 parts = append(parts, fmt.Sprintf("Service=%s", m.Service)) 92 } 93 94 if m.Action != "" { 95 parts = append(parts, fmt.Sprintf("Action=%s", m.Action)) 96 } 97 98 if m.IsEc2IMDS { 99 parts = append(parts, fmt.Sprintf("imds=%t", m.IsEc2IMDS)) 100 } 101 102 if m.Hostname != "" { 103 parts = append(parts, fmt.Sprintf("Hostname=%s", m.Hostname)) 104 } 105 106 if m.Path != "" { 107 parts = append(parts, fmt.Sprintf("Path=%s", m.Path)) 108 } 109 110 if m.Method != "" { 111 parts = append(parts, fmt.Sprintf("Method=%s", m.Method)) 112 } 113 114 if m.PathRegex != nil { 115 parts = append(parts, fmt.Sprintf("PathRegex=%s", m.PathRegex.String())) 116 } 117 118 if len(m.Params) > 0 { 119 parts = append(parts, fmt.Sprintf("Params=%s", m.Params.Encode())) 120 } 121 122 if m.Body != "" { 123 parts = append(parts, fmt.Sprintf("Body=%s", m.Body)) 124 } 125 126 return "MReq<" + strings.Join(parts, " ") + ">" 127 } 128 129 func (m *MockedRequest) matchRequest(rr *ReceivedRequest) bool { 130 131 if m.MaxMatchCount > 0 && m.matchCount >= int64(m.MaxMatchCount) { 132 return false 133 } 134 135 if !m.matchRequestLazy(rr) { 136 return false 137 } 138 139 if m.Strict { 140 return m.matchRequestStrict(rr) 141 } 142 143 return true 144 } 145 146 func (m *MockedRequest) matchRequestLazy(rr *ReceivedRequest) bool { 147 148 if m.Hostname != "" && rr.Hostname != m.Hostname { 149 return false 150 } 151 152 if m.Service != "" && rr.Service != m.Service { 153 return false 154 } 155 156 if m.Action != "" && rr.Action != m.Action { 157 return false 158 } 159 160 if m.Path != "" && rr.Path != m.Path { 161 return false 162 } 163 164 if m.Method != "" && rr.HttpRequest.Method != m.Method { 165 return false 166 } 167 168 if m.IsEc2IMDS && !(rr.Hostname == imdsHost4 || rr.Hostname == imdsHost6) { 169 return false 170 } 171 172 if m.PathRegex != nil && !m.PathRegex.MatchString(rr.Path) { 173 return false 174 } 175 176 if m.JMESPathMatches != nil && len(m.JMESPathMatches) > 0 { 177 if ret := m.matchJmespath(rr); !ret { 178 return false 179 } 180 } 181 182 if m.Matcher != nil && !m.Matcher(rr) { 183 return false 184 } 185 186 if m.Params != nil && len(m.Params) > 0 { 187 // if the request has no params, it cant match something with params... 188 if rr.HttpRequest.Form == nil || len(rr.HttpRequest.Form) == 0 { 189 return false 190 } 191 192 for k, v := range m.Params { 193 if !rr.HttpRequest.Form.Has(k) { 194 // key is missing 195 return false 196 } 197 198 if !slices.Equal(v, rr.HttpRequest.Form[k]) { 199 return false 200 } 201 } 202 } 203 204 return true 205 } 206 207 func (m *MockedRequest) matchJmespath(rr *ReceivedRequest) bool { 208 // just bail out if there is nothing to match 209 if m.JMESPathMatches == nil || len(m.JMESPathMatches) == 0 { 210 return true 211 } 212 213 // you provided Jmes matchers, but this isnt a JSON payload, so it will never match 214 if rr.JsonPayload == nil { 215 return false 216 } 217 218 for k, v := range m.JMESPathMatches { 219 if !JMESMatch(rr.JsonPayload, k, v) { 220 // if any are false, then bail out checking 221 return false 222 } 223 } 224 225 return true 226 } 227 228 func (m *MockedRequest) matchRequestStrict(rr *ReceivedRequest) bool { 229 // assume the lazy check has already run 230 231 return maps.EqualFunc(rr.HttpRequest.Form, m.Params, func(v1, v2 []string) bool { 232 return slices.Equal(v1, v2) 233 }) 234 }