1
2
3
4
5
6
7
8
9
10
11
12
13 package couchdb
14
15 import (
16 "context"
17 "encoding/json"
18 "errors"
19 "fmt"
20 "io"
21 "net/http"
22 "strings"
23 "testing"
24
25 "gitlab.com/flimzy/testy"
26
27 kivik "github.com/go-kivik/kivik/v4"
28 "github.com/go-kivik/kivik/v4/driver"
29 internal "github.com/go-kivik/kivik/v4/int/errors"
30 "github.com/go-kivik/kivik/v4/int/mock"
31 )
32
33 func TestExplain(t *testing.T) {
34 tests := []struct {
35 name string
36 db *db
37 query interface{}
38 options kivik.Option
39 expected *driver.QueryPlan
40 status int
41 err string
42 }{
43 {
44 name: "invalid query",
45 db: newTestDB(nil, nil),
46 query: make(chan int),
47 status: http.StatusBadRequest,
48 err: `Post "?http://example.com/testdb/_explain"?: json: unsupported type: chan int`,
49 },
50 {
51 name: "network error",
52 db: newTestDB(nil, errors.New("net error")),
53 status: http.StatusBadGateway,
54 err: `Post "?http://example.com/testdb/_explain"?: net error`,
55 },
56 {
57 name: "error response",
58 db: newTestDB(&http.Response{
59 StatusCode: http.StatusNotFound,
60 Body: io.NopCloser(strings.NewReader("")),
61 }, nil),
62 status: http.StatusNotFound,
63 err: "Not Found",
64 },
65 {
66 name: "success",
67 db: newTestDB(&http.Response{
68 StatusCode: http.StatusOK,
69 Body: io.NopCloser(strings.NewReader(`{"dbname":"foo"}`)),
70 }, nil),
71 expected: &driver.QueryPlan{DBName: "foo"},
72 },
73 {
74 name: "raw query",
75 db: newCustomDB(func(req *http.Request) (*http.Response, error) {
76 defer req.Body.Close()
77 var result interface{}
78 if err := json.NewDecoder(req.Body).Decode(&result); err != nil {
79 return nil, fmt.Errorf("decode error: %s", err)
80 }
81 expected := map[string]interface{}{"_id": "foo"}
82 if d := testy.DiffInterface(expected, result); d != nil {
83 return nil, fmt.Errorf("unexpected result:\n%s", d)
84 }
85 return nil, errors.New("success")
86 }),
87 query: []byte(`{"_id":"foo"}`),
88 status: http.StatusBadGateway,
89 err: `Post "?http://example.com/testdb/_explain"?: success`,
90 },
91 {
92 name: "partitioned request",
93 db: newTestDB(nil, errors.New("expected")),
94 options: OptionPartition("x1"),
95 status: http.StatusBadGateway,
96 err: `Post "?http://example.com/testdb/_partition/x1/_explain"?: expected`,
97 },
98 }
99 for _, test := range tests {
100 t.Run(test.name, func(t *testing.T) {
101 opts := test.options
102 if opts == nil {
103 opts = mock.NilOption
104 }
105 result, err := test.db.Explain(context.Background(), test.query, opts)
106 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
107 t.Error(d)
108 }
109 if d := testy.DiffInterface(test.expected, result); d != nil {
110 t.Error(d)
111 }
112 })
113 }
114 }
115
116 func TestUnmarshalQueryPlan(t *testing.T) {
117 tests := []struct {
118 name string
119 input string
120 expected *queryPlan
121 err string
122 }{
123 {
124 name: "non-array",
125 input: `{"fields":{}}`,
126 err: "json: cannot unmarshal object into Go",
127 },
128 {
129 name: "all_fields",
130 input: `{"fields":"all_fields","dbname":"foo"}`,
131 expected: &queryPlan{DBName: "foo"},
132 },
133 {
134 name: "simple field list",
135 input: `{"fields":["foo","bar"],"dbname":"foo"}`,
136 expected: &queryPlan{Fields: []interface{}{"foo", "bar"}, DBName: "foo"},
137 },
138 {
139 name: "complex field list",
140 input: `{"dbname":"foo", "fields":[{"foo":"asc"},{"bar":"desc"}]}`,
141 expected: &queryPlan{
142 DBName: "foo",
143 Fields: []interface{}{
144 map[string]interface{}{"foo": "asc"},
145 map[string]interface{}{"bar": "desc"},
146 },
147 },
148 },
149 {
150 name: "invalid bare string",
151 input: `{"fields":"not_all_fields"}`,
152 err: "json: cannot unmarshal string into Go",
153 },
154 }
155 for _, test := range tests {
156 t.Run(test.name, func(t *testing.T) {
157 result := new(queryPlan)
158 err := json.Unmarshal([]byte(test.input), &result)
159 if !testy.ErrorMatchesRE(test.err, err) {
160 t.Errorf("Unexpected error: %s", err)
161 }
162 if err != nil {
163 return
164 }
165 if d := testy.DiffInterface(test.expected, result); d != nil {
166 t.Error(d)
167 }
168 })
169 }
170 }
171
172 func TestCreateIndex(t *testing.T) {
173 tests := []struct {
174 name string
175 ddoc, indexName string
176 index interface{}
177 options kivik.Option
178 db *db
179 status int
180 err string
181 }{
182 {
183 name: "invalid JSON index",
184 db: newTestDB(nil, nil),
185 index: `invalid json`,
186 status: http.StatusBadRequest,
187 err: "invalid character 'i' looking for beginning of value",
188 },
189 {
190 name: "invalid raw index",
191 db: newTestDB(nil, nil),
192 index: map[string]interface{}{"foo": make(chan int)},
193 status: http.StatusBadRequest,
194 err: `Post "?http://example.com/testdb/_index"?: json: unsupported type: chan int`,
195 },
196 {
197 name: "network error",
198 db: newTestDB(nil, errors.New("net error")),
199 status: http.StatusBadGateway,
200 err: `Post "?http://example.com/testdb/_index"?: net error`,
201 },
202 {
203 name: "success 2.1.0",
204 db: newTestDB(&http.Response{
205 StatusCode: 200,
206 Header: http.Header{
207 "X-CouchDB-Body-Time": {"0"},
208 "X-Couch-Request-ID": {"8e4aef0c2f"},
209 "Server": {"CouchDB/2.1.0 (Erlang OTP/17)"},
210 "Date": {"Fri, 27 Oct 2017 18:14:38 GMT"},
211 "Content-Type": {"application/json"},
212 "Content-Length": {"126"},
213 "Cache-Control": {"must-revalidate"},
214 },
215 Body: Body(`{"result":"created","id":"_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea","name":"a7ee061f1a2c0c6882258b2f1e148b714e79ccea"}`),
216 }, nil),
217 },
218 {
219 name: "partitioned query",
220 db: newTestDB(nil, errors.New("expected")),
221 options: OptionPartition("xxy"),
222 status: http.StatusBadGateway,
223 err: `Post "?http://example.com/testdb/_partition/xxy/_index"?: expected`,
224 },
225 }
226 for _, test := range tests {
227 t.Run(test.name, func(t *testing.T) {
228 opts := test.options
229 if opts == nil {
230 opts = mock.NilOption
231 }
232 err := test.db.CreateIndex(context.Background(), test.ddoc, test.indexName, test.index, opts)
233 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
234 t.Error(d)
235 }
236 })
237 }
238 }
239
240 func TestGetIndexes(t *testing.T) {
241 tests := []struct {
242 name string
243 options kivik.Option
244 db *db
245 expected []driver.Index
246 status int
247 err string
248 }{
249 {
250 name: "network error",
251 db: newTestDB(nil, errors.New("net error")),
252 status: http.StatusBadGateway,
253 err: `Get "?http://example.com/testdb/_index"?: net error`,
254 },
255 {
256 name: "2.1.0",
257 db: newTestDB(&http.Response{
258 StatusCode: 200,
259 Header: http.Header{
260 "X-CouchDB-Body-Time": {"0"},
261 "X-Couch-Request-ID": {"f44881735c"},
262 "Server": {"CouchDB/2.1.0 (Erlang OTP/17)"},
263 "Date": {"Fri, 27 Oct 2017 18:23:29 GMT"},
264 "Content-Type": {"application/json"},
265 "Content-Length": {"269"},
266 "Cache-Control": {"must-revalidate"},
267 },
268 Body: Body(`{"total_rows":2,"indexes":[{"ddoc":null,"name":"_all_docs","type":"special","def":{"fields":[{"_id":"asc"}]}},{"ddoc":"_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea","name":"a7ee061f1a2c0c6882258b2f1e148b714e79ccea","type":"json","def":{"fields":[{"foo":"asc"}]}}]}`),
269 }, nil),
270 expected: []driver.Index{
271 {
272 Name: "_all_docs",
273 Type: "special",
274 Definition: map[string]interface{}{
275 "fields": []interface{}{
276 map[string]interface{}{"_id": "asc"},
277 },
278 },
279 },
280 {
281 DesignDoc: "_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
282 Name: "a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
283 Type: "json",
284 Definition: map[string]interface{}{
285 "fields": []interface{}{
286 map[string]interface{}{"foo": "asc"},
287 },
288 },
289 },
290 },
291 },
292 {
293 name: "partitioned query",
294 db: newTestDB(nil, errors.New("expected")),
295 options: OptionPartition("yyz"),
296 status: http.StatusBadGateway,
297 err: `Get "?http://example.com/testdb/_partition/yyz/_index"?: expected`,
298 },
299 }
300 for _, test := range tests {
301 t.Run(test.name, func(t *testing.T) {
302 opts := test.options
303 if opts == nil {
304 opts = mock.NilOption
305 }
306 result, err := test.db.GetIndexes(context.Background(), opts)
307 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
308 t.Error(d)
309 }
310 if d := testy.DiffInterface(test.expected, result); d != nil {
311 t.Error(d)
312 }
313 })
314 }
315 }
316
317 func TestDeleteIndex(t *testing.T) {
318 tests := []struct {
319 name string
320 ddoc, indexName string
321 options kivik.Option
322 db *db
323 status int
324 err string
325 }{
326 {
327 name: "no ddoc",
328 status: http.StatusBadRequest,
329 db: newTestDB(nil, nil),
330 err: "kivik: ddoc required",
331 },
332 {
333 name: "no index name",
334 ddoc: "foo",
335 status: http.StatusBadRequest,
336 db: newTestDB(nil, nil),
337 err: "kivik: name required",
338 },
339 {
340 name: "network error",
341 ddoc: "foo",
342 indexName: "bar",
343 db: newTestDB(nil, errors.New("net error")),
344 status: http.StatusBadGateway,
345 err: `^(Delete "?http://example.com/testdb/_index/foo/json/bar"?: )?net error`,
346 },
347 {
348 name: "2.1.0 success",
349 ddoc: "_design/a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
350 indexName: "a7ee061f1a2c0c6882258b2f1e148b714e79ccea",
351 db: newTestDB(&http.Response{
352 StatusCode: 200,
353 Header: http.Header{
354 "X-CouchDB-Body-Time": {"0"},
355 "X-Couch-Request-ID": {"6018a0a693"},
356 "Server": {"CouchDB/2.1.0 (Erlang OTP/17)"},
357 "Date": {"Fri, 27 Oct 2017 19:06:28 GMT"},
358 "Content-Type": {"application/json"},
359 "Content-Length": {"11"},
360 "Cache-Control": {"must-revalidate"},
361 },
362 Body: Body(`{"ok":true}`),
363 }, nil),
364 },
365 {
366 name: "partitioned query",
367 ddoc: "_design/foo",
368 indexName: "bar",
369 db: newTestDB(nil, errors.New("expected")),
370 options: OptionPartition("qqz"),
371 status: http.StatusBadGateway,
372 err: `Delete "?http://example.com/testdb/_partition/qqz/_index/_design/foo/json/bar"?: expected`,
373 },
374 }
375 for _, test := range tests {
376 t.Run(test.name, func(t *testing.T) {
377 opts := test.options
378 if opts == nil {
379 opts = mock.NilOption
380 }
381 err := test.db.DeleteIndex(context.Background(), test.ddoc, test.indexName, opts)
382 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
383 t.Error(d)
384 }
385 })
386 }
387 }
388
389 func TestFind(t *testing.T) {
390 tests := []struct {
391 name string
392 db *db
393 query interface{}
394 options kivik.Option
395 status int
396 err string
397 }{
398 {
399 name: "invalid query json",
400 db: newTestDB(nil, nil),
401 query: make(chan int),
402 status: http.StatusBadRequest,
403 err: `Post "?http://example.com/testdb/_find"?: json: unsupported type: chan int`,
404 },
405 {
406 name: "network error",
407 db: newTestDB(nil, errors.New("net error")),
408 status: http.StatusBadGateway,
409 err: `Post "?http://example.com/testdb/_find"?: net error`,
410 },
411 {
412 name: "error response",
413 db: newTestDB(&http.Response{
414 StatusCode: 415,
415 Header: http.Header{
416 "Content-Type": {"application/json"},
417 "X-CouchDB-Body-Time": {"0"},
418 "X-Couch-Request-ID": {"aa1f852b27"},
419 "Server": {"CouchDB/2.1.0 (Erlang OTP/17)"},
420 "Date": {"Fri, 27 Oct 2017 19:20:04 GMT"},
421 "Content-Length": {"77"},
422 "Cache-Control": {"must-revalidate"},
423 },
424 ContentLength: 77,
425 Body: Body(`{"error":"bad_content_type","reason":"Content-Type must be application/json"}`),
426 }, nil),
427 status: http.StatusUnsupportedMediaType,
428 err: "Unsupported Media Type: Content-Type must be application/json",
429 },
430 {
431 name: "success 2.1.0",
432 query: map[string]interface{}{
433 "selector": map[string]string{"_id": "foo"},
434 },
435 db: newTestDB(&http.Response{
436 StatusCode: 200,
437 Header: http.Header{
438 "Content-Type": {"application/json"},
439 "X-CouchDB-Body-Time": {"0"},
440 "X-Couch-Request-ID": {"a0884508d8"},
441 "Server": {"CouchDB/2.1.0 (Erlang OTP/17)"},
442 "Date": {"Fri, 27 Oct 2017 19:20:04 GMT"},
443 "Transfer-Encoding": {"chunked"},
444 "Cache-Control": {"must-revalidate"},
445 },
446 Body: Body(`{"docs":[
447 {"_id":"foo","_rev":"2-f5d2de1376388f1b54d93654df9dc9c7","_attachments":{"foo.txt":{"content_type":"text/plain","revpos":2,"digest":"md5-ENGoH7oK8V9R3BMnfDHZmw==","length":13,"stub":true}}}
448 ]}`),
449 }, nil),
450 },
451 {
452 name: "partitioned request",
453 db: newTestDB(nil, errors.New("expected")),
454 options: OptionPartition("x2"),
455 status: http.StatusBadGateway,
456 err: `Post "?http://example.com/testdb/_partition/x2/_find"?: expected`,
457 },
458 }
459 for _, test := range tests {
460 t.Run(test.name, func(t *testing.T) {
461 opts := test.options
462 if opts == nil {
463 opts = mock.NilOption
464 }
465 result, err := test.db.Find(context.Background(), test.query, opts)
466 if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
467 t.Error(d)
468 }
469 if err != nil {
470 return
471 }
472 if _, ok := result.(*rows); !ok {
473 t.Errorf("Unexpected type returned: %t", result)
474 }
475 })
476 }
477 }
478
View as plain text