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