1
2
3
4
5
6
7
8
9
10
11
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
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
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
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
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
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()
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()
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()
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()
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
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()
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()
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 }
2853
View as plain text