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 "net/http"
21 "net/url"
22 "testing"
23
24 "github.com/google/go-cmp/cmp"
25 "gitlab.com/flimzy/testy"
26
27 kivik "github.com/go-kivik/kivik/v4"
28 "github.com/go-kivik/kivik/v4/driver"
29 internal "github.com/go-kivik/kivik/v4/int/errors"
30 "github.com/go-kivik/kivik/v4/int/mock"
31 )
32
33 func TestAllDBs(t *testing.T) {
34 tests := []struct {
35 name string
36 client *client
37 options kivik.Option
38 expected []string
39 status int
40 err string
41 }{
42 {
43 name: "network error",
44 client: newTestClient(nil, errors.New("net error")),
45 status: http.StatusBadGateway,
46 err: `Get "?http://example.com/_all_dbs"?: net error`,
47 },
48 {
49 name: "2.0.0",
50 client: newTestClient(&http.Response{
51 StatusCode: 200,
52 Header: http.Header{
53 "Server": {"CouchDB/2.0.0 (Erlang OTP/17)"},
54 "Date": {"Fri, 27 Oct 2017 15:15:07 GMT"},
55 "Content-Type": {"application/json"},
56 "ETag": {`"33UVNAZU752CYNGBBTMWQFP7U"`},
57 "Transfer-Encoding": {"chunked"},
58 "X-Couch-Request-ID": {"ab5cd97c3e"},
59 "X-CouchDB-Body-Time": {"0"},
60 },
61 Body: Body(`["_global_changes","_replicator","_users"]`),
62 }, nil),
63 expected: []string{"_global_changes", "_replicator", "_users"},
64 },
65 }
66 for _, test := range tests {
67 t.Run(test.name, func(t *testing.T) {
68 opts := test.options
69 if opts == nil {
70 opts = mock.NilOption
71 }
72 result, err := test.client.AllDBs(context.Background(), opts)
73 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
74 t.Error(d)
75 }
76 if d := testy.DiffInterface(test.expected, result); d != nil {
77 t.Error(d)
78 }
79 })
80 }
81 }
82
83 func TestDBExists(t *testing.T) {
84 tests := []struct {
85 name string
86 client *client
87 dbName string
88 exists bool
89 status int
90 err string
91 }{
92 {
93 name: "no db specified",
94 status: http.StatusBadRequest,
95 err: "kivik: dbName required",
96 },
97 {
98 name: "network error",
99 dbName: "foo",
100 client: newTestClient(nil, errors.New("net error")),
101 status: http.StatusBadGateway,
102 err: `Head "?http://example.com/foo"?: net error`,
103 },
104 {
105 name: "not found, 1.6.1",
106 dbName: "foox",
107 client: newTestClient(&http.Response{
108 StatusCode: 404,
109 Header: http.Header{
110 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
111 "Date": {"Fri, 27 Oct 2017 15:09:19 GMT"},
112 "Content-Type": {"text/plain; charset=utf-8"},
113 "Content-Length": {"44"},
114 "Cache-Control": {"must-revalidate"},
115 },
116 Body: Body(""),
117 }, nil),
118 exists: false,
119 },
120 {
121 name: "exists, 1.6.1",
122 dbName: "foo",
123 client: newTestClient(&http.Response{
124 StatusCode: 200,
125 Header: http.Header{
126 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
127 "Date": {"Fri, 27 Oct 2017 15:09:19 GMT"},
128 "Content-Type": {"text/plain; charset=utf-8"},
129 "Content-Length": {"229"},
130 "Cache-Control": {"must-revalidate"},
131 },
132 Body: Body(""),
133 }, nil),
134 exists: true,
135 },
136 {
137 name: "slashes",
138 dbName: "foo/bar",
139 client: newCustomClient(func(req *http.Request) (*http.Response, error) {
140 if err := consume(req.Body); err != nil {
141 return nil, err
142 }
143 expected := "/" + url.PathEscape("foo/bar")
144 actual := req.URL.RawPath
145 if actual != expected {
146 return nil, fmt.Errorf("expected path %s, got %s", expected, actual)
147 }
148 response := &http.Response{
149 StatusCode: 200,
150 Header: http.Header{
151 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
152 "Date": {"Fri, 27 Oct 2017 15:09:19 GMT"},
153 "Content-Type": {"text/plain; charset=utf-8"},
154 "Content-Length": {"229"},
155 "Cache-Control": {"must-revalidate"},
156 },
157 Body: Body(""),
158 }
159 response.Request = req
160 return response, nil
161 }),
162 exists: true,
163 },
164 }
165 for _, test := range tests {
166 t.Run(test.name, func(t *testing.T) {
167 exists, err := test.client.DBExists(context.Background(), test.dbName, nil)
168 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
169 t.Error(d)
170 }
171 if exists != test.exists {
172 t.Errorf("Unexpected result: %t", exists)
173 }
174 })
175 }
176 }
177
178 func TestCreateDB(t *testing.T) {
179 tests := []struct {
180 name string
181 dbName string
182 options kivik.Option
183 client *client
184 status int
185 err string
186 }{
187 {
188 name: "missing dbname",
189 status: http.StatusBadRequest,
190 err: "kivik: dbName required",
191 },
192 {
193 name: "network error",
194 dbName: "foo",
195 client: newTestClient(nil, errors.New("net error")),
196 status: http.StatusBadGateway,
197 err: `Put "?http://example.com/foo"?: net error`,
198 },
199 {
200 name: "conflict 1.6.1",
201 dbName: "foo",
202 client: newTestClient(&http.Response{
203 StatusCode: 412,
204 Header: http.Header{
205 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
206 "Date": {"Fri, 27 Oct 2017 15:23:57 GMT"},
207 "Content-Type": {"application/json"},
208 "Content-Length": {"94"},
209 "Cache-Control": {"must-revalidate"},
210 },
211 ContentLength: 94,
212 Body: Body(`{"error":"file_exists","reason":"The database could not be created, the file already exists."}`),
213 }, nil),
214 status: http.StatusPreconditionFailed,
215 err: "Precondition Failed: The database could not be created, the file already exists.",
216 },
217 }
218 for _, test := range tests {
219 t.Run(test.name, func(t *testing.T) {
220 opts := test.options
221 if opts == nil {
222 opts = mock.NilOption
223 }
224 err := test.client.CreateDB(context.Background(), test.dbName, opts)
225 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
226 t.Error(d)
227 }
228 })
229 }
230 }
231
232 func TestDestroyDB(t *testing.T) {
233 tests := []struct {
234 name string
235 client *client
236 dbName string
237 status int
238 err string
239 }{
240 {
241 name: "no db name",
242 status: http.StatusBadRequest,
243 err: "kivik: dbName required",
244 },
245 {
246 name: "network error",
247 dbName: "foo",
248 client: newTestClient(nil, errors.New("net error")),
249 status: http.StatusBadGateway,
250 err: `(Delete "?http://example.com/foo"?: )?net error`,
251 },
252 {
253 name: "1.6.1",
254 dbName: "foo",
255 client: newTestClient(&http.Response{
256 StatusCode: 200,
257 Header: http.Header{
258 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
259 "Date": {"Fri, 27 Oct 2017 17:12:45 GMT"},
260 "Content-Type": {"application/json"},
261 "Content-Length": {"12"},
262 "Cache-Control": {"must-revalidate"},
263 },
264 Body: Body(`{"ok":true}`),
265 }, nil),
266 },
267 }
268 for _, test := range tests {
269 t.Run(test.name, func(t *testing.T) {
270 err := test.client.DestroyDB(context.Background(), test.dbName, nil)
271 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
272 t.Error(d)
273 }
274 })
275 }
276 }
277
278 func TestDBUpdates(t *testing.T) {
279 tests := []struct {
280 name string
281 client *client
282 options driver.Options
283 want []driver.DBUpdate
284 wantStatus int
285 wantErr string
286 }{
287 {
288 name: "network error",
289 client: newTestClient(nil, errors.New("net error")),
290 wantStatus: http.StatusBadGateway,
291 wantErr: `Get "?http://example.com/_db_updates\?feed=continuous&since=now"?: net error`,
292 },
293 {
294 name: "CouchDB defaults, network error",
295 options: kivik.Params(map[string]interface{}{
296 "feed": "",
297 "since": "",
298 }),
299 client: newTestClient(nil, errors.New("net error")),
300 wantStatus: http.StatusBadGateway,
301 wantErr: `Get "?http://example.com/_db_updates"?: net error`,
302 },
303 {
304 name: "error response",
305 client: newTestClient(&http.Response{
306 StatusCode: 400,
307 Body: Body(""),
308 }, nil),
309 wantStatus: http.StatusBadRequest,
310 wantErr: "Bad Request",
311 },
312 {
313 name: "Success 1.6.1",
314 client: newTestClient(&http.Response{
315 StatusCode: 200,
316 Header: http.Header{
317 "Transfer-Encoding": {"chunked"},
318 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
319 "Date": {"Fri, 27 Oct 2017 19:55:43 GMT"},
320 "Content-Type": {"application/json"},
321 "Cache-Control": {"must-revalidate"},
322 },
323 Body: Body(`{"db_name":"mailbox","type":"created","seq":"1-g1AAAAFR"}
324 {"db_name":"mailbox","type":"deleted","seq":"2-g1AAAAFR"}`),
325 }, nil),
326 want: []driver.DBUpdate{
327 {DBName: "mailbox", Type: "created", Seq: "1-g1AAAAFR"},
328 {DBName: "mailbox", Type: "deleted", Seq: "2-g1AAAAFR"},
329 },
330 },
331 {
332 name: "non-JSON response",
333 client: newTestClient(&http.Response{
334 StatusCode: 200,
335 Header: http.Header{
336 "Transfer-Encoding": {"chunked"},
337 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
338 "Date": {"Fri, 27 Oct 2017 19:55:43 GMT"},
339 "Content-Type": {"application/json"},
340 "Cache-Control": {"must-revalidate"},
341 },
342 Body: Body(`invalid json`),
343 }, nil),
344 wantStatus: http.StatusBadGateway,
345 wantErr: `invalid character 'i' looking for beginning of value`,
346 },
347 {
348 name: "wrong opening JSON token",
349 client: newTestClient(&http.Response{
350 StatusCode: 200,
351 Header: http.Header{
352 "Transfer-Encoding": {"chunked"},
353 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
354 "Date": {"Fri, 27 Oct 2017 19:55:43 GMT"},
355 "Content-Type": {"application/json"},
356 "Cache-Control": {"must-revalidate"},
357 },
358 Body: Body(`[]`),
359 }, nil),
360 wantStatus: http.StatusBadGateway,
361 wantErr: "expected `{`",
362 },
363 {
364 name: "wrong second JSON token type",
365 client: newTestClient(&http.Response{
366 StatusCode: 200,
367 Header: http.Header{
368 "Transfer-Encoding": {"chunked"},
369 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
370 "Date": {"Fri, 27 Oct 2017 19:55:43 GMT"},
371 "Content-Type": {"application/json"},
372 "Cache-Control": {"must-revalidate"},
373 },
374 Body: Body(`{"foo":"bar"}`),
375 }, nil),
376 wantStatus: http.StatusBadGateway,
377 wantErr: "expected `db_name` or `results`",
378 },
379 {
380 name: "CouchDB defaults",
381 client: newTestClient(&http.Response{
382 StatusCode: 200,
383 Header: http.Header{
384 "Transfer-Encoding": {"chunked"},
385 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
386 "Date": {"Fri, 27 Oct 2017 19:55:43 GMT"},
387 "Content-Type": {"application/json"},
388 "Cache-Control": {"must-revalidate"},
389 },
390 Body: Body(`{
391 "results":[
392 {"db_name":"mailbox","type":"created","seq":"1-g1AAAAFR"},
393 {"db_name":"mailbox","type":"deleted","seq":"2-g1AAAAFR"}
394 ],
395 "last_seq": "2-g1AAAAFR"
396 }`),
397 }, nil),
398 options: kivik.Params(map[string]interface{}{
399 "feed": "",
400 "since": "",
401 }),
402 want: []driver.DBUpdate{
403 {DBName: "mailbox", Type: "created", Seq: "1-g1AAAAFR"},
404 {DBName: "mailbox", Type: "deleted", Seq: "2-g1AAAAFR"},
405 },
406 },
407 {
408 name: "eventsource",
409 options: kivik.Params(map[string]interface{}{
410 "feed": "eventsource",
411 "since": "",
412 }),
413 wantStatus: http.StatusBadRequest,
414 wantErr: "eventsource feed type not supported",
415 },
416 {
417
418
419 name: "no databases",
420 client: newTestClient(&http.Response{
421 StatusCode: 200,
422 Header: http.Header{
423 "Content-Type": {"application/json"},
424 },
425 Body: Body(`{"last_seq":"38-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5UTgXKMBuZmFmYWFgjq4Yh_Y8FiDJ0ACk_qOYYpyanGiQYoquJwsAM_UqgA"}`),
426 }, nil),
427 },
428 }
429 for _, tt := range tests {
430 t.Run(tt.name, func(t *testing.T) {
431 opts := tt.options
432 if opts == nil {
433 opts = mock.NilOption
434 }
435 result, err := tt.client.DBUpdates(context.TODO(), opts)
436 if d := internal.StatusErrorDiffRE(tt.wantErr, tt.wantStatus, err); d != "" {
437 t.Error(d)
438 }
439 if err != nil {
440 return
441 }
442
443 var got []driver.DBUpdate
444 for {
445 var update driver.DBUpdate
446 err := result.Next(&update)
447 if err == io.EOF {
448 break
449 }
450 if err != nil {
451 t.Fatal(err)
452 }
453 got = append(got, update)
454 }
455 if d := cmp.Diff(tt.want, got); d != "" {
456 t.Errorf("Unexpected result:\n%s\n", d)
457 }
458 })
459 }
460 }
461
462 func newTestUpdates(t *testing.T, body io.ReadCloser) driver.DBUpdates {
463 t.Helper()
464 u, err := newUpdates(context.Background(), body)
465 if err != nil {
466 t.Fatal(err)
467 }
468 return u
469 }
470
471 func TestUpdatesNext(t *testing.T) {
472 t.Parallel()
473 tests := []struct {
474 name string
475 updates driver.DBUpdates
476 status int
477 err string
478 expected *driver.DBUpdate
479 }{
480 {
481 name: "consumed feed",
482 updates: newContinuousUpdates(context.TODO(), Body("")),
483 expected: &driver.DBUpdate{},
484 status: http.StatusInternalServerError,
485 err: "EOF",
486 },
487 {
488 name: "read feed",
489 updates: newTestUpdates(t, Body(`{"db_name":"mailbox","type":"created","seq":"1-g1AAAAFReJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjMlMiTJ____PyuDOZExFyjAnmJhkWaeaIquGIf2JAUgmWQPMiGRAZcaB5CaePxqEkBq6vGqyWMBkgwNQAqobD4h"},`)),
490 expected: &driver.DBUpdate{
491 DBName: "mailbox",
492 Type: "created",
493 Seq: "1-g1AAAAFReJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjMlMiTJ____PyuDOZExFyjAnmJhkWaeaIquGIf2JAUgmWQPMiGRAZcaB5CaePxqEkBq6vGqyWMBkgwNQAqobD4h",
494 },
495 },
496 }
497 for _, test := range tests {
498 t.Run(test.name, func(t *testing.T) {
499 result := new(driver.DBUpdate)
500 err := test.updates.Next(result)
501 if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
502 t.Error(d)
503 }
504 if d := testy.DiffInterface(test.expected, result); d != nil {
505 t.Error(d)
506 }
507 })
508 }
509 }
510
511 func TestUpdatesClose(t *testing.T) {
512 t.Parallel()
513 body := &closeTracker{ReadCloser: Body("")}
514 u := newContinuousUpdates(context.TODO(), body)
515 if err := u.Close(); err != nil {
516 t.Fatal(err)
517 }
518 if !body.closed {
519 t.Errorf("Failed to close")
520 }
521 }
522
523 func TestUpdatesLastSeq(t *testing.T) {
524 t.Parallel()
525
526 client := newTestClient(&http.Response{
527 StatusCode: 200,
528 Header: http.Header{
529 "Transfer-Encoding": {"chunked"},
530 "Server": {"CouchDB/1.6.1 (Erlang OTP/17)"},
531 "Date": {"Fri, 27 Oct 2017 19:55:43 GMT"},
532 "Content-Type": {"application/json"},
533 "Cache-Control": {"must-revalidate"},
534 },
535 Body: Body(`{"results":[],"last_seq":"99-asdf"}`),
536 }, nil)
537
538 updates, err := client.DBUpdates(context.TODO(), mock.NilOption)
539 if err != nil {
540 t.Fatal(err)
541 }
542 for {
543 err := updates.Next(&driver.DBUpdate{})
544 if err == io.EOF {
545 break
546 }
547 if err != nil {
548 t.Fatal(err)
549 }
550
551 }
552 want := "99-asdf"
553 got, err := updates.(driver.LastSeqer).LastSeq()
554 if err != nil {
555 t.Fatal(err)
556 }
557 if got != want {
558 t.Errorf("Unexpected last_seq: %s", got)
559 }
560 }
561
562 func TestPing(t *testing.T) {
563 type pingTest struct {
564 name string
565 client *client
566 expected bool
567 status int
568 err string
569 }
570
571 tests := []pingTest{
572 {
573 name: "Couch 1.6",
574 client: newTestClient(&http.Response{
575 StatusCode: http.StatusBadRequest,
576 ProtoMajor: 1,
577 ProtoMinor: 1,
578 Header: http.Header{
579 "Server": []string{"CouchDB/1.6.1 (Erlang OTP/17)"},
580 },
581 }, nil),
582 expected: true,
583 },
584 {
585 name: "Couch 2.x offline",
586 client: newTestClient(&http.Response{
587 StatusCode: http.StatusNotFound,
588 ProtoMajor: 1,
589 ProtoMinor: 1,
590 }, nil),
591 expected: false,
592 },
593 {
594 name: "Couch 2.x online",
595 client: newTestClient(&http.Response{
596 StatusCode: http.StatusOK,
597 ProtoMajor: 1,
598 ProtoMinor: 1,
599 }, nil),
600 expected: true,
601 },
602 {
603 name: "network error",
604 client: newTestClient(nil, errors.New("network error")),
605 expected: false,
606 status: http.StatusBadGateway,
607 err: `Head "?http://example.com/_up"?: network error`,
608 },
609 }
610
611 for _, test := range tests {
612 t.Run(test.name, func(t *testing.T) {
613 result, err := test.client.Ping(context.Background())
614 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
615 t.Error(d)
616 }
617 if result != test.expected {
618 t.Errorf("Unexpected result: %t", result)
619 }
620 })
621 }
622 }
623
View as plain text