github.com/go-kivik/kivik/v4@v4.3.2/couchdb/find_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 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 }