1
2
3
4
5
6
7
8
9
10
11
12
13 package couchdb
14
15 import (
16 "context"
17 "errors"
18 "fmt"
19 "io"
20 "mime"
21 "net/http"
22 "strings"
23 "testing"
24
25 "gitlab.com/flimzy/testy"
26
27 kivik "github.com/go-kivik/kivik/v4"
28 "github.com/go-kivik/kivik/v4/driver"
29 internal "github.com/go-kivik/kivik/v4/int/errors"
30 "github.com/go-kivik/kivik/v4/int/mock"
31 )
32
33 type closer struct {
34 io.Reader
35 closed bool
36 }
37
38 var _ io.ReadCloser = &closer{}
39
40 func (c *closer) Close() error {
41 c.closed = true
42 return nil
43 }
44
45 func TestPutAttachment(t *testing.T) {
46 type paoTest struct {
47 name string
48 db *db
49 id string
50 att *driver.Attachment
51 options kivik.Option
52
53 newRev string
54 status int
55 err string
56 final func(*testing.T)
57 }
58 tests := []paoTest{
59 {
60 name: "missing docID",
61 status: http.StatusBadRequest,
62 err: "kivik: docID required",
63 },
64 {
65 name: "nil attachment",
66 id: "foo",
67 options: kivik.Rev("1-xxx"),
68 status: http.StatusBadRequest,
69 err: "kivik: att required",
70 },
71 {
72 name: "missing filename",
73 id: "foo",
74 options: kivik.Rev("1-xxx"),
75 att: &driver.Attachment{},
76 status: http.StatusBadRequest,
77 err: "kivik: att.Filename required",
78 },
79 {
80 name: "no body",
81 id: "foo",
82 options: kivik.Rev("1-xxx"),
83 att: &driver.Attachment{
84 Filename: "x.jpg",
85 ContentType: "image/jpeg",
86 },
87 status: http.StatusBadRequest,
88 err: "kivik: att.Content required",
89 },
90 {
91 name: "network error",
92 db: newTestDB(nil, errors.New("net error")),
93 id: "foo",
94 options: kivik.Rev("1-xxx"),
95 att: &driver.Attachment{
96 Filename: "x.jpg",
97 ContentType: "image/jpeg",
98 Content: Body("x"),
99 },
100 status: http.StatusBadGateway,
101 err: `Put "?http://example.com/testdb/foo/x.jpg\?rev=1-xxx"?: net error`,
102 },
103 {
104 name: "1.6.1",
105 id: "foo",
106 options: kivik.Rev("1-4c6114c65e295552ab1019e2b046b10e"),
107 att: &driver.Attachment{
108 Filename: "foo.txt",
109 ContentType: "text/plain",
110 Content: Body("Hello, World!"),
111 },
112 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
113 defer req.Body.Close()
114 if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != "text/plain" {
115 return nil, fmt.Errorf("Unexpected Content-Type: %s", ct)
116 }
117 expectedRev := "1-4c6114c65e295552ab1019e2b046b10e"
118 if rev := req.URL.Query().Get("rev"); rev != expectedRev {
119 return nil, fmt.Errorf("Unexpected rev: %s", rev)
120 }
121 body, err := io.ReadAll(req.Body)
122 if err != nil {
123 return nil, err
124 }
125 expected := "Hello, World!"
126 if d := testy.DiffText(expected, string(body)); d != nil {
127 t.Errorf("Unexpected body:\n%s", d)
128 }
129 return &http.Response{
130 StatusCode: 201,
131 Header: http.Header{
132 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
133 "Location": {"http://localhost:5984/foo/foo/foo.txt"},
134 "ETag": {`"2-8ee3381d24ee4ac3e9f8c1f6c7395641"`},
135 "Date": {"Thu, 26 Oct 2017 20:51:35 GMT"},
136 "Content-Type": {"text/plain; charset=utf-8"},
137 "Content-Length": {"66"},
138 "Cache-Control": {"must-revalidate"},
139 },
140 Body: Body(`{"ok":true,"id":"foo","rev":"2-8ee3381d24ee4ac3e9f8c1f6c7395641"}`),
141 }, nil
142 }),
143 newRev: "2-8ee3381d24ee4ac3e9f8c1f6c7395641",
144 },
145 {
146 name: "no rev",
147 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
148 if _, ok := req.URL.Query()["rev"]; ok {
149 t.Errorf("'rev' should not be present in the query")
150 }
151 return nil, errors.New("ignore this error")
152 }),
153 id: "foo",
154 att: &driver.Attachment{
155 Filename: "foo.txt",
156 ContentType: "text/plain",
157 Content: Body("x"),
158 },
159 status: http.StatusBadGateway,
160 err: `Put "?http://example.com/testdb/foo/foo.txt"?: ignore this error`,
161 },
162 {
163 name: "with options",
164 db: newTestDB(nil, errors.New("success")),
165 id: "foo",
166 att: &driver.Attachment{
167 Filename: "foo.txt",
168 ContentType: "text/plain",
169 Content: Body("x"),
170 },
171 options: kivik.Params(map[string]interface{}{
172 "foo": "oink",
173 "rev": "1-xxx",
174 }),
175 status: http.StatusBadGateway,
176 err: "foo=oink",
177 },
178 {
179 name: "invalid options",
180 db: &db{},
181 id: "foo",
182 att: &driver.Attachment{
183 Filename: "foo.txt",
184 ContentType: "text/plain",
185 Content: Body("x"),
186 },
187 options: kivik.Param("foo", make(chan int)),
188 status: http.StatusBadRequest,
189 err: "kivik: invalid type chan int for options",
190 },
191 {
192 name: "full commit",
193 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
194 if err := consume(req.Body); err != nil {
195 return nil, err
196 }
197 if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" {
198 return nil, errors.New("X-Couch-Full-Commit not true")
199 }
200 return nil, errors.New("success")
201 }),
202 id: "foo",
203 att: &driver.Attachment{
204 Filename: "foo.txt",
205 ContentType: "text/plain",
206 Content: Body("x"),
207 },
208 options: multiOptions{
209 OptionFullCommit(),
210 kivik.Rev("1-xxx"),
211 },
212 status: http.StatusBadGateway,
213 err: "success",
214 },
215 func() paoTest {
216 body := &closer{Reader: strings.NewReader("x")}
217 return paoTest{
218 name: "ReadCloser",
219 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
220 if err := consume(req.Body); err != nil {
221 return nil, err
222 }
223 if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" {
224 return nil, errors.New("X-Couch-Full-Commit not true")
225 }
226 return nil, errors.New("success")
227 }),
228 id: "foo",
229 att: &driver.Attachment{
230 Filename: "foo.txt",
231 ContentType: "text/plain",
232 Content: Body("x"),
233 },
234 options: multiOptions{
235 kivik.Rev("1-xxx"),
236 OptionFullCommit(),
237 },
238 status: http.StatusBadGateway,
239 err: "success",
240 final: func(t *testing.T) {
241 if !body.closed {
242 t.Fatal("body wasn't closed")
243 }
244 },
245 }
246 }(),
247 }
248 for _, test := range tests {
249 t.Run(test.name, func(t *testing.T) {
250 opts := test.options
251 if opts == nil {
252 opts = mock.NilOption
253 }
254 newRev, err := test.db.PutAttachment(context.Background(), test.id, test.att, opts)
255 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
256 t.Error(d)
257 }
258 if err != nil {
259 return
260 }
261 if newRev != test.newRev {
262 t.Errorf("Expected %s, got %s\n", test.newRev, newRev)
263 }
264 if test.final != nil {
265 test.final(t)
266 }
267 })
268 }
269 }
270
271 func TestGetAttachmentMeta(t *testing.T) {
272 tests := []struct {
273 name string
274 db *db
275 id, filename string
276
277 expected *driver.Attachment
278 status int
279 err string
280 }{
281 {
282 name: "network error",
283 id: "foo",
284 filename: "foo.txt",
285 db: newTestDB(nil, errors.New("net error")),
286 status: http.StatusBadGateway,
287 err: `^Head "?http://example.com/testdb/foo/foo.txt"?: net error$`,
288 },
289 {
290 name: "1.6.1",
291 id: "foo",
292 filename: "foo.txt",
293 db: newTestDB(&http.Response{
294 StatusCode: 200,
295 Header: http.Header{
296 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
297 "ETag": {`"gSr8dSmynwAoomH7V6RVYw=="`},
298 "Date": {"Thu, 26 Oct 2017 21:15:13 GMT"},
299 "Content-Type": {"text/plain"},
300 "Content-Length": {"13"},
301 "Cache-Control": {"must-revalidate"},
302 "Accept-Ranges": {"none"},
303 },
304 Body: Body(""),
305 }, nil),
306 expected: &driver.Attachment{
307 ContentType: "text/plain",
308 Digest: "gSr8dSmynwAoomH7V6RVYw==",
309 Content: Body(""),
310 },
311 },
312 }
313 for _, test := range tests {
314 t.Run(test.name, func(t *testing.T) {
315 att, err := test.db.GetAttachmentMeta(context.Background(), test.id, test.filename, mock.NilOption)
316 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
317 t.Error(d)
318 }
319 if d := testy.DiffInterface(test.expected, att); d != nil {
320 t.Errorf("Unexpected attachment:\n%s", d)
321 }
322 })
323 }
324 }
325
326 func TestGetDigest(t *testing.T) {
327 tests := []struct {
328 name string
329 resp *http.Response
330 expected string
331 status int
332 err string
333 }{
334 {
335 name: "no etag header",
336 resp: &http.Response{},
337 status: http.StatusBadGateway,
338 err: "ETag header not found",
339 },
340 {
341 name: "Standard ETag header",
342 resp: &http.Response{
343 Header: http.Header{"ETag": []string{`"ENGoH7oK8V9R3BMnfDHZmw=="`}},
344 },
345 expected: "ENGoH7oK8V9R3BMnfDHZmw==",
346 },
347 {
348 name: "normalized Etag header",
349 resp: &http.Response{
350 Header: http.Header{"Etag": []string{`"ENGoH7oK8V9R3BMnfDHZmw=="`}},
351 },
352 expected: "ENGoH7oK8V9R3BMnfDHZmw==",
353 },
354 }
355 for _, test := range tests {
356 t.Run(test.name, func(t *testing.T) {
357 digest, err := getDigest(test.resp)
358 if !testy.ErrorMatches(test.err, err) {
359 t.Errorf("Unexpected error: %s", err)
360 }
361 if digest != test.expected {
362 t.Errorf("Unexpected result: %0x", digest)
363 }
364 })
365 }
366 }
367
368 func TestGetAttachment(t *testing.T) {
369 tests := []struct {
370 name string
371 db *db
372 id, filename string
373 options kivik.Option
374
375 expected *driver.Attachment
376 content string
377 status int
378 err string
379 }{
380 {
381 name: "network error",
382 id: "foo",
383 filename: "foo.txt",
384 db: newTestDB(nil, errors.New("net error")),
385 status: http.StatusBadGateway,
386 err: `Get "?http://example.com/testdb/foo/foo.txt"?: net error`,
387 },
388 {
389 name: "1.6.1",
390 id: "foo",
391 filename: "foo.txt",
392 db: newCustomDB(func(*http.Request) (*http.Response, error) {
393 return &http.Response{
394 StatusCode: 200,
395 Header: http.Header{
396 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
397 "ETag": {`"gSr8dSmynwAoomH7V6RVYw=="`},
398 "Date": {"Fri, 27 Oct 2017 11:24:50 GMT"},
399 "Content-Type": {"text/plain"},
400 "Content-Length": {"13"},
401 "Cache-Control": {"must-revalidate"},
402 "Accept-Ranges": {"none"},
403 },
404 Body: Body(`Hello, world!`),
405 }, nil
406 }),
407 expected: &driver.Attachment{
408 ContentType: "text/plain",
409 Digest: "gSr8dSmynwAoomH7V6RVYw==",
410 },
411 content: "Hello, world!",
412 },
413 }
414 for _, test := range tests {
415 t.Run(test.name, func(t *testing.T) {
416 opts := test.options
417 if opts == nil {
418 opts = mock.NilOption
419 }
420 att, err := test.db.GetAttachment(context.Background(), test.id, test.filename, opts)
421 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
422 t.Error(d)
423 }
424 if err != nil {
425 return
426 }
427 fileContent, err := io.ReadAll(att.Content)
428 if err != nil {
429 t.Fatal(err)
430 }
431 if d := testy.DiffText(test.content, string(fileContent)); d != nil {
432 t.Errorf("Unexpected content:\n%s", d)
433 }
434 _ = att.Content.Close()
435 att.Content = nil
436 if d := testy.DiffInterface(test.expected, att); d != nil {
437 t.Errorf("Unexpected attachment:\n%s", d)
438 }
439 })
440 }
441 }
442
443 func TestFetchAttachment(t *testing.T) {
444 tests := []struct {
445 name string
446 db *db
447 method, id, filename string
448 options kivik.Option
449
450 resp *http.Response
451 status int
452 err string
453 }{
454 {
455 name: "no method",
456 status: http.StatusInternalServerError,
457 err: "method required",
458 },
459 {
460 name: "no docID",
461 method: "GET",
462 status: http.StatusBadRequest,
463 err: "kivik: docID required",
464 },
465 {
466 name: "no filename",
467 method: "GET",
468 id: "foo",
469 status: http.StatusBadRequest,
470 err: "kivik: filename required",
471 },
472 {
473 name: "no rev",
474 method: "GET",
475 id: "foo",
476 filename: "foo.txt",
477 db: newTestDB(nil, errors.New("ignore this error")),
478 status: http.StatusBadGateway,
479 err: "http://example.com/testdb/foo/foo.txt",
480 },
481 {
482 name: "success",
483 method: "GET",
484 id: "foo",
485 filename: "foo.txt",
486 db: newTestDB(&http.Response{
487 StatusCode: 200,
488 }, nil),
489 resp: &http.Response{
490 StatusCode: 200,
491 },
492 },
493 {
494 name: "options",
495 db: newTestDB(nil, errors.New("success")),
496 method: "GET",
497 id: "foo",
498 filename: "foo.txt",
499 options: kivik.Param("foo", "bar"),
500 status: http.StatusBadGateway,
501 err: "foo=bar",
502 },
503 {
504 name: "invalid option",
505 db: &db{},
506 method: "GET",
507 id: "foo",
508 filename: "foo.txt",
509 options: kivik.Param("foo", make(chan int)),
510 status: http.StatusBadRequest,
511 err: "kivik: invalid type chan int for options",
512 },
513 {
514 name: "If-None-Match",
515 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
516 if err := consume(req.Body); err != nil {
517 return nil, err
518 }
519 if inm := req.Header.Get("If-None-Match"); inm != `"foo"` {
520 return nil, fmt.Errorf(`If-None-Match: %s != "foo"`, inm)
521 }
522 return nil, errors.New("success")
523 }),
524 method: "GET",
525 id: "foo",
526 filename: "foo.txt",
527 options: OptionIfNoneMatch("foo"),
528 status: http.StatusBadGateway,
529 err: "success",
530 },
531 }
532 for _, test := range tests {
533 t.Run(test.name, func(t *testing.T) {
534 opts := test.options
535 if opts == nil {
536 opts = mock.NilOption
537 }
538 resp, err := test.db.fetchAttachment(context.Background(), test.method, test.id, test.filename, opts)
539 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
540 t.Error(d)
541 }
542 if err != nil {
543 return
544 }
545 if d := testy.DiffJSON(test.resp.Body, resp.Body); d != nil {
546 t.Errorf("Response body: %s", d)
547 }
548
549 resp.Request = nil
550 resp.Body = nil
551 test.resp.Body = nil
552
553 if d := testy.DiffInterface(test.resp, resp); d != nil {
554 t.Error(d)
555 }
556 })
557 }
558 }
559
560 func TestDecodeAttachment(t *testing.T) {
561 tests := []struct {
562 name string
563 resp *http.Response
564 expected *driver.Attachment
565 content string
566 status int
567 err string
568 }{
569 {
570 name: "no content type",
571 resp: &http.Response{},
572 status: http.StatusBadGateway,
573 err: "no Content-Type in response",
574 },
575 {
576 name: "no etag header",
577 resp: &http.Response{
578 Header: http.Header{"Content-Type": {"text/plain"}},
579 },
580 status: http.StatusBadGateway,
581 err: "ETag header not found",
582 },
583 {
584 name: "success",
585 resp: &http.Response{
586 Header: http.Header{
587 "Content-Type": {"text/plain"},
588 "ETag": {`"gSr8dSmynwAoomH7V6RVYw=="`},
589 },
590 Body: Body("Hello, World!"),
591 },
592 expected: &driver.Attachment{
593 ContentType: "text/plain",
594 Digest: "gSr8dSmynwAoomH7V6RVYw==",
595 },
596 content: "Hello, World!",
597 },
598 }
599 for _, test := range tests {
600 t.Run(test.name, func(t *testing.T) {
601 att, err := decodeAttachment(test.resp)
602 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
603 t.Error(d)
604 }
605 if err != nil {
606 return
607 }
608 fileContent, err := io.ReadAll(att.Content)
609 if err != nil {
610 t.Fatal(err)
611 }
612 if d := testy.DiffText(test.content, string(fileContent)); d != nil {
613 t.Errorf("Unexpected content:\n%s", d)
614 }
615 _ = att.Content.Close()
616 att.Content = nil
617 if d := testy.DiffInterface(test.expected, att); d != nil {
618 t.Errorf("Unexpected attachment:\n%s", d)
619 }
620 })
621 }
622 }
623
624 func TestDeleteAttachment(t *testing.T) {
625 tests := []struct {
626 name string
627 db *db
628 id, filename string
629 options kivik.Option
630
631 newRev string
632 status int
633 err string
634 }{
635 {
636 name: "no doc id",
637 status: http.StatusBadRequest,
638 err: "kivik: docID required",
639 },
640 {
641 name: "no rev",
642 id: "foo",
643 status: http.StatusBadRequest,
644 err: "kivik: rev required",
645 },
646 {
647 name: "no filename",
648 id: "foo",
649 options: kivik.Rev("1-xxx"),
650 status: http.StatusBadRequest,
651 err: "kivik: filename required",
652 },
653 {
654 name: "network error",
655 id: "foo",
656 options: kivik.Rev("1-xxx"),
657 filename: "foo.txt",
658 db: newTestDB(nil, errors.New("net error")),
659 status: http.StatusBadGateway,
660 err: `(Delete "?http://example.com/testdb/foo/foo.txt\\?rev=1-xxx"?: )?net error`,
661 },
662 {
663 name: "success 1.6.1",
664 id: "foo",
665 options: kivik.Rev("2-8ee3381d24ee4ac3e9f8c1f6c7395641"),
666 filename: "foo.txt",
667 db: newTestDB(&http.Response{
668 StatusCode: 200,
669 Header: http.Header{
670 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
671 "ETag": {`"3-231a932924f61816915289fecd35b14a"`},
672 "Date": {"Fri, 27 Oct 2017 13:30:40 GMT"},
673 "Content-Type": {"text/plain; charset=utf-8"},
674 "Content-Length": {"66"},
675 "Cache-Control": {"must-revalidate"},
676 },
677 Body: Body(`{"ok":true,"id":"foo","rev":"3-231a932924f61816915289fecd35b14a"}`),
678 }, nil),
679 newRev: "3-231a932924f61816915289fecd35b14a",
680 },
681 {
682 name: "with options",
683 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
684 if err := consume(req.Body); err != nil {
685 return nil, err
686 }
687 if foo := req.URL.Query().Get("foo"); foo != "oink" {
688 return nil, fmt.Errorf("Unexpected query foo=%s", foo)
689 }
690 return nil, errors.New("success")
691 }),
692 id: "foo",
693 filename: "foo.txt",
694 options: kivik.Params(map[string]interface{}{
695 "rev": "1-xxx",
696 "foo": "oink",
697 }),
698 status: http.StatusBadGateway,
699 err: "success",
700 },
701 {
702 name: "invalid option",
703 db: &db{},
704 id: "foo",
705 filename: "foo.txt",
706 options: kivik.Params(map[string]interface{}{
707 "rev": "1-xxx",
708 "foo": make(chan int),
709 }),
710 status: http.StatusBadRequest,
711 err: "kivik: invalid type chan int for options",
712 },
713 {
714 name: "full commit",
715 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
716 if err := consume(req.Body); err != nil {
717 return nil, err
718 }
719 if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" {
720 return nil, errors.New("X-Couch-Full-Commit not true")
721 }
722 return nil, errors.New("success")
723 }),
724 id: "foo",
725 filename: "foo.txt",
726 options: multiOptions{
727 kivik.Rev("1-xxx"),
728 OptionFullCommit(),
729 },
730 status: http.StatusBadGateway,
731 err: "success",
732 },
733 }
734 for _, test := range tests {
735 t.Run(test.name, func(t *testing.T) {
736 opts := test.options
737 if opts == nil {
738 opts = mock.NilOption
739 }
740 newRev, err := test.db.DeleteAttachment(context.Background(), test.id, test.filename, opts)
741 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
742 t.Error(d)
743 }
744 if newRev != test.newRev {
745 t.Errorf("Unexpected new rev: %s", newRev)
746 }
747 })
748 }
749 }
750
View as plain text