github.com/go-kivik/kivik/v4@v4.3.2/couchdb/db_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 "bytes" 17 "context" 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io" 22 "mime" 23 "mime/multipart" 24 "net/http" 25 "net/url" 26 "os" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/google/go-cmp/cmp" 32 "gitlab.com/flimzy/testy" 33 34 kivik "github.com/go-kivik/kivik/v4" 35 "github.com/go-kivik/kivik/v4/couchdb/chttp" 36 "github.com/go-kivik/kivik/v4/driver" 37 internal "github.com/go-kivik/kivik/v4/int/errors" 38 "github.com/go-kivik/kivik/v4/int/mock" 39 ) 40 41 func TestAllDocs(t *testing.T) { 42 t.Run("standard", func(t *testing.T) { 43 db := newTestDB(nil, errors.New("test error")) 44 _, err := db.AllDocs(context.Background(), mock.NilOption) 45 if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_all_docs"?: test error`, err) { 46 t.Errorf("Unexpected error: %s", err) 47 } 48 }) 49 50 t.Run("partitioned", func(t *testing.T) { 51 db := newTestDB(nil, errors.New("test error")) 52 _, err := db.AllDocs(context.Background(), OptionPartition("a1")) 53 if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_partition/a1/_all_docs"?: test error`, err) { 54 t.Errorf("Unexpected error: %s", err) 55 } 56 }) 57 } 58 59 func TestDesignDocs(t *testing.T) { 60 db := newTestDB(nil, errors.New("test error")) 61 _, err := db.DesignDocs(context.Background(), mock.NilOption) 62 if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_design_docs"?: test error`, err) { 63 t.Errorf("Unexpected error: %s", err) 64 } 65 } 66 67 func TestLocalDocs(t *testing.T) { 68 db := newTestDB(nil, errors.New("test error")) 69 _, err := db.LocalDocs(context.Background(), mock.NilOption) 70 if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_local_docs"?: test error`, err) { 71 t.Errorf("Unexpected error: %s", err) 72 } 73 } 74 75 func TestQuery(t *testing.T) { 76 t.Run("standard", func(t *testing.T) { 77 db := newTestDB(nil, errors.New("test error")) 78 _, err := db.Query(context.Background(), "ddoc", "view", mock.NilOption) 79 if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_design/ddoc/_view/view"?: test error`, err) { 80 t.Errorf("Unexpected error: %s", err) 81 } 82 }) 83 t.Run("partitioned", func(t *testing.T) { 84 db := newTestDB(nil, errors.New("test error")) 85 _, err := db.Query(context.Background(), "ddoc", "view", OptionPartition("a2")) 86 if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_partition/a2/_design/ddoc/_view/view"?: test error`, err) { 87 t.Errorf("Unexpected error: %s", err) 88 } 89 }) 90 } 91 92 type Attachment struct { 93 Filename string 94 ContentType string 95 Size int64 96 Content string 97 } 98 99 func TestGet(t *testing.T) { 100 type tt struct { 101 db *db 102 id string 103 options kivik.Option 104 doc *driver.Document 105 expected string 106 attachments []*Attachment 107 status int 108 err string 109 } 110 111 tests := testy.NewTable() 112 tests.Add("missing doc ID", tt{ 113 status: http.StatusBadRequest, 114 err: "kivik: docID required", 115 }) 116 tests.Add("invalid options", tt{ 117 id: "foo", 118 options: kivik.Param("foo", make(chan int)), 119 status: http.StatusBadRequest, 120 err: "kivik: invalid type chan int for options", 121 }) 122 tests.Add("network failure", tt{ 123 id: "foo", 124 db: newTestDB(nil, errors.New("net error")), 125 status: http.StatusBadGateway, 126 err: `Get "?http://example.com/testdb/foo"?: net error`, 127 }) 128 tests.Add("error response", tt{ 129 id: "foo", 130 db: newTestDB(&http.Response{ 131 StatusCode: http.StatusBadRequest, 132 Body: Body(""), 133 }, nil), 134 status: http.StatusBadRequest, 135 err: "Bad Request", 136 }) 137 tests.Add("status OK", tt{ 138 id: "foo", 139 db: newTestDB(&http.Response{ 140 StatusCode: http.StatusOK, 141 Header: http.Header{ 142 "Content-Type": {typeJSON}, 143 "ETag": {`"12-xxx"`}, 144 }, 145 ContentLength: 13, 146 Body: Body(`{"foo":"bar"}`), 147 }, nil), 148 doc: &driver.Document{ 149 Rev: "12-xxx", 150 }, 151 expected: `{"foo":"bar"}`, 152 }) 153 tests.Add("If-None-Match", tt{ 154 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 155 if err := consume(req.Body); err != nil { 156 return nil, err 157 } 158 if inm := req.Header.Get("If-None-Match"); inm != `"foo"` { 159 return nil, fmt.Errorf(`If-None-Match: %s != "foo"`, inm) 160 } 161 return nil, errors.New("success") 162 }), 163 id: "foo", 164 options: OptionIfNoneMatch("foo"), 165 status: http.StatusBadGateway, 166 err: `Get "?http://example.com/testdb/foo"?: success`, 167 }) 168 tests.Add("invalid content type in response", tt{ 169 id: "foo", 170 db: newTestDB(&http.Response{ 171 StatusCode: http.StatusOK, 172 Header: http.Header{ 173 "Content-Type": {"image/jpeg"}, 174 "ETag": {`"12-xxx"`}, 175 }, 176 ContentLength: 13, 177 Body: Body("some response"), 178 }, nil), 179 status: http.StatusBadGateway, 180 err: "kivik: invalid content type in response: image/jpeg", 181 }) 182 tests.Add("invalid content type header", tt{ 183 id: "foo", 184 db: newTestDB(&http.Response{ 185 StatusCode: http.StatusOK, 186 Header: http.Header{ 187 "Content-Type": {"cow; =moo"}, 188 "ETag": {`"12-xxx"`}, 189 }, 190 ContentLength: 13, 191 Body: Body("some response"), 192 }, nil), 193 status: http.StatusBadGateway, 194 err: "mime: invalid media parameter", 195 }) 196 tests.Add("missing multipart boundary", tt{ 197 db: newTestDB(&http.Response{ 198 StatusCode: http.StatusOK, 199 Header: http.Header{ 200 "Content-Type": {typeMPRelated}, 201 "ETag": {`"12-xxx"`}, 202 }, 203 ContentLength: 13, 204 Body: Body("some response"), 205 }, nil), 206 id: "foo", 207 status: http.StatusBadGateway, 208 err: "kivik: boundary missing for multipart/related response", 209 }) 210 tests.Add("no multipart data", tt{ 211 db: newTestDB(&http.Response{ 212 StatusCode: http.StatusOK, 213 Header: http.Header{ 214 "Content-Length": {"538"}, 215 "Content-Type": {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`}, 216 "Date": {"Sat, 28 Sep 2013 08:08:22 GMT"}, 217 "ETag": {`"2-c1c6c44c4bc3c9344b037c8690468605"`}, 218 "ServeR": {"CouchDB (Erlang OTP)"}, 219 }, 220 ContentLength: 538, 221 Body: Body(`bogus data`), 222 }, nil), 223 id: "foo", 224 options: kivik.IncludeDocs(), 225 status: http.StatusBadGateway, 226 err: "multipart: NextPart: EOF", 227 }) 228 tests.Add("incomplete multipart data", tt{ 229 db: newTestDB(&http.Response{ 230 StatusCode: http.StatusOK, 231 Header: http.Header{ 232 "Content-Length": {"538"}, 233 "Content-Type": {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`}, 234 "Date": {"Sat, 28 Sep 2013 08:08:22 GMT"}, 235 "ETag": {`"2-c1c6c44c4bc3c9344b037c8690468605"`}, 236 "ServeR": {"CouchDB (Erlang OTP)"}, 237 }, 238 ContentLength: 538, 239 Body: Body(`--e89b3e29388aef23453450d10e5aaed0 240 bogus data`), 241 }, nil), 242 id: "foo", 243 options: kivik.IncludeDocs(), 244 status: http.StatusBadGateway, 245 err: "malformed MIME header (initial )?line:.*bogus data", 246 }) 247 tests.Add("multipart accept header", tt{ 248 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 249 expected := "multipart/mixed, multipart/related, application/json" 250 if accept := r.Header.Get("Accept"); accept != expected { 251 return nil, fmt.Errorf("Unexpected Accept header: %s", accept) 252 } 253 return nil, errors.New("not an error") 254 }), 255 id: "foo", 256 status: http.StatusBadGateway, 257 err: "not an error", 258 }) 259 tests.Add("disable multipart accept header", tt{ 260 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 261 expected := "application/json" 262 if accept := r.Header.Get("Accept"); accept != expected { 263 return nil, fmt.Errorf("Unexpected Accept header: %s", accept) 264 } 265 return nil, errors.New("not an error") 266 }), 267 options: OptionNoMultipartGet(), 268 id: "foo", 269 status: http.StatusBadGateway, 270 err: "not an error", 271 }) 272 tests.Add("multipart attachments", tt{ 273 // response borrowed from http://docs.couchdb.org/en/2.1.1/api/document/common.html#efficient-multiple-attachments-retrieving 274 db: newTestDB(&http.Response{ 275 StatusCode: http.StatusOK, 276 Header: http.Header{ 277 "Content-Length": {"538"}, 278 "Content-Type": {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`}, 279 "Date": {"Sat, 28 Sep 2013 08:08:22 GMT"}, 280 "ETag": {`"2-c1c6c44c4bc3c9344b037c8690468605"`}, 281 "ServeR": {"CouchDB (Erlang OTP)"}, 282 }, 283 ContentLength: 538, 284 Body: Body(`--e89b3e29388aef23453450d10e5aaed0 285 Content-Type: application/json 286 287 {"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}} 288 --e89b3e29388aef23453450d10e5aaed0 289 Content-Disposition: attachment; filename="recipe.txt" 290 Content-Type: text/plain 291 Content-Length: 86 292 293 1. Take R 294 2. Take E 295 3. Mix with L 296 4. Add some A 297 5. Serve with X 298 299 --e89b3e29388aef23453450d10e5aaed0--`), 300 }, nil), 301 id: "foo", 302 options: kivik.IncludeDocs(), 303 doc: &driver.Document{ 304 Rev: "2-c1c6c44c4bc3c9344b037c8690468605", 305 Attachments: &multipartAttachments{ 306 meta: map[string]attMeta{ 307 "recipe.txt": { 308 Follows: true, 309 ContentType: "text/plain", 310 Size: func() *int64 { x := int64(86); return &x }(), 311 }, 312 }, 313 }, 314 }, 315 expected: `{"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}}`, 316 attachments: []*Attachment{ 317 { 318 Filename: "recipe.txt", 319 Size: 86, 320 ContentType: "text/plain", 321 Content: "1. Take R\n2. Take E\n3. Mix with L\n4. Add some A\n5. Serve with X\n", 322 }, 323 }, 324 }) 325 tests.Add("multipart attachments, doc content length", tt{ 326 // response borrowed from http://docs.couchdb.org/en/2.1.1/api/document/common.html#efficient-multiple-attachments-retrieving 327 db: newTestDB(&http.Response{ 328 StatusCode: http.StatusOK, 329 Header: http.Header{ 330 "Content-Length": {"558"}, 331 "Content-Type": {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`}, 332 "Date": {"Sat, 28 Sep 2013 08:08:22 GMT"}, 333 "ETag": {`"2-c1c6c44c4bc3c9344b037c8690468605"`}, 334 "ServeR": {"CouchDB (Erlang OTP)"}, 335 }, 336 ContentLength: 558, 337 Body: Body(`--e89b3e29388aef23453450d10e5aaed0 338 Content-Type: application/json 339 Content-Length: 199 340 341 {"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}} 342 --e89b3e29388aef23453450d10e5aaed0 343 Content-Disposition: attachment; filename="recipe.txt" 344 Content-Type: text/plain 345 Content-Length: 86 346 347 1. Take R 348 2. Take E 349 3. Mix with L 350 4. Add some A 351 5. Serve with X 352 353 --e89b3e29388aef23453450d10e5aaed0--`), 354 }, nil), 355 id: "foo", 356 options: kivik.IncludeDocs(), 357 doc: &driver.Document{ 358 Rev: "2-c1c6c44c4bc3c9344b037c8690468605", 359 Attachments: &multipartAttachments{ 360 meta: map[string]attMeta{ 361 "recipe.txt": { 362 Follows: true, 363 ContentType: "text/plain", 364 Size: func() *int64 { x := int64(86); return &x }(), 365 }, 366 }, 367 }, 368 }, 369 expected: `{"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}}`, 370 attachments: []*Attachment{ 371 { 372 Filename: "recipe.txt", 373 Size: 86, 374 ContentType: "text/plain", 375 Content: "1. Take R\n2. Take E\n3. Mix with L\n4. Add some A\n5. Serve with X\n", 376 }, 377 }, 378 }) 379 tests.Add("bug268 - complex id", func(*testing.T) interface{} { 380 return tt{ 381 db: newCustomDB(func(*http.Request) (*http.Response, error) { 382 return nil, errors.New("success") 383 }), 384 id: "http://example.com/", 385 status: http.StatusBadGateway, 386 err: `Get "?http://example.com/testdb/http%3A%2F%2Fexample\.com%2F"?: success`, 387 } 388 }) 389 tests.Add("plus sign", func(*testing.T) interface{} { 390 return tt{ 391 db: newCustomDB(func(*http.Request) (*http.Response, error) { 392 return nil, errors.New("success") 393 }), 394 id: "2020-01-30T13:33:00.00+05:30|kl", 395 status: http.StatusBadGateway, 396 err: `^Get "?http://example.com/testdb/2020-01-30T13%3A33%3A00\.00%2B05%3A30%7Ckl"?: success$`, 397 } 398 }) 399 400 tests.Run(t, func(t *testing.T, tt tt) { 401 opts := tt.options 402 if opts == nil { 403 opts = mock.NilOption 404 } 405 result, err := tt.db.Get(context.Background(), tt.id, opts) 406 if !testy.ErrorMatchesRE(tt.err, err) { 407 t.Errorf("Unexpected error: \n Got: %s\nWant: /%s/", err, tt.err) 408 } 409 if err != nil { 410 return 411 } 412 413 if d := testy.DiffAsJSON([]byte(tt.expected), result.Body); d != nil { 414 t.Errorf("Unexpected result: %s", d) 415 } 416 attachments := rowAttachments(t, result.Attachments) 417 418 _ = result.Body.Close() 419 result.Body = nil // Determinism 420 if d := testy.DiffInterface(tt.doc, result); d != nil { 421 t.Errorf("Unexpected doc:\n%s", d) 422 } 423 if d := testy.DiffInterface(tt.attachments, attachments); d != nil { 424 t.Errorf("Unexpected attachments:\n%s", d) 425 } 426 }) 427 } 428 429 func rowAttachments(t *testing.T, atts driver.Attachments) []*Attachment { 430 t.Helper() 431 var attachments []*Attachment 432 if atts != nil { 433 att := new(driver.Attachment) 434 for { 435 if err := atts.Next(att); err != nil { 436 if err != io.EOF { 437 t.Fatal(err) 438 } 439 break 440 } 441 content, e := io.ReadAll(att.Content) 442 if e != nil { 443 t.Fatal(e) 444 } 445 attachments = append(attachments, &Attachment{ 446 Filename: att.Filename, 447 ContentType: att.ContentType, 448 Size: att.Size, 449 Content: string(content), 450 }) 451 } 452 atts.(*multipartAttachments).content = nil // Determinism 453 atts.(*multipartAttachments).mpReader = nil 454 } 455 return attachments 456 } 457 458 func TestOpenRevs(t *testing.T) { 459 type rowResult struct { 460 ID string 461 Rev string 462 Error string 463 } 464 type tt struct { 465 db *db 466 id string 467 revs []string 468 options kivik.Option 469 want []rowResult 470 err string 471 } 472 473 tests := testy.NewTable() 474 tests.Add("open_revs", func(*testing.T) interface{} { 475 return tt{ 476 db: newCustomDB(func(*http.Request) (*http.Response, error) { 477 return &http.Response{ 478 StatusCode: http.StatusOK, 479 Header: http.Header{ 480 "Content-Type": []string{`multipart/mixed; boundary="ea68bec945fd9dece3e826462c5604e8"`}, 481 }, 482 Body: Body(`--ea68bec945fd9dece3e826462c5604e8 483 Content-Type: application/json 484 485 {"_id":"bar","_rev":"2-e2a6df12e36615e8def0bb38bb17b48d","foo":123} 486 --ea68bec945fd9dece3e826462c5604e8-- 487 `), 488 }, nil 489 }), 490 id: "bar", 491 want: []rowResult{ 492 { 493 ID: "bar", 494 Rev: "2-e2a6df12e36615e8def0bb38bb17b48d", 495 }, 496 }, 497 } 498 }) 499 tests.Add("open_revs with multiple revs", func(*testing.T) interface{} { 500 return tt{ 501 db: newCustomDB(func(*http.Request) (*http.Response, error) { 502 return &http.Response{ 503 StatusCode: http.StatusOK, 504 Header: http.Header{ 505 "Content-Type": []string{`multipart/mixed; boundary="7b1596fc4940bc1be725ad67f11ec1c4"`}, 506 }, 507 Body: Body(`--7b1596fc4940bc1be725ad67f11ec1c4 508 Content-Type: application/json 509 510 { 511 "_id": "SpaghettiWithMeatballs", 512 "_rev": "1-917fa23", 513 "_revisions": { 514 "ids": [ 515 "917fa23" 516 ], 517 "start": 1 518 }, 519 "description": "An Italian-American delicious dish", 520 "ingredients": [ 521 "spaghetti", 522 "tomato sauce", 523 "meatballs" 524 ], 525 "name": "Spaghetti with meatballs" 526 } 527 --7b1596fc4940bc1be725ad67f11ec1c4 528 Content-Type: multipart/related; boundary="a81a77b0ca68389dda3243a43ca946f2" 529 530 --a81a77b0ca68389dda3243a43ca946f2 531 Content-Type: application/json 532 533 { 534 "_attachments": { 535 "recipe.txt": { 536 "content_type": "text/plain", 537 "digest": "md5-R5CrCb6fX10Y46AqtNn0oQ==", 538 "follows": true, 539 "length": 87, 540 "revpos": 7 541 } 542 }, 543 "_id": "SpaghettiWithMeatballs", 544 "_rev": "7-474f12e", 545 "_revisions": { 546 "ids": [ 547 "474f12e", 548 "5949cfc", 549 "00ecbbc", 550 "fc997b6", 551 "3552c87", 552 "404838b", 553 "5defd9d", 554 "dc1e4be" 555 ], 556 "start": 7 557 }, 558 "description": "An Italian-American delicious dish", 559 "ingredients": [ 560 "spaghetti", 561 "tomato sauce", 562 "meatballs", 563 "love" 564 ], 565 "name": "Spaghetti with meatballs" 566 } 567 --a81a77b0ca68389dda3243a43ca946f2 568 Content-Disposition: attachment; filename="recipe.txt" 569 Content-Type: text/plain 570 Content-Length: 87 571 572 1. Cook spaghetti 573 2. Cook meetballs 574 3. Mix them 575 4. Add tomato sauce 576 5. ... 577 6. PROFIT! 578 579 --a81a77b0ca68389dda3243a43ca946f2-- 580 --7b1596fc4940bc1be725ad67f11ec1c4 581 Content-Type: application/json; error="true" 582 583 {"missing":"3-6bcedf1"} 584 --7b1596fc4940bc1be725ad67f11ec1c4--`), 585 }, nil 586 }), 587 id: "bar", 588 want: []rowResult{ 589 { 590 ID: "bar", 591 Rev: "1-917fa23", 592 }, 593 { 594 ID: "bar", 595 Rev: "7-474f12e", 596 }, 597 { 598 ID: "bar", 599 Rev: "3-6bcedf1", 600 Error: "missing", 601 }, 602 }, 603 } 604 }) 605 tests.Add("not found", func(*testing.T) interface{} { 606 return tt{ 607 db: newCustomDB(func(*http.Request) (*http.Response, error) { 608 return &http.Response{ 609 StatusCode: http.StatusNotFound, 610 Header: http.Header{ 611 "Content-Type": []string{`application/json`}, 612 }, 613 Body: Body(`{"error":"not_found","reason":"missing"}`), 614 }, nil 615 }), 616 id: "bar", 617 err: "Not Found", 618 } 619 }) 620 tests.Run(t, func(t *testing.T, tt tt) { 621 opts := tt.options 622 if opts == nil { 623 opts = mock.NilOption 624 } 625 rows, err := tt.db.OpenRevs(context.Background(), tt.id, tt.revs, opts) 626 var errMsg string 627 if err != nil { 628 errMsg = err.Error() 629 } 630 if errMsg != tt.err { 631 t.Errorf("Unexpected error: %s", err) 632 } 633 if errMsg != "" { 634 return 635 } 636 637 got := []rowResult{} 638 for i := 0; ; i++ { 639 row := new(driver.Row) 640 if err := rows.Next(row); err != nil { 641 if err == io.EOF { 642 break 643 } 644 t.Fatal(err) 645 } 646 row.Doc = nil // Determinism 647 row.Attachments = nil 648 got = append(got, rowResult{ 649 ID: row.ID, 650 Rev: row.Rev, 651 Error: func() string { 652 if row.Error != nil { 653 return row.Error.Error() 654 } 655 return "" 656 }(), 657 }) 658 } 659 if d := testy.DiffInterface(tt.want, got); d != nil { 660 t.Errorf("Unexpected result: %s", d) 661 } 662 }) 663 } 664 665 func TestCreateDoc(t *testing.T) { 666 tests := []struct { 667 name string 668 db *db 669 doc interface{} 670 options kivik.Option 671 id, rev string 672 status int 673 err string 674 }{ 675 { 676 name: "network error", 677 db: newTestDB(nil, errors.New("foo error")), 678 status: http.StatusBadGateway, 679 err: `Post "?http://example.com/testdb"?: foo error`, 680 }, 681 { 682 name: "invalid doc", 683 doc: make(chan int), 684 db: newTestDB(nil, errors.New("")), 685 status: http.StatusBadRequest, 686 err: `Post "?http://example.com/testdb"?: json: unsupported type: chan int`, 687 }, 688 { 689 name: "error response", 690 doc: map[string]interface{}{"foo": "bar"}, 691 db: newTestDB(&http.Response{ 692 StatusCode: http.StatusBadRequest, 693 Body: io.NopCloser(strings.NewReader("")), 694 }, nil), 695 status: http.StatusBadRequest, 696 err: "Bad Request", 697 }, 698 { 699 name: "invalid JSON response", 700 doc: map[string]interface{}{"foo": "bar"}, 701 db: newTestDB(&http.Response{ 702 StatusCode: http.StatusOK, 703 Body: io.NopCloser(strings.NewReader("invalid json")), 704 }, nil), 705 status: http.StatusBadGateway, 706 err: "invalid character 'i' looking for beginning of value", 707 }, 708 { 709 name: "success, 1.6.1", 710 doc: map[string]interface{}{"foo": "bar"}, 711 db: newTestDB(&http.Response{ 712 StatusCode: http.StatusOK, 713 Header: map[string][]string{ 714 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 715 "Location": {"http://localhost:5984/foo/43734cf3ce6d5a37050c050bb600006b"}, 716 "ETag": {`"1-4c6114c65e295552ab1019e2b046b10e"`}, 717 "Date": {"Wed, 25 Oct 2017 10:38:38 GMT"}, 718 "Content-Type": {"text/plain; charset=utf-8"}, 719 "Content-Length": {"95"}, 720 "Cache-Control": {"must-revalidate"}, 721 }, 722 Body: io.NopCloser(strings.NewReader(`{"ok":true,"id":"43734cf3ce6d5a37050c050bb600006b","rev":"1-4c6114c65e295552ab1019e2b046b10e"} 723 `)), 724 }, nil), 725 id: "43734cf3ce6d5a37050c050bb600006b", 726 rev: "1-4c6114c65e295552ab1019e2b046b10e", 727 }, 728 { 729 name: "batch mode", 730 db: newTestDB(nil, errors.New("success")), 731 doc: map[string]string{"foo": "bar"}, 732 options: kivik.Param("batch", "ok"), 733 status: http.StatusBadGateway, 734 err: `^Post "?http://example.com/testdb\?batch=ok"?: success$`, 735 }, 736 { 737 name: "full commit", 738 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 739 if err := consume(req.Body); err != nil { 740 return nil, err 741 } 742 if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" { 743 return nil, errors.New("X-Couch-Full-Commit not true") 744 } 745 return nil, errors.New("success") 746 }), 747 options: OptionFullCommit(), 748 status: http.StatusBadGateway, 749 err: `Post "?http://example.com/testdb"?: success`, 750 }, 751 { 752 name: "invalid options", 753 db: &db{}, 754 options: kivik.Param("foo", make(chan int)), 755 status: http.StatusBadRequest, 756 err: "kivik: invalid type chan int for options", 757 }, 758 } 759 for _, test := range tests { 760 t.Run(test.name, func(t *testing.T) { 761 opts := test.options 762 if opts == nil { 763 opts = mock.NilOption 764 } 765 id, rev, err := test.db.CreateDoc(context.Background(), test.doc, opts) 766 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 767 t.Error(d) 768 } 769 if test.id != id || test.rev != rev { 770 t.Errorf("Unexpected results: ID=%s rev=%s", id, rev) 771 } 772 }) 773 } 774 } 775 776 func TestOptionsToParams(t *testing.T) { 777 type otpTest struct { 778 Name string 779 Input map[string]interface{} 780 Expected url.Values 781 Error string 782 } 783 tests := []otpTest{ 784 { 785 Name: "Unmarshalable key", 786 Input: map[string]interface{}{"key": make(chan int)}, 787 Error: "json: unsupported type: chan int", 788 }, 789 { 790 Name: "String", 791 Input: map[string]interface{}{"foo": "bar"}, 792 Expected: map[string][]string{"foo": {"bar"}}, 793 }, 794 { 795 Name: "StringSlice", 796 Input: map[string]interface{}{"foo": []string{"bar", "baz"}}, 797 Expected: map[string][]string{"foo": {"bar", "baz"}}, 798 }, 799 { 800 Name: "Bool", 801 Input: map[string]interface{}{"foo": true}, 802 Expected: map[string][]string{"foo": {"true"}}, 803 }, 804 { 805 Name: "Int", 806 Input: map[string]interface{}{"foo": 123}, 807 Expected: map[string][]string{"foo": {"123"}}, 808 }, 809 { 810 Name: "Error", 811 Input: map[string]interface{}{"foo": []byte("foo")}, 812 Error: "kivik: invalid type []uint8 for options", 813 }, 814 } 815 for _, test := range tests { 816 func(test otpTest) { 817 t.Run(test.Name, func(t *testing.T) { 818 params, err := optionsToParams(test.Input) 819 var msg string 820 if err != nil { 821 msg = err.Error() 822 } 823 if msg != test.Error { 824 t.Errorf("Error\n\tExpected: %s\n\t Actual: %s\n", test.Error, msg) 825 } 826 if d := testy.DiffInterface(test.Expected, params); d != nil { 827 t.Errorf("Params not as expected:\n%s\n", d) 828 } 829 }) 830 }(test) 831 } 832 } 833 834 func TestCompact(t *testing.T) { 835 tests := []struct { 836 name string 837 db *db 838 status int 839 err string 840 }{ 841 { 842 name: "net error", 843 db: newTestDB(nil, errors.New("net error")), 844 status: http.StatusBadGateway, 845 err: `Post "?http://example.com/testdb/_compact"?: net error`, 846 }, 847 { 848 name: "1.6.1", 849 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 850 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON { 851 return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct) 852 } 853 return &http.Response{ 854 StatusCode: http.StatusOK, 855 Header: http.Header{ 856 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 857 "Date": {"Thu, 26 Oct 2017 13:07:52 GMT"}, 858 "Content-Type": {"text/plain; charset=utf-8"}, 859 "Content-Length": {"12"}, 860 "Cache-Control": {"must-revalidate"}, 861 }, 862 Body: io.NopCloser(strings.NewReader(`{"ok":true}`)), 863 }, nil 864 }), 865 }, 866 } 867 for _, test := range tests { 868 t.Run(test.name, func(t *testing.T) { 869 err := test.db.Compact(context.Background()) 870 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 871 t.Error(d) 872 } 873 }) 874 } 875 } 876 877 func TestCompactView(t *testing.T) { 878 tests := []struct { 879 name string 880 db *db 881 id string 882 status int 883 err string 884 }{ 885 { 886 name: "no ddoc", 887 status: http.StatusBadRequest, 888 err: "kivik: ddocID required", 889 }, 890 { 891 name: "network error", 892 db: newTestDB(nil, errors.New("net error")), 893 id: "foo", 894 status: http.StatusBadGateway, 895 err: `Post "?http://example.com/testdb/_compact/foo"?: net error`, 896 }, 897 { 898 name: "1.6.1", 899 id: "foo", 900 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 901 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON { 902 return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct) 903 } 904 return &http.Response{ 905 StatusCode: http.StatusAccepted, 906 Header: http.Header{ 907 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 908 "Date": {"Thu, 26 Oct 2017 13:07:52 GMT"}, 909 "Content-Type": {"text/plain; charset=utf-8"}, 910 "Content-Length": {"12"}, 911 "Cache-Control": {"must-revalidate"}, 912 }, 913 Body: io.NopCloser(strings.NewReader(`{"ok":true}`)), 914 }, nil 915 }), 916 }, 917 } 918 for _, test := range tests { 919 t.Run(test.name, func(t *testing.T) { 920 err := test.db.CompactView(context.Background(), test.id) 921 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 922 t.Error(d) 923 } 924 }) 925 } 926 } 927 928 func TestViewCleanup(t *testing.T) { 929 tests := []struct { 930 name string 931 db *db 932 status int 933 err string 934 }{ 935 { 936 name: "net error", 937 db: newTestDB(nil, errors.New("net error")), 938 status: http.StatusBadGateway, 939 err: `Post "?http://example.com/testdb/_view_cleanup"?: net error`, 940 }, 941 { 942 name: "1.6.1", 943 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 944 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON { 945 return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct) 946 } 947 return &http.Response{ 948 StatusCode: http.StatusOK, 949 Header: http.Header{ 950 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 951 "Date": {"Thu, 26 Oct 2017 13:07:52 GMT"}, 952 "Content-Type": {"text/plain; charset=utf-8"}, 953 "Content-Length": {"12"}, 954 "Cache-Control": {"must-revalidate"}, 955 }, 956 Body: io.NopCloser(strings.NewReader(`{"ok":true}`)), 957 }, nil 958 }), 959 }, 960 } 961 for _, test := range tests { 962 t.Run(test.name, func(t *testing.T) { 963 err := test.db.ViewCleanup(context.Background()) 964 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 965 t.Error(d) 966 } 967 }) 968 } 969 } 970 971 func TestPut(t *testing.T) { 972 type pTest struct { 973 name string 974 db *db 975 id string 976 doc interface{} 977 options kivik.Option 978 rev string 979 status int 980 err string 981 finish func() error 982 } 983 tests := []pTest{ 984 { 985 name: "missing docID", 986 status: http.StatusBadRequest, 987 err: "kivik: docID required", 988 }, 989 { 990 name: "network error", 991 id: "foo", 992 db: newTestDB(nil, errors.New("net error")), 993 status: http.StatusBadGateway, 994 err: `Put "?http://example.com/testdb/foo"?: net error`, 995 }, 996 { 997 name: "error response", 998 id: "foo", 999 db: newTestDB(&http.Response{ 1000 StatusCode: http.StatusBadRequest, 1001 Body: io.NopCloser(strings.NewReader("")), 1002 }, nil), 1003 status: http.StatusBadRequest, 1004 err: "Bad Request", 1005 }, 1006 { 1007 name: "invalid JSON response", 1008 id: "foo", 1009 db: newTestDB(&http.Response{ 1010 StatusCode: http.StatusOK, 1011 Body: io.NopCloser(strings.NewReader("invalid json")), 1012 }, nil), 1013 status: http.StatusBadGateway, 1014 err: "invalid character 'i' looking for beginning of value", 1015 }, 1016 { 1017 name: "invalid document", 1018 id: "foo", 1019 doc: make(chan int), 1020 db: newTestDB(&http.Response{ 1021 StatusCode: http.StatusOK, 1022 Body: io.NopCloser(strings.NewReader("")), 1023 }, nil), 1024 status: http.StatusBadRequest, 1025 err: `Put "?http://example.com/testdb/foo"?: json: unsupported type: chan int`, 1026 }, 1027 { 1028 name: "doc created, 1.6.1", 1029 id: "foo", 1030 doc: map[string]string{"foo": "bar"}, 1031 db: newTestDB(&http.Response{ 1032 StatusCode: http.StatusCreated, 1033 Header: http.Header{ 1034 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1035 "Location": {"http://localhost:5984/foo/foo"}, 1036 "ETag": {`"1-4c6114c65e295552ab1019e2b046b10e"`}, 1037 "Date": {"Wed, 25 Oct 2017 12:33:09 GMT"}, 1038 "Content-Type": {"text/plain; charset=utf-8"}, 1039 "Content-Length": {"66"}, 1040 "Cache-Control": {"must-revalidate"}, 1041 }, 1042 Body: io.NopCloser(strings.NewReader(`{"ok":true,"id":"foo","rev":"1-4c6114c65e295552ab1019e2b046b10e"}`)), 1043 }, nil), 1044 rev: "1-4c6114c65e295552ab1019e2b046b10e", 1045 }, 1046 { 1047 name: "full commit", 1048 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1049 if err := consume(req.Body); err != nil { 1050 return nil, err 1051 } 1052 if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" { 1053 return nil, errors.New("X-Couch-Full-Commit not true") 1054 } 1055 return nil, errors.New("success") 1056 }), 1057 id: "foo", 1058 doc: map[string]string{"foo": "bar"}, 1059 options: OptionFullCommit(), 1060 status: http.StatusBadGateway, 1061 err: `Put "?http://example.com/testdb/foo"?: success`, 1062 }, 1063 { 1064 name: "connection refused", 1065 db: func() *db { 1066 c, err := chttp.New(&http.Client{}, "http://127.0.0.1:1/", mock.NilOption) 1067 if err != nil { 1068 t.Fatal(err) 1069 } 1070 return &db{ 1071 client: &client{Client: c}, 1072 dbName: "animals", 1073 } 1074 }(), 1075 id: "cow", 1076 doc: map[string]interface{}{"feet": 4}, 1077 status: http.StatusBadGateway, 1078 err: `Put "?http://127.0.0.1:1/animals/cow"?: dial tcp ([::1]|127.0.0.1):1: (getsockopt|connect): connection refused`, 1079 }, 1080 func() pTest { 1081 db := realDB(t) 1082 return pTest{ 1083 name: "real database, multipart attachments", 1084 db: db, 1085 id: "foo", 1086 doc: map[string]interface{}{ 1087 "feet": 4, 1088 "_attachments": &kivik.Attachments{ 1089 "foo.txt": &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("test content")}, 1090 }, 1091 }, 1092 rev: "1-1e527110339245a3191b3f6cbea27ab1", 1093 finish: func() error { 1094 return db.client.DestroyDB(context.Background(), db.dbName, nil) 1095 }, 1096 } 1097 }(), 1098 } 1099 for _, test := range tests { 1100 t.Run(test.name, func(t *testing.T) { 1101 if test.finish != nil { 1102 t.Cleanup(func() { 1103 if err := test.finish(); err != nil { 1104 t.Fatal(err) 1105 } 1106 }) 1107 } 1108 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 1109 defer cancel() 1110 opts := test.options 1111 if opts == nil { 1112 opts = mock.NilOption 1113 } 1114 rev, err := test.db.Put(ctx, test.id, test.doc, opts) 1115 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 1116 t.Error(d) 1117 } 1118 if rev != test.rev { 1119 t.Errorf("Unexpected rev: %s", rev) 1120 } 1121 }) 1122 } 1123 } 1124 1125 func TestDelete(t *testing.T) { 1126 type tt struct { 1127 db *db 1128 id string 1129 options kivik.Option 1130 newrev string 1131 status int 1132 err string 1133 } 1134 1135 tests := testy.NewTable() 1136 tests.Add("no doc id", tt{ 1137 status: http.StatusBadRequest, 1138 err: "kivik: docID required", 1139 }) 1140 tests.Add("no rev", tt{ 1141 id: "foo", 1142 status: http.StatusBadRequest, 1143 err: "kivik: rev required", 1144 }) 1145 tests.Add("network error", tt{ 1146 id: "foo", 1147 options: kivik.Rev("1-xxx"), 1148 db: newTestDB(nil, errors.New("net error")), 1149 status: http.StatusBadGateway, 1150 err: `(Delete "?http://example.com/testdb/foo\?rev="?: )?net error`, 1151 }) 1152 tests.Add("1.6.1 conflict", tt{ 1153 id: "43734cf3ce6d5a37050c050bb600006b", 1154 options: kivik.Rev("1-xxx"), 1155 db: newTestDB(&http.Response{ 1156 StatusCode: 409, 1157 Header: http.Header{ 1158 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1159 "Date": {"Thu, 26 Oct 2017 13:29:06 GMT"}, 1160 "Content-Type": {"text/plain; charset=utf-8"}, 1161 "Content-Length": {"58"}, 1162 "Cache-Control": {"must-revalidate"}, 1163 }, 1164 Body: io.NopCloser(strings.NewReader(`{"error":"conflict","reason":"Document update conflict."}`)), 1165 }, nil), 1166 status: http.StatusConflict, 1167 err: "Conflict", 1168 }) 1169 tests.Add("1.6.1 success", tt{ 1170 id: "43734cf3ce6d5a37050c050bb600006b", 1171 options: kivik.Rev("1-4c6114c65e295552ab1019e2b046b10e"), 1172 db: newTestDB(&http.Response{ 1173 StatusCode: 200, 1174 Header: http.Header{ 1175 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1176 "Date": {"Thu, 26 Oct 2017 13:29:06 GMT"}, 1177 "Content-Type": {"text/plain; charset=utf-8"}, 1178 "ETag": {`"2-185ccf92154a9f24a4f4fd12233bf463"`}, 1179 "Content-Length": {"95"}, 1180 "Cache-Control": {"must-revalidate"}, 1181 }, 1182 Body: io.NopCloser(strings.NewReader(`{"ok":true,"id":"43734cf3ce6d5a37050c050bb600006b","rev":"2-185ccf92154a9f24a4f4fd12233bf463"}`)), 1183 }, nil), 1184 newrev: "2-185ccf92154a9f24a4f4fd12233bf463", 1185 }) 1186 tests.Add("batch mode", tt{ 1187 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1188 if err := consume(req.Body); err != nil { 1189 return nil, err 1190 } 1191 if batch := req.URL.Query().Get("batch"); batch != "ok" { 1192 return nil, fmt.Errorf("Unexpected query batch=%s", batch) 1193 } 1194 return nil, errors.New("success") 1195 }), 1196 id: "foo", 1197 options: kivik.Params(map[string]interface{}{ 1198 "batch": "ok", 1199 "rev": "1-xxx", 1200 }), 1201 status: http.StatusBadGateway, 1202 err: "success", 1203 }) 1204 tests.Add("invalid options", tt{ 1205 db: &db{}, 1206 id: "foo", 1207 options: kivik.Params(map[string]interface{}{ 1208 "foo": make(chan int), 1209 "rev": "1-xxx", 1210 }), 1211 status: http.StatusBadRequest, 1212 err: "kivik: invalid type chan int for options", 1213 }) 1214 tests.Add("full commit", tt{ 1215 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1216 if err := consume(req.Body); err != nil { 1217 return nil, err 1218 } 1219 if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" { 1220 return nil, errors.New("X-Couch-Full-Commit not true") 1221 } 1222 return nil, errors.New("success") 1223 }), 1224 id: "foo", 1225 options: multiOptions{ 1226 OptionFullCommit(), 1227 kivik.Rev("1-xxx"), 1228 }, 1229 status: http.StatusBadGateway, 1230 err: "success", 1231 }) 1232 1233 tests.Run(t, func(t *testing.T, tt tt) { 1234 opts := tt.options 1235 if opts == nil { 1236 opts = mock.NilOption 1237 } 1238 newrev, err := tt.db.Delete(context.Background(), tt.id, opts) 1239 if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" { 1240 t.Error(d) 1241 } 1242 if newrev != tt.newrev { 1243 t.Errorf("Unexpected new rev: %s", newrev) 1244 } 1245 }) 1246 } 1247 1248 func TestFlush(t *testing.T) { 1249 tests := []struct { 1250 name string 1251 db *db 1252 status int 1253 err string 1254 }{ 1255 { 1256 name: "network error", 1257 db: newTestDB(nil, errors.New("net error")), 1258 status: http.StatusBadGateway, 1259 err: `Post "?http://example.com/testdb/_ensure_full_commit"?: net error`, 1260 }, 1261 { 1262 name: "1.6.1", 1263 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1264 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON { 1265 return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct) 1266 } 1267 return &http.Response{ 1268 StatusCode: 201, 1269 Header: http.Header{ 1270 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1271 "Date": {"Thu, 26 Oct 2017 13:07:52 GMT"}, 1272 "Content-Type": {"text/plain; charset=utf-8"}, 1273 "Content-Length": {"53"}, 1274 "Cache-Control": {"must-revalidate"}, 1275 }, 1276 Body: io.NopCloser(strings.NewReader(`{"ok":true,"instance_start_time":"1509022681259533"}`)), 1277 }, nil 1278 }), 1279 }, 1280 { 1281 name: "2.0.0", 1282 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1283 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON { 1284 return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct) 1285 } 1286 return &http.Response{ 1287 StatusCode: 201, 1288 Header: http.Header{ 1289 "Server": {"CouchDB/2.0.0 (Erlang OTP/17)"}, 1290 "Date": {"Thu, 26 Oct 2017 13:07:52 GMT"}, 1291 "Content-Type": {typeJSON}, 1292 "Content-Length": {"38"}, 1293 "Cache-Control": {"must-revalidate"}, 1294 "X-Couch-Request-ID": {"e454023cb8"}, 1295 "X-CouchDB-Body-Time": {"0"}, 1296 }, 1297 Body: io.NopCloser(strings.NewReader(`{"ok":true,"instance_start_time":"0"}`)), 1298 }, nil 1299 }), 1300 }, 1301 } 1302 for _, test := range tests { 1303 t.Run(test.name, func(t *testing.T) { 1304 err := test.db.Flush(context.Background()) 1305 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 1306 t.Error(d) 1307 } 1308 }) 1309 } 1310 } 1311 1312 type queryResult struct { 1313 Offset int64 1314 TotalRows int64 1315 Warning string 1316 UpdateSeq string 1317 Err string 1318 Rows []*driver.Row 1319 } 1320 1321 func queryResultDiff(got, want queryResult) string { 1322 type qr struct { 1323 Offset int64 1324 TotalRows int64 1325 Warning string 1326 UpdateSeq string 1327 Err string 1328 Rows []*row 1329 } 1330 g := qr{ 1331 Offset: got.Offset, 1332 TotalRows: got.TotalRows, 1333 Warning: got.Warning, 1334 UpdateSeq: got.UpdateSeq, 1335 Err: got.Err, 1336 Rows: make([]*row, len(got.Rows)), 1337 } 1338 for i, row := range got.Rows { 1339 g.Rows[i] = driverRow2row(row) 1340 } 1341 1342 w := qr{ 1343 Offset: want.Offset, 1344 TotalRows: want.TotalRows, 1345 Warning: want.Warning, 1346 UpdateSeq: want.UpdateSeq, 1347 Err: want.Err, 1348 Rows: make([]*row, len(want.Rows)), 1349 } 1350 for i, row := range want.Rows { 1351 w.Rows[i] = driverRow2row(row) 1352 } 1353 return cmp.Diff(g, w) 1354 } 1355 1356 func TestRowsQuery(t *testing.T) { 1357 tests := []struct { 1358 name string 1359 db *db 1360 path string 1361 options kivik.Option 1362 expected queryResult 1363 status int 1364 err string 1365 }{ 1366 { 1367 name: "invalid options", 1368 path: "_all_docs", 1369 options: kivik.Param("foo", make(chan int)), 1370 status: http.StatusBadRequest, 1371 err: "kivik: invalid type chan int for options", 1372 }, 1373 { 1374 name: "network error", 1375 path: "_all_docs", 1376 db: newTestDB(nil, errors.New("go away")), 1377 status: http.StatusBadGateway, 1378 err: `Get "?http://example.com/testdb/_all_docs"?: go away`, 1379 }, 1380 { 1381 name: "error response", 1382 path: "_all_docs", 1383 db: newTestDB(&http.Response{ 1384 StatusCode: http.StatusBadRequest, 1385 Body: io.NopCloser(strings.NewReader("")), 1386 }, nil), 1387 status: http.StatusBadRequest, 1388 err: "Bad Request", 1389 }, 1390 { 1391 name: "all docs default 1.6.1", 1392 path: "_all_docs", 1393 db: newTestDB(&http.Response{ 1394 StatusCode: http.StatusOK, 1395 Header: map[string][]string{ 1396 "Transfer-Encoding": {"chunked"}, 1397 "Date": {"Tue, 24 Oct 2017 21:17:12 GMT"}, 1398 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1399 "ETag": {`"2MVNDK3T2PN4JUK89TKD10QDA"`}, 1400 "Content-Type": {"text/plain; charset=utf-8"}, 1401 "Cache-Control": {"must-revalidate"}, 1402 }, 1403 Body: io.NopCloser(strings.NewReader(`{"total_rows":3,"offset":0,"rows":[ 1404 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-75efcce1f083316d622d389f3f9813f7"}}, 1405 {"id":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","key":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","value":{"rev":"1-747e6766038164010fd0efcabd1a31dd"}}, 1406 {"id":"org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye","key":"org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye","value":{"rev":"1-4645438e6e1aa2230a1b06b5c1f5c63f"}} 1407 ]} 1408 `)), 1409 }, nil), 1410 expected: queryResult{ 1411 TotalRows: 3, 1412 Rows: []*driver.Row{ 1413 { 1414 ID: "_design/_auth", 1415 Key: []byte(`"_design/_auth"`), 1416 Value: strings.NewReader(`{"rev":"1-75efcce1f083316d622d389f3f9813f7"}`), 1417 }, 1418 { 1419 ID: "org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye", 1420 Key: []byte(`"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye"`), 1421 Value: strings.NewReader(`{"rev":"1-747e6766038164010fd0efcabd1a31dd"}`), 1422 }, 1423 { 1424 ID: "org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye", 1425 Key: []byte(`"org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye"`), 1426 Value: strings.NewReader(`{"rev":"1-4645438e6e1aa2230a1b06b5c1f5c63f"}`), 1427 }, 1428 }, 1429 }, 1430 }, 1431 { 1432 name: "all docs options 1.6.1", 1433 path: "/_all_docs?update_seq=true&limit=1&skip=1", 1434 db: newTestDB(&http.Response{ 1435 StatusCode: http.StatusOK, 1436 Header: map[string][]string{ 1437 "Transfer-Encoding": {"chunked"}, 1438 "Date": {"Tue, 24 Oct 2017 21:17:12 GMT"}, 1439 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1440 "ETag": {`"2MVNDK3T2PN4JUK89TKD10QDA"`}, 1441 "Content-Type": {"text/plain; charset=utf-8"}, 1442 "Cache-Control": {"must-revalidate"}, 1443 }, 1444 Body: io.NopCloser(strings.NewReader(`{"total_rows":3,"offset":1,"update_seq":31,"rows":[ 1445 {"id":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","key":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","value":{"rev":"1-747e6766038164010fd0efcabd1a31dd"}} 1446 ]} 1447 `)), 1448 }, nil), 1449 expected: queryResult{ 1450 TotalRows: 3, 1451 Offset: 1, 1452 UpdateSeq: "31", 1453 Rows: []*driver.Row{ 1454 { 1455 ID: "org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye", 1456 Key: []byte(`"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye"`), 1457 Value: strings.NewReader(`{"rev":"1-747e6766038164010fd0efcabd1a31dd"}`), 1458 }, 1459 }, 1460 }, 1461 }, 1462 { 1463 name: "all docs options 2.0.0, no results", 1464 path: "/_all_docs?update_seq=true&limit=1", 1465 db: newTestDB(&http.Response{ 1466 StatusCode: http.StatusOK, 1467 Header: map[string][]string{ 1468 "Transfer-Encoding": {"chunked"}, 1469 "Date": {"Tue, 24 Oct 2017 21:21:30 GMT"}, 1470 "Server": {"CouchDB/2.0.0 (Erlang OTP/17)"}, 1471 "Content-Type": {typeJSON}, 1472 "Cache-Control": {"must-revalidate"}, 1473 "X-Couch-Request-ID": {"a9688d9335"}, 1474 "X-Couch-Body-Time": {"0"}, 1475 }, 1476 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":0,"update_seq":"13-g1AAAAEzeJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjPlsQBJhgdA6j8QZCUy4Fv4AKLuflYiE151DRB18wmZtwCibj9u85ISgGRSPV63JSmA1NiD1bDgUJPIkCSP3xAHkCHxYDWsWQDg12MD","rows":[ 1477 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-75efcce1f083316d622d389f3f9813f7"}} 1478 ]} 1479 `)), 1480 }, nil), 1481 expected: queryResult{ 1482 TotalRows: 1, 1483 UpdateSeq: "13-g1AAAAEzeJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjPlsQBJhgdA6j8QZCUy4Fv4AKLuflYiE151DRB18wmZtwCibj9u85ISgGRSPV63JSmA1NiD1bDgUJPIkCSP3xAHkCHxYDWsWQDg12MD", 1484 Rows: []*driver.Row{ 1485 { 1486 ID: "_design/_auth", 1487 Key: []byte(`"_design/_auth"`), 1488 Value: strings.NewReader(`{"rev":"1-75efcce1f083316d622d389f3f9813f7"}`), 1489 }, 1490 }, 1491 }, 1492 }, 1493 { 1494 name: "all docs with keys", 1495 path: "/_all_docs", 1496 options: kivik.Param("keys", []string{"_design/_auth", "foo"}), 1497 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 1498 if r.Method != http.MethodPost { 1499 t.Errorf("Unexpected method: %s", r.Method) 1500 } 1501 defer r.Body.Close() // nolint: errcheck 1502 if d := testy.DiffAsJSON(map[string][]string{"keys": {"_design/_auth", "foo"}}, r.Body); d != nil { 1503 t.Error(d) 1504 } 1505 if keys := r.URL.Query().Get("keys"); keys != "" { 1506 t.Error("query key 'keys' should be absent") 1507 } 1508 return &http.Response{ 1509 StatusCode: http.StatusOK, 1510 Header: http.Header{ 1511 "Transfer-Encoding": {"chunked"}, 1512 "Date": {"Sat, 01 Sep 2018 19:01:30 GMT"}, 1513 "Server": {"CouchDB/2.2.0 (Erlang OTP/19)"}, 1514 "Content-Type": {typeJSON}, 1515 "Cache-Control": {"must-revalidate"}, 1516 "X-Couch-Request-ID": {"24fdb3fd86"}, 1517 "X-Couch-Body-Time": {"0"}, 1518 }, 1519 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[ 1520 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}} 1521 ]}`)), 1522 }, nil 1523 }), 1524 expected: queryResult{ 1525 TotalRows: 1, 1526 UpdateSeq: "", 1527 Rows: []*driver.Row{ 1528 { 1529 ID: "_design/_auth", 1530 Key: []byte(`"_design/_auth"`), 1531 Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`), 1532 }, 1533 }, 1534 }, 1535 }, 1536 { 1537 name: "all docs with endkey", 1538 path: "/_all_docs", 1539 options: kivik.Param("endkey", []string{"foo", "bar"}), 1540 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 1541 if d := testy.DiffAsJSON([]byte(`["foo","bar"]`), []byte(r.URL.Query().Get("endkey"))); d != nil { 1542 t.Error(d) 1543 } 1544 return &http.Response{ 1545 StatusCode: http.StatusOK, 1546 Header: http.Header{ 1547 "Transfer-Encoding": {"chunked"}, 1548 "Date": {"Sat, 01 Sep 2018 19:01:30 GMT"}, 1549 "Server": {"CouchDB/2.2.0 (Erlang OTP/19)"}, 1550 "Content-Type": {typeJSON}, 1551 "Cache-Control": {"must-revalidate"}, 1552 "X-Couch-Request-ID": {"24fdb3fd86"}, 1553 "X-Couch-Body-Time": {"0"}, 1554 }, 1555 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[ 1556 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}} 1557 ]}`)), 1558 }, nil 1559 }), 1560 expected: queryResult{ 1561 TotalRows: 1, 1562 UpdateSeq: "", 1563 Rows: []*driver.Row{ 1564 { 1565 ID: "_design/_auth", 1566 Key: []byte(`"_design/_auth"`), 1567 Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`), 1568 }, 1569 }, 1570 }, 1571 }, 1572 { 1573 name: "all docs with simple string endkey", 1574 path: "/_all_docs", 1575 options: kivik.Param("endkey", "foo"), 1576 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 1577 if d := testy.DiffAsJSON([]byte(`"foo"`), []byte(r.URL.Query().Get("endkey"))); d != nil { 1578 t.Error(d) 1579 } 1580 return &http.Response{ 1581 StatusCode: http.StatusOK, 1582 Header: http.Header{ 1583 "Transfer-Encoding": {"chunked"}, 1584 "Date": {"Sat, 01 Sep 2018 19:01:30 GMT"}, 1585 "Server": {"CouchDB/2.2.0 (Erlang OTP/19)"}, 1586 "Content-Type": {typeJSON}, 1587 "Cache-Control": {"must-revalidate"}, 1588 "X-Couch-Request-ID": {"24fdb3fd86"}, 1589 "X-Couch-Body-Time": {"0"}, 1590 }, 1591 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[ 1592 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}} 1593 ]}`)), 1594 }, nil 1595 }), 1596 expected: queryResult{ 1597 TotalRows: 1, 1598 UpdateSeq: "", 1599 Rows: []*driver.Row{ 1600 { 1601 ID: "_design/_auth", 1602 Key: []byte(`"_design/_auth"`), 1603 Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`), 1604 }, 1605 }, 1606 }, 1607 }, 1608 { 1609 name: "all docs with raw JSON endkey", 1610 path: "/_all_docs", 1611 options: kivik.Param("endkey", json.RawMessage(`"foo"`)), 1612 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 1613 if d := testy.DiffAsJSON([]byte(`"foo"`), []byte(r.URL.Query().Get("endkey"))); d != nil { 1614 t.Error(d) 1615 } 1616 return &http.Response{ 1617 StatusCode: http.StatusOK, 1618 Header: http.Header{ 1619 "Transfer-Encoding": {"chunked"}, 1620 "Date": {"Sat, 01 Sep 2018 19:01:30 GMT"}, 1621 "Server": {"CouchDB/2.2.0 (Erlang OTP/19)"}, 1622 "Content-Type": {typeJSON}, 1623 "Cache-Control": {"must-revalidate"}, 1624 "X-Couch-Request-ID": {"24fdb3fd86"}, 1625 "X-Couch-Body-Time": {"0"}, 1626 }, 1627 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[ 1628 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}} 1629 ]}`)), 1630 }, nil 1631 }), 1632 expected: queryResult{ 1633 TotalRows: 1, 1634 UpdateSeq: "", 1635 Rows: []*driver.Row{ 1636 { 1637 ID: "_design/_auth", 1638 Key: []byte(`"_design/_auth"`), 1639 Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`), 1640 }, 1641 }, 1642 }, 1643 }, 1644 { 1645 name: "all docs with object keys", 1646 path: "/_all_docs", 1647 options: kivik.Param("keys", []interface{}{"_design/_auth", "foo", []string{"bar", "baz"}}), 1648 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 1649 if r.Method != http.MethodPost { 1650 t.Errorf("Unexpected method: %s", r.Method) 1651 } 1652 defer r.Body.Close() // nolint: errcheck 1653 if d := testy.DiffAsJSON(map[string][]interface{}{"keys": {"_design/_auth", "foo", []string{"bar", "baz"}}}, r.Body); d != nil { 1654 t.Error(d) 1655 } 1656 if keys := r.URL.Query().Get("keys"); keys != "" { 1657 t.Error("query key 'keys' should be absent") 1658 } 1659 return &http.Response{ 1660 StatusCode: http.StatusOK, 1661 Header: http.Header{ 1662 "Transfer-Encoding": {"chunked"}, 1663 "Date": {"Sat, 01 Sep 2018 19:01:30 GMT"}, 1664 "Server": {"CouchDB/2.2.0 (Erlang OTP/19)"}, 1665 "Content-Type": {typeJSON}, 1666 "Cache-Control": {"must-revalidate"}, 1667 "X-Couch-Request-ID": {"24fdb3fd86"}, 1668 "X-Couch-Body-Time": {"0"}, 1669 }, 1670 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[ 1671 {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}} 1672 ]}`)), 1673 }, nil 1674 }), 1675 expected: queryResult{ 1676 TotalRows: 1, 1677 UpdateSeq: "", 1678 Rows: []*driver.Row{ 1679 { 1680 ID: "_design/_auth", 1681 Key: []byte(`"_design/_auth"`), 1682 Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`), 1683 }, 1684 }, 1685 }, 1686 }, 1687 { 1688 name: "all docs with docs", 1689 path: "/_all_docs", 1690 options: kivik.Param("keys", []string{"_design/_auth", "foo"}), 1691 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 1692 if r.Method != http.MethodPost { 1693 t.Errorf("Unexpected method: %s", r.Method) 1694 } 1695 defer r.Body.Close() // nolint: errcheck 1696 if d := testy.DiffAsJSON(map[string][]string{"keys": {"_design/_auth", "foo"}}, r.Body); d != nil { 1697 t.Error(d) 1698 } 1699 if keys := r.URL.Query().Get("keys"); keys != "" { 1700 t.Error("query key 'keys' should be absent") 1701 } 1702 return &http.Response{ 1703 StatusCode: http.StatusOK, 1704 Header: http.Header{ 1705 "Transfer-Encoding": {"chunked"}, 1706 "Date": {"Sat, 01 Sep 2018 19:01:30 GMT"}, 1707 "Server": {"CouchDB/2.2.0 (Erlang OTP/19)"}, 1708 "Content-Type": {typeJSON}, 1709 "Cache-Control": {"must-revalidate"}, 1710 "X-Couch-Request-ID": {"24fdb3fd86"}, 1711 "X-Couch-Body-Time": {"0"}, 1712 }, 1713 Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[ 1714 {"id":"foo","doc":{"_id":"foo"}} 1715 ]}`)), 1716 }, nil 1717 }), 1718 expected: queryResult{ 1719 TotalRows: 1, 1720 UpdateSeq: "", 1721 Rows: []*driver.Row{ 1722 { 1723 ID: "foo", 1724 Doc: strings.NewReader(`{"_id":"foo"}`), 1725 }, 1726 }, 1727 }, 1728 }, 1729 } 1730 for _, test := range tests { 1731 t.Run(test.name, func(t *testing.T) { 1732 opts := test.options 1733 if opts == nil { 1734 opts = mock.NilOption 1735 } 1736 rows, err := test.db.rowsQuery(context.Background(), test.path, opts) 1737 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 1738 t.Error(d) 1739 } 1740 if err != nil { 1741 return 1742 } 1743 result := queryResult{ 1744 Rows: []*driver.Row{}, 1745 } 1746 for { 1747 var row driver.Row 1748 if e := rows.Next(&row); e != nil { 1749 if e != io.EOF { 1750 result.Err = e.Error() 1751 } 1752 break 1753 } 1754 result.Rows = append(result.Rows, &row) 1755 } 1756 result.Offset = rows.Offset() 1757 result.TotalRows = rows.TotalRows() 1758 result.UpdateSeq = rows.UpdateSeq() 1759 if warner, ok := rows.(driver.RowsWarner); ok { 1760 result.Warning = warner.Warning() 1761 } else { 1762 t.Errorf("RowsWarner interface not satisfied!!?") 1763 } 1764 1765 if d := queryResultDiff(test.expected, result); d != "" { 1766 t.Error(d) 1767 } 1768 }) 1769 } 1770 } 1771 1772 func TestSecurity(t *testing.T) { 1773 tests := []struct { 1774 name string 1775 db *db 1776 expected *driver.Security 1777 status int 1778 err string 1779 }{ 1780 { 1781 name: "network error", 1782 db: newTestDB(nil, errors.New("net error")), 1783 status: http.StatusBadGateway, 1784 err: `Get "?http://example.com/testdb/_security"?: net error`, 1785 }, 1786 { 1787 name: "1.6.1 empty", 1788 db: newTestDB(&http.Response{ 1789 StatusCode: 200, 1790 Header: http.Header{ 1791 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1792 "Date": {"Thu, 26 Oct 2017 14:28:14 GMT"}, 1793 "Content-Type": {"text/plain; charset=utf-8"}, 1794 "Content-Length": {"3"}, 1795 "Cache-Control": {"must-revalidate"}, 1796 }, 1797 Body: io.NopCloser(strings.NewReader("{}")), 1798 }, nil), 1799 expected: &driver.Security{}, 1800 }, 1801 { 1802 name: "1.6.1 non-empty", 1803 db: newTestDB(&http.Response{ 1804 StatusCode: 200, 1805 Header: http.Header{ 1806 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1807 "Date": {"Thu, 26 Oct 2017 14:28:14 GMT"}, 1808 "Content-Type": {"text/plain; charset=utf-8"}, 1809 "Content-Length": {"65"}, 1810 "Cache-Control": {"must-revalidate"}, 1811 }, 1812 Body: io.NopCloser(strings.NewReader(`{"admins":{},"members":{"names":["32dgsme3cmi6pddghslq5egiye"]}}`)), 1813 }, nil), 1814 expected: &driver.Security{ 1815 Members: driver.Members{ 1816 Names: []string{"32dgsme3cmi6pddghslq5egiye"}, 1817 }, 1818 }, 1819 }, 1820 } 1821 for _, test := range tests { 1822 t.Run(test.name, func(t *testing.T) { 1823 result, err := test.db.Security(context.Background()) 1824 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 1825 t.Error(d) 1826 } 1827 if d := testy.DiffInterface(test.expected, result); d != nil { 1828 t.Error(d) 1829 } 1830 }) 1831 } 1832 } 1833 1834 func TestSetSecurity(t *testing.T) { 1835 type tt struct { 1836 db *db 1837 security *driver.Security 1838 status int 1839 err string 1840 } 1841 tests := testy.NewTable() 1842 1843 tests.Add("network error", tt{ 1844 db: newTestDB(nil, errors.New("net error")), 1845 status: http.StatusBadGateway, 1846 err: `Put "?http://example.com/testdb/_security"?: net error`, 1847 }) 1848 tests.Add("1.6.1", func(t *testing.T) interface{} { 1849 return tt{ 1850 security: &driver.Security{ 1851 Admins: driver.Members{ 1852 Names: []string{"bob"}, 1853 }, 1854 Members: driver.Members{ 1855 Roles: []string{"users"}, 1856 }, 1857 }, 1858 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1859 defer req.Body.Close() // nolint: errcheck 1860 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON { 1861 return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct) 1862 } 1863 var body interface{} 1864 if err := json.NewDecoder(req.Body).Decode(&body); err != nil { 1865 return nil, err 1866 } 1867 expected := map[string]interface{}{ 1868 "admins": map[string]interface{}{ 1869 "names": []string{"bob"}, 1870 }, 1871 "members": map[string]interface{}{ 1872 "roles": []string{"users"}, 1873 }, 1874 } 1875 if d := testy.DiffAsJSON(expected, body); d != nil { 1876 t.Error(d) 1877 } 1878 return &http.Response{ 1879 StatusCode: 200, 1880 Header: http.Header{ 1881 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1882 "Date": {"Thu, 26 Oct 2017 15:06:21 GMT"}, 1883 "Content-Type": {"text/plain; charset=utf-8"}, 1884 "Content-Length": {"12"}, 1885 "Cache-Control": {"must-revalidate"}, 1886 }, 1887 Body: io.NopCloser(strings.NewReader(`{"ok":true}`)), 1888 }, nil 1889 }), 1890 } 1891 }) 1892 1893 tests.Run(t, func(t *testing.T, tt tt) { 1894 err := tt.db.SetSecurity(context.Background(), tt.security) 1895 if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" { 1896 t.Error(d) 1897 } 1898 }) 1899 } 1900 1901 func TestGetRev(t *testing.T) { 1902 tests := []struct { 1903 name string 1904 db *db 1905 id string 1906 rev string 1907 status int 1908 err string 1909 }{ 1910 { 1911 name: "no doc id", 1912 status: http.StatusBadRequest, 1913 err: "kivik: docID required", 1914 }, 1915 { 1916 name: "network error", 1917 id: "foo", 1918 db: newTestDB(nil, errors.New("net error")), 1919 status: http.StatusBadGateway, 1920 err: `Head "?http://example.com/testdb/foo"?: net error`, 1921 }, 1922 { 1923 name: "1.6.1", 1924 id: "foo", 1925 db: newTestDB(&http.Response{ 1926 StatusCode: 200, 1927 Request: &http.Request{ 1928 Method: "HEAD", 1929 }, 1930 Header: http.Header{ 1931 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 1932 "ETag": {`"1-4c6114c65e295552ab1019e2b046b10e"`}, 1933 "Date": {"Thu, 26 Oct 2017 15:21:15 GMT"}, 1934 "Content-Type": {"text/plain; charset=utf-8"}, 1935 "Content-Length": {"70"}, 1936 "Cache-Control": {"must-revalidate"}, 1937 }, 1938 ContentLength: 70, 1939 Body: io.NopCloser(strings.NewReader("")), 1940 }, nil), 1941 rev: "1-4c6114c65e295552ab1019e2b046b10e", 1942 }, 1943 } 1944 for _, test := range tests { 1945 t.Run(test.name, func(t *testing.T) { 1946 rev, err := test.db.GetRev(context.Background(), test.id, mock.NilOption) 1947 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 1948 t.Error(d) 1949 } 1950 if rev != test.rev { 1951 t.Errorf("Got rev %s, expected %s", rev, test.rev) 1952 } 1953 }) 1954 } 1955 } 1956 1957 func TestCopy(t *testing.T) { 1958 type tt struct { 1959 target, source string 1960 options kivik.Option 1961 db *db 1962 rev string 1963 status int 1964 err string 1965 } 1966 1967 tests := testy.NewTable() 1968 tests.Add("missing source", tt{ 1969 status: http.StatusBadRequest, 1970 err: "kivik: sourceID required", 1971 }) 1972 tests.Add("missing target", tt{ 1973 source: "foo", 1974 status: http.StatusBadRequest, 1975 err: "kivik: targetID required", 1976 }) 1977 tests.Add("network error", tt{ 1978 source: "foo", 1979 target: "bar", 1980 db: newTestDB(nil, errors.New("net error")), 1981 status: http.StatusBadGateway, 1982 err: "(Copy http://example.com/testdb/foo: )?net error", 1983 }) 1984 tests.Add("invalid options", tt{ 1985 db: &db{}, 1986 source: "foo", 1987 target: "bar", 1988 options: kivik.Param("foo", make(chan int)), 1989 status: http.StatusBadRequest, 1990 err: "kivik: invalid type chan int for options", 1991 }) 1992 tests.Add("create 1.6.1", tt{ 1993 source: "foo", 1994 target: "bar", 1995 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 1996 if req.Header.Get("Destination") != "bar" { 1997 return nil, errors.New("Unexpected destination") 1998 } 1999 return &http.Response{ 2000 StatusCode: 201, 2001 Header: http.Header{ 2002 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 2003 "Location": {"http://example.com/foo/bar"}, 2004 "ETag": {`"1-f81c8a795b0c6f9e9f699f64c6b82256"`}, 2005 "Date": {"Thu, 26 Oct 2017 15:45:57 GMT"}, 2006 "Content-Type": {"text/plain; charset=utf-8"}, 2007 "Content-Length": {"66"}, 2008 "Cache-Control": {"must-revalidate"}, 2009 }, 2010 Body: Body(`{"ok":true,"id":"bar","rev":"1-f81c8a795b0c6f9e9f699f64c6b82256"}`), 2011 }, nil 2012 }), 2013 rev: "1-f81c8a795b0c6f9e9f699f64c6b82256", 2014 }) 2015 tests.Add("full commit 1.6.1", tt{ 2016 source: "foo", 2017 target: "bar", 2018 options: OptionFullCommit(), 2019 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 2020 if dest := req.Header.Get("Destination"); dest != "bar" { 2021 return nil, fmt.Errorf("Unexpected destination: %s", dest) 2022 } 2023 if fc := req.Header.Get("X-Couch-Full-Commit"); fc != "true" { 2024 return nil, fmt.Errorf("X-Couch-Full-Commit: %s", fc) 2025 } 2026 return &http.Response{ 2027 StatusCode: 201, 2028 Header: http.Header{ 2029 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 2030 "Location": {"http://example.com/foo/bar"}, 2031 "ETag": {`"1-f81c8a795b0c6f9e9f699f64c6b82256"`}, 2032 "Date": {"Thu, 26 Oct 2017 15:45:57 GMT"}, 2033 "Content-Type": {"text/plain; charset=utf-8"}, 2034 "Content-Length": {"66"}, 2035 "Cache-Control": {"must-revalidate"}, 2036 }, 2037 Body: Body(`{"ok":true,"id":"bar","rev":"1-f81c8a795b0c6f9e9f699f64c6b82256"}`), 2038 }, nil 2039 }), 2040 rev: "1-f81c8a795b0c6f9e9f699f64c6b82256", 2041 }) 2042 tests.Add("target rev", tt{ 2043 source: "foo", 2044 target: "bar?rev=1-xxx", 2045 options: OptionFullCommit(), 2046 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 2047 if dest := req.Header.Get("Destination"); dest != "bar?rev=1-xxx" { 2048 return nil, fmt.Errorf("Unexpected destination: %s", dest) 2049 } 2050 if fc := req.Header.Get("X-Couch-Full-Commit"); fc != "true" { 2051 return nil, fmt.Errorf("X-Couch-Full-Commit: %s", fc) 2052 } 2053 return &http.Response{ 2054 StatusCode: 201, 2055 Header: http.Header{ 2056 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"}, 2057 "Location": {"http://example.com/foo/bar"}, 2058 "ETag": {`"2-yyy"`}, 2059 "Date": {"Thu, 26 Oct 2017 15:45:57 GMT"}, 2060 "Content-Type": {"text/plain; charset=utf-8"}, 2061 "Content-Length": {"66"}, 2062 "Cache-Control": {"must-revalidate"}, 2063 }, 2064 Body: Body(`{"ok":true,"id":"bar","rev":"2-yyy"}`), 2065 }, nil 2066 }), 2067 rev: "2-yyy", 2068 }) 2069 2070 tests.Run(t, func(t *testing.T, tt tt) { 2071 opts := tt.options 2072 if opts == nil { 2073 opts = mock.NilOption 2074 } 2075 rev, err := tt.db.Copy(context.Background(), tt.target, tt.source, opts) 2076 if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" { 2077 t.Error(d) 2078 } 2079 if rev != tt.rev { 2080 t.Errorf("Got %s, expected %s", rev, tt.rev) 2081 } 2082 }) 2083 } 2084 2085 func TestMultipartAttachmentsNext(t *testing.T) { 2086 tests := []struct { 2087 name string 2088 atts *multipartAttachments 2089 content string 2090 expected *driver.Attachment 2091 status int 2092 err string 2093 }{ 2094 { 2095 name: "done reading", 2096 atts: &multipartAttachments{ 2097 mpReader: func() *multipart.Reader { 2098 r := multipart.NewReader(strings.NewReader("--xxx\r\n\r\n--xxx--"), "xxx") 2099 _, _ = r.NextPart() 2100 return r 2101 }(), 2102 }, 2103 status: 500, 2104 err: io.EOF.Error(), 2105 }, 2106 { 2107 name: "malformed message", 2108 atts: &multipartAttachments{ 2109 mpReader: func() *multipart.Reader { 2110 r := multipart.NewReader(strings.NewReader("oink"), "xxx") 2111 _, _ = r.NextPart() 2112 return r 2113 }(), 2114 }, 2115 status: http.StatusBadGateway, 2116 err: "multipart: NextPart: EOF", 2117 }, 2118 { 2119 name: "malformed Content-Disposition", 2120 atts: &multipartAttachments{ 2121 mpReader: multipart.NewReader(strings.NewReader(`--xxx 2122 Content-Type: text/plain 2123 2124 --xxx--`), "xxx"), 2125 }, 2126 status: http.StatusBadGateway, 2127 err: "Content-Disposition: mime: no media type", 2128 }, 2129 { 2130 name: "malformed Content-Type", 2131 atts: &multipartAttachments{ 2132 meta: map[string]attMeta{ 2133 "foo.txt": {Follows: true}, 2134 }, 2135 mpReader: multipart.NewReader(strings.NewReader(`--xxx 2136 Content-Type: text/plain; =foo 2137 Content-Disposition: attachment; filename="foo.txt" 2138 2139 --xxx--`), "xxx"), 2140 }, 2141 status: http.StatusBadGateway, 2142 err: "mime: invalid media parameter", 2143 }, 2144 { 2145 name: "file not in manifest", 2146 atts: &multipartAttachments{ 2147 mpReader: multipart.NewReader(strings.NewReader(`--xxx 2148 Content-Type: text/plain; charset=foobar 2149 Content-Disposition: attachment; filename="foo.txt" 2150 2151 test content 2152 --xxx--`), "xxx"), 2153 }, 2154 status: http.StatusBadGateway, 2155 err: "File 'foo.txt' not in manifest", 2156 }, 2157 { 2158 name: "invalid content-disposition", 2159 atts: &multipartAttachments{ 2160 mpReader: multipart.NewReader(strings.NewReader(`--xxx 2161 Content-Type: text/plain 2162 Content-Disposition: oink 2163 2164 --xxx--`), "xxx"), 2165 }, 2166 status: http.StatusBadGateway, 2167 err: "Unexpected Content-Disposition: oink", 2168 }, 2169 { 2170 name: "success", 2171 atts: &multipartAttachments{ 2172 meta: map[string]attMeta{ 2173 "foo.txt": {Follows: true}, 2174 }, 2175 mpReader: multipart.NewReader(strings.NewReader(`--xxx 2176 Content-Type: text/plain; charset=foobar 2177 Content-Disposition: attachment; filename="foo.txt" 2178 2179 test content 2180 --xxx--`), "xxx"), 2181 }, 2182 content: "test content", 2183 expected: &driver.Attachment{ 2184 Filename: "foo.txt", 2185 ContentType: "text/plain", 2186 Size: -1, 2187 }, 2188 }, 2189 { 2190 name: "success, no Content-Type header, & Content-Length header", 2191 atts: &multipartAttachments{ 2192 meta: map[string]attMeta{ 2193 "foo.txt": { 2194 Follows: true, 2195 ContentType: "text/plain", 2196 }, 2197 }, 2198 mpReader: multipart.NewReader(strings.NewReader(`--xxx 2199 Content-Disposition: attachment; filename="foo.txt" 2200 Content-Length: 123 2201 2202 test content 2203 --xxx--`), "xxx"), 2204 }, 2205 content: "test content", 2206 expected: &driver.Attachment{ 2207 Filename: "foo.txt", 2208 ContentType: "text/plain", 2209 Size: 123, 2210 }, 2211 }, 2212 } 2213 for _, test := range tests { 2214 t.Run(test.name, func(t *testing.T) { 2215 result := new(driver.Attachment) 2216 err := test.atts.Next(result) 2217 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" { 2218 t.Error(d) 2219 } 2220 if err != nil { 2221 return 2222 } 2223 content, err := io.ReadAll(result.Content) 2224 if err != nil { 2225 t.Fatal(err) 2226 } 2227 if d := testy.DiffText(test.content, string(content)); d != nil { 2228 t.Errorf("Unexpected content:\n%s", d) 2229 } 2230 result.Content = nil // Determinism 2231 if d := testy.DiffInterface(test.expected, result); d != nil { 2232 t.Error(d) 2233 } 2234 }) 2235 } 2236 } 2237 2238 func TestMultipartAttachmentsClose(t *testing.T) { 2239 const wantErr = "some error" 2240 atts := &multipartAttachments{ 2241 content: &mockReadCloser{ 2242 CloseFunc: func() error { 2243 return errors.New(wantErr) 2244 }, 2245 }, 2246 } 2247 2248 if err := atts.Close(); !testy.ErrorMatches(wantErr, err) { 2249 t.Errorf("Unexpected error: %s", err) 2250 } 2251 } 2252 2253 func TestPurge(t *testing.T) { 2254 expectedDocMap := map[string][]string{ 2255 "foo": {"1-abc", "2-def"}, 2256 "bar": {"3-ghi"}, 2257 } 2258 tests := []struct { 2259 name string 2260 db *db 2261 docMap map[string][]string 2262 2263 expected *driver.PurgeResult 2264 err string 2265 status int 2266 }{ 2267 { 2268 name: "1.7.1, nothing deleted", 2269 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 2270 if r.Method != "POST" { 2271 return nil, fmt.Errorf("Unexpected method: %s", r.Method) 2272 } 2273 if r.URL.Path != "/testdb/_purge" { 2274 return nil, fmt.Errorf("Unexpected path: %s", r.URL.Path) 2275 } 2276 if ct := r.Header.Get("Content-Type"); ct != typeJSON { 2277 return nil, fmt.Errorf("Unexpected Content-Type: %s", ct) 2278 } 2279 defer r.Body.Close() // nolint: errcheck 2280 var result interface{} 2281 if err := json.NewDecoder(r.Body).Decode(&result); err != nil { 2282 return nil, err 2283 } 2284 if d := testy.DiffAsJSON(expectedDocMap, result); d != nil { 2285 return nil, fmt.Errorf("Unexpected payload:\n%s", d) 2286 } 2287 return &http.Response{ 2288 StatusCode: http.StatusOK, 2289 Header: http.Header{ 2290 "Server": []string{"CouchDB/1.7.1 (Erlang OTP/17)"}, 2291 "Date": []string{"Thu, 06 Sep 2018 16:55:26 GMT"}, 2292 "Content-Type": []string{"text/plain; charset=utf-8"}, 2293 "Content-Length": []string{"28"}, 2294 "Cache-Control": []string{"must-revalidate"}, 2295 }, 2296 Body: io.NopCloser(strings.NewReader(`{"purge_seq":3,"purged":{}}`)), 2297 }, nil 2298 }), 2299 docMap: expectedDocMap, 2300 expected: &driver.PurgeResult{Seq: 3, Purged: map[string][]string{}}, 2301 }, 2302 { 2303 name: "1.7.1, all deleted", 2304 db: newTestDB(&http.Response{ 2305 StatusCode: http.StatusOK, 2306 Header: http.Header{ 2307 "Server": []string{"CouchDB/1.7.1 (Erlang OTP/17)"}, 2308 "Date": []string{"Thu, 06 Sep 2018 16:55:26 GMT"}, 2309 "Content-Type": []string{"text/plain; charset=utf-8"}, 2310 "Content-Length": []string{"168"}, 2311 "Cache-Control": []string{"must-revalidate"}, 2312 }, 2313 Body: io.NopCloser(strings.NewReader(`{"purge_seq":5,"purged":{"foo":["1-abc","2-def"],"bar":["3-ghi"]}}`)), 2314 }, nil), 2315 docMap: expectedDocMap, 2316 expected: &driver.PurgeResult{Seq: 5, Purged: expectedDocMap}, 2317 }, 2318 { 2319 name: "2.2.0, not supported", 2320 db: newTestDB(&http.Response{ 2321 StatusCode: 501, 2322 ContentLength: 75, 2323 Header: http.Header{ 2324 "Server": []string{"CouchDB/2.2.0 (Erlang OTP/19)"}, 2325 "Date": []string{"Thu, 06 Sep 2018 16:55:26 GMT"}, 2326 "Content-Type": []string{typeJSON}, 2327 "Content-Length": []string{"75"}, 2328 "Cache-Control": []string{"must-revalidate"}, 2329 "X-Couch-Request-ID": []string{"03e91291c8"}, 2330 "X-CouchDB-Body-Time": []string{"0"}, 2331 }, 2332 Body: io.NopCloser(strings.NewReader(`{"error":"not_implemented","reason":"this feature is not yet implemented"}`)), 2333 }, nil), 2334 docMap: expectedDocMap, 2335 err: "Not Implemented: this feature is not yet implemented", 2336 status: http.StatusNotImplemented, 2337 }, 2338 } 2339 for _, test := range tests { 2340 t.Run(test.name, func(t *testing.T) { 2341 result, err := test.db.Purge(context.Background(), test.docMap) 2342 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" { 2343 t.Error(d) 2344 } 2345 if err != nil { 2346 return 2347 } 2348 if d := testy.DiffInterface(test.expected, result); d != nil { 2349 t.Error(d) 2350 } 2351 }) 2352 } 2353 } 2354 2355 func TestMultipartAttachments(t *testing.T) { 2356 tests := []struct { 2357 name string 2358 input string 2359 atts *kivik.Attachments 2360 expected string 2361 size int64 2362 err string 2363 }{ 2364 { 2365 name: "no attachments", 2366 input: `{"foo":"bar","baz":"qux"}`, 2367 atts: &kivik.Attachments{}, 2368 expected: ` 2369 --%[1]s 2370 Content-Type: application/json 2371 2372 {"foo":"bar","baz":"qux"} 2373 --%[1]s-- 2374 `, 2375 size: 191, 2376 }, 2377 { 2378 name: "simple", 2379 input: `{"_attachments":{}}`, 2380 atts: &kivik.Attachments{ 2381 "foo.txt": &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("test content")}, 2382 }, 2383 expected: ` 2384 --%[1]s 2385 Content-Type: application/json 2386 2387 {"_attachments":{"foo.txt":{"content_type":"text/plain","length":13,"follows":true}} 2388 } 2389 --%[1]s 2390 2391 test content 2392 2393 --%[1]s-- 2394 `, 2395 size: 333, 2396 }, 2397 } 2398 for _, test := range tests { 2399 t.Run(test.name, func(t *testing.T) { 2400 in := io.NopCloser(strings.NewReader(test.input)) 2401 boundary, size, body, err := newMultipartAttachments(in, test.atts) 2402 if !testy.ErrorMatches(test.err, err) { 2403 t.Errorf("Unexpected error: %s", err) 2404 } 2405 if test.size != size { 2406 t.Errorf("Unexpected size: %d (want %d)", size, test.size) 2407 } 2408 result, _ := io.ReadAll(body) 2409 expected := fmt.Sprintf(test.expected, boundary) 2410 expected = strings.TrimPrefix(expected, "\n") 2411 result = bytes.ReplaceAll(result, []byte("\r\n"), []byte("\n")) 2412 if d := testy.DiffText(expected, string(result)); d != nil { 2413 t.Error(d) 2414 } 2415 }) 2416 } 2417 } 2418 2419 func TestAttachmentStubs(t *testing.T) { 2420 tests := []struct { 2421 name string 2422 atts *kivik.Attachments 2423 expected map[string]*stub 2424 }{ 2425 { 2426 name: "simple", 2427 atts: &kivik.Attachments{ 2428 "foo.txt": &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("test content")}, 2429 }, 2430 expected: map[string]*stub{ 2431 "foo.txt": { 2432 ContentType: "text/plain", 2433 Size: 13, 2434 }, 2435 }, 2436 }, 2437 } 2438 for _, test := range tests { 2439 t.Run(test.name, func(t *testing.T) { 2440 result, _ := attachmentStubs(test.atts) 2441 if d := testy.DiffInterface(test.expected, result); d != nil { 2442 t.Error(d) 2443 } 2444 }) 2445 } 2446 } 2447 2448 func TestInterfaceToAttachments(t *testing.T) { 2449 tests := []struct { 2450 name string 2451 input interface{} 2452 output interface{} 2453 expected *kivik.Attachments 2454 ok bool 2455 }{ 2456 { 2457 name: "non-attachment input", 2458 input: "foo", 2459 output: "foo", 2460 expected: nil, 2461 ok: false, 2462 }, 2463 { 2464 name: "pointer input", 2465 input: &kivik.Attachments{ 2466 "foo.txt": nil, 2467 }, 2468 output: new(kivik.Attachments), 2469 expected: &kivik.Attachments{ 2470 "foo.txt": nil, 2471 }, 2472 ok: true, 2473 }, 2474 { 2475 name: "non-pointer input", 2476 input: kivik.Attachments{ 2477 "foo.txt": nil, 2478 }, 2479 output: kivik.Attachments{}, 2480 expected: &kivik.Attachments{ 2481 "foo.txt": nil, 2482 }, 2483 ok: true, 2484 }, 2485 } 2486 for _, test := range tests { 2487 t.Run(test.name, func(t *testing.T) { 2488 result, ok := interfaceToAttachments(test.input) 2489 if ok != test.ok { 2490 t.Errorf("Unexpected OK result: %v", result) 2491 } 2492 if d := testy.DiffInterface(test.expected, result); d != nil { 2493 t.Errorf("Unexpected result:\n%s\n", d) 2494 } 2495 if d := testy.DiffInterface(test.output, test.input); d != nil { 2496 t.Errorf("Input not properly modified:\n%s\n", d) 2497 } 2498 }) 2499 } 2500 } 2501 2502 func TestStubMarshalJSON(t *testing.T) { 2503 att := &stub{ 2504 ContentType: "text/plain", 2505 Size: 123, 2506 } 2507 expected := `{"content_type":"text/plain","length":123,"follows":true}` 2508 result, err := json.Marshal(att) 2509 if !testy.ErrorMatches("", err) { 2510 t.Errorf("Unexpected error: %s", err) 2511 } 2512 if d := testy.DiffJSON([]byte(expected), result); d != nil { 2513 t.Error(d) 2514 } 2515 } 2516 2517 func Test_attachmentSize(t *testing.T) { 2518 type tst struct { 2519 att *kivik.Attachment 2520 expected *kivik.Attachment 2521 err string 2522 } 2523 tests := testy.NewTable() 2524 tests.Add("size already set", tst{ 2525 att: &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text"), Size: 4}, 2526 expected: &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text"), Size: 4}, 2527 }) 2528 tests.Add("bytes buffer", tst{ 2529 att: &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text")}, 2530 expected: &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text"), Size: 5}, 2531 }) 2532 tests.Run(t, func(t *testing.T, test tst) { 2533 err := attachmentSize(test.att) 2534 if !testy.ErrorMatches(test.err, err) { 2535 t.Errorf("Unexpected error: %s", err) 2536 } 2537 body, err := io.ReadAll(test.att.Content) 2538 if err != nil { 2539 t.Fatal(err) 2540 } 2541 expBody, err := io.ReadAll(test.expected.Content) 2542 if err != nil { 2543 t.Fatal(err) 2544 } 2545 if d := testy.DiffText(expBody, body); d != nil { 2546 t.Errorf("Content differs:\n%s\n", d) 2547 } 2548 test.att.Content = nil 2549 test.expected.Content = nil 2550 if d := testy.DiffInterface(test.expected, test.att); d != nil { 2551 t.Error(d) 2552 } 2553 }) 2554 } 2555 2556 type lenReader interface { 2557 io.Reader 2558 lener 2559 } 2560 2561 type myReader struct { 2562 lenReader 2563 } 2564 2565 var _ interface { 2566 io.Closer 2567 lenReader 2568 } = &myReader{} 2569 2570 func (r *myReader) Close() error { return nil } 2571 2572 func Test_readerSize(t *testing.T) { 2573 type tst struct { 2574 in io.ReadCloser 2575 size int64 2576 body string 2577 err string 2578 } 2579 tests := testy.NewTable() 2580 tests.Add("*bytes.Buffer", tst{ 2581 in: &myReader{bytes.NewBuffer([]byte("foo bar"))}, 2582 size: 7, 2583 body: "foo bar", 2584 }) 2585 tests.Add("bytes.NewReader", tst{ 2586 in: &myReader{bytes.NewReader([]byte("foo bar"))}, 2587 size: 7, 2588 body: "foo bar", 2589 }) 2590 tests.Add("strings.NewReader", tst{ 2591 in: &myReader{strings.NewReader("foo bar")}, 2592 size: 7, 2593 body: "foo bar", 2594 }) 2595 tests.Add("file", func(t *testing.T) interface{} { 2596 f, err := os.CreateTemp("", "file-reader-*") 2597 if err != nil { 2598 t.Fatal(err) 2599 } 2600 tests.Cleanup(func() { 2601 _ = os.Remove(f.Name()) 2602 }) 2603 if _, err := f.Write([]byte("foo bar")); err != nil { 2604 t.Fatal(err) 2605 } 2606 if _, err := f.Seek(0, 0); err != nil { 2607 t.Fatal(err) 2608 } 2609 return tst{ 2610 in: f, 2611 size: 7, 2612 body: "foo bar", 2613 } 2614 }) 2615 tests.Add("nop closer", tst{ 2616 in: io.NopCloser(strings.NewReader("foo bar")), 2617 size: 7, 2618 body: "foo bar", 2619 }) 2620 tests.Add("seeker", tst{ 2621 in: &seeker{strings.NewReader("asdf asdf")}, 2622 size: 9, 2623 body: "asdf asdf", 2624 }) 2625 tests.Run(t, func(t *testing.T, test tst) { 2626 size, r, err := readerSize(test.in) 2627 if !testy.ErrorMatches(test.err, err) { 2628 t.Errorf("Unexpected error: %s", err) 2629 } 2630 body, err := io.ReadAll(r) 2631 if err != nil { 2632 t.Fatal(err) 2633 } 2634 if d := testy.DiffText(test.body, body); d != nil { 2635 t.Errorf("Unexpected body content:\n%s\n", d) 2636 } 2637 if size != test.size { 2638 t.Errorf("Unexpected size: %d\n", size) 2639 } 2640 }) 2641 } 2642 2643 type seeker struct { 2644 r *strings.Reader 2645 } 2646 2647 func (s *seeker) Read(b []byte) (int, error) { 2648 return s.r.Read(b) 2649 } 2650 2651 func (s *seeker) Seek(offset int64, whence int) (int64, error) { 2652 return s.r.Seek(offset, whence) 2653 } 2654 2655 func (s *seeker) Close() error { return nil } 2656 2657 func TestNewAttachment(t *testing.T) { 2658 type tst struct { 2659 content io.Reader 2660 size []int64 2661 expected *kivik.Attachment 2662 expContent string 2663 err string 2664 } 2665 tests := testy.NewTable() 2666 tests.Add("size provided", tst{ 2667 content: strings.NewReader("xxx"), 2668 size: []int64{99}, 2669 expected: &kivik.Attachment{ 2670 Filename: "foo.txt", 2671 ContentType: "text/plain", 2672 Size: 99, 2673 }, 2674 expContent: "xxx", 2675 }) 2676 tests.Add("strings.NewReader", tst{ 2677 content: strings.NewReader("xxx"), 2678 expected: &kivik.Attachment{ 2679 Filename: "foo.txt", 2680 ContentType: "text/plain", 2681 Size: 3, 2682 }, 2683 expContent: "xxx", 2684 }) 2685 tests.Run(t, func(t *testing.T, test tst) { 2686 result, err := NewAttachment("foo.txt", "text/plain", test.content, test.size...) 2687 if !testy.ErrorMatches(test.err, err) { 2688 t.Errorf("Unexpected error: %s", err) 2689 } 2690 content, err := io.ReadAll(result.Content) 2691 if err != nil { 2692 t.Fatal(err) 2693 } 2694 if d := testy.DiffText(test.expContent, content); d != nil { 2695 t.Errorf("Unexpected content:\n%s\n", d) 2696 } 2697 result.Content = nil 2698 if d := testy.DiffInterface(test.expected, result); d != nil { 2699 t.Error(d) 2700 } 2701 }) 2702 } 2703 2704 func TestCopyWithAttachmentStubs(t *testing.T) { 2705 type tst struct { 2706 input io.Reader 2707 w io.Writer 2708 expected string 2709 atts map[string]*stub 2710 status int 2711 err string 2712 } 2713 tests := testy.NewTable() 2714 tests.Add("no attachments", tst{ 2715 input: strings.NewReader("{}"), 2716 expected: "{}", 2717 }) 2718 tests.Add("Unexpected delim", tst{ 2719 input: strings.NewReader("[]"), 2720 status: http.StatusBadRequest, 2721 err: `^expected '{', found '\['$`, 2722 }) 2723 tests.Add("read error", tst{ 2724 input: testy.ErrorReader("", errors.New("read error")), 2725 status: http.StatusInternalServerError, 2726 err: "^read error$", 2727 }) 2728 tests.Add("write error", tst{ 2729 input: strings.NewReader("{}"), 2730 w: testy.ErrorWriter(0, errors.New("write error")), 2731 status: http.StatusInternalServerError, 2732 err: "^write error$", 2733 }) 2734 tests.Add("decode error", tst{ 2735 input: strings.NewReader("{}}"), 2736 status: http.StatusBadRequest, 2737 err: "^invalid character '}' +looking for beginning of value$", 2738 }) 2739 tests.Add("one attachment", tst{ 2740 input: strings.NewReader(`{"_attachments":{}}`), 2741 atts: map[string]*stub{ 2742 "foo.txt": { 2743 ContentType: "text/plain", 2744 Size: 3, 2745 }, 2746 }, 2747 expected: `{"_attachments":{"foo.txt":{"content_type":"text/plain","length":3,"follows":true}} 2748 }`, 2749 }) 2750 2751 tests.Run(t, func(t *testing.T, test tst) { 2752 w := test.w 2753 if w == nil { 2754 w = &bytes.Buffer{} 2755 } 2756 err := copyWithAttachmentStubs(w, test.input, test.atts) 2757 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" { 2758 t.Error(d) 2759 } 2760 if err != nil { 2761 return 2762 } 2763 if d := testy.DiffText(test.expected, w.(*bytes.Buffer).String()); d != nil { 2764 t.Error(d) 2765 } 2766 }) 2767 } 2768 2769 func TestRevsDiff(t *testing.T) { 2770 type tt struct { 2771 db *db 2772 revMap map[string][]string 2773 status int 2774 err string 2775 } 2776 tests := testy.NewTable() 2777 tests.Add("net error", tt{ 2778 db: newTestDB(nil, errors.New("net error")), 2779 status: http.StatusBadGateway, 2780 err: `Post "?http://example.com/testdb/_revs_diff"?: net error`, 2781 }) 2782 tests.Add("success", tt{ 2783 db: newCustomDB(func(r *http.Request) (*http.Response, error) { 2784 expectedBody := json.RawMessage(`{ 2785 "190f721ca3411be7aa9477db5f948bbb": [ 2786 "3-bb72a7682290f94a985f7afac8b27137", 2787 "4-10265e5a26d807a3cfa459cf1a82ef2e", 2788 "5-067a00dff5e02add41819138abb3284d" 2789 ] 2790 }`) 2791 defer r.Body.Close() // nolint: errcheck 2792 if d := testy.DiffAsJSON(expectedBody, r.Body); d != nil { 2793 return nil, fmt.Errorf("Unexpected payload: %s", d) 2794 } 2795 2796 return &http.Response{ 2797 StatusCode: http.StatusOK, 2798 Body: io.NopCloser(strings.NewReader(`{ 2799 "190f721ca3411be7aa9477db5f948bbb": { 2800 "missing": [ 2801 "3-bb72a7682290f94a985f7afac8b27137", 2802 "5-067a00dff5e02add41819138abb3284d" 2803 ], 2804 "possible_ancestors": [ 2805 "4-10265e5a26d807a3cfa459cf1a82ef2e" 2806 ] 2807 }, 2808 "foo": { 2809 "missing": ["1-xxx"] 2810 } 2811 }`)), 2812 }, nil 2813 }), 2814 revMap: map[string][]string{ 2815 "190f721ca3411be7aa9477db5f948bbb": { 2816 "3-bb72a7682290f94a985f7afac8b27137", 2817 "4-10265e5a26d807a3cfa459cf1a82ef2e", 2818 "5-067a00dff5e02add41819138abb3284d", 2819 }, 2820 }, 2821 }) 2822 2823 tests.Run(t, func(t *testing.T, tt tt) { 2824 ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) 2825 defer cancel() 2826 rows, err := tt.db.RevsDiff(ctx, tt.revMap) 2827 if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" { 2828 t.Error(d) 2829 } 2830 if err != nil { 2831 return 2832 } 2833 results := make(map[string]interface{}) 2834 drow := new(driver.Row) 2835 for { 2836 if err := rows.Next(drow); err != nil { 2837 if err == io.EOF { 2838 break 2839 } 2840 t.Fatal(err) 2841 } 2842 var row interface{} 2843 if err := json.NewDecoder(drow.Value).Decode(&row); err != nil { 2844 t.Fatal(err) 2845 } 2846 results[drow.ID] = row 2847 } 2848 if d := testy.DiffAsJSON(testy.Snapshot(t), results); d != nil { 2849 t.Error(d) 2850 } 2851 }) 2852 }