1// Copyright 2015 The etcd Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package integration
16
17import (
18	"encoding/json"
19	"fmt"
20	"io"
21	"io/ioutil"
22	"net/http"
23	"net/url"
24	"reflect"
25	"strings"
26	"testing"
27	"time"
28
29	"github.com/coreos/etcd/pkg/testutil"
30	"github.com/coreos/etcd/pkg/transport"
31)
32
33func TestV2Set(t *testing.T) {
34	defer testutil.AfterTest(t)
35	cl := NewCluster(t, 1)
36	cl.Launch(t)
37	defer cl.Terminate(t)
38
39	u := cl.URL(0)
40	tc := NewTestClient()
41	v := url.Values{}
42	v.Set("value", "bar")
43	vAndNoValue := url.Values{}
44	vAndNoValue.Set("value", "bar")
45	vAndNoValue.Set("noValueOnSuccess", "true")
46
47	tests := []struct {
48		relativeURL string
49		value       url.Values
50		wStatus     int
51		w           string
52	}{
53		{
54			"/v2/keys/foo/bar",
55			v,
56			http.StatusCreated,
57			`{"action":"set","node":{"key":"/foo/bar","value":"bar","modifiedIndex":4,"createdIndex":4}}`,
58		},
59		{
60			"/v2/keys/foodir?dir=true",
61			url.Values{},
62			http.StatusCreated,
63			`{"action":"set","node":{"key":"/foodir","dir":true,"modifiedIndex":5,"createdIndex":5}}`,
64		},
65		{
66			"/v2/keys/fooempty",
67			url.Values(map[string][]string{"value": {""}}),
68			http.StatusCreated,
69			`{"action":"set","node":{"key":"/fooempty","value":"","modifiedIndex":6,"createdIndex":6}}`,
70		},
71		{
72			"/v2/keys/foo/novalue",
73			vAndNoValue,
74			http.StatusCreated,
75			`{"action":"set"}`,
76		},
77	}
78
79	for i, tt := range tests {
80		resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value)
81		if err != nil {
82			t.Errorf("#%d: err = %v, want nil", i, err)
83		}
84		g := string(tc.ReadBody(resp))
85		w := tt.w + "\n"
86		if g != w {
87			t.Errorf("#%d: body = %v, want %v", i, g, w)
88		}
89		if resp.StatusCode != tt.wStatus {
90			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
91		}
92	}
93}
94
95func TestV2CreateUpdate(t *testing.T) {
96	defer testutil.AfterTest(t)
97	cl := NewCluster(t, 1)
98	cl.Launch(t)
99	defer cl.Terminate(t)
100
101	u := cl.URL(0)
102	tc := NewTestClient()
103
104	tests := []struct {
105		relativeURL string
106		value       url.Values
107		wStatus     int
108		w           map[string]interface{}
109	}{
110		// key with ttl
111		{
112			"/v2/keys/ttl/foo",
113			url.Values(map[string][]string{"value": {"XXX"}, "ttl": {"20"}}),
114			http.StatusCreated,
115			map[string]interface{}{
116				"node": map[string]interface{}{
117					"value": "XXX",
118					"ttl":   float64(20),
119				},
120			},
121		},
122		// key with bad ttl
123		{
124			"/v2/keys/ttl/foo",
125			url.Values(map[string][]string{"value": {"XXX"}, "ttl": {"bad_ttl"}}),
126			http.StatusBadRequest,
127			map[string]interface{}{
128				"errorCode": float64(202),
129				"message":   "The given TTL in POST form is not a number",
130			},
131		},
132		// create key
133		{
134			"/v2/keys/create/foo",
135			url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}}),
136			http.StatusCreated,
137			map[string]interface{}{
138				"node": map[string]interface{}{
139					"value": "XXX",
140				},
141			},
142		},
143		// created key failed
144		{
145			"/v2/keys/create/foo",
146			url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}}),
147			http.StatusPreconditionFailed,
148			map[string]interface{}{
149				"errorCode": float64(105),
150				"message":   "Key already exists",
151				"cause":     "/create/foo",
152			},
153		},
154		// update the newly created key with ttl
155		{
156			"/v2/keys/create/foo",
157			url.Values(map[string][]string{"value": {"YYY"}, "prevExist": {"true"}, "ttl": {"20"}}),
158			http.StatusOK,
159			map[string]interface{}{
160				"node": map[string]interface{}{
161					"value": "YYY",
162					"ttl":   float64(20),
163				},
164				"action": "update",
165			},
166		},
167		// update the ttl to none
168		{
169			"/v2/keys/create/foo",
170			url.Values(map[string][]string{"value": {"ZZZ"}, "prevExist": {"true"}}),
171			http.StatusOK,
172			map[string]interface{}{
173				"node": map[string]interface{}{
174					"value": "ZZZ",
175				},
176				"action": "update",
177			},
178		},
179		// update on a non-existing key
180		{
181			"/v2/keys/nonexist",
182			url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"true"}}),
183			http.StatusNotFound,
184			map[string]interface{}{
185				"errorCode": float64(100),
186				"message":   "Key not found",
187				"cause":     "/nonexist",
188			},
189		},
190		// create with no value on success
191		{
192			"/v2/keys/create/novalue",
193			url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}, "noValueOnSuccess": {"true"}}),
194			http.StatusCreated,
195			map[string]interface{}{},
196		},
197		// update with no value on success
198		{
199			"/v2/keys/create/novalue",
200			url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"true"}, "noValueOnSuccess": {"true"}}),
201			http.StatusOK,
202			map[string]interface{}{},
203		},
204		// created key failed with no value on success
205		{
206			"/v2/keys/create/foo",
207			url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}, "noValueOnSuccess": {"true"}}),
208			http.StatusPreconditionFailed,
209			map[string]interface{}{
210				"errorCode": float64(105),
211				"message":   "Key already exists",
212				"cause":     "/create/foo",
213			},
214		},
215	}
216
217	for i, tt := range tests {
218		resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value)
219		if err != nil {
220			t.Fatalf("#%d: put err = %v, want nil", i, err)
221		}
222		if resp.StatusCode != tt.wStatus {
223			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
224		}
225		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
226			t.Errorf("#%d: %v", i, err)
227		}
228	}
229}
230
231func TestV2CAS(t *testing.T) {
232	defer testutil.AfterTest(t)
233	cl := NewCluster(t, 1)
234	cl.Launch(t)
235	defer cl.Terminate(t)
236
237	u := cl.URL(0)
238	tc := NewTestClient()
239
240	tests := []struct {
241		relativeURL string
242		value       url.Values
243		wStatus     int
244		w           map[string]interface{}
245	}{
246		{
247			"/v2/keys/cas/foo",
248			url.Values(map[string][]string{"value": {"XXX"}}),
249			http.StatusCreated,
250			nil,
251		},
252		{
253			"/v2/keys/cas/foo",
254			url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"4"}}),
255			http.StatusOK,
256			map[string]interface{}{
257				"node": map[string]interface{}{
258					"value":         "YYY",
259					"modifiedIndex": float64(5),
260				},
261				"action": "compareAndSwap",
262			},
263		},
264		{
265			"/v2/keys/cas/foo",
266			url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"10"}}),
267			http.StatusPreconditionFailed,
268			map[string]interface{}{
269				"errorCode": float64(101),
270				"message":   "Compare failed",
271				"cause":     "[10 != 5]",
272				"index":     float64(5),
273			},
274		},
275		{
276			"/v2/keys/cas/foo",
277			url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"bad_index"}}),
278			http.StatusBadRequest,
279			map[string]interface{}{
280				"errorCode": float64(203),
281				"message":   "The given index in POST form is not a number",
282			},
283		},
284		{
285			"/v2/keys/cas/foo",
286			url.Values(map[string][]string{"value": {"ZZZ"}, "prevValue": {"YYY"}}),
287			http.StatusOK,
288			map[string]interface{}{
289				"node": map[string]interface{}{
290					"value": "ZZZ",
291				},
292				"action": "compareAndSwap",
293			},
294		},
295		{
296			"/v2/keys/cas/foo",
297			url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}}),
298			http.StatusPreconditionFailed,
299			map[string]interface{}{
300				"errorCode": float64(101),
301				"message":   "Compare failed",
302				"cause":     "[bad_value != ZZZ]",
303			},
304		},
305		// prevValue is required
306		{
307			"/v2/keys/cas/foo",
308			url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {""}}),
309			http.StatusBadRequest,
310			map[string]interface{}{
311				"errorCode": float64(201),
312			},
313		},
314		{
315			"/v2/keys/cas/foo",
316			url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}, "prevIndex": {"100"}}),
317			http.StatusPreconditionFailed,
318			map[string]interface{}{
319				"errorCode": float64(101),
320				"message":   "Compare failed",
321				"cause":     "[bad_value != ZZZ] [100 != 6]",
322			},
323		},
324		{
325			"/v2/keys/cas/foo",
326			url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"ZZZ"}, "prevIndex": {"100"}}),
327			http.StatusPreconditionFailed,
328			map[string]interface{}{
329				"errorCode": float64(101),
330				"message":   "Compare failed",
331				"cause":     "[100 != 6]",
332			},
333		},
334		{
335			"/v2/keys/cas/foo",
336			url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}, "prevIndex": {"6"}}),
337			http.StatusPreconditionFailed,
338			map[string]interface{}{
339				"errorCode": float64(101),
340				"message":   "Compare failed",
341				"cause":     "[bad_value != ZZZ]",
342			},
343		},
344		{
345			"/v2/keys/cas/foo",
346			url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"6"}, "noValueOnSuccess": {"true"}}),
347			http.StatusOK,
348			map[string]interface{}{
349				"action": "compareAndSwap",
350			},
351		},
352		{
353			"/v2/keys/cas/foo",
354			url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"10"}, "noValueOnSuccess": {"true"}}),
355			http.StatusPreconditionFailed,
356			map[string]interface{}{
357				"errorCode": float64(101),
358				"message":   "Compare failed",
359				"cause":     "[10 != 7]",
360				"index":     float64(7),
361			},
362		},
363	}
364
365	for i, tt := range tests {
366		resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value)
367		if err != nil {
368			t.Fatalf("#%d: put err = %v, want nil", i, err)
369		}
370		if resp.StatusCode != tt.wStatus {
371			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
372		}
373		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
374			t.Errorf("#%d: %v", i, err)
375		}
376	}
377}
378
379func TestV2Delete(t *testing.T) {
380	defer testutil.AfterTest(t)
381	cl := NewCluster(t, 1)
382	cl.Launch(t)
383	defer cl.Terminate(t)
384
385	u := cl.URL(0)
386	tc := NewTestClient()
387
388	v := url.Values{}
389	v.Set("value", "XXX")
390	r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v)
391	if err != nil {
392		t.Error(err)
393	}
394	r.Body.Close()
395	r, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/emptydir?dir=true"), v)
396	if err != nil {
397		t.Error(err)
398	}
399	r.Body.Close()
400	r, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foodir/bar?dir=true"), v)
401	if err != nil {
402		t.Error(err)
403	}
404	r.Body.Close()
405
406	tests := []struct {
407		relativeURL string
408		wStatus     int
409		w           map[string]interface{}
410	}{
411		{
412			"/v2/keys/foo",
413			http.StatusOK,
414			map[string]interface{}{
415				"node": map[string]interface{}{
416					"key": "/foo",
417				},
418				"prevNode": map[string]interface{}{
419					"key":   "/foo",
420					"value": "XXX",
421				},
422				"action": "delete",
423			},
424		},
425		{
426			"/v2/keys/emptydir",
427			http.StatusForbidden,
428			map[string]interface{}{
429				"errorCode": float64(102),
430				"message":   "Not a file",
431				"cause":     "/emptydir",
432			},
433		},
434		{
435			"/v2/keys/emptydir?dir=true",
436			http.StatusOK,
437			nil,
438		},
439		{
440			"/v2/keys/foodir?dir=true",
441			http.StatusForbidden,
442			map[string]interface{}{
443				"errorCode": float64(108),
444				"message":   "Directory not empty",
445				"cause":     "/foodir",
446			},
447		},
448		{
449			"/v2/keys/foodir?recursive=true",
450			http.StatusOK,
451			map[string]interface{}{
452				"node": map[string]interface{}{
453					"key": "/foodir",
454					"dir": true,
455				},
456				"prevNode": map[string]interface{}{
457					"key": "/foodir",
458					"dir": true,
459				},
460				"action": "delete",
461			},
462		},
463	}
464
465	for i, tt := range tests {
466		resp, err := tc.DeleteForm(fmt.Sprintf("%s%s", u, tt.relativeURL), nil)
467		if err != nil {
468			t.Fatalf("#%d: delete err = %v, want nil", i, err)
469		}
470		if resp.StatusCode != tt.wStatus {
471			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
472		}
473		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
474			t.Errorf("#%d: %v", i, err)
475		}
476	}
477}
478
479func TestV2CAD(t *testing.T) {
480	defer testutil.AfterTest(t)
481	cl := NewCluster(t, 1)
482	cl.Launch(t)
483	defer cl.Terminate(t)
484
485	u := cl.URL(0)
486	tc := NewTestClient()
487
488	v := url.Values{}
489	v.Set("value", "XXX")
490	r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v)
491	if err != nil {
492		t.Error(err)
493	}
494	r.Body.Close()
495
496	r, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foovalue"), v)
497	if err != nil {
498		t.Error(err)
499	}
500	r.Body.Close()
501
502	tests := []struct {
503		relativeURL string
504		wStatus     int
505		w           map[string]interface{}
506	}{
507		{
508			"/v2/keys/foo?prevIndex=100",
509			http.StatusPreconditionFailed,
510			map[string]interface{}{
511				"errorCode": float64(101),
512				"message":   "Compare failed",
513				"cause":     "[100 != 4]",
514			},
515		},
516		{
517			"/v2/keys/foo?prevIndex=bad_index",
518			http.StatusBadRequest,
519			map[string]interface{}{
520				"errorCode": float64(203),
521				"message":   "The given index in POST form is not a number",
522			},
523		},
524		{
525			"/v2/keys/foo?prevIndex=4",
526			http.StatusOK,
527			map[string]interface{}{
528				"node": map[string]interface{}{
529					"key":           "/foo",
530					"modifiedIndex": float64(6),
531				},
532				"action": "compareAndDelete",
533			},
534		},
535		{
536			"/v2/keys/foovalue?prevValue=YYY",
537			http.StatusPreconditionFailed,
538			map[string]interface{}{
539				"errorCode": float64(101),
540				"message":   "Compare failed",
541				"cause":     "[YYY != XXX]",
542			},
543		},
544		{
545			"/v2/keys/foovalue?prevValue=",
546			http.StatusBadRequest,
547			map[string]interface{}{
548				"errorCode": float64(201),
549				"cause":     `"prevValue" cannot be empty`,
550			},
551		},
552		{
553			"/v2/keys/foovalue?prevValue=XXX",
554			http.StatusOK,
555			map[string]interface{}{
556				"node": map[string]interface{}{
557					"key":           "/foovalue",
558					"modifiedIndex": float64(7),
559				},
560				"action": "compareAndDelete",
561			},
562		},
563	}
564
565	for i, tt := range tests {
566		resp, err := tc.DeleteForm(fmt.Sprintf("%s%s", u, tt.relativeURL), nil)
567		if err != nil {
568			t.Fatalf("#%d: delete err = %v, want nil", i, err)
569		}
570		if resp.StatusCode != tt.wStatus {
571			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
572		}
573		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
574			t.Errorf("#%d: %v", i, err)
575		}
576	}
577}
578
579func TestV2Unique(t *testing.T) {
580	defer testutil.AfterTest(t)
581	cl := NewCluster(t, 1)
582	cl.Launch(t)
583	defer cl.Terminate(t)
584
585	u := cl.URL(0)
586	tc := NewTestClient()
587
588	tests := []struct {
589		relativeURL string
590		value       url.Values
591		wStatus     int
592		w           map[string]interface{}
593	}{
594		{
595			"/v2/keys/foo",
596			url.Values(map[string][]string{"value": {"XXX"}}),
597			http.StatusCreated,
598			map[string]interface{}{
599				"node": map[string]interface{}{
600					"key":   "/foo/00000000000000000004",
601					"value": "XXX",
602				},
603				"action": "create",
604			},
605		},
606		{
607			"/v2/keys/foo",
608			url.Values(map[string][]string{"value": {"XXX"}}),
609			http.StatusCreated,
610			map[string]interface{}{
611				"node": map[string]interface{}{
612					"key":   "/foo/00000000000000000005",
613					"value": "XXX",
614				},
615				"action": "create",
616			},
617		},
618		{
619			"/v2/keys/bar",
620			url.Values(map[string][]string{"value": {"XXX"}}),
621			http.StatusCreated,
622			map[string]interface{}{
623				"node": map[string]interface{}{
624					"key":   "/bar/00000000000000000006",
625					"value": "XXX",
626				},
627				"action": "create",
628			},
629		},
630	}
631
632	for i, tt := range tests {
633		resp, err := tc.PostForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value)
634		if err != nil {
635			t.Fatalf("#%d: post err = %v, want nil", i, err)
636		}
637		if resp.StatusCode != tt.wStatus {
638			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
639		}
640		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
641			t.Errorf("#%d: %v", i, err)
642		}
643	}
644}
645
646func TestV2Get(t *testing.T) {
647	defer testutil.AfterTest(t)
648	cl := NewCluster(t, 1)
649	cl.Launch(t)
650	defer cl.Terminate(t)
651
652	u := cl.URL(0)
653	tc := NewTestClient()
654
655	v := url.Values{}
656	v.Set("value", "XXX")
657	r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar/zar"), v)
658	if err != nil {
659		t.Error(err)
660	}
661	r.Body.Close()
662
663	tests := []struct {
664		relativeURL string
665		wStatus     int
666		w           map[string]interface{}
667	}{
668		{
669			"/v2/keys/foo/bar/zar",
670			http.StatusOK,
671			map[string]interface{}{
672				"node": map[string]interface{}{
673					"key":   "/foo/bar/zar",
674					"value": "XXX",
675				},
676				"action": "get",
677			},
678		},
679		{
680			"/v2/keys/foo",
681			http.StatusOK,
682			map[string]interface{}{
683				"node": map[string]interface{}{
684					"key": "/foo",
685					"dir": true,
686					"nodes": []interface{}{
687						map[string]interface{}{
688							"key":           "/foo/bar",
689							"dir":           true,
690							"createdIndex":  float64(4),
691							"modifiedIndex": float64(4),
692						},
693					},
694				},
695				"action": "get",
696			},
697		},
698		{
699			"/v2/keys/foo?recursive=true",
700			http.StatusOK,
701			map[string]interface{}{
702				"node": map[string]interface{}{
703					"key": "/foo",
704					"dir": true,
705					"nodes": []interface{}{
706						map[string]interface{}{
707							"key":           "/foo/bar",
708							"dir":           true,
709							"createdIndex":  float64(4),
710							"modifiedIndex": float64(4),
711							"nodes": []interface{}{
712								map[string]interface{}{
713									"key":           "/foo/bar/zar",
714									"value":         "XXX",
715									"createdIndex":  float64(4),
716									"modifiedIndex": float64(4),
717								},
718							},
719						},
720					},
721				},
722				"action": "get",
723			},
724		},
725	}
726
727	for i, tt := range tests {
728		resp, err := tc.Get(fmt.Sprintf("%s%s", u, tt.relativeURL))
729		if err != nil {
730			t.Fatalf("#%d: get err = %v, want nil", i, err)
731		}
732		if resp.StatusCode != tt.wStatus {
733			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
734		}
735		if resp.Header.Get("Content-Type") != "application/json" {
736			t.Errorf("#%d: header = %v, want %v", i, resp.Header.Get("Content-Type"), "application/json")
737		}
738		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
739			t.Errorf("#%d: %v", i, err)
740		}
741	}
742}
743
744func TestV2QuorumGet(t *testing.T) {
745	defer testutil.AfterTest(t)
746	cl := NewCluster(t, 1)
747	cl.Launch(t)
748	defer cl.Terminate(t)
749
750	u := cl.URL(0)
751	tc := NewTestClient()
752
753	v := url.Values{}
754	v.Set("value", "XXX")
755	r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar/zar?quorum=true"), v)
756	if err != nil {
757		t.Error(err)
758	}
759	r.Body.Close()
760
761	tests := []struct {
762		relativeURL string
763		wStatus     int
764		w           map[string]interface{}
765	}{
766		{
767			"/v2/keys/foo/bar/zar",
768			http.StatusOK,
769			map[string]interface{}{
770				"node": map[string]interface{}{
771					"key":   "/foo/bar/zar",
772					"value": "XXX",
773				},
774				"action": "get",
775			},
776		},
777		{
778			"/v2/keys/foo",
779			http.StatusOK,
780			map[string]interface{}{
781				"node": map[string]interface{}{
782					"key": "/foo",
783					"dir": true,
784					"nodes": []interface{}{
785						map[string]interface{}{
786							"key":           "/foo/bar",
787							"dir":           true,
788							"createdIndex":  float64(4),
789							"modifiedIndex": float64(4),
790						},
791					},
792				},
793				"action": "get",
794			},
795		},
796		{
797			"/v2/keys/foo?recursive=true",
798			http.StatusOK,
799			map[string]interface{}{
800				"node": map[string]interface{}{
801					"key": "/foo",
802					"dir": true,
803					"nodes": []interface{}{
804						map[string]interface{}{
805							"key":           "/foo/bar",
806							"dir":           true,
807							"createdIndex":  float64(4),
808							"modifiedIndex": float64(4),
809							"nodes": []interface{}{
810								map[string]interface{}{
811									"key":           "/foo/bar/zar",
812									"value":         "XXX",
813									"createdIndex":  float64(4),
814									"modifiedIndex": float64(4),
815								},
816							},
817						},
818					},
819				},
820				"action": "get",
821			},
822		},
823	}
824
825	for i, tt := range tests {
826		resp, err := tc.Get(fmt.Sprintf("%s%s", u, tt.relativeURL))
827		if err != nil {
828			t.Fatalf("#%d: get err = %v, want nil", i, err)
829		}
830		if resp.StatusCode != tt.wStatus {
831			t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus)
832		}
833		if resp.Header.Get("Content-Type") != "application/json" {
834			t.Errorf("#%d: header = %v, want %v", i, resp.Header.Get("Content-Type"), "application/json")
835		}
836		if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil {
837			t.Errorf("#%d: %v", i, err)
838		}
839	}
840}
841
842func TestV2Watch(t *testing.T) {
843	defer testutil.AfterTest(t)
844	cl := NewCluster(t, 1)
845	cl.Launch(t)
846	defer cl.Terminate(t)
847
848	u := cl.URL(0)
849	tc := NewTestClient()
850
851	watchResp, err := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar?wait=true"))
852	if err != nil {
853		t.Fatalf("watch err = %v, want nil", err)
854	}
855
856	// Set a value.
857	v := url.Values{}
858	v.Set("value", "XXX")
859	resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v)
860	if err != nil {
861		t.Fatalf("put err = %v, want nil", err)
862	}
863	resp.Body.Close()
864
865	body := tc.ReadBodyJSON(watchResp)
866	w := map[string]interface{}{
867		"node": map[string]interface{}{
868			"key":           "/foo/bar",
869			"value":         "XXX",
870			"modifiedIndex": float64(4),
871		},
872		"action": "set",
873	}
874
875	if err := checkBody(body, w); err != nil {
876		t.Error(err)
877	}
878}
879
880func TestV2WatchWithIndex(t *testing.T) {
881	defer testutil.AfterTest(t)
882	cl := NewCluster(t, 1)
883	cl.Launch(t)
884	defer cl.Terminate(t)
885
886	u := cl.URL(0)
887	tc := NewTestClient()
888
889	var body map[string]interface{}
890	c := make(chan bool, 1)
891	go func() {
892		resp, err := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar?wait=true&waitIndex=5"))
893		if err != nil {
894			t.Fatalf("watch err = %v, want nil", err)
895		}
896		body = tc.ReadBodyJSON(resp)
897		c <- true
898	}()
899
900	select {
901	case <-c:
902		t.Fatal("should not get the watch result")
903	case <-time.After(time.Millisecond):
904	}
905
906	// Set a value (before given index).
907	v := url.Values{}
908	v.Set("value", "XXX")
909	resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v)
910	if err != nil {
911		t.Fatalf("put err = %v, want nil", err)
912	}
913	resp.Body.Close()
914
915	select {
916	case <-c:
917		t.Fatal("should not get the watch result")
918	case <-time.After(time.Millisecond):
919	}
920
921	// Set a value (before given index).
922	resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v)
923	if err != nil {
924		t.Fatalf("put err = %v, want nil", err)
925	}
926	resp.Body.Close()
927
928	select {
929	case <-c:
930	case <-time.After(time.Second):
931		t.Fatal("cannot get watch result")
932	}
933
934	w := map[string]interface{}{
935		"node": map[string]interface{}{
936			"key":           "/foo/bar",
937			"value":         "XXX",
938			"modifiedIndex": float64(5),
939		},
940		"action": "set",
941	}
942	if err := checkBody(body, w); err != nil {
943		t.Error(err)
944	}
945}
946
947func TestV2WatchKeyInDir(t *testing.T) {
948	defer testutil.AfterTest(t)
949	cl := NewCluster(t, 1)
950	cl.Launch(t)
951	defer cl.Terminate(t)
952
953	u := cl.URL(0)
954	tc := NewTestClient()
955
956	var body map[string]interface{}
957	c := make(chan bool)
958
959	// Create an expiring directory
960	v := url.Values{}
961	v.Set("dir", "true")
962	v.Set("ttl", "1")
963	resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir"), v)
964	if err != nil {
965		t.Fatalf("put err = %v, want nil", err)
966	}
967	resp.Body.Close()
968
969	// Create a permanent node within the directory
970	v = url.Values{}
971	v.Set("value", "XXX")
972	resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir/bar"), v)
973	if err != nil {
974		t.Fatalf("put err = %v, want nil", err)
975	}
976	resp.Body.Close()
977
978	go func() {
979		// Expect a notification when watching the node
980		resp, err := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir/bar?wait=true"))
981		if err != nil {
982			t.Fatalf("watch err = %v, want nil", err)
983		}
984		body = tc.ReadBodyJSON(resp)
985		c <- true
986	}()
987
988	select {
989	case <-c:
990	// 1s ttl + 0.5s sync delay + 1.5s disk and network delay
991	// We set that long disk and network delay because travis may be slow
992	// when do system calls.
993	case <-time.After(3 * time.Second):
994		t.Fatal("timed out waiting for watch result")
995	}
996
997	w := map[string]interface{}{
998		"node": map[string]interface{}{
999			"key": "/keyindir",
1000		},
1001		"action": "expire",
1002	}
1003	if err := checkBody(body, w); err != nil {
1004		t.Error(err)
1005	}
1006}
1007
1008func TestV2Head(t *testing.T) {
1009	defer testutil.AfterTest(t)
1010	cl := NewCluster(t, 1)
1011	cl.Launch(t)
1012	defer cl.Terminate(t)
1013
1014	u := cl.URL(0)
1015	tc := NewTestClient()
1016
1017	v := url.Values{}
1018	v.Set("value", "XXX")
1019	fullURL := fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar")
1020	resp, err := tc.Head(fullURL)
1021	if err != nil {
1022		t.Fatalf("head err = %v, want nil", err)
1023	}
1024	resp.Body.Close()
1025	if resp.StatusCode != http.StatusNotFound {
1026		t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNotFound)
1027	}
1028	if resp.ContentLength <= 0 {
1029		t.Errorf("ContentLength = %d, want > 0", resp.ContentLength)
1030	}
1031
1032	resp, err = tc.PutForm(fullURL, v)
1033	if err != nil {
1034		t.Fatalf("put err = %v, want nil", err)
1035	}
1036	resp.Body.Close()
1037
1038	resp, err = tc.Head(fullURL)
1039	if err != nil {
1040		t.Fatalf("head err = %v, want nil", err)
1041	}
1042	resp.Body.Close()
1043	if resp.StatusCode != http.StatusOK {
1044		t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
1045	}
1046	if resp.ContentLength <= 0 {
1047		t.Errorf("ContentLength = %d, want > 0", resp.ContentLength)
1048	}
1049}
1050
1051func checkBody(body map[string]interface{}, w map[string]interface{}) error {
1052	if body["node"] != nil {
1053		if w["node"] != nil {
1054			wn := w["node"].(map[string]interface{})
1055			n := body["node"].(map[string]interface{})
1056			for k := range n {
1057				if wn[k] == nil {
1058					delete(n, k)
1059				}
1060			}
1061			body["node"] = n
1062		}
1063		if w["prevNode"] != nil {
1064			wn := w["prevNode"].(map[string]interface{})
1065			n := body["prevNode"].(map[string]interface{})
1066			for k := range n {
1067				if wn[k] == nil {
1068					delete(n, k)
1069				}
1070			}
1071			body["prevNode"] = n
1072		}
1073	}
1074	for k, v := range w {
1075		g := body[k]
1076		if !reflect.DeepEqual(g, v) {
1077			return fmt.Errorf("%v = %+v, want %+v", k, g, v)
1078		}
1079	}
1080	return nil
1081}
1082
1083type testHttpClient struct {
1084	*http.Client
1085}
1086
1087// Creates a new HTTP client with KeepAlive disabled.
1088func NewTestClient() *testHttpClient {
1089	tr, _ := transport.NewTransport(transport.TLSInfo{}, time.Second)
1090	tr.DisableKeepAlives = true
1091	return &testHttpClient{&http.Client{Transport: tr}}
1092}
1093
1094// Reads the body from the response and closes it.
1095func (t *testHttpClient) ReadBody(resp *http.Response) []byte {
1096	if resp == nil {
1097		return []byte{}
1098	}
1099	body, _ := ioutil.ReadAll(resp.Body)
1100	resp.Body.Close()
1101	return body
1102}
1103
1104// Reads the body from the response and parses it as JSON.
1105func (t *testHttpClient) ReadBodyJSON(resp *http.Response) map[string]interface{} {
1106	m := make(map[string]interface{})
1107	b := t.ReadBody(resp)
1108	if err := json.Unmarshal(b, &m); err != nil {
1109		panic(fmt.Sprintf("HTTP body JSON parse error: %v: %s", err, string(b)))
1110	}
1111	return m
1112}
1113
1114func (t *testHttpClient) Head(url string) (*http.Response, error) {
1115	return t.send("HEAD", url, "application/json", nil)
1116}
1117
1118func (t *testHttpClient) Get(url string) (*http.Response, error) {
1119	return t.send("GET", url, "application/json", nil)
1120}
1121
1122func (t *testHttpClient) Post(url string, bodyType string, body io.Reader) (*http.Response, error) {
1123	return t.send("POST", url, bodyType, body)
1124}
1125
1126func (t *testHttpClient) PostForm(url string, data url.Values) (*http.Response, error) {
1127	return t.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
1128}
1129
1130func (t *testHttpClient) Put(url string, bodyType string, body io.Reader) (*http.Response, error) {
1131	return t.send("PUT", url, bodyType, body)
1132}
1133
1134func (t *testHttpClient) PutForm(url string, data url.Values) (*http.Response, error) {
1135	return t.Put(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
1136}
1137
1138func (t *testHttpClient) Delete(url string, bodyType string, body io.Reader) (*http.Response, error) {
1139	return t.send("DELETE", url, bodyType, body)
1140}
1141
1142func (t *testHttpClient) DeleteForm(url string, data url.Values) (*http.Response, error) {
1143	return t.Delete(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
1144}
1145
1146func (t *testHttpClient) send(method string, url string, bodyType string, body io.Reader) (*http.Response, error) {
1147	req, err := http.NewRequest(method, url, body)
1148	if err != nil {
1149		return nil, err
1150	}
1151	req.Header.Set("Content-Type", bodyType)
1152	return t.Do(req)
1153}
1154