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