...

Source file src/github.com/go-kivik/kivik/v4/couchdb/bulkget_test.go

Documentation: github.com/go-kivik/kivik/v4/couchdb

     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  }
   419  

View as plain text