1package couchdb 2 3import ( 4 "context" 5 "encoding/json" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "strings" 10 "testing" 11 12 "github.com/flimzy/diff" 13 "github.com/flimzy/testy" 14 15 "github.com/go-kivik/kivik" 16 "github.com/go-kivik/kivik/driver" 17 "github.com/go-kivik/kivik/errors" 18) 19 20func TestBulkDocs(t *testing.T) { 21 tests := []struct { 22 name string 23 db *db 24 docs []interface{} 25 options map[string]interface{} 26 status int 27 err string 28 }{ 29 { 30 name: "network error", 31 db: newTestDB(nil, errors.New("net error")), 32 status: kivik.StatusNetworkError, 33 err: "Post http://example.com/testdb/_bulk_docs: net error", 34 }, 35 { 36 name: "JSON encoding error", 37 db: newTestDB(&http.Response{ 38 StatusCode: kivik.StatusOK, 39 Body: ioutil.NopCloser(strings.NewReader("")), 40 }, nil), 41 docs: []interface{}{make(chan int)}, 42 status: kivik.StatusBadRequest, 43 err: "Post http://example.com/testdb/_bulk_docs: json: unsupported type: chan int", 44 }, 45 { 46 name: "docs rejected", 47 db: newTestDB(&http.Response{ 48 StatusCode: kivik.StatusExpectationFailed, 49 Body: ioutil.NopCloser(strings.NewReader("[]")), 50 }, nil), 51 docs: []interface{}{1, 2, 3}, 52 status: kivik.StatusExpectationFailed, 53 err: "Expectation Failed: one or more document was rejected", 54 }, 55 { 56 name: "error response", 57 db: newTestDB(&http.Response{ 58 StatusCode: kivik.StatusBadRequest, 59 Body: ioutil.NopCloser(strings.NewReader("")), 60 }, nil), 61 docs: []interface{}{1, 2, 3}, 62 status: kivik.StatusBadRequest, 63 err: "Bad Request", 64 }, 65 { 66 name: "invalid JSON response", 67 db: newTestDB(&http.Response{ 68 StatusCode: kivik.StatusCreated, 69 Body: ioutil.NopCloser(strings.NewReader("invalid json")), 70 }, nil), 71 docs: []interface{}{1, 2, 3}, 72 status: kivik.StatusBadResponse, 73 err: "no closing delimiter: invalid character 'i' looking for beginning of value", 74 }, 75 { 76 name: "unexpected response code", 77 db: newTestDB(&http.Response{ 78 StatusCode: kivik.StatusOK, 79 Body: ioutil.NopCloser(strings.NewReader("[]")), 80 }, nil), 81 docs: []interface{}{1, 2, 3}, 82 }, 83 { 84 name: "new_edits", 85 options: map[string]interface{}{"new_edits": true}, 86 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 87 defer req.Body.Close() // nolint: errcheck 88 var body struct { 89 NewEdits bool `json:"new_edits"` 90 } 91 if err := json.NewDecoder(req.Body).Decode(&body); err != nil { 92 return nil, err 93 } 94 if !body.NewEdits { 95 return nil, errors.New("`new_edits` not set") 96 } 97 return &http.Response{ 98 StatusCode: kivik.StatusCreated, 99 Body: ioutil.NopCloser(strings.NewReader("[]")), 100 }, nil 101 }), 102 }, 103 { 104 name: "full commit", 105 options: map[string]interface{}{OptionFullCommit: true}, 106 db: newCustomDB(func(req *http.Request) (*http.Response, error) { 107 defer req.Body.Close() // nolint: errcheck 108 var body map[string]interface{} 109 if err := json.NewDecoder(req.Body).Decode(&body); err != nil { 110 return nil, err 111 } 112 if _, ok := body[OptionFullCommit]; ok { 113 return nil, errors.New("Full Commit key found in body") 114 } 115 if value := req.Header.Get("X-Couch-Full-Commit"); value != "true" { 116 return nil, errors.New("X-Couch-Full-Commit not set to true") 117 } 118 return &http.Response{ 119 StatusCode: kivik.StatusCreated, 120 Body: ioutil.NopCloser(strings.NewReader("[]")), 121 }, nil 122 }), 123 }, 124 { 125 name: "invalid full commit type", 126 db: &db{}, 127 options: map[string]interface{}{OptionFullCommit: 123}, 128 status: kivik.StatusBadRequest, 129 err: "kivik: option 'X-Couch-Full-Commit' must be bool, not int", 130 }, 131 } 132 for _, test := range tests { 133 t.Run(test.name, func(t *testing.T) { 134 _, err := test.db.BulkDocs(context.Background(), test.docs, test.options) 135 testy.StatusError(t, test.err, test.status, err) 136 }) 137 } 138} 139 140func TestBulkNext(t *testing.T) { 141 tests := []struct { 142 name string 143 results *bulkResults 144 status int 145 err string 146 expected *driver.BulkResult 147 }{ 148 { 149 name: "no results", 150 results: func() *bulkResults { 151 r, err := newBulkResults(Body(`[]`)) 152 if err != nil { 153 t.Fatal(err) 154 } 155 return r 156 }(), 157 status: 500, 158 err: "EOF", 159 }, 160 { 161 name: "closing delimiter missing", 162 results: func() *bulkResults { 163 r, err := newBulkResults(Body(`[`)) 164 if err != nil { 165 t.Fatal(err) 166 } 167 return r 168 }(), 169 status: kivik.StatusBadResponse, 170 err: "no closing delimiter: EOF", 171 }, 172 { 173 name: "invalid doc json", 174 results: func() *bulkResults { 175 r, err := newBulkResults(Body(`[{foo}]`)) 176 if err != nil { 177 t.Fatal(err) 178 } 179 return r 180 }(), 181 status: kivik.StatusBadResponse, 182 err: "invalid character 'f' looking for beginning of object key string", 183 }, 184 { 185 name: "successful update", 186 results: func() *bulkResults { 187 r, err := newBulkResults(Body(`[{"id":"foo","rev":"1-xxx"}]`)) 188 if err != nil { 189 t.Fatal(err) 190 } 191 return r 192 }(), 193 expected: &driver.BulkResult{ 194 ID: "foo", 195 Rev: "1-xxx", 196 }, 197 }, 198 { 199 name: "conflict", 200 results: func() *bulkResults { 201 r, err := newBulkResults(Body(`[{"id":"foo","error":"conflict","reason":"annoying conflict"}]`)) 202 if err != nil { 203 t.Fatal(err) 204 } 205 return r 206 }(), 207 expected: &driver.BulkResult{ 208 ID: "foo", 209 Error: errors.Status(kivik.StatusConflict, "annoying conflict"), 210 }, 211 }, 212 { 213 name: "unknown error", 214 results: func() *bulkResults { 215 r, err := newBulkResults(Body(`[{"id":"foo","error":"foo","reason":"foo is erroneous"}]`)) 216 if err != nil { 217 t.Fatal(err) 218 } 219 return r 220 }(), 221 expected: &driver.BulkResult{ 222 ID: "foo", 223 Error: errors.Status(kivik.StatusUnknownError, "foo is erroneous"), 224 }, 225 }, 226 } 227 for _, test := range tests { 228 t.Run(test.name, func(t *testing.T) { 229 result := new(driver.BulkResult) 230 err := test.results.Next(result) 231 testy.StatusError(t, test.err, test.status, err) 232 if d := diff.Interface(test.expected, result); d != nil { 233 t.Error(d) 234 } 235 }) 236 } 237} 238 239type closeTracker struct { 240 closed bool 241 io.ReadCloser 242} 243 244func (c *closeTracker) Close() error { 245 c.closed = true 246 return c.ReadCloser.Close() 247} 248 249func TestBulkClose(t *testing.T) { 250 body := &closeTracker{ 251 ReadCloser: Body(`[{"id":"foo","error":"foo","reason":"foo is erroneous"}]`), 252 } 253 r, err := newBulkResults(body) 254 if err != nil { 255 t.Fatal(err) 256 } 257 if e := r.Close(); e != nil { 258 t.Fatal(e) 259 } 260 if !body.closed { 261 t.Errorf("Failed to close") 262 } 263} 264