...

Source file src/github.com/go-kivik/kivik/v4/couchdb/db_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  	"bytes"
    17  	"context"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"mime"
    23  	"mime/multipart"
    24  	"net/http"
    25  	"net/url"
    26  	"os"
    27  	"strings"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/google/go-cmp/cmp"
    32  	"gitlab.com/flimzy/testy"
    33  
    34  	kivik "github.com/go-kivik/kivik/v4"
    35  	"github.com/go-kivik/kivik/v4/couchdb/chttp"
    36  	"github.com/go-kivik/kivik/v4/driver"
    37  	internal "github.com/go-kivik/kivik/v4/int/errors"
    38  	"github.com/go-kivik/kivik/v4/int/mock"
    39  )
    40  
    41  func TestAllDocs(t *testing.T) {
    42  	t.Run("standard", func(t *testing.T) {
    43  		db := newTestDB(nil, errors.New("test error"))
    44  		_, err := db.AllDocs(context.Background(), mock.NilOption)
    45  		if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_all_docs"?: test error`, err) {
    46  			t.Errorf("Unexpected error: %s", err)
    47  		}
    48  	})
    49  
    50  	t.Run("partitioned", func(t *testing.T) {
    51  		db := newTestDB(nil, errors.New("test error"))
    52  		_, err := db.AllDocs(context.Background(), OptionPartition("a1"))
    53  		if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_partition/a1/_all_docs"?: test error`, err) {
    54  			t.Errorf("Unexpected error: %s", err)
    55  		}
    56  	})
    57  }
    58  
    59  func TestDesignDocs(t *testing.T) {
    60  	db := newTestDB(nil, errors.New("test error"))
    61  	_, err := db.DesignDocs(context.Background(), mock.NilOption)
    62  	if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_design_docs"?: test error`, err) {
    63  		t.Errorf("Unexpected error: %s", err)
    64  	}
    65  }
    66  
    67  func TestLocalDocs(t *testing.T) {
    68  	db := newTestDB(nil, errors.New("test error"))
    69  	_, err := db.LocalDocs(context.Background(), mock.NilOption)
    70  	if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_local_docs"?: test error`, err) {
    71  		t.Errorf("Unexpected error: %s", err)
    72  	}
    73  }
    74  
    75  func TestQuery(t *testing.T) {
    76  	t.Run("standard", func(t *testing.T) {
    77  		db := newTestDB(nil, errors.New("test error"))
    78  		_, err := db.Query(context.Background(), "ddoc", "view", mock.NilOption)
    79  		if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_design/ddoc/_view/view"?: test error`, err) {
    80  			t.Errorf("Unexpected error: %s", err)
    81  		}
    82  	})
    83  	t.Run("partitioned", func(t *testing.T) {
    84  		db := newTestDB(nil, errors.New("test error"))
    85  		_, err := db.Query(context.Background(), "ddoc", "view", OptionPartition("a2"))
    86  		if !testy.ErrorMatchesRE(`Get "?http://example.com/testdb/_partition/a2/_design/ddoc/_view/view"?: test error`, err) {
    87  			t.Errorf("Unexpected error: %s", err)
    88  		}
    89  	})
    90  }
    91  
    92  type Attachment struct {
    93  	Filename    string
    94  	ContentType string
    95  	Size        int64
    96  	Content     string
    97  }
    98  
    99  func TestGet(t *testing.T) {
   100  	type tt struct {
   101  		db          *db
   102  		id          string
   103  		options     kivik.Option
   104  		doc         *driver.Document
   105  		expected    string
   106  		attachments []*Attachment
   107  		status      int
   108  		err         string
   109  	}
   110  
   111  	tests := testy.NewTable()
   112  	tests.Add("missing doc ID", tt{
   113  		status: http.StatusBadRequest,
   114  		err:    "kivik: docID required",
   115  	})
   116  	tests.Add("invalid options", tt{
   117  		id:      "foo",
   118  		options: kivik.Param("foo", make(chan int)),
   119  		status:  http.StatusBadRequest,
   120  		err:     "kivik: invalid type chan int for options",
   121  	})
   122  	tests.Add("network failure", tt{
   123  		id:     "foo",
   124  		db:     newTestDB(nil, errors.New("net error")),
   125  		status: http.StatusBadGateway,
   126  		err:    `Get "?http://example.com/testdb/foo"?: net error`,
   127  	})
   128  	tests.Add("error response", tt{
   129  		id: "foo",
   130  		db: newTestDB(&http.Response{
   131  			StatusCode: http.StatusBadRequest,
   132  			Body:       Body(""),
   133  		}, nil),
   134  		status: http.StatusBadRequest,
   135  		err:    "Bad Request",
   136  	})
   137  	tests.Add("status OK", tt{
   138  		id: "foo",
   139  		db: newTestDB(&http.Response{
   140  			StatusCode: http.StatusOK,
   141  			Header: http.Header{
   142  				"Content-Type": {typeJSON},
   143  				"ETag":         {`"12-xxx"`},
   144  			},
   145  			ContentLength: 13,
   146  			Body:          Body(`{"foo":"bar"}`),
   147  		}, nil),
   148  		doc: &driver.Document{
   149  			Rev: "12-xxx",
   150  		},
   151  		expected: `{"foo":"bar"}`,
   152  	})
   153  	tests.Add("If-None-Match", tt{
   154  		db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   155  			if err := consume(req.Body); err != nil {
   156  				return nil, err
   157  			}
   158  			if inm := req.Header.Get("If-None-Match"); inm != `"foo"` {
   159  				return nil, fmt.Errorf(`If-None-Match: %s != "foo"`, inm)
   160  			}
   161  			return nil, errors.New("success")
   162  		}),
   163  		id:      "foo",
   164  		options: OptionIfNoneMatch("foo"),
   165  		status:  http.StatusBadGateway,
   166  		err:     `Get "?http://example.com/testdb/foo"?: success`,
   167  	})
   168  	tests.Add("invalid content type in response", tt{
   169  		id: "foo",
   170  		db: newTestDB(&http.Response{
   171  			StatusCode: http.StatusOK,
   172  			Header: http.Header{
   173  				"Content-Type": {"image/jpeg"},
   174  				"ETag":         {`"12-xxx"`},
   175  			},
   176  			ContentLength: 13,
   177  			Body:          Body("some response"),
   178  		}, nil),
   179  		status: http.StatusBadGateway,
   180  		err:    "kivik: invalid content type in response: image/jpeg",
   181  	})
   182  	tests.Add("invalid content type header", tt{
   183  		id: "foo",
   184  		db: newTestDB(&http.Response{
   185  			StatusCode: http.StatusOK,
   186  			Header: http.Header{
   187  				"Content-Type": {"cow; =moo"},
   188  				"ETag":         {`"12-xxx"`},
   189  			},
   190  			ContentLength: 13,
   191  			Body:          Body("some response"),
   192  		}, nil),
   193  		status: http.StatusBadGateway,
   194  		err:    "mime: invalid media parameter",
   195  	})
   196  	tests.Add("missing multipart boundary", tt{
   197  		db: newTestDB(&http.Response{
   198  			StatusCode: http.StatusOK,
   199  			Header: http.Header{
   200  				"Content-Type": {typeMPRelated},
   201  				"ETag":         {`"12-xxx"`},
   202  			},
   203  			ContentLength: 13,
   204  			Body:          Body("some response"),
   205  		}, nil),
   206  		id:     "foo",
   207  		status: http.StatusBadGateway,
   208  		err:    "kivik: boundary missing for multipart/related response",
   209  	})
   210  	tests.Add("no multipart data", tt{
   211  		db: newTestDB(&http.Response{
   212  			StatusCode: http.StatusOK,
   213  			Header: http.Header{
   214  				"Content-Length": {"538"},
   215  				"Content-Type":   {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`},
   216  				"Date":           {"Sat, 28 Sep 2013 08:08:22 GMT"},
   217  				"ETag":           {`"2-c1c6c44c4bc3c9344b037c8690468605"`},
   218  				"ServeR":         {"CouchDB (Erlang OTP)"},
   219  			},
   220  			ContentLength: 538,
   221  			Body:          Body(`bogus data`),
   222  		}, nil),
   223  		id:      "foo",
   224  		options: kivik.IncludeDocs(),
   225  		status:  http.StatusBadGateway,
   226  		err:     "multipart: NextPart: EOF",
   227  	})
   228  	tests.Add("incomplete multipart data", tt{
   229  		db: newTestDB(&http.Response{
   230  			StatusCode: http.StatusOK,
   231  			Header: http.Header{
   232  				"Content-Length": {"538"},
   233  				"Content-Type":   {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`},
   234  				"Date":           {"Sat, 28 Sep 2013 08:08:22 GMT"},
   235  				"ETag":           {`"2-c1c6c44c4bc3c9344b037c8690468605"`},
   236  				"ServeR":         {"CouchDB (Erlang OTP)"},
   237  			},
   238  			ContentLength: 538,
   239  			Body: Body(`--e89b3e29388aef23453450d10e5aaed0
   240  				bogus data`),
   241  		}, nil),
   242  		id:      "foo",
   243  		options: kivik.IncludeDocs(),
   244  		status:  http.StatusBadGateway,
   245  		err:     "malformed MIME header (initial )?line:.*bogus data",
   246  	})
   247  	tests.Add("multipart accept header", tt{
   248  		db: newCustomDB(func(r *http.Request) (*http.Response, error) {
   249  			expected := "multipart/mixed, multipart/related, application/json"
   250  			if accept := r.Header.Get("Accept"); accept != expected {
   251  				return nil, fmt.Errorf("Unexpected Accept header: %s", accept)
   252  			}
   253  			return nil, errors.New("not an error")
   254  		}),
   255  		id:     "foo",
   256  		status: http.StatusBadGateway,
   257  		err:    "not an error",
   258  	})
   259  	tests.Add("disable multipart accept header", tt{
   260  		db: newCustomDB(func(r *http.Request) (*http.Response, error) {
   261  			expected := "application/json"
   262  			if accept := r.Header.Get("Accept"); accept != expected {
   263  				return nil, fmt.Errorf("Unexpected Accept header: %s", accept)
   264  			}
   265  			return nil, errors.New("not an error")
   266  		}),
   267  		options: OptionNoMultipartGet(),
   268  		id:      "foo",
   269  		status:  http.StatusBadGateway,
   270  		err:     "not an error",
   271  	})
   272  	tests.Add("multipart attachments", tt{
   273  		// response borrowed from http://docs.couchdb.org/en/2.1.1/api/document/common.html#efficient-multiple-attachments-retrieving
   274  		db: newTestDB(&http.Response{
   275  			StatusCode: http.StatusOK,
   276  			Header: http.Header{
   277  				"Content-Length": {"538"},
   278  				"Content-Type":   {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`},
   279  				"Date":           {"Sat, 28 Sep 2013 08:08:22 GMT"},
   280  				"ETag":           {`"2-c1c6c44c4bc3c9344b037c8690468605"`},
   281  				"ServeR":         {"CouchDB (Erlang OTP)"},
   282  			},
   283  			ContentLength: 538,
   284  			Body: Body(`--e89b3e29388aef23453450d10e5aaed0
   285  Content-Type: application/json
   286  
   287  {"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}}
   288  --e89b3e29388aef23453450d10e5aaed0
   289  Content-Disposition: attachment; filename="recipe.txt"
   290  Content-Type: text/plain
   291  Content-Length: 86
   292  
   293  1. Take R
   294  2. Take E
   295  3. Mix with L
   296  4. Add some A
   297  5. Serve with X
   298  
   299  --e89b3e29388aef23453450d10e5aaed0--`),
   300  		}, nil),
   301  		id:      "foo",
   302  		options: kivik.IncludeDocs(),
   303  		doc: &driver.Document{
   304  			Rev: "2-c1c6c44c4bc3c9344b037c8690468605",
   305  			Attachments: &multipartAttachments{
   306  				meta: map[string]attMeta{
   307  					"recipe.txt": {
   308  						Follows:     true,
   309  						ContentType: "text/plain",
   310  						Size:        func() *int64 { x := int64(86); return &x }(),
   311  					},
   312  				},
   313  			},
   314  		},
   315  		expected: `{"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}}`,
   316  		attachments: []*Attachment{
   317  			{
   318  				Filename:    "recipe.txt",
   319  				Size:        86,
   320  				ContentType: "text/plain",
   321  				Content:     "1. Take R\n2. Take E\n3. Mix with L\n4. Add some A\n5. Serve with X\n",
   322  			},
   323  		},
   324  	})
   325  	tests.Add("multipart attachments, doc content length", tt{
   326  		// response borrowed from http://docs.couchdb.org/en/2.1.1/api/document/common.html#efficient-multiple-attachments-retrieving
   327  		db: newTestDB(&http.Response{
   328  			StatusCode: http.StatusOK,
   329  			Header: http.Header{
   330  				"Content-Length": {"558"},
   331  				"Content-Type":   {`multipart/related; boundary="e89b3e29388aef23453450d10e5aaed0"`},
   332  				"Date":           {"Sat, 28 Sep 2013 08:08:22 GMT"},
   333  				"ETag":           {`"2-c1c6c44c4bc3c9344b037c8690468605"`},
   334  				"ServeR":         {"CouchDB (Erlang OTP)"},
   335  			},
   336  			ContentLength: 558,
   337  			Body: Body(`--e89b3e29388aef23453450d10e5aaed0
   338  Content-Type: application/json
   339  Content-Length: 199
   340  
   341  {"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}}
   342  --e89b3e29388aef23453450d10e5aaed0
   343  Content-Disposition: attachment; filename="recipe.txt"
   344  Content-Type: text/plain
   345  Content-Length: 86
   346  
   347  1. Take R
   348  2. Take E
   349  3. Mix with L
   350  4. Add some A
   351  5. Serve with X
   352  
   353  --e89b3e29388aef23453450d10e5aaed0--`),
   354  		}, nil),
   355  		id:      "foo",
   356  		options: kivik.IncludeDocs(),
   357  		doc: &driver.Document{
   358  			Rev: "2-c1c6c44c4bc3c9344b037c8690468605",
   359  			Attachments: &multipartAttachments{
   360  				meta: map[string]attMeta{
   361  					"recipe.txt": {
   362  						Follows:     true,
   363  						ContentType: "text/plain",
   364  						Size:        func() *int64 { x := int64(86); return &x }(),
   365  					},
   366  				},
   367  			},
   368  		},
   369  		expected: `{"_id":"secret","_rev":"2-c1c6c44c4bc3c9344b037c8690468605","_attachments":{"recipe.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-HV9aXJdEnu0xnMQYTKgOFA==","length":86,"follows":true}}}`,
   370  		attachments: []*Attachment{
   371  			{
   372  				Filename:    "recipe.txt",
   373  				Size:        86,
   374  				ContentType: "text/plain",
   375  				Content:     "1. Take R\n2. Take E\n3. Mix with L\n4. Add some A\n5. Serve with X\n",
   376  			},
   377  		},
   378  	})
   379  	tests.Add("bug268 - complex id", func(*testing.T) interface{} {
   380  		return tt{
   381  			db: newCustomDB(func(*http.Request) (*http.Response, error) {
   382  				return nil, errors.New("success")
   383  			}),
   384  			id:     "http://example.com/",
   385  			status: http.StatusBadGateway,
   386  			err:    `Get "?http://example.com/testdb/http%3A%2F%2Fexample\.com%2F"?: success`,
   387  		}
   388  	})
   389  	tests.Add("plus sign", func(*testing.T) interface{} {
   390  		return tt{
   391  			db: newCustomDB(func(*http.Request) (*http.Response, error) {
   392  				return nil, errors.New("success")
   393  			}),
   394  			id:     "2020-01-30T13:33:00.00+05:30|kl",
   395  			status: http.StatusBadGateway,
   396  			err:    `^Get "?http://example.com/testdb/2020-01-30T13%3A33%3A00\.00%2B05%3A30%7Ckl"?: success$`,
   397  		}
   398  	})
   399  
   400  	tests.Run(t, func(t *testing.T, tt tt) {
   401  		opts := tt.options
   402  		if opts == nil {
   403  			opts = mock.NilOption
   404  		}
   405  		result, err := tt.db.Get(context.Background(), tt.id, opts)
   406  		if !testy.ErrorMatchesRE(tt.err, err) {
   407  			t.Errorf("Unexpected error: \n Got: %s\nWant: /%s/", err, tt.err)
   408  		}
   409  		if err != nil {
   410  			return
   411  		}
   412  
   413  		if d := testy.DiffAsJSON([]byte(tt.expected), result.Body); d != nil {
   414  			t.Errorf("Unexpected result: %s", d)
   415  		}
   416  		attachments := rowAttachments(t, result.Attachments)
   417  
   418  		_ = result.Body.Close()
   419  		result.Body = nil // Determinism
   420  		if d := testy.DiffInterface(tt.doc, result); d != nil {
   421  			t.Errorf("Unexpected doc:\n%s", d)
   422  		}
   423  		if d := testy.DiffInterface(tt.attachments, attachments); d != nil {
   424  			t.Errorf("Unexpected attachments:\n%s", d)
   425  		}
   426  	})
   427  }
   428  
   429  func rowAttachments(t *testing.T, atts driver.Attachments) []*Attachment {
   430  	t.Helper()
   431  	var attachments []*Attachment
   432  	if atts != nil {
   433  		att := new(driver.Attachment)
   434  		for {
   435  			if err := atts.Next(att); err != nil {
   436  				if err != io.EOF {
   437  					t.Fatal(err)
   438  				}
   439  				break
   440  			}
   441  			content, e := io.ReadAll(att.Content)
   442  			if e != nil {
   443  				t.Fatal(e)
   444  			}
   445  			attachments = append(attachments, &Attachment{
   446  				Filename:    att.Filename,
   447  				ContentType: att.ContentType,
   448  				Size:        att.Size,
   449  				Content:     string(content),
   450  			})
   451  		}
   452  		atts.(*multipartAttachments).content = nil // Determinism
   453  		atts.(*multipartAttachments).mpReader = nil
   454  	}
   455  	return attachments
   456  }
   457  
   458  func TestOpenRevs(t *testing.T) {
   459  	type rowResult struct {
   460  		ID    string
   461  		Rev   string
   462  		Error string
   463  	}
   464  	type tt struct {
   465  		db      *db
   466  		id      string
   467  		revs    []string
   468  		options kivik.Option
   469  		want    []rowResult
   470  		err     string
   471  	}
   472  
   473  	tests := testy.NewTable()
   474  	tests.Add("open_revs", func(*testing.T) interface{} {
   475  		return tt{
   476  			db: newCustomDB(func(*http.Request) (*http.Response, error) {
   477  				return &http.Response{
   478  					StatusCode: http.StatusOK,
   479  					Header: http.Header{
   480  						"Content-Type": []string{`multipart/mixed; boundary="ea68bec945fd9dece3e826462c5604e8"`},
   481  					},
   482  					Body: Body(`--ea68bec945fd9dece3e826462c5604e8
   483  Content-Type: application/json
   484  
   485  {"_id":"bar","_rev":"2-e2a6df12e36615e8def0bb38bb17b48d","foo":123}
   486  --ea68bec945fd9dece3e826462c5604e8--
   487  `),
   488  				}, nil
   489  			}),
   490  			id: "bar",
   491  			want: []rowResult{
   492  				{
   493  					ID:  "bar",
   494  					Rev: "2-e2a6df12e36615e8def0bb38bb17b48d",
   495  				},
   496  			},
   497  		}
   498  	})
   499  	tests.Add("open_revs with multiple revs", func(*testing.T) interface{} {
   500  		return tt{
   501  			db: newCustomDB(func(*http.Request) (*http.Response, error) {
   502  				return &http.Response{
   503  					StatusCode: http.StatusOK,
   504  					Header: http.Header{
   505  						"Content-Type": []string{`multipart/mixed; boundary="7b1596fc4940bc1be725ad67f11ec1c4"`},
   506  					},
   507  					Body: Body(`--7b1596fc4940bc1be725ad67f11ec1c4
   508  Content-Type: application/json
   509  
   510  {
   511  	"_id": "SpaghettiWithMeatballs",
   512  	"_rev": "1-917fa23",
   513  	"_revisions": {
   514  		"ids": [
   515  			"917fa23"
   516  		],
   517  		"start": 1
   518  	},
   519  	"description": "An Italian-American delicious dish",
   520  	"ingredients": [
   521  		"spaghetti",
   522  		"tomato sauce",
   523  		"meatballs"
   524  	],
   525  	"name": "Spaghetti with meatballs"
   526  }
   527  --7b1596fc4940bc1be725ad67f11ec1c4
   528  Content-Type: multipart/related; boundary="a81a77b0ca68389dda3243a43ca946f2"
   529  
   530  --a81a77b0ca68389dda3243a43ca946f2
   531  Content-Type: application/json
   532  
   533  {
   534  	"_attachments": {
   535  		"recipe.txt": {
   536  			"content_type": "text/plain",
   537  			"digest": "md5-R5CrCb6fX10Y46AqtNn0oQ==",
   538  			"follows": true,
   539  			"length": 87,
   540  			"revpos": 7
   541  		}
   542  	},
   543  	"_id": "SpaghettiWithMeatballs",
   544  	"_rev": "7-474f12e",
   545  	"_revisions": {
   546  		"ids": [
   547  			"474f12e",
   548  			"5949cfc",
   549  			"00ecbbc",
   550  			"fc997b6",
   551  			"3552c87",
   552  			"404838b",
   553  			"5defd9d",
   554  			"dc1e4be"
   555  		],
   556  		"start": 7
   557  	},
   558  	"description": "An Italian-American delicious dish",
   559  	"ingredients": [
   560  		"spaghetti",
   561  		"tomato sauce",
   562  		"meatballs",
   563  		"love"
   564  	],
   565  	"name": "Spaghetti with meatballs"
   566  }
   567  --a81a77b0ca68389dda3243a43ca946f2
   568  Content-Disposition: attachment; filename="recipe.txt"
   569  Content-Type: text/plain
   570  Content-Length: 87
   571  
   572  1. Cook spaghetti
   573  2. Cook meetballs
   574  3. Mix them
   575  4. Add tomato sauce
   576  5. ...
   577  6. PROFIT!
   578  
   579  --a81a77b0ca68389dda3243a43ca946f2--
   580  --7b1596fc4940bc1be725ad67f11ec1c4
   581  Content-Type: application/json; error="true"
   582  
   583  {"missing":"3-6bcedf1"}
   584  --7b1596fc4940bc1be725ad67f11ec1c4--`),
   585  				}, nil
   586  			}),
   587  			id: "bar",
   588  			want: []rowResult{
   589  				{
   590  					ID:  "bar",
   591  					Rev: "1-917fa23",
   592  				},
   593  				{
   594  					ID:  "bar",
   595  					Rev: "7-474f12e",
   596  				},
   597  				{
   598  					ID:    "bar",
   599  					Rev:   "3-6bcedf1",
   600  					Error: "missing",
   601  				},
   602  			},
   603  		}
   604  	})
   605  	tests.Add("not found", func(*testing.T) interface{} {
   606  		return tt{
   607  			db: newCustomDB(func(*http.Request) (*http.Response, error) {
   608  				return &http.Response{
   609  					StatusCode: http.StatusNotFound,
   610  					Header: http.Header{
   611  						"Content-Type": []string{`application/json`},
   612  					},
   613  					Body: Body(`{"error":"not_found","reason":"missing"}`),
   614  				}, nil
   615  			}),
   616  			id:  "bar",
   617  			err: "Not Found",
   618  		}
   619  	})
   620  	tests.Run(t, func(t *testing.T, tt tt) {
   621  		opts := tt.options
   622  		if opts == nil {
   623  			opts = mock.NilOption
   624  		}
   625  		rows, err := tt.db.OpenRevs(context.Background(), tt.id, tt.revs, opts)
   626  		var errMsg string
   627  		if err != nil {
   628  			errMsg = err.Error()
   629  		}
   630  		if errMsg != tt.err {
   631  			t.Errorf("Unexpected error: %s", err)
   632  		}
   633  		if errMsg != "" {
   634  			return
   635  		}
   636  
   637  		got := []rowResult{}
   638  		for i := 0; ; i++ {
   639  			row := new(driver.Row)
   640  			if err := rows.Next(row); err != nil {
   641  				if err == io.EOF {
   642  					break
   643  				}
   644  				t.Fatal(err)
   645  			}
   646  			row.Doc = nil // Determinism
   647  			row.Attachments = nil
   648  			got = append(got, rowResult{
   649  				ID:  row.ID,
   650  				Rev: row.Rev,
   651  				Error: func() string {
   652  					if row.Error != nil {
   653  						return row.Error.Error()
   654  					}
   655  					return ""
   656  				}(),
   657  			})
   658  		}
   659  		if d := testy.DiffInterface(tt.want, got); d != nil {
   660  			t.Errorf("Unexpected result: %s", d)
   661  		}
   662  	})
   663  }
   664  
   665  func TestCreateDoc(t *testing.T) {
   666  	tests := []struct {
   667  		name    string
   668  		db      *db
   669  		doc     interface{}
   670  		options kivik.Option
   671  		id, rev string
   672  		status  int
   673  		err     string
   674  	}{
   675  		{
   676  			name:   "network error",
   677  			db:     newTestDB(nil, errors.New("foo error")),
   678  			status: http.StatusBadGateway,
   679  			err:    `Post "?http://example.com/testdb"?: foo error`,
   680  		},
   681  		{
   682  			name:   "invalid doc",
   683  			doc:    make(chan int),
   684  			db:     newTestDB(nil, errors.New("")),
   685  			status: http.StatusBadRequest,
   686  			err:    `Post "?http://example.com/testdb"?: json: unsupported type: chan int`,
   687  		},
   688  		{
   689  			name: "error response",
   690  			doc:  map[string]interface{}{"foo": "bar"},
   691  			db: newTestDB(&http.Response{
   692  				StatusCode: http.StatusBadRequest,
   693  				Body:       io.NopCloser(strings.NewReader("")),
   694  			}, nil),
   695  			status: http.StatusBadRequest,
   696  			err:    "Bad Request",
   697  		},
   698  		{
   699  			name: "invalid JSON response",
   700  			doc:  map[string]interface{}{"foo": "bar"},
   701  			db: newTestDB(&http.Response{
   702  				StatusCode: http.StatusOK,
   703  				Body:       io.NopCloser(strings.NewReader("invalid json")),
   704  			}, nil),
   705  			status: http.StatusBadGateway,
   706  			err:    "invalid character 'i' looking for beginning of value",
   707  		},
   708  		{
   709  			name: "success, 1.6.1",
   710  			doc:  map[string]interface{}{"foo": "bar"},
   711  			db: newTestDB(&http.Response{
   712  				StatusCode: http.StatusOK,
   713  				Header: map[string][]string{
   714  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   715  					"Location":       {"http://localhost:5984/foo/43734cf3ce6d5a37050c050bb600006b"},
   716  					"ETag":           {`"1-4c6114c65e295552ab1019e2b046b10e"`},
   717  					"Date":           {"Wed, 25 Oct 2017 10:38:38 GMT"},
   718  					"Content-Type":   {"text/plain; charset=utf-8"},
   719  					"Content-Length": {"95"},
   720  					"Cache-Control":  {"must-revalidate"},
   721  				},
   722  				Body: io.NopCloser(strings.NewReader(`{"ok":true,"id":"43734cf3ce6d5a37050c050bb600006b","rev":"1-4c6114c65e295552ab1019e2b046b10e"}
   723  `)),
   724  			}, nil),
   725  			id:  "43734cf3ce6d5a37050c050bb600006b",
   726  			rev: "1-4c6114c65e295552ab1019e2b046b10e",
   727  		},
   728  		{
   729  			name:    "batch mode",
   730  			db:      newTestDB(nil, errors.New("success")),
   731  			doc:     map[string]string{"foo": "bar"},
   732  			options: kivik.Param("batch", "ok"),
   733  			status:  http.StatusBadGateway,
   734  			err:     `^Post "?http://example.com/testdb\?batch=ok"?: success$`,
   735  		},
   736  		{
   737  			name: "full commit",
   738  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   739  				if err := consume(req.Body); err != nil {
   740  					return nil, err
   741  				}
   742  				if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" {
   743  					return nil, errors.New("X-Couch-Full-Commit not true")
   744  				}
   745  				return nil, errors.New("success")
   746  			}),
   747  			options: OptionFullCommit(),
   748  			status:  http.StatusBadGateway,
   749  			err:     `Post "?http://example.com/testdb"?: success`,
   750  		},
   751  		{
   752  			name:    "invalid options",
   753  			db:      &db{},
   754  			options: kivik.Param("foo", make(chan int)),
   755  			status:  http.StatusBadRequest,
   756  			err:     "kivik: invalid type chan int for options",
   757  		},
   758  	}
   759  	for _, test := range tests {
   760  		t.Run(test.name, func(t *testing.T) {
   761  			opts := test.options
   762  			if opts == nil {
   763  				opts = mock.NilOption
   764  			}
   765  			id, rev, err := test.db.CreateDoc(context.Background(), test.doc, opts)
   766  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   767  				t.Error(d)
   768  			}
   769  			if test.id != id || test.rev != rev {
   770  				t.Errorf("Unexpected results: ID=%s rev=%s", id, rev)
   771  			}
   772  		})
   773  	}
   774  }
   775  
   776  func TestOptionsToParams(t *testing.T) {
   777  	type otpTest struct {
   778  		Name     string
   779  		Input    map[string]interface{}
   780  		Expected url.Values
   781  		Error    string
   782  	}
   783  	tests := []otpTest{
   784  		{
   785  			Name:  "Unmarshalable key",
   786  			Input: map[string]interface{}{"key": make(chan int)},
   787  			Error: "json: unsupported type: chan int",
   788  		},
   789  		{
   790  			Name:     "String",
   791  			Input:    map[string]interface{}{"foo": "bar"},
   792  			Expected: map[string][]string{"foo": {"bar"}},
   793  		},
   794  		{
   795  			Name:     "StringSlice",
   796  			Input:    map[string]interface{}{"foo": []string{"bar", "baz"}},
   797  			Expected: map[string][]string{"foo": {"bar", "baz"}},
   798  		},
   799  		{
   800  			Name:     "Bool",
   801  			Input:    map[string]interface{}{"foo": true},
   802  			Expected: map[string][]string{"foo": {"true"}},
   803  		},
   804  		{
   805  			Name:     "Int",
   806  			Input:    map[string]interface{}{"foo": 123},
   807  			Expected: map[string][]string{"foo": {"123"}},
   808  		},
   809  		{
   810  			Name:  "Error",
   811  			Input: map[string]interface{}{"foo": []byte("foo")},
   812  			Error: "kivik: invalid type []uint8 for options",
   813  		},
   814  	}
   815  	for _, test := range tests {
   816  		func(test otpTest) {
   817  			t.Run(test.Name, func(t *testing.T) {
   818  				params, err := optionsToParams(test.Input)
   819  				var msg string
   820  				if err != nil {
   821  					msg = err.Error()
   822  				}
   823  				if msg != test.Error {
   824  					t.Errorf("Error\n\tExpected: %s\n\t  Actual: %s\n", test.Error, msg)
   825  				}
   826  				if d := testy.DiffInterface(test.Expected, params); d != nil {
   827  					t.Errorf("Params not as expected:\n%s\n", d)
   828  				}
   829  			})
   830  		}(test)
   831  	}
   832  }
   833  
   834  func TestCompact(t *testing.T) {
   835  	tests := []struct {
   836  		name   string
   837  		db     *db
   838  		status int
   839  		err    string
   840  	}{
   841  		{
   842  			name:   "net error",
   843  			db:     newTestDB(nil, errors.New("net error")),
   844  			status: http.StatusBadGateway,
   845  			err:    `Post "?http://example.com/testdb/_compact"?: net error`,
   846  		},
   847  		{
   848  			name: "1.6.1",
   849  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   850  				if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON {
   851  					return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct)
   852  				}
   853  				return &http.Response{
   854  					StatusCode: http.StatusOK,
   855  					Header: http.Header{
   856  						"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   857  						"Date":           {"Thu, 26 Oct 2017 13:07:52 GMT"},
   858  						"Content-Type":   {"text/plain; charset=utf-8"},
   859  						"Content-Length": {"12"},
   860  						"Cache-Control":  {"must-revalidate"},
   861  					},
   862  					Body: io.NopCloser(strings.NewReader(`{"ok":true}`)),
   863  				}, nil
   864  			}),
   865  		},
   866  	}
   867  	for _, test := range tests {
   868  		t.Run(test.name, func(t *testing.T) {
   869  			err := test.db.Compact(context.Background())
   870  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   871  				t.Error(d)
   872  			}
   873  		})
   874  	}
   875  }
   876  
   877  func TestCompactView(t *testing.T) {
   878  	tests := []struct {
   879  		name   string
   880  		db     *db
   881  		id     string
   882  		status int
   883  		err    string
   884  	}{
   885  		{
   886  			name:   "no ddoc",
   887  			status: http.StatusBadRequest,
   888  			err:    "kivik: ddocID required",
   889  		},
   890  		{
   891  			name:   "network error",
   892  			db:     newTestDB(nil, errors.New("net error")),
   893  			id:     "foo",
   894  			status: http.StatusBadGateway,
   895  			err:    `Post "?http://example.com/testdb/_compact/foo"?: net error`,
   896  		},
   897  		{
   898  			name: "1.6.1",
   899  			id:   "foo",
   900  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   901  				if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON {
   902  					return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct)
   903  				}
   904  				return &http.Response{
   905  					StatusCode: http.StatusAccepted,
   906  					Header: http.Header{
   907  						"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   908  						"Date":           {"Thu, 26 Oct 2017 13:07:52 GMT"},
   909  						"Content-Type":   {"text/plain; charset=utf-8"},
   910  						"Content-Length": {"12"},
   911  						"Cache-Control":  {"must-revalidate"},
   912  					},
   913  					Body: io.NopCloser(strings.NewReader(`{"ok":true}`)),
   914  				}, nil
   915  			}),
   916  		},
   917  	}
   918  	for _, test := range tests {
   919  		t.Run(test.name, func(t *testing.T) {
   920  			err := test.db.CompactView(context.Background(), test.id)
   921  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   922  				t.Error(d)
   923  			}
   924  		})
   925  	}
   926  }
   927  
   928  func TestViewCleanup(t *testing.T) {
   929  	tests := []struct {
   930  		name   string
   931  		db     *db
   932  		status int
   933  		err    string
   934  	}{
   935  		{
   936  			name:   "net error",
   937  			db:     newTestDB(nil, errors.New("net error")),
   938  			status: http.StatusBadGateway,
   939  			err:    `Post "?http://example.com/testdb/_view_cleanup"?: net error`,
   940  		},
   941  		{
   942  			name: "1.6.1",
   943  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   944  				if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON {
   945  					return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct)
   946  				}
   947  				return &http.Response{
   948  					StatusCode: http.StatusOK,
   949  					Header: http.Header{
   950  						"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   951  						"Date":           {"Thu, 26 Oct 2017 13:07:52 GMT"},
   952  						"Content-Type":   {"text/plain; charset=utf-8"},
   953  						"Content-Length": {"12"},
   954  						"Cache-Control":  {"must-revalidate"},
   955  					},
   956  					Body: io.NopCloser(strings.NewReader(`{"ok":true}`)),
   957  				}, nil
   958  			}),
   959  		},
   960  	}
   961  	for _, test := range tests {
   962  		t.Run(test.name, func(t *testing.T) {
   963  			err := test.db.ViewCleanup(context.Background())
   964  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   965  				t.Error(d)
   966  			}
   967  		})
   968  	}
   969  }
   970  
   971  func TestPut(t *testing.T) {
   972  	type pTest struct {
   973  		name    string
   974  		db      *db
   975  		id      string
   976  		doc     interface{}
   977  		options kivik.Option
   978  		rev     string
   979  		status  int
   980  		err     string
   981  		finish  func() error
   982  	}
   983  	tests := []pTest{
   984  		{
   985  			name:   "missing docID",
   986  			status: http.StatusBadRequest,
   987  			err:    "kivik: docID required",
   988  		},
   989  		{
   990  			name:   "network error",
   991  			id:     "foo",
   992  			db:     newTestDB(nil, errors.New("net error")),
   993  			status: http.StatusBadGateway,
   994  			err:    `Put "?http://example.com/testdb/foo"?: net error`,
   995  		},
   996  		{
   997  			name: "error response",
   998  			id:   "foo",
   999  			db: newTestDB(&http.Response{
  1000  				StatusCode: http.StatusBadRequest,
  1001  				Body:       io.NopCloser(strings.NewReader("")),
  1002  			}, nil),
  1003  			status: http.StatusBadRequest,
  1004  			err:    "Bad Request",
  1005  		},
  1006  		{
  1007  			name: "invalid JSON response",
  1008  			id:   "foo",
  1009  			db: newTestDB(&http.Response{
  1010  				StatusCode: http.StatusOK,
  1011  				Body:       io.NopCloser(strings.NewReader("invalid json")),
  1012  			}, nil),
  1013  			status: http.StatusBadGateway,
  1014  			err:    "invalid character 'i' looking for beginning of value",
  1015  		},
  1016  		{
  1017  			name: "invalid document",
  1018  			id:   "foo",
  1019  			doc:  make(chan int),
  1020  			db: newTestDB(&http.Response{
  1021  				StatusCode: http.StatusOK,
  1022  				Body:       io.NopCloser(strings.NewReader("")),
  1023  			}, nil),
  1024  			status: http.StatusBadRequest,
  1025  			err:    `Put "?http://example.com/testdb/foo"?: json: unsupported type: chan int`,
  1026  		},
  1027  		{
  1028  			name: "doc created, 1.6.1",
  1029  			id:   "foo",
  1030  			doc:  map[string]string{"foo": "bar"},
  1031  			db: newTestDB(&http.Response{
  1032  				StatusCode: http.StatusCreated,
  1033  				Header: http.Header{
  1034  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1035  					"Location":       {"http://localhost:5984/foo/foo"},
  1036  					"ETag":           {`"1-4c6114c65e295552ab1019e2b046b10e"`},
  1037  					"Date":           {"Wed, 25 Oct 2017 12:33:09 GMT"},
  1038  					"Content-Type":   {"text/plain; charset=utf-8"},
  1039  					"Content-Length": {"66"},
  1040  					"Cache-Control":  {"must-revalidate"},
  1041  				},
  1042  				Body: io.NopCloser(strings.NewReader(`{"ok":true,"id":"foo","rev":"1-4c6114c65e295552ab1019e2b046b10e"}`)),
  1043  			}, nil),
  1044  			rev: "1-4c6114c65e295552ab1019e2b046b10e",
  1045  		},
  1046  		{
  1047  			name: "full commit",
  1048  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1049  				if err := consume(req.Body); err != nil {
  1050  					return nil, err
  1051  				}
  1052  				if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" {
  1053  					return nil, errors.New("X-Couch-Full-Commit not true")
  1054  				}
  1055  				return nil, errors.New("success")
  1056  			}),
  1057  			id:      "foo",
  1058  			doc:     map[string]string{"foo": "bar"},
  1059  			options: OptionFullCommit(),
  1060  			status:  http.StatusBadGateway,
  1061  			err:     `Put "?http://example.com/testdb/foo"?: success`,
  1062  		},
  1063  		{
  1064  			name: "connection refused",
  1065  			db: func() *db {
  1066  				c, err := chttp.New(&http.Client{}, "http://127.0.0.1:1/", mock.NilOption)
  1067  				if err != nil {
  1068  					t.Fatal(err)
  1069  				}
  1070  				return &db{
  1071  					client: &client{Client: c},
  1072  					dbName: "animals",
  1073  				}
  1074  			}(),
  1075  			id:     "cow",
  1076  			doc:    map[string]interface{}{"feet": 4},
  1077  			status: http.StatusBadGateway,
  1078  			err:    `Put "?http://127.0.0.1:1/animals/cow"?: dial tcp ([::1]|127.0.0.1):1: (getsockopt|connect): connection refused`,
  1079  		},
  1080  		func() pTest {
  1081  			db := realDB(t)
  1082  			return pTest{
  1083  				name: "real database, multipart attachments",
  1084  				db:   db,
  1085  				id:   "foo",
  1086  				doc: map[string]interface{}{
  1087  					"feet": 4,
  1088  					"_attachments": &kivik.Attachments{
  1089  						"foo.txt": &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("test content")},
  1090  					},
  1091  				},
  1092  				rev: "1-1e527110339245a3191b3f6cbea27ab1",
  1093  				finish: func() error {
  1094  					return db.client.DestroyDB(context.Background(), db.dbName, nil)
  1095  				},
  1096  			}
  1097  		}(),
  1098  	}
  1099  	for _, test := range tests {
  1100  		t.Run(test.name, func(t *testing.T) {
  1101  			if test.finish != nil {
  1102  				t.Cleanup(func() {
  1103  					if err := test.finish(); err != nil {
  1104  						t.Fatal(err)
  1105  					}
  1106  				})
  1107  			}
  1108  			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  1109  			defer cancel()
  1110  			opts := test.options
  1111  			if opts == nil {
  1112  				opts = mock.NilOption
  1113  			}
  1114  			rev, err := test.db.Put(ctx, test.id, test.doc, opts)
  1115  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
  1116  				t.Error(d)
  1117  			}
  1118  			if rev != test.rev {
  1119  				t.Errorf("Unexpected rev: %s", rev)
  1120  			}
  1121  		})
  1122  	}
  1123  }
  1124  
  1125  func TestDelete(t *testing.T) {
  1126  	type tt struct {
  1127  		db      *db
  1128  		id      string
  1129  		options kivik.Option
  1130  		newrev  string
  1131  		status  int
  1132  		err     string
  1133  	}
  1134  
  1135  	tests := testy.NewTable()
  1136  	tests.Add("no doc id", tt{
  1137  		status: http.StatusBadRequest,
  1138  		err:    "kivik: docID required",
  1139  	})
  1140  	tests.Add("no rev", tt{
  1141  		id:     "foo",
  1142  		status: http.StatusBadRequest,
  1143  		err:    "kivik: rev required",
  1144  	})
  1145  	tests.Add("network error", tt{
  1146  		id:      "foo",
  1147  		options: kivik.Rev("1-xxx"),
  1148  		db:      newTestDB(nil, errors.New("net error")),
  1149  		status:  http.StatusBadGateway,
  1150  		err:     `(Delete "?http://example.com/testdb/foo\?rev="?: )?net error`,
  1151  	})
  1152  	tests.Add("1.6.1 conflict", tt{
  1153  		id:      "43734cf3ce6d5a37050c050bb600006b",
  1154  		options: kivik.Rev("1-xxx"),
  1155  		db: newTestDB(&http.Response{
  1156  			StatusCode: 409,
  1157  			Header: http.Header{
  1158  				"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1159  				"Date":           {"Thu, 26 Oct 2017 13:29:06 GMT"},
  1160  				"Content-Type":   {"text/plain; charset=utf-8"},
  1161  				"Content-Length": {"58"},
  1162  				"Cache-Control":  {"must-revalidate"},
  1163  			},
  1164  			Body: io.NopCloser(strings.NewReader(`{"error":"conflict","reason":"Document update conflict."}`)),
  1165  		}, nil),
  1166  		status: http.StatusConflict,
  1167  		err:    "Conflict",
  1168  	})
  1169  	tests.Add("1.6.1 success", tt{
  1170  		id:      "43734cf3ce6d5a37050c050bb600006b",
  1171  		options: kivik.Rev("1-4c6114c65e295552ab1019e2b046b10e"),
  1172  		db: newTestDB(&http.Response{
  1173  			StatusCode: 200,
  1174  			Header: http.Header{
  1175  				"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1176  				"Date":           {"Thu, 26 Oct 2017 13:29:06 GMT"},
  1177  				"Content-Type":   {"text/plain; charset=utf-8"},
  1178  				"ETag":           {`"2-185ccf92154a9f24a4f4fd12233bf463"`},
  1179  				"Content-Length": {"95"},
  1180  				"Cache-Control":  {"must-revalidate"},
  1181  			},
  1182  			Body: io.NopCloser(strings.NewReader(`{"ok":true,"id":"43734cf3ce6d5a37050c050bb600006b","rev":"2-185ccf92154a9f24a4f4fd12233bf463"}`)),
  1183  		}, nil),
  1184  		newrev: "2-185ccf92154a9f24a4f4fd12233bf463",
  1185  	})
  1186  	tests.Add("batch mode", tt{
  1187  		db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1188  			if err := consume(req.Body); err != nil {
  1189  				return nil, err
  1190  			}
  1191  			if batch := req.URL.Query().Get("batch"); batch != "ok" {
  1192  				return nil, fmt.Errorf("Unexpected query batch=%s", batch)
  1193  			}
  1194  			return nil, errors.New("success")
  1195  		}),
  1196  		id: "foo",
  1197  		options: kivik.Params(map[string]interface{}{
  1198  			"batch": "ok",
  1199  			"rev":   "1-xxx",
  1200  		}),
  1201  		status: http.StatusBadGateway,
  1202  		err:    "success",
  1203  	})
  1204  	tests.Add("invalid options", tt{
  1205  		db: &db{},
  1206  		id: "foo",
  1207  		options: kivik.Params(map[string]interface{}{
  1208  			"foo": make(chan int),
  1209  			"rev": "1-xxx",
  1210  		}),
  1211  		status: http.StatusBadRequest,
  1212  		err:    "kivik: invalid type chan int for options",
  1213  	})
  1214  	tests.Add("full commit", tt{
  1215  		db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1216  			if err := consume(req.Body); err != nil {
  1217  				return nil, err
  1218  			}
  1219  			if fullCommit := req.Header.Get("X-Couch-Full-Commit"); fullCommit != "true" {
  1220  				return nil, errors.New("X-Couch-Full-Commit not true")
  1221  			}
  1222  			return nil, errors.New("success")
  1223  		}),
  1224  		id: "foo",
  1225  		options: multiOptions{
  1226  			OptionFullCommit(),
  1227  			kivik.Rev("1-xxx"),
  1228  		},
  1229  		status: http.StatusBadGateway,
  1230  		err:    "success",
  1231  	})
  1232  
  1233  	tests.Run(t, func(t *testing.T, tt tt) {
  1234  		opts := tt.options
  1235  		if opts == nil {
  1236  			opts = mock.NilOption
  1237  		}
  1238  		newrev, err := tt.db.Delete(context.Background(), tt.id, opts)
  1239  		if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" {
  1240  			t.Error(d)
  1241  		}
  1242  		if newrev != tt.newrev {
  1243  			t.Errorf("Unexpected new rev: %s", newrev)
  1244  		}
  1245  	})
  1246  }
  1247  
  1248  func TestFlush(t *testing.T) {
  1249  	tests := []struct {
  1250  		name   string
  1251  		db     *db
  1252  		status int
  1253  		err    string
  1254  	}{
  1255  		{
  1256  			name:   "network error",
  1257  			db:     newTestDB(nil, errors.New("net error")),
  1258  			status: http.StatusBadGateway,
  1259  			err:    `Post "?http://example.com/testdb/_ensure_full_commit"?: net error`,
  1260  		},
  1261  		{
  1262  			name: "1.6.1",
  1263  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1264  				if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON {
  1265  					return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct)
  1266  				}
  1267  				return &http.Response{
  1268  					StatusCode: 201,
  1269  					Header: http.Header{
  1270  						"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1271  						"Date":           {"Thu, 26 Oct 2017 13:07:52 GMT"},
  1272  						"Content-Type":   {"text/plain; charset=utf-8"},
  1273  						"Content-Length": {"53"},
  1274  						"Cache-Control":  {"must-revalidate"},
  1275  					},
  1276  					Body: io.NopCloser(strings.NewReader(`{"ok":true,"instance_start_time":"1509022681259533"}`)),
  1277  				}, nil
  1278  			}),
  1279  		},
  1280  		{
  1281  			name: "2.0.0",
  1282  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1283  				if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON {
  1284  					return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct)
  1285  				}
  1286  				return &http.Response{
  1287  					StatusCode: 201,
  1288  					Header: http.Header{
  1289  						"Server":              {"CouchDB/2.0.0 (Erlang OTP/17)"},
  1290  						"Date":                {"Thu, 26 Oct 2017 13:07:52 GMT"},
  1291  						"Content-Type":        {typeJSON},
  1292  						"Content-Length":      {"38"},
  1293  						"Cache-Control":       {"must-revalidate"},
  1294  						"X-Couch-Request-ID":  {"e454023cb8"},
  1295  						"X-CouchDB-Body-Time": {"0"},
  1296  					},
  1297  					Body: io.NopCloser(strings.NewReader(`{"ok":true,"instance_start_time":"0"}`)),
  1298  				}, nil
  1299  			}),
  1300  		},
  1301  	}
  1302  	for _, test := range tests {
  1303  		t.Run(test.name, func(t *testing.T) {
  1304  			err := test.db.Flush(context.Background())
  1305  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
  1306  				t.Error(d)
  1307  			}
  1308  		})
  1309  	}
  1310  }
  1311  
  1312  type queryResult struct {
  1313  	Offset    int64
  1314  	TotalRows int64
  1315  	Warning   string
  1316  	UpdateSeq string
  1317  	Err       string
  1318  	Rows      []*driver.Row
  1319  }
  1320  
  1321  func queryResultDiff(got, want queryResult) string {
  1322  	type qr struct {
  1323  		Offset    int64
  1324  		TotalRows int64
  1325  		Warning   string
  1326  		UpdateSeq string
  1327  		Err       string
  1328  		Rows      []*row
  1329  	}
  1330  	g := qr{
  1331  		Offset:    got.Offset,
  1332  		TotalRows: got.TotalRows,
  1333  		Warning:   got.Warning,
  1334  		UpdateSeq: got.UpdateSeq,
  1335  		Err:       got.Err,
  1336  		Rows:      make([]*row, len(got.Rows)),
  1337  	}
  1338  	for i, row := range got.Rows {
  1339  		g.Rows[i] = driverRow2row(row)
  1340  	}
  1341  
  1342  	w := qr{
  1343  		Offset:    want.Offset,
  1344  		TotalRows: want.TotalRows,
  1345  		Warning:   want.Warning,
  1346  		UpdateSeq: want.UpdateSeq,
  1347  		Err:       want.Err,
  1348  		Rows:      make([]*row, len(want.Rows)),
  1349  	}
  1350  	for i, row := range want.Rows {
  1351  		w.Rows[i] = driverRow2row(row)
  1352  	}
  1353  	return cmp.Diff(g, w)
  1354  }
  1355  
  1356  func TestRowsQuery(t *testing.T) {
  1357  	tests := []struct {
  1358  		name     string
  1359  		db       *db
  1360  		path     string
  1361  		options  kivik.Option
  1362  		expected queryResult
  1363  		status   int
  1364  		err      string
  1365  	}{
  1366  		{
  1367  			name:    "invalid options",
  1368  			path:    "_all_docs",
  1369  			options: kivik.Param("foo", make(chan int)),
  1370  			status:  http.StatusBadRequest,
  1371  			err:     "kivik: invalid type chan int for options",
  1372  		},
  1373  		{
  1374  			name:   "network error",
  1375  			path:   "_all_docs",
  1376  			db:     newTestDB(nil, errors.New("go away")),
  1377  			status: http.StatusBadGateway,
  1378  			err:    `Get "?http://example.com/testdb/_all_docs"?: go away`,
  1379  		},
  1380  		{
  1381  			name: "error response",
  1382  			path: "_all_docs",
  1383  			db: newTestDB(&http.Response{
  1384  				StatusCode: http.StatusBadRequest,
  1385  				Body:       io.NopCloser(strings.NewReader("")),
  1386  			}, nil),
  1387  			status: http.StatusBadRequest,
  1388  			err:    "Bad Request",
  1389  		},
  1390  		{
  1391  			name: "all docs default 1.6.1",
  1392  			path: "_all_docs",
  1393  			db: newTestDB(&http.Response{
  1394  				StatusCode: http.StatusOK,
  1395  				Header: map[string][]string{
  1396  					"Transfer-Encoding": {"chunked"},
  1397  					"Date":              {"Tue, 24 Oct 2017 21:17:12 GMT"},
  1398  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1399  					"ETag":              {`"2MVNDK3T2PN4JUK89TKD10QDA"`},
  1400  					"Content-Type":      {"text/plain; charset=utf-8"},
  1401  					"Cache-Control":     {"must-revalidate"},
  1402  				},
  1403  				Body: io.NopCloser(strings.NewReader(`{"total_rows":3,"offset":0,"rows":[
  1404  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-75efcce1f083316d622d389f3f9813f7"}},
  1405  {"id":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","key":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","value":{"rev":"1-747e6766038164010fd0efcabd1a31dd"}},
  1406  {"id":"org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye","key":"org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye","value":{"rev":"1-4645438e6e1aa2230a1b06b5c1f5c63f"}}
  1407  ]}
  1408  `)),
  1409  			}, nil),
  1410  			expected: queryResult{
  1411  				TotalRows: 3,
  1412  				Rows: []*driver.Row{
  1413  					{
  1414  						ID:    "_design/_auth",
  1415  						Key:   []byte(`"_design/_auth"`),
  1416  						Value: strings.NewReader(`{"rev":"1-75efcce1f083316d622d389f3f9813f7"}`),
  1417  					},
  1418  					{
  1419  						ID:    "org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye",
  1420  						Key:   []byte(`"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye"`),
  1421  						Value: strings.NewReader(`{"rev":"1-747e6766038164010fd0efcabd1a31dd"}`),
  1422  					},
  1423  					{
  1424  						ID:    "org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye",
  1425  						Key:   []byte(`"org.couchdb.user:zqfdn6u3cqi6pol3hslq5egiye"`),
  1426  						Value: strings.NewReader(`{"rev":"1-4645438e6e1aa2230a1b06b5c1f5c63f"}`),
  1427  					},
  1428  				},
  1429  			},
  1430  		},
  1431  		{
  1432  			name: "all docs options 1.6.1",
  1433  			path: "/_all_docs?update_seq=true&limit=1&skip=1",
  1434  			db: newTestDB(&http.Response{
  1435  				StatusCode: http.StatusOK,
  1436  				Header: map[string][]string{
  1437  					"Transfer-Encoding": {"chunked"},
  1438  					"Date":              {"Tue, 24 Oct 2017 21:17:12 GMT"},
  1439  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1440  					"ETag":              {`"2MVNDK3T2PN4JUK89TKD10QDA"`},
  1441  					"Content-Type":      {"text/plain; charset=utf-8"},
  1442  					"Cache-Control":     {"must-revalidate"},
  1443  				},
  1444  				Body: io.NopCloser(strings.NewReader(`{"total_rows":3,"offset":1,"update_seq":31,"rows":[
  1445  {"id":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","key":"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye","value":{"rev":"1-747e6766038164010fd0efcabd1a31dd"}}
  1446  ]}
  1447  `)),
  1448  			}, nil),
  1449  			expected: queryResult{
  1450  				TotalRows: 3,
  1451  				Offset:    1,
  1452  				UpdateSeq: "31",
  1453  				Rows: []*driver.Row{
  1454  					{
  1455  						ID:    "org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye",
  1456  						Key:   []byte(`"org.couchdb.user:5wmxzru3b4i6pdmvhslq5egiye"`),
  1457  						Value: strings.NewReader(`{"rev":"1-747e6766038164010fd0efcabd1a31dd"}`),
  1458  					},
  1459  				},
  1460  			},
  1461  		},
  1462  		{
  1463  			name: "all docs options 2.0.0, no results",
  1464  			path: "/_all_docs?update_seq=true&limit=1",
  1465  			db: newTestDB(&http.Response{
  1466  				StatusCode: http.StatusOK,
  1467  				Header: map[string][]string{
  1468  					"Transfer-Encoding":  {"chunked"},
  1469  					"Date":               {"Tue, 24 Oct 2017 21:21:30 GMT"},
  1470  					"Server":             {"CouchDB/2.0.0 (Erlang OTP/17)"},
  1471  					"Content-Type":       {typeJSON},
  1472  					"Cache-Control":      {"must-revalidate"},
  1473  					"X-Couch-Request-ID": {"a9688d9335"},
  1474  					"X-Couch-Body-Time":  {"0"},
  1475  				},
  1476  				Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":0,"update_seq":"13-g1AAAAEzeJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjPlsQBJhgdA6j8QZCUy4Fv4AKLuflYiE151DRB18wmZtwCibj9u85ISgGRSPV63JSmA1NiD1bDgUJPIkCSP3xAHkCHxYDWsWQDg12MD","rows":[
  1477  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-75efcce1f083316d622d389f3f9813f7"}}
  1478  ]}
  1479  `)),
  1480  			}, nil),
  1481  			expected: queryResult{
  1482  				TotalRows: 1,
  1483  				UpdateSeq: "13-g1AAAAEzeJzLYWBg4MhgTmHgzcvPy09JdcjLz8gvLskBCjPlsQBJhgdA6j8QZCUy4Fv4AKLuflYiE151DRB18wmZtwCibj9u85ISgGRSPV63JSmA1NiD1bDgUJPIkCSP3xAHkCHxYDWsWQDg12MD",
  1484  				Rows: []*driver.Row{
  1485  					{
  1486  						ID:    "_design/_auth",
  1487  						Key:   []byte(`"_design/_auth"`),
  1488  						Value: strings.NewReader(`{"rev":"1-75efcce1f083316d622d389f3f9813f7"}`),
  1489  					},
  1490  				},
  1491  			},
  1492  		},
  1493  		{
  1494  			name:    "all docs with keys",
  1495  			path:    "/_all_docs",
  1496  			options: kivik.Param("keys", []string{"_design/_auth", "foo"}),
  1497  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  1498  				if r.Method != http.MethodPost {
  1499  					t.Errorf("Unexpected method: %s", r.Method)
  1500  				}
  1501  				defer r.Body.Close() // nolint: errcheck
  1502  				if d := testy.DiffAsJSON(map[string][]string{"keys": {"_design/_auth", "foo"}}, r.Body); d != nil {
  1503  					t.Error(d)
  1504  				}
  1505  				if keys := r.URL.Query().Get("keys"); keys != "" {
  1506  					t.Error("query key 'keys' should be absent")
  1507  				}
  1508  				return &http.Response{
  1509  					StatusCode: http.StatusOK,
  1510  					Header: http.Header{
  1511  						"Transfer-Encoding":  {"chunked"},
  1512  						"Date":               {"Sat, 01 Sep 2018 19:01:30 GMT"},
  1513  						"Server":             {"CouchDB/2.2.0 (Erlang OTP/19)"},
  1514  						"Content-Type":       {typeJSON},
  1515  						"Cache-Control":      {"must-revalidate"},
  1516  						"X-Couch-Request-ID": {"24fdb3fd86"},
  1517  						"X-Couch-Body-Time":  {"0"},
  1518  					},
  1519  					Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[
  1520  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}}
  1521  ]}`)),
  1522  				}, nil
  1523  			}),
  1524  			expected: queryResult{
  1525  				TotalRows: 1,
  1526  				UpdateSeq: "",
  1527  				Rows: []*driver.Row{
  1528  					{
  1529  						ID:    "_design/_auth",
  1530  						Key:   []byte(`"_design/_auth"`),
  1531  						Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`),
  1532  					},
  1533  				},
  1534  			},
  1535  		},
  1536  		{
  1537  			name:    "all docs with endkey",
  1538  			path:    "/_all_docs",
  1539  			options: kivik.Param("endkey", []string{"foo", "bar"}),
  1540  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  1541  				if d := testy.DiffAsJSON([]byte(`["foo","bar"]`), []byte(r.URL.Query().Get("endkey"))); d != nil {
  1542  					t.Error(d)
  1543  				}
  1544  				return &http.Response{
  1545  					StatusCode: http.StatusOK,
  1546  					Header: http.Header{
  1547  						"Transfer-Encoding":  {"chunked"},
  1548  						"Date":               {"Sat, 01 Sep 2018 19:01:30 GMT"},
  1549  						"Server":             {"CouchDB/2.2.0 (Erlang OTP/19)"},
  1550  						"Content-Type":       {typeJSON},
  1551  						"Cache-Control":      {"must-revalidate"},
  1552  						"X-Couch-Request-ID": {"24fdb3fd86"},
  1553  						"X-Couch-Body-Time":  {"0"},
  1554  					},
  1555  					Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[
  1556  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}}
  1557  ]}`)),
  1558  				}, nil
  1559  			}),
  1560  			expected: queryResult{
  1561  				TotalRows: 1,
  1562  				UpdateSeq: "",
  1563  				Rows: []*driver.Row{
  1564  					{
  1565  						ID:    "_design/_auth",
  1566  						Key:   []byte(`"_design/_auth"`),
  1567  						Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`),
  1568  					},
  1569  				},
  1570  			},
  1571  		},
  1572  		{
  1573  			name:    "all docs with simple string endkey",
  1574  			path:    "/_all_docs",
  1575  			options: kivik.Param("endkey", "foo"),
  1576  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  1577  				if d := testy.DiffAsJSON([]byte(`"foo"`), []byte(r.URL.Query().Get("endkey"))); d != nil {
  1578  					t.Error(d)
  1579  				}
  1580  				return &http.Response{
  1581  					StatusCode: http.StatusOK,
  1582  					Header: http.Header{
  1583  						"Transfer-Encoding":  {"chunked"},
  1584  						"Date":               {"Sat, 01 Sep 2018 19:01:30 GMT"},
  1585  						"Server":             {"CouchDB/2.2.0 (Erlang OTP/19)"},
  1586  						"Content-Type":       {typeJSON},
  1587  						"Cache-Control":      {"must-revalidate"},
  1588  						"X-Couch-Request-ID": {"24fdb3fd86"},
  1589  						"X-Couch-Body-Time":  {"0"},
  1590  					},
  1591  					Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[
  1592  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}}
  1593  ]}`)),
  1594  				}, nil
  1595  			}),
  1596  			expected: queryResult{
  1597  				TotalRows: 1,
  1598  				UpdateSeq: "",
  1599  				Rows: []*driver.Row{
  1600  					{
  1601  						ID:    "_design/_auth",
  1602  						Key:   []byte(`"_design/_auth"`),
  1603  						Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`),
  1604  					},
  1605  				},
  1606  			},
  1607  		},
  1608  		{
  1609  			name:    "all docs with raw JSON endkey",
  1610  			path:    "/_all_docs",
  1611  			options: kivik.Param("endkey", json.RawMessage(`"foo"`)),
  1612  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  1613  				if d := testy.DiffAsJSON([]byte(`"foo"`), []byte(r.URL.Query().Get("endkey"))); d != nil {
  1614  					t.Error(d)
  1615  				}
  1616  				return &http.Response{
  1617  					StatusCode: http.StatusOK,
  1618  					Header: http.Header{
  1619  						"Transfer-Encoding":  {"chunked"},
  1620  						"Date":               {"Sat, 01 Sep 2018 19:01:30 GMT"},
  1621  						"Server":             {"CouchDB/2.2.0 (Erlang OTP/19)"},
  1622  						"Content-Type":       {typeJSON},
  1623  						"Cache-Control":      {"must-revalidate"},
  1624  						"X-Couch-Request-ID": {"24fdb3fd86"},
  1625  						"X-Couch-Body-Time":  {"0"},
  1626  					},
  1627  					Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[
  1628  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}}
  1629  ]}`)),
  1630  				}, nil
  1631  			}),
  1632  			expected: queryResult{
  1633  				TotalRows: 1,
  1634  				UpdateSeq: "",
  1635  				Rows: []*driver.Row{
  1636  					{
  1637  						ID:    "_design/_auth",
  1638  						Key:   []byte(`"_design/_auth"`),
  1639  						Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`),
  1640  					},
  1641  				},
  1642  			},
  1643  		},
  1644  		{
  1645  			name:    "all docs with object keys",
  1646  			path:    "/_all_docs",
  1647  			options: kivik.Param("keys", []interface{}{"_design/_auth", "foo", []string{"bar", "baz"}}),
  1648  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  1649  				if r.Method != http.MethodPost {
  1650  					t.Errorf("Unexpected method: %s", r.Method)
  1651  				}
  1652  				defer r.Body.Close() // nolint: errcheck
  1653  				if d := testy.DiffAsJSON(map[string][]interface{}{"keys": {"_design/_auth", "foo", []string{"bar", "baz"}}}, r.Body); d != nil {
  1654  					t.Error(d)
  1655  				}
  1656  				if keys := r.URL.Query().Get("keys"); keys != "" {
  1657  					t.Error("query key 'keys' should be absent")
  1658  				}
  1659  				return &http.Response{
  1660  					StatusCode: http.StatusOK,
  1661  					Header: http.Header{
  1662  						"Transfer-Encoding":  {"chunked"},
  1663  						"Date":               {"Sat, 01 Sep 2018 19:01:30 GMT"},
  1664  						"Server":             {"CouchDB/2.2.0 (Erlang OTP/19)"},
  1665  						"Content-Type":       {typeJSON},
  1666  						"Cache-Control":      {"must-revalidate"},
  1667  						"X-Couch-Request-ID": {"24fdb3fd86"},
  1668  						"X-Couch-Body-Time":  {"0"},
  1669  					},
  1670  					Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[
  1671  {"id":"_design/_auth","key":"_design/_auth","value":{"rev":"1-6e609020e0371257432797b4319c5829"}}
  1672  ]}`)),
  1673  				}, nil
  1674  			}),
  1675  			expected: queryResult{
  1676  				TotalRows: 1,
  1677  				UpdateSeq: "",
  1678  				Rows: []*driver.Row{
  1679  					{
  1680  						ID:    "_design/_auth",
  1681  						Key:   []byte(`"_design/_auth"`),
  1682  						Value: strings.NewReader(`{"rev":"1-6e609020e0371257432797b4319c5829"}`),
  1683  					},
  1684  				},
  1685  			},
  1686  		},
  1687  		{
  1688  			name:    "all docs with docs",
  1689  			path:    "/_all_docs",
  1690  			options: kivik.Param("keys", []string{"_design/_auth", "foo"}),
  1691  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  1692  				if r.Method != http.MethodPost {
  1693  					t.Errorf("Unexpected method: %s", r.Method)
  1694  				}
  1695  				defer r.Body.Close() // nolint: errcheck
  1696  				if d := testy.DiffAsJSON(map[string][]string{"keys": {"_design/_auth", "foo"}}, r.Body); d != nil {
  1697  					t.Error(d)
  1698  				}
  1699  				if keys := r.URL.Query().Get("keys"); keys != "" {
  1700  					t.Error("query key 'keys' should be absent")
  1701  				}
  1702  				return &http.Response{
  1703  					StatusCode: http.StatusOK,
  1704  					Header: http.Header{
  1705  						"Transfer-Encoding":  {"chunked"},
  1706  						"Date":               {"Sat, 01 Sep 2018 19:01:30 GMT"},
  1707  						"Server":             {"CouchDB/2.2.0 (Erlang OTP/19)"},
  1708  						"Content-Type":       {typeJSON},
  1709  						"Cache-Control":      {"must-revalidate"},
  1710  						"X-Couch-Request-ID": {"24fdb3fd86"},
  1711  						"X-Couch-Body-Time":  {"0"},
  1712  					},
  1713  					Body: io.NopCloser(strings.NewReader(`{"total_rows":1,"offset":null,"rows":[
  1714  {"id":"foo","doc":{"_id":"foo"}}
  1715  ]}`)),
  1716  				}, nil
  1717  			}),
  1718  			expected: queryResult{
  1719  				TotalRows: 1,
  1720  				UpdateSeq: "",
  1721  				Rows: []*driver.Row{
  1722  					{
  1723  						ID:  "foo",
  1724  						Doc: strings.NewReader(`{"_id":"foo"}`),
  1725  					},
  1726  				},
  1727  			},
  1728  		},
  1729  	}
  1730  	for _, test := range tests {
  1731  		t.Run(test.name, func(t *testing.T) {
  1732  			opts := test.options
  1733  			if opts == nil {
  1734  				opts = mock.NilOption
  1735  			}
  1736  			rows, err := test.db.rowsQuery(context.Background(), test.path, opts)
  1737  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
  1738  				t.Error(d)
  1739  			}
  1740  			if err != nil {
  1741  				return
  1742  			}
  1743  			result := queryResult{
  1744  				Rows: []*driver.Row{},
  1745  			}
  1746  			for {
  1747  				var row driver.Row
  1748  				if e := rows.Next(&row); e != nil {
  1749  					if e != io.EOF {
  1750  						result.Err = e.Error()
  1751  					}
  1752  					break
  1753  				}
  1754  				result.Rows = append(result.Rows, &row)
  1755  			}
  1756  			result.Offset = rows.Offset()
  1757  			result.TotalRows = rows.TotalRows()
  1758  			result.UpdateSeq = rows.UpdateSeq()
  1759  			if warner, ok := rows.(driver.RowsWarner); ok {
  1760  				result.Warning = warner.Warning()
  1761  			} else {
  1762  				t.Errorf("RowsWarner interface not satisfied!!?")
  1763  			}
  1764  
  1765  			if d := queryResultDiff(test.expected, result); d != "" {
  1766  				t.Error(d)
  1767  			}
  1768  		})
  1769  	}
  1770  }
  1771  
  1772  func TestSecurity(t *testing.T) {
  1773  	tests := []struct {
  1774  		name     string
  1775  		db       *db
  1776  		expected *driver.Security
  1777  		status   int
  1778  		err      string
  1779  	}{
  1780  		{
  1781  			name:   "network error",
  1782  			db:     newTestDB(nil, errors.New("net error")),
  1783  			status: http.StatusBadGateway,
  1784  			err:    `Get "?http://example.com/testdb/_security"?: net error`,
  1785  		},
  1786  		{
  1787  			name: "1.6.1 empty",
  1788  			db: newTestDB(&http.Response{
  1789  				StatusCode: 200,
  1790  				Header: http.Header{
  1791  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1792  					"Date":           {"Thu, 26 Oct 2017 14:28:14 GMT"},
  1793  					"Content-Type":   {"text/plain; charset=utf-8"},
  1794  					"Content-Length": {"3"},
  1795  					"Cache-Control":  {"must-revalidate"},
  1796  				},
  1797  				Body: io.NopCloser(strings.NewReader("{}")),
  1798  			}, nil),
  1799  			expected: &driver.Security{},
  1800  		},
  1801  		{
  1802  			name: "1.6.1 non-empty",
  1803  			db: newTestDB(&http.Response{
  1804  				StatusCode: 200,
  1805  				Header: http.Header{
  1806  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1807  					"Date":           {"Thu, 26 Oct 2017 14:28:14 GMT"},
  1808  					"Content-Type":   {"text/plain; charset=utf-8"},
  1809  					"Content-Length": {"65"},
  1810  					"Cache-Control":  {"must-revalidate"},
  1811  				},
  1812  				Body: io.NopCloser(strings.NewReader(`{"admins":{},"members":{"names":["32dgsme3cmi6pddghslq5egiye"]}}`)),
  1813  			}, nil),
  1814  			expected: &driver.Security{
  1815  				Members: driver.Members{
  1816  					Names: []string{"32dgsme3cmi6pddghslq5egiye"},
  1817  				},
  1818  			},
  1819  		},
  1820  	}
  1821  	for _, test := range tests {
  1822  		t.Run(test.name, func(t *testing.T) {
  1823  			result, err := test.db.Security(context.Background())
  1824  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
  1825  				t.Error(d)
  1826  			}
  1827  			if d := testy.DiffInterface(test.expected, result); d != nil {
  1828  				t.Error(d)
  1829  			}
  1830  		})
  1831  	}
  1832  }
  1833  
  1834  func TestSetSecurity(t *testing.T) {
  1835  	type tt struct {
  1836  		db       *db
  1837  		security *driver.Security
  1838  		status   int
  1839  		err      string
  1840  	}
  1841  	tests := testy.NewTable()
  1842  
  1843  	tests.Add("network error", tt{
  1844  		db:     newTestDB(nil, errors.New("net error")),
  1845  		status: http.StatusBadGateway,
  1846  		err:    `Put "?http://example.com/testdb/_security"?: net error`,
  1847  	})
  1848  	tests.Add("1.6.1", func(t *testing.T) interface{} {
  1849  		return tt{
  1850  			security: &driver.Security{
  1851  				Admins: driver.Members{
  1852  					Names: []string{"bob"},
  1853  				},
  1854  				Members: driver.Members{
  1855  					Roles: []string{"users"},
  1856  				},
  1857  			},
  1858  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1859  				defer req.Body.Close() // nolint: errcheck
  1860  				if ct, _, _ := mime.ParseMediaType(req.Header.Get("Content-Type")); ct != typeJSON {
  1861  					return nil, fmt.Errorf("Expected Content-Type: application/json, got %s", ct)
  1862  				}
  1863  				var body interface{}
  1864  				if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
  1865  					return nil, err
  1866  				}
  1867  				expected := map[string]interface{}{
  1868  					"admins": map[string]interface{}{
  1869  						"names": []string{"bob"},
  1870  					},
  1871  					"members": map[string]interface{}{
  1872  						"roles": []string{"users"},
  1873  					},
  1874  				}
  1875  				if d := testy.DiffAsJSON(expected, body); d != nil {
  1876  					t.Error(d)
  1877  				}
  1878  				return &http.Response{
  1879  					StatusCode: 200,
  1880  					Header: http.Header{
  1881  						"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1882  						"Date":           {"Thu, 26 Oct 2017 15:06:21 GMT"},
  1883  						"Content-Type":   {"text/plain; charset=utf-8"},
  1884  						"Content-Length": {"12"},
  1885  						"Cache-Control":  {"must-revalidate"},
  1886  					},
  1887  					Body: io.NopCloser(strings.NewReader(`{"ok":true}`)),
  1888  				}, nil
  1889  			}),
  1890  		}
  1891  	})
  1892  
  1893  	tests.Run(t, func(t *testing.T, tt tt) {
  1894  		err := tt.db.SetSecurity(context.Background(), tt.security)
  1895  		if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" {
  1896  			t.Error(d)
  1897  		}
  1898  	})
  1899  }
  1900  
  1901  func TestGetRev(t *testing.T) {
  1902  	tests := []struct {
  1903  		name   string
  1904  		db     *db
  1905  		id     string
  1906  		rev    string
  1907  		status int
  1908  		err    string
  1909  	}{
  1910  		{
  1911  			name:   "no doc id",
  1912  			status: http.StatusBadRequest,
  1913  			err:    "kivik: docID required",
  1914  		},
  1915  		{
  1916  			name:   "network error",
  1917  			id:     "foo",
  1918  			db:     newTestDB(nil, errors.New("net error")),
  1919  			status: http.StatusBadGateway,
  1920  			err:    `Head "?http://example.com/testdb/foo"?: net error`,
  1921  		},
  1922  		{
  1923  			name: "1.6.1",
  1924  			id:   "foo",
  1925  			db: newTestDB(&http.Response{
  1926  				StatusCode: 200,
  1927  				Request: &http.Request{
  1928  					Method: "HEAD",
  1929  				},
  1930  				Header: http.Header{
  1931  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  1932  					"ETag":           {`"1-4c6114c65e295552ab1019e2b046b10e"`},
  1933  					"Date":           {"Thu, 26 Oct 2017 15:21:15 GMT"},
  1934  					"Content-Type":   {"text/plain; charset=utf-8"},
  1935  					"Content-Length": {"70"},
  1936  					"Cache-Control":  {"must-revalidate"},
  1937  				},
  1938  				ContentLength: 70,
  1939  				Body:          io.NopCloser(strings.NewReader("")),
  1940  			}, nil),
  1941  			rev: "1-4c6114c65e295552ab1019e2b046b10e",
  1942  		},
  1943  	}
  1944  	for _, test := range tests {
  1945  		t.Run(test.name, func(t *testing.T) {
  1946  			rev, err := test.db.GetRev(context.Background(), test.id, mock.NilOption)
  1947  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
  1948  				t.Error(d)
  1949  			}
  1950  			if rev != test.rev {
  1951  				t.Errorf("Got rev %s, expected %s", rev, test.rev)
  1952  			}
  1953  		})
  1954  	}
  1955  }
  1956  
  1957  func TestCopy(t *testing.T) {
  1958  	type tt struct {
  1959  		target, source string
  1960  		options        kivik.Option
  1961  		db             *db
  1962  		rev            string
  1963  		status         int
  1964  		err            string
  1965  	}
  1966  
  1967  	tests := testy.NewTable()
  1968  	tests.Add("missing source", tt{
  1969  		status: http.StatusBadRequest,
  1970  		err:    "kivik: sourceID required",
  1971  	})
  1972  	tests.Add("missing target", tt{
  1973  		source: "foo",
  1974  		status: http.StatusBadRequest,
  1975  		err:    "kivik: targetID required",
  1976  	})
  1977  	tests.Add("network error", tt{
  1978  		source: "foo",
  1979  		target: "bar",
  1980  		db:     newTestDB(nil, errors.New("net error")),
  1981  		status: http.StatusBadGateway,
  1982  		err:    "(Copy http://example.com/testdb/foo: )?net error",
  1983  	})
  1984  	tests.Add("invalid options", tt{
  1985  		db:      &db{},
  1986  		source:  "foo",
  1987  		target:  "bar",
  1988  		options: kivik.Param("foo", make(chan int)),
  1989  		status:  http.StatusBadRequest,
  1990  		err:     "kivik: invalid type chan int for options",
  1991  	})
  1992  	tests.Add("create 1.6.1", tt{
  1993  		source: "foo",
  1994  		target: "bar",
  1995  		db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  1996  			if req.Header.Get("Destination") != "bar" {
  1997  				return nil, errors.New("Unexpected destination")
  1998  			}
  1999  			return &http.Response{
  2000  				StatusCode: 201,
  2001  				Header: http.Header{
  2002  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  2003  					"Location":       {"http://example.com/foo/bar"},
  2004  					"ETag":           {`"1-f81c8a795b0c6f9e9f699f64c6b82256"`},
  2005  					"Date":           {"Thu, 26 Oct 2017 15:45:57 GMT"},
  2006  					"Content-Type":   {"text/plain; charset=utf-8"},
  2007  					"Content-Length": {"66"},
  2008  					"Cache-Control":  {"must-revalidate"},
  2009  				},
  2010  				Body: Body(`{"ok":true,"id":"bar","rev":"1-f81c8a795b0c6f9e9f699f64c6b82256"}`),
  2011  			}, nil
  2012  		}),
  2013  		rev: "1-f81c8a795b0c6f9e9f699f64c6b82256",
  2014  	})
  2015  	tests.Add("full commit 1.6.1", tt{
  2016  		source:  "foo",
  2017  		target:  "bar",
  2018  		options: OptionFullCommit(),
  2019  		db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  2020  			if dest := req.Header.Get("Destination"); dest != "bar" {
  2021  				return nil, fmt.Errorf("Unexpected destination: %s", dest)
  2022  			}
  2023  			if fc := req.Header.Get("X-Couch-Full-Commit"); fc != "true" {
  2024  				return nil, fmt.Errorf("X-Couch-Full-Commit: %s", fc)
  2025  			}
  2026  			return &http.Response{
  2027  				StatusCode: 201,
  2028  				Header: http.Header{
  2029  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  2030  					"Location":       {"http://example.com/foo/bar"},
  2031  					"ETag":           {`"1-f81c8a795b0c6f9e9f699f64c6b82256"`},
  2032  					"Date":           {"Thu, 26 Oct 2017 15:45:57 GMT"},
  2033  					"Content-Type":   {"text/plain; charset=utf-8"},
  2034  					"Content-Length": {"66"},
  2035  					"Cache-Control":  {"must-revalidate"},
  2036  				},
  2037  				Body: Body(`{"ok":true,"id":"bar","rev":"1-f81c8a795b0c6f9e9f699f64c6b82256"}`),
  2038  			}, nil
  2039  		}),
  2040  		rev: "1-f81c8a795b0c6f9e9f699f64c6b82256",
  2041  	})
  2042  	tests.Add("target rev", tt{
  2043  		source:  "foo",
  2044  		target:  "bar?rev=1-xxx",
  2045  		options: OptionFullCommit(),
  2046  		db: newCustomDB(func(req *http.Request) (*http.Response, error) {
  2047  			if dest := req.Header.Get("Destination"); dest != "bar?rev=1-xxx" {
  2048  				return nil, fmt.Errorf("Unexpected destination: %s", dest)
  2049  			}
  2050  			if fc := req.Header.Get("X-Couch-Full-Commit"); fc != "true" {
  2051  				return nil, fmt.Errorf("X-Couch-Full-Commit: %s", fc)
  2052  			}
  2053  			return &http.Response{
  2054  				StatusCode: 201,
  2055  				Header: http.Header{
  2056  					"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
  2057  					"Location":       {"http://example.com/foo/bar"},
  2058  					"ETag":           {`"2-yyy"`},
  2059  					"Date":           {"Thu, 26 Oct 2017 15:45:57 GMT"},
  2060  					"Content-Type":   {"text/plain; charset=utf-8"},
  2061  					"Content-Length": {"66"},
  2062  					"Cache-Control":  {"must-revalidate"},
  2063  				},
  2064  				Body: Body(`{"ok":true,"id":"bar","rev":"2-yyy"}`),
  2065  			}, nil
  2066  		}),
  2067  		rev: "2-yyy",
  2068  	})
  2069  
  2070  	tests.Run(t, func(t *testing.T, tt tt) {
  2071  		opts := tt.options
  2072  		if opts == nil {
  2073  			opts = mock.NilOption
  2074  		}
  2075  		rev, err := tt.db.Copy(context.Background(), tt.target, tt.source, opts)
  2076  		if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" {
  2077  			t.Error(d)
  2078  		}
  2079  		if rev != tt.rev {
  2080  			t.Errorf("Got %s, expected %s", rev, tt.rev)
  2081  		}
  2082  	})
  2083  }
  2084  
  2085  func TestMultipartAttachmentsNext(t *testing.T) {
  2086  	tests := []struct {
  2087  		name     string
  2088  		atts     *multipartAttachments
  2089  		content  string
  2090  		expected *driver.Attachment
  2091  		status   int
  2092  		err      string
  2093  	}{
  2094  		{
  2095  			name: "done reading",
  2096  			atts: &multipartAttachments{
  2097  				mpReader: func() *multipart.Reader {
  2098  					r := multipart.NewReader(strings.NewReader("--xxx\r\n\r\n--xxx--"), "xxx")
  2099  					_, _ = r.NextPart()
  2100  					return r
  2101  				}(),
  2102  			},
  2103  			status: 500,
  2104  			err:    io.EOF.Error(),
  2105  		},
  2106  		{
  2107  			name: "malformed message",
  2108  			atts: &multipartAttachments{
  2109  				mpReader: func() *multipart.Reader {
  2110  					r := multipart.NewReader(strings.NewReader("oink"), "xxx")
  2111  					_, _ = r.NextPart()
  2112  					return r
  2113  				}(),
  2114  			},
  2115  			status: http.StatusBadGateway,
  2116  			err:    "multipart: NextPart: EOF",
  2117  		},
  2118  		{
  2119  			name: "malformed Content-Disposition",
  2120  			atts: &multipartAttachments{
  2121  				mpReader: multipart.NewReader(strings.NewReader(`--xxx
  2122  Content-Type: text/plain
  2123  
  2124  --xxx--`), "xxx"),
  2125  			},
  2126  			status: http.StatusBadGateway,
  2127  			err:    "Content-Disposition: mime: no media type",
  2128  		},
  2129  		{
  2130  			name: "malformed Content-Type",
  2131  			atts: &multipartAttachments{
  2132  				meta: map[string]attMeta{
  2133  					"foo.txt": {Follows: true},
  2134  				},
  2135  				mpReader: multipart.NewReader(strings.NewReader(`--xxx
  2136  Content-Type: text/plain; =foo
  2137  Content-Disposition: attachment; filename="foo.txt"
  2138  
  2139  --xxx--`), "xxx"),
  2140  			},
  2141  			status: http.StatusBadGateway,
  2142  			err:    "mime: invalid media parameter",
  2143  		},
  2144  		{
  2145  			name: "file not in manifest",
  2146  			atts: &multipartAttachments{
  2147  				mpReader: multipart.NewReader(strings.NewReader(`--xxx
  2148  Content-Type: text/plain; charset=foobar
  2149  Content-Disposition: attachment; filename="foo.txt"
  2150  
  2151  test content
  2152  --xxx--`), "xxx"),
  2153  			},
  2154  			status: http.StatusBadGateway,
  2155  			err:    "File 'foo.txt' not in manifest",
  2156  		},
  2157  		{
  2158  			name: "invalid content-disposition",
  2159  			atts: &multipartAttachments{
  2160  				mpReader: multipart.NewReader(strings.NewReader(`--xxx
  2161  Content-Type: text/plain
  2162  Content-Disposition: oink
  2163  
  2164  --xxx--`), "xxx"),
  2165  			},
  2166  			status: http.StatusBadGateway,
  2167  			err:    "Unexpected Content-Disposition: oink",
  2168  		},
  2169  		{
  2170  			name: "success",
  2171  			atts: &multipartAttachments{
  2172  				meta: map[string]attMeta{
  2173  					"foo.txt": {Follows: true},
  2174  				},
  2175  				mpReader: multipart.NewReader(strings.NewReader(`--xxx
  2176  Content-Type: text/plain; charset=foobar
  2177  Content-Disposition: attachment; filename="foo.txt"
  2178  
  2179  test content
  2180  --xxx--`), "xxx"),
  2181  			},
  2182  			content: "test content",
  2183  			expected: &driver.Attachment{
  2184  				Filename:    "foo.txt",
  2185  				ContentType: "text/plain",
  2186  				Size:        -1,
  2187  			},
  2188  		},
  2189  		{
  2190  			name: "success, no Content-Type header, & Content-Length header",
  2191  			atts: &multipartAttachments{
  2192  				meta: map[string]attMeta{
  2193  					"foo.txt": {
  2194  						Follows:     true,
  2195  						ContentType: "text/plain",
  2196  					},
  2197  				},
  2198  				mpReader: multipart.NewReader(strings.NewReader(`--xxx
  2199  Content-Disposition: attachment; filename="foo.txt"
  2200  Content-Length: 123
  2201  
  2202  test content
  2203  --xxx--`), "xxx"),
  2204  			},
  2205  			content: "test content",
  2206  			expected: &driver.Attachment{
  2207  				Filename:    "foo.txt",
  2208  				ContentType: "text/plain",
  2209  				Size:        123,
  2210  			},
  2211  		},
  2212  	}
  2213  	for _, test := range tests {
  2214  		t.Run(test.name, func(t *testing.T) {
  2215  			result := new(driver.Attachment)
  2216  			err := test.atts.Next(result)
  2217  			if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
  2218  				t.Error(d)
  2219  			}
  2220  			if err != nil {
  2221  				return
  2222  			}
  2223  			content, err := io.ReadAll(result.Content)
  2224  			if err != nil {
  2225  				t.Fatal(err)
  2226  			}
  2227  			if d := testy.DiffText(test.content, string(content)); d != nil {
  2228  				t.Errorf("Unexpected content:\n%s", d)
  2229  			}
  2230  			result.Content = nil // Determinism
  2231  			if d := testy.DiffInterface(test.expected, result); d != nil {
  2232  				t.Error(d)
  2233  			}
  2234  		})
  2235  	}
  2236  }
  2237  
  2238  func TestMultipartAttachmentsClose(t *testing.T) {
  2239  	const wantErr = "some error"
  2240  	atts := &multipartAttachments{
  2241  		content: &mockReadCloser{
  2242  			CloseFunc: func() error {
  2243  				return errors.New(wantErr)
  2244  			},
  2245  		},
  2246  	}
  2247  
  2248  	if err := atts.Close(); !testy.ErrorMatches(wantErr, err) {
  2249  		t.Errorf("Unexpected error: %s", err)
  2250  	}
  2251  }
  2252  
  2253  func TestPurge(t *testing.T) {
  2254  	expectedDocMap := map[string][]string{
  2255  		"foo": {"1-abc", "2-def"},
  2256  		"bar": {"3-ghi"},
  2257  	}
  2258  	tests := []struct {
  2259  		name   string
  2260  		db     *db
  2261  		docMap map[string][]string
  2262  
  2263  		expected *driver.PurgeResult
  2264  		err      string
  2265  		status   int
  2266  	}{
  2267  		{
  2268  			name: "1.7.1, nothing deleted",
  2269  			db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  2270  				if r.Method != "POST" {
  2271  					return nil, fmt.Errorf("Unexpected method: %s", r.Method)
  2272  				}
  2273  				if r.URL.Path != "/testdb/_purge" {
  2274  					return nil, fmt.Errorf("Unexpected path: %s", r.URL.Path)
  2275  				}
  2276  				if ct := r.Header.Get("Content-Type"); ct != typeJSON {
  2277  					return nil, fmt.Errorf("Unexpected Content-Type: %s", ct)
  2278  				}
  2279  				defer r.Body.Close() // nolint: errcheck
  2280  				var result interface{}
  2281  				if err := json.NewDecoder(r.Body).Decode(&result); err != nil {
  2282  					return nil, err
  2283  				}
  2284  				if d := testy.DiffAsJSON(expectedDocMap, result); d != nil {
  2285  					return nil, fmt.Errorf("Unexpected payload:\n%s", d)
  2286  				}
  2287  				return &http.Response{
  2288  					StatusCode: http.StatusOK,
  2289  					Header: http.Header{
  2290  						"Server":         []string{"CouchDB/1.7.1 (Erlang OTP/17)"},
  2291  						"Date":           []string{"Thu, 06 Sep 2018 16:55:26 GMT"},
  2292  						"Content-Type":   []string{"text/plain; charset=utf-8"},
  2293  						"Content-Length": []string{"28"},
  2294  						"Cache-Control":  []string{"must-revalidate"},
  2295  					},
  2296  					Body: io.NopCloser(strings.NewReader(`{"purge_seq":3,"purged":{}}`)),
  2297  				}, nil
  2298  			}),
  2299  			docMap:   expectedDocMap,
  2300  			expected: &driver.PurgeResult{Seq: 3, Purged: map[string][]string{}},
  2301  		},
  2302  		{
  2303  			name: "1.7.1, all deleted",
  2304  			db: newTestDB(&http.Response{
  2305  				StatusCode: http.StatusOK,
  2306  				Header: http.Header{
  2307  					"Server":         []string{"CouchDB/1.7.1 (Erlang OTP/17)"},
  2308  					"Date":           []string{"Thu, 06 Sep 2018 16:55:26 GMT"},
  2309  					"Content-Type":   []string{"text/plain; charset=utf-8"},
  2310  					"Content-Length": []string{"168"},
  2311  					"Cache-Control":  []string{"must-revalidate"},
  2312  				},
  2313  				Body: io.NopCloser(strings.NewReader(`{"purge_seq":5,"purged":{"foo":["1-abc","2-def"],"bar":["3-ghi"]}}`)),
  2314  			}, nil),
  2315  			docMap:   expectedDocMap,
  2316  			expected: &driver.PurgeResult{Seq: 5, Purged: expectedDocMap},
  2317  		},
  2318  		{
  2319  			name: "2.2.0, not supported",
  2320  			db: newTestDB(&http.Response{
  2321  				StatusCode:    501,
  2322  				ContentLength: 75,
  2323  				Header: http.Header{
  2324  					"Server":              []string{"CouchDB/2.2.0 (Erlang OTP/19)"},
  2325  					"Date":                []string{"Thu, 06 Sep 2018 16:55:26 GMT"},
  2326  					"Content-Type":        []string{typeJSON},
  2327  					"Content-Length":      []string{"75"},
  2328  					"Cache-Control":       []string{"must-revalidate"},
  2329  					"X-Couch-Request-ID":  []string{"03e91291c8"},
  2330  					"X-CouchDB-Body-Time": []string{"0"},
  2331  				},
  2332  				Body: io.NopCloser(strings.NewReader(`{"error":"not_implemented","reason":"this feature is not yet implemented"}`)),
  2333  			}, nil),
  2334  			docMap: expectedDocMap,
  2335  			err:    "Not Implemented: this feature is not yet implemented",
  2336  			status: http.StatusNotImplemented,
  2337  		},
  2338  	}
  2339  	for _, test := range tests {
  2340  		t.Run(test.name, func(t *testing.T) {
  2341  			result, err := test.db.Purge(context.Background(), test.docMap)
  2342  			if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
  2343  				t.Error(d)
  2344  			}
  2345  			if err != nil {
  2346  				return
  2347  			}
  2348  			if d := testy.DiffInterface(test.expected, result); d != nil {
  2349  				t.Error(d)
  2350  			}
  2351  		})
  2352  	}
  2353  }
  2354  
  2355  func TestMultipartAttachments(t *testing.T) {
  2356  	tests := []struct {
  2357  		name     string
  2358  		input    string
  2359  		atts     *kivik.Attachments
  2360  		expected string
  2361  		size     int64
  2362  		err      string
  2363  	}{
  2364  		{
  2365  			name:  "no attachments",
  2366  			input: `{"foo":"bar","baz":"qux"}`,
  2367  			atts:  &kivik.Attachments{},
  2368  			expected: `
  2369  --%[1]s
  2370  Content-Type: application/json
  2371  
  2372  {"foo":"bar","baz":"qux"}
  2373  --%[1]s--
  2374  `,
  2375  			size: 191,
  2376  		},
  2377  		{
  2378  			name:  "simple",
  2379  			input: `{"_attachments":{}}`,
  2380  			atts: &kivik.Attachments{
  2381  				"foo.txt": &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("test content")},
  2382  			},
  2383  			expected: `
  2384  --%[1]s
  2385  Content-Type: application/json
  2386  
  2387  {"_attachments":{"foo.txt":{"content_type":"text/plain","length":13,"follows":true}}
  2388  }
  2389  --%[1]s
  2390  
  2391  test content
  2392  
  2393  --%[1]s--
  2394  `,
  2395  			size: 333,
  2396  		},
  2397  	}
  2398  	for _, test := range tests {
  2399  		t.Run(test.name, func(t *testing.T) {
  2400  			in := io.NopCloser(strings.NewReader(test.input))
  2401  			boundary, size, body, err := newMultipartAttachments(in, test.atts)
  2402  			if !testy.ErrorMatches(test.err, err) {
  2403  				t.Errorf("Unexpected error: %s", err)
  2404  			}
  2405  			if test.size != size {
  2406  				t.Errorf("Unexpected size: %d (want %d)", size, test.size)
  2407  			}
  2408  			result, _ := io.ReadAll(body)
  2409  			expected := fmt.Sprintf(test.expected, boundary)
  2410  			expected = strings.TrimPrefix(expected, "\n")
  2411  			result = bytes.ReplaceAll(result, []byte("\r\n"), []byte("\n"))
  2412  			if d := testy.DiffText(expected, string(result)); d != nil {
  2413  				t.Error(d)
  2414  			}
  2415  		})
  2416  	}
  2417  }
  2418  
  2419  func TestAttachmentStubs(t *testing.T) {
  2420  	tests := []struct {
  2421  		name     string
  2422  		atts     *kivik.Attachments
  2423  		expected map[string]*stub
  2424  	}{
  2425  		{
  2426  			name: "simple",
  2427  			atts: &kivik.Attachments{
  2428  				"foo.txt": &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("test content")},
  2429  			},
  2430  			expected: map[string]*stub{
  2431  				"foo.txt": {
  2432  					ContentType: "text/plain",
  2433  					Size:        13,
  2434  				},
  2435  			},
  2436  		},
  2437  	}
  2438  	for _, test := range tests {
  2439  		t.Run(test.name, func(t *testing.T) {
  2440  			result, _ := attachmentStubs(test.atts)
  2441  			if d := testy.DiffInterface(test.expected, result); d != nil {
  2442  				t.Error(d)
  2443  			}
  2444  		})
  2445  	}
  2446  }
  2447  
  2448  func TestInterfaceToAttachments(t *testing.T) {
  2449  	tests := []struct {
  2450  		name     string
  2451  		input    interface{}
  2452  		output   interface{}
  2453  		expected *kivik.Attachments
  2454  		ok       bool
  2455  	}{
  2456  		{
  2457  			name:     "non-attachment input",
  2458  			input:    "foo",
  2459  			output:   "foo",
  2460  			expected: nil,
  2461  			ok:       false,
  2462  		},
  2463  		{
  2464  			name: "pointer input",
  2465  			input: &kivik.Attachments{
  2466  				"foo.txt": nil,
  2467  			},
  2468  			output: new(kivik.Attachments),
  2469  			expected: &kivik.Attachments{
  2470  				"foo.txt": nil,
  2471  			},
  2472  			ok: true,
  2473  		},
  2474  		{
  2475  			name: "non-pointer input",
  2476  			input: kivik.Attachments{
  2477  				"foo.txt": nil,
  2478  			},
  2479  			output: kivik.Attachments{},
  2480  			expected: &kivik.Attachments{
  2481  				"foo.txt": nil,
  2482  			},
  2483  			ok: true,
  2484  		},
  2485  	}
  2486  	for _, test := range tests {
  2487  		t.Run(test.name, func(t *testing.T) {
  2488  			result, ok := interfaceToAttachments(test.input)
  2489  			if ok != test.ok {
  2490  				t.Errorf("Unexpected OK result: %v", result)
  2491  			}
  2492  			if d := testy.DiffInterface(test.expected, result); d != nil {
  2493  				t.Errorf("Unexpected result:\n%s\n", d)
  2494  			}
  2495  			if d := testy.DiffInterface(test.output, test.input); d != nil {
  2496  				t.Errorf("Input not properly modified:\n%s\n", d)
  2497  			}
  2498  		})
  2499  	}
  2500  }
  2501  
  2502  func TestStubMarshalJSON(t *testing.T) {
  2503  	att := &stub{
  2504  		ContentType: "text/plain",
  2505  		Size:        123,
  2506  	}
  2507  	expected := `{"content_type":"text/plain","length":123,"follows":true}`
  2508  	result, err := json.Marshal(att)
  2509  	if !testy.ErrorMatches("", err) {
  2510  		t.Errorf("Unexpected error: %s", err)
  2511  	}
  2512  	if d := testy.DiffJSON([]byte(expected), result); d != nil {
  2513  		t.Error(d)
  2514  	}
  2515  }
  2516  
  2517  func Test_attachmentSize(t *testing.T) {
  2518  	type tst struct {
  2519  		att      *kivik.Attachment
  2520  		expected *kivik.Attachment
  2521  		err      string
  2522  	}
  2523  	tests := testy.NewTable()
  2524  	tests.Add("size already set", tst{
  2525  		att:      &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text"), Size: 4},
  2526  		expected: &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text"), Size: 4},
  2527  	})
  2528  	tests.Add("bytes buffer", tst{
  2529  		att:      &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text")},
  2530  		expected: &kivik.Attachment{Filename: "foo.txt", ContentType: "text/plain", Content: Body("text"), Size: 5},
  2531  	})
  2532  	tests.Run(t, func(t *testing.T, test tst) {
  2533  		err := attachmentSize(test.att)
  2534  		if !testy.ErrorMatches(test.err, err) {
  2535  			t.Errorf("Unexpected error: %s", err)
  2536  		}
  2537  		body, err := io.ReadAll(test.att.Content)
  2538  		if err != nil {
  2539  			t.Fatal(err)
  2540  		}
  2541  		expBody, err := io.ReadAll(test.expected.Content)
  2542  		if err != nil {
  2543  			t.Fatal(err)
  2544  		}
  2545  		if d := testy.DiffText(expBody, body); d != nil {
  2546  			t.Errorf("Content differs:\n%s\n", d)
  2547  		}
  2548  		test.att.Content = nil
  2549  		test.expected.Content = nil
  2550  		if d := testy.DiffInterface(test.expected, test.att); d != nil {
  2551  			t.Error(d)
  2552  		}
  2553  	})
  2554  }
  2555  
  2556  type lenReader interface {
  2557  	io.Reader
  2558  	lener
  2559  }
  2560  
  2561  type myReader struct {
  2562  	lenReader
  2563  }
  2564  
  2565  var _ interface {
  2566  	io.Closer
  2567  	lenReader
  2568  } = &myReader{}
  2569  
  2570  func (r *myReader) Close() error { return nil }
  2571  
  2572  func Test_readerSize(t *testing.T) {
  2573  	type tst struct {
  2574  		in   io.ReadCloser
  2575  		size int64
  2576  		body string
  2577  		err  string
  2578  	}
  2579  	tests := testy.NewTable()
  2580  	tests.Add("*bytes.Buffer", tst{
  2581  		in:   &myReader{bytes.NewBuffer([]byte("foo bar"))},
  2582  		size: 7,
  2583  		body: "foo bar",
  2584  	})
  2585  	tests.Add("bytes.NewReader", tst{
  2586  		in:   &myReader{bytes.NewReader([]byte("foo bar"))},
  2587  		size: 7,
  2588  		body: "foo bar",
  2589  	})
  2590  	tests.Add("strings.NewReader", tst{
  2591  		in:   &myReader{strings.NewReader("foo bar")},
  2592  		size: 7,
  2593  		body: "foo bar",
  2594  	})
  2595  	tests.Add("file", func(t *testing.T) interface{} {
  2596  		f, err := os.CreateTemp("", "file-reader-*")
  2597  		if err != nil {
  2598  			t.Fatal(err)
  2599  		}
  2600  		tests.Cleanup(func() {
  2601  			_ = os.Remove(f.Name())
  2602  		})
  2603  		if _, err := f.Write([]byte("foo bar")); err != nil {
  2604  			t.Fatal(err)
  2605  		}
  2606  		if _, err := f.Seek(0, 0); err != nil {
  2607  			t.Fatal(err)
  2608  		}
  2609  		return tst{
  2610  			in:   f,
  2611  			size: 7,
  2612  			body: "foo bar",
  2613  		}
  2614  	})
  2615  	tests.Add("nop closer", tst{
  2616  		in:   io.NopCloser(strings.NewReader("foo bar")),
  2617  		size: 7,
  2618  		body: "foo bar",
  2619  	})
  2620  	tests.Add("seeker", tst{
  2621  		in:   &seeker{strings.NewReader("asdf asdf")},
  2622  		size: 9,
  2623  		body: "asdf asdf",
  2624  	})
  2625  	tests.Run(t, func(t *testing.T, test tst) {
  2626  		size, r, err := readerSize(test.in)
  2627  		if !testy.ErrorMatches(test.err, err) {
  2628  			t.Errorf("Unexpected error: %s", err)
  2629  		}
  2630  		body, err := io.ReadAll(r)
  2631  		if err != nil {
  2632  			t.Fatal(err)
  2633  		}
  2634  		if d := testy.DiffText(test.body, body); d != nil {
  2635  			t.Errorf("Unexpected body content:\n%s\n", d)
  2636  		}
  2637  		if size != test.size {
  2638  			t.Errorf("Unexpected size: %d\n", size)
  2639  		}
  2640  	})
  2641  }
  2642  
  2643  type seeker struct {
  2644  	r *strings.Reader
  2645  }
  2646  
  2647  func (s *seeker) Read(b []byte) (int, error) {
  2648  	return s.r.Read(b)
  2649  }
  2650  
  2651  func (s *seeker) Seek(offset int64, whence int) (int64, error) {
  2652  	return s.r.Seek(offset, whence)
  2653  }
  2654  
  2655  func (s *seeker) Close() error { return nil }
  2656  
  2657  func TestNewAttachment(t *testing.T) {
  2658  	type tst struct {
  2659  		content    io.Reader
  2660  		size       []int64
  2661  		expected   *kivik.Attachment
  2662  		expContent string
  2663  		err        string
  2664  	}
  2665  	tests := testy.NewTable()
  2666  	tests.Add("size provided", tst{
  2667  		content: strings.NewReader("xxx"),
  2668  		size:    []int64{99},
  2669  		expected: &kivik.Attachment{
  2670  			Filename:    "foo.txt",
  2671  			ContentType: "text/plain",
  2672  			Size:        99,
  2673  		},
  2674  		expContent: "xxx",
  2675  	})
  2676  	tests.Add("strings.NewReader", tst{
  2677  		content: strings.NewReader("xxx"),
  2678  		expected: &kivik.Attachment{
  2679  			Filename:    "foo.txt",
  2680  			ContentType: "text/plain",
  2681  			Size:        3,
  2682  		},
  2683  		expContent: "xxx",
  2684  	})
  2685  	tests.Run(t, func(t *testing.T, test tst) {
  2686  		result, err := NewAttachment("foo.txt", "text/plain", test.content, test.size...)
  2687  		if !testy.ErrorMatches(test.err, err) {
  2688  			t.Errorf("Unexpected error: %s", err)
  2689  		}
  2690  		content, err := io.ReadAll(result.Content)
  2691  		if err != nil {
  2692  			t.Fatal(err)
  2693  		}
  2694  		if d := testy.DiffText(test.expContent, content); d != nil {
  2695  			t.Errorf("Unexpected content:\n%s\n", d)
  2696  		}
  2697  		result.Content = nil
  2698  		if d := testy.DiffInterface(test.expected, result); d != nil {
  2699  			t.Error(d)
  2700  		}
  2701  	})
  2702  }
  2703  
  2704  func TestCopyWithAttachmentStubs(t *testing.T) {
  2705  	type tst struct {
  2706  		input    io.Reader
  2707  		w        io.Writer
  2708  		expected string
  2709  		atts     map[string]*stub
  2710  		status   int
  2711  		err      string
  2712  	}
  2713  	tests := testy.NewTable()
  2714  	tests.Add("no attachments", tst{
  2715  		input:    strings.NewReader("{}"),
  2716  		expected: "{}",
  2717  	})
  2718  	tests.Add("Unexpected delim", tst{
  2719  		input:  strings.NewReader("[]"),
  2720  		status: http.StatusBadRequest,
  2721  		err:    `^expected '{', found '\['$`,
  2722  	})
  2723  	tests.Add("read error", tst{
  2724  		input:  testy.ErrorReader("", errors.New("read error")),
  2725  		status: http.StatusInternalServerError,
  2726  		err:    "^read error$",
  2727  	})
  2728  	tests.Add("write error", tst{
  2729  		input:  strings.NewReader("{}"),
  2730  		w:      testy.ErrorWriter(0, errors.New("write error")),
  2731  		status: http.StatusInternalServerError,
  2732  		err:    "^write error$",
  2733  	})
  2734  	tests.Add("decode error", tst{
  2735  		input:  strings.NewReader("{}}"),
  2736  		status: http.StatusBadRequest,
  2737  		err:    "^invalid character '}' +looking for beginning of value$",
  2738  	})
  2739  	tests.Add("one attachment", tst{
  2740  		input: strings.NewReader(`{"_attachments":{}}`),
  2741  		atts: map[string]*stub{
  2742  			"foo.txt": {
  2743  				ContentType: "text/plain",
  2744  				Size:        3,
  2745  			},
  2746  		},
  2747  		expected: `{"_attachments":{"foo.txt":{"content_type":"text/plain","length":3,"follows":true}}
  2748  }`,
  2749  	})
  2750  
  2751  	tests.Run(t, func(t *testing.T, test tst) {
  2752  		w := test.w
  2753  		if w == nil {
  2754  			w = &bytes.Buffer{}
  2755  		}
  2756  		err := copyWithAttachmentStubs(w, test.input, test.atts)
  2757  		if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
  2758  			t.Error(d)
  2759  		}
  2760  		if err != nil {
  2761  			return
  2762  		}
  2763  		if d := testy.DiffText(test.expected, w.(*bytes.Buffer).String()); d != nil {
  2764  			t.Error(d)
  2765  		}
  2766  	})
  2767  }
  2768  
  2769  func TestRevsDiff(t *testing.T) {
  2770  	type tt struct {
  2771  		db     *db
  2772  		revMap map[string][]string
  2773  		status int
  2774  		err    string
  2775  	}
  2776  	tests := testy.NewTable()
  2777  	tests.Add("net error", tt{
  2778  		db:     newTestDB(nil, errors.New("net error")),
  2779  		status: http.StatusBadGateway,
  2780  		err:    `Post "?http://example.com/testdb/_revs_diff"?: net error`,
  2781  	})
  2782  	tests.Add("success", tt{
  2783  		db: newCustomDB(func(r *http.Request) (*http.Response, error) {
  2784  			expectedBody := json.RawMessage(`{
  2785  				"190f721ca3411be7aa9477db5f948bbb": [
  2786  					"3-bb72a7682290f94a985f7afac8b27137",
  2787  					"4-10265e5a26d807a3cfa459cf1a82ef2e",
  2788  					"5-067a00dff5e02add41819138abb3284d"
  2789  				]
  2790  			}`)
  2791  			defer r.Body.Close() // nolint: errcheck
  2792  			if d := testy.DiffAsJSON(expectedBody, r.Body); d != nil {
  2793  				return nil, fmt.Errorf("Unexpected payload: %s", d)
  2794  			}
  2795  
  2796  			return &http.Response{
  2797  				StatusCode: http.StatusOK,
  2798  				Body: io.NopCloser(strings.NewReader(`{
  2799  					"190f721ca3411be7aa9477db5f948bbb": {
  2800  						"missing": [
  2801  							"3-bb72a7682290f94a985f7afac8b27137",
  2802  							"5-067a00dff5e02add41819138abb3284d"
  2803  						],
  2804  						"possible_ancestors": [
  2805  							"4-10265e5a26d807a3cfa459cf1a82ef2e"
  2806  						]
  2807  					},
  2808  					"foo": {
  2809  						"missing": ["1-xxx"]
  2810  					}
  2811  				}`)),
  2812  			}, nil
  2813  		}),
  2814  		revMap: map[string][]string{
  2815  			"190f721ca3411be7aa9477db5f948bbb": {
  2816  				"3-bb72a7682290f94a985f7afac8b27137",
  2817  				"4-10265e5a26d807a3cfa459cf1a82ef2e",
  2818  				"5-067a00dff5e02add41819138abb3284d",
  2819  			},
  2820  		},
  2821  	})
  2822  
  2823  	tests.Run(t, func(t *testing.T, tt tt) {
  2824  		ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second)
  2825  		defer cancel()
  2826  		rows, err := tt.db.RevsDiff(ctx, tt.revMap)
  2827  		if d := internal.StatusErrorDiffRE(tt.err, tt.status, err); d != "" {
  2828  			t.Error(d)
  2829  		}
  2830  		if err != nil {
  2831  			return
  2832  		}
  2833  		results := make(map[string]interface{})
  2834  		drow := new(driver.Row)
  2835  		for {
  2836  			if err := rows.Next(drow); err != nil {
  2837  				if err == io.EOF {
  2838  					break
  2839  				}
  2840  				t.Fatal(err)
  2841  			}
  2842  			var row interface{}
  2843  			if err := json.NewDecoder(drow.Value).Decode(&row); err != nil {
  2844  				t.Fatal(err)
  2845  			}
  2846  			results[drow.ID] = row
  2847  		}
  2848  		if d := testy.DiffAsJSON(testy.Snapshot(t), results); d != nil {
  2849  			t.Error(d)
  2850  		}
  2851  	})
  2852  }
  2853  

View as plain text