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 }