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