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