1package couchdb
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"io/ioutil"
8	"net/http"
9	"strings"
10	"testing"
11
12	"github.com/flimzy/diff"
13	"github.com/flimzy/testy"
14
15	"github.com/go-kivik/kivik"
16	"github.com/go-kivik/kivik/driver"
17	"github.com/go-kivik/kivik/errors"
18)
19
20func TestBulkDocs(t *testing.T) {
21	tests := []struct {
22		name    string
23		db      *db
24		docs    []interface{}
25		options map[string]interface{}
26		status  int
27		err     string
28	}{
29		{
30			name:   "network error",
31			db:     newTestDB(nil, errors.New("net error")),
32			status: kivik.StatusNetworkError,
33			err:    "Post http://example.com/testdb/_bulk_docs: net error",
34		},
35		{
36			name: "JSON encoding error",
37			db: newTestDB(&http.Response{
38				StatusCode: kivik.StatusOK,
39				Body:       ioutil.NopCloser(strings.NewReader("")),
40			}, nil),
41			docs:   []interface{}{make(chan int)},
42			status: kivik.StatusBadRequest,
43			err:    "Post http://example.com/testdb/_bulk_docs: json: unsupported type: chan int",
44		},
45		{
46			name: "docs rejected",
47			db: newTestDB(&http.Response{
48				StatusCode: kivik.StatusExpectationFailed,
49				Body:       ioutil.NopCloser(strings.NewReader("[]")),
50			}, nil),
51			docs:   []interface{}{1, 2, 3},
52			status: kivik.StatusExpectationFailed,
53			err:    "Expectation Failed: one or more document was rejected",
54		},
55		{
56			name: "error response",
57			db: newTestDB(&http.Response{
58				StatusCode: kivik.StatusBadRequest,
59				Body:       ioutil.NopCloser(strings.NewReader("")),
60			}, nil),
61			docs:   []interface{}{1, 2, 3},
62			status: kivik.StatusBadRequest,
63			err:    "Bad Request",
64		},
65		{
66			name: "invalid JSON response",
67			db: newTestDB(&http.Response{
68				StatusCode: kivik.StatusCreated,
69				Body:       ioutil.NopCloser(strings.NewReader("invalid json")),
70			}, nil),
71			docs:   []interface{}{1, 2, 3},
72			status: kivik.StatusBadResponse,
73			err:    "no closing delimiter: invalid character 'i' looking for beginning of value",
74		},
75		{
76			name: "unexpected response code",
77			db: newTestDB(&http.Response{
78				StatusCode: kivik.StatusOK,
79				Body:       ioutil.NopCloser(strings.NewReader("[]")),
80			}, nil),
81			docs: []interface{}{1, 2, 3},
82		},
83		{
84			name:    "new_edits",
85			options: map[string]interface{}{"new_edits": true},
86			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
87				defer req.Body.Close() // nolint: errcheck
88				var body struct {
89					NewEdits bool `json:"new_edits"`
90				}
91				if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
92					return nil, err
93				}
94				if !body.NewEdits {
95					return nil, errors.New("`new_edits` not set")
96				}
97				return &http.Response{
98					StatusCode: kivik.StatusCreated,
99					Body:       ioutil.NopCloser(strings.NewReader("[]")),
100				}, nil
101			}),
102		},
103		{
104			name:    "full commit",
105			options: map[string]interface{}{OptionFullCommit: true},
106			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
107				defer req.Body.Close() // nolint: errcheck
108				var body map[string]interface{}
109				if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
110					return nil, err
111				}
112				if _, ok := body[OptionFullCommit]; ok {
113					return nil, errors.New("Full Commit key found in body")
114				}
115				if value := req.Header.Get("X-Couch-Full-Commit"); value != "true" {
116					return nil, errors.New("X-Couch-Full-Commit not set to true")
117				}
118				return &http.Response{
119					StatusCode: kivik.StatusCreated,
120					Body:       ioutil.NopCloser(strings.NewReader("[]")),
121				}, nil
122			}),
123		},
124		{
125			name:    "invalid full commit type",
126			db:      &db{},
127			options: map[string]interface{}{OptionFullCommit: 123},
128			status:  kivik.StatusBadRequest,
129			err:     "kivik: option 'X-Couch-Full-Commit' must be bool, not int",
130		},
131	}
132	for _, test := range tests {
133		t.Run(test.name, func(t *testing.T) {
134			_, err := test.db.BulkDocs(context.Background(), test.docs, test.options)
135			testy.StatusError(t, test.err, test.status, err)
136		})
137	}
138}
139
140func TestBulkNext(t *testing.T) {
141	tests := []struct {
142		name     string
143		results  *bulkResults
144		status   int
145		err      string
146		expected *driver.BulkResult
147	}{
148		{
149			name: "no results",
150			results: func() *bulkResults {
151				r, err := newBulkResults(Body(`[]`))
152				if err != nil {
153					t.Fatal(err)
154				}
155				return r
156			}(),
157			status: 500,
158			err:    "EOF",
159		},
160		{
161			name: "closing delimiter missing",
162			results: func() *bulkResults {
163				r, err := newBulkResults(Body(`[`))
164				if err != nil {
165					t.Fatal(err)
166				}
167				return r
168			}(),
169			status: kivik.StatusBadResponse,
170			err:    "no closing delimiter: EOF",
171		},
172		{
173			name: "invalid doc json",
174			results: func() *bulkResults {
175				r, err := newBulkResults(Body(`[{foo}]`))
176				if err != nil {
177					t.Fatal(err)
178				}
179				return r
180			}(),
181			status: kivik.StatusBadResponse,
182			err:    "invalid character 'f' looking for beginning of object key string",
183		},
184		{
185			name: "successful update",
186			results: func() *bulkResults {
187				r, err := newBulkResults(Body(`[{"id":"foo","rev":"1-xxx"}]`))
188				if err != nil {
189					t.Fatal(err)
190				}
191				return r
192			}(),
193			expected: &driver.BulkResult{
194				ID:  "foo",
195				Rev: "1-xxx",
196			},
197		},
198		{
199			name: "conflict",
200			results: func() *bulkResults {
201				r, err := newBulkResults(Body(`[{"id":"foo","error":"conflict","reason":"annoying conflict"}]`))
202				if err != nil {
203					t.Fatal(err)
204				}
205				return r
206			}(),
207			expected: &driver.BulkResult{
208				ID:    "foo",
209				Error: errors.Status(kivik.StatusConflict, "annoying conflict"),
210			},
211		},
212		{
213			name: "unknown error",
214			results: func() *bulkResults {
215				r, err := newBulkResults(Body(`[{"id":"foo","error":"foo","reason":"foo is erroneous"}]`))
216				if err != nil {
217					t.Fatal(err)
218				}
219				return r
220			}(),
221			expected: &driver.BulkResult{
222				ID:    "foo",
223				Error: errors.Status(kivik.StatusUnknownError, "foo is erroneous"),
224			},
225		},
226	}
227	for _, test := range tests {
228		t.Run(test.name, func(t *testing.T) {
229			result := new(driver.BulkResult)
230			err := test.results.Next(result)
231			testy.StatusError(t, test.err, test.status, err)
232			if d := diff.Interface(test.expected, result); d != nil {
233				t.Error(d)
234			}
235		})
236	}
237}
238
239type closeTracker struct {
240	closed bool
241	io.ReadCloser
242}
243
244func (c *closeTracker) Close() error {
245	c.closed = true
246	return c.ReadCloser.Close()
247}
248
249func TestBulkClose(t *testing.T) {
250	body := &closeTracker{
251		ReadCloser: Body(`[{"id":"foo","error":"foo","reason":"foo is erroneous"}]`),
252	}
253	r, err := newBulkResults(body)
254	if err != nil {
255		t.Fatal(err)
256	}
257	if e := r.Close(); e != nil {
258		t.Fatal(e)
259	}
260	if !body.closed {
261		t.Errorf("Failed to close")
262	}
263}
264