github.com/go-kivik/kivik/v4@v4.3.2/couchdb/chttp/chttp_test.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 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 // Determinism 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() {}, // Functions cannot be marshaled to JSON 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() // nolint: errcheck 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()) // determinism 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 // No error here 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 // success! 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 { //nolint:thelper // Not a helper 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 // response body trace 736 }) 737 tests.Add("response body trace", tt{ 738 trace: func(t *testing.T, success *bool) *ClientTrace { //nolint:thelper // Not a helper 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 // response trace 759 }) 760 tests.Add("request trace", tt{ 761 trace: func(t *testing.T, success *bool) *ClientTrace { //nolint:thelper // Not a helper 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 // request trace 786 }) 787 tests.Add("request body trace", tt{ 788 trace: func(t *testing.T, success *bool) *ClientTrace { //nolint:thelper // Not a helper 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 // request body trace 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 // No error 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 }