1
2
3
4
5
6
7
8
9
10
11
12
13 package chttp
14
15 import (
16 "bytes"
17 "context"
18 "encoding/json"
19 "errors"
20 "fmt"
21 "io"
22 "net/http"
23 "net/http/cookiejar"
24 "net/http/httptest"
25 "net/url"
26 "runtime"
27 "strings"
28 "testing"
29 "time"
30
31 "gitlab.com/flimzy/testy"
32 "golang.org/x/net/publicsuffix"
33
34 kivik "github.com/go-kivik/kivik/v4"
35 internal "github.com/go-kivik/kivik/v4/int/errors"
36 "github.com/go-kivik/kivik/v4/int/mock"
37 "github.com/go-kivik/kivik/v4/internal/nettest"
38 )
39
40 var defaultUA = func() string {
41 c := &Client{}
42 return c.userAgent()
43 }()
44
45 func TestNew(t *testing.T) {
46 type tt struct {
47 dsn string
48 options kivik.Option
49 expected *Client
50 status int
51 err string
52 }
53
54 tests := testy.NewTable()
55 tests.Add("invalid url", tt{
56 dsn: "http://foo.com/%xx",
57 status: http.StatusBadRequest,
58 err: `parse "?http://foo.com/%xx"?: invalid URL escape "%xx"`,
59 })
60 tests.Add("no url", tt{
61 dsn: "",
62 status: http.StatusBadRequest,
63 err: "no URL specified",
64 })
65 tests.Add("no auth", tt{
66 dsn: "http://foo.com/",
67 expected: &Client{
68 Client: &http.Client{},
69 rawDSN: "http://foo.com/",
70 dsn: &url.URL{
71 Scheme: "http",
72 Host: "foo.com",
73 Path: "/",
74 },
75 },
76 })
77 tests.Add("auth success", func(t *testing.T) interface{} {
78 h := func(w http.ResponseWriter, _ *http.Request) {
79 w.WriteHeader(http.StatusOK)
80 _, _ = fmt.Fprintf(w, `{"userCtx":{"name":"user"}}`)
81 }
82 s := nettest.NewHTTPTestServer(t, http.HandlerFunc(h))
83 authDSN, _ := url.Parse(s.URL)
84 dsn, _ := url.Parse(s.URL + "/")
85 authDSN.User = url.UserPassword("user", "password")
86 jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
87 c := &Client{
88 Client: &http.Client{Jar: jar},
89 rawDSN: authDSN.String(),
90 dsn: dsn,
91 }
92 auth := &cookieAuth{
93 Username: "user",
94 Password: "password",
95 client: c,
96 transport: http.DefaultTransport,
97 }
98 c.Client.Transport = auth
99
100 return tt{
101 dsn: authDSN.String(),
102 expected: c,
103 }
104 })
105 tests.Add("default url scheme", tt{
106 dsn: "foo.com",
107 expected: &Client{
108 Client: &http.Client{},
109 rawDSN: "foo.com",
110 dsn: &url.URL{
111 Scheme: "http",
112 Host: "foo.com",
113 Path: "/",
114 },
115 },
116 })
117 tests.Add("auth as option", func(t *testing.T) interface{} {
118 h := func(w http.ResponseWriter, _ *http.Request) {
119 w.WriteHeader(http.StatusOK)
120 _, _ = fmt.Fprintf(w, `{"userCtx":{"name":"user"}}`)
121 }
122 s := nettest.NewHTTPTestServer(t, http.HandlerFunc(h))
123 authDSN, _ := url.Parse(s.URL)
124 dsn, _ := url.Parse(s.URL + "/")
125 jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
126 c := &Client{
127 Client: &http.Client{Jar: jar},
128 rawDSN: authDSN.String(),
129 dsn: dsn,
130 }
131 auth := &cookieAuth{
132 Username: "user",
133 Password: "password",
134 client: c,
135 transport: http.DefaultTransport,
136 }
137 c.Client.Transport = auth
138
139 return tt{
140 dsn: authDSN.String(),
141 expected: c,
142 options: CookieAuth("user", "password"),
143 }
144 })
145 tests.Run(t, func(t *testing.T, tt tt) {
146 opts := tt.options
147 if opts == nil {
148 opts = mock.NilOption
149 }
150 result, err := New(&http.Client{}, tt.dsn, opts)
151 statusErrorRE(t, tt.err, tt.status, err)
152 result.UserAgents = nil
153 if d := testy.DiffInterface(tt.expected, result); d != nil {
154 t.Error(d)
155 }
156 })
157 }
158
159 func TestParseDSN(t *testing.T) {
160 tests := []struct {
161 name string
162 input string
163 expected *url.URL
164 status int
165 err string
166 }{
167 {
168 name: "happy path",
169 input: "http://foo.com/",
170 expected: &url.URL{
171 Scheme: "http",
172 Host: "foo.com",
173 Path: "/",
174 },
175 },
176 {
177 name: "default scheme",
178 input: "foo.com",
179 expected: &url.URL{
180 Scheme: "http",
181 Host: "foo.com",
182 Path: "/",
183 },
184 },
185 }
186 for _, test := range tests {
187 t.Run(test.name, func(t *testing.T) {
188 result, err := parseDSN(test.input)
189 statusErrorRE(t, test.err, test.status, err)
190 if d := testy.DiffInterface(test.expected, result); d != nil {
191 t.Fatal(d)
192 }
193 })
194 }
195 }
196
197 func TestDSN(t *testing.T) {
198 expected := "foo"
199 client := &Client{rawDSN: expected}
200 result := client.DSN()
201 if result != expected {
202 t.Errorf("Unexpected result: %s", result)
203 }
204 }
205
206 func TestFixPath(t *testing.T) {
207 tests := []struct {
208 Input string
209 Expected string
210 }{
211 {Input: "foo", Expected: "/foo"},
212 {Input: "foo?oink=yes", Expected: "/foo"},
213 {Input: "foo/bar", Expected: "/foo/bar"},
214 {Input: "foo%2Fbar", Expected: "/foo%2Fbar"},
215 }
216 for _, test := range tests {
217 req, _ := http.NewRequest("GET", "http://localhost/"+test.Input, nil)
218 fixPath(req, test.Input)
219 if req.URL.EscapedPath() != test.Expected {
220 t.Errorf("Path for '%s' not fixed.\n\tExpected: %s\n\t Actual: %s\n", test.Input, test.Expected, req.URL.EscapedPath())
221 }
222 }
223 }
224
225 func TestEncodeBody(t *testing.T) {
226 type encodeTest struct {
227 name string
228 input interface{}
229
230 expected string
231 status int
232 err string
233 }
234 tests := []encodeTest{
235 {
236 name: "Null",
237 input: nil,
238 expected: "null",
239 },
240 {
241 name: "Struct",
242 input: struct {
243 Foo string `json:"foo"`
244 }{Foo: "bar"},
245 expected: `{"foo":"bar"}`,
246 },
247 {
248 name: "JSONError",
249 input: func() {},
250 status: http.StatusBadRequest,
251 err: "json: unsupported type: func()",
252 },
253 {
254 name: "raw json input",
255 input: json.RawMessage(`{"foo":"bar"}`),
256 expected: `{"foo":"bar"}`,
257 },
258 {
259 name: "byte slice input",
260 input: []byte(`{"foo":"bar"}`),
261 expected: `{"foo":"bar"}`,
262 },
263 {
264 name: "string input",
265 input: `{"foo":"bar"}`,
266 expected: `{"foo":"bar"}`,
267 },
268 }
269 for _, test := range tests {
270 func(test encodeTest) {
271 t.Run(test.name, func(t *testing.T) {
272 t.Parallel()
273 r := EncodeBody(test.input)
274 defer r.Close()
275 body, err := io.ReadAll(r)
276 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
277 t.Error(d)
278 }
279 result := strings.TrimSpace(string(body))
280 if result != test.expected {
281 t.Errorf("Result\nExpected: %s\n Actual: %s\n", test.expected, result)
282 }
283 })
284 }(test)
285 }
286 }
287
288 func TestSetHeaders(t *testing.T) {
289 type shTest struct {
290 Name string
291 Options *Options
292 Expected http.Header
293 }
294 tests := []shTest{
295 {
296 Name: "NoOpts",
297 Expected: http.Header{
298 "Accept": {"application/json"},
299 "Content-Type": {"application/json"},
300 },
301 },
302 {
303 Name: "Content-Type",
304 Options: &Options{ContentType: "image/gif"},
305 Expected: http.Header{
306 "Accept": {"application/json"},
307 "Content-Type": {"image/gif"},
308 },
309 },
310 {
311 Name: "Accept",
312 Options: &Options{Accept: "image/gif"},
313 Expected: http.Header{
314 "Accept": {"image/gif"},
315 "Content-Type": {"application/json"},
316 },
317 },
318 {
319 Name: "FullCommit",
320 Options: &Options{FullCommit: true},
321 Expected: http.Header{
322 "Accept": {"application/json"},
323 "Content-Type": {"application/json"},
324 "X-Couch-Full-Commit": {"true"},
325 },
326 },
327 {
328 Name: "Destination",
329 Options: &Options{Header: http.Header{
330 HeaderDestination: []string{"somewhere nice"},
331 }},
332 Expected: http.Header{
333 "Accept": {"application/json"},
334 "Content-Type": {"application/json"},
335 "Destination": {"somewhere nice"},
336 },
337 },
338 {
339 Name: "If-None-Match",
340 Options: &Options{IfNoneMatch: `"foo"`},
341 Expected: http.Header{
342 "Accept": {"application/json"},
343 "Content-Type": {"application/json"},
344 "If-None-Match": {`"foo"`},
345 },
346 },
347 {
348 Name: "Unquoted If-None-Match",
349 Options: &Options{IfNoneMatch: `foo`},
350 Expected: http.Header{
351 "Accept": {"application/json"},
352 "Content-Type": {"application/json"},
353 "If-None-Match": {`"foo"`},
354 },
355 },
356 }
357 for _, test := range tests {
358 func(test shTest) {
359 t.Run(test.Name, func(t *testing.T) {
360 t.Parallel()
361 req, err := http.NewRequest("GET", "/", nil)
362 if err != nil {
363 panic(err)
364 }
365 setHeaders(req, test.Options)
366 if d := testy.DiffInterface(test.Expected, req.Header); d != nil {
367 t.Errorf("Headers:\n%s\n", d)
368 }
369 })
370 }(test)
371 }
372 }
373
374 func TestSetQuery(t *testing.T) {
375 tests := []struct {
376 name string
377 req *http.Request
378 opts *Options
379 expected *http.Request
380 }{
381 {
382 name: "nil query",
383 req: &http.Request{URL: &url.URL{}},
384 expected: &http.Request{URL: &url.URL{}},
385 },
386 {
387 name: "empty query",
388 req: &http.Request{URL: &url.URL{RawQuery: "a=b"}},
389 opts: &Options{Query: url.Values{}},
390 expected: &http.Request{URL: &url.URL{RawQuery: "a=b"}},
391 },
392 {
393 name: "options query",
394 req: &http.Request{URL: &url.URL{}},
395 opts: &Options{Query: url.Values{"foo": []string{"a"}}},
396 expected: &http.Request{URL: &url.URL{RawQuery: "foo=a"}},
397 },
398 {
399 name: "merged queries",
400 req: &http.Request{URL: &url.URL{RawQuery: "bar=b"}},
401 opts: &Options{Query: url.Values{"foo": []string{"a"}}},
402 expected: &http.Request{URL: &url.URL{RawQuery: "bar=b&foo=a"}},
403 },
404 }
405 for _, test := range tests {
406 t.Run(test.name, func(t *testing.T) {
407 setQuery(test.req, test.opts)
408 if d := testy.DiffInterface(test.expected, test.req); d != nil {
409 t.Error(d)
410 }
411 })
412 }
413 }
414
415 func TestETag(t *testing.T) {
416 tests := []struct {
417 name string
418 input *http.Response
419 expected string
420 found bool
421 }{
422 {
423 name: "nil response",
424 input: nil,
425 expected: "",
426 found: false,
427 },
428 {
429 name: "No etag",
430 input: &http.Response{},
431 expected: "",
432 found: false,
433 },
434 {
435 name: "ETag",
436 input: &http.Response{
437 Header: http.Header{
438 "ETag": {`"foo"`},
439 },
440 },
441 expected: "foo",
442 found: true,
443 },
444 {
445 name: "Etag",
446 input: &http.Response{
447 Header: http.Header{
448 "Etag": {`"bar"`},
449 },
450 },
451 expected: "bar",
452 found: true,
453 },
454 }
455 for _, test := range tests {
456 t.Run(test.name, func(t *testing.T) {
457 result, found := ETag(test.input)
458 if result != test.expected {
459 t.Errorf("Unexpected result: %s", result)
460 }
461 if found != test.found {
462 t.Errorf("Unexpected found: %v", found)
463 }
464 })
465 }
466 }
467
468 func TestGetRev(t *testing.T) {
469 tests := []struct {
470 name string
471 resp *http.Response
472 expected, err string
473 }{
474 {
475 resp: &http.Response{
476 Request: &http.Request{
477 Method: http.MethodHead,
478 },
479 },
480 expected: "",
481 err: "unable to determine document revision",
482 },
483 {
484 name: "no ETag header",
485 resp: &http.Response{
486 StatusCode: 200,
487 Request: &http.Request{Method: "POST"},
488 Body: io.NopCloser(strings.NewReader("")),
489 },
490 err: "unable to determine document revision: EOF",
491 },
492 {
493 name: "normalized Etag header",
494 resp: &http.Response{
495 StatusCode: 200,
496 Request: &http.Request{Method: "POST"},
497 Header: http.Header{"Etag": {`"12345"`}},
498 Body: io.NopCloser(strings.NewReader("")),
499 },
500 expected: `12345`,
501 },
502 {
503 name: "standard ETag header",
504 resp: &http.Response{
505 StatusCode: 200,
506 Request: &http.Request{Method: "POST"},
507 Header: http.Header{"ETag": {`"12345"`}},
508 Body: Body(""),
509 },
510 expected: `12345`,
511 },
512 }
513 for _, test := range tests {
514 t.Run(test.name, func(t *testing.T) {
515 result, err := GetRev(test.resp)
516 if !testy.ErrorMatches(test.err, err) {
517 t.Errorf("Unexpected error: %s", err)
518 }
519 if result != test.expected {
520 t.Errorf("Got %s, expected %s", result, test.expected)
521 }
522 })
523 }
524 }
525
526 func TestDoJSON(t *testing.T) {
527 tests := []struct {
528 name string
529 method, path string
530 opts *Options
531 client *Client
532 expected interface{}
533 status int
534 err string
535 }{
536 {
537 name: "network error",
538 method: "GET",
539 client: newTestClient(nil, errors.New("net error")),
540 status: http.StatusBadGateway,
541 err: `Get "?http://example.com"?: net error`,
542 },
543 {
544 name: "error response",
545 method: "GET",
546 client: newTestClient(&http.Response{
547 StatusCode: 401,
548 Header: http.Header{
549 "Content-Type": {"application/json"},
550 "Content-Length": {"67"},
551 },
552 ContentLength: 67,
553 Body: Body(`{"error":"unauthorized","reason":"Name or password is incorrect."}`),
554 Request: &http.Request{Method: "GET"},
555 }, nil),
556 status: http.StatusUnauthorized,
557 err: "Unauthorized: Name or password is incorrect.",
558 },
559 {
560 name: "invalid JSON in response",
561 method: "GET",
562 client: newTestClient(&http.Response{
563 StatusCode: 200,
564 Header: http.Header{
565 "Content-Type": {"application/json"},
566 "Content-Length": {"67"},
567 },
568 ContentLength: 67,
569 Body: Body(`invalid response`),
570 Request: &http.Request{Method: "GET"},
571 }, nil),
572 status: http.StatusBadGateway,
573 err: "invalid character 'i' looking for beginning of value",
574 },
575 {
576 name: "success",
577 method: "GET",
578 client: newTestClient(&http.Response{
579 StatusCode: 200,
580 Header: http.Header{
581 "Content-Type": {"application/json"},
582 "Content-Length": {"15"},
583 },
584 ContentLength: 15,
585 Body: Body(`{"foo":"bar"}`),
586 Request: &http.Request{Method: "GET"},
587 }, nil),
588 expected: map[string]interface{}{"foo": "bar"},
589 },
590 }
591 for _, test := range tests {
592 t.Run(test.name, func(t *testing.T) {
593 var i interface{}
594 err := test.client.DoJSON(context.Background(), test.method, test.path, test.opts, &i)
595 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
596 t.Error(d)
597 }
598 if d := testy.DiffInterface(test.expected, i); d != nil {
599 t.Errorf("JSON result differs:\n%s\n", d)
600 }
601 })
602 }
603 }
604
605 func TestNewRequest(t *testing.T) {
606 tests := []struct {
607 name string
608 method, path string
609 expected *http.Request
610 client *Client
611 status int
612 err string
613 }{
614 {
615 name: "invalid URL",
616 client: newTestClient(nil, nil),
617 method: "GET",
618 path: "%xx",
619 status: http.StatusBadRequest,
620 err: `parse "?%xx"?: invalid URL escape "%xx"`,
621 },
622 {
623 name: "invalid method",
624 method: "FOO BAR",
625 client: newTestClient(nil, nil),
626 status: http.StatusBadRequest,
627 err: `net/http: invalid method "FOO BAR"`,
628 },
629 {
630 name: "success",
631 method: "GET",
632 path: "foo",
633 client: newTestClient(nil, nil),
634 expected: &http.Request{
635 Method: "GET",
636 URL: func() *url.URL {
637 url := newTestClient(nil, nil).dsn
638 url.Path = "/foo"
639 return url
640 }(),
641 Proto: "HTTP/1.1",
642 ProtoMajor: 1,
643 ProtoMinor: 1,
644 Header: http.Header{
645 "User-Agent": []string{defaultUA},
646 },
647 Host: "example.com",
648 },
649 },
650 }
651 for _, test := range tests {
652 t.Run(test.name, func(t *testing.T) {
653 req, err := test.client.NewRequest(context.Background(), test.method, test.path, nil, nil)
654 statusErrorRE(t, test.err, test.status, err)
655 test.expected = test.expected.WithContext(req.Context())
656 if d := testy.DiffInterface(test.expected, req); d != nil {
657 t.Error(d)
658 }
659 })
660 }
661 }
662
663 func TestDoReq(t *testing.T) {
664 type tt struct {
665 trace func(t *testing.T, success *bool) *ClientTrace
666 method, path string
667 opts *Options
668 client *Client
669 status int
670 err string
671 }
672
673 tests := testy.NewTable()
674 tests.Add("no method", tt{
675 status: 500,
676 err: "chttp: method required",
677 })
678 tests.Add("invalid url", tt{
679 method: "GET",
680 path: "%xx",
681 client: newTestClient(nil, nil),
682 status: http.StatusBadRequest,
683 err: `parse "?%xx"?: invalid URL escape "%xx"`,
684 })
685 tests.Add("network error", tt{
686 method: "GET",
687 path: "foo",
688 client: newTestClient(nil, errors.New("net error")),
689 status: http.StatusBadGateway,
690 err: `Get "?http://example.com/foo"?: net error`,
691 })
692 tests.Add("error response", tt{
693 method: "GET",
694 path: "foo",
695 client: newTestClient(&http.Response{
696 StatusCode: 400,
697 Body: Body(""),
698 }, nil),
699
700 })
701 tests.Add("success", tt{
702 method: "GET",
703 path: "foo",
704 client: newTestClient(&http.Response{
705 StatusCode: 200,
706 Body: Body(""),
707 }, nil),
708
709 })
710 tests.Add("body error", tt{
711 method: "PUT",
712 path: "foo",
713 client: newTestClient(nil, &internal.Error{Status: http.StatusBadRequest, Message: "bad request"}),
714 status: http.StatusBadRequest,
715 err: `Put "?http://example.com/foo"?: bad request`,
716 })
717 tests.Add("response trace", tt{
718 trace: func(t *testing.T, success *bool) *ClientTrace {
719 return &ClientTrace{
720 HTTPResponse: func(r *http.Response) {
721 *success = true
722 expected := &http.Response{StatusCode: 200}
723 if d := testy.DiffHTTPResponse(expected, r); d != nil {
724 t.Error(d)
725 }
726 },
727 }
728 },
729 method: "GET",
730 path: "foo",
731 client: newTestClient(&http.Response{
732 StatusCode: 200,
733 Body: Body(""),
734 }, nil),
735
736 })
737 tests.Add("response body trace", tt{
738 trace: func(t *testing.T, success *bool) *ClientTrace {
739 return &ClientTrace{
740 HTTPResponseBody: func(r *http.Response) {
741 *success = true
742 expected := &http.Response{
743 StatusCode: 200,
744 Body: Body("foo"),
745 }
746 if d := testy.DiffHTTPResponse(expected, r); d != nil {
747 t.Error(d)
748 }
749 },
750 }
751 },
752 method: "PUT",
753 path: "foo",
754 client: newTestClient(&http.Response{
755 StatusCode: 200,
756 Body: Body("foo"),
757 }, nil),
758
759 })
760 tests.Add("request trace", tt{
761 trace: func(t *testing.T, success *bool) *ClientTrace {
762 return &ClientTrace{
763 HTTPRequest: func(r *http.Request) {
764 *success = true
765 expected := httptest.NewRequest("PUT", "/foo", nil)
766 expected.Header.Add("Accept", "application/json")
767 expected.Header.Add("Content-Type", "application/json")
768 expected.Header.Add("Content-Encoding", "gzip")
769 expected.Header.Add("User-Agent", defaultUA)
770 if d := testy.DiffHTTPRequest(expected, r); d != nil {
771 t.Error(d)
772 }
773 },
774 }
775 },
776 method: "PUT",
777 path: "/foo",
778 client: newTestClient(&http.Response{
779 StatusCode: 200,
780 Body: Body("foo"),
781 }, nil),
782 opts: &Options{
783 Body: Body("bar"),
784 },
785
786 })
787 tests.Add("request body trace", tt{
788 trace: func(t *testing.T, success *bool) *ClientTrace {
789 return &ClientTrace{
790 HTTPRequestBody: func(r *http.Request) {
791 *success = true
792 body := io.NopCloser(bytes.NewReader([]byte{
793 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 74, 74, 44, 2,
794 4, 0, 0, 255, 255, 170, 140, 255, 118, 3, 0, 0, 0,
795 }))
796 expected := httptest.NewRequest("PUT", "/foo", body)
797 expected.Header.Add("Accept", "application/json")
798 expected.Header.Add("Content-Type", "application/json")
799 expected.Header.Add("Content-Encoding", "gzip")
800 expected.Header.Add("User-Agent", defaultUA)
801 expected.Header.Add("Content-Length", "27")
802 if d := testy.DiffHTTPRequest(expected, r); d != nil {
803 t.Error(d)
804 }
805 },
806 }
807 },
808 method: "PUT",
809 path: "/foo",
810 client: newTestClient(&http.Response{
811 StatusCode: 200,
812 Body: Body("foo"),
813 }, nil),
814 opts: &Options{
815 Body: Body("bar"),
816 },
817
818 })
819 tests.Add("couchdb mounted below root", tt{
820 client: newCustomClient("http://foo.com/dbroot/", func(r *http.Request) (*http.Response, error) {
821 if r.URL.Path != "/dbroot/foo" {
822 return nil, fmt.Errorf("Unexpected path: %s", r.URL.Path)
823 }
824 return &http.Response{}, nil
825 }),
826 method: "GET",
827 path: "/foo",
828 })
829 tests.Add("user agent", tt{
830 client: newCustomClient("http://foo.com/", func(r *http.Request) (*http.Response, error) {
831 if ua := r.UserAgent(); ua != defaultUA {
832 return nil, fmt.Errorf("Unexpected User Agent: %s", ua)
833 }
834 return &http.Response{}, nil
835 }),
836 method: "GET",
837 path: "/foo",
838 })
839 tests.Add("gzipped request", tt{
840 client: newCustomClient("http://foo.com/", func(r *http.Request) (*http.Response, error) {
841 if ce := r.Header.Get("Content-Encoding"); ce != "gzip" {
842 return nil, fmt.Errorf("Unexpected Content-Encoding: %s", ce)
843 }
844 return &http.Response{}, nil
845 }),
846 method: "PUT",
847 path: "/foo",
848 opts: &Options{
849 Body: Body("raw body"),
850 },
851 })
852 tests.Add("gzipped disabled", tt{
853 client: newCustomClient("http://foo.com/", func(r *http.Request) (*http.Response, error) {
854 if ce := r.Header.Get("Content-Encoding"); ce != "" {
855 return nil, fmt.Errorf("Unexpected Content-Encoding: %s", ce)
856 }
857 return &http.Response{}, nil
858 }),
859 method: "PUT",
860 path: "/foo",
861 opts: &Options{
862 Body: Body("raw body"),
863 NoGzip: true,
864 },
865 })
866
867 tests.Run(t, func(t *testing.T, tt tt) {
868 ctx := context.Background()
869 traceSuccess := true
870 if tt.trace != nil {
871 traceSuccess = false
872 ctx = WithClientTrace(ctx, tt.trace(t, &traceSuccess))
873 }
874 res, err := tt.client.DoReq(ctx, tt.method, tt.path, tt.opts)
875 statusErrorRE(t, tt.err, tt.status, err)
876 t.Cleanup(func() {
877 _ = res.Body.Close()
878 })
879 _, _ = io.Copy(io.Discard, res.Body)
880 if !traceSuccess {
881 t.Error("Trace failed")
882 }
883 })
884 }
885
886 func TestDoError(t *testing.T) {
887 tests := []struct {
888 name string
889 method, path string
890 opts *Options
891 client *Client
892 status int
893 err string
894 }{
895 {
896 name: "no method",
897 status: 500,
898 err: "chttp: method required",
899 },
900 {
901 name: "error response",
902 method: "GET",
903 path: "foo",
904 client: newTestClient(&http.Response{
905 StatusCode: http.StatusBadRequest,
906 Body: Body(""),
907 Request: &http.Request{Method: "GET"},
908 }, nil),
909 status: http.StatusBadRequest,
910 err: "Bad Request",
911 },
912 {
913 name: "success",
914 method: "GET",
915 path: "foo",
916 client: newTestClient(&http.Response{
917 StatusCode: http.StatusOK,
918 Body: Body(""),
919 Request: &http.Request{Method: "GET"},
920 }, nil),
921
922 },
923 }
924 for _, test := range tests {
925 t.Run(test.name, func(t *testing.T) {
926 _, err := test.client.DoError(context.Background(), test.method, test.path, test.opts)
927 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
928 t.Error(d)
929 }
930 })
931 }
932 }
933
934 func TestNetError(t *testing.T) {
935 tests := []struct {
936 name string
937 input error
938
939 status int
940 err string
941 }{
942 {
943 name: "nil",
944 input: nil,
945 status: 0,
946 err: "",
947 },
948 {
949 name: "timeout",
950 input: func() error {
951 s := nettest.NewHTTPTestServer(t, http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
952 time.Sleep(1 * time.Second)
953 }))
954 ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
955 defer cancel()
956 req, err := http.NewRequest("GET", s.URL, nil)
957 if err != nil {
958 t.Fatal(err)
959 }
960 _, err = http.DefaultClient.Do(req.WithContext(ctx))
961 return err
962 }(),
963 status: http.StatusBadGateway,
964 err: `(Get "?http://127.0.0.1:\d+"?: context deadline exceeded|dial tcp 127.0.0.1:\d+: i/o timeout)`,
965 },
966 {
967 name: "cannot resolve host",
968 input: func() error {
969 req, err := http.NewRequest("GET", "http://foo.com.invalid.hostname", nil)
970 if err != nil {
971 t.Fatal(err)
972 }
973 _, err = http.DefaultClient.Do(req)
974 return err
975 }(),
976 status: http.StatusBadGateway,
977 err: ": no such host$",
978 },
979 {
980 name: "connection refused",
981 input: func() error {
982 req, err := http.NewRequest("GET", "http://localhost:99", nil)
983 if err != nil {
984 t.Fatal(err)
985 }
986 _, err = http.DefaultClient.Do(req)
987 return err
988 }(),
989 status: http.StatusBadGateway,
990 err: ": connection refused$",
991 },
992 {
993 name: "too many redirects",
994 input: func() error {
995 var s *httptest.Server
996 redirHandler := func(w http.ResponseWriter, r *http.Request) {
997 http.Redirect(w, r, s.URL, 302)
998 }
999 s = nettest.NewHTTPTestServer(t, http.HandlerFunc(redirHandler))
1000 _, err := http.Get(s.URL)
1001 return err
1002 }(),
1003 status: http.StatusBadGateway,
1004 err: `^Get "?http://127.0.0.1:\d+"?: stopped after 10 redirects$`,
1005 },
1006 {
1007 name: "url error",
1008 input: &url.Error{
1009 Op: "Get",
1010 URL: "http://foo.com/",
1011 Err: errors.New("some error"),
1012 },
1013 status: http.StatusBadGateway,
1014 err: `Get "?http://foo.com/"?: some error`,
1015 },
1016 {
1017 name: "url error with embedded status",
1018 input: &url.Error{
1019 Op: "Get",
1020 URL: "http://foo.com/",
1021 Err: &internal.Error{Status: http.StatusBadRequest, Message: "some error"},
1022 },
1023 status: http.StatusBadRequest,
1024 err: `Get "?http://foo.com/"?: some error`,
1025 },
1026 {
1027 name: "other error",
1028 input: errors.New("other error"),
1029 status: http.StatusBadGateway,
1030 err: "other error",
1031 },
1032 {
1033 name: "other error with embedded status",
1034 input: &internal.Error{Status: http.StatusBadRequest, Message: "bad req"},
1035 status: http.StatusBadRequest,
1036 err: "bad req",
1037 },
1038 }
1039 for _, test := range tests {
1040 t.Run(test.name, func(t *testing.T) {
1041 err := netError(test.input)
1042 statusErrorRE(t, test.err, test.status, err)
1043 })
1044 }
1045 }
1046
1047 func TestUserAgent(t *testing.T) {
1048 tests := []struct {
1049 name string
1050 ua []string
1051 expected string
1052 }{
1053 {
1054 name: "defaults",
1055 expected: fmt.Sprintf("%s/%s (Language=%s; Platform=%s/%s)",
1056 userAgent, kivik.Version, runtime.Version(), runtime.GOARCH, runtime.GOOS),
1057 },
1058 {
1059 name: "custom",
1060 ua: []string{"Oinky/1.2.3"},
1061 expected: fmt.Sprintf("%s/%s (Language=%s; Platform=%s/%s) Oinky/1.2.3",
1062 userAgent, kivik.Version, runtime.Version(), runtime.GOARCH, runtime.GOOS),
1063 },
1064 {
1065 name: "multiple",
1066 ua: []string{"Oinky/1.2.3", "Moo/5.4.3"},
1067 expected: fmt.Sprintf("%s/%s (Language=%s; Platform=%s/%s) Oinky/1.2.3 Moo/5.4.3",
1068 userAgent, kivik.Version, runtime.Version(), runtime.GOARCH, runtime.GOOS),
1069 },
1070 }
1071 for _, test := range tests {
1072 t.Run(test.name, func(t *testing.T) {
1073 c := &Client{
1074 UserAgents: test.ua,
1075 }
1076 result := c.userAgent()
1077 if result != test.expected {
1078 t.Errorf("Unexpected user agent: %s", result)
1079 }
1080 })
1081 }
1082 }
1083
1084 func TestExtractRev(t *testing.T) {
1085 type tt struct {
1086 rc io.ReadCloser
1087 rev string
1088 err string
1089 }
1090
1091 tests := testy.NewTable()
1092 tests.Add("empty body", tt{
1093 rc: io.NopCloser(strings.NewReader("")),
1094 rev: "",
1095 err: "unable to determine document revision: EOF",
1096 })
1097 tests.Add("invalid JSON", tt{
1098 rc: io.NopCloser(strings.NewReader(`bogus`)),
1099 err: `unable to determine document revision: invalid character 'b' looking for beginning of value`,
1100 })
1101 tests.Add("rev found", tt{
1102 rc: io.NopCloser(strings.NewReader(`{"_rev":"1-xyz"}`)),
1103 rev: "1-xyz",
1104 })
1105 tests.Add("rev found in middle", tt{
1106 rc: io.NopCloser(strings.NewReader(`{
1107 "_id":"foo",
1108 "_rev":"1-xyz",
1109 "asdf":"qwerty",
1110 "number":12345
1111 }`)),
1112 rev: "1-xyz",
1113 })
1114 tests.Add("rev not found", tt{
1115 rc: io.NopCloser(strings.NewReader(`{
1116 "_id":"foo",
1117 "asdf":"qwerty",
1118 "number":12345
1119 }`)),
1120 err: "unable to determine document revision: _rev key not found in response body",
1121 })
1122
1123 tests.Run(t, func(t *testing.T, tt tt) {
1124 reassembled, rev, err := ExtractRev(tt.rc)
1125 if !testy.ErrorMatches(tt.err, err) {
1126 t.Errorf("Unexpected error: %s", err)
1127 }
1128 if err != nil {
1129 return
1130 }
1131 if tt.rev != rev {
1132 t.Errorf("Expected %s, got %s", tt.rev, rev)
1133 }
1134 if d := testy.DiffJSON(testy.Snapshot(t), reassembled); d != nil {
1135 t.Error(d)
1136 }
1137 })
1138 }
1139
1140 func Test_readRev(t *testing.T) {
1141 type tt struct {
1142 input string
1143 rev string
1144 err string
1145 }
1146
1147 tests := testy.NewTable()
1148 tests.Add("empty body", tt{
1149 input: "",
1150 err: "EOF",
1151 })
1152 tests.Add("invalid JSON", tt{
1153 input: "bogus",
1154 err: `invalid character 'b' looking for beginning of value`,
1155 })
1156 tests.Add("non-object", tt{
1157 input: "[]",
1158 err: `Expected '{' token, found "["`,
1159 })
1160 tests.Add("_rev missing", tt{
1161 input: "{}",
1162 err: "_rev key not found in response body",
1163 })
1164 tests.Add("invalid key", tt{
1165 input: "{asdf",
1166 err: `invalid character 'a'`,
1167 })
1168 tests.Add("invalid value", tt{
1169 input: `{"_rev":xyz}`,
1170 err: `invalid character 'x' looking for beginning of value`,
1171 })
1172 tests.Add("non-string rev", tt{
1173 input: `{"_rev":[]}`,
1174 err: `found "[" in place of _rev value`,
1175 })
1176 tests.Add("success", tt{
1177 input: `{"_rev":"1-xyz"}`,
1178 rev: "1-xyz",
1179 })
1180
1181 tests.Run(t, func(t *testing.T, tt tt) {
1182 rev, err := readRev(strings.NewReader(tt.input))
1183 if !testy.ErrorMatches(tt.err, err) {
1184 t.Errorf("Unexpected error: %s", err)
1185 }
1186 if rev != tt.rev {
1187 t.Errorf("Wanted %s, got %s", tt.rev, rev)
1188 }
1189 })
1190 }
1191
View as plain text