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 v2http
16
17import (
18	"bytes"
19	"context"
20	"encoding/json"
21	"errors"
22	"io/ioutil"
23	"net/http"
24	"net/http/httptest"
25	"net/url"
26	"path"
27	"reflect"
28	"strings"
29	"testing"
30	"time"
31
32	etcdErr "github.com/coreos/etcd/error"
33	"github.com/coreos/etcd/etcdserver"
34	"github.com/coreos/etcd/etcdserver/api"
35	"github.com/coreos/etcd/etcdserver/api/v2http/httptypes"
36	"github.com/coreos/etcd/etcdserver/etcdserverpb"
37	"github.com/coreos/etcd/etcdserver/membership"
38	"github.com/coreos/etcd/pkg/testutil"
39	"github.com/coreos/etcd/pkg/types"
40	"github.com/coreos/etcd/raft/raftpb"
41	"github.com/coreos/etcd/store"
42
43	"github.com/coreos/go-semver/semver"
44	"github.com/jonboulle/clockwork"
45)
46
47func mustMarshalEvent(t *testing.T, ev *store.Event) string {
48	b := new(bytes.Buffer)
49	if err := json.NewEncoder(b).Encode(ev); err != nil {
50		t.Fatalf("error marshalling event %#v: %v", ev, err)
51	}
52	return b.String()
53}
54
55// mustNewForm takes a set of Values and constructs a PUT *http.Request,
56// with a URL constructed from appending the given path to the standard keysPrefix
57func mustNewForm(t *testing.T, p string, vals url.Values) *http.Request {
58	u := testutil.MustNewURL(t, path.Join(keysPrefix, p))
59	req, err := http.NewRequest("PUT", u.String(), strings.NewReader(vals.Encode()))
60	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
61	if err != nil {
62		t.Fatalf("error creating new request: %v", err)
63	}
64	return req
65}
66
67// mustNewPostForm takes a set of Values and constructs a POST *http.Request,
68// with a URL constructed from appending the given path to the standard keysPrefix
69func mustNewPostForm(t *testing.T, p string, vals url.Values) *http.Request {
70	u := testutil.MustNewURL(t, path.Join(keysPrefix, p))
71	req, err := http.NewRequest("POST", u.String(), strings.NewReader(vals.Encode()))
72	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
73	if err != nil {
74		t.Fatalf("error creating new request: %v", err)
75	}
76	return req
77}
78
79// mustNewRequest takes a path, appends it to the standard keysPrefix, and constructs
80// a GET *http.Request referencing the resulting URL
81func mustNewRequest(t *testing.T, p string) *http.Request {
82	return mustNewMethodRequest(t, "GET", p)
83}
84
85func mustNewMethodRequest(t *testing.T, m, p string) *http.Request {
86	return &http.Request{
87		Method: m,
88		URL:    testutil.MustNewURL(t, path.Join(keysPrefix, p)),
89	}
90}
91
92type fakeServer struct {
93	dummyRaftTimer
94	dummyStats
95}
96
97func (s *fakeServer) Leader() types.ID                    { return types.ID(1) }
98func (s *fakeServer) Alarms() []*etcdserverpb.AlarmMember { return nil }
99func (s *fakeServer) Cluster() api.Cluster                { return nil }
100func (s *fakeServer) ClusterVersion() *semver.Version     { return nil }
101func (s *fakeServer) RaftHandler() http.Handler           { return nil }
102func (s *fakeServer) Do(ctx context.Context, r etcdserverpb.Request) (rr etcdserver.Response, err error) {
103	return
104}
105func (s *fakeServer) ClientCertAuthEnabled() bool { return false }
106
107type serverRecorder struct {
108	fakeServer
109	actions []action
110}
111
112func (s *serverRecorder) Do(_ context.Context, r etcdserverpb.Request) (etcdserver.Response, error) {
113	s.actions = append(s.actions, action{name: "Do", params: []interface{}{r}})
114	return etcdserver.Response{}, nil
115}
116func (s *serverRecorder) Process(_ context.Context, m raftpb.Message) error {
117	s.actions = append(s.actions, action{name: "Process", params: []interface{}{m}})
118	return nil
119}
120func (s *serverRecorder) AddMember(_ context.Context, m membership.Member) ([]*membership.Member, error) {
121	s.actions = append(s.actions, action{name: "AddMember", params: []interface{}{m}})
122	return nil, nil
123}
124func (s *serverRecorder) RemoveMember(_ context.Context, id uint64) ([]*membership.Member, error) {
125	s.actions = append(s.actions, action{name: "RemoveMember", params: []interface{}{id}})
126	return nil, nil
127}
128
129func (s *serverRecorder) UpdateMember(_ context.Context, m membership.Member) ([]*membership.Member, error) {
130	s.actions = append(s.actions, action{name: "UpdateMember", params: []interface{}{m}})
131	return nil, nil
132}
133
134type action struct {
135	name   string
136	params []interface{}
137}
138
139// flushingRecorder provides a channel to allow users to block until the Recorder is Flushed()
140type flushingRecorder struct {
141	*httptest.ResponseRecorder
142	ch chan struct{}
143}
144
145func (fr *flushingRecorder) Flush() {
146	fr.ResponseRecorder.Flush()
147	fr.ch <- struct{}{}
148}
149
150// resServer implements the etcd.Server interface for testing.
151// It returns the given response from any Do calls, and nil error
152type resServer struct {
153	fakeServer
154	res etcdserver.Response
155}
156
157func (rs *resServer) Do(_ context.Context, _ etcdserverpb.Request) (etcdserver.Response, error) {
158	return rs.res, nil
159}
160func (rs *resServer) Process(_ context.Context, _ raftpb.Message) error { return nil }
161func (rs *resServer) AddMember(_ context.Context, _ membership.Member) ([]*membership.Member, error) {
162	return nil, nil
163}
164func (rs *resServer) RemoveMember(_ context.Context, _ uint64) ([]*membership.Member, error) {
165	return nil, nil
166}
167func (rs *resServer) UpdateMember(_ context.Context, _ membership.Member) ([]*membership.Member, error) {
168	return nil, nil
169}
170
171func boolp(b bool) *bool { return &b }
172
173type dummyRaftTimer struct{}
174
175func (drt dummyRaftTimer) Index() uint64 { return uint64(100) }
176func (drt dummyRaftTimer) Term() uint64  { return uint64(5) }
177
178type dummyWatcher struct {
179	echan chan *store.Event
180	sidx  uint64
181}
182
183func (w *dummyWatcher) EventChan() chan *store.Event {
184	return w.echan
185}
186func (w *dummyWatcher) StartIndex() uint64 { return w.sidx }
187func (w *dummyWatcher) Remove()            {}
188
189func TestBadRefreshRequest(t *testing.T) {
190	tests := []struct {
191		in    *http.Request
192		wcode int
193	}{
194		{
195			mustNewRequest(t, "foo?refresh=true&value=test"),
196			etcdErr.EcodeRefreshValue,
197		},
198		{
199			mustNewRequest(t, "foo?refresh=true&value=10"),
200			etcdErr.EcodeRefreshValue,
201		},
202		{
203			mustNewRequest(t, "foo?refresh=true"),
204			etcdErr.EcodeRefreshTTLRequired,
205		},
206		{
207			mustNewRequest(t, "foo?refresh=true&ttl="),
208			etcdErr.EcodeRefreshTTLRequired,
209		},
210	}
211	for i, tt := range tests {
212		got, _, err := parseKeyRequest(tt.in, clockwork.NewFakeClock())
213		if err == nil {
214			t.Errorf("#%d: unexpected nil error!", i)
215			continue
216		}
217		ee, ok := err.(*etcdErr.Error)
218		if !ok {
219			t.Errorf("#%d: err is not etcd.Error!", i)
220			continue
221		}
222		if ee.ErrorCode != tt.wcode {
223			t.Errorf("#%d: code=%d, want %v", i, ee.ErrorCode, tt.wcode)
224			t.Logf("cause: %#v", ee.Cause)
225		}
226		if !reflect.DeepEqual(got, etcdserverpb.Request{}) {
227			t.Errorf("#%d: unexpected non-empty Request: %#v", i, got)
228		}
229	}
230}
231
232func TestBadParseRequest(t *testing.T) {
233	tests := []struct {
234		in    *http.Request
235		wcode int
236	}{
237		{
238			// parseForm failure
239			&http.Request{
240				Body:   nil,
241				Method: "PUT",
242			},
243			etcdErr.EcodeInvalidForm,
244		},
245		{
246			// bad key prefix
247			&http.Request{
248				URL: testutil.MustNewURL(t, "/badprefix/"),
249			},
250			etcdErr.EcodeInvalidForm,
251		},
252		// bad values for prevIndex, waitIndex, ttl
253		{
254			mustNewForm(t, "foo", url.Values{"prevIndex": []string{"garbage"}}),
255			etcdErr.EcodeIndexNaN,
256		},
257		{
258			mustNewForm(t, "foo", url.Values{"prevIndex": []string{"1.5"}}),
259			etcdErr.EcodeIndexNaN,
260		},
261		{
262			mustNewForm(t, "foo", url.Values{"prevIndex": []string{"-1"}}),
263			etcdErr.EcodeIndexNaN,
264		},
265		{
266			mustNewForm(t, "foo", url.Values{"waitIndex": []string{"garbage"}}),
267			etcdErr.EcodeIndexNaN,
268		},
269		{
270			mustNewForm(t, "foo", url.Values{"waitIndex": []string{"??"}}),
271			etcdErr.EcodeIndexNaN,
272		},
273		{
274			mustNewForm(t, "foo", url.Values{"ttl": []string{"-1"}}),
275			etcdErr.EcodeTTLNaN,
276		},
277		// bad values for recursive, sorted, wait, prevExist, dir, stream
278		{
279			mustNewForm(t, "foo", url.Values{"recursive": []string{"hahaha"}}),
280			etcdErr.EcodeInvalidField,
281		},
282		{
283			mustNewForm(t, "foo", url.Values{"recursive": []string{"1234"}}),
284			etcdErr.EcodeInvalidField,
285		},
286		{
287			mustNewForm(t, "foo", url.Values{"recursive": []string{"?"}}),
288			etcdErr.EcodeInvalidField,
289		},
290		{
291			mustNewForm(t, "foo", url.Values{"sorted": []string{"?"}}),
292			etcdErr.EcodeInvalidField,
293		},
294		{
295			mustNewForm(t, "foo", url.Values{"sorted": []string{"x"}}),
296			etcdErr.EcodeInvalidField,
297		},
298		{
299			mustNewForm(t, "foo", url.Values{"wait": []string{"?!"}}),
300			etcdErr.EcodeInvalidField,
301		},
302		{
303			mustNewForm(t, "foo", url.Values{"wait": []string{"yes"}}),
304			etcdErr.EcodeInvalidField,
305		},
306		{
307			mustNewForm(t, "foo", url.Values{"prevExist": []string{"yes"}}),
308			etcdErr.EcodeInvalidField,
309		},
310		{
311			mustNewForm(t, "foo", url.Values{"prevExist": []string{"#2"}}),
312			etcdErr.EcodeInvalidField,
313		},
314		{
315			mustNewForm(t, "foo", url.Values{"dir": []string{"no"}}),
316			etcdErr.EcodeInvalidField,
317		},
318		{
319			mustNewForm(t, "foo", url.Values{"dir": []string{"file"}}),
320			etcdErr.EcodeInvalidField,
321		},
322		{
323			mustNewForm(t, "foo", url.Values{"quorum": []string{"no"}}),
324			etcdErr.EcodeInvalidField,
325		},
326		{
327			mustNewForm(t, "foo", url.Values{"quorum": []string{"file"}}),
328			etcdErr.EcodeInvalidField,
329		},
330		{
331			mustNewForm(t, "foo", url.Values{"stream": []string{"zzz"}}),
332			etcdErr.EcodeInvalidField,
333		},
334		{
335			mustNewForm(t, "foo", url.Values{"stream": []string{"something"}}),
336			etcdErr.EcodeInvalidField,
337		},
338		// prevValue cannot be empty
339		{
340			mustNewForm(t, "foo", url.Values{"prevValue": []string{""}}),
341			etcdErr.EcodePrevValueRequired,
342		},
343		// wait is only valid with GET requests
344		{
345			mustNewMethodRequest(t, "HEAD", "foo?wait=true"),
346			etcdErr.EcodeInvalidField,
347		},
348		// query values are considered
349		{
350			mustNewRequest(t, "foo?prevExist=wrong"),
351			etcdErr.EcodeInvalidField,
352		},
353		{
354			mustNewRequest(t, "foo?ttl=wrong"),
355			etcdErr.EcodeTTLNaN,
356		},
357		// but body takes precedence if both are specified
358		{
359			mustNewForm(
360				t,
361				"foo?ttl=12",
362				url.Values{"ttl": []string{"garbage"}},
363			),
364			etcdErr.EcodeTTLNaN,
365		},
366		{
367			mustNewForm(
368				t,
369				"foo?prevExist=false",
370				url.Values{"prevExist": []string{"yes"}},
371			),
372			etcdErr.EcodeInvalidField,
373		},
374	}
375	for i, tt := range tests {
376		got, _, err := parseKeyRequest(tt.in, clockwork.NewFakeClock())
377		if err == nil {
378			t.Errorf("#%d: unexpected nil error!", i)
379			continue
380		}
381		ee, ok := err.(*etcdErr.Error)
382		if !ok {
383			t.Errorf("#%d: err is not etcd.Error!", i)
384			continue
385		}
386		if ee.ErrorCode != tt.wcode {
387			t.Errorf("#%d: code=%d, want %v", i, ee.ErrorCode, tt.wcode)
388			t.Logf("cause: %#v", ee.Cause)
389		}
390		if !reflect.DeepEqual(got, etcdserverpb.Request{}) {
391			t.Errorf("#%d: unexpected non-empty Request: %#v", i, got)
392		}
393	}
394}
395
396func TestGoodParseRequest(t *testing.T) {
397	fc := clockwork.NewFakeClock()
398	fc.Advance(1111)
399	tests := []struct {
400		in      *http.Request
401		w       etcdserverpb.Request
402		noValue bool
403	}{
404		{
405			// good prefix, all other values default
406			mustNewRequest(t, "foo"),
407			etcdserverpb.Request{
408				Method: "GET",
409				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
410			},
411			false,
412		},
413		{
414			// value specified
415			mustNewForm(
416				t,
417				"foo",
418				url.Values{"value": []string{"some_value"}},
419			),
420			etcdserverpb.Request{
421				Method: "PUT",
422				Val:    "some_value",
423				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
424			},
425			false,
426		},
427		{
428			// prevIndex specified
429			mustNewForm(
430				t,
431				"foo",
432				url.Values{"prevIndex": []string{"98765"}},
433			),
434			etcdserverpb.Request{
435				Method:    "PUT",
436				PrevIndex: 98765,
437				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
438			},
439			false,
440		},
441		{
442			// recursive specified
443			mustNewForm(
444				t,
445				"foo",
446				url.Values{"recursive": []string{"true"}},
447			),
448			etcdserverpb.Request{
449				Method:    "PUT",
450				Recursive: true,
451				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
452			},
453			false,
454		},
455		{
456			// sorted specified
457			mustNewForm(
458				t,
459				"foo",
460				url.Values{"sorted": []string{"true"}},
461			),
462			etcdserverpb.Request{
463				Method: "PUT",
464				Sorted: true,
465				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
466			},
467			false,
468		},
469		{
470			// quorum specified
471			mustNewForm(
472				t,
473				"foo",
474				url.Values{"quorum": []string{"true"}},
475			),
476			etcdserverpb.Request{
477				Method: "PUT",
478				Quorum: true,
479				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
480			},
481			false,
482		},
483		{
484			// wait specified
485			mustNewRequest(t, "foo?wait=true"),
486			etcdserverpb.Request{
487				Method: "GET",
488				Wait:   true,
489				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
490			},
491			false,
492		},
493		{
494			// empty TTL specified
495			mustNewRequest(t, "foo?ttl="),
496			etcdserverpb.Request{
497				Method:     "GET",
498				Path:       path.Join(etcdserver.StoreKeysPrefix, "/foo"),
499				Expiration: 0,
500			},
501			false,
502		},
503		{
504			// non-empty TTL specified
505			mustNewRequest(t, "foo?ttl=5678"),
506			etcdserverpb.Request{
507				Method:     "GET",
508				Path:       path.Join(etcdserver.StoreKeysPrefix, "/foo"),
509				Expiration: fc.Now().Add(5678 * time.Second).UnixNano(),
510			},
511			false,
512		},
513		{
514			// zero TTL specified
515			mustNewRequest(t, "foo?ttl=0"),
516			etcdserverpb.Request{
517				Method:     "GET",
518				Path:       path.Join(etcdserver.StoreKeysPrefix, "/foo"),
519				Expiration: fc.Now().UnixNano(),
520			},
521			false,
522		},
523		{
524			// dir specified
525			mustNewRequest(t, "foo?dir=true"),
526			etcdserverpb.Request{
527				Method: "GET",
528				Dir:    true,
529				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
530			},
531			false,
532		},
533		{
534			// dir specified negatively
535			mustNewRequest(t, "foo?dir=false"),
536			etcdserverpb.Request{
537				Method: "GET",
538				Dir:    false,
539				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
540			},
541			false,
542		},
543		{
544			// prevExist should be non-null if specified
545			mustNewForm(
546				t,
547				"foo",
548				url.Values{"prevExist": []string{"true"}},
549			),
550			etcdserverpb.Request{
551				Method:    "PUT",
552				PrevExist: boolp(true),
553				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
554			},
555			false,
556		},
557		{
558			// prevExist should be non-null if specified
559			mustNewForm(
560				t,
561				"foo",
562				url.Values{"prevExist": []string{"false"}},
563			),
564			etcdserverpb.Request{
565				Method:    "PUT",
566				PrevExist: boolp(false),
567				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
568			},
569			false,
570		},
571		// mix various fields
572		{
573			mustNewForm(
574				t,
575				"foo",
576				url.Values{
577					"value":     []string{"some value"},
578					"prevExist": []string{"true"},
579					"prevValue": []string{"previous value"},
580				},
581			),
582			etcdserverpb.Request{
583				Method:    "PUT",
584				PrevExist: boolp(true),
585				PrevValue: "previous value",
586				Val:       "some value",
587				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
588			},
589			false,
590		},
591		// query parameters should be used if given
592		{
593			mustNewForm(
594				t,
595				"foo?prevValue=woof",
596				url.Values{},
597			),
598			etcdserverpb.Request{
599				Method:    "PUT",
600				PrevValue: "woof",
601				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
602			},
603			false,
604		},
605		// but form values should take precedence over query parameters
606		{
607			mustNewForm(
608				t,
609				"foo?prevValue=woof",
610				url.Values{
611					"prevValue": []string{"miaow"},
612				},
613			),
614			etcdserverpb.Request{
615				Method:    "PUT",
616				PrevValue: "miaow",
617				Path:      path.Join(etcdserver.StoreKeysPrefix, "/foo"),
618			},
619			false,
620		},
621		{
622			// noValueOnSuccess specified
623			mustNewForm(
624				t,
625				"foo",
626				url.Values{"noValueOnSuccess": []string{"true"}},
627			),
628			etcdserverpb.Request{
629				Method: "PUT",
630				Path:   path.Join(etcdserver.StoreKeysPrefix, "/foo"),
631			},
632			true,
633		},
634	}
635
636	for i, tt := range tests {
637		got, noValueOnSuccess, err := parseKeyRequest(tt.in, fc)
638		if err != nil {
639			t.Errorf("#%d: err = %v, want %v", i, err, nil)
640		}
641
642		if noValueOnSuccess != tt.noValue {
643			t.Errorf("#%d: noValue=%t, want %t", i, noValueOnSuccess, tt.noValue)
644		}
645
646		if !reflect.DeepEqual(got, tt.w) {
647			t.Errorf("#%d: request=%#v, want %#v", i, got, tt.w)
648		}
649	}
650}
651
652func TestServeMembers(t *testing.T) {
653	memb1 := membership.Member{ID: 12, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080"}}}
654	memb2 := membership.Member{ID: 13, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8081"}}}
655	cluster := &fakeCluster{
656		id:      1,
657		members: map[uint64]*membership.Member{1: &memb1, 2: &memb2},
658	}
659	h := &membersHandler{
660		server:  &serverRecorder{},
661		clock:   clockwork.NewFakeClock(),
662		cluster: cluster,
663	}
664
665	wmc := string(`{"members":[{"id":"c","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]},{"id":"d","name":"","peerURLs":[],"clientURLs":["http://localhost:8081"]}]}`)
666
667	tests := []struct {
668		path  string
669		wcode int
670		wct   string
671		wbody string
672	}{
673		{membersPrefix, http.StatusOK, "application/json", wmc + "\n"},
674		{membersPrefix + "/", http.StatusOK, "application/json", wmc + "\n"},
675		{path.Join(membersPrefix, "100"), http.StatusNotFound, "application/json", `{"message":"Not found"}`},
676		{path.Join(membersPrefix, "foobar"), http.StatusNotFound, "application/json", `{"message":"Not found"}`},
677	}
678
679	for i, tt := range tests {
680		req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil)
681		if err != nil {
682			t.Fatal(err)
683		}
684		rw := httptest.NewRecorder()
685		h.ServeHTTP(rw, req)
686
687		if rw.Code != tt.wcode {
688			t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
689		}
690		if gct := rw.Header().Get("Content-Type"); gct != tt.wct {
691			t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct)
692		}
693		gcid := rw.Header().Get("X-Etcd-Cluster-ID")
694		wcid := cluster.ID().String()
695		if gcid != wcid {
696			t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
697		}
698		if rw.Body.String() != tt.wbody {
699			t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody)
700		}
701	}
702}
703
704// TODO: consolidate **ALL** fake server implementations and add no leader test case.
705func TestServeLeader(t *testing.T) {
706	memb1 := membership.Member{ID: 1, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8080"}}}
707	memb2 := membership.Member{ID: 2, Attributes: membership.Attributes{ClientURLs: []string{"http://localhost:8081"}}}
708	cluster := &fakeCluster{
709		id:      1,
710		members: map[uint64]*membership.Member{1: &memb1, 2: &memb2},
711	}
712	h := &membersHandler{
713		server:  &serverRecorder{},
714		clock:   clockwork.NewFakeClock(),
715		cluster: cluster,
716	}
717
718	wmc := string(`{"id":"1","name":"","peerURLs":[],"clientURLs":["http://localhost:8080"]}`)
719
720	tests := []struct {
721		path  string
722		wcode int
723		wct   string
724		wbody string
725	}{
726		{membersPrefix + "leader", http.StatusOK, "application/json", wmc + "\n"},
727		// TODO: add no leader case
728	}
729
730	for i, tt := range tests {
731		req, err := http.NewRequest("GET", testutil.MustNewURL(t, tt.path).String(), nil)
732		if err != nil {
733			t.Fatal(err)
734		}
735		rw := httptest.NewRecorder()
736		h.ServeHTTP(rw, req)
737
738		if rw.Code != tt.wcode {
739			t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
740		}
741		if gct := rw.Header().Get("Content-Type"); gct != tt.wct {
742			t.Errorf("#%d: content-type = %s, want %s", i, gct, tt.wct)
743		}
744		gcid := rw.Header().Get("X-Etcd-Cluster-ID")
745		wcid := cluster.ID().String()
746		if gcid != wcid {
747			t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
748		}
749		if rw.Body.String() != tt.wbody {
750			t.Errorf("#%d: body = %q, want %q", i, rw.Body.String(), tt.wbody)
751		}
752	}
753}
754
755func TestServeMembersCreate(t *testing.T) {
756	u := testutil.MustNewURL(t, membersPrefix)
757	b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`)
758	req, err := http.NewRequest("POST", u.String(), bytes.NewReader(b))
759	if err != nil {
760		t.Fatal(err)
761	}
762	req.Header.Set("Content-Type", "application/json")
763	s := &serverRecorder{}
764	h := &membersHandler{
765		server:  s,
766		clock:   clockwork.NewFakeClock(),
767		cluster: &fakeCluster{id: 1},
768	}
769	rw := httptest.NewRecorder()
770
771	h.ServeHTTP(rw, req)
772
773	wcode := http.StatusCreated
774	if rw.Code != wcode {
775		t.Errorf("code=%d, want %d", rw.Code, wcode)
776	}
777
778	wct := "application/json"
779	if gct := rw.Header().Get("Content-Type"); gct != wct {
780		t.Errorf("content-type = %s, want %s", gct, wct)
781	}
782	gcid := rw.Header().Get("X-Etcd-Cluster-ID")
783	wcid := h.cluster.ID().String()
784	if gcid != wcid {
785		t.Errorf("cid = %s, want %s", gcid, wcid)
786	}
787
788	wb := `{"id":"c29b431f04be0bc7","name":"","peerURLs":["http://127.0.0.1:1"],"clientURLs":[]}` + "\n"
789	g := rw.Body.String()
790	if g != wb {
791		t.Errorf("got body=%q, want %q", g, wb)
792	}
793
794	wm := membership.Member{
795		ID: 14022875665250782151,
796		RaftAttributes: membership.RaftAttributes{
797			PeerURLs: []string{"http://127.0.0.1:1"},
798		},
799	}
800
801	wactions := []action{{name: "AddMember", params: []interface{}{wm}}}
802	if !reflect.DeepEqual(s.actions, wactions) {
803		t.Errorf("actions = %+v, want %+v", s.actions, wactions)
804	}
805}
806
807func TestServeMembersDelete(t *testing.T) {
808	req := &http.Request{
809		Method: "DELETE",
810		URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "BEEF")),
811	}
812	s := &serverRecorder{}
813	h := &membersHandler{
814		server:  s,
815		cluster: &fakeCluster{id: 1},
816	}
817	rw := httptest.NewRecorder()
818
819	h.ServeHTTP(rw, req)
820
821	wcode := http.StatusNoContent
822	if rw.Code != wcode {
823		t.Errorf("code=%d, want %d", rw.Code, wcode)
824	}
825	gcid := rw.Header().Get("X-Etcd-Cluster-ID")
826	wcid := h.cluster.ID().String()
827	if gcid != wcid {
828		t.Errorf("cid = %s, want %s", gcid, wcid)
829	}
830	g := rw.Body.String()
831	if g != "" {
832		t.Errorf("got body=%q, want %q", g, "")
833	}
834	wactions := []action{{name: "RemoveMember", params: []interface{}{uint64(0xBEEF)}}}
835	if !reflect.DeepEqual(s.actions, wactions) {
836		t.Errorf("actions = %+v, want %+v", s.actions, wactions)
837	}
838}
839
840func TestServeMembersUpdate(t *testing.T) {
841	u := testutil.MustNewURL(t, path.Join(membersPrefix, "1"))
842	b := []byte(`{"peerURLs":["http://127.0.0.1:1"]}`)
843	req, err := http.NewRequest("PUT", u.String(), bytes.NewReader(b))
844	if err != nil {
845		t.Fatal(err)
846	}
847	req.Header.Set("Content-Type", "application/json")
848	s := &serverRecorder{}
849	h := &membersHandler{
850		server:  s,
851		clock:   clockwork.NewFakeClock(),
852		cluster: &fakeCluster{id: 1},
853	}
854	rw := httptest.NewRecorder()
855
856	h.ServeHTTP(rw, req)
857
858	wcode := http.StatusNoContent
859	if rw.Code != wcode {
860		t.Errorf("code=%d, want %d", rw.Code, wcode)
861	}
862
863	gcid := rw.Header().Get("X-Etcd-Cluster-ID")
864	wcid := h.cluster.ID().String()
865	if gcid != wcid {
866		t.Errorf("cid = %s, want %s", gcid, wcid)
867	}
868
869	wm := membership.Member{
870		ID: 1,
871		RaftAttributes: membership.RaftAttributes{
872			PeerURLs: []string{"http://127.0.0.1:1"},
873		},
874	}
875
876	wactions := []action{{name: "UpdateMember", params: []interface{}{wm}}}
877	if !reflect.DeepEqual(s.actions, wactions) {
878		t.Errorf("actions = %+v, want %+v", s.actions, wactions)
879	}
880}
881
882func TestServeMembersFail(t *testing.T) {
883	tests := []struct {
884		req    *http.Request
885		server etcdserver.ServerV2
886
887		wcode int
888	}{
889		{
890			// bad method
891			&http.Request{
892				Method: "CONNECT",
893			},
894			&resServer{},
895
896			http.StatusMethodNotAllowed,
897		},
898		{
899			// bad method
900			&http.Request{
901				Method: "TRACE",
902			},
903			&resServer{},
904
905			http.StatusMethodNotAllowed,
906		},
907		{
908			// parse body error
909			&http.Request{
910				URL:    testutil.MustNewURL(t, membersPrefix),
911				Method: "POST",
912				Body:   ioutil.NopCloser(strings.NewReader("bad json")),
913				Header: map[string][]string{"Content-Type": {"application/json"}},
914			},
915			&resServer{},
916
917			http.StatusBadRequest,
918		},
919		{
920			// bad content type
921			&http.Request{
922				URL:    testutil.MustNewURL(t, membersPrefix),
923				Method: "POST",
924				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
925				Header: map[string][]string{"Content-Type": {"application/bad"}},
926			},
927			&errServer{},
928
929			http.StatusUnsupportedMediaType,
930		},
931		{
932			// bad url
933			&http.Request{
934				URL:    testutil.MustNewURL(t, membersPrefix),
935				Method: "POST",
936				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://a"]}`)),
937				Header: map[string][]string{"Content-Type": {"application/json"}},
938			},
939			&errServer{},
940
941			http.StatusBadRequest,
942		},
943		{
944			// etcdserver.AddMember error
945			&http.Request{
946				URL:    testutil.MustNewURL(t, membersPrefix),
947				Method: "POST",
948				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
949				Header: map[string][]string{"Content-Type": {"application/json"}},
950			},
951			&errServer{
952				err: errors.New("Error while adding a member"),
953			},
954
955			http.StatusInternalServerError,
956		},
957		{
958			// etcdserver.AddMember error
959			&http.Request{
960				URL:    testutil.MustNewURL(t, membersPrefix),
961				Method: "POST",
962				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
963				Header: map[string][]string{"Content-Type": {"application/json"}},
964			},
965			&errServer{
966				err: membership.ErrIDExists,
967			},
968
969			http.StatusConflict,
970		},
971		{
972			// etcdserver.AddMember error
973			&http.Request{
974				URL:    testutil.MustNewURL(t, membersPrefix),
975				Method: "POST",
976				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
977				Header: map[string][]string{"Content-Type": {"application/json"}},
978			},
979			&errServer{
980				err: membership.ErrPeerURLexists,
981			},
982
983			http.StatusConflict,
984		},
985		{
986			// etcdserver.RemoveMember error with arbitrary server error
987			&http.Request{
988				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "1")),
989				Method: "DELETE",
990			},
991			&errServer{
992				err: errors.New("Error while removing member"),
993			},
994
995			http.StatusInternalServerError,
996		},
997		{
998			// etcdserver.RemoveMember error with previously removed ID
999			&http.Request{
1000				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1001				Method: "DELETE",
1002			},
1003			&errServer{
1004				err: membership.ErrIDRemoved,
1005			},
1006
1007			http.StatusGone,
1008		},
1009		{
1010			// etcdserver.RemoveMember error with nonexistent ID
1011			&http.Request{
1012				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1013				Method: "DELETE",
1014			},
1015			&errServer{
1016				err: membership.ErrIDNotFound,
1017			},
1018
1019			http.StatusNotFound,
1020		},
1021		{
1022			// etcdserver.RemoveMember error with badly formed ID
1023			&http.Request{
1024				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "bad_id")),
1025				Method: "DELETE",
1026			},
1027			nil,
1028
1029			http.StatusNotFound,
1030		},
1031		{
1032			// etcdserver.RemoveMember with no ID
1033			&http.Request{
1034				URL:    testutil.MustNewURL(t, membersPrefix),
1035				Method: "DELETE",
1036			},
1037			nil,
1038
1039			http.StatusMethodNotAllowed,
1040		},
1041		{
1042			// parse body error
1043			&http.Request{
1044				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1045				Method: "PUT",
1046				Body:   ioutil.NopCloser(strings.NewReader("bad json")),
1047				Header: map[string][]string{"Content-Type": {"application/json"}},
1048			},
1049			&resServer{},
1050
1051			http.StatusBadRequest,
1052		},
1053		{
1054			// bad content type
1055			&http.Request{
1056				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1057				Method: "PUT",
1058				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
1059				Header: map[string][]string{"Content-Type": {"application/bad"}},
1060			},
1061			&errServer{},
1062
1063			http.StatusUnsupportedMediaType,
1064		},
1065		{
1066			// bad url
1067			&http.Request{
1068				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1069				Method: "PUT",
1070				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://a"]}`)),
1071				Header: map[string][]string{"Content-Type": {"application/json"}},
1072			},
1073			&errServer{},
1074
1075			http.StatusBadRequest,
1076		},
1077		{
1078			// etcdserver.UpdateMember error
1079			&http.Request{
1080				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1081				Method: "PUT",
1082				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
1083				Header: map[string][]string{"Content-Type": {"application/json"}},
1084			},
1085			&errServer{
1086				err: errors.New("blah"),
1087			},
1088
1089			http.StatusInternalServerError,
1090		},
1091		{
1092			// etcdserver.UpdateMember error
1093			&http.Request{
1094				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1095				Method: "PUT",
1096				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
1097				Header: map[string][]string{"Content-Type": {"application/json"}},
1098			},
1099			&errServer{
1100				err: membership.ErrPeerURLexists,
1101			},
1102
1103			http.StatusConflict,
1104		},
1105		{
1106			// etcdserver.UpdateMember error
1107			&http.Request{
1108				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "0")),
1109				Method: "PUT",
1110				Body:   ioutil.NopCloser(strings.NewReader(`{"PeerURLs": ["http://127.0.0.1:1"]}`)),
1111				Header: map[string][]string{"Content-Type": {"application/json"}},
1112			},
1113			&errServer{
1114				err: membership.ErrIDNotFound,
1115			},
1116
1117			http.StatusNotFound,
1118		},
1119		{
1120			// etcdserver.UpdateMember error with badly formed ID
1121			&http.Request{
1122				URL:    testutil.MustNewURL(t, path.Join(membersPrefix, "bad_id")),
1123				Method: "PUT",
1124			},
1125			nil,
1126
1127			http.StatusNotFound,
1128		},
1129		{
1130			// etcdserver.UpdateMember with no ID
1131			&http.Request{
1132				URL:    testutil.MustNewURL(t, membersPrefix),
1133				Method: "PUT",
1134			},
1135			nil,
1136
1137			http.StatusMethodNotAllowed,
1138		},
1139	}
1140	for i, tt := range tests {
1141		h := &membersHandler{
1142			server:  tt.server,
1143			cluster: &fakeCluster{id: 1},
1144			clock:   clockwork.NewFakeClock(),
1145		}
1146		rw := httptest.NewRecorder()
1147		h.ServeHTTP(rw, tt.req)
1148		if rw.Code != tt.wcode {
1149			t.Errorf("#%d: code=%d, want %d", i, rw.Code, tt.wcode)
1150		}
1151		if rw.Code != http.StatusMethodNotAllowed {
1152			gcid := rw.Header().Get("X-Etcd-Cluster-ID")
1153			wcid := h.cluster.ID().String()
1154			if gcid != wcid {
1155				t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
1156			}
1157		}
1158	}
1159}
1160
1161func TestWriteEvent(t *testing.T) {
1162	// nil event should not panic
1163	rec := httptest.NewRecorder()
1164	writeKeyEvent(rec, etcdserver.Response{}, false)
1165	h := rec.Header()
1166	if len(h) > 0 {
1167		t.Fatalf("unexpected non-empty headers: %#v", h)
1168	}
1169	b := rec.Body.String()
1170	if len(b) > 0 {
1171		t.Fatalf("unexpected non-empty body: %q", b)
1172	}
1173
1174	tests := []struct {
1175		ev      *store.Event
1176		noValue bool
1177		idx     string
1178		// TODO(jonboulle): check body as well as just status code
1179		code int
1180		err  error
1181	}{
1182		// standard case, standard 200 response
1183		{
1184			&store.Event{
1185				Action:   store.Get,
1186				Node:     &store.NodeExtern{},
1187				PrevNode: &store.NodeExtern{},
1188			},
1189			false,
1190			"0",
1191			http.StatusOK,
1192			nil,
1193		},
1194		// check new nodes return StatusCreated
1195		{
1196			&store.Event{
1197				Action:   store.Create,
1198				Node:     &store.NodeExtern{},
1199				PrevNode: &store.NodeExtern{},
1200			},
1201			false,
1202			"0",
1203			http.StatusCreated,
1204			nil,
1205		},
1206	}
1207
1208	for i, tt := range tests {
1209		rw := httptest.NewRecorder()
1210		resp := etcdserver.Response{Event: tt.ev, Term: 5, Index: 100}
1211		writeKeyEvent(rw, resp, tt.noValue)
1212		if gct := rw.Header().Get("Content-Type"); gct != "application/json" {
1213			t.Errorf("case %d: bad Content-Type: got %q, want application/json", i, gct)
1214		}
1215		if gri := rw.Header().Get("X-Raft-Index"); gri != "100" {
1216			t.Errorf("case %d: bad X-Raft-Index header: got %s, want %s", i, gri, "100")
1217		}
1218		if grt := rw.Header().Get("X-Raft-Term"); grt != "5" {
1219			t.Errorf("case %d: bad X-Raft-Term header: got %s, want %s", i, grt, "5")
1220		}
1221		if gei := rw.Header().Get("X-Etcd-Index"); gei != tt.idx {
1222			t.Errorf("case %d: bad X-Etcd-Index header: got %s, want %s", i, gei, tt.idx)
1223		}
1224		if rw.Code != tt.code {
1225			t.Errorf("case %d: bad response code: got %d, want %v", i, rw.Code, tt.code)
1226		}
1227
1228	}
1229}
1230
1231func TestV2DMachinesEndpoint(t *testing.T) {
1232	tests := []struct {
1233		method string
1234		wcode  int
1235	}{
1236		{"GET", http.StatusOK},
1237		{"HEAD", http.StatusOK},
1238		{"POST", http.StatusMethodNotAllowed},
1239	}
1240
1241	m := &machinesHandler{cluster: &fakeCluster{}}
1242	s := httptest.NewServer(m)
1243	defer s.Close()
1244
1245	for _, tt := range tests {
1246		req, err := http.NewRequest(tt.method, s.URL+machinesPrefix, nil)
1247		if err != nil {
1248			t.Fatal(err)
1249		}
1250		resp, err := http.DefaultClient.Do(req)
1251		if err != nil {
1252			t.Fatal(err)
1253		}
1254
1255		if resp.StatusCode != tt.wcode {
1256			t.Errorf("StatusCode = %d, expected %d", resp.StatusCode, tt.wcode)
1257		}
1258	}
1259}
1260
1261func TestServeMachines(t *testing.T) {
1262	cluster := &fakeCluster{
1263		clientURLs: []string{"http://localhost:8080", "http://localhost:8081", "http://localhost:8082"},
1264	}
1265	writer := httptest.NewRecorder()
1266	req, err := http.NewRequest("GET", "", nil)
1267	if err != nil {
1268		t.Fatal(err)
1269	}
1270	h := &machinesHandler{cluster: cluster}
1271	h.ServeHTTP(writer, req)
1272	w := "http://localhost:8080, http://localhost:8081, http://localhost:8082"
1273	if g := writer.Body.String(); g != w {
1274		t.Errorf("body = %s, want %s", g, w)
1275	}
1276	if writer.Code != http.StatusOK {
1277		t.Errorf("code = %d, want %d", writer.Code, http.StatusOK)
1278	}
1279}
1280
1281func TestGetID(t *testing.T) {
1282	tests := []struct {
1283		path string
1284
1285		wok   bool
1286		wid   types.ID
1287		wcode int
1288	}{
1289		{
1290			"123",
1291			true, 0x123, http.StatusOK,
1292		},
1293		{
1294			"bad_id",
1295			false, 0, http.StatusNotFound,
1296		},
1297		{
1298			"",
1299			false, 0, http.StatusMethodNotAllowed,
1300		},
1301	}
1302
1303	for i, tt := range tests {
1304		w := httptest.NewRecorder()
1305		id, ok := getID(tt.path, w)
1306		if id != tt.wid {
1307			t.Errorf("#%d: id = %d, want %d", i, id, tt.wid)
1308		}
1309		if ok != tt.wok {
1310			t.Errorf("#%d: ok = %t, want %t", i, ok, tt.wok)
1311		}
1312		if w.Code != tt.wcode {
1313			t.Errorf("#%d code = %d, want %d", i, w.Code, tt.wcode)
1314		}
1315	}
1316}
1317
1318type dummyStats struct {
1319	data []byte
1320}
1321
1322func (ds *dummyStats) SelfStats() []byte                 { return ds.data }
1323func (ds *dummyStats) LeaderStats() []byte               { return ds.data }
1324func (ds *dummyStats) StoreStats() []byte                { return ds.data }
1325func (ds *dummyStats) UpdateRecvApp(_ types.ID, _ int64) {}
1326
1327func TestServeSelfStats(t *testing.T) {
1328	wb := []byte("some statistics")
1329	w := string(wb)
1330	sh := &statsHandler{
1331		stats: &dummyStats{data: wb},
1332	}
1333	rw := httptest.NewRecorder()
1334	sh.serveSelf(rw, &http.Request{Method: "GET"})
1335	if rw.Code != http.StatusOK {
1336		t.Errorf("code = %d, want %d", rw.Code, http.StatusOK)
1337	}
1338	wct := "application/json"
1339	if gct := rw.Header().Get("Content-Type"); gct != wct {
1340		t.Errorf("Content-Type = %q, want %q", gct, wct)
1341	}
1342	if g := rw.Body.String(); g != w {
1343		t.Errorf("body = %s, want %s", g, w)
1344	}
1345}
1346
1347func TestSelfServeStatsBad(t *testing.T) {
1348	for _, m := range []string{"PUT", "POST", "DELETE"} {
1349		sh := &statsHandler{}
1350		rw := httptest.NewRecorder()
1351		sh.serveSelf(
1352			rw,
1353			&http.Request{
1354				Method: m,
1355			},
1356		)
1357		if rw.Code != http.StatusMethodNotAllowed {
1358			t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed)
1359		}
1360	}
1361}
1362
1363func TestLeaderServeStatsBad(t *testing.T) {
1364	for _, m := range []string{"PUT", "POST", "DELETE"} {
1365		sh := &statsHandler{}
1366		rw := httptest.NewRecorder()
1367		sh.serveLeader(
1368			rw,
1369			&http.Request{
1370				Method: m,
1371			},
1372		)
1373		if rw.Code != http.StatusMethodNotAllowed {
1374			t.Errorf("method %s: code=%d, want %d", m, rw.Code, http.StatusMethodNotAllowed)
1375		}
1376	}
1377}
1378
1379func TestServeLeaderStats(t *testing.T) {
1380	wb := []byte("some statistics")
1381	w := string(wb)
1382	sh := &statsHandler{
1383		stats: &dummyStats{data: wb},
1384	}
1385	rw := httptest.NewRecorder()
1386	sh.serveLeader(rw, &http.Request{Method: "GET"})
1387	if rw.Code != http.StatusOK {
1388		t.Errorf("code = %d, want %d", rw.Code, http.StatusOK)
1389	}
1390	wct := "application/json"
1391	if gct := rw.Header().Get("Content-Type"); gct != wct {
1392		t.Errorf("Content-Type = %q, want %q", gct, wct)
1393	}
1394	if g := rw.Body.String(); g != w {
1395		t.Errorf("body = %s, want %s", g, w)
1396	}
1397}
1398
1399func TestServeStoreStats(t *testing.T) {
1400	wb := []byte("some statistics")
1401	w := string(wb)
1402	sh := &statsHandler{
1403		stats: &dummyStats{data: wb},
1404	}
1405	rw := httptest.NewRecorder()
1406	sh.serveStore(rw, &http.Request{Method: "GET"})
1407	if rw.Code != http.StatusOK {
1408		t.Errorf("code = %d, want %d", rw.Code, http.StatusOK)
1409	}
1410	wct := "application/json"
1411	if gct := rw.Header().Get("Content-Type"); gct != wct {
1412		t.Errorf("Content-Type = %q, want %q", gct, wct)
1413	}
1414	if g := rw.Body.String(); g != w {
1415		t.Errorf("body = %s, want %s", g, w)
1416	}
1417
1418}
1419
1420func TestBadServeKeys(t *testing.T) {
1421	testBadCases := []struct {
1422		req    *http.Request
1423		server etcdserver.ServerV2
1424
1425		wcode int
1426		wbody string
1427	}{
1428		{
1429			// bad method
1430			&http.Request{
1431				Method: "CONNECT",
1432			},
1433			&resServer{},
1434
1435			http.StatusMethodNotAllowed,
1436			"Method Not Allowed",
1437		},
1438		{
1439			// bad method
1440			&http.Request{
1441				Method: "TRACE",
1442			},
1443			&resServer{},
1444
1445			http.StatusMethodNotAllowed,
1446			"Method Not Allowed",
1447		},
1448		{
1449			// parseRequest error
1450			&http.Request{
1451				Body:   nil,
1452				Method: "PUT",
1453			},
1454			&resServer{},
1455
1456			http.StatusBadRequest,
1457			`{"errorCode":210,"message":"Invalid POST form","cause":"missing form body","index":0}`,
1458		},
1459		{
1460			// etcdserver.Server error
1461			mustNewRequest(t, "foo"),
1462			&errServer{
1463				err: errors.New("Internal Server Error"),
1464			},
1465
1466			http.StatusInternalServerError,
1467			`{"errorCode":300,"message":"Raft Internal Error","cause":"Internal Server Error","index":0}`,
1468		},
1469		{
1470			// etcdserver.Server etcd error
1471			mustNewRequest(t, "foo"),
1472			&errServer{
1473				err: etcdErr.NewError(etcdErr.EcodeKeyNotFound, "/1/pant", 0),
1474			},
1475
1476			http.StatusNotFound,
1477			`{"errorCode":100,"message":"Key not found","cause":"/pant","index":0}`,
1478		},
1479		{
1480			// non-event/watcher response from etcdserver.Server
1481			mustNewRequest(t, "foo"),
1482			&resServer{
1483				res: etcdserver.Response{},
1484			},
1485
1486			http.StatusInternalServerError,
1487			`{"errorCode":300,"message":"Raft Internal Error","cause":"received response with no Event/Watcher!","index":0}`,
1488		},
1489	}
1490	for i, tt := range testBadCases {
1491		h := &keysHandler{
1492			timeout: 0, // context times out immediately
1493			server:  tt.server,
1494			cluster: &fakeCluster{id: 1},
1495		}
1496		rw := httptest.NewRecorder()
1497		h.ServeHTTP(rw, tt.req)
1498		if rw.Code != tt.wcode {
1499			t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode)
1500		}
1501		if rw.Code != http.StatusMethodNotAllowed {
1502			gcid := rw.Header().Get("X-Etcd-Cluster-ID")
1503			wcid := h.cluster.ID().String()
1504			if gcid != wcid {
1505				t.Errorf("#%d: cid = %s, want %s", i, gcid, wcid)
1506			}
1507		}
1508		if g := strings.TrimSuffix(rw.Body.String(), "\n"); g != tt.wbody {
1509			t.Errorf("#%d: body = %s, want %s", i, g, tt.wbody)
1510		}
1511	}
1512}
1513
1514func TestServeKeysGood(t *testing.T) {
1515	tests := []struct {
1516		req   *http.Request
1517		wcode int
1518	}{
1519		{
1520			mustNewMethodRequest(t, "HEAD", "foo"),
1521			http.StatusOK,
1522		},
1523		{
1524			mustNewMethodRequest(t, "GET", "foo"),
1525			http.StatusOK,
1526		},
1527		{
1528			mustNewForm(t, "foo", url.Values{"value": []string{"bar"}}),
1529			http.StatusOK,
1530		},
1531		{
1532			mustNewMethodRequest(t, "DELETE", "foo"),
1533			http.StatusOK,
1534		},
1535		{
1536			mustNewPostForm(t, "foo", url.Values{"value": []string{"bar"}}),
1537			http.StatusOK,
1538		},
1539	}
1540	server := &resServer{
1541		res: etcdserver.Response{
1542			Event: &store.Event{
1543				Action: store.Get,
1544				Node:   &store.NodeExtern{},
1545			},
1546		},
1547	}
1548	for i, tt := range tests {
1549		h := &keysHandler{
1550			timeout: time.Hour,
1551			server:  server,
1552			cluster: &fakeCluster{id: 1},
1553		}
1554		rw := httptest.NewRecorder()
1555		h.ServeHTTP(rw, tt.req)
1556		if rw.Code != tt.wcode {
1557			t.Errorf("#%d: got code=%d, want %d", i, rw.Code, tt.wcode)
1558		}
1559	}
1560}
1561
1562func TestServeKeysEvent(t *testing.T) {
1563	tests := []struct {
1564		req   *http.Request
1565		rsp   etcdserver.Response
1566		wcode int
1567		event *store.Event
1568	}{
1569		{
1570			mustNewRequest(t, "foo"),
1571			etcdserver.Response{
1572				Event: &store.Event{
1573					Action: store.Get,
1574					Node:   &store.NodeExtern{},
1575				},
1576			},
1577			http.StatusOK,
1578			&store.Event{
1579				Action: store.Get,
1580				Node:   &store.NodeExtern{},
1581			},
1582		},
1583		{
1584			mustNewForm(
1585				t,
1586				"foo",
1587				url.Values{"noValueOnSuccess": []string{"true"}},
1588			),
1589			etcdserver.Response{
1590				Event: &store.Event{
1591					Action: store.CompareAndSwap,
1592					Node:   &store.NodeExtern{},
1593				},
1594			},
1595			http.StatusOK,
1596			&store.Event{
1597				Action: store.CompareAndSwap,
1598				Node:   nil,
1599			},
1600		},
1601	}
1602
1603	server := &resServer{}
1604	h := &keysHandler{
1605		timeout: time.Hour,
1606		server:  server,
1607		cluster: &fakeCluster{id: 1},
1608	}
1609
1610	for _, tt := range tests {
1611		server.res = tt.rsp
1612		rw := httptest.NewRecorder()
1613		h.ServeHTTP(rw, tt.req)
1614
1615		wbody := mustMarshalEvent(
1616			t,
1617			tt.event,
1618		)
1619
1620		if rw.Code != tt.wcode {
1621			t.Errorf("got code=%d, want %d", rw.Code, tt.wcode)
1622		}
1623		gcid := rw.Header().Get("X-Etcd-Cluster-ID")
1624		wcid := h.cluster.ID().String()
1625		if gcid != wcid {
1626			t.Errorf("cid = %s, want %s", gcid, wcid)
1627		}
1628		g := rw.Body.String()
1629		if g != wbody {
1630			t.Errorf("got body=%#v, want %#v", g, wbody)
1631		}
1632	}
1633}
1634
1635func TestServeKeysWatch(t *testing.T) {
1636	req := mustNewRequest(t, "/foo/bar")
1637	ec := make(chan *store.Event)
1638	dw := &dummyWatcher{
1639		echan: ec,
1640	}
1641	server := &resServer{
1642		res: etcdserver.Response{
1643			Watcher: dw,
1644		},
1645	}
1646	h := &keysHandler{
1647		timeout: time.Hour,
1648		server:  server,
1649		cluster: &fakeCluster{id: 1},
1650	}
1651	go func() {
1652		ec <- &store.Event{
1653			Action: store.Get,
1654			Node:   &store.NodeExtern{},
1655		}
1656	}()
1657	rw := httptest.NewRecorder()
1658
1659	h.ServeHTTP(rw, req)
1660
1661	wcode := http.StatusOK
1662	wbody := mustMarshalEvent(
1663		t,
1664		&store.Event{
1665			Action: store.Get,
1666			Node:   &store.NodeExtern{},
1667		},
1668	)
1669
1670	if rw.Code != wcode {
1671		t.Errorf("got code=%d, want %d", rw.Code, wcode)
1672	}
1673	gcid := rw.Header().Get("X-Etcd-Cluster-ID")
1674	wcid := h.cluster.ID().String()
1675	if gcid != wcid {
1676		t.Errorf("cid = %s, want %s", gcid, wcid)
1677	}
1678	g := rw.Body.String()
1679	if g != wbody {
1680		t.Errorf("got body=%#v, want %#v", g, wbody)
1681	}
1682}
1683
1684type recordingCloseNotifier struct {
1685	*httptest.ResponseRecorder
1686	cn chan bool
1687}
1688
1689func (rcn *recordingCloseNotifier) CloseNotify() <-chan bool {
1690	return rcn.cn
1691}
1692
1693func TestHandleWatch(t *testing.T) {
1694	defaultRwRr := func() (http.ResponseWriter, *httptest.ResponseRecorder) {
1695		r := httptest.NewRecorder()
1696		return r, r
1697	}
1698	noopEv := func(chan *store.Event) {}
1699
1700	tests := []struct {
1701		getCtx   func() context.Context
1702		getRwRr  func() (http.ResponseWriter, *httptest.ResponseRecorder)
1703		doToChan func(chan *store.Event)
1704
1705		wbody string
1706	}{
1707		{
1708			// Normal case: one event
1709			context.Background,
1710			defaultRwRr,
1711			func(ch chan *store.Event) {
1712				ch <- &store.Event{
1713					Action: store.Get,
1714					Node:   &store.NodeExtern{},
1715				}
1716			},
1717
1718			mustMarshalEvent(
1719				t,
1720				&store.Event{
1721					Action: store.Get,
1722					Node:   &store.NodeExtern{},
1723				},
1724			),
1725		},
1726		{
1727			// Channel is closed, no event
1728			context.Background,
1729			defaultRwRr,
1730			func(ch chan *store.Event) {
1731				close(ch)
1732			},
1733
1734			"",
1735		},
1736		{
1737			// Simulate a timed-out context
1738			func() context.Context {
1739				ctx, cancel := context.WithCancel(context.Background())
1740				cancel()
1741				return ctx
1742			},
1743			defaultRwRr,
1744			noopEv,
1745
1746			"",
1747		},
1748		{
1749			// Close-notifying request
1750			context.Background,
1751			func() (http.ResponseWriter, *httptest.ResponseRecorder) {
1752				rw := &recordingCloseNotifier{
1753					ResponseRecorder: httptest.NewRecorder(),
1754					cn:               make(chan bool, 1),
1755				}
1756				rw.cn <- true
1757				return rw, rw.ResponseRecorder
1758			},
1759			noopEv,
1760
1761			"",
1762		},
1763	}
1764
1765	for i, tt := range tests {
1766		rw, rr := tt.getRwRr()
1767		wa := &dummyWatcher{
1768			echan: make(chan *store.Event, 1),
1769			sidx:  10,
1770		}
1771		tt.doToChan(wa.echan)
1772
1773		resp := etcdserver.Response{Term: 5, Index: 100, Watcher: wa}
1774		handleKeyWatch(tt.getCtx(), rw, resp, false)
1775
1776		wcode := http.StatusOK
1777		wct := "application/json"
1778		wei := "10"
1779		wri := "100"
1780		wrt := "5"
1781
1782		if rr.Code != wcode {
1783			t.Errorf("#%d: got code=%d, want %d", i, rr.Code, wcode)
1784		}
1785		h := rr.Header()
1786		if ct := h.Get("Content-Type"); ct != wct {
1787			t.Errorf("#%d: Content-Type=%q, want %q", i, ct, wct)
1788		}
1789		if ei := h.Get("X-Etcd-Index"); ei != wei {
1790			t.Errorf("#%d: X-Etcd-Index=%q, want %q", i, ei, wei)
1791		}
1792		if ri := h.Get("X-Raft-Index"); ri != wri {
1793			t.Errorf("#%d: X-Raft-Index=%q, want %q", i, ri, wri)
1794		}
1795		if rt := h.Get("X-Raft-Term"); rt != wrt {
1796			t.Errorf("#%d: X-Raft-Term=%q, want %q", i, rt, wrt)
1797		}
1798		g := rr.Body.String()
1799		if g != tt.wbody {
1800			t.Errorf("#%d: got body=%#v, want %#v", i, g, tt.wbody)
1801		}
1802	}
1803}
1804
1805func TestHandleWatchStreaming(t *testing.T) {
1806	rw := &flushingRecorder{
1807		httptest.NewRecorder(),
1808		make(chan struct{}, 1),
1809	}
1810	wa := &dummyWatcher{
1811		echan: make(chan *store.Event),
1812	}
1813
1814	// Launch the streaming handler in the background with a cancellable context
1815	ctx, cancel := context.WithCancel(context.Background())
1816	done := make(chan struct{})
1817	go func() {
1818		resp := etcdserver.Response{Watcher: wa}
1819		handleKeyWatch(ctx, rw, resp, true)
1820		close(done)
1821	}()
1822
1823	// Expect one Flush for the headers etc.
1824	select {
1825	case <-rw.ch:
1826	case <-time.After(time.Second):
1827		t.Fatalf("timed out waiting for flush")
1828	}
1829
1830	// Expect headers but no body
1831	wcode := http.StatusOK
1832	wct := "application/json"
1833	wbody := ""
1834
1835	if rw.Code != wcode {
1836		t.Errorf("got code=%d, want %d", rw.Code, wcode)
1837	}
1838	h := rw.Header()
1839	if ct := h.Get("Content-Type"); ct != wct {
1840		t.Errorf("Content-Type=%q, want %q", ct, wct)
1841	}
1842	g := rw.Body.String()
1843	if g != wbody {
1844		t.Errorf("got body=%#v, want %#v", g, wbody)
1845	}
1846
1847	// Now send the first event
1848	select {
1849	case wa.echan <- &store.Event{
1850		Action: store.Get,
1851		Node:   &store.NodeExtern{},
1852	}:
1853	case <-time.After(time.Second):
1854		t.Fatal("timed out waiting for send")
1855	}
1856
1857	// Wait for it to be flushed...
1858	select {
1859	case <-rw.ch:
1860	case <-time.After(time.Second):
1861		t.Fatalf("timed out waiting for flush")
1862	}
1863
1864	// And check the body is as expected
1865	wbody = mustMarshalEvent(
1866		t,
1867		&store.Event{
1868			Action: store.Get,
1869			Node:   &store.NodeExtern{},
1870		},
1871	)
1872	g = rw.Body.String()
1873	if g != wbody {
1874		t.Errorf("got body=%#v, want %#v", g, wbody)
1875	}
1876
1877	// Rinse and repeat
1878	select {
1879	case wa.echan <- &store.Event{
1880		Action: store.Get,
1881		Node:   &store.NodeExtern{},
1882	}:
1883	case <-time.After(time.Second):
1884		t.Fatal("timed out waiting for send")
1885	}
1886
1887	select {
1888	case <-rw.ch:
1889	case <-time.After(time.Second):
1890		t.Fatalf("timed out waiting for flush")
1891	}
1892
1893	// This time, we expect to see both events
1894	wbody = wbody + wbody
1895	g = rw.Body.String()
1896	if g != wbody {
1897		t.Errorf("got body=%#v, want %#v", g, wbody)
1898	}
1899
1900	// Finally, time out the connection and ensure the serving goroutine returns
1901	cancel()
1902
1903	select {
1904	case <-done:
1905	case <-time.After(time.Second):
1906		t.Fatalf("timed out waiting for done")
1907	}
1908}
1909
1910func TestTrimEventPrefix(t *testing.T) {
1911	pre := "/abc"
1912	tests := []struct {
1913		ev  *store.Event
1914		wev *store.Event
1915	}{
1916		{
1917			nil,
1918			nil,
1919		},
1920		{
1921			&store.Event{},
1922			&store.Event{},
1923		},
1924		{
1925			&store.Event{Node: &store.NodeExtern{Key: "/abc/def"}},
1926			&store.Event{Node: &store.NodeExtern{Key: "/def"}},
1927		},
1928		{
1929			&store.Event{PrevNode: &store.NodeExtern{Key: "/abc/ghi"}},
1930			&store.Event{PrevNode: &store.NodeExtern{Key: "/ghi"}},
1931		},
1932		{
1933			&store.Event{
1934				Node:     &store.NodeExtern{Key: "/abc/def"},
1935				PrevNode: &store.NodeExtern{Key: "/abc/ghi"},
1936			},
1937			&store.Event{
1938				Node:     &store.NodeExtern{Key: "/def"},
1939				PrevNode: &store.NodeExtern{Key: "/ghi"},
1940			},
1941		},
1942	}
1943	for i, tt := range tests {
1944		ev := trimEventPrefix(tt.ev, pre)
1945		if !reflect.DeepEqual(ev, tt.wev) {
1946			t.Errorf("#%d: event = %+v, want %+v", i, ev, tt.wev)
1947		}
1948	}
1949}
1950
1951func TestTrimNodeExternPrefix(t *testing.T) {
1952	pre := "/abc"
1953	tests := []struct {
1954		n  *store.NodeExtern
1955		wn *store.NodeExtern
1956	}{
1957		{
1958			nil,
1959			nil,
1960		},
1961		{
1962			&store.NodeExtern{Key: "/abc/def"},
1963			&store.NodeExtern{Key: "/def"},
1964		},
1965		{
1966			&store.NodeExtern{
1967				Key: "/abc/def",
1968				Nodes: []*store.NodeExtern{
1969					{Key: "/abc/def/1"},
1970					{Key: "/abc/def/2"},
1971				},
1972			},
1973			&store.NodeExtern{
1974				Key: "/def",
1975				Nodes: []*store.NodeExtern{
1976					{Key: "/def/1"},
1977					{Key: "/def/2"},
1978				},
1979			},
1980		},
1981	}
1982	for i, tt := range tests {
1983		trimNodeExternPrefix(tt.n, pre)
1984		if !reflect.DeepEqual(tt.n, tt.wn) {
1985			t.Errorf("#%d: node = %+v, want %+v", i, tt.n, tt.wn)
1986		}
1987	}
1988}
1989
1990func TestTrimPrefix(t *testing.T) {
1991	tests := []struct {
1992		in     string
1993		prefix string
1994		w      string
1995	}{
1996		{"/v2/members", "/v2/members", ""},
1997		{"/v2/members/", "/v2/members", ""},
1998		{"/v2/members/foo", "/v2/members", "foo"},
1999	}
2000	for i, tt := range tests {
2001		if g := trimPrefix(tt.in, tt.prefix); g != tt.w {
2002			t.Errorf("#%d: trimPrefix = %q, want %q", i, g, tt.w)
2003		}
2004	}
2005}
2006
2007func TestNewMemberCollection(t *testing.T) {
2008	fixture := []*membership.Member{
2009		{
2010			ID:             12,
2011			Attributes:     membership.Attributes{ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}},
2012			RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}},
2013		},
2014		{
2015			ID:             13,
2016			Attributes:     membership.Attributes{ClientURLs: []string{"http://localhost:9090", "http://localhost:9091"}},
2017			RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:9092", "http://localhost:9093"}},
2018		},
2019	}
2020	got := newMemberCollection(fixture)
2021
2022	want := httptypes.MemberCollection([]httptypes.Member{
2023		{
2024			ID:         "c",
2025			ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"},
2026			PeerURLs:   []string{"http://localhost:8082", "http://localhost:8083"},
2027		},
2028		{
2029			ID:         "d",
2030			ClientURLs: []string{"http://localhost:9090", "http://localhost:9091"},
2031			PeerURLs:   []string{"http://localhost:9092", "http://localhost:9093"},
2032		},
2033	})
2034
2035	if !reflect.DeepEqual(&want, got) {
2036		t.Fatalf("newMemberCollection failure: want=%#v, got=%#v", &want, got)
2037	}
2038}
2039
2040func TestNewMember(t *testing.T) {
2041	fixture := &membership.Member{
2042		ID:             12,
2043		Attributes:     membership.Attributes{ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"}},
2044		RaftAttributes: membership.RaftAttributes{PeerURLs: []string{"http://localhost:8082", "http://localhost:8083"}},
2045	}
2046	got := newMember(fixture)
2047
2048	want := httptypes.Member{
2049		ID:         "c",
2050		ClientURLs: []string{"http://localhost:8080", "http://localhost:8081"},
2051		PeerURLs:   []string{"http://localhost:8082", "http://localhost:8083"},
2052	}
2053
2054	if !reflect.DeepEqual(want, got) {
2055		t.Fatalf("newMember failure: want=%#v, got=%#v", want, got)
2056	}
2057}
2058