1
2
3
4
5
6
7
8
9
10
11
12
13 package couchdb
14
15 import (
16 "context"
17 "encoding/json"
18 "errors"
19 "fmt"
20 "io"
21 "net/http"
22 "strings"
23 "testing"
24 "unicode"
25
26 "github.com/google/go-cmp/cmp"
27 "gitlab.com/flimzy/testy"
28
29 kivik "github.com/go-kivik/kivik/v4"
30 "github.com/go-kivik/kivik/v4/driver"
31 internal "github.com/go-kivik/kivik/v4/int/errors"
32 "github.com/go-kivik/kivik/v4/int/mock"
33 )
34
35 func TestBulkGet(t *testing.T) {
36 type tst struct {
37 db *db
38 docs []driver.BulkGetReference
39 options kivik.Option
40 status int
41 err string
42
43 rowStatus int
44 rowErr string
45
46 expected *driver.Row
47 }
48 tests := testy.NewTable()
49 tests.Add("network error", tst{
50 db: &db{
51 client: newTestClient(nil, errors.New("random network error")),
52 },
53 status: http.StatusBadGateway,
54 err: `^Post "?http://example.com/_bulk_get"?: random network error$`,
55 })
56 tests.Add("valid document", tst{
57 db: &db{
58 client: newTestClient(&http.Response{
59 StatusCode: http.StatusOK,
60 ProtoMajor: 1,
61 ProtoMinor: 1,
62 Header: http.Header{
63 "Content-Type": []string{"application/json"},
64 },
65 Body: io.NopCloser(strings.NewReader(removeSpaces(`{
66 "results": [
67 {
68 "id": "foo",
69 "docs": [
70 {
71 "ok": {
72 "_id": "foo",
73 "_rev": "4-753875d51501a6b1883a9d62b4d33f91",
74 "value": "this is foo"
75 }
76 }
77 ]
78 }
79 ]`))),
80 }, nil),
81 dbName: "xxx",
82 },
83 expected: &driver.Row{
84 ID: "foo",
85 Doc: strings.NewReader(`{"_id":"foo","_rev":"4-753875d51501a6b1883a9d62b4d33f91","value":"thisisfoo"}`),
86 },
87 })
88 tests.Add("invalid id", tst{
89 db: &db{
90 client: newTestClient(&http.Response{
91 StatusCode: http.StatusOK,
92 ProtoMajor: 1,
93 ProtoMinor: 1,
94 Body: io.NopCloser(strings.NewReader(`{"results": [{"id": "", "docs": [{"error":{"id":"","rev":null,"error":"illegal_docid","reason":"Document id must not be empty"}}]}]}`)),
95 }, nil),
96 dbName: "xxx",
97 },
98 docs: []driver.BulkGetReference{{ID: ""}},
99 expected: &driver.Row{
100 Error: &bulkGetError{
101 ID: "",
102 Rev: "",
103 Err: "illegal_docid",
104 Reason: "Document id must not be empty",
105 },
106 },
107 })
108 tests.Add("not found", tst{
109 db: &db{
110 client: newTestClient(&http.Response{
111 StatusCode: http.StatusOK,
112 ProtoMajor: 1,
113 ProtoMinor: 1,
114 Body: io.NopCloser(strings.NewReader(`{"results": [{"id": "asdf", "docs": [{"error":{"id":"asdf","rev":"1-xxx","error":"not_found","reason":"missing"}}]}]}`)),
115 }, nil),
116 dbName: "xxx",
117 },
118 docs: []driver.BulkGetReference{{ID: ""}},
119 expected: &driver.Row{
120 ID: "asdf",
121 Error: &bulkGetError{
122 ID: "asdf",
123 Rev: "1-xxx",
124 Err: "not_found",
125 Reason: "missing",
126 },
127 },
128 })
129 tests.Add("revs", tst{
130 db: &db{
131 client: newCustomClient(func(r *http.Request) (*http.Response, error) {
132 revs := r.URL.Query().Get("revs")
133 if revs != "true" {
134 return nil, errors.New("Expected revs=true")
135 }
136 return &http.Response{
137 StatusCode: http.StatusOK,
138 ProtoMajor: 1,
139 ProtoMinor: 1,
140 Body: io.NopCloser(strings.NewReader(`{"results": [{"id": "test1", "docs": [{"ok":{"_id":"test1","_rev":"4-8158177eb5931358b3ddaadd6377cf00","moo":123,"oink":true,"_revisions":{"start":4,"ids":["8158177eb5931358b3ddaadd6377cf00","1c08032eef899e52f35cbd1cd5f93826","e22bea278e8c9e00f3197cb2edee8bf4","7d6ff0b102072755321aa0abb630865a"]},"_attachments":{"foo.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-WiGw80mG3uQuqTKfUnIZsg==","length":9,"stub":true}}}}]}]}`)),
141 }, nil
142 }),
143 dbName: "xxx",
144 },
145 options: kivik.Param("revs", true),
146 expected: &driver.Row{
147 ID: "test1",
148 Doc: strings.NewReader(`{"_id":"test1","_rev":"4-8158177eb5931358b3ddaadd6377cf00","moo":123,"oink":true,"_revisions":{"start":4,"ids":["8158177eb5931358b3ddaadd6377cf00","1c08032eef899e52f35cbd1cd5f93826","e22bea278e8c9e00f3197cb2edee8bf4","7d6ff0b102072755321aa0abb630865a"]},"_attachments":{"foo.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-WiGw80mG3uQuqTKfUnIZsg==","length":9,"stub":true}}}`),
149 },
150 })
151 tests.Add("request", func(t *testing.T) interface{} {
152 return tst{
153 db: &db{
154 client: newCustomClient(func(r *http.Request) (*http.Response, error) {
155 defer r.Body.Close()
156 if d := testy.DiffAsJSON(testy.Snapshot(t), r.Body); d != nil {
157 return nil, fmt.Errorf("Unexpected request: %s", d)
158 }
159 return nil, errors.New("success")
160 }),
161 dbName: "xxx",
162 },
163 docs: []driver.BulkGetReference{
164 {ID: "foo"},
165 {ID: "bar"},
166 },
167 status: 502,
168 err: "success",
169 }
170 })
171
172 tests.Run(t, func(t *testing.T, test tst) {
173 opts := test.options
174 if opts == nil {
175 opts = mock.NilOption
176 }
177 rows, err := test.db.BulkGet(context.Background(), test.docs, opts)
178 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
179 t.Error(d)
180 }
181 if err != nil {
182 return
183 }
184
185 row := new(driver.Row)
186 err = rows.Next(row)
187 t.Cleanup(func() {
188 _ = rows.Close()
189 })
190 if d := internal.StatusErrorDiff(test.rowErr, test.rowStatus, err); d != "" {
191 t.Error(d)
192 }
193
194 if d := rowsDiff(test.expected, row); d != "" {
195 t.Error(d)
196 }
197 })
198 }
199
200 type row struct {
201 ID string
202 Key string
203 Value string
204 Doc string
205 Error string
206 }
207
208 func driverRow2row(r *driver.Row) *row {
209 var value, doc []byte
210 if r.Value != nil {
211 value, _ = io.ReadAll(r.Value)
212 }
213 if r.Doc != nil {
214 doc, _ = io.ReadAll(r.Doc)
215 }
216 var err string
217 if r.Error != nil {
218 err = r.Error.Error()
219 }
220 return &row{
221 ID: r.ID,
222 Key: string(r.Key),
223 Value: string(value),
224 Doc: string(doc),
225 Error: err,
226 }
227 }
228
229 func rowsDiff(got, want *driver.Row) string {
230 return cmp.Diff(driverRow2row(want), driverRow2row(got))
231 }
232
233 var bulkGetInput = `
234 {
235 "results": [
236 {
237 "id": "foo",
238 "docs": [
239 {
240 "ok": {
241 "_id": "foo",
242 "_rev": "4-753875d51501a6b1883a9d62b4d33f91",
243 "value": "this is foo",
244 "_revisions": {
245 "start": 4,
246 "ids": [
247 "753875d51501a6b1883a9d62b4d33f91",
248 "efc54218773c6acd910e2e97fea2a608",
249 "2ee767305024673cfb3f5af037cd2729",
250 "4a7e4ae49c4366eaed8edeaea8f784ad"
251 ]
252 }
253 }
254 }
255 ]
256 },
257 {
258 "id": "foo",
259 "docs": [
260 {
261 "ok": {
262 "_id": "foo",
263 "_rev": "1-4a7e4ae49c4366eaed8edeaea8f784ad",
264 "value": "this is the first revision of foo",
265 "_revisions": {
266 "start": 1,
267 "ids": [
268 "4a7e4ae49c4366eaed8edeaea8f784ad"
269 ]
270 }
271 }
272 }
273 ]
274 },
275 {
276 "id": "bar",
277 "docs": [
278 {
279 "ok": {
280 "_id": "bar",
281 "_rev": "2-9b71d36dfdd9b4815388eb91cc8fb61d",
282 "baz": true,
283 "_revisions": {
284 "start": 2,
285 "ids": [
286 "9b71d36dfdd9b4815388eb91cc8fb61d",
287 "309651b95df56d52658650fb64257b97"
288 ]
289 }
290 }
291 }
292 ]
293 },
294 {
295 "id": "baz",
296 "docs": [
297 {
298 "error": {
299 "id": "baz",
300 "rev": "undefined",
301 "error": "not_found",
302 "reason": "missing"
303 }
304 }
305 ]
306 }
307 ]
308 }
309 `
310
311 func TestGetBulkRowsIterator(t *testing.T) {
312 type result struct {
313 ID string
314 Err string
315 }
316 expected := []result{
317 {ID: "foo"},
318 {ID: "foo"},
319 {ID: "bar"},
320 {ID: "baz", Err: "not_found: missing"},
321 }
322 results := []result{}
323 rows := newBulkGetRows(context.TODO(), io.NopCloser(strings.NewReader(bulkGetInput)))
324 var count int
325 for {
326 row := &driver.Row{}
327 err := rows.Next(row)
328 if err == io.EOF {
329 break
330 }
331 if err != nil {
332 t.Fatalf("Next() failed: %s", err)
333 }
334 results = append(results, result{
335 ID: row.ID,
336 Err: func() string {
337 if row.Error == nil {
338 return ""
339 }
340 return row.Error.Error()
341 }(),
342 })
343 if count++; count > 10 {
344 t.Fatalf("Ran too many iterations.")
345 }
346 }
347 if d := testy.DiffInterface(expected, results); d != nil {
348 t.Error(d)
349 }
350 if expected := 4; count != expected {
351 t.Errorf("Expected %d rows, got %d", expected, count)
352 }
353 if err := rows.Next(&driver.Row{}); err != io.EOF {
354 t.Errorf("Calling Next() after end returned unexpected error: %s", err)
355 }
356 if err := rows.Close(); err != nil {
357 t.Errorf("Error closing rows iterator: %s", err)
358 }
359 }
360
361 func removeSpaces(in string) string {
362 return strings.Map(func(r rune) rune {
363 if unicode.IsSpace(r) {
364 return -1
365 }
366 return r
367 }, in)
368 }
369
370 func TestDecodeBulkResult(t *testing.T) {
371 type tst struct {
372 input string
373 err string
374 expected bulkResult
375 }
376 tests := testy.NewTable()
377 tests.Add("real example", tst{
378 input: removeSpaces(`{
379 "id": "test1",
380 "docs": [
381 {
382 "ok": {
383 "_id": "test1",
384 "_rev": "3-1c08032eef899e52f35cbd1cd5f93826",
385 "moo": 123,
386 "oink": false,
387 "_attachments": {
388 "foo.txt": {
389 "content_type": "text/plain",
390 "revpos": 2,
391 "digest": "md5-WiGw80mG3uQuqTKfUnIZsg==",
392 "length": 9,
393 "stub": true
394 }
395 }
396 }
397 }
398 ]
399 }`),
400 expected: bulkResult{
401 ID: "test1",
402 Docs: []bulkResultDoc{{
403 Doc: json.RawMessage(`{"_id":"test1","_rev":"3-1c08032eef899e52f35cbd1cd5f93826","moo":123,"oink":false,"_attachments":{"foo.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-WiGw80mG3uQuqTKfUnIZsg==","length":9,"stub":true}}}`),
404 }},
405 },
406 })
407
408 tests.Run(t, func(t *testing.T, test tst) {
409 var result bulkResult
410 err := json.Unmarshal([]byte(test.input), &result)
411 if !testy.ErrorMatches(test.err, err) {
412 t.Errorf("Unexpected error: %s", err)
413 }
414 if d := testy.DiffInterface(test.expected, result); d != nil {
415 t.Error(d)
416 }
417 })
418 }
419
View as plain text