...

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

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

     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 couchdb
    14  
    15  import (
    16  	"context"
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"net/http"
    21  	"net/url"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"gitlab.com/flimzy/testy"
    26  
    27  	kivik "github.com/go-kivik/kivik/v4"
    28  	"github.com/go-kivik/kivik/v4/driver"
    29  	internal "github.com/go-kivik/kivik/v4/int/errors"
    30  	"github.com/go-kivik/kivik/v4/int/mock"
    31  )
    32  
    33  func TestAllDBs(t *testing.T) {
    34  	tests := []struct {
    35  		name     string
    36  		client   *client
    37  		options  kivik.Option
    38  		expected []string
    39  		status   int
    40  		err      string
    41  	}{
    42  		{
    43  			name:   "network error",
    44  			client: newTestClient(nil, errors.New("net error")),
    45  			status: http.StatusBadGateway,
    46  			err:    `Get "?http://example.com/_all_dbs"?: net error`,
    47  		},
    48  		{
    49  			name: "2.0.0",
    50  			client: newTestClient(&http.Response{
    51  				StatusCode: 200,
    52  				Header: http.Header{
    53  					"Server":              {"CouchDB/2.0.0 (Erlang OTP/17)"},
    54  					"Date":                {"Fri, 27 Oct 2017 15:15:07 GMT"},
    55  					"Content-Type":        {"application/json"},
    56  					"ETag":                {`"33UVNAZU752CYNGBBTMWQFP7U"`},
    57  					"Transfer-Encoding":   {"chunked"},
    58  					"X-Couch-Request-ID":  {"ab5cd97c3e"},
    59  					"X-CouchDB-Body-Time": {"0"},
    60  				},
    61  				Body: Body(`["_global_changes","_replicator","_users"]`),
    62  			}, nil),
    63  			expected: []string{"_global_changes", "_replicator", "_users"},
    64  		},
    65  	}
    66  	for _, test := range tests {
    67  		t.Run(test.name, func(t *testing.T) {
    68  			opts := test.options
    69  			if opts == nil {
    70  				opts = mock.NilOption
    71  			}
    72  			result, err := test.client.AllDBs(context.Background(), opts)
    73  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
    74  				t.Error(d)
    75  			}
    76  			if d := testy.DiffInterface(test.expected, result); d != nil {
    77  				t.Error(d)
    78  			}
    79  		})
    80  	}
    81  }
    82  
    83  func TestDBExists(t *testing.T) {
    84  	tests := []struct {
    85  		name   string
    86  		client *client
    87  		dbName string
    88  		exists bool
    89  		status int
    90  		err    string
    91  	}{
    92  		{
    93  			name:   "no db specified",
    94  			status: http.StatusBadRequest,
    95  			err:    "kivik: dbName required",
    96  		},
    97  		{
    98  			name:   "network error",
    99  			dbName: "foo",
   100  			client: newTestClient(nil, errors.New("net error")),
   101  			status: http.StatusBadGateway,
   102  			err:    `Head "?http://example.com/foo"?: net error`,
   103  		},
   104  		{
   105  			name:   "not found, 1.6.1",
   106  			dbName: "foox",
   107  			client: newTestClient(&http.Response{
   108  				StatusCode: 404,
   109  				Header: http.Header{
   110  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   111  					"Date":           {"Fri, 27 Oct 2017 15:09:19 GMT"},
   112  					"Content-Type":   {"text/plain; charset=utf-8"},
   113  					"Content-Length": {"44"},
   114  					"Cache-Control":  {"must-revalidate"},
   115  				},
   116  				Body: Body(""),
   117  			}, nil),
   118  			exists: false,
   119  		},
   120  		{
   121  			name:   "exists, 1.6.1",
   122  			dbName: "foo",
   123  			client: newTestClient(&http.Response{
   124  				StatusCode: 200,
   125  				Header: http.Header{
   126  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   127  					"Date":           {"Fri, 27 Oct 2017 15:09:19 GMT"},
   128  					"Content-Type":   {"text/plain; charset=utf-8"},
   129  					"Content-Length": {"229"},
   130  					"Cache-Control":  {"must-revalidate"},
   131  				},
   132  				Body: Body(""),
   133  			}, nil),
   134  			exists: true,
   135  		},
   136  		{
   137  			name:   "slashes",
   138  			dbName: "foo/bar",
   139  			client: newCustomClient(func(req *http.Request) (*http.Response, error) {
   140  				if err := consume(req.Body); err != nil {
   141  					return nil, err
   142  				}
   143  				expected := "/" + url.PathEscape("foo/bar")
   144  				actual := req.URL.RawPath
   145  				if actual != expected {
   146  					return nil, fmt.Errorf("expected path %s, got %s", expected, actual)
   147  				}
   148  				response := &http.Response{
   149  					StatusCode: 200,
   150  					Header: http.Header{
   151  						"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   152  						"Date":           {"Fri, 27 Oct 2017 15:09:19 GMT"},
   153  						"Content-Type":   {"text/plain; charset=utf-8"},
   154  						"Content-Length": {"229"},
   155  						"Cache-Control":  {"must-revalidate"},
   156  					},
   157  					Body: Body(""),
   158  				}
   159  				response.Request = req
   160  				return response, nil
   161  			}),
   162  			exists: true,
   163  		},
   164  	}
   165  	for _, test := range tests {
   166  		t.Run(test.name, func(t *testing.T) {
   167  			exists, err := test.client.DBExists(context.Background(), test.dbName, nil)
   168  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   169  				t.Error(d)
   170  			}
   171  			if exists != test.exists {
   172  				t.Errorf("Unexpected result: %t", exists)
   173  			}
   174  		})
   175  	}
   176  }
   177  
   178  func TestCreateDB(t *testing.T) {
   179  	tests := []struct {
   180  		name    string
   181  		dbName  string
   182  		options kivik.Option
   183  		client  *client
   184  		status  int
   185  		err     string
   186  	}{
   187  		{
   188  			name:   "missing dbname",
   189  			status: http.StatusBadRequest,
   190  			err:    "kivik: dbName required",
   191  		},
   192  		{
   193  			name:   "network error",
   194  			dbName: "foo",
   195  			client: newTestClient(nil, errors.New("net error")),
   196  			status: http.StatusBadGateway,
   197  			err:    `Put "?http://example.com/foo"?: net error`,
   198  		},
   199  		{
   200  			name:   "conflict 1.6.1",
   201  			dbName: "foo",
   202  			client: newTestClient(&http.Response{
   203  				StatusCode: 412,
   204  				Header: http.Header{
   205  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   206  					"Date":           {"Fri, 27 Oct 2017 15:23:57 GMT"},
   207  					"Content-Type":   {"application/json"},
   208  					"Content-Length": {"94"},
   209  					"Cache-Control":  {"must-revalidate"},
   210  				},
   211  				ContentLength: 94,
   212  				Body:          Body(`{"error":"file_exists","reason":"The database could not be created, the file already exists."}`),
   213  			}, nil),
   214  			status: http.StatusPreconditionFailed,
   215  			err:    "Precondition Failed: The database could not be created, the file already exists.",
   216  		},
   217  	}
   218  	for _, test := range tests {
   219  		t.Run(test.name, func(t *testing.T) {
   220  			opts := test.options
   221  			if opts == nil {
   222  				opts = mock.NilOption
   223  			}
   224  			err := test.client.CreateDB(context.Background(), test.dbName, opts)
   225  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   226  				t.Error(d)
   227  			}
   228  		})
   229  	}
   230  }
   231  
   232  func TestDestroyDB(t *testing.T) {
   233  	tests := []struct {
   234  		name   string
   235  		client *client
   236  		dbName string
   237  		status int
   238  		err    string
   239  	}{
   240  		{
   241  			name:   "no db name",
   242  			status: http.StatusBadRequest,
   243  			err:    "kivik: dbName required",
   244  		},
   245  		{
   246  			name:   "network error",
   247  			dbName: "foo",
   248  			client: newTestClient(nil, errors.New("net error")),
   249  			status: http.StatusBadGateway,
   250  			err:    `(Delete "?http://example.com/foo"?: )?net error`,
   251  		},
   252  		{
   253  			name:   "1.6.1",
   254  			dbName: "foo",
   255  			client: newTestClient(&http.Response{
   256  				StatusCode: 200,
   257  				Header: http.Header{
   258  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   259  					"Date":           {"Fri, 27 Oct 2017 17:12:45 GMT"},
   260  					"Content-Type":   {"application/json"},
   261  					"Content-Length": {"12"},
   262  					"Cache-Control":  {"must-revalidate"},
   263  				},
   264  				Body: Body(`{"ok":true}`),
   265  			}, nil),
   266  		},
   267  	}
   268  	for _, test := range tests {
   269  		t.Run(test.name, func(t *testing.T) {
   270  			err := test.client.DestroyDB(context.Background(), test.dbName, nil)
   271  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   272  				t.Error(d)
   273  			}
   274  		})
   275  	}
   276  }
   277  
   278  func TestDBUpdates(t *testing.T) {
   279  	tests := []struct {
   280  		name       string
   281  		client     *client
   282  		options    driver.Options
   283  		want       []driver.DBUpdate
   284  		wantStatus int
   285  		wantErr    string
   286  	}{
   287  		{
   288  			name:       "network error",
   289  			client:     newTestClient(nil, errors.New("net error")),
   290  			wantStatus: http.StatusBadGateway,
   291  			wantErr:    `Get "?http://example.com/_db_updates\?feed=continuous&since=now"?: net error`,
   292  		},
   293  		{
   294  			name: "CouchDB defaults, network error",
   295  			options: kivik.Params(map[string]interface{}{
   296  				"feed":  "",
   297  				"since": "",
   298  			}),
   299  			client:     newTestClient(nil, errors.New("net error")),
   300  			wantStatus: http.StatusBadGateway,
   301  			wantErr:    `Get "?http://example.com/_db_updates"?: net error`,
   302  		},
   303  		{
   304  			name: "error response",
   305  			client: newTestClient(&http.Response{
   306  				StatusCode: 400,
   307  				Body:       Body(""),
   308  			}, nil),
   309  			wantStatus: http.StatusBadRequest,
   310  			wantErr:    "Bad Request",
   311  		},
   312  		{
   313  			name: "Success 1.6.1",
   314  			client: newTestClient(&http.Response{
   315  				StatusCode: 200,
   316  				Header: http.Header{
   317  					"Transfer-Encoding": {"chunked"},
   318  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   319  					"Date":              {"Fri, 27 Oct 2017 19:55:43 GMT"},
   320  					"Content-Type":      {"application/json"},
   321  					"Cache-Control":     {"must-revalidate"},
   322  				},
   323  				Body: Body(`{"db_name":"mailbox","type":"created","seq":"1-g1AAAAFR"}
   324  				{"db_name":"mailbox","type":"deleted","seq":"2-g1AAAAFR"}`),
   325  			}, nil),
   326  			want: []driver.DBUpdate{
   327  				{DBName: "mailbox", Type: "created", Seq: "1-g1AAAAFR"},
   328  				{DBName: "mailbox", Type: "deleted", Seq: "2-g1AAAAFR"},
   329  			},
   330  		},
   331  		{
   332  			name: "non-JSON response",
   333  			client: newTestClient(&http.Response{
   334  				StatusCode: 200,
   335  				Header: http.Header{
   336  					"Transfer-Encoding": {"chunked"},
   337  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   338  					"Date":              {"Fri, 27 Oct 2017 19:55:43 GMT"},
   339  					"Content-Type":      {"application/json"},
   340  					"Cache-Control":     {"must-revalidate"},
   341  				},
   342  				Body: Body(`invalid json`),
   343  			}, nil),
   344  			wantStatus: http.StatusBadGateway,
   345  			wantErr:    `invalid character 'i' looking for beginning of value`,
   346  		},
   347  		{
   348  			name: "wrong opening JSON token",
   349  			client: newTestClient(&http.Response{
   350  				StatusCode: 200,
   351  				Header: http.Header{
   352  					"Transfer-Encoding": {"chunked"},
   353  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   354  					"Date":              {"Fri, 27 Oct 2017 19:55:43 GMT"},
   355  					"Content-Type":      {"application/json"},
   356  					"Cache-Control":     {"must-revalidate"},
   357  				},
   358  				Body: Body(`[]`),
   359  			}, nil),
   360  			wantStatus: http.StatusBadGateway,
   361  			wantErr:    "expected `{`",
   362  		},
   363  		{
   364  			name: "wrong second JSON token type",
   365  			client: newTestClient(&http.Response{
   366  				StatusCode: 200,
   367  				Header: http.Header{
   368  					"Transfer-Encoding": {"chunked"},
   369  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   370  					"Date":              {"Fri, 27 Oct 2017 19:55:43 GMT"},
   371  					"Content-Type":      {"application/json"},
   372  					"Cache-Control":     {"must-revalidate"},
   373  				},
   374  				Body: Body(`{"foo":"bar"}`),
   375  			}, nil),
   376  			wantStatus: http.StatusBadGateway,
   377  			wantErr:    "expected `db_name` or `results`",
   378  		},
   379  		{
   380  			name: "CouchDB defaults",
   381  			client: newTestClient(&http.Response{
   382  				StatusCode: 200,
   383  				Header: http.Header{
   384  					"Transfer-Encoding": {"chunked"},
   385  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   386  					"Date":              {"Fri, 27 Oct 2017 19:55:43 GMT"},
   387  					"Content-Type":      {"application/json"},
   388  					"Cache-Control":     {"must-revalidate"},
   389  				},
   390  				Body: Body(`{
   391  					"results":[
   392  						{"db_name":"mailbox","type":"created","seq":"1-g1AAAAFR"},
   393  						{"db_name":"mailbox","type":"deleted","seq":"2-g1AAAAFR"}
   394  					],
   395  					"last_seq": "2-g1AAAAFR"
   396  				}`),
   397  			}, nil),
   398  			options: kivik.Params(map[string]interface{}{
   399  				"feed":  "",
   400  				"since": "",
   401  			}),
   402  			want: []driver.DBUpdate{
   403  				{DBName: "mailbox", Type: "created", Seq: "1-g1AAAAFR"},
   404  				{DBName: "mailbox", Type: "deleted", Seq: "2-g1AAAAFR"},
   405  			},
   406  		},
   407  		{
   408  			name: "eventsource",
   409  			options: kivik.Params(map[string]interface{}{
   410  				"feed":  "eventsource",
   411  				"since": "",
   412  			}),
   413  			wantStatus: http.StatusBadRequest,
   414  			wantErr:    "eventsource feed type not supported",
   415  		},
   416  		{
   417  			// Based on CI test failures, presumably from a race condition that
   418  			// causes the query to happen before any database is created.
   419  			name: "no databases",
   420  			client: newTestClient(&http.Response{
   421  				StatusCode: 200,
   422  				Header: http.Header{
   423  					"Content-Type": {"application/json"},
   424  				},
   425  				Body: Body(`{"last_seq":"38-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5UTgXKMBuZmFmYWFgjq4Yh_Y8FiDJ0ACk_qOYYpyanGiQYoquJwsAM_UqgA"}`),
   426  			}, nil),
   427  		},
   428  	}
   429  	for _, tt := range tests {
   430  		t.Run(tt.name, func(t *testing.T) {
   431  			opts := tt.options
   432  			if opts == nil {
   433  				opts = mock.NilOption
   434  			}
   435  			result, err := tt.client.DBUpdates(context.TODO(), opts)
   436  			if d := internal.StatusErrorDiffRE(tt.wantErr, tt.wantStatus, err); d != "" {
   437  				t.Error(d)
   438  			}
   439  			if err != nil {
   440  				return
   441  			}
   442  
   443  			var got []driver.DBUpdate
   444  			for {
   445  				var update driver.DBUpdate
   446  				err := result.Next(&update)
   447  				if err == io.EOF {
   448  					break
   449  				}
   450  				if err != nil {
   451  					t.Fatal(err)
   452  				}
   453  				got = append(got, update)
   454  			}
   455  			if d := cmp.Diff(tt.want, got); d != "" {
   456  				t.Errorf("Unexpected result:\n%s\n", d)
   457  			}
   458  		})
   459  	}
   460  }
   461  
   462  func newTestUpdates(t *testing.T, body io.ReadCloser) driver.DBUpdates {
   463  	t.Helper()
   464  	u, err := newUpdates(context.Background(), body)
   465  	if err != nil {
   466  		t.Fatal(err)
   467  	}
   468  	return u
   469  }
   470  
   471  func TestUpdatesNext(t *testing.T) {
   472  	t.Parallel()
   473  	tests := []struct {
   474  		name     string
   475  		updates  driver.DBUpdates
   476  		status   int
   477  		err      string
   478  		expected *driver.DBUpdate
   479  	}{
   480  		{
   481  			name:     "consumed feed",
   482  			updates:  newContinuousUpdates(context.TODO(), Body("")),
   483  			expected: &driver.DBUpdate{},
   484  			status:   http.StatusInternalServerError,
   485  			err:      "EOF",
   486  		},
   487  		{
   488  			name:    "read feed",
   489  			updates: newTestUpdates(t, Body(`{"db_name":"mailbox","type":"created","seq":"1-g1AAAAFReJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjMlMiTJ____PyuDOZExFyjAnmJhkWaeaIquGIf2JAUgmWQPMiGRAZcaB5CaePxqEkBq6vGqyWMBkgwNQAqobD4h"},`)),
   490  			expected: &driver.DBUpdate{
   491  				DBName: "mailbox",
   492  				Type:   "created",
   493  				Seq:    "1-g1AAAAFReJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjMlMiTJ____PyuDOZExFyjAnmJhkWaeaIquGIf2JAUgmWQPMiGRAZcaB5CaePxqEkBq6vGqyWMBkgwNQAqobD4h",
   494  			},
   495  		},
   496  	}
   497  	for _, test := range tests {
   498  		t.Run(test.name, func(t *testing.T) {
   499  			result := new(driver.DBUpdate)
   500  			err := test.updates.Next(result)
   501  			if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
   502  				t.Error(d)
   503  			}
   504  			if d := testy.DiffInterface(test.expected, result); d != nil {
   505  				t.Error(d)
   506  			}
   507  		})
   508  	}
   509  }
   510  
   511  func TestUpdatesClose(t *testing.T) {
   512  	t.Parallel()
   513  	body := &closeTracker{ReadCloser: Body("")}
   514  	u := newContinuousUpdates(context.TODO(), body)
   515  	if err := u.Close(); err != nil {
   516  		t.Fatal(err)
   517  	}
   518  	if !body.closed {
   519  		t.Errorf("Failed to close")
   520  	}
   521  }
   522  
   523  func TestUpdatesLastSeq(t *testing.T) {
   524  	t.Parallel()
   525  
   526  	client := newTestClient(&http.Response{
   527  		StatusCode: 200,
   528  		Header: http.Header{
   529  			"Transfer-Encoding": {"chunked"},
   530  			"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   531  			"Date":              {"Fri, 27 Oct 2017 19:55:43 GMT"},
   532  			"Content-Type":      {"application/json"},
   533  			"Cache-Control":     {"must-revalidate"},
   534  		},
   535  		Body: Body(`{"results":[],"last_seq":"99-asdf"}`),
   536  	}, nil)
   537  
   538  	updates, err := client.DBUpdates(context.TODO(), mock.NilOption)
   539  	if err != nil {
   540  		t.Fatal(err)
   541  	}
   542  	for {
   543  		err := updates.Next(&driver.DBUpdate{})
   544  		if err == io.EOF {
   545  			break
   546  		}
   547  		if err != nil {
   548  			t.Fatal(err)
   549  		}
   550  
   551  	}
   552  	want := "99-asdf"
   553  	got, err := updates.(driver.LastSeqer).LastSeq()
   554  	if err != nil {
   555  		t.Fatal(err)
   556  	}
   557  	if got != want {
   558  		t.Errorf("Unexpected last_seq: %s", got)
   559  	}
   560  }
   561  
   562  func TestPing(t *testing.T) {
   563  	type pingTest struct {
   564  		name     string
   565  		client   *client
   566  		expected bool
   567  		status   int
   568  		err      string
   569  	}
   570  
   571  	tests := []pingTest{
   572  		{
   573  			name: "Couch 1.6",
   574  			client: newTestClient(&http.Response{
   575  				StatusCode: http.StatusBadRequest,
   576  				ProtoMajor: 1,
   577  				ProtoMinor: 1,
   578  				Header: http.Header{
   579  					"Server": []string{"CouchDB/1.6.1 (Erlang OTP/17)"},
   580  				},
   581  			}, nil),
   582  			expected: true,
   583  		},
   584  		{
   585  			name: "Couch 2.x offline",
   586  			client: newTestClient(&http.Response{
   587  				StatusCode: http.StatusNotFound,
   588  				ProtoMajor: 1,
   589  				ProtoMinor: 1,
   590  			}, nil),
   591  			expected: false,
   592  		},
   593  		{
   594  			name: "Couch 2.x online",
   595  			client: newTestClient(&http.Response{
   596  				StatusCode: http.StatusOK,
   597  				ProtoMajor: 1,
   598  				ProtoMinor: 1,
   599  			}, nil),
   600  			expected: true,
   601  		},
   602  		{
   603  			name:     "network error",
   604  			client:   newTestClient(nil, errors.New("network error")),
   605  			expected: false,
   606  			status:   http.StatusBadGateway,
   607  			err:      `Head "?http://example.com/_up"?: network error`,
   608  		},
   609  	}
   610  
   611  	for _, test := range tests {
   612  		t.Run(test.name, func(t *testing.T) {
   613  			result, err := test.client.Ping(context.Background())
   614  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   615  				t.Error(d)
   616  			}
   617  			if result != test.expected {
   618  				t.Errorf("Unexpected result: %t", result)
   619  			}
   620  		})
   621  	}
   622  }
   623  

View as plain text