...

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

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

     1  // Licensed under the Apache License, Version 2.0 (the "License"); you may not
     2  // use this file except in compliance with the License. You may obtain a copy of
     3  // the License at
     4  //
     5  //  http://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     9  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    10  // License for the specific language governing permissions and limitations under
    11  // the License.
    12  
    13  package couchdb
    14  
    15  import (
    16  	"context"
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"net/http"
    21  	"testing"
    22  	"time"
    23  
    24  	"gitlab.com/flimzy/testy"
    25  
    26  	kivik "github.com/go-kivik/kivik/v4"
    27  	"github.com/go-kivik/kivik/v4/driver"
    28  	internal "github.com/go-kivik/kivik/v4/int/errors"
    29  	"github.com/go-kivik/kivik/v4/int/mock"
    30  )
    31  
    32  func TestChanges_metadata(t *testing.T) {
    33  	db := newTestDB(&http.Response{
    34  		StatusCode: 200,
    35  		Header:     http.Header{},
    36  		Body: Body(`{"results":[
    37  			{"seq":"1-g1AAAABteJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5kTEXKMBuZmKebGSehK4Yh_Y8FiDJ0ACk_oNMSWTIAgDjASHc","id":"56d164e9566e12cb9dff87d455000f3d","changes":[{"rev":"1-967a00dff5e02add41819138abb3284d"}]},
    38  			{"seq":"2-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5kTEXKMBuZmKebGSehK4Yh_Y8FiDJ0ACk_qOYYm5qYGBklIquJwsAO5gqIA","id":"56d164e9566e12cb9dff87d455001b58","changes":[{"rev":"1-967a00dff5e02add41819138abb3284d"}]},
    39  			{"seq":"3-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5kSkXKMBuZmKebGSehK4Yh_Y8FiDJ0ACk_kNNYQSbYm5qYGBklIquJwsAO_wqIQ","id":"56d164e9566e12cb9dff87d455002462","changes":[{"rev":"1-967a00dff5e02add41819138abb3284d"}]},
    40  			{"seq":"4-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5kSkXKMBuZmKebGSehK4Yh_Y8FiDJ0ACk_qOYYm5qYGBklIquJwsAPBoqIg","id":"56d164e9566e12cb9dff87d455004150","changes":[{"rev":"1-967a00dff5e02add41819138abb3284d"}]},
    41  			{"seq":"5-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5kTkXKMBuZmKebGSehK4Yh_Y8FiDJ0ACk_kNNYQKbYm5qYGBklIquJwsAPH4qIw","id":"56d164e9566e12cb9dff87d455003421","changes":[{"rev":"1-967a00dff5e02add41819138abb3284d"}]}
    42  			],
    43  			"last_seq":"5-g1AAAACLeJzLYWBgYMpgTmHgzcvPy09JdcjLz8gvLskBCScyJNX___8_K4M5kTkXKMBuZmKebGSehK4Yh_Y8FiDJ0ACk_kNNYQKbYm5qYGBklIquJwsAPH4qIw","pending":10}
    44  		`),
    45  	}, nil)
    46  
    47  	changes, err := db.Changes(context.Background(), mock.NilOption)
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  	ch := &driver.Change{}
    52  	for {
    53  		if changes.Next(ch) != nil {
    54  			break
    55  		}
    56  	}
    57  	want := int64(10)
    58  	if got := changes.Pending(); want != got {
    59  		t.Errorf("want: %d, got: %d", want, got)
    60  	}
    61  }
    62  
    63  func TestChanges(t *testing.T) {
    64  	tests := []struct {
    65  		name    string
    66  		options kivik.Option
    67  		db      *db
    68  		status  int
    69  		err     string
    70  		etag    string
    71  	}{
    72  		{
    73  			name: "invalid options",
    74  			db: newTestDB(&http.Response{
    75  				StatusCode: http.StatusBadRequest,
    76  				Body:       Body(""),
    77  			}, nil),
    78  			options: kivik.Param("foo", make(chan int)),
    79  			status:  http.StatusBadRequest,
    80  			err:     "kivik: invalid type chan int for options",
    81  		},
    82  		{
    83  			name:    "eventsource",
    84  			options: kivik.Param("feed", "eventsource"),
    85  			status:  http.StatusBadRequest,
    86  			err:     "kivik: eventsource feed not supported, use 'continuous'",
    87  		},
    88  		{
    89  			name:   "network error",
    90  			db:     newTestDB(nil, errors.New("net error")),
    91  			status: http.StatusBadGateway,
    92  			err:    `Post "?http://example.com/testdb/_changes"?: net error`,
    93  		},
    94  		{
    95  			name:    "continuous",
    96  			db:      newTestDB(nil, errors.New("net error")),
    97  			options: kivik.Param("feed", "continuous"),
    98  			status:  http.StatusBadGateway,
    99  			err:     `Post "?http://example.com/testdb/_changes\?feed=continuous"?: net error`,
   100  		},
   101  		{
   102  			name: "error response",
   103  			db: newTestDB(&http.Response{
   104  				StatusCode: http.StatusBadRequest,
   105  				Body:       Body(""),
   106  			}, nil),
   107  			status: http.StatusBadRequest,
   108  			err:    "Bad Request",
   109  		},
   110  		{
   111  			name: "success 1.6.1",
   112  			db: newTestDB(&http.Response{
   113  				StatusCode: 200,
   114  				Header: http.Header{
   115  					"Transfer-Encoding": {"chunked"},
   116  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   117  					"Date":              {"Fri, 27 Oct 2017 14:43:57 GMT"},
   118  					"Content-Type":      {"text/plain; charset=utf-8"},
   119  					"Cache-Control":     {"must-revalidate"},
   120  					"ETag":              {`"etag-foo"`},
   121  				},
   122  				Body: Body(`{"seq":3,"id":"43734cf3ce6d5a37050c050bb600006b","changes":[{"rev":"2-185ccf92154a9f24a4f4fd12233bf463"}],"deleted":true}`),
   123  			}, nil),
   124  			etag: "etag-foo",
   125  		},
   126  		{
   127  			name: "method post",
   128  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   129  				wantMethod := http.MethodPost
   130  				if req.Method != wantMethod {
   131  					return nil, fmt.Errorf("Unexpected method %v", req.Method)
   132  				}
   133  				if len(req.URL.Query()) > 0 {
   134  					return nil, fmt.Errorf("Unexpected query parameters: %v", req.URL.Query())
   135  				}
   136  				wantCT := typeJSON
   137  				ct := req.Header.Get("Content-Type")
   138  				if wantCT != ct {
   139  					return nil, fmt.Errorf("Unexpected Content-Type: %s", ct)
   140  				}
   141  				wantBody := `null`
   142  				var body []byte
   143  				if req.Body != nil {
   144  					defer req.Body.Close()
   145  					var err error
   146  					body, err = io.ReadAll(req.Body)
   147  					if err != nil {
   148  						t.Fatal(err)
   149  					}
   150  				}
   151  				if d := testy.DiffJSON(wantBody, body); d != nil {
   152  					return nil, fmt.Errorf("Unexpected request body: %s", d)
   153  				}
   154  				return &http.Response{
   155  					StatusCode: 200,
   156  					Header: http.Header{
   157  						"Transfer-Encoding": {"chunked"},
   158  						"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   159  						"Date":              {"Fri, 27 Oct 2017 14:43:57 GMT"},
   160  						"Content-Type":      {"text/plain; charset=utf-8"},
   161  						"Cache-Control":     {"must-revalidate"},
   162  						"ETag":              {`"etag-foo"`},
   163  					},
   164  					Body: Body(`{"seq":3,"id":"43734cf3ce6d5a37050c050bb600006b","changes":[{"rev":"2-185ccf92154a9f24a4f4fd12233bf463"}],"deleted":true}`),
   165  				}, nil
   166  			}),
   167  			etag: "etag-foo",
   168  		},
   169  		{
   170  			name: "doc_ids",
   171  			db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   172  				wantMethod := http.MethodPost
   173  				if req.Method != wantMethod {
   174  					return nil, fmt.Errorf("Unexpected method %v", req.Method)
   175  				}
   176  				if len(req.URL.Query()) > 0 {
   177  					return nil, fmt.Errorf("Unexpected query parameters: %v", req.URL.Query())
   178  				}
   179  				wantCT := typeJSON
   180  				ct := req.Header.Get("Content-Type")
   181  				if wantCT != ct {
   182  					return nil, fmt.Errorf("Unexpected Content-Type: %s", ct)
   183  				}
   184  				wantBody := `{"doc_ids":["a","b","c"]}`
   185  				defer req.Body.Close()
   186  				body, err := io.ReadAll(req.Body)
   187  				if err != nil {
   188  					t.Fatal(err)
   189  				}
   190  				if d := testy.DiffJSON(wantBody, body); d != nil {
   191  					return nil, fmt.Errorf("Unexpected request body: %s", d)
   192  				}
   193  				return &http.Response{
   194  					StatusCode: 200,
   195  					Header: http.Header{
   196  						"Transfer-Encoding": {"chunked"},
   197  						"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   198  						"Date":              {"Fri, 27 Oct 2017 14:43:57 GMT"},
   199  						"Content-Type":      {"text/plain; charset=utf-8"},
   200  						"Cache-Control":     {"must-revalidate"},
   201  						"ETag":              {`"etag-foo"`},
   202  					},
   203  					Body: Body(`{"seq":3,"id":"43734cf3ce6d5a37050c050bb600006b","changes":[{"rev":"2-185ccf92154a9f24a4f4fd12233bf463"}],"deleted":true}`),
   204  				}, nil
   205  			}),
   206  			options: kivik.Param("doc_ids", []string{"a", "b", "c"}),
   207  			etag:    "etag-foo",
   208  		},
   209  	}
   210  
   211  	for _, test := range tests {
   212  		t.Run(test.name, func(t *testing.T) {
   213  			opts := test.options
   214  			if opts == nil {
   215  				opts = mock.NilOption
   216  			}
   217  			ch, err := test.db.Changes(context.Background(), opts)
   218  			if ch != nil {
   219  				t.Cleanup(func() {
   220  					_ = ch.Close()
   221  				})
   222  			}
   223  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   224  				t.Error(d)
   225  			}
   226  			if err != nil {
   227  				return
   228  			}
   229  			if etag := ch.ETag(); etag != test.etag {
   230  				t.Errorf("Unexpected ETag: %s", etag)
   231  			}
   232  		})
   233  	}
   234  }
   235  
   236  func TestChangesNext(t *testing.T) {
   237  	tests := []struct {
   238  		name     string
   239  		changes  *changesRows
   240  		status   int
   241  		err      string
   242  		expected *driver.Change
   243  	}{
   244  		{
   245  			name:    "invalid json",
   246  			changes: newChangesRows(context.TODO(), "", Body("invalid json"), ""),
   247  			status:  http.StatusBadGateway,
   248  			err:     "invalid character 'i' looking for beginning of value",
   249  		},
   250  		{
   251  			name: "success",
   252  			changes: newChangesRows(context.TODO(), "", Body(`{"seq":3,"id":"43734cf3ce6d5a37050c050bb600006b","changes":[{"rev":"2-185ccf92154a9f24a4f4fd12233bf463"}],"deleted":true}
   253                  `), ""),
   254  			expected: &driver.Change{
   255  				ID:      "43734cf3ce6d5a37050c050bb600006b",
   256  				Seq:     "3",
   257  				Deleted: true,
   258  				Changes: []string{"2-185ccf92154a9f24a4f4fd12233bf463"},
   259  			},
   260  		},
   261  		{
   262  			name:    "read error",
   263  			changes: newChangesRows(context.TODO(), "", io.NopCloser(testy.ErrorReader("", errors.New("read error"))), ""),
   264  			status:  http.StatusBadGateway,
   265  			err:     "read error",
   266  		},
   267  		{
   268  			name:     "end of input",
   269  			changes:  newChangesRows(context.TODO(), "", Body(``), ""),
   270  			expected: &driver.Change{},
   271  			status:   http.StatusInternalServerError,
   272  			err:      "EOF",
   273  		},
   274  	}
   275  	for _, test := range tests {
   276  		t.Run(test.name, func(t *testing.T) {
   277  			row := new(driver.Change)
   278  			err := test.changes.Next(row)
   279  			if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
   280  				t.Error(d)
   281  			}
   282  			if err != nil {
   283  				return
   284  			}
   285  			if d := testy.DiffInterface(test.expected, row); d != nil {
   286  				t.Error(d)
   287  			}
   288  		})
   289  	}
   290  }
   291  
   292  func TestChangesClose(t *testing.T) {
   293  	t.Run("normal", func(t *testing.T) {
   294  		body := &closeTracker{ReadCloser: Body("foo")}
   295  		feed := newChangesRows(context.TODO(), "", body, "")
   296  		_ = feed.Close()
   297  		if !body.closed {
   298  			t.Errorf("Failed to close")
   299  		}
   300  	})
   301  
   302  	t.Run("next in progress", func(t *testing.T) {
   303  		body := &closeTracker{ReadCloser: io.NopCloser(testy.NeverReader())}
   304  		feed := newChangesRows(context.TODO(), "", body, "")
   305  		row := new(driver.Change)
   306  		done := make(chan struct{})
   307  		go func() {
   308  			_ = feed.Next(row)
   309  			close(done)
   310  		}()
   311  		time.Sleep(50 * time.Millisecond)
   312  		_ = feed.Close()
   313  		<-done
   314  		if !body.closed {
   315  			t.Errorf("Failed to close")
   316  		}
   317  	})
   318  }
   319  

View as plain text