From c12a8244cd4fa4e60ab0d78bbf4f9c4f8965c059 Mon Sep 17 00:00:00 2001 From: ahuigo <1781999+ahuigo@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:50:31 +0800 Subject: [PATCH] example: add examples about ssl, debug, curl, trace, error, etc. --- .gitignore | 5 +- README.md | 5 + client.go | 24 ++- conf/nginx.crt | 19 +++ conf/nginx.key | 28 ++++ examples/auth_test.go | 32 ++++ examples/context_test.go | 120 +++++++++++++++ examples/cookie_test.go | 182 +++++++++++++++++++++++ examples/debug_curl_test.go | 39 +++++ examples/debug_test.go | 20 +++ examples/debug_trace_test.go | 24 +++ examples/delete_test.go | 28 ++++ examples/error_test.go | 64 ++++++++ examples/get_test.go | 108 ++++++++++++++ examples/post_file_test.go | 47 ++++++ examples/post_test.go | 167 +++++++++++++++++++++ examples/proxy_test.go | 23 +++ examples/req_header_test.go | 33 +++++ examples/response_build_test.go | 41 ++++++ examples/response_test.go | 55 +++++++ examples/retry_test.go | 77 ++++++++++ examples/server_test.go | 249 ++++++++++++++++++++++++++++++++ examples/ssl_test.go | 135 +++++++++++++++++ examples/timeout_test.go | 21 +++ examples/transport_test.go | 35 +++++ examples/utils_test.go | 57 ++++++++ middleware.go | 7 + request.go | 21 +++ shellescape/shellescape.go | 34 +++++ util_curl.go | 72 +++++++++ 30 files changed, 1763 insertions(+), 9 deletions(-) create mode 100644 conf/nginx.crt create mode 100644 conf/nginx.key create mode 100644 examples/auth_test.go create mode 100644 examples/context_test.go create mode 100644 examples/cookie_test.go create mode 100644 examples/debug_curl_test.go create mode 100644 examples/debug_test.go create mode 100644 examples/debug_trace_test.go create mode 100644 examples/delete_test.go create mode 100644 examples/error_test.go create mode 100644 examples/get_test.go create mode 100644 examples/post_file_test.go create mode 100644 examples/post_test.go create mode 100644 examples/proxy_test.go create mode 100644 examples/req_header_test.go create mode 100644 examples/response_build_test.go create mode 100644 examples/response_test.go create mode 100644 examples/retry_test.go create mode 100644 examples/server_test.go create mode 100644 examples/ssl_test.go create mode 100644 examples/timeout_test.go create mode 100644 examples/transport_test.go create mode 100644 examples/utils_test.go create mode 100644 shellescape/shellescape.go create mode 100644 util_curl.go diff --git a/.gitignore b/.gitignore index 9e856bd4..5f673f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,8 @@ _testmain.go coverage.out coverage.txt -# Exclude intellij IDE folders +# Exclude IDE folders .idea/* +.vscode/* +__debug_bin +.DS_Store diff --git a/README.md b/README.md index 5949af03..e8d493d0 100644 --- a/README.md +++ b/README.md @@ -126,12 +126,17 @@ import "github.com/go-resty/resty/v2" ```go // Create a Resty Client +var curlCmdExecuted string client := resty.New() resp, err := client.R(). + SetResultCurlCmd(&curlCmdExecuted). EnableTrace(). Get("https://httpbin.org/get") +// Explore curl command +fmt.Println("Curl Command:", curlCmdExecuted) + // Explore response object fmt.Println("Response Info:") fmt.Println(" Error :", err) diff --git a/client.go b/client.go index 446ba851..906f5aeb 100644 --- a/client.go +++ b/client.go @@ -1129,9 +1129,7 @@ func (c *Client) GetClient() *http.Client { // Client Unexported methods //_______________________________________________________________________ -// Executes method executes the given `Request` object and returns response -// error. -func (c *Client) execute(req *Request) (*Response, error) { +func (c *Client) executeBefore(req *Request) (error) { // Lock the user-defined pre-request hooks. c.udBeforeRequestLock.RLock() defer c.udBeforeRequestLock.RUnlock() @@ -1147,7 +1145,7 @@ func (c *Client) execute(req *Request) (*Response, error) { // to modify the *resty.Request object for _, f := range c.udBeforeRequest { if err = f(c, req); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } } @@ -1155,14 +1153,14 @@ func (c *Client) execute(req *Request) (*Response, error) { // will return an error if the rate limit is exceeded. if req.client.rateLimiter != nil { if !req.client.rateLimiter.Allow() { - return nil, wrapNoRetryErr(ErrRateLimitExceeded) + return wrapNoRetryErr(ErrRateLimitExceeded) } } // resty middlewares for _, f := range c.beforeRequest { if err = f(c, req); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } } @@ -1173,15 +1171,24 @@ func (c *Client) execute(req *Request) (*Response, error) { // call pre-request if defined if c.preReqHook != nil { if err = c.preReqHook(c, req.RawRequest); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } } if err = requestLogger(c, req); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf) + return nil +} + +// Executes method executes the given `Request` object and returns response +// error. +func (c *Client) execute(req *Request) (*Response, error) { + if err:= c.executeBefore(req);err!=nil{ + return nil, err + } req.Time = time.Now() resp, err := c.httpClient.Do(req.RawRequest) @@ -1375,6 +1382,7 @@ func createClient(hc *http.Client) *Client { parseRequestBody, createHTTPRequest, addCredentials, + createCurlCmd, } // user defined request middlewares diff --git a/conf/nginx.crt b/conf/nginx.crt new file mode 100644 index 00000000..321ef35f --- /dev/null +++ b/conf/nginx.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIjCCAgqgAwIBAgIUPkbRM1znk+3fLI8g5eB5i8Ie3K0wDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UEAwwKbG9jYWwuc2VsZjAeFw0yMzAzMjgwMzQ1MDlaFw0yNDAz +MjcwMzQ1MDlaMBUxEzARBgNVBAMMCmxvY2FsLnNlbGYwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDIdnZmgBgAGOChpZGwikpQgTRTkqFNKQV5jRQXr7r7 +3xV+PilkDShz+UWfYG3zszLX7L9jllIDj1V5YALV+f3tjDaPnptSUnNIKIdjripQ +ojX5639oCmHA4ZnYbDxx8GcNlfOLGW4oAVuY9PHaL69nmrHq58wQX2VPR6jCjzCY +T0putkMCRbCTbzeb0ntcEHNKrVuk8TlLxGnNNXyeF1BS6YHa9/3PwXmwsApZ1fY2 +KepHclqVISz+E0Cj4KkVvGI++9KBBDcNMEyerhU0ocotXoqQxQKm+eeED44LKnnh +czPION+xRTfsqn+j8AyrShY+JkBt+VseOlOeNpn25p5dAgMBAAGjajBoMB0GA1Ud +DgQWBBQ8aBcGhCLZzEGztBxgChe+zIY88TAfBgNVHSMEGDAWgBQ8aBcGhCLZzEGz +tBxgChe+zIY88TAPBgNVHRMBAf8EBTADAQH/MBUGA1UdEQQOMAyCCmxvY2FsLnNl +bGYwDQYJKoZIhvcNAQELBQADggEBAHBKNQHNBlRdI6cICeEkBYpoJRg1UBCEpxPo +A7He5EN1vZReJcMoFoc86tPsvUaIwsgqiEu6S0sQNahJHKF0FwcB+A6F9kQmW7si +CrQw9hHneooEmYs2CldNV4w51HWKNDZ5Ra+gH5B2AKR1EWFDh4MMzPlL2MPgasFC +OeAOFrftD8hLLSvCvDsIall/pOg6wTP5vimndsjw+fPk7/SRAqZZzM1EQ4WZ7uZq +oRXdSOhtfyEZYsC04VQNT0KAD+m73ctYXn4EHRpHx5tA1ZkKuuOLOXSVYmHRkk72 +ReutF5Khp+XIjc77fHX6KRCWxT/KE23a4aEmmJzKhV3A1bcvpQ4= +-----END CERTIFICATE----- diff --git a/conf/nginx.key b/conf/nginx.key new file mode 100644 index 00000000..2ec16d60 --- /dev/null +++ b/conf/nginx.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIdnZmgBgAGOCh +pZGwikpQgTRTkqFNKQV5jRQXr7r73xV+PilkDShz+UWfYG3zszLX7L9jllIDj1V5 +YALV+f3tjDaPnptSUnNIKIdjripQojX5639oCmHA4ZnYbDxx8GcNlfOLGW4oAVuY +9PHaL69nmrHq58wQX2VPR6jCjzCYT0putkMCRbCTbzeb0ntcEHNKrVuk8TlLxGnN +NXyeF1BS6YHa9/3PwXmwsApZ1fY2KepHclqVISz+E0Cj4KkVvGI++9KBBDcNMEye +rhU0ocotXoqQxQKm+eeED44LKnnhczPION+xRTfsqn+j8AyrShY+JkBt+VseOlOe +Npn25p5dAgMBAAECggEAYB1AdrPbDgzfg1Gt0V309LWGX72xvhu5hsaoSBUXS5Vy +KjXmxZRzfWuawKhM/6g/a/0U5MxZpWBPhKMOqQa0g/WS4XiUEzv6DzJze80xd7jW +tW3/B+TWatMgXv7CWoT//CkV/O8j/GVuRB/JaCot6ThoLu+VzZHXstPya9qY26Vr +Evc6NF+MED/n2ciagBAJzSEG0L8Ry948Vbzz3yEu1p9Mdg31AVWu6/m4LfMWB7km +rtpSiPdZwqVCsTjGRb2T4hF3TmMgBC1+0meDMxd33xws1XaOoGwaqwdgMpIdowTK +DBonkT10De6cqjtVg/IqeZvmCZl4al4lkSzfGtwGRwKBgQDyx0kQw8D6PpMQ4YSk +cFgVgEwJ2ED2yVlOqnOpw+cFG+k9AlS9VuLOYDyuxsLCX4VdbGitmBnd70E3Zpnj +V74Tqu+nnUr/i8BDl8XG+EceBW+xpMgVIaMnKHbWa3RcIKU4CALbtDL0sl8l96Ch +NxjNje3E/E9hmAeFSBr6zsqPEwKBgQDTYTqzGMeBk7oNE0YonR8UxX1Ah5tSU9Eu +usY1vYGv3FoeJR1RexN7+o4rFxVN+cVy3Mwpml1ptPRZP1V3NGZCCObOaOU4Pplt +HjzhClBTeIsbmZ1Bq/MoRaXp4yykPkHaniR+d7/GjwnSrIzyupsWAe2fPfeIx1/K +VFg1eLcazwKBgQCuv/16wLy2I7ZuDzYPuwHcMCYLbAqO2K2c7xokF2vBhK6lCHmA +c/r7e9ASKeVkTadMcM0ELxhnZGD4BLU+LBkYRREOAC1MtgYlYSiuKGXgWR9lqeuP +MlAizoCDpIL0EVd7dmDATfvjoETWqmCHyoXi54c/JDHrWKgJKrao05J/2wKBgDxu +H4n0G5U/1oDGcdhKkwgtLZG1MwJmU/c4DlJuyxSrulfD5I3W0csv5lULVFvmfDxK +Q1PhfbMquHCLWrOpl1JpmRKJin555wL7EgyEFlLGs35AfGS589ofjz8+YxTRd6I4 +c9Z0Ba+OVRCVo/YAwzWXd4d+/7VqykfdtRoUWMShAoGAVamX3xdVKVUH1vkbcW47 +MZMvli5qeWFKkjNYzAIkHCOKgP+LcOhj2yywSliXZjBP3/AM4IyTf85rL7WWUHs9 +M9V6iy4s0v95+NQ3gBhU8dqGZMlMAfHTdOYP5QZWKQQfF0iGHDH6faEsdBBWiqhI +laxSnUjUN5VMS/ViBExyBQs= +-----END PRIVATE KEY----- diff --git a/examples/auth_test.go b/examples/auth_test.go new file mode 100644 index 00000000..37d80ea1 --- /dev/null +++ b/examples/auth_test.go @@ -0,0 +1,32 @@ +package examples + +import ( + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about sending Authentication header +func TestAuth(t *testing.T) { + var curlCmdExecuted string + ts := createEchoServer() + defer ts.Close() + // Test authentication usernae and password + client := resty.New() + resp, err := client.R(). + SetBasicAuth("USER", "PASSWORD"). + SetResultCurlCmd(&curlCmdExecuted). + Get( ts.URL+"/echo",) + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(curlCmdExecuted, "Authorization: Basic ") { + t.Fatal("bad curl:", curlCmdExecuted) + } + if !strings.Contains(string(resp.Body()), "Authorization: Basic ") { + t.Fatal("bad auth body:\n" + resp.String()) + } + t.Log(curlCmdExecuted) +} diff --git a/examples/context_test.go b/examples/context_test.go new file mode 100644 index 00000000..063bb114 --- /dev/null +++ b/examples/context_test.go @@ -0,0 +1,120 @@ +package examples + +import ( + "context" + "net/http" + "net/http/httptrace" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +// Example about cancel request with context +func TestSetContextCancelMulti(t *testing.T) { + // 0. Init test server + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Microsecond) + n, err := w.Write([]byte("TestSetContextCancel: response")) + t.Logf("%s Server: wrote %d bytes", time.Now(), n) + t.Logf("%s Server: err is %v ", time.Now(), err) + }, 0) + defer ts.Close() + + // 1. Create client + ctx, cancel := context.WithCancel(context.Background()) + client := resty.New().R().SetContext(ctx) + go func() { + time.Sleep(1 * time.Microsecond) + cancel() + }() + + // 2. First request + _, err := client.Get(ts.URL + "/get") + if !errIsContextCancel(err) { + t.Fatalf("Got unexpected error: %v", err) + } + + // 3. Second request + _, err = client.Get(ts.URL + "/get") + if !errIsContextCancel(err) { + t.Fatalf("Got unexpected error: %v", err) + } +} + +// Test context: cancel with chan +func TestSetContextCancelWithChan(t *testing.T) { + ch := make(chan struct{}) + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + defer func() { + ch <- struct{}{} // tell test request is finished + }() + t.Logf("%s Server: %v %v", time.Now(), r.Method, r.URL.Path) + ch <- struct{}{} // tell test request is canceld + t.Logf("%s Server: call canceld", time.Now()) + + <-ch // wait for client to finish request + n, err := w.Write([]byte("TestSetContextCancel: response")) + // FIXME? test server doesn't handle request cancellation + t.Logf("%s Server: wrote %d bytes", time.Now(), n) + t.Logf("%s Server: err is %v ", time.Now(), err) + + }, 0) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + <-ch // wait for server to start request handling + cancel() + }() + + _, err := resty.New().R().SetContext(ctx).Get(ts.URL + "/get") + t.Logf("%s:client:is canceled", time.Now()) + + ch <- struct{}{} // tell server to continue request handling + t.Logf("%s:client:tell server to continue", time.Now()) + + <-ch // wait for server to finish request handling + + if !errIsContextCancel(err) { + t.Fatalf("Got unexpected error: %v", err) + } +} + +// test with trace context +func TestContextWithTrace(t *testing.T) { + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("TestSetContextWithTrace: response")) + }, 0) + defer ts.Close() + + //1. Create Trace context + traceInfo := struct { + dnsDone time.Time + connectDone time.Time + }{} + + trace := &httptrace.ClientTrace{ + ConnectStart: func(network, addr string) { + traceInfo.dnsDone = time.Now() + t.Log(time.Now(), "ConnectStart:", "network=", network, ",addr=", addr) + }, + ConnectDone: func(network, addr string, err error) { + traceInfo.connectDone = time.Now() + t.Log(time.Now(), "ConnectDone:", "network=", network, ",addr=", addr) + }, + } + ctx := httptrace.WithClientTrace(context.Background(), trace) + + //2. Send request with Trace context + session := resty.New().R().SetContext(ctx) + params := MapString{"name": "ahuigo", "page": "1"} + _, err := session.SetQueryParams(params).Get(ts.URL+"/get") + if err != nil { + t.Fatal(err) + } + if traceInfo.connectDone.Sub(traceInfo.dnsDone) <= 0 { + t.Fatal("Bad trace info") + } + +} diff --git a/examples/cookie_test.go b/examples/cookie_test.go new file mode 100644 index 00000000..a020c2de --- /dev/null +++ b/examples/cookie_test.go @@ -0,0 +1,182 @@ +package examples + +import ( + "net/http" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about sending cookie +func TestSendCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + data := struct { + Cookies struct{ Token string } + }{} + + resp, err := resty.New().R().SetResult(&data).SetHeader("Cookie", "token=1234").Get(ts.URL + "/cookie/count") + if err != nil { + panic(err) + } + if data.Cookies.Token != "1234" { + t.Errorf("Can not read cookie from response:%s", resp.String()) + } + +} + +// Test session Cookie +func TestSessionCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + result := struct { + Cookies struct { + Count string + Name1 string + Name2 string + } + }{} + cookie1 := &http.Cookie{ + Name: "name1", + Value: "value1", + Path: "/", + } + cookie2 := &http.Cookie{ + Name: "name2", + Value: "value2", + } + session := resty.New().SetDebug(true).R() + + // 1. set cookie1 + session.SetCookie(cookie1).Get(ts.URL + "/cookie/count") + + // 2. set cookie2 and get all cookies + resp, err := session.SetCookie(cookie2).SetResult(&result).Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + cookies := map[string]string{} + // cookies's type is `[]*http.Cookies` + for _, c := range resp.Cookies() { + if _, exists := cookies[c.Name]; exists { + t.Fatal("duplicated cookie:", c.Name, c.Value) + } + cookies[c.Name] = c.Value + } + if cookies["count"] != "2" { + t.Fatalf("cookie count is not 2(%+v)", resp.Cookies()) + } + + if result.Cookies.Name1 != "value1" || result.Cookies.Name2 != "value2" { + t.Fatalf("Failed to send valid cookie(%+v)", resp.Cookies()) + } + +} + +// Test session Cookie +func TestSessionCookieWithClone(t *testing.T) { + ts := createHttpbinServer(0) + url := ts.URL + "/cookie/count" + defer ts.Close() + + client := resty.New() + req := client.R() + + // 0. Prepare cookie1 and cookie2 + cookie1 := &http.Cookie{ + Name: "name1", + Value: "value1", + Path: "/", + } + cookie2 := &http.Cookie{ + Name: "name2", + Value: "value2", + } + + // 1. Set cookie1 + client.SetCookie(cookie1) + req.SetCookie(cookie1).Get(url) + + // 2. Set cookie2 and get all cookies + resp, err := req.SetCookie(cookie2).Get(url) + if err != nil { + t.Fatal(err) + } + + // 3. Check cookies: client and response + respCookies := map[string]string{} + clientCookies := map[string]string{} + // cookies's type is `[]*http.Cookies` + // 3.1 Check response cookies + for _, c := range resp.Cookies() { + if _, exists := respCookies[c.Name]; exists { + t.Fatal("duplicated cookie:", c.Name, c.Value) + } + respCookies[c.Name] = c.Value + } + // 3.2 Check client cookies + for _, c := range client.Cookies { + if _, exists := clientCookies[c.Name]; exists { + t.Fatal("duplicated cookie:", c.Name, c.Value) + } + clientCookies[c.Name] = c.Value + } + if clientCookies["name1"] != "value1" || respCookies["count"] == "" { + t.Fatalf("bad cookie, respCookies=%+v, clientCookies=%+v", resp.Cookies(), client.Cookies) + } + + // 4. Check response body + body := resp.String() + if (!strings.Contains(body, `"name1"`) || + !strings.Contains(body, `"name2"`) || + !strings.Contains(body, `"count"`) ){ + t.Fatalf("invalid response: %s", body) + } + +} + +// Test Set-Cookie +func TestResponseCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + session := resty.New().R() + resp, err := session.Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + + cs := resp.Cookies() + if len(cs) == 0 { + t.Fatalf("require cookies, body=%s", resp.Body()) + } +} + +func TestResponseBuildCookie(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + session := resty.New().R() + resp, err := session.Get(ts.URL + "/cookie/count") + if err != nil { + t.Fatal(err) + } + + // build new resposne + cs := resp.Cookies() + if len(cs) == 0 { + t.Fatalf("require cookies, headers=%#v, body=%s", resp.Header(), resp.Body()) + } + findCount := false + for _, c := range cs { + if c.Name == "count" && c.Value == "1" { + findCount = true + } + } + if !findCount { + t.Fatalf("could not find cookie, headers=%#v", resp.Header()) + } +} diff --git a/examples/debug_curl_test.go b/examples/debug_curl_test.go new file mode 100644 index 00000000..877aaa2a --- /dev/null +++ b/examples/debug_curl_test.go @@ -0,0 +1,39 @@ +package examples + +import ( + "net/http" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about generating curl command +func TestDebugCurl(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + req := resty.New().R().SetBody(MapString{ + "name": "Alex", + }).SetCookies( + []*http.Cookie{ + { Name: "count", Value: "1", }, + }, + ) + + // 1. Generate curl for request(not executed) + curlCmdUnexecuted := req.GetCurlCmd() + if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") || !strings.Contains(curlCmdUnexecuted, "curl -X GET") { + t.Fatal("bad curl:", curlCmdUnexecuted) + } + + // 2. Generate curl for request(executed) + var curlCmdExecuted string + req.SetResultCurlCmd(&curlCmdExecuted) + if _, err := req.Post(ts.URL+"/post"); err != nil { + t.Fatal(err) + } + if !strings.Contains(curlCmdExecuted, "Cookie: count=1") || !strings.Contains(curlCmdExecuted, "curl -X POST") { + t.Fatal("bad curl:", curlCmdExecuted) + } +} \ No newline at end of file diff --git a/examples/debug_test.go b/examples/debug_test.go new file mode 100644 index 00000000..1461f50b --- /dev/null +++ b/examples/debug_test.go @@ -0,0 +1,20 @@ +package examples + +import ( + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about debuging/showing request and response +func TestDebugRequestAndResponse(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + client := resty.New().SetDebug(true) + req := client.R().SetBody(MapString{ "name": "Alex", }) + _, err := req.Post(ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } +} diff --git a/examples/debug_trace_test.go b/examples/debug_trace_test.go new file mode 100644 index 00000000..b3ad9281 --- /dev/null +++ b/examples/debug_trace_test.go @@ -0,0 +1,24 @@ +package examples + +import ( + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about getting trace info +func TestTrace(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + client := resty.New().EnableTrace() + req := client.R().SetBody(MapString{ "name": "Alex", }) + resp, err := req.Post(ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } + traceInfo := resp.Request.TraceInfo() + if traceInfo.TotalTime <= 0 { + t.Fatalf("invalid traceInfo: %+v\n body:%s", traceInfo, resp.String()) + } +} diff --git a/examples/delete_test.go b/examples/delete_test.go new file mode 100644 index 00000000..5bd58081 --- /dev/null +++ b/examples/delete_test.go @@ -0,0 +1,28 @@ +package examples + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about DELETE method with Form Request +func TestDeleteForm(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + json := MapString{ + "name": "Alex", + } + data := struct { + Body string + }{} + + r:=resty.New().R().SetBody(&json).SetResult(&data) + resp, err := r.Delete(ts.URL+"/delete") + if err == nil { + fmt.Println(resp.String()) + } + +} diff --git a/examples/error_test.go b/examples/error_test.go new file mode 100644 index 00000000..0a22d2b4 --- /dev/null +++ b/examples/error_test.go @@ -0,0 +1,64 @@ +package examples + +import ( + "errors" + "net/url" + "strings" + "testing" + + "context" + + "github.com/go-resty/resty/v2" +) + +// Example about error handling +func TestErrorConnnect(t *testing.T) { + _, err := resty.New().R().Get("http://127.0.0.1:12346/connect-refused") + var err2 *url.Error + if !errors.As(err, &err2) { + t.Fatalf("not expected url error:%+v", err) + } + if !strings.Contains(err2.Error(), "connection refused") { + t.Fatalf("not expected connnect error:%+v", err2) + } +} + +func TestErrorTimeout(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // resp, err := resty.New().Get("https://httpbin.org/json") + _, err := resty.New().SetTimeout(1).R().Get(ts.URL + "/sleep/10") + + var err2 *url.Error + if !errors.As(err, &err2) { + t.Fatalf("not expected url error:%+v", err) + } + + if !strings.Contains(err2.Error(), "context deadline exceeded") { + t.Fatalf("unexpected error:%+v", err2) + } + +} + +func TestErrorURL(t *testing.T) { + _, err := resty.New().R().Get("xxxx") + + var err2 *url.Error + if !errors.As(err, &err2) { + t.Fatalf("not expected url error:%+v", err) + } + + if err2.Op != "Get" { + t.Fatalf("unexpected error(op=%s,err=%+v)", err2.Op,err2) + } +} + +func errIsContextCancel(err error) bool { + var ue *url.Error + ok := errors.As(err, &ue) + if !ok { + return false + } + return ue.Err == context.Canceled +} diff --git a/examples/get_test.go b/examples/get_test.go new file mode 100644 index 00000000..0450fdbb --- /dev/null +++ b/examples/get_test.go @@ -0,0 +1,108 @@ +package examples + +import ( + "net/url" + "testing" + + "github.com/go-resty/resty/v2" +) + +var client = resty.New() + +// Example about sending GET request + +// Get example: fetch json response +func TestGetJson(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var json map[string]interface{} + _, err := resty.New().R().SetResult(&json).Get(ts.URL + "/get") + if err != nil { + t.Fatal(err) + }else { + t.Logf("response json:%#v\n", json) + } +} + +// Get example: fetch string response +func TestGetBody(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + resp, err := resty.New().R().Get(ts.URL + "/get") + if err != nil { + t.Fatal(err) + }else { + t.Logf("response body:%#v\n", string(resp.Body())) + } +} + +// Get with params +func TestGetParams(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + type HbResponse struct { + Args map[string]string `json:"args"` + } + json := &HbResponse{} + params := map[string]string{"name": "Alex", "page": "1"} + resp, err := client.R().SetQueryParams(params).SetResult(&json).Get(ts.URL + "/get") + + if err != nil { + t.Fatal(err) + } + if err == nil { + if json.Args["name"] != "Alex" { + t.Fatalf("bad json:%s", string(resp.Body())) + } + } +} + +type MapString= map[string]string + + +// Support array args like: ids=id1&ids=id2&ids=id3 +func TestGetParamArray(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + paramsArray := url.Values{ + "ids": []string{"id1", "id2"}, + } + + type HbResponse struct { + Args map[string]string `json:"args"` + } + json := &HbResponse{} + resp, err := client.R().SetQueryParamsFromValues(paramsArray).SetResult(&json).Get(ts.URL + "/get") + + if err != nil { + t.Fatal(err) + } + if err == nil { + if json.Args["ids"] != "id1,id2" { + t.Fatal("Invalid response: " + string(resp.Body())) + } + } +} + +func TestGetWithHeader(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + type HbResponse struct { + Args map[string]string `json:"args"` + } + json := &HbResponse{} + params := MapString{"name": "Alex"} + resp, err := client.R().SetResult(&json).SetQueryParams(params).Get(ts.URL+"/get",) + + if err != nil { + t.Fatal(err) + } + if err == nil { + t.Log(string(resp.Body())) + } +} diff --git a/examples/post_file_test.go b/examples/post_file_test.go new file mode 100644 index 00000000..cbfac38c --- /dev/null +++ b/examples/post_file_test.go @@ -0,0 +1,47 @@ +package examples + +import ( + "path/filepath" + "testing" + + "github.com/go-resty/resty/v2" +) + +/* +An example about post `file` with `form data`: +curl "https://www.httpbin.org/post" -F 'file1=@./test-file.txt' -F 'name=alex' +*/ +func TestPostFile(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + Files struct { + File1 string + } + Form struct { + Name string + } + }{} + r:=resty.New().R().SetFormData(MapString{ + "name": "Alex", + }). + SetFile("file1", filepath.Join(getTestDataPath(),"text-file.txt")). + SetResult(&data) + + // 2. Post file + resp, err := r.Post( ts.URL+"/file",) + if err != nil { + t.Fatal(err) + } + + // 3. Check response + if data.Files.File1 == "" { + t.Error("invalid response files:", resp.String()) + } + if data.Form.Name == "" { + t.Error("invalid response forms:", resp.String()) + } + +} diff --git a/examples/post_test.go b/examples/post_test.go new file mode 100644 index 00000000..43956a75 --- /dev/null +++ b/examples/post_test.go @@ -0,0 +1,167 @@ +package examples + +import ( + ejson "encoding/json" + "net/url" + "path/filepath" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about sending POST request + +// Post Params: use with content-type: none +// curl -X POST "https://www.httpbin.org/post?name=Alex" +func TestPostParams(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Args struct { + Name string + } + }{} + resp, err := resty.New().R().SetResult(&data).SetQueryParams(MapString{"name":"Alex"}).Post( + ts.URL+"/post", + ) + if err != nil { + t.Fatal(err) + } + if data.Args.Name != "Alex" { + t.Fatal("invalid response body:", resp.String()) + } +} + +// Post Datas: use
with application/x-www-form-urlencoded +// curl -H 'Content-Type: application/x-www-form-urlencoded' https://www.httpbin.org/post -d 'name=Alex' +func TestPostFormUrlEncode(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + }{} + r:=resty.New().R().SetFormDataFromValues(url.Values{ + "name": []string{"Alex"}, + }).SetResult(&data) + resp, err := r.Post( ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } + if data.Body != "name=Alex" { + t.Fatal("invalid response body:", resp.String()) + } +} + +// POST FormData: multipart/form-data; boundary=.... +// curl https://www.httpbin.org/post -F 'name=Alex' -F "file1=@./testdata/text-file.txt" +func TestPostFormData(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + }{} + r:=resty.New().R().SetFormData(MapString{ + "name": "Alex", + }).SetFile("file1", filepath.Join(getTestDataPath(),"text-file.txt")). + SetResult(&data) + resp, err := r.Post( ts.URL+"/post",) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(data.Body, "form-data; name=\"name\"\r\n\r\nAlex\r\n") { + t.Error("invalid response body:", resp.String()) + } +} + +// POST Json: application/json +// curl -H "Content-Type: application/json" https://www.httpbin.org/post -d '{"name":"Alex"}' +func TestPostJson(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + json := MapString{ + "name": "Alex", + } + data := struct { + Body string + }{} + r:=resty.New().R().SetBody(json).SetResult(&data) + resp, err := r.Post(ts.URL+"/post") + if err != nil { + t.Fatal(err) + } + + + // is expected results + jsonData, _ := ejson.Marshal(json) // if data.Data!= "{\"name\":\"Alex\"}"{ + if data.Body != string(jsonData) { + t.Error("invalid response body:", resp.String()) + } +} + +// Post Raw Bypes: text/plain(default) +// curl -H "Content-Type: text/plain" https://www.httpbin.org/post -d 'raw data: Hi, Jack!' +func TestRawBytes(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + rawText := "raw data: Hi, Jack!" + var data = struct { + Body string + }{} + r:=resty.New().R().SetBody([]byte(rawText)).SetResult(&data) + resp, err := r.Post(ts.URL+"/post") + if err != nil { + t.Fatal(err) + } + if data.Body != rawText { + t.Error("invalid response body:", resp.String()) + } +} + +// Post Raw String: text/plain +// curl -H "Content-Type: text/plain" http://0:4500/post -d 'raw data: Hi, Jack!' +func TestRawString(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data interface{} + rawText := "raw data: Hi, Jack!" + r:=resty.New().R().SetHeader("Content-Type", "text/plain").SetBody([]byte(rawText)).SetResult(&data) + resp, err := r.Post(ts.URL+"/post", ) + if err != nil { + t.Fatal(err) + } + if data.(map[string]interface{})["body"].(string) != rawText { + t.Error("invalid response body:", resp.String()) + } +} + + +// TestPostEncodedString: application/x-www-form-urlencoded +// curl -H 'Content-Type: application/x-www-form-urlencoded' http://0:4500/post -d 'name=Alex&age=29' +func TestPostEncodedString(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var data = struct { + Body string + }{} + r:=resty.New(). + SetDebug(true). + R(). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetBody("name=Alex&age=29"). + SetResult(&data) + resp, err := r.Post(ts.URL+"/post") + if err != nil { + t.Fatal(err) + } + if data.Body != "name=Alex\u0026age=29" { + t.Error("invalid response body:", resp.String()) + } +} diff --git a/examples/proxy_test.go b/examples/proxy_test.go new file mode 100644 index 00000000..7c4171d2 --- /dev/null +++ b/examples/proxy_test.go @@ -0,0 +1,23 @@ +package examples + +import ( + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about using proxy +func TestProxy(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + var json map[string]interface{} + client := resty.New().SetProxy("http://proxy:8888") + client.RemoveProxy() // remove proxy. TODO: mock proxy server in future + _, err := client.R().SetResult(&json).Get(ts.URL + "/get") + if err != nil { + t.Fatal(err) + }else { + t.Logf("response json:%#v\n", json) + } +} diff --git a/examples/req_header_test.go b/examples/req_header_test.go new file mode 100644 index 00000000..8659ea29 --- /dev/null +++ b/examples/req_header_test.go @@ -0,0 +1,33 @@ +package examples + +import ( + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about sending headers +func TestSendHeader(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + json := struct{ + Args struct{ + Name string + Age string `json:"age"` + } + }{} + _, err := resty.New(). + SetDebug(true). + R(). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetQueryString("name=Alex&age=29"). + SetResult(&json). + Get(ts.URL + "/get") + if err != nil { + t.Fatal(err) + } + if json.Args.Age != "29" { + t.Fatalf("invalid json:%v\n", json) + } +} diff --git a/examples/response_build_test.go b/examples/response_build_test.go new file mode 100644 index 00000000..966f9fb3 --- /dev/null +++ b/examples/response_build_test.go @@ -0,0 +1,41 @@ +package examples + +import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about building response +func TestResponseBuilder(t *testing.T) { + var err error + var data = 1 + responseBytes, _ := json.Marshal(data) + + respRecorder := httptest.NewRecorder() + respRecorder.Write(responseBytes) + + request := resty.New().R() + // build response + resp := resty.Response{ + Request: request, + RawResponse :respRecorder.Result(), + // body: []byte("abc"), + } + // if resp.body, err = io.ReadAll(resp.RawResponse.Body); err != nil { + // t.Fatalf("err:%v", err) + // } + ndata, err := io.ReadAll(resp.RawResponse.Body) + if err != nil { + t.Fatalf("err:%v", err) + } + + if !bytes.Equal(ndata , responseBytes) { + t.Fatalf("expect response:%v", data) + } + +} diff --git a/examples/response_test.go b/examples/response_test.go new file mode 100644 index 00000000..21861ca2 --- /dev/null +++ b/examples/response_test.go @@ -0,0 +1,55 @@ +package examples + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about using response +func TestResponse(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R() + resp, _ := request.Get(ts.URL + "/get") + fmt.Println("Status Code:", resp.StatusCode()) + fmt.Println("Time:", resp.Time()) + fmt.Println("Size:", resp.Size()) + fmt.Println("Headers:") + for key, value := range resp.Header() { + fmt.Println(key, "=", value) + } + fmt.Println("Cookies:") + for i, cookie := range resp.Cookies() { + fmt.Printf("cookie%d: name:%s value:%s\n", i, cookie.Name, cookie.Value) + } + +} + +// Test response headers +func TestResponseHeader(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R() + resp, _ := request.Get(ts.URL + "/get") + + if resp.Header().Get("content-type") != "application/json" { + t.Fatal("bad response header") + } + + println("content-type:", resp.Header().Get("content-type")) +} + +// Test response body +func TestResponseBody(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + request := resty.New().R() + resp, _ := request.Get(ts.URL + "/get") + println(resp.Body()) + println(resp.String()) +} diff --git a/examples/retry_test.go b/examples/retry_test.go new file mode 100644 index 00000000..0452ed1d --- /dev/null +++ b/examples/retry_test.go @@ -0,0 +1,77 @@ +package examples + +import ( + "encoding/json" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +// Example about retrying request +func TestRetryCondition(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // retry 3 times + maxRetries := 2 + r := resty.New(). + SetRetryCount(maxRetries). + SetRetryWaitTime(time.Microsecond). + SetRetryMaxWaitTime(time.Microsecond). + AddRetryCondition( + func(r *resty.Response, _ error) bool { + var data map[string]interface{} + err:=json.Unmarshal(r.Body(), &data) + if err != nil { + return true + } + return data["headers"] != "a" + }, + ) .R() + + var data struct{ + Body string + Method string + } + resp, err := r.SetBody([]byte("alex")).SetResult(&data).Post(ts.URL+"/post", ) + if err != nil { + t.Fatal(err, resp.String()) + } + + if resp.Request.Attempt != maxRetries+1 { + t.Fatalf("Attempt %d, expected: %d", resp.Request.Attempt, maxRetries+1) + } + + if data.Body != "alex" { + t.Fatalf("Bad response body:%s", resp.String()) + } + if data.Method != "POST" { + t.Fatalf("Bad request method:%s", resp.String()) + } +} + +// func TestRetryConditionFalse(t *testing.T) { +// ts := createHttpbinServer(0) +// defer ts.Close() + +// // retry 3 times +// r := requests.R(). +// SetRetryCount(3). +// SetRetryCondition(func(resp *requests.Response, err error) bool { +// return false +// }) + +// resp, err := r.Get(ts.URL + "/get") +// if err != nil { +// t.Fatal(err) +// } + +// if resp.Attempt != 0 { +// t.Fatalf("Attemp %d not equal to %d", resp.Attempt, 0) +// } + +// var json map[string]interface{} +// resp.Json(&json) +// t.Logf("response json:%#v\n", json["headers"]) +// } diff --git a/examples/server_test.go b/examples/server_test.go new file mode 100644 index 00000000..d0f188f5 --- /dev/null +++ b/examples/server_test.go @@ -0,0 +1,249 @@ +package examples + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + ioutil "io" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strconv" + "strings" + "time" +) + +const maxMultipartMemory = 4 << 30 // 4MB + +// tlsCert: +// +// 0 no certificate +// 1 with self-signed certificate +// 2 with custom certificate from CA +func createHttpbinServer(tlsCert int) (ts *httptest.Server) { + ts = createTestServer(func(w http.ResponseWriter, r *http.Request) { + switch path := r.URL.Path; { + case path == "/get": + getHandler(w, r) + case path == "/post": + postHandler(w, r) + case path == "/delete": + postHandler(w, r) + case path == "/file": + fileHandler(w, r) + case strings.HasPrefix(path, "/sleep/"): //sleep/3 + sleepHandler(w, r) + case path == "/cookie/count": + cookieHandler(w, r) + default: + _, _ = w.Write([]byte("404 " + path)) + } + }, tlsCert) + + return ts +} + +func postHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := ioutil.ReadAll(r.Body) + m := map[string]interface{}{ + "headers": dumpRequestHeader(r), + "args": parseRequestArgs(r), + "body": string(body), + "method": r.Method, + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + + +func fileHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := r.ParseMultipartForm(maxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + panic(fmt.Sprintf("error on parse multipart form array: %v", err)) + } + } + // parse form data + formData := make(map[string]string) + for k, vs := range r.PostForm { + for _, v := range vs { + formData[k] = v + } + } + // parse files + files := make(map[string]string) + if r.MultipartForm != nil && r.MultipartForm.File != nil { + for key, fhs := range r.MultipartForm.File { + // if len(fhs)>0 + // f, err := fhs[0].Open() + files[key] = fhs[0].Filename + } + } + + //output + m := map[string]interface{}{ + "headers": dumpRequestHeader(r), + "args": parseRequestArgs(r), + "form": formData, + "files": files, + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + +func sleepHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + regx := regexp.MustCompile(`^/sleep/(\d+)`) + res := regx.FindStringSubmatch(r.URL.Path) // res may be: []string(nil) + miliseconds := 0 + if res != nil { + miliseconds, _ = strconv.Atoi(res[1]) + } + time.Sleep(time.Duration(miliseconds) * time.Microsecond) + out := fmt.Sprintf("sleep %d ms", miliseconds) + _, _ = w.Write([]byte(out)) +} + +func getHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := ioutil.ReadAll(r.Body) + m := map[string]interface{}{ + "headers": dumpRequestHeader(r), + "args": parseRequestArgs(r), + "body": string(body), + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + +func cookieHandler(w http.ResponseWriter, r *http.Request) { + + switch r.URL.Path { + case "/cookie/count": + reqCookies := map[string]string{} + for _, c := range r.Cookies() { + reqCookies[c.Name] = c.Value + } + + count := "1" + cookie, err := r.Cookie("count") + if err == nil { + i, _ := strconv.Atoi(cookie.Value) + count = strconv.Itoa(i + 1) + } + http.SetCookie(w, &http.Cookie{Name: "count", Value: url.QueryEscape(count)}) + w.Header().Set("Content-Type", "application/json") + + body, _ := ioutil.ReadAll(r.Body) + m := map[string]interface{}{ + "args": parseRequestArgs(r), + "body": string(body), + "count": count, + "cookies": reqCookies, + "headers": dumpRequestHeader(r), + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) + default: + _, _ = w.Write([]byte("404 " + r.URL.Path)) + } +} + +func dumpRequestHeader(req *http.Request) string { + var res strings.Builder + headers := sortHeaders(req) + for _, kv := range headers { + res.WriteString(kv[0] + ": " + kv[1] + "\n") + } + return res.String() +} + +// sortHeaders +func sortHeaders(request *http.Request) [][2]string { + headers := [][2]string{} + for k, vs := range request.Header { + for _, v := range vs { + headers = append(headers, [2]string{k, v}) + } + } + n := len(headers) + for i := 0; i < n; i++ { + for j := n - 1; j > i; j-- { + jj := j - 1 + h1, h2 := headers[j], headers[jj] + if h1[0] < h2[0] { + headers[jj], headers[j] = headers[j], headers[jj] + } + } + } + return headers +} + +func createEchoServer() (ts *httptest.Server) { + ts = createTestServer(func(w http.ResponseWriter, r *http.Request) { + res := dumpRequest(r) + _, _ = w.Write([]byte(res)) + }, 0) + + return ts +} +func parseRequestArgs(request *http.Request) map[string]string { + query := request.URL.RawQuery + params := map[string]string{} + paramsList, _ := url.ParseQuery(query) + for key, vals := range paramsList { + // params[key] = vals[len(vals)-1] + params[key] = strings.Join(vals, ",") + } + return params +} + +func dumpRequest(request *http.Request) string { + var r strings.Builder + // dump header + res := request.Method + " " + //request.URL.String() +" "+ + request.Host + + request.URL.Path + "?" + request.URL.RawQuery + " " + request.Proto + " " + + "\n" + r.WriteString(res) + r.WriteString(dumpRequestHeader(request)) + r.WriteString("\n") + + // dump body + buf, _ := ioutil.ReadAll(request.Body) + request.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) // important!! + r.WriteString(string(buf)) + return r.String() +} + +/* +* + - tlsCert: + 0 no certificate + 1 with self-signed certificate + 2 with custom certificate from CA +*/ +func createTestServer(fn func(w http.ResponseWriter, r *http.Request), tlsCert int) (ts *httptest.Server) { + if tlsCert == 0 { + // 1. http test server + ts = httptest.NewServer(http.HandlerFunc(fn)) + } else { + // 2. https test server: https://stackoverflow.com/questions/54899550/create-https-test-server-for-any-client + ts = httptest.NewUnstartedServer(http.HandlerFunc(fn)) + + // 3. use own cert + if tlsCert == 2 { + cert, err := tls.LoadX509KeyPair("../conf/nginx.crt", "../conf/nginx.key") + if err != nil { + panic(err) + } + _ = cert + ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} + } + ts.StartTLS() + } + return ts +} diff --git a/examples/ssl_test.go b/examples/ssl_test.go new file mode 100644 index 00000000..726fa744 --- /dev/null +++ b/examples/ssl_test.go @@ -0,0 +1,135 @@ +package examples + +import ( + "context" + "crypto/tls" + "crypto/x509" + "log" + "net" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about ssl +func TestSkipSsl(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(2) + defer ts.Close() + + client := resty.New() + + // 2. fake CA certificate + // client.SetRootCertificate("conf/rootCA.crt") + + // 3. skip ssl + client = client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + + // 4. send get request + resp, err := client.R().Get(ts.URL + "/get?a=1") + if err != nil { + t.Fatal(err) + } + if string(resp.Body()) == "" { + t.Fatal(string(resp.Body())) + } +} + +func TestSslSkipViaTransport(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(2) + defer ts.Close() + + client := resty.New() + + // 3. skip ssl & proxy connect + tsp,_ := client.Transport() + _ = tsp + tsp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // not connect to a proxy server,, keep pathname only + return net.Dial("tcp", ts.URL[strings.LastIndex(ts.URL, "/")+1:]) + } + tsp.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + + // 4. send get request + resp, err := client.R().Get(ts.URL + "/get?a=1") + if err != nil { + t.Fatal(err) + } + if string(resp.Body()) == "" { + t.Fatal(string(resp.Body())) + } +} + +func TestSslCertSelf(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(1) + defer ts.Close() + + client := resty.New() + // 2. certs + certs := x509.NewCertPool() + for _, c := range ts.TLS.Certificates { + roots, err := x509.ParseCertificates(c.Certificate[len(c.Certificate)-1]) + if err != nil { + log.Fatalf("error parsing server's root cert: %v", err) + } + for _, root := range roots { + certs.AddCert(root) + } + } + + // 3. 代替 client.SetRootCertificate("tmp/ca.crt") + // 3. with RootCAs & proxy connect + tsp,_ := client.Transport() + tsp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // not connect to a proxy server,, keep pathname only + return net.Dial("tcp", ts.URL[strings.LastIndex(ts.URL, "/")+1:]) + } + tsp.TLSClientConfig = &tls.Config{ + // InsecureSkipVerify: true, + RootCAs: certs, + } + + // 4. send get request + resp, err := client.R().Get(ts.URL + "/get?a=1") + if err != nil { + t.Fatal(err) + } + if string(resp.Body()) == "" { + t.Fatal(string(resp.Body())) + } +} + +// go test -timeout 6000s -run '^TesSslCertCustom$' github.com/ahuigo/requests/v2/examples -v -httptest.serve=127.0.0.1:443 +func TesSslCertCustom(t *testing.T) { + // 1. create tls test server + ts := createHttpbinServer(2) + defer ts.Close() + + client := resty.New() + + + // 2. fake CA or self-signed certificate like nginx.crt + client.SetRootCertificate("../conf/nginx.crt") + tsp,_ := client.Transport() + tsp.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // not connect to a proxy server,, keep pathname only + return net.Dial("tcp", ts.URL[strings.LastIndex(ts.URL, "/")+1:]) + } + + url := strings.Replace(ts.URL, "127.0.0.1", "local.self", 1) + "/get?a=1" + t.Log(url) + // time.Sleep(10 * time.Minute) + // 4. send get request + resp, err := client.R().Get(url) + if err != nil { + t.Fatal(err) + } + if string(resp.Body()) == "" { + t.Fatal(string(resp.Body())) + } +} diff --git a/examples/timeout_test.go b/examples/timeout_test.go new file mode 100644 index 00000000..530e601f --- /dev/null +++ b/examples/timeout_test.go @@ -0,0 +1,21 @@ +package examples + +import ( + "strings" + "testing" + "time" + + "github.com/go-resty/resty/v2" +) + +// Example about setting timeout +func TestTimeout(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + req := resty.New().SetTimeout(1*time.Microsecond).R() + _, err:= req.Get(ts.URL+"/sleep/2") + assertNotEqual(t, nil, err) + assertEqual(t, true, strings.Contains(err.Error(), "Client.Timeout exceeded")) + +} diff --git a/examples/transport_test.go b/examples/transport_test.go new file mode 100644 index 00000000..7abc3f06 --- /dev/null +++ b/examples/transport_test.go @@ -0,0 +1,35 @@ +/** + * refer to: git@github.com:go-resty/resty.git + */ +package examples + +import ( + "net/http" + "testing" + + "github.com/go-resty/resty/v2" +) + +// Example about using custom transport +func TestTransportSet(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + session := resty.New() + + // tsp:= otelhttp.NewTransport(http.DefaultTransport) + tsp := http.DefaultTransport.(*http.Transport).Clone() + tsp.MaxIdleConnsPerHost = 1 + tsp.MaxIdleConns = 1 + tsp.MaxConnsPerHost = 1 + session.SetTransport(tsp) + + resp, err := session.R().Get(ts.URL + "/sleep/11") + if err != nil { + t.Fatal(err) + } + body := string(resp.Body()) + if body== "" { + t.Fatal("emptay body") + } +} diff --git a/examples/utils_test.go b/examples/utils_test.go new file mode 100644 index 00000000..1afc0ff8 --- /dev/null +++ b/examples/utils_test.go @@ -0,0 +1,57 @@ +package examples + +import ( + "errors" + "os" + "path/filepath" + "reflect" + "testing" +) + +func getTestDataPath() string { + pwd, _ := os.Getwd() + return filepath.Join(pwd, "../.testdata") +} + + +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 assertErrorIs(t *testing.T, e, g error) (r bool) { + if !errors.Is(g, e) { + t.Errorf("Expected [%v], got [%v]", e, g) + } + + return true +} + +func assertEqual(t *testing.T, e, g interface{}) (r bool) { + if !equal(e, g) { + t.Fatalf("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) +} \ No newline at end of file diff --git a/middleware.go b/middleware.go index a43a41b5..0853cc19 100644 --- a/middleware.go +++ b/middleware.go @@ -307,6 +307,13 @@ func addCredentials(c *Client, r *Request) error { return nil } +func createCurlCmd(c *Client, r *Request) (err error) { + if r.resultCurlCmd!=nil{ + *r.resultCurlCmd = BuildCurlRequest(r.RawRequest, c.httpClient.Jar) + } + return nil +} + func requestLogger(c *Client, r *Request) error { if r.Debug { rr := r.RawRequest diff --git a/request.go b/request.go index fec09763..0031990f 100644 --- a/request.go +++ b/request.go @@ -39,6 +39,7 @@ type Request struct { Time time.Time Body interface{} Result interface{} + resultCurlCmd *string Error interface{} RawRequest *http.Request SRV *SRVRecord @@ -73,6 +74,20 @@ type Request struct { retryConditions []RetryConditionFunc } +func (r *Request) GetCurlCmd() string { + // trigger beforeRequest from middleware + if r.RawRequest == nil { + r.client.executeBefore(r) // mock r.Get("/") + } + if r.resultCurlCmd == nil { + r.resultCurlCmd = new(string) + } + if *r.resultCurlCmd == "" { + *r.resultCurlCmd = BuildCurlRequest(r.RawRequest, r.client.httpClient.Jar) + } + return *r.resultCurlCmd +} + // Context method returns the Context if its already set in request // otherwise it creates new one using `context.Background()`. func (r *Request) Context() context.Context { @@ -333,6 +348,12 @@ func (r *Request) SetResult(res interface{}) *Request { return r } +// This method is to register curl cmd for request executed. +func (r *Request) SetResultCurlCmd(curlCmd *string) *Request { + r.resultCurlCmd = curlCmd + return r +} + // SetError method is to register the request `Error` object for automatic unmarshalling for the request, // if response status code is greater than 399 and content type either JSON or XML. // diff --git a/shellescape/shellescape.go b/shellescape/shellescape.go new file mode 100644 index 00000000..3d6b5888 --- /dev/null +++ b/shellescape/shellescape.go @@ -0,0 +1,34 @@ +/* +Package shellescape provides the shellescape.Quote to escape arbitrary +strings for a safe use as command line arguments in the most common +POSIX shells. + +The original Python package which this work was inspired by can be found +at https://pypi.python.org/pypi/shellescape. +*/ +package shellescape // "import gopkg.in/alessio/shellescape.v1" + +import ( + "regexp" + "strings" +) + +var pattern *regexp.Regexp + +func init() { + pattern = regexp.MustCompile(`[^\w@%+=:,./-]`) +} + +// Quote returns a shell-escaped version of the string s. The returned value +// is a string that can safely be used as one token in a shell command line. +func Quote(s string) string { + if len(s) == 0 { + return "''" + } + + if pattern.MatchString(s) { + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" + } + + return s +} diff --git a/util_curl.go b/util_curl.go new file mode 100644 index 00000000..017e504e --- /dev/null +++ b/util_curl.go @@ -0,0 +1,72 @@ +package resty + +import ( + "bytes" + "io" + "net/http" + "net/http/cookiejar" + + "net/url" + "strings" + + "github.com/go-resty/resty/v2/shellescape" +) + +func BuildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) { + // 1. generate curl request + curl = "curl -X " + req.Method + " " + // req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " " + headers := getHeaders(req) + for _, kv := range *headers { + curl += `-H ` + shellescape.Quote(kv[0]+": "+kv[1]) + ` ` + } + + // 1.2 generate curl with cookies + if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok{ + cookies := cookieJar.Cookies(req.URL) + if len(cookies) > 0 { + curl += ` -H ` + shellescape.Quote(dumpCookies(cookies)) + " " + } + } + + + // body + if req.Body != nil { + buf, _ := io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!! + curl += `-d ` + shellescape.Quote(string(buf)) + } + + curl += " " + shellescape.Quote(req.URL.String()) + return curl +} + +func dumpCookies(cookies []*http.Cookie) string { + sb := strings.Builder{} + sb.WriteString("Cookie: ") + for _, cookie := range cookies { + sb.WriteString(cookie.Name + "=" + url.QueryEscape(cookie.Value) + "&") + } + return strings.TrimRight(sb.String(), "&") +} + +// getHeaders +func getHeaders(req *http.Request) *[][2]string { + headers := [][2]string{} + for k, vs := range req.Header { + for _, v := range vs { + headers = append(headers, [2]string{k, v}) + } + } + n := len(headers) + for i := 0; i < n; i++ { + for j := n - 1; j > i; j-- { + jj := j - 1 + h1, h2 := headers[j], headers[jj] + if h1[0] < h2[0] { + headers[jj], headers[j] = headers[j], headers[jj] + } + } + } + return &headers +}