1
16
17 package http
18
19 import (
20 "fmt"
21 "net"
22 "net/url"
23 "strings"
24 "testing"
25 "time"
26
27 "sigs.k8s.io/gateway-api/conformance/utils/config"
28 "sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
29 )
30
31
32 type ExpectedResponse struct {
33
34 Request Request
35
36
37
38
39
40 ExpectedRequest *ExpectedRequest
41
42 RedirectRequest *roundtripper.RedirectRequest
43
44
45
46 BackendSetResponseHeaders map[string]string
47
48
49
50 Response Response
51
52 Backend string
53 Namespace string
54
55
56 MirroredTo []BackendRef
57
58
59 TestCaseName string
60 }
61
62
63
64
65 type Request struct {
66 Host string
67 Method string
68 Path string
69 Headers map[string]string
70 UnfollowRedirect bool
71 Protocol string
72 }
73
74
75 type ExpectedRequest struct {
76 Request
77
78
79
80 AbsentHeaders []string
81 }
82
83
84 type Response struct {
85 StatusCode int
86 Headers map[string]string
87 AbsentHeaders []string
88 }
89
90 type BackendRef struct {
91 Name string
92 Namespace string
93 }
94
95
96
97
98
99
100 func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripper.RoundTripper, timeoutConfig config.TimeoutConfig, gwAddr string, expected ExpectedResponse) {
101 t.Helper()
102
103 req := MakeRequest(t, &expected, gwAddr, "HTTP", "http")
104
105 WaitForConsistentResponse(t, r, req, expected, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency)
106 }
107
108 func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, scheme string) roundtripper.Request {
109 t.Helper()
110
111 if expected.Request.Method == "" {
112 expected.Request.Method = "GET"
113 }
114
115 if expected.Response.StatusCode == 0 {
116 expected.Response.StatusCode = 200
117 }
118
119 if expected.Request.Protocol == "" {
120 expected.Request.Protocol = protocol
121 }
122
123 path, query, _ := strings.Cut(expected.Request.Path, "?")
124 reqURL := url.URL{Scheme: scheme, Host: CalculateHost(t, gwAddr, scheme), Path: path, RawQuery: query}
125
126 t.Logf("Making %s request to %s", expected.Request.Method, reqURL.String())
127
128 req := roundtripper.Request{
129 Method: expected.Request.Method,
130 Host: expected.Request.Host,
131 URL: reqURL,
132 Protocol: expected.Request.Protocol,
133 Headers: map[string][]string{},
134 UnfollowRedirect: expected.Request.UnfollowRedirect,
135 }
136
137 if expected.Request.Headers != nil {
138 for name, value := range expected.Request.Headers {
139 req.Headers[name] = []string{value}
140 }
141 }
142
143 backendSetHeaders := []string{}
144 for name, val := range expected.BackendSetResponseHeaders {
145 backendSetHeaders = append(backendSetHeaders, name+":"+val)
146 }
147 req.Headers["X-Echo-Set-Header"] = []string{strings.Join(backendSetHeaders, ",")}
148
149 return req
150 }
151
152
153
154
155
156
157 func CalculateHost(t *testing.T, gwAddr, scheme string) string {
158 host, port, err := net.SplitHostPort(gwAddr)
159 if err != nil && strings.Contains(err.Error(), "too many colons in address") {
160
161
162 gwAddr = "[" + gwAddr + "]"
163 host, port, err = net.SplitHostPort(gwAddr)
164 }
165 if err != nil {
166 t.Logf("Failed to parse host %q: %v", gwAddr, err)
167 return gwAddr
168 }
169 if strings.ToLower(scheme) == "http" && port == "80" {
170 return ipv6SafeHost(host)
171 }
172 if strings.ToLower(scheme) == "https" && port == "443" {
173 return ipv6SafeHost(host)
174 }
175 return gwAddr
176 }
177
178 func ipv6SafeHost(host string) string {
179
180
181
182
183 if strings.Contains(host, ":") {
184 return "[" + host + "]"
185 }
186 return host
187 }
188
189
190
191 func AwaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Duration, fn func(elapsed time.Duration) bool) {
192 successes := 0
193 attempts := 0
194 start := time.Now()
195 to := time.After(maxTimeToConsistency)
196 delay := time.Second
197 for {
198 select {
199 case <-to:
200 t.Fatalf("timeout while waiting after %d attempts", attempts)
201 default:
202 }
203
204 completed := fn(time.Now().Sub(start))
205 attempts++
206 if completed {
207 successes++
208 if successes >= threshold {
209 return
210 }
211
212 continue
213 }
214
215 successes = 0
216 select {
217
218 case <-to:
219 t.Fatalf("timeout while waiting after %d attempts, %d/%d successes", attempts, successes, threshold)
220
221 case <-time.After(delay):
222 }
223 }
224 }
225
226
227
228
229 func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req roundtripper.Request, expected ExpectedResponse, threshold int, maxTimeToConsistency time.Duration) {
230 AwaitConvergence(t, threshold, maxTimeToConsistency, func(elapsed time.Duration) bool {
231 cReq, cRes, err := r.CaptureRoundTrip(req)
232 if err != nil {
233 t.Logf("Request failed, not ready yet: %v (after %v)", err.Error(), elapsed)
234 return false
235 }
236
237 if err := CompareRequest(t, &req, cReq, cRes, expected); err != nil {
238 t.Logf("Response expectation failed for request: %+v not ready yet: %v (after %v)", req, err, elapsed)
239 return false
240 }
241
242 return true
243 })
244 t.Logf("Request passed")
245 }
246
247 func CompareRequest(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected ExpectedResponse) error {
248 if roundtripper.IsTimeoutError(cRes.StatusCode) {
249 if roundtripper.IsTimeoutError(expected.Response.StatusCode) {
250 return nil
251 }
252 }
253 if expected.Response.StatusCode != cRes.StatusCode {
254 return fmt.Errorf("expected status code to be %d, got %d", expected.Response.StatusCode, cRes.StatusCode)
255 }
256 if cRes.StatusCode == 200 {
257
258
259
260 if expected.ExpectedRequest == nil {
261 expected.ExpectedRequest = &ExpectedRequest{Request: expected.Request}
262 }
263
264 if expected.ExpectedRequest.Method == "" {
265 expected.ExpectedRequest.Method = "GET"
266 }
267
268 if expected.ExpectedRequest.Host != "" && expected.ExpectedRequest.Host != cReq.Host {
269 return fmt.Errorf("expected host to be %s, got %s", expected.ExpectedRequest.Host, cReq.Host)
270 }
271
272 if expected.ExpectedRequest.Path != cReq.Path {
273 return fmt.Errorf("expected path to be %s, got %s", expected.ExpectedRequest.Path, cReq.Path)
274 }
275 if expected.ExpectedRequest.Method != cReq.Method {
276 return fmt.Errorf("expected method to be %s, got %s", expected.ExpectedRequest.Method, cReq.Method)
277 }
278 if expected.Namespace != cReq.Namespace {
279 return fmt.Errorf("expected namespace to be %s, got %s", expected.Namespace, cReq.Namespace)
280 }
281 if expected.ExpectedRequest.Headers != nil {
282 if cReq.Headers == nil {
283 return fmt.Errorf("no headers captured, expected %v", len(expected.ExpectedRequest.Headers))
284 }
285 for name, val := range cReq.Headers {
286 cReq.Headers[strings.ToLower(name)] = val
287 }
288 for name, expectedVal := range expected.ExpectedRequest.Headers {
289 actualVal, ok := cReq.Headers[strings.ToLower(name)]
290 if !ok {
291 return fmt.Errorf("expected %s header to be set, actual headers: %v", name, cReq.Headers)
292 } else if strings.Join(actualVal, ",") != expectedVal {
293 return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ","))
294 }
295 }
296 }
297
298 if expected.Response.Headers != nil {
299 if cRes.Headers == nil {
300 return fmt.Errorf("no headers captured, expected %v", len(expected.ExpectedRequest.Headers))
301 }
302 for name, val := range cRes.Headers {
303 cRes.Headers[strings.ToLower(name)] = val
304 }
305
306 for name, expectedVal := range expected.Response.Headers {
307 actualVal, ok := cRes.Headers[strings.ToLower(name)]
308 if !ok {
309 return fmt.Errorf("expected %s header to be set, actual headers: %v", name, cRes.Headers)
310 } else if strings.Join(actualVal, ",") != expectedVal {
311 return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ","))
312 }
313 }
314 }
315
316 if len(expected.Response.AbsentHeaders) > 0 {
317 for name, val := range cRes.Headers {
318 cRes.Headers[strings.ToLower(name)] = val
319 }
320
321 for _, name := range expected.Response.AbsentHeaders {
322 val, ok := cRes.Headers[strings.ToLower(name)]
323 if ok {
324 return fmt.Errorf("expected %s header to not be set, got %s", name, val)
325 }
326 }
327 }
328
329
330
331 if len(expected.ExpectedRequest.AbsentHeaders) > 0 {
332 for name, val := range cReq.Headers {
333 cReq.Headers[strings.ToLower(name)] = val
334 }
335
336 for _, name := range expected.ExpectedRequest.AbsentHeaders {
337 val, ok := cReq.Headers[strings.ToLower(name)]
338 if ok {
339 return fmt.Errorf("expected %s header to not be set, got %s", name, val)
340 }
341 }
342 }
343
344 if !strings.HasPrefix(cReq.Pod, expected.Backend) {
345 return fmt.Errorf("expected pod name to start with %s, got %s", expected.Backend, cReq.Pod)
346 }
347 } else if roundtripper.IsRedirect(cRes.StatusCode) {
348 if expected.RedirectRequest == nil {
349 return nil
350 }
351
352 setRedirectRequestDefaults(req, cRes, &expected)
353
354 if expected.RedirectRequest.Host != cRes.RedirectRequest.Host {
355 return fmt.Errorf("expected redirected hostname to be %q, got %q", expected.RedirectRequest.Host, cRes.RedirectRequest.Host)
356 }
357
358 gotPort := cRes.RedirectRequest.Port
359 if expected.RedirectRequest.Port == "" {
360
361
362
363 if strings.ToLower(cRes.RedirectRequest.Scheme) == "http" && gotPort != "80" && gotPort != "" {
364 return fmt.Errorf("for http scheme, expected redirected port to be 80 or not set, got %q", gotPort)
365 }
366 if strings.ToLower(cRes.RedirectRequest.Scheme) == "https" && gotPort != "443" && gotPort != "" {
367 return fmt.Errorf("for https scheme, expected redirected port to be 443 or not set, got %q", gotPort)
368 }
369 t.Logf("Can't validate redirectPort for unrecognized scheme %v", cRes.RedirectRequest.Scheme)
370 } else if expected.RedirectRequest.Port != gotPort {
371
372
373 return fmt.Errorf("expected redirected port to be %q, got %q", expected.RedirectRequest.Port, gotPort)
374 }
375
376 if expected.RedirectRequest.Scheme != cRes.RedirectRequest.Scheme {
377 return fmt.Errorf("expected redirected scheme to be %q, got %q", expected.RedirectRequest.Scheme, cRes.RedirectRequest.Scheme)
378 }
379
380 if expected.RedirectRequest.Path != cRes.RedirectRequest.Path {
381 return fmt.Errorf("expected redirected path to be %q, got %q", expected.RedirectRequest.Path, cRes.RedirectRequest.Path)
382 }
383 }
384 return nil
385 }
386
387
388 func (er *ExpectedResponse) GetTestCaseName(i int) string {
389
390 if er.TestCaseName != "" {
391 return er.TestCaseName
392 }
393
394 headerStr := ""
395 reqStr := ""
396
397 if er.Request.Headers != nil {
398 headerStr = " with headers"
399 }
400
401 reqStr = fmt.Sprintf("%d request to '%s%s'%s", i, er.Request.Host, er.Request.Path, headerStr)
402
403 if er.Backend != "" {
404 return fmt.Sprintf("%s should go to %s", reqStr, er.Backend)
405 }
406 return fmt.Sprintf("%s should receive a %d", reqStr, er.Response.StatusCode)
407 }
408
409 func setRedirectRequestDefaults(req *roundtripper.Request, cRes *roundtripper.CapturedResponse, expected *ExpectedResponse) {
410
411
412 if expected.RedirectRequest.Host == "" {
413 expected.RedirectRequest.Host = cRes.RedirectRequest.Host
414 }
415
416 if expected.RedirectRequest.Scheme == "" {
417 expected.RedirectRequest.Scheme = req.URL.Scheme
418 }
419
420 if expected.RedirectRequest.Path == "" {
421 expected.RedirectRequest.Path = req.URL.Path
422 }
423 }
424
View as plain text