...

Source file src/github.com/go-kivik/kivik/v4/couchdb/chttp/chttp_test.go

Documentation: github.com/go-kivik/kivik/v4/couchdb/chttp

     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  }
  1191  

View as plain text