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