github.com/bingoohuang/gg@v0.0.0-20240325092523-45da7dee9335/pkg/resty/resty_test.go (about) 1 // Copyright (c) 2015-2021 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. 2 // resty source code and usage is governed by a MIT style 3 // license that can be found in the LICENSE file. 4 5 package resty 6 7 import ( 8 "compress/gzip" 9 "encoding/base64" 10 "encoding/json" 11 "encoding/xml" 12 "fmt" 13 "io" 14 "net/http" 15 "net/http/httptest" 16 "os" 17 "path/filepath" 18 "strconv" 19 "strings" 20 "sync/atomic" 21 "testing" 22 "time" 23 24 "github.com/stretchr/testify/assert" 25 ) 26 27 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 28 // Testing Unexported methods 29 //___________________________________ 30 31 func getTestDataPath() string { 32 pwd, _ := os.Getwd() 33 return filepath.Join(pwd, "testdata") 34 } 35 36 func createGetServer(t *testing.T) *httptest.Server { 37 var attempt int32 38 var sequence int32 39 var lastRequest time.Time 40 ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { 41 t.Logf("Method: %v", r.Method) 42 t.Logf("Path: %v", r.URL.Path) 43 44 if r.Method == http.MethodGet { 45 switch r.URL.Path { 46 case "/": 47 _, _ = w.Write([]byte("TestGet: text response")) 48 case "/no-content": 49 _, _ = w.Write([]byte("")) 50 case "/json": 51 w.Header().Set("Content-Type", "application/json") 52 _, _ = w.Write([]byte(`{"TestGet": "JSON response"}`)) 53 case "/json-invalid": 54 w.Header().Set("Content-Type", "application/json") 55 _, _ = w.Write([]byte("TestGet: Invalid JSON")) 56 case "/long-text": 57 _, _ = w.Write([]byte("TestGet: text response with size > 30")) 58 case "/long-json": 59 w.Header().Set("Content-Type", "application/json") 60 _, _ = w.Write([]byte(`{"TestGet": "JSON response with size > 30"}`)) 61 case "/mypage": 62 w.WriteHeader(http.StatusBadRequest) 63 case "/mypage2": 64 _, _ = w.Write([]byte("TestGet: text response from mypage2")) 65 case "/set-retrycount-test": 66 attp := atomic.AddInt32(&attempt, 1) 67 if attp <= 4 { 68 time.Sleep(time.Second * 6) 69 } 70 _, _ = w.Write([]byte("TestClientRetry page")) 71 case "/set-retrywaittime-test": 72 // Returns time.Duration since last request here 73 // or 0 for the very first request 74 if atomic.LoadInt32(&attempt) == 0 { 75 lastRequest = time.Now() 76 _, _ = fmt.Fprint(w, "0") 77 } else { 78 now := time.Now() 79 sinceLastRequest := now.Sub(lastRequest) 80 lastRequest = now 81 _, _ = fmt.Fprintf(w, "%d", uint64(sinceLastRequest)) 82 } 83 atomic.AddInt32(&attempt, 1) 84 85 case "/set-retry-error-recover": 86 w.Header().Set(hdrContentTypeKey, "application/json; charset=utf-8") 87 if atomic.LoadInt32(&attempt) == 0 { 88 w.WriteHeader(http.StatusTooManyRequests) 89 _, _ = w.Write([]byte(`{ "message": "too many" }`)) 90 } else { 91 _, _ = w.Write([]byte(`{ "message": "hello" }`)) 92 } 93 atomic.AddInt32(&attempt, 1) 94 case "/set-timeout-test-with-sequence": 95 seq := atomic.AddInt32(&sequence, 1) 96 time.Sleep(time.Second * 2) 97 _, _ = fmt.Fprintf(w, "%d", seq) 98 case "/set-timeout-test": 99 time.Sleep(time.Second * 6) 100 _, _ = w.Write([]byte("TestClientTimeout page")) 101 case "/my-image.png": 102 fileBytes, _ := os.ReadFile(filepath.Join(getTestDataPath(), "test-img.png")) 103 w.Header().Set("Content-Type", "image/png") 104 w.Header().Set("Content-Length", strconv.Itoa(len(fileBytes))) 105 _, _ = w.Write(fileBytes) 106 case "/get-method-payload-test": 107 body, err := io.ReadAll(r.Body) 108 if err != nil { 109 t.Errorf("Error: could not read get body: %s", err.Error()) 110 } 111 _, _ = w.Write(body) 112 case "/host-header": 113 _, _ = w.Write([]byte(r.Host)) 114 } 115 116 switch { 117 case strings.HasPrefix(r.URL.Path, "/v1/users/sample@sample.com/100002"): 118 if strings.HasSuffix(r.URL.Path, "details") { 119 _, _ = w.Write([]byte("TestGetPathParams: text response: " + r.URL.String())) 120 } else { 121 _, _ = w.Write([]byte("TestPathParamURLInput: text response: " + r.URL.String())) 122 } 123 } 124 125 } 126 }) 127 128 return ts 129 } 130 131 func handleLoginEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request) { 132 if r.URL.Path == "/login" { 133 user := &User{} 134 135 // JSON 136 if IsJSONType(r.Header.Get(hdrContentTypeKey)) { 137 jd := json.NewDecoder(r.Body) 138 err := jd.Decode(user) 139 if r.URL.Query().Get("ct") == "problem" { 140 w.Header().Set(hdrContentTypeKey, "application/problem+json; charset=utf-8") 141 } else if r.URL.Query().Get("ct") == "rpc" { 142 w.Header().Set(hdrContentTypeKey, "application/json-rpc") 143 } else { 144 w.Header().Set(hdrContentTypeKey, "application/json") 145 } 146 147 if err != nil { 148 t.Logf("Error: %#v", err) 149 w.WriteHeader(http.StatusBadRequest) 150 _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Unable to read user info" }`)) 151 return 152 } 153 154 if user.Username == "testuser" && user.Password == "testpass" { 155 _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) 156 } else if user.Username == "testuser" && user.Password == "invalidjson" { 157 _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful", }`)) 158 } else { 159 w.WriteHeader(http.StatusUnauthorized) 160 _, _ = w.Write([]byte(`{ "id": "unauthorized", "message": "Invalid credentials" }`)) 161 } 162 163 return 164 } 165 166 // XML 167 if IsXMLType(r.Header.Get(hdrContentTypeKey)) { 168 xd := xml.NewDecoder(r.Body) 169 err := xd.Decode(user) 170 171 w.Header().Set(hdrContentTypeKey, "application/xml") 172 if err != nil { 173 t.Logf("Error: %v", err) 174 w.WriteHeader(http.StatusBadRequest) 175 _, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>`)) 176 _, _ = w.Write([]byte(`<AuthError><Id>bad_request</Id><Message>Unable to read user info</Message></AuthError>`)) 177 return 178 } 179 180 if user.Username == "testuser" && user.Password == "testpass" { 181 _, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>`)) 182 _, _ = w.Write([]byte(`<AuthSuccess><Id>success</Id><Message>login successful</Message></AuthSuccess>`)) 183 } else if user.Username == "testuser" && user.Password == "invalidxml" { 184 _, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>`)) 185 _, _ = w.Write([]byte(`<AuthSuccess><Id>success</Id><Message>login successful</AuthSuccess>`)) 186 } else { 187 w.Header().Set("Www-Authenticate", "Protected Realm") 188 w.WriteHeader(http.StatusUnauthorized) 189 _, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?>`)) 190 _, _ = w.Write([]byte(`<AuthError><Id>unauthorized</Id><Message>Invalid credentials</Message></AuthError>`)) 191 } 192 193 return 194 } 195 } 196 } 197 198 func handleUsersEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request) { 199 if r.URL.Path == "/users" { 200 // JSON 201 if IsJSONType(r.Header.Get(hdrContentTypeKey)) { 202 var users []ExampleUser 203 jd := json.NewDecoder(r.Body) 204 err := jd.Decode(&users) 205 w.Header().Set(hdrContentTypeKey, "application/json") 206 if err != nil { 207 t.Logf("Error: %v", err) 208 w.WriteHeader(http.StatusBadRequest) 209 _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Unable to read user info" }`)) 210 return 211 } 212 213 // logic check, since we are excepting to reach 3 records 214 if len(users) != 3 { 215 t.Log("Error: Excepted count of 3 records") 216 w.WriteHeader(http.StatusBadRequest) 217 _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Expected record count doesn't match" }`)) 218 return 219 } 220 221 eu := users[2] 222 if eu.FirstName == "firstname3" && eu.ZipCode == "10003" { 223 w.WriteHeader(http.StatusAccepted) 224 _, _ = w.Write([]byte(`{ "message": "Accepted" }`)) 225 } 226 227 return 228 } 229 } 230 } 231 232 func createPostServer(t *testing.T) *httptest.Server { 233 ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { 234 t.Logf("Method: %v", r.Method) 235 t.Logf("Path: %v", r.URL.Path) 236 t.Logf("RawQuery: %v", r.URL.RawQuery) 237 t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) 238 239 if r.Method == http.MethodPost { 240 handleLoginEndpoint(t, w, r) 241 242 handleUsersEndpoint(t, w, r) 243 244 if r.URL.Path == "/login-json-html" { 245 w.Header().Set(hdrContentTypeKey, "text/html") 246 w.WriteHeader(http.StatusOK) 247 _, _ = w.Write([]byte(`<htm><body>Test JSON request with HTML response</body></html>`)) 248 return 249 } 250 251 if r.URL.Path == "/usersmap" { 252 // JSON 253 if IsJSONType(r.Header.Get(hdrContentTypeKey)) { 254 if r.URL.Query().Get("status") == "500" { 255 body, err := io.ReadAll(r.Body) 256 if err != nil { 257 t.Errorf("Error: could not read post body: %s", err.Error()) 258 } 259 t.Logf("Got query param: status=500 so we're returning the post body as response and a 500 status code. body: %s", string(body)) 260 w.Header().Set(hdrContentTypeKey, "application/json; charset=utf-8") 261 w.WriteHeader(http.StatusInternalServerError) 262 _, _ = w.Write(body) 263 return 264 } 265 266 var users []map[string]interface{} 267 jd := json.NewDecoder(r.Body) 268 err := jd.Decode(&users) 269 w.Header().Set(hdrContentTypeKey, "application/json; charset=utf-8") 270 if err != nil { 271 t.Logf("Error: %v", err) 272 w.WriteHeader(http.StatusBadRequest) 273 _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Unable to read user info" }`)) 274 return 275 } 276 277 // logic check, since we are excepting to reach 1 map records 278 if len(users) != 1 { 279 t.Log("Error: Excepted count of 1 map records") 280 w.WriteHeader(http.StatusBadRequest) 281 _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Expected record count doesn't match" }`)) 282 return 283 } 284 285 w.WriteHeader(http.StatusAccepted) 286 _, _ = w.Write([]byte(`{ "message": "Accepted" }`)) 287 288 return 289 } 290 } else if r.URL.Path == "/redirect" { 291 w.Header().Set(hdrLocationKey, "/login") 292 w.WriteHeader(http.StatusTemporaryRedirect) 293 } 294 } 295 }) 296 297 return ts 298 } 299 300 func createFormPostServer(t *testing.T) *httptest.Server { 301 ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { 302 t.Logf("Method: %v", r.Method) 303 t.Logf("Path: %v", r.URL.Path) 304 t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) 305 306 if r.Method == http.MethodPost { 307 _ = r.ParseMultipartForm(10e6) 308 309 if r.URL.Path == "/profile" { 310 t.Logf("FirstName: %v", r.FormValue("first_name")) 311 t.Logf("LastName: %v", r.FormValue("last_name")) 312 t.Logf("City: %v", r.FormValue("city")) 313 t.Logf("Zip Code: %v", r.FormValue("zip_code")) 314 315 _, _ = w.Write([]byte("Success")) 316 return 317 } else if r.URL.Path == "/search" { 318 formEncodedData := r.Form.Encode() 319 t.Logf("Received Form Encoded values: %v", formEncodedData) 320 321 assert.Equal(t, true, strings.Contains(formEncodedData, "search_criteria=pencil")) 322 assert.Equal(t, true, strings.Contains(formEncodedData, "search_criteria=glass")) 323 324 _, _ = w.Write([]byte("Success")) 325 return 326 } else if r.URL.Path == "/upload" { 327 t.Logf("FirstName: %v", r.FormValue("first_name")) 328 t.Logf("LastName: %v", r.FormValue("last_name")) 329 330 targetPath := filepath.Join(getTestDataPath(), "upload") 331 _ = os.MkdirAll(targetPath, 0o700) 332 333 for _, fhdrs := range r.MultipartForm.File { 334 for _, hdr := range fhdrs { 335 t.Logf("Name: %v", hdr.Filename) 336 t.Logf("Header: %v", hdr.Header) 337 dotPos := strings.LastIndex(hdr.Filename, ".") 338 339 fname := fmt.Sprintf("%s-%v%s", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:]) 340 t.Logf("Write name: %v", fname) 341 342 infile, _ := hdr.Open() 343 f, err := os.OpenFile(filepath.Join(targetPath, fname), os.O_WRONLY|os.O_CREATE, 0o666) 344 if err != nil { 345 t.Logf("Error: %v", err) 346 return 347 } 348 defer func() { 349 _ = f.Close() 350 }() 351 _, _ = io.Copy(f, infile) 352 353 _, _ = w.Write([]byte(fmt.Sprintf("File: %v, uploaded as: %v\n", hdr.Filename, fname))) 354 } 355 } 356 357 return 358 } 359 } 360 }) 361 362 return ts 363 } 364 365 func createFilePostServer(t *testing.T) *httptest.Server { 366 ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { 367 t.Logf("Method: %v", r.Method) 368 t.Logf("Path: %v", r.URL.Path) 369 t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) 370 371 if r.Method != http.MethodPost { 372 t.Log("createPostServer:: Not a Post request") 373 w.WriteHeader(http.StatusBadRequest) 374 fmt.Fprint(w, http.StatusText(http.StatusBadRequest)) 375 return 376 } 377 378 targetPath := filepath.Join(getTestDataPath(), "upload-large") 379 _ = os.MkdirAll(targetPath, 0o700) 380 defer cleanupFiles(targetPath) 381 382 switch r.URL.Path { 383 case "/upload": 384 f, err := os.OpenFile(filepath.Join(targetPath, "large-file.png"), 385 os.O_WRONLY|os.O_CREATE, 0o666) 386 if err != nil { 387 t.Logf("Error: %v", err) 388 return 389 } 390 defer func() { 391 _ = f.Close() 392 }() 393 size, _ := io.Copy(f, r.Body) 394 395 fmt.Fprintf(w, "File Uploaded successfully, file size: %v", size) 396 } 397 }) 398 399 return ts 400 } 401 402 func createAuthServer(t *testing.T) *httptest.Server { 403 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 404 t.Logf("Method: %v", r.Method) 405 t.Logf("Path: %v", r.URL.Path) 406 t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) 407 408 if r.Method == http.MethodGet { 409 if r.URL.Path == "/profile" { 410 // 004DDB79-6801-4587-B976-F093E6AC44FF 411 auth := r.Header.Get("Authorization") 412 t.Logf("Bearer Auth: %v", auth) 413 414 w.Header().Set(hdrContentTypeKey, "application/json; charset=utf-8") 415 416 if !strings.HasPrefix(auth, "Bearer ") { 417 w.Header().Set("Www-Authenticate", "Protected Realm") 418 w.WriteHeader(http.StatusUnauthorized) 419 _, _ = w.Write([]byte(`{ "id": "unauthorized", "message": "Invalid credentials" }`)) 420 421 return 422 } 423 424 if auth[7:] == "004DDB79-6801-4587-B976-F093E6AC44FF" || auth[7:] == "004DDB79-6801-4587-B976-F093E6AC44FF-Request" { 425 _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) 426 } 427 } 428 429 return 430 } 431 432 if r.Method == http.MethodPost { 433 if r.URL.Path == "/login" { 434 auth := r.Header.Get("Authorization") 435 t.Logf("Basic Auth: %v", auth) 436 437 w.Header().Set(hdrContentTypeKey, "application/json; charset=utf-8") 438 439 password, err := base64.StdEncoding.DecodeString(auth[6:]) 440 if err != nil || string(password) != "myuser:basicauth" { 441 w.Header().Set("Www-Authenticate", "Protected Realm") 442 w.WriteHeader(http.StatusUnauthorized) 443 _, _ = w.Write([]byte(`{ "id": "unauthorized", "message": "Invalid credentials" }`)) 444 445 return 446 } 447 448 _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) 449 } 450 451 return 452 } 453 })) 454 455 return ts 456 } 457 458 func createGenServer(t *testing.T) *httptest.Server { 459 ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { 460 t.Logf("Method: %v", r.Method) 461 t.Logf("Path: %v", r.URL.Path) 462 463 if r.Method == http.MethodGet { 464 if r.URL.Path == "/json-no-set" { 465 // Set empty header value for testing, since Go server sets to 466 // text/plain; charset=utf-8 467 w.Header().Set(hdrContentTypeKey, "") 468 _, _ = w.Write([]byte(`{"response":"json response no content type set"}`)) 469 } else if r.URL.Path == "/gzip-test" { 470 w.Header().Set(hdrContentTypeKey, plainTextType) 471 w.Header().Set(hdrContentEncodingKey, "gzip") 472 zw := gzip.NewWriter(w) 473 _, _ = zw.Write([]byte("This is Gzip response testing")) 474 zw.Close() 475 } else if r.URL.Path == "/gzip-test-gziped-empty-body" { 476 w.Header().Set(hdrContentTypeKey, plainTextType) 477 w.Header().Set(hdrContentEncodingKey, "gzip") 478 zw := gzip.NewWriter(w) 479 // write gzipped empty body 480 _, _ = zw.Write([]byte("")) 481 zw.Close() 482 } else if r.URL.Path == "/gzip-test-no-gziped-body" { 483 w.Header().Set(hdrContentTypeKey, plainTextType) 484 w.Header().Set(hdrContentEncodingKey, "gzip") 485 // don't write body 486 } 487 488 return 489 } 490 491 if r.Method == http.MethodPut { 492 if r.URL.Path == "/plaintext" { 493 _, _ = w.Write([]byte("TestPut: plain text response")) 494 } else if r.URL.Path == "/json" { 495 w.Header().Set(hdrContentTypeKey, "application/json; charset=utf-8") 496 _, _ = w.Write([]byte(`{"response":"json response"}`)) 497 } else if r.URL.Path == "/xml" { 498 w.Header().Set(hdrContentTypeKey, "application/xml") 499 _, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><Response>XML response</Response>`)) 500 } 501 return 502 } 503 504 if r.Method == http.MethodOptions && r.URL.Path == "/options" { 505 w.Header().Set("Access-Control-Allow-Origin", "localhost") 506 w.Header().Set("Access-Control-Allow-Methods", "PUT, PATCH") 507 w.Header().Set("Access-Control-Expose-Headers", "x-go-resty-id") 508 w.WriteHeader(http.StatusOK) 509 return 510 } 511 512 if r.Method == http.MethodPatch && r.URL.Path == "/patch" { 513 w.WriteHeader(http.StatusOK) 514 return 515 } 516 517 if r.Method == "REPORT" && r.URL.Path == "/report" { 518 body, _ := io.ReadAll(r.Body) 519 if len(body) == 0 { 520 w.WriteHeader(http.StatusOK) 521 } 522 return 523 } 524 }) 525 526 return ts 527 } 528 529 func createRedirectServer(t *testing.T) *httptest.Server { 530 ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { 531 t.Logf("Method: %v", r.Method) 532 t.Logf("Path: %v", r.URL.Path) 533 534 if r.Method == http.MethodGet { 535 if strings.HasPrefix(r.URL.Path, "/redirect-host-check-") { 536 cntStr := strings.SplitAfter(r.URL.Path, "-")[3] 537 cnt, _ := strconv.Atoi(cntStr) 538 539 if cnt != 7 { // Testing hard stop via logical 540 if cnt >= 5 { 541 http.Redirect(w, r, "http://httpbin.org/get", http.StatusTemporaryRedirect) 542 } else { 543 http.Redirect(w, r, fmt.Sprintf("/redirect-host-check-%d", cnt+1), http.StatusTemporaryRedirect) 544 } 545 } 546 } else if strings.HasPrefix(r.URL.Path, "/redirect-") { 547 cntStr := strings.SplitAfter(r.URL.Path, "-")[1] 548 cnt, _ := strconv.Atoi(cntStr) 549 550 http.Redirect(w, r, fmt.Sprintf("/redirect-%d", cnt+1), http.StatusTemporaryRedirect) 551 } 552 } 553 }) 554 555 return ts 556 } 557 558 func createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server { 559 return httptest.NewServer(http.HandlerFunc(fn)) 560 } 561 562 func dc() *Client { 563 c := New(). 564 outputLogTo(io.Discard) 565 return c 566 } 567 568 func dcl() *Client { 569 c := New(). 570 SetDebug(true). 571 outputLogTo(io.Discard) 572 return c 573 } 574 575 func dcr() *Request { 576 return dc().R() 577 } 578 579 func dclr() *Request { 580 c := dc(). 581 SetDebug(true). 582 outputLogTo(io.Discard) 583 return c.R() 584 } 585 586 func logResponse(t *testing.T, resp *Response) { 587 t.Logf("Response Status: %v", resp.Status()) 588 t.Logf("Response Time: %v", resp.Time()) 589 t.Logf("Response Headers: %v", resp.Header()) 590 t.Logf("Response Cookies: %v", resp.Cookies()) 591 t.Logf("Response Body: %v", resp) 592 } 593 594 func cleanupFiles(files ...string) { 595 pwd, _ := os.Getwd() 596 597 for _, f := range files { 598 if filepath.IsAbs(f) { 599 _ = os.RemoveAll(f) 600 } else { 601 _ = os.RemoveAll(filepath.Join(pwd, f)) 602 } 603 } 604 }