1
2
3
4
5
6
7
8
9
10
11
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