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