github.com/go-kivik/kivik/v4@v4.3.2/couchdb/bulkget_test.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 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() // nolint:errcheck 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 }