...

Source file src/github.com/go-kivik/kivik/v4/couchdb/find_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  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"strings"
    23  	"testing"
    24  
    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 TestExplain(t *testing.T) {
    34  	tests := []struct {
    35  		name     string
    36  		db       *db
    37  		query    interface{}
    38  		options  kivik.Option
    39  		expected *driver.QueryPlan
    40  		status   int
    41  		err      string
    42  	}{
    43  		{
    44  			name:   "invalid query",
    45  			db:     newTestDB(nil, nil),
    46  			query:  make(chan int),
    47  			status: http.StatusBadRequest,
    48  			err:    `Post "?http://example.com/testdb/_explain"?: json: unsupported type: chan int`,
    49  		},
    50  		{
    51  			name:   "network error",
    52  			db:     newTestDB(nil, errors.New("net error")),
    53  			status: http.StatusBadGateway,
    54  			err:    `Post "?http://example.com/testdb/_explain"?: net error`,
    55  		},
    56  		{
    57  			name: "error response",
    58  			db: newTestDB(&http.Response{
    59  				StatusCode: http.StatusNotFound,
    60  				Body:       io.NopCloser(strings.NewReader("")),
    61  			}, nil),
    62  			status: http.StatusNotFound,
    63  			err:    "Not Found",
    64  		},
    65  		{
    66  			name: "success",
    67  			db: newTestDB(&http.Response{
    68  				StatusCode: http.StatusOK,
    69  				Body:       io.NopCloser(strings.NewReader(`{"dbname":"foo"}`)),
    70  			}, nil),
    71  			expected: &driver.QueryPlan{DBName: "foo"},
    72  		},
    73  		{
    74  			name: "raw query",
    75  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
    76  				defer req.Body.Close() // nolint: errcheck
    77  				var result interface{}
    78  				if err := json.NewDecoder(req.Body).Decode(&result); err != nil {
    79  					return nil, fmt.Errorf("decode error: %s", err)
    80  				}
    81  				expected := map[string]interface{}{"_id": "foo"}
    82  				if d := testy.DiffInterface(expected, result); d != nil {
    83  					return nil, fmt.Errorf("unexpected result:\n%s", d)
    84  				}
    85  				return nil, errors.New("success")
    86  			}),
    87  			query:  []byte(`{"_id":"foo"}`),
    88  			status: http.StatusBadGateway,
    89  			err:    `Post "?http://example.com/testdb/_explain"?: success`,
    90  		},
    91  		{
    92  			name:    "partitioned request",
    93  			db:      newTestDB(nil, errors.New("expected")),
    94  			options: OptionPartition("x1"),
    95  			status:  http.StatusBadGateway,
    96  			err:     `Post "?http://example.com/testdb/_partition/x1/_explain"?: expected`,
    97  		},
    98  	}
    99  	for _, test := range tests {
   100  		t.Run(test.name, func(t *testing.T) {
   101  			opts := test.options
   102  			if opts == nil {
   103  				opts = mock.NilOption
   104  			}
   105  			result, err := test.db.Explain(context.Background(), test.query, opts)
   106  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   107  				t.Error(d)
   108  			}
   109  			if d := testy.DiffInterface(test.expected, result); d != nil {
   110  				t.Error(d)
   111  			}
   112  		})
   113  	}
   114  }
   115  
   116  func TestUnmarshalQueryPlan(t *testing.T) {
   117  	tests := []struct {
   118  		name     string
   119  		input    string
   120  		expected *queryPlan
   121  		err      string
   122  	}{
   123  		{
   124  			name:  "non-array",
   125  			input: `{"fields":{}}`,
   126  			err:   "json: cannot unmarshal object into Go",
   127  		},
   128  		{
   129  			name:     "all_fields",
   130  			input:    `{"fields":"all_fields","dbname":"foo"}`,
   131  			expected: &queryPlan{DBName: "foo"},
   132  		},
   133  		{
   134  			name:     "simple field list",
   135  			input:    `{"fields":["foo","bar"],"dbname":"foo"}`,
   136  			expected: &queryPlan{Fields: []interface{}{"foo", "bar"}, DBName: "foo"},
   137  		},
   138  		{
   139  			name:  "complex field list",
   140  			input: `{"dbname":"foo", "fields":[{"foo":"asc"},{"bar":"desc"}]}`,
   141  			expected: &queryPlan{
   142  				DBName: "foo",
   143  				Fields: []interface{}{
   144  					map[string]interface{}{"foo": "asc"},
   145  					map[string]interface{}{"bar": "desc"},
   146  				},
   147  			},
   148  		},
   149  		{
   150  			name:  "invalid bare string",
   151  			input: `{"fields":"not_all_fields"}`,
   152  			err:   "json: cannot unmarshal string into Go",
   153  		},
   154  	}
   155  	for _, test := range tests {
   156  		t.Run(test.name, func(t *testing.T) {
   157  			result := new(queryPlan)
   158  			err := json.Unmarshal([]byte(test.input), &result)
   159  			if !testy.ErrorMatchesRE(test.err, err) {
   160  				t.Errorf("Unexpected error: %s", err)
   161  			}
   162  			if err != nil {
   163  				return
   164  			}
   165  			if d := testy.DiffInterface(test.expected, result); d != nil {
   166  				t.Error(d)
   167  			}
   168  		})
   169  	}
   170  }
   171  
   172  func TestCreateIndex(t *testing.T) {
   173  	tests := []struct {
   174  		name            string
   175  		ddoc, indexName string
   176  		index           interface{}
   177  		options         kivik.Option
   178  		db              *db
   179  		status          int
   180  		err             string
   181  	}{
   182  		{
   183  			name:   "invalid JSON index",
   184  			db:     newTestDB(nil, nil),
   185  			index:  `invalid json`,
   186  			status: http.StatusBadRequest,
   187  			err:    "invalid character 'i' looking for beginning of value",
   188  		},
   189  		{
   190  			name:   "invalid raw index",
   191  			db:     newTestDB(nil, nil),
   192  			index:  map[string]interface{}{"foo": make(chan int)},
   193  			status: http.StatusBadRequest,
   194  			err:    `Post "?http://example.com/testdb/_index"?: json: unsupported type: chan int`,
   195  		},
   196  		{
   197  			name:   "network error",
   198  			db:     newTestDB(nil, errors.New("net error")),
   199  			status: http.StatusBadGateway,
   200  			err:    `Post "?http://example.com/testdb/_index"?: net error`,
   201  		},
   202  		{
   203  			name: "success 2.1.0",
   204  			db: newTestDB(&http.Response{
   205  				StatusCode: 200,
   206  				Header: http.Header{
   207  					"X-CouchDB-Body-Time": {"0"},
   208  					"X-Couch-Request-ID":  {"8e4aef0c2f"},
   209  					"Server":              {"CouchDB/2.1.0 (Erlang OTP/17)"},
   210  					"Date":                {"Fri, 27 Oct 2017 18:14:38 GMT"},
   211  					"Content-Type":        {"application/json"},
   212  					"Content-Length":      {"126"},
   213  					"Cache-Control":       {"must-revalidate"},
   214  				},
   215  				Body: Body(`{"result":"created","id":"_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea","name":"a7ee061f1a2c0c6882258b2f1e148b714e79ccea"}`),
   216  			}, nil),
   217  		},
   218  		{
   219  			name:    "partitioned query",
   220  			db:      newTestDB(nil, errors.New("expected")),
   221  			options: OptionPartition("xxy"),
   222  			status:  http.StatusBadGateway,
   223  			err:     `Post "?http://example.com/testdb/_partition/xxy/_index"?: expected`,
   224  		},
   225  	}
   226  	for _, test := range tests {
   227  		t.Run(test.name, func(t *testing.T) {
   228  			opts := test.options
   229  			if opts == nil {
   230  				opts = mock.NilOption
   231  			}
   232  			err := test.db.CreateIndex(context.Background(), test.ddoc, test.indexName, test.index, opts)
   233  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   234  				t.Error(d)
   235  			}
   236  		})
   237  	}
   238  }
   239  
   240  func TestGetIndexes(t *testing.T) {
   241  	tests := []struct {
   242  		name     string
   243  		options  kivik.Option
   244  		db       *db
   245  		expected []driver.Index
   246  		status   int
   247  		err      string
   248  	}{
   249  		{
   250  			name:   "network error",
   251  			db:     newTestDB(nil, errors.New("net error")),
   252  			status: http.StatusBadGateway,
   253  			err:    `Get "?http://example.com/testdb/_index"?: net error`,
   254  		},
   255  		{
   256  			name: "2.1.0",
   257  			db: newTestDB(&http.Response{
   258  				StatusCode: 200,
   259  				Header: http.Header{
   260  					"X-CouchDB-Body-Time": {"0"},
   261  					"X-Couch-Request-ID":  {"f44881735c"},
   262  					"Server":              {"CouchDB/2.1.0 (Erlang OTP/17)"},
   263  					"Date":                {"Fri, 27 Oct 2017 18:23:29 GMT"},
   264  					"Content-Type":        {"application/json"},
   265  					"Content-Length":      {"269"},
   266  					"Cache-Control":       {"must-revalidate"},
   267  				},
   268  				Body: Body(`{"total_rows":2,"indexes":[{"ddoc":null,"name":"_all_docs","type":"special","def":{"fields":[{"_id":"asc"}]}},{"ddoc":"_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea","name":"a7ee061f1a2c0c6882258b2f1e148b714e79ccea","type":"json","def":{"fields":[{"foo":"asc"}]}}]}`),
   269  			}, nil),
   270  			expected: []driver.Index{
   271  				{
   272  					Name: "_all_docs",
   273  					Type: "special",
   274  					Definition: map[string]interface{}{
   275  						"fields": []interface{}{
   276  							map[string]interface{}{"_id": "asc"},
   277  						},
   278  					},
   279  				},
   280  				{
   281  					DesignDoc: "_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
   282  					Name:      "a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
   283  					Type:      "json",
   284  					Definition: map[string]interface{}{
   285  						"fields": []interface{}{
   286  							map[string]interface{}{"foo": "asc"},
   287  						},
   288  					},
   289  				},
   290  			},
   291  		},
   292  		{
   293  			name:    "partitioned query",
   294  			db:      newTestDB(nil, errors.New("expected")),
   295  			options: OptionPartition("yyz"),
   296  			status:  http.StatusBadGateway,
   297  			err:     `Get "?http://example.com/testdb/_partition/yyz/_index"?: expected`,
   298  		},
   299  	}
   300  	for _, test := range tests {
   301  		t.Run(test.name, func(t *testing.T) {
   302  			opts := test.options
   303  			if opts == nil {
   304  				opts = mock.NilOption
   305  			}
   306  			result, err := test.db.GetIndexes(context.Background(), opts)
   307  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   308  				t.Error(d)
   309  			}
   310  			if d := testy.DiffInterface(test.expected, result); d != nil {
   311  				t.Error(d)
   312  			}
   313  		})
   314  	}
   315  }
   316  
   317  func TestDeleteIndex(t *testing.T) {
   318  	tests := []struct {
   319  		name            string
   320  		ddoc, indexName string
   321  		options         kivik.Option
   322  		db              *db
   323  		status          int
   324  		err             string
   325  	}{
   326  		{
   327  			name:   "no ddoc",
   328  			status: http.StatusBadRequest,
   329  			db:     newTestDB(nil, nil),
   330  			err:    "kivik: ddoc required",
   331  		},
   332  		{
   333  			name:   "no index name",
   334  			ddoc:   "foo",
   335  			status: http.StatusBadRequest,
   336  			db:     newTestDB(nil, nil),
   337  			err:    "kivik: name required",
   338  		},
   339  		{
   340  			name:      "network error",
   341  			ddoc:      "foo",
   342  			indexName: "bar",
   343  			db:        newTestDB(nil, errors.New("net error")),
   344  			status:    http.StatusBadGateway,
   345  			err:       `^(Delete "?http://example.com/testdb/_index/foo/json/bar"?: )?net error`,
   346  		},
   347  		{
   348  			name:      "2.1.0 success",
   349  			ddoc:      "_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
   350  			indexName: "a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
   351  			db: newTestDB(&http.Response{
   352  				StatusCode: 200,
   353  				Header: http.Header{
   354  					"X-CouchDB-Body-Time": {"0"},
   355  					"X-Couch-Request-ID":  {"6018a0a693"},
   356  					"Server":              {"CouchDB/2.1.0 (Erlang OTP/17)"},
   357  					"Date":                {"Fri, 27 Oct 2017 19:06:28 GMT"},
   358  					"Content-Type":        {"application/json"},
   359  					"Content-Length":      {"11"},
   360  					"Cache-Control":       {"must-revalidate"},
   361  				},
   362  				Body: Body(`{"ok":true}`),
   363  			}, nil),
   364  		},
   365  		{
   366  			name:      "partitioned query",
   367  			ddoc:      "_design/foo",
   368  			indexName: "bar",
   369  			db:        newTestDB(nil, errors.New("expected")),
   370  			options:   OptionPartition("qqz"),
   371  			status:    http.StatusBadGateway,
   372  			err:       `Delete "?http://example.com/testdb/_partition/qqz/_index/_design/foo/json/bar"?: expected`,
   373  		},
   374  	}
   375  	for _, test := range tests {
   376  		t.Run(test.name, func(t *testing.T) {
   377  			opts := test.options
   378  			if opts == nil {
   379  				opts = mock.NilOption
   380  			}
   381  			err := test.db.DeleteIndex(context.Background(), test.ddoc, test.indexName, opts)
   382  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   383  				t.Error(d)
   384  			}
   385  		})
   386  	}
   387  }
   388  
   389  func TestFind(t *testing.T) {
   390  	tests := []struct {
   391  		name    string
   392  		db      *db
   393  		query   interface{}
   394  		options kivik.Option
   395  		status  int
   396  		err     string
   397  	}{
   398  		{
   399  			name:   "invalid query json",
   400  			db:     newTestDB(nil, nil),
   401  			query:  make(chan int),
   402  			status: http.StatusBadRequest,
   403  			err:    `Post "?http://example.com/testdb/_find"?: json: unsupported type: chan int`,
   404  		},
   405  		{
   406  			name:   "network error",
   407  			db:     newTestDB(nil, errors.New("net error")),
   408  			status: http.StatusBadGateway,
   409  			err:    `Post "?http://example.com/testdb/_find"?: net error`,
   410  		},
   411  		{
   412  			name: "error response",
   413  			db: newTestDB(&http.Response{
   414  				StatusCode: 415,
   415  				Header: http.Header{
   416  					"Content-Type":        {"application/json"},
   417  					"X-CouchDB-Body-Time": {"0"},
   418  					"X-Couch-Request-ID":  {"aa1f852b27"},
   419  					"Server":              {"CouchDB/2.1.0 (Erlang OTP/17)"},
   420  					"Date":                {"Fri, 27 Oct 2017 19:20:04 GMT"},
   421  					"Content-Length":      {"77"},
   422  					"Cache-Control":       {"must-revalidate"},
   423  				},
   424  				ContentLength: 77,
   425  				Body:          Body(`{"error":"bad_content_type","reason":"Content-Type must be application/json"}`),
   426  			}, nil),
   427  			status: http.StatusUnsupportedMediaType,
   428  			err:    "Unsupported Media Type: Content-Type must be application/json",
   429  		},
   430  		{
   431  			name: "success 2.1.0",
   432  			query: map[string]interface{}{
   433  				"selector": map[string]string{"_id": "foo"},
   434  			},
   435  			db: newTestDB(&http.Response{
   436  				StatusCode: 200,
   437  				Header: http.Header{
   438  					"Content-Type":        {"application/json"},
   439  					"X-CouchDB-Body-Time": {"0"},
   440  					"X-Couch-Request-ID":  {"a0884508d8"},
   441  					"Server":              {"CouchDB/2.1.0 (Erlang OTP/17)"},
   442  					"Date":                {"Fri, 27 Oct 2017 19:20:04 GMT"},
   443  					"Transfer-Encoding":   {"chunked"},
   444  					"Cache-Control":       {"must-revalidate"},
   445  				},
   446  				Body: Body(`{"docs":[
   447  {"_id":"foo","_rev":"2-f5d2de1376388f1b54d93654df9dc9c7","_attachments":{"foo.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-ENGoH7oK8V9R3BMnfDHZmw==","length":13,"stub":true}}}
   448  ]}`),
   449  			}, nil),
   450  		},
   451  		{
   452  			name:    "partitioned request",
   453  			db:      newTestDB(nil, errors.New("expected")),
   454  			options: OptionPartition("x2"),
   455  			status:  http.StatusBadGateway,
   456  			err:     `Post "?http://example.com/testdb/_partition/x2/_find"?: expected`,
   457  		},
   458  	}
   459  	for _, test := range tests {
   460  		t.Run(test.name, func(t *testing.T) {
   461  			opts := test.options
   462  			if opts == nil {
   463  				opts = mock.NilOption
   464  			}
   465  			result, err := test.db.Find(context.Background(), test.query, opts)
   466  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   467  				t.Error(d)
   468  			}
   469  			if err != nil {
   470  				return
   471  			}
   472  			if _, ok := result.(*rows); !ok {
   473  				t.Errorf("Unexpected type returned: %t", result)
   474  			}
   475  		})
   476  	}
   477  }
   478  

View as plain text