go.etcd.io/etcd@v3.3.27+incompatible/etcdserver/api/v2http/client_test.go (about)

     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  
    15  package v2http
    16  
    17  import (
    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  
    47  func 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
    57  func 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
    69  func 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
    81  func mustNewRequest(t *testing.T, p string) *http.Request {
    82  	return mustNewMethodRequest(t, "GET", p)
    83  }
    84  
    85  func 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  
    92  type fakeServer struct {
    93  	dummyRaftTimer
    94  	dummyStats
    95  }
    96  
    97  func (s *fakeServer) Leader() types.ID                    { return types.ID(1) }
    98  func (s *fakeServer) Alarms() []*etcdserverpb.AlarmMember { return nil }
    99  func (s *fakeServer) Cluster() api.Cluster                { return nil }
   100  func (s *fakeServer) ClusterVersion() *semver.Version     { return nil }
   101  func (s *fakeServer) RaftHandler() http.Handler           { return nil }
   102  func (s *fakeServer) Do(ctx context.Context, r etcdserverpb.Request) (rr etcdserver.Response, err error) {
   103  	return
   104  }
   105  func (s *fakeServer) ClientCertAuthEnabled() bool { return false }
   106  
   107  type serverRecorder struct {
   108  	fakeServer
   109  	actions []action
   110  }
   111  
   112  func (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  }
   116  func (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  }
   120  func (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  }
   124  func (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  
   129  func (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  
   134  type 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()
   140  type flushingRecorder struct {
   141  	*httptest.ResponseRecorder
   142  	ch chan struct{}
   143  }
   144  
   145  func (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
   152  type resServer struct {
   153  	fakeServer
   154  	res etcdserver.Response
   155  }
   156  
   157  func (rs *resServer) Do(_ context.Context, _ etcdserverpb.Request) (etcdserver.Response, error) {
   158  	return rs.res, nil
   159  }
   160  func (rs *resServer) Process(_ context.Context, _ raftpb.Message) error { return nil }
   161  func (rs *resServer) AddMember(_ context.Context, _ membership.Member) ([]*membership.Member, error) {
   162  	return nil, nil
   163  }
   164  func (rs *resServer) RemoveMember(_ context.Context, _ uint64) ([]*membership.Member, error) {
   165  	return nil, nil
   166  }
   167  func (rs *resServer) UpdateMember(_ context.Context, _ membership.Member) ([]*membership.Member, error) {
   168  	return nil, nil
   169  }
   170  
   171  func boolp(b bool) *bool { return &b }
   172  
   173  type dummyRaftTimer struct{}
   174  
   175  func (drt dummyRaftTimer) Index() uint64 { return uint64(100) }
   176  func (drt dummyRaftTimer) Term() uint64  { return uint64(5) }
   177  
   178  type dummyWatcher struct {
   179  	echan chan *store.Event
   180  	sidx  uint64
   181  }
   182  
   183  func (w *dummyWatcher) EventChan() chan *store.Event {
   184  	return w.echan
   185  }
   186  func (w *dummyWatcher) StartIndex() uint64 { return w.sidx }
   187  func (w *dummyWatcher) Remove()            {}
   188  
   189  func 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  
   232  func 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  
   396  func 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  
   652  func 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.
   705  func 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  
   755  func 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  
   807  func 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  
   840  func 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  
   882  func 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  
  1161  func 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  
  1231  func 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  
  1261  func 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  
  1281  func 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  
  1318  type dummyStats struct {
  1319  	data []byte
  1320  }
  1321  
  1322  func (ds *dummyStats) SelfStats() []byte                 { return ds.data }
  1323  func (ds *dummyStats) LeaderStats() []byte               { return ds.data }
  1324  func (ds *dummyStats) StoreStats() []byte                { return ds.data }
  1325  func (ds *dummyStats) UpdateRecvApp(_ types.ID, _ int64) {}
  1326  
  1327  func 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  
  1347  func 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  
  1363  func 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  
  1379  func 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  
  1399  func 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  
  1420  func 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  
  1514  func 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  
  1562  func 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  
  1635  func 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  
  1684  type recordingCloseNotifier struct {
  1685  	*httptest.ResponseRecorder
  1686  	cn chan bool
  1687  }
  1688  
  1689  func (rcn *recordingCloseNotifier) CloseNotify() <-chan bool {
  1690  	return rcn.cn
  1691  }
  1692  
  1693  func 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  
  1805  func 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  
  1910  func 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  
  1951  func 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  
  1990  func 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  
  2007  func 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  
  2040  func 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  }