go.etcd.io/etcd@v3.3.27+incompatible/client/keys_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 client 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "net/http" 23 "net/url" 24 "reflect" 25 "testing" 26 "time" 27 ) 28 29 func TestV2KeysURLHelper(t *testing.T) { 30 tests := []struct { 31 endpoint url.URL 32 prefix string 33 key string 34 want url.URL 35 }{ 36 // key is empty, no problem 37 { 38 endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}, 39 prefix: "", 40 key: "", 41 want: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}, 42 }, 43 44 // key is joined to path 45 { 46 endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}, 47 prefix: "", 48 key: "/foo/bar", 49 want: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar"}, 50 }, 51 52 // key is joined to path when path is empty 53 { 54 endpoint: url.URL{Scheme: "http", Host: "example.com", Path: ""}, 55 prefix: "", 56 key: "/foo/bar", 57 want: url.URL{Scheme: "http", Host: "example.com", Path: "/foo/bar"}, 58 }, 59 60 // Host field carries through with port 61 { 62 endpoint: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"}, 63 prefix: "", 64 key: "", 65 want: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"}, 66 }, 67 68 // Scheme carries through 69 { 70 endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"}, 71 prefix: "", 72 key: "", 73 want: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"}, 74 }, 75 // Prefix is applied 76 { 77 endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"}, 78 prefix: "/bar", 79 key: "/baz", 80 want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz"}, 81 }, 82 // Prefix is joined to path 83 { 84 endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"}, 85 prefix: "/bar", 86 key: "", 87 want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar"}, 88 }, 89 // Keep trailing slash 90 { 91 endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"}, 92 prefix: "/bar", 93 key: "/baz/", 94 want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz/"}, 95 }, 96 } 97 98 for i, tt := range tests { 99 got := v2KeysURL(tt.endpoint, tt.prefix, tt.key) 100 if tt.want != *got { 101 t.Errorf("#%d: want=%#v, got=%#v", i, tt.want, *got) 102 } 103 } 104 } 105 106 func TestGetAction(t *testing.T) { 107 ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"} 108 baseWantURL := &url.URL{ 109 Scheme: "http", 110 Host: "example.com", 111 Path: "/v2/keys/foo/bar", 112 } 113 wantHeader := http.Header{} 114 115 tests := []struct { 116 recursive bool 117 sorted bool 118 quorum bool 119 wantQuery string 120 }{ 121 { 122 recursive: false, 123 sorted: false, 124 quorum: false, 125 wantQuery: "quorum=false&recursive=false&sorted=false", 126 }, 127 { 128 recursive: true, 129 sorted: false, 130 quorum: false, 131 wantQuery: "quorum=false&recursive=true&sorted=false", 132 }, 133 { 134 recursive: false, 135 sorted: true, 136 quorum: false, 137 wantQuery: "quorum=false&recursive=false&sorted=true", 138 }, 139 { 140 recursive: true, 141 sorted: true, 142 quorum: false, 143 wantQuery: "quorum=false&recursive=true&sorted=true", 144 }, 145 { 146 recursive: false, 147 sorted: false, 148 quorum: true, 149 wantQuery: "quorum=true&recursive=false&sorted=false", 150 }, 151 } 152 153 for i, tt := range tests { 154 f := getAction{ 155 Key: "/foo/bar", 156 Recursive: tt.recursive, 157 Sorted: tt.sorted, 158 Quorum: tt.quorum, 159 } 160 got := *f.HTTPRequest(ep) 161 162 wantURL := baseWantURL 163 wantURL.RawQuery = tt.wantQuery 164 165 err := assertRequest(got, "GET", wantURL, wantHeader, nil) 166 if err != nil { 167 t.Errorf("#%d: %v", i, err) 168 } 169 } 170 } 171 172 func TestWaitAction(t *testing.T) { 173 ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"} 174 baseWantURL := &url.URL{ 175 Scheme: "http", 176 Host: "example.com", 177 Path: "/v2/keys/foo/bar", 178 } 179 wantHeader := http.Header{} 180 181 tests := []struct { 182 waitIndex uint64 183 recursive bool 184 wantQuery string 185 }{ 186 { 187 recursive: false, 188 waitIndex: uint64(0), 189 wantQuery: "recursive=false&wait=true&waitIndex=0", 190 }, 191 { 192 recursive: false, 193 waitIndex: uint64(12), 194 wantQuery: "recursive=false&wait=true&waitIndex=12", 195 }, 196 { 197 recursive: true, 198 waitIndex: uint64(12), 199 wantQuery: "recursive=true&wait=true&waitIndex=12", 200 }, 201 } 202 203 for i, tt := range tests { 204 f := waitAction{ 205 Key: "/foo/bar", 206 WaitIndex: tt.waitIndex, 207 Recursive: tt.recursive, 208 } 209 got := *f.HTTPRequest(ep) 210 211 wantURL := baseWantURL 212 wantURL.RawQuery = tt.wantQuery 213 214 err := assertRequest(got, "GET", wantURL, wantHeader, nil) 215 if err != nil { 216 t.Errorf("#%d: unexpected error: %#v", i, err) 217 } 218 } 219 } 220 221 func TestSetAction(t *testing.T) { 222 wantHeader := http.Header(map[string][]string{ 223 "Content-Type": {"application/x-www-form-urlencoded"}, 224 }) 225 226 tests := []struct { 227 act setAction 228 wantURL string 229 wantBody string 230 }{ 231 // default prefix 232 { 233 act: setAction{ 234 Prefix: defaultV2KeysPrefix, 235 Key: "foo", 236 }, 237 wantURL: "http://example.com/v2/keys/foo", 238 wantBody: "value=", 239 }, 240 241 // non-default prefix 242 { 243 act: setAction{ 244 Prefix: "/pfx", 245 Key: "foo", 246 }, 247 wantURL: "http://example.com/pfx/foo", 248 wantBody: "value=", 249 }, 250 251 // no prefix 252 { 253 act: setAction{ 254 Key: "foo", 255 }, 256 wantURL: "http://example.com/foo", 257 wantBody: "value=", 258 }, 259 260 // Key with path separators 261 { 262 act: setAction{ 263 Prefix: defaultV2KeysPrefix, 264 Key: "foo/bar/baz", 265 }, 266 wantURL: "http://example.com/v2/keys/foo/bar/baz", 267 wantBody: "value=", 268 }, 269 270 // Key with leading slash, Prefix with trailing slash 271 { 272 act: setAction{ 273 Prefix: "/foo/", 274 Key: "/bar", 275 }, 276 wantURL: "http://example.com/foo/bar", 277 wantBody: "value=", 278 }, 279 280 // Key with trailing slash 281 { 282 act: setAction{ 283 Key: "/foo/", 284 }, 285 wantURL: "http://example.com/foo/", 286 wantBody: "value=", 287 }, 288 289 // Value is set 290 { 291 act: setAction{ 292 Key: "foo", 293 Value: "baz", 294 }, 295 wantURL: "http://example.com/foo", 296 wantBody: "value=baz", 297 }, 298 299 // PrevExist set, but still ignored 300 { 301 act: setAction{ 302 Key: "foo", 303 PrevExist: PrevIgnore, 304 }, 305 wantURL: "http://example.com/foo", 306 wantBody: "value=", 307 }, 308 309 // PrevExist set to true 310 { 311 act: setAction{ 312 Key: "foo", 313 PrevExist: PrevExist, 314 }, 315 wantURL: "http://example.com/foo?prevExist=true", 316 wantBody: "value=", 317 }, 318 319 // PrevExist set to false 320 { 321 act: setAction{ 322 Key: "foo", 323 PrevExist: PrevNoExist, 324 }, 325 wantURL: "http://example.com/foo?prevExist=false", 326 wantBody: "value=", 327 }, 328 329 // PrevValue is urlencoded 330 { 331 act: setAction{ 332 Key: "foo", 333 PrevValue: "bar baz", 334 }, 335 wantURL: "http://example.com/foo?prevValue=bar+baz", 336 wantBody: "value=", 337 }, 338 339 // PrevIndex is set 340 { 341 act: setAction{ 342 Key: "foo", 343 PrevIndex: uint64(12), 344 }, 345 wantURL: "http://example.com/foo?prevIndex=12", 346 wantBody: "value=", 347 }, 348 349 // TTL is set 350 { 351 act: setAction{ 352 Key: "foo", 353 TTL: 3 * time.Minute, 354 }, 355 wantURL: "http://example.com/foo", 356 wantBody: "ttl=180&value=", 357 }, 358 359 // Refresh is set 360 { 361 act: setAction{ 362 Key: "foo", 363 TTL: 3 * time.Minute, 364 Refresh: true, 365 }, 366 wantURL: "http://example.com/foo", 367 wantBody: "refresh=true&ttl=180&value=", 368 }, 369 370 // Dir is set 371 { 372 act: setAction{ 373 Key: "foo", 374 Dir: true, 375 }, 376 wantURL: "http://example.com/foo?dir=true", 377 wantBody: "", 378 }, 379 // Dir is set with a value 380 { 381 act: setAction{ 382 Key: "foo", 383 Value: "bar", 384 Dir: true, 385 }, 386 wantURL: "http://example.com/foo?dir=true", 387 wantBody: "", 388 }, 389 // Dir is set with PrevExist set to true 390 { 391 act: setAction{ 392 Key: "foo", 393 PrevExist: PrevExist, 394 Dir: true, 395 }, 396 wantURL: "http://example.com/foo?dir=true&prevExist=true", 397 wantBody: "", 398 }, 399 // Dir is set with PrevValue 400 { 401 act: setAction{ 402 Key: "foo", 403 PrevValue: "bar", 404 Dir: true, 405 }, 406 wantURL: "http://example.com/foo?dir=true", 407 wantBody: "", 408 }, 409 // NoValueOnSuccess is set 410 { 411 act: setAction{ 412 Key: "foo", 413 NoValueOnSuccess: true, 414 }, 415 wantURL: "http://example.com/foo?noValueOnSuccess=true", 416 wantBody: "value=", 417 }, 418 } 419 420 for i, tt := range tests { 421 u, err := url.Parse(tt.wantURL) 422 if err != nil { 423 t.Errorf("#%d: unable to use wantURL fixture: %v", i, err) 424 } 425 426 got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"}) 427 if err := assertRequest(*got, "PUT", u, wantHeader, []byte(tt.wantBody)); err != nil { 428 t.Errorf("#%d: %v", i, err) 429 } 430 } 431 } 432 433 func TestCreateInOrderAction(t *testing.T) { 434 wantHeader := http.Header(map[string][]string{ 435 "Content-Type": {"application/x-www-form-urlencoded"}, 436 }) 437 438 tests := []struct { 439 act createInOrderAction 440 wantURL string 441 wantBody string 442 }{ 443 // default prefix 444 { 445 act: createInOrderAction{ 446 Prefix: defaultV2KeysPrefix, 447 Dir: "foo", 448 }, 449 wantURL: "http://example.com/v2/keys/foo", 450 wantBody: "value=", 451 }, 452 453 // non-default prefix 454 { 455 act: createInOrderAction{ 456 Prefix: "/pfx", 457 Dir: "foo", 458 }, 459 wantURL: "http://example.com/pfx/foo", 460 wantBody: "value=", 461 }, 462 463 // no prefix 464 { 465 act: createInOrderAction{ 466 Dir: "foo", 467 }, 468 wantURL: "http://example.com/foo", 469 wantBody: "value=", 470 }, 471 472 // Key with path separators 473 { 474 act: createInOrderAction{ 475 Prefix: defaultV2KeysPrefix, 476 Dir: "foo/bar/baz", 477 }, 478 wantURL: "http://example.com/v2/keys/foo/bar/baz", 479 wantBody: "value=", 480 }, 481 482 // Key with leading slash, Prefix with trailing slash 483 { 484 act: createInOrderAction{ 485 Prefix: "/foo/", 486 Dir: "/bar", 487 }, 488 wantURL: "http://example.com/foo/bar", 489 wantBody: "value=", 490 }, 491 492 // Key with trailing slash 493 { 494 act: createInOrderAction{ 495 Dir: "/foo/", 496 }, 497 wantURL: "http://example.com/foo/", 498 wantBody: "value=", 499 }, 500 501 // Value is set 502 { 503 act: createInOrderAction{ 504 Dir: "foo", 505 Value: "baz", 506 }, 507 wantURL: "http://example.com/foo", 508 wantBody: "value=baz", 509 }, 510 // TTL is set 511 { 512 act: createInOrderAction{ 513 Dir: "foo", 514 TTL: 3 * time.Minute, 515 }, 516 wantURL: "http://example.com/foo", 517 wantBody: "ttl=180&value=", 518 }, 519 } 520 521 for i, tt := range tests { 522 u, err := url.Parse(tt.wantURL) 523 if err != nil { 524 t.Errorf("#%d: unable to use wantURL fixture: %v", i, err) 525 } 526 527 got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"}) 528 if err := assertRequest(*got, "POST", u, wantHeader, []byte(tt.wantBody)); err != nil { 529 t.Errorf("#%d: %v", i, err) 530 } 531 } 532 } 533 534 func TestDeleteAction(t *testing.T) { 535 wantHeader := http.Header(map[string][]string{ 536 "Content-Type": {"application/x-www-form-urlencoded"}, 537 }) 538 539 tests := []struct { 540 act deleteAction 541 wantURL string 542 }{ 543 // default prefix 544 { 545 act: deleteAction{ 546 Prefix: defaultV2KeysPrefix, 547 Key: "foo", 548 }, 549 wantURL: "http://example.com/v2/keys/foo", 550 }, 551 552 // non-default prefix 553 { 554 act: deleteAction{ 555 Prefix: "/pfx", 556 Key: "foo", 557 }, 558 wantURL: "http://example.com/pfx/foo", 559 }, 560 561 // no prefix 562 { 563 act: deleteAction{ 564 Key: "foo", 565 }, 566 wantURL: "http://example.com/foo", 567 }, 568 569 // Key with path separators 570 { 571 act: deleteAction{ 572 Prefix: defaultV2KeysPrefix, 573 Key: "foo/bar/baz", 574 }, 575 wantURL: "http://example.com/v2/keys/foo/bar/baz", 576 }, 577 578 // Key with leading slash, Prefix with trailing slash 579 { 580 act: deleteAction{ 581 Prefix: "/foo/", 582 Key: "/bar", 583 }, 584 wantURL: "http://example.com/foo/bar", 585 }, 586 587 // Key with trailing slash 588 { 589 act: deleteAction{ 590 Key: "/foo/", 591 }, 592 wantURL: "http://example.com/foo/", 593 }, 594 595 // Recursive set to true 596 { 597 act: deleteAction{ 598 Key: "foo", 599 Recursive: true, 600 }, 601 wantURL: "http://example.com/foo?recursive=true", 602 }, 603 604 // PrevValue is urlencoded 605 { 606 act: deleteAction{ 607 Key: "foo", 608 PrevValue: "bar baz", 609 }, 610 wantURL: "http://example.com/foo?prevValue=bar+baz", 611 }, 612 613 // PrevIndex is set 614 { 615 act: deleteAction{ 616 Key: "foo", 617 PrevIndex: uint64(12), 618 }, 619 wantURL: "http://example.com/foo?prevIndex=12", 620 }, 621 } 622 623 for i, tt := range tests { 624 u, err := url.Parse(tt.wantURL) 625 if err != nil { 626 t.Errorf("#%d: unable to use wantURL fixture: %v", i, err) 627 } 628 629 got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"}) 630 if err := assertRequest(*got, "DELETE", u, wantHeader, nil); err != nil { 631 t.Errorf("#%d: %v", i, err) 632 } 633 } 634 } 635 636 func assertRequest(got http.Request, wantMethod string, wantURL *url.URL, wantHeader http.Header, wantBody []byte) error { 637 if wantMethod != got.Method { 638 return fmt.Errorf("want.Method=%#v got.Method=%#v", wantMethod, got.Method) 639 } 640 641 if !reflect.DeepEqual(wantURL, got.URL) { 642 return fmt.Errorf("want.URL=%#v got.URL=%#v", wantURL, got.URL) 643 } 644 645 if !reflect.DeepEqual(wantHeader, got.Header) { 646 return fmt.Errorf("want.Header=%#v got.Header=%#v", wantHeader, got.Header) 647 } 648 649 if got.Body == nil { 650 if wantBody != nil { 651 return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, got.Body) 652 } 653 } else { 654 if wantBody == nil { 655 return fmt.Errorf("want.Body=%v got.Body=%s", wantBody, got.Body) 656 } 657 gotBytes, err := ioutil.ReadAll(got.Body) 658 if err != nil { 659 return err 660 } 661 662 if !reflect.DeepEqual(wantBody, gotBytes) { 663 return fmt.Errorf("want.Body=%s got.Body=%s", wantBody, gotBytes) 664 } 665 } 666 667 return nil 668 } 669 670 func TestUnmarshalSuccessfulResponse(t *testing.T) { 671 var expiration time.Time 672 expiration.UnmarshalText([]byte("2015-04-07T04:40:23.044979686Z")) 673 674 tests := []struct { 675 indexHdr string 676 clusterIDHdr string 677 body string 678 wantRes *Response 679 wantErr bool 680 }{ 681 // Neither PrevNode or Node 682 { 683 indexHdr: "1", 684 body: `{"action":"delete"}`, 685 wantRes: &Response{Action: "delete", Index: 1}, 686 wantErr: false, 687 }, 688 689 // PrevNode 690 { 691 indexHdr: "15", 692 body: `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`, 693 wantRes: &Response{ 694 Action: "delete", 695 Index: 15, 696 Node: nil, 697 PrevNode: &Node{ 698 Key: "/foo", 699 Value: "bar", 700 ModifiedIndex: 12, 701 CreatedIndex: 10, 702 }, 703 }, 704 wantErr: false, 705 }, 706 707 // Node 708 { 709 indexHdr: "15", 710 body: `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`, 711 wantRes: &Response{ 712 Action: "get", 713 Index: 15, 714 Node: &Node{ 715 Key: "/foo", 716 Value: "bar", 717 ModifiedIndex: 12, 718 CreatedIndex: 10, 719 TTL: 10, 720 Expiration: &expiration, 721 }, 722 PrevNode: nil, 723 }, 724 wantErr: false, 725 }, 726 727 // Node Dir 728 { 729 indexHdr: "15", 730 clusterIDHdr: "abcdef", 731 body: `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`, 732 wantRes: &Response{ 733 Action: "get", 734 Index: 15, 735 Node: &Node{ 736 Key: "/foo", 737 Dir: true, 738 ModifiedIndex: 12, 739 CreatedIndex: 10, 740 }, 741 PrevNode: nil, 742 ClusterID: "abcdef", 743 }, 744 wantErr: false, 745 }, 746 747 // PrevNode and Node 748 { 749 indexHdr: "15", 750 body: `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`, 751 wantRes: &Response{ 752 Action: "update", 753 Index: 15, 754 PrevNode: &Node{ 755 Key: "/foo", 756 Value: "baz", 757 ModifiedIndex: 10, 758 CreatedIndex: 10, 759 }, 760 Node: &Node{ 761 Key: "/foo", 762 Value: "bar", 763 ModifiedIndex: 12, 764 CreatedIndex: 10, 765 }, 766 }, 767 wantErr: false, 768 }, 769 770 // Garbage in body 771 { 772 indexHdr: "", 773 body: `garbage`, 774 wantRes: nil, 775 wantErr: true, 776 }, 777 778 // non-integer index 779 { 780 indexHdr: "poo", 781 body: `{}`, 782 wantRes: nil, 783 wantErr: true, 784 }, 785 } 786 787 for i, tt := range tests { 788 h := make(http.Header) 789 h.Add("X-Etcd-Index", tt.indexHdr) 790 res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body)) 791 if tt.wantErr != (err != nil) { 792 t.Errorf("#%d: wantErr=%t, err=%v", i, tt.wantErr, err) 793 } 794 795 if (res == nil) != (tt.wantRes == nil) { 796 t.Errorf("#%d: received res=%#v, but expected res=%#v", i, res, tt.wantRes) 797 continue 798 } else if tt.wantRes == nil { 799 // expected and successfully got nil response 800 continue 801 } 802 803 if res.Action != tt.wantRes.Action { 804 t.Errorf("#%d: Action=%s, expected %s", i, res.Action, tt.wantRes.Action) 805 } 806 if res.Index != tt.wantRes.Index { 807 t.Errorf("#%d: Index=%d, expected %d", i, res.Index, tt.wantRes.Index) 808 } 809 if !reflect.DeepEqual(res.Node, tt.wantRes.Node) { 810 t.Errorf("#%d: Node=%v, expected %v", i, res.Node, tt.wantRes.Node) 811 } 812 } 813 } 814 815 func TestUnmarshalFailedKeysResponse(t *testing.T) { 816 body := []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`) 817 818 wantErr := Error{ 819 Code: 100, 820 Message: "Key not found", 821 Cause: "/foo", 822 Index: uint64(18), 823 } 824 825 gotErr := unmarshalFailedKeysResponse(body) 826 if !reflect.DeepEqual(wantErr, gotErr) { 827 t.Errorf("unexpected error: want=%#v got=%#v", wantErr, gotErr) 828 } 829 } 830 831 func TestUnmarshalFailedKeysResponseBadJSON(t *testing.T) { 832 err := unmarshalFailedKeysResponse([]byte(`{"er`)) 833 if err == nil { 834 t.Errorf("got nil error") 835 } else if _, ok := err.(Error); ok { 836 t.Errorf("error is of incorrect type *Error: %#v", err) 837 } 838 } 839 840 func TestHTTPWatcherNextWaitAction(t *testing.T) { 841 initAction := waitAction{ 842 Prefix: "/pants", 843 Key: "/foo/bar", 844 Recursive: true, 845 WaitIndex: 19, 846 } 847 848 client := &actionAssertingHTTPClient{ 849 t: t, 850 act: &initAction, 851 resp: http.Response{ 852 StatusCode: http.StatusOK, 853 Header: http.Header{"X-Etcd-Index": []string{"42"}}, 854 }, 855 body: []byte(`{"action":"update","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`), 856 } 857 858 wantResponse := &Response{ 859 Action: "update", 860 Node: &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(21)}, 861 PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)}, 862 Index: uint64(42), 863 } 864 865 wantNextWait := waitAction{ 866 Prefix: "/pants", 867 Key: "/foo/bar", 868 Recursive: true, 869 WaitIndex: 22, 870 } 871 872 watcher := &httpWatcher{ 873 client: client, 874 nextWait: initAction, 875 } 876 877 resp, err := watcher.Next(context.Background()) 878 if err != nil { 879 t.Errorf("non-nil error: %#v", err) 880 } 881 882 if !reflect.DeepEqual(wantResponse, resp) { 883 t.Errorf("received incorrect Response: want=%#v got=%#v", wantResponse, resp) 884 } 885 886 if !reflect.DeepEqual(wantNextWait, watcher.nextWait) { 887 t.Errorf("nextWait incorrect: want=%#v got=%#v", wantNextWait, watcher.nextWait) 888 } 889 } 890 891 func TestHTTPWatcherNextFail(t *testing.T) { 892 tests := []httpClient{ 893 // generic HTTP client failure 894 &staticHTTPClient{ 895 err: errors.New("fail!"), 896 }, 897 898 // unusable status code 899 &staticHTTPClient{ 900 resp: http.Response{ 901 StatusCode: http.StatusTeapot, 902 }, 903 }, 904 905 // etcd Error response 906 &staticHTTPClient{ 907 resp: http.Response{ 908 StatusCode: http.StatusNotFound, 909 }, 910 body: []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`), 911 }, 912 } 913 914 for i, tt := range tests { 915 act := waitAction{ 916 Prefix: "/pants", 917 Key: "/foo/bar", 918 Recursive: true, 919 WaitIndex: 19, 920 } 921 922 watcher := &httpWatcher{ 923 client: tt, 924 nextWait: act, 925 } 926 927 resp, err := watcher.Next(context.Background()) 928 if err == nil { 929 t.Errorf("#%d: expected non-nil error", i) 930 } 931 if resp != nil { 932 t.Errorf("#%d: expected nil Response, got %#v", i, resp) 933 } 934 if !reflect.DeepEqual(act, watcher.nextWait) { 935 t.Errorf("#%d: nextWait changed: want=%#v got=%#v", i, act, watcher.nextWait) 936 } 937 } 938 } 939 940 func TestHTTPKeysAPIWatcherAction(t *testing.T) { 941 tests := []struct { 942 key string 943 opts *WatcherOptions 944 want waitAction 945 }{ 946 { 947 key: "/foo", 948 opts: nil, 949 want: waitAction{ 950 Key: "/foo", 951 Recursive: false, 952 WaitIndex: 0, 953 }, 954 }, 955 956 { 957 key: "/foo", 958 opts: &WatcherOptions{ 959 Recursive: false, 960 AfterIndex: 0, 961 }, 962 want: waitAction{ 963 Key: "/foo", 964 Recursive: false, 965 WaitIndex: 0, 966 }, 967 }, 968 969 { 970 key: "/foo", 971 opts: &WatcherOptions{ 972 Recursive: true, 973 AfterIndex: 0, 974 }, 975 want: waitAction{ 976 Key: "/foo", 977 Recursive: true, 978 WaitIndex: 0, 979 }, 980 }, 981 982 { 983 key: "/foo", 984 opts: &WatcherOptions{ 985 Recursive: false, 986 AfterIndex: 19, 987 }, 988 want: waitAction{ 989 Key: "/foo", 990 Recursive: false, 991 WaitIndex: 20, 992 }, 993 }, 994 } 995 996 for i, tt := range tests { 997 kAPI := &httpKeysAPI{ 998 client: &staticHTTPClient{err: errors.New("fail!")}, 999 } 1000 1001 want := &httpWatcher{ 1002 client: &staticHTTPClient{err: errors.New("fail!")}, 1003 nextWait: tt.want, 1004 } 1005 1006 got := kAPI.Watcher(tt.key, tt.opts) 1007 if !reflect.DeepEqual(want, got) { 1008 t.Errorf("#%d: incorrect watcher: want=%#v got=%#v", i, want, got) 1009 } 1010 } 1011 } 1012 1013 func TestHTTPKeysAPISetAction(t *testing.T) { 1014 tests := []struct { 1015 key string 1016 value string 1017 opts *SetOptions 1018 wantAction httpAction 1019 }{ 1020 // nil SetOptions 1021 { 1022 key: "/foo", 1023 value: "bar", 1024 opts: nil, 1025 wantAction: &setAction{ 1026 Key: "/foo", 1027 Value: "bar", 1028 PrevValue: "", 1029 PrevIndex: 0, 1030 PrevExist: PrevIgnore, 1031 TTL: 0, 1032 }, 1033 }, 1034 // empty SetOptions 1035 { 1036 key: "/foo", 1037 value: "bar", 1038 opts: &SetOptions{}, 1039 wantAction: &setAction{ 1040 Key: "/foo", 1041 Value: "bar", 1042 PrevValue: "", 1043 PrevIndex: 0, 1044 PrevExist: PrevIgnore, 1045 TTL: 0, 1046 }, 1047 }, 1048 // populated SetOptions 1049 { 1050 key: "/foo", 1051 value: "bar", 1052 opts: &SetOptions{ 1053 PrevValue: "baz", 1054 PrevIndex: 13, 1055 PrevExist: PrevExist, 1056 TTL: time.Minute, 1057 Dir: true, 1058 }, 1059 wantAction: &setAction{ 1060 Key: "/foo", 1061 Value: "bar", 1062 PrevValue: "baz", 1063 PrevIndex: 13, 1064 PrevExist: PrevExist, 1065 TTL: time.Minute, 1066 Dir: true, 1067 }, 1068 }, 1069 } 1070 1071 for i, tt := range tests { 1072 client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction} 1073 kAPI := httpKeysAPI{client: client} 1074 kAPI.Set(context.Background(), tt.key, tt.value, tt.opts) 1075 } 1076 } 1077 1078 func TestHTTPKeysAPISetError(t *testing.T) { 1079 tests := []httpClient{ 1080 // generic HTTP client failure 1081 &staticHTTPClient{ 1082 err: errors.New("fail!"), 1083 }, 1084 1085 // unusable status code 1086 &staticHTTPClient{ 1087 resp: http.Response{ 1088 StatusCode: http.StatusTeapot, 1089 }, 1090 }, 1091 1092 // etcd Error response 1093 &staticHTTPClient{ 1094 resp: http.Response{ 1095 StatusCode: http.StatusInternalServerError, 1096 }, 1097 body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`), 1098 }, 1099 } 1100 1101 for i, tt := range tests { 1102 kAPI := httpKeysAPI{client: tt} 1103 resp, err := kAPI.Set(context.Background(), "/foo", "bar", nil) 1104 if err == nil { 1105 t.Errorf("#%d: received nil error", i) 1106 } 1107 if resp != nil { 1108 t.Errorf("#%d: received non-nil Response: %#v", i, resp) 1109 } 1110 } 1111 } 1112 1113 func TestHTTPKeysAPISetResponse(t *testing.T) { 1114 client := &staticHTTPClient{ 1115 resp: http.Response{ 1116 StatusCode: http.StatusOK, 1117 Header: http.Header{"X-Etcd-Index": []string{"21"}}, 1118 }, 1119 body: []byte(`{"action":"set","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":21},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`), 1120 } 1121 1122 wantResponse := &Response{ 1123 Action: "set", 1124 Node: &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(21), ModifiedIndex: uint64(21)}, 1125 PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)}, 1126 Index: uint64(21), 1127 } 1128 1129 kAPI := &httpKeysAPI{client: client, prefix: "/pants"} 1130 resp, err := kAPI.Set(context.Background(), "/foo/bar/baz", "snarf", nil) 1131 if err != nil { 1132 t.Errorf("non-nil error: %#v", err) 1133 } 1134 if !reflect.DeepEqual(wantResponse, resp) { 1135 t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp) 1136 } 1137 } 1138 1139 func TestHTTPKeysAPIGetAction(t *testing.T) { 1140 tests := []struct { 1141 key string 1142 opts *GetOptions 1143 wantAction httpAction 1144 }{ 1145 // nil GetOptions 1146 { 1147 key: "/foo", 1148 opts: nil, 1149 wantAction: &getAction{ 1150 Key: "/foo", 1151 Sorted: false, 1152 Recursive: false, 1153 }, 1154 }, 1155 // empty GetOptions 1156 { 1157 key: "/foo", 1158 opts: &GetOptions{}, 1159 wantAction: &getAction{ 1160 Key: "/foo", 1161 Sorted: false, 1162 Recursive: false, 1163 }, 1164 }, 1165 // populated GetOptions 1166 { 1167 key: "/foo", 1168 opts: &GetOptions{ 1169 Sort: true, 1170 Recursive: true, 1171 Quorum: true, 1172 }, 1173 wantAction: &getAction{ 1174 Key: "/foo", 1175 Sorted: true, 1176 Recursive: true, 1177 Quorum: true, 1178 }, 1179 }, 1180 } 1181 1182 for i, tt := range tests { 1183 client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction} 1184 kAPI := httpKeysAPI{client: client} 1185 kAPI.Get(context.Background(), tt.key, tt.opts) 1186 } 1187 } 1188 1189 func TestHTTPKeysAPIGetError(t *testing.T) { 1190 tests := []httpClient{ 1191 // generic HTTP client failure 1192 &staticHTTPClient{ 1193 err: errors.New("fail!"), 1194 }, 1195 1196 // unusable status code 1197 &staticHTTPClient{ 1198 resp: http.Response{ 1199 StatusCode: http.StatusTeapot, 1200 }, 1201 }, 1202 1203 // etcd Error response 1204 &staticHTTPClient{ 1205 resp: http.Response{ 1206 StatusCode: http.StatusInternalServerError, 1207 }, 1208 body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`), 1209 }, 1210 } 1211 1212 for i, tt := range tests { 1213 kAPI := httpKeysAPI{client: tt} 1214 resp, err := kAPI.Get(context.Background(), "/foo", nil) 1215 if err == nil { 1216 t.Errorf("#%d: received nil error", i) 1217 } 1218 if resp != nil { 1219 t.Errorf("#%d: received non-nil Response: %#v", i, resp) 1220 } 1221 } 1222 } 1223 1224 func TestHTTPKeysAPIGetResponse(t *testing.T) { 1225 client := &staticHTTPClient{ 1226 resp: http.Response{ 1227 StatusCode: http.StatusOK, 1228 Header: http.Header{"X-Etcd-Index": []string{"42"}}, 1229 }, 1230 body: []byte(`{"action":"get","node":{"key":"/pants/foo/bar","modifiedIndex":25,"createdIndex":19,"nodes":[{"key":"/pants/foo/bar/baz","value":"snarf","createdIndex":21,"modifiedIndex":25}]}}`), 1231 } 1232 1233 wantResponse := &Response{ 1234 Action: "get", 1235 Node: &Node{ 1236 Key: "/pants/foo/bar", 1237 Nodes: []*Node{ 1238 {Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: 21, ModifiedIndex: 25}, 1239 }, 1240 CreatedIndex: uint64(19), 1241 ModifiedIndex: uint64(25), 1242 }, 1243 Index: uint64(42), 1244 } 1245 1246 kAPI := &httpKeysAPI{client: client, prefix: "/pants"} 1247 resp, err := kAPI.Get(context.Background(), "/foo/bar", &GetOptions{Recursive: true}) 1248 if err != nil { 1249 t.Errorf("non-nil error: %#v", err) 1250 } 1251 if !reflect.DeepEqual(wantResponse, resp) { 1252 t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp) 1253 } 1254 } 1255 1256 func TestHTTPKeysAPIDeleteAction(t *testing.T) { 1257 tests := []struct { 1258 key string 1259 opts *DeleteOptions 1260 wantAction httpAction 1261 }{ 1262 // nil DeleteOptions 1263 { 1264 key: "/foo", 1265 opts: nil, 1266 wantAction: &deleteAction{ 1267 Key: "/foo", 1268 PrevValue: "", 1269 PrevIndex: 0, 1270 Recursive: false, 1271 }, 1272 }, 1273 // empty DeleteOptions 1274 { 1275 key: "/foo", 1276 opts: &DeleteOptions{}, 1277 wantAction: &deleteAction{ 1278 Key: "/foo", 1279 PrevValue: "", 1280 PrevIndex: 0, 1281 Recursive: false, 1282 }, 1283 }, 1284 // populated DeleteOptions 1285 { 1286 key: "/foo", 1287 opts: &DeleteOptions{ 1288 PrevValue: "baz", 1289 PrevIndex: 13, 1290 Recursive: true, 1291 }, 1292 wantAction: &deleteAction{ 1293 Key: "/foo", 1294 PrevValue: "baz", 1295 PrevIndex: 13, 1296 Recursive: true, 1297 }, 1298 }, 1299 } 1300 1301 for i, tt := range tests { 1302 client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction} 1303 kAPI := httpKeysAPI{client: client} 1304 kAPI.Delete(context.Background(), tt.key, tt.opts) 1305 } 1306 } 1307 1308 func TestHTTPKeysAPIDeleteError(t *testing.T) { 1309 tests := []httpClient{ 1310 // generic HTTP client failure 1311 &staticHTTPClient{ 1312 err: errors.New("fail!"), 1313 }, 1314 1315 // unusable status code 1316 &staticHTTPClient{ 1317 resp: http.Response{ 1318 StatusCode: http.StatusTeapot, 1319 }, 1320 }, 1321 1322 // etcd Error response 1323 &staticHTTPClient{ 1324 resp: http.Response{ 1325 StatusCode: http.StatusInternalServerError, 1326 }, 1327 body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`), 1328 }, 1329 } 1330 1331 for i, tt := range tests { 1332 kAPI := httpKeysAPI{client: tt} 1333 resp, err := kAPI.Delete(context.Background(), "/foo", nil) 1334 if err == nil { 1335 t.Errorf("#%d: received nil error", i) 1336 } 1337 if resp != nil { 1338 t.Errorf("#%d: received non-nil Response: %#v", i, resp) 1339 } 1340 } 1341 } 1342 1343 func TestHTTPKeysAPIDeleteResponse(t *testing.T) { 1344 client := &staticHTTPClient{ 1345 resp: http.Response{ 1346 StatusCode: http.StatusOK, 1347 Header: http.Header{"X-Etcd-Index": []string{"22"}}, 1348 }, 1349 body: []byte(`{"action":"delete","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":22,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`), 1350 } 1351 1352 wantResponse := &Response{ 1353 Action: "delete", 1354 Node: &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(22)}, 1355 PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)}, 1356 Index: uint64(22), 1357 } 1358 1359 kAPI := &httpKeysAPI{client: client, prefix: "/pants"} 1360 resp, err := kAPI.Delete(context.Background(), "/foo/bar/baz", nil) 1361 if err != nil { 1362 t.Errorf("non-nil error: %#v", err) 1363 } 1364 if !reflect.DeepEqual(wantResponse, resp) { 1365 t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp) 1366 } 1367 } 1368 1369 func TestHTTPKeysAPICreateAction(t *testing.T) { 1370 act := &setAction{ 1371 Key: "/foo", 1372 Value: "bar", 1373 PrevExist: PrevNoExist, 1374 PrevIndex: 0, 1375 PrevValue: "", 1376 TTL: 0, 1377 } 1378 1379 kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}} 1380 kAPI.Create(context.Background(), "/foo", "bar") 1381 } 1382 1383 func TestHTTPKeysAPICreateInOrderAction(t *testing.T) { 1384 act := &createInOrderAction{ 1385 Dir: "/foo", 1386 Value: "bar", 1387 TTL: 0, 1388 } 1389 kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}} 1390 kAPI.CreateInOrder(context.Background(), "/foo", "bar", nil) 1391 } 1392 1393 func TestHTTPKeysAPIUpdateAction(t *testing.T) { 1394 act := &setAction{ 1395 Key: "/foo", 1396 Value: "bar", 1397 PrevExist: PrevExist, 1398 PrevIndex: 0, 1399 PrevValue: "", 1400 TTL: 0, 1401 } 1402 1403 kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}} 1404 kAPI.Update(context.Background(), "/foo", "bar") 1405 } 1406 1407 func TestNodeTTLDuration(t *testing.T) { 1408 tests := []struct { 1409 node *Node 1410 want time.Duration 1411 }{ 1412 { 1413 node: &Node{TTL: 0}, 1414 want: 0, 1415 }, 1416 { 1417 node: &Node{TTL: 97}, 1418 want: 97 * time.Second, 1419 }, 1420 } 1421 1422 for i, tt := range tests { 1423 got := tt.node.TTLDuration() 1424 if tt.want != got { 1425 t.Errorf("#%d: incorrect duration: want=%v got=%v", i, tt.want, got) 1426 } 1427 } 1428 }