// Copyright (c) 2015-2019 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package resty import ( "compress/gzip" "encoding/base64" "encoding/json" "encoding/xml" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "strconv" "strings" "sync/atomic" "testing" "time" ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Testing Unexported methods //___________________________________ func getTestDataPath() string { pwd, _ := os.Getwd() return filepath.Join(pwd, ".testdata") } func createGetServer(t *testing.T) *httptest.Server { var attempt int32 var sequence int32 var lastRequest time.Time ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) if r.Method == MethodGet { switch r.URL.Path { case "/": _, _ = w.Write([]byte("TestGet: text response")) case "/no-content": _, _ = w.Write([]byte("")) case "/json": w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"TestGet": "JSON response"}`)) case "/json-invalid": w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte("TestGet: Invalid JSON")) case "/long-text": _, _ = w.Write([]byte("TestGet: text response with size > 30")) case "/long-json": w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"TestGet": "JSON response with size > 30"}`)) case "/mypage": w.WriteHeader(http.StatusBadRequest) case "/mypage2": _, _ = w.Write([]byte("TestGet: text response from mypage2")) case "/set-retrycount-test": attp := atomic.AddInt32(&attempt, 1) if attp <= 3 { time.Sleep(time.Second * 6) } _, _ = w.Write([]byte("TestClientRetry page")) case "/set-retrywaittime-test": // Returns time.Duration since last request here // or 0 for the very first request if atomic.LoadInt32(&attempt) == 0 { lastRequest = time.Now() _, _ = fmt.Fprint(w, "0") } else { now := time.Now() sinceLastRequest := now.Sub(lastRequest) lastRequest = now _, _ = fmt.Fprintf(w, "%d", uint64(sinceLastRequest)) } atomic.AddInt32(&attempt, 1) case "/set-timeout-test-with-sequence": seq := atomic.AddInt32(&sequence, 1) time.Sleep(time.Second * 2) _, _ = fmt.Fprintf(w, "%d", seq) case "/set-timeout-test": time.Sleep(time.Second * 6) _, _ = w.Write([]byte("TestClientTimeout page")) case "/my-image.png": fileBytes, _ := ioutil.ReadFile(filepath.Join(getTestDataPath(), "test-img.png")) w.Header().Set("Content-Type", "image/png") w.Header().Set("Content-Length", strconv.Itoa(len(fileBytes))) _, _ = w.Write(fileBytes) case "/get-method-payload-test": body, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("Error: could not read get body: %s", err.Error()) } _, _ = w.Write(body) case "/host-header": _, _ = w.Write([]byte(r.Host)) } switch { case strings.HasPrefix(r.URL.Path, "/v1/users/sample@sample.com/100002"): if strings.HasSuffix(r.URL.Path, "details") { _, _ = w.Write([]byte("TestGetPathParams: text response: " + r.URL.String())) } else { _, _ = w.Write([]byte("TestPathParamURLInput: text response: " + r.URL.String())) } } } }) return ts } func handleLoginEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/login" { user := &User{} // JSON if IsJSONType(r.Header.Get(hdrContentTypeKey)) { jd := json.NewDecoder(r.Body) err := jd.Decode(user) if r.URL.Query().Get("ct") == "problem" { w.Header().Set(hdrContentTypeKey, "application/problem+json; charset=utf-8") } else if r.URL.Query().Get("ct") == "rpc" { w.Header().Set(hdrContentTypeKey, "application/json-rpc") } else { w.Header().Set(hdrContentTypeKey, jsonContentType) } if err != nil { t.Logf("Error: %#v", err) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Unable to read user info" }`)) return } if user.Username == "testuser" && user.Password == "testpass" { _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) } else if user.Username == "testuser" && user.Password == "invalidjson" { _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful", }`)) } else { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{ "id": "unauthorized", "message": "Invalid credentials" }`)) } return } // XML if IsXMLType(r.Header.Get(hdrContentTypeKey)) { xd := xml.NewDecoder(r.Body) err := xd.Decode(user) w.Header().Set(hdrContentTypeKey, "application/xml") if err != nil { t.Logf("Error: %v", err) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(``)) _, _ = w.Write([]byte(`bad_requestUnable to read user info`)) return } if user.Username == "testuser" && user.Password == "testpass" { _, _ = w.Write([]byte(``)) _, _ = w.Write([]byte(`successlogin successful`)) } else if user.Username == "testuser" && user.Password == "invalidxml" { _, _ = w.Write([]byte(``)) _, _ = w.Write([]byte(`successlogin successful`)) } else { w.Header().Set("Www-Authenticate", "Protected Realm") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(``)) _, _ = w.Write([]byte(`unauthorizedInvalid credentials`)) } return } } } func handleUsersEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/users" { // JSON if IsJSONType(r.Header.Get(hdrContentTypeKey)) { var users []ExampleUser jd := json.NewDecoder(r.Body) err := jd.Decode(&users) w.Header().Set(hdrContentTypeKey, jsonContentType) if err != nil { t.Logf("Error: %v", err) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Unable to read user info" }`)) return } // logic check, since we are excepting to reach 3 records if len(users) != 3 { t.Log("Error: Excepted count of 3 records") w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Expected record count doesn't match" }`)) return } eu := users[2] if eu.FirstName == "firstname3" && eu.ZipCode == "10003" { w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{ "message": "Accepted" }`)) } return } } } func createPostServer(t *testing.T) *httptest.Server { ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) t.Logf("RawQuery: %v", r.URL.RawQuery) t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) if r.Method == MethodPost { handleLoginEndpoint(t, w, r) handleUsersEndpoint(t, w, r) if r.URL.Path == "/usersmap" { // JSON if IsJSONType(r.Header.Get(hdrContentTypeKey)) { if r.URL.Query().Get("status") == "500" { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("Error: could not read post body: %s", err.Error()) } 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)) w.Header().Set(hdrContentTypeKey, jsonContentType) w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write(body) return } var users []map[string]interface{} jd := json.NewDecoder(r.Body) err := jd.Decode(&users) w.Header().Set(hdrContentTypeKey, jsonContentType) if err != nil { t.Logf("Error: %v", err) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Unable to read user info" }`)) return } // logic check, since we are excepting to reach 1 map records if len(users) != 1 { t.Log("Error: Excepted count of 1 map records") w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{ "id": "bad_request", "message": "Expected record count doesn't match" }`)) return } w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{ "message": "Accepted" }`)) return } } } }) return ts } func createFormPostServer(t *testing.T) *httptest.Server { ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) if r.Method == MethodPost { _ = r.ParseMultipartForm(10e6) if r.URL.Path == "/profile" { t.Logf("FirstName: %v", r.FormValue("first_name")) t.Logf("LastName: %v", r.FormValue("last_name")) t.Logf("City: %v", r.FormValue("city")) t.Logf("Zip Code: %v", r.FormValue("zip_code")) _, _ = w.Write([]byte("Success")) return } else if r.URL.Path == "/search" { formEncodedData := r.Form.Encode() t.Logf("Received Form Encoded values: %v", formEncodedData) assertEqual(t, true, strings.Contains(formEncodedData, "search_criteria=pencil")) assertEqual(t, true, strings.Contains(formEncodedData, "search_criteria=glass")) _, _ = w.Write([]byte("Success")) return } else if r.URL.Path == "/upload" { t.Logf("FirstName: %v", r.FormValue("first_name")) t.Logf("LastName: %v", r.FormValue("last_name")) targetPath := filepath.Join(getTestDataPath(), "upload") _ = os.MkdirAll(targetPath, 0700) for _, fhdrs := range r.MultipartForm.File { for _, hdr := range fhdrs { t.Logf("Name: %v", hdr.Filename) t.Logf("Header: %v", hdr.Header) dotPos := strings.LastIndex(hdr.Filename, ".") fname := fmt.Sprintf("%s-%v%s", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:]) t.Logf("Write name: %v", fname) infile, _ := hdr.Open() f, err := os.OpenFile(filepath.Join(targetPath, fname), os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Logf("Error: %v", err) return } defer func() { _ = f.Close() }() _, _ = io.Copy(f, infile) _, _ = w.Write([]byte(fmt.Sprintf("File: %v, uploaded as: %v\n", hdr.Filename, fname))) } } return } } }) return ts } func createFilePostServer(t *testing.T) *httptest.Server { ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) if r.Method != MethodPost { t.Log("createPostServer:: Not a Post request") w.WriteHeader(http.StatusBadRequest) fmt.Fprint(w, http.StatusText(http.StatusBadRequest)) return } targetPath := filepath.Join(getTestDataPath(), "upload-large") _ = os.MkdirAll(targetPath, 0700) defer cleanupFiles(targetPath) switch r.URL.Path { case "/upload": f, err := os.OpenFile(filepath.Join(targetPath, "large-file.png"), os.O_WRONLY|os.O_CREATE, 0666) if err != nil { t.Logf("Error: %v", err) return } defer func() { _ = f.Close() }() size, _ := io.Copy(f, r.Body) fmt.Fprintf(w, "File Uploaded successfully, file size: %v", size) } }) return ts } func createAuthServer(t *testing.T) *httptest.Server { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) t.Logf("Content-Type: %v", r.Header.Get(hdrContentTypeKey)) if r.Method == MethodGet { if r.URL.Path == "/profile" { // 004DDB79-6801-4587-B976-F093E6AC44FF auth := r.Header.Get("Authorization") t.Logf("Bearer Auth: %v", auth) w.Header().Set(hdrContentTypeKey, jsonContentType) if !strings.HasPrefix(auth, "Bearer ") { w.Header().Set("Www-Authenticate", "Protected Realm") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{ "id": "unauthorized", "message": "Invalid credentials" }`)) return } if auth[7:] == "004DDB79-6801-4587-B976-F093E6AC44FF" || auth[7:] == "004DDB79-6801-4587-B976-F093E6AC44FF-Request" { _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) } } return } if r.Method == MethodPost { if r.URL.Path == "/login" { auth := r.Header.Get("Authorization") t.Logf("Basic Auth: %v", auth) w.Header().Set(hdrContentTypeKey, jsonContentType) password, err := base64.StdEncoding.DecodeString(auth[6:]) if err != nil || string(password) != "myuser:basicauth" { w.Header().Set("Www-Authenticate", "Protected Realm") w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{ "id": "unauthorized", "message": "Invalid credentials" }`)) return } _, _ = w.Write([]byte(`{ "id": "success", "message": "login successful" }`)) } return } })) return ts } func createGenServer(t *testing.T) *httptest.Server { ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) if r.Method == MethodGet { if r.URL.Path == "/json-no-set" { // Set empty header value for testing, since Go server sets to // text/plain; charset=utf-8 w.Header().Set(hdrContentTypeKey, "") _, _ = w.Write([]byte(`{"response":"json response no content type set"}`)) } else if r.URL.Path == "/gzip-test" { w.Header().Set(hdrContentTypeKey, plainTextType) w.Header().Set(hdrContentEncodingKey, "gzip") zw := gzip.NewWriter(w) zw.Write([]byte("This is Gzip response testing")) zw.Close() } else if r.URL.Path == "/gzip-test-gziped-empty-body" { w.Header().Set(hdrContentTypeKey, plainTextType) w.Header().Set(hdrContentEncodingKey, "gzip") zw := gzip.NewWriter(w) // write gziped empty body zw.Write([]byte("")) zw.Close() } else if r.URL.Path == "/gzip-test-no-gziped-body" { w.Header().Set(hdrContentTypeKey, plainTextType) w.Header().Set(hdrContentEncodingKey, "gzip") // don't write body } return } if r.Method == MethodPut { if r.URL.Path == "/plaintext" { _, _ = w.Write([]byte("TestPut: plain text response")) } else if r.URL.Path == "/json" { w.Header().Set(hdrContentTypeKey, jsonContentType) _, _ = w.Write([]byte(`{"response":"json response"}`)) } else if r.URL.Path == "/xml" { w.Header().Set(hdrContentTypeKey, "application/xml") _, _ = w.Write([]byte(`XML response`)) } return } if r.Method == MethodOptions && r.URL.Path == "/options" { w.Header().Set("Access-Control-Allow-Origin", "localhost") w.Header().Set("Access-Control-Allow-Methods", "PUT, PATCH") w.Header().Set("Access-Control-Expose-Headers", "x-go-resty-id") w.WriteHeader(http.StatusOK) return } if r.Method == MethodPatch && r.URL.Path == "/patch" { w.WriteHeader(http.StatusOK) return } if r.Method == "REPORT" && r.URL.Path == "/report" { body, _ := ioutil.ReadAll(r.Body) if len(body) == 0 { w.WriteHeader(http.StatusOK) } return } }) return ts } func createRedirectServer(t *testing.T) *httptest.Server { ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { t.Logf("Method: %v", r.Method) t.Logf("Path: %v", r.URL.Path) if r.Method == MethodGet { if strings.HasPrefix(r.URL.Path, "/redirect-host-check-") { cntStr := strings.SplitAfter(r.URL.Path, "-")[3] cnt, _ := strconv.Atoi(cntStr) if cnt != 7 { // Testing hard stop via logical if cnt >= 5 { http.Redirect(w, r, "http://httpbin.org/get", http.StatusTemporaryRedirect) } else { http.Redirect(w, r, fmt.Sprintf("/redirect-host-check-%d", cnt+1), http.StatusTemporaryRedirect) } } } else if strings.HasPrefix(r.URL.Path, "/redirect-") { cntStr := strings.SplitAfter(r.URL.Path, "-")[1] cnt, _ := strconv.Atoi(cntStr) http.Redirect(w, r, fmt.Sprintf("/redirect-%d", cnt+1), http.StatusTemporaryRedirect) } } }) return ts } func createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server { return httptest.NewServer(http.HandlerFunc(fn)) } func dc() *Client { DefaultClient = New() DefaultClient.SetLogger(ioutil.Discard) return DefaultClient } func dcr() *Request { return dc().R() } func dclr() *Request { c := dc() c.SetDebug(true) c.SetLogger(ioutil.Discard) return c.R() } func assertNil(t *testing.T, v interface{}) { if !isNil(v) { t.Errorf("[%v] was expected to be nil", v) } } func assertNotNil(t *testing.T, v interface{}) { if isNil(v) { t.Errorf("[%v] was expected to be non-nil", v) } } func assertType(t *testing.T, typ, v interface{}) { if reflect.DeepEqual(reflect.TypeOf(typ), reflect.TypeOf(v)) { t.Errorf("Expected type %t, got %t", typ, v) } } func assertError(t *testing.T, err error) { if err != nil { t.Errorf("Error occurred [%v]", err) } } func assertEqual(t *testing.T, e, g interface{}) (r bool) { if !equal(e, g) { t.Errorf("Expected [%v], got [%v]", e, g) } return } func assertNotEqual(t *testing.T, e, g interface{}) (r bool) { if equal(e, g) { t.Errorf("Expected [%v], got [%v]", e, g) } else { r = true } return } func equal(expected, got interface{}) bool { return reflect.DeepEqual(expected, got) } func isNil(v interface{}) bool { if v == nil { return true } rv := reflect.ValueOf(v) kind := rv.Kind() if kind >= reflect.Chan && kind <= reflect.Slice && rv.IsNil() { return true } return false } func logResponse(t *testing.T, resp *Response) { t.Logf("Response Status: %v", resp.Status()) t.Logf("Response Time: %v", resp.Time()) t.Logf("Response Headers: %v", resp.Header()) t.Logf("Response Cookies: %v", resp.Cookies()) t.Logf("Response Body: %v", resp) } func cleanupFiles(files ...string) { pwd, _ := os.Getwd() for _, f := range files { if filepath.IsAbs(f) { _ = os.RemoveAll(f) } else { _ = os.RemoveAll(filepath.Join(pwd, f)) } } }