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