client_test.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  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. package client
  15. import (
  16. "errors"
  17. "io"
  18. "io/ioutil"
  19. "math/rand"
  20. "net/http"
  21. "net/url"
  22. "reflect"
  23. "sort"
  24. "strings"
  25. "testing"
  26. "time"
  27. "github.com/coreos/etcd/pkg/testutil"
  28. "github.com/coreos/etcd/version"
  29. "golang.org/x/net/context"
  30. )
  31. type actionAssertingHTTPClient struct {
  32. t *testing.T
  33. num int
  34. act httpAction
  35. resp http.Response
  36. body []byte
  37. err error
  38. }
  39. func (a *actionAssertingHTTPClient) Do(_ context.Context, act httpAction) (*http.Response, []byte, error) {
  40. if !reflect.DeepEqual(a.act, act) {
  41. a.t.Errorf("#%d: unexpected httpAction: want=%#v got=%#v", a.num, a.act, act)
  42. }
  43. return &a.resp, a.body, a.err
  44. }
  45. type staticHTTPClient struct {
  46. resp http.Response
  47. body []byte
  48. err error
  49. }
  50. func (s *staticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  51. return &s.resp, s.body, s.err
  52. }
  53. type staticHTTPAction struct {
  54. request http.Request
  55. }
  56. func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
  57. return &s.request
  58. }
  59. type staticHTTPResponse struct {
  60. resp http.Response
  61. body []byte
  62. err error
  63. }
  64. type multiStaticHTTPClient struct {
  65. responses []staticHTTPResponse
  66. cur int
  67. }
  68. func (s *multiStaticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  69. r := s.responses[s.cur]
  70. s.cur++
  71. return &r.resp, r.body, r.err
  72. }
  73. func newStaticHTTPClientFactory(responses []staticHTTPResponse) httpClientFactory {
  74. var cur int
  75. return func(url.URL) httpClient {
  76. r := responses[cur]
  77. cur++
  78. return &staticHTTPClient{resp: r.resp, body: r.body, err: r.err}
  79. }
  80. }
  81. type fakeTransport struct {
  82. respchan chan *http.Response
  83. errchan chan error
  84. startCancel chan struct{}
  85. finishCancel chan struct{}
  86. }
  87. func newFakeTransport() *fakeTransport {
  88. return &fakeTransport{
  89. respchan: make(chan *http.Response, 1),
  90. errchan: make(chan error, 1),
  91. startCancel: make(chan struct{}, 1),
  92. finishCancel: make(chan struct{}, 1),
  93. }
  94. }
  95. func (t *fakeTransport) CancelRequest(*http.Request) {
  96. t.startCancel <- struct{}{}
  97. }
  98. type fakeAction struct{}
  99. func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
  100. return &http.Request{}
  101. }
  102. func TestSimpleHTTPClientDoSuccess(t *testing.T) {
  103. tr := newFakeTransport()
  104. c := &simpleHTTPClient{transport: tr}
  105. tr.respchan <- &http.Response{
  106. StatusCode: http.StatusTeapot,
  107. Body: ioutil.NopCloser(strings.NewReader("foo")),
  108. }
  109. resp, body, err := c.Do(context.Background(), &fakeAction{})
  110. if err != nil {
  111. t.Fatalf("incorrect error value: want=nil got=%v", err)
  112. }
  113. wantCode := http.StatusTeapot
  114. if wantCode != resp.StatusCode {
  115. t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
  116. }
  117. wantBody := []byte("foo")
  118. if !reflect.DeepEqual(wantBody, body) {
  119. t.Fatalf("invalid response body: want=%q got=%q", wantBody, body)
  120. }
  121. }
  122. func TestSimpleHTTPClientDoError(t *testing.T) {
  123. tr := newFakeTransport()
  124. c := &simpleHTTPClient{transport: tr}
  125. tr.errchan <- errors.New("fixture")
  126. _, _, err := c.Do(context.Background(), &fakeAction{})
  127. if err == nil {
  128. t.Fatalf("expected non-nil error, got nil")
  129. }
  130. }
  131. func TestSimpleHTTPClientDoCancelContext(t *testing.T) {
  132. tr := newFakeTransport()
  133. c := &simpleHTTPClient{transport: tr}
  134. tr.startCancel <- struct{}{}
  135. tr.finishCancel <- struct{}{}
  136. _, _, err := c.Do(context.Background(), &fakeAction{})
  137. if err == nil {
  138. t.Fatalf("expected non-nil error, got nil")
  139. }
  140. }
  141. type checkableReadCloser struct {
  142. io.ReadCloser
  143. closed bool
  144. }
  145. func (c *checkableReadCloser) Close() error {
  146. if !c.closed {
  147. c.closed = true
  148. return c.ReadCloser.Close()
  149. }
  150. return nil
  151. }
  152. func TestSimpleHTTPClientDoCancelContextResponseBodyClosed(t *testing.T) {
  153. tr := newFakeTransport()
  154. c := &simpleHTTPClient{transport: tr}
  155. // create an already-cancelled context
  156. ctx, cancel := context.WithCancel(context.Background())
  157. cancel()
  158. body := &checkableReadCloser{ReadCloser: ioutil.NopCloser(strings.NewReader("foo"))}
  159. go func() {
  160. // wait that simpleHTTPClient knows the context is already timed out,
  161. // and calls CancelRequest
  162. testutil.WaitSchedule()
  163. // response is returned before cancel effects
  164. tr.respchan <- &http.Response{Body: body}
  165. }()
  166. _, _, err := c.Do(ctx, &fakeAction{})
  167. if err == nil {
  168. t.Fatalf("expected non-nil error, got nil")
  169. }
  170. if !body.closed {
  171. t.Fatalf("expected closed body")
  172. }
  173. }
  174. type blockingBody struct {
  175. c chan struct{}
  176. }
  177. func (bb *blockingBody) Read(p []byte) (n int, err error) {
  178. <-bb.c
  179. return 0, errors.New("closed")
  180. }
  181. func (bb *blockingBody) Close() error {
  182. close(bb.c)
  183. return nil
  184. }
  185. func TestSimpleHTTPClientDoCancelContextResponseBodyClosedWithBlockingBody(t *testing.T) {
  186. tr := newFakeTransport()
  187. c := &simpleHTTPClient{transport: tr}
  188. ctx, cancel := context.WithCancel(context.Background())
  189. body := &checkableReadCloser{ReadCloser: &blockingBody{c: make(chan struct{})}}
  190. go func() {
  191. tr.respchan <- &http.Response{Body: body}
  192. time.Sleep(2 * time.Millisecond)
  193. // cancel after the body is received
  194. cancel()
  195. }()
  196. _, _, err := c.Do(ctx, &fakeAction{})
  197. if err != context.Canceled {
  198. t.Fatalf("expected %+v, got %+v", context.Canceled, err)
  199. }
  200. if !body.closed {
  201. t.Fatalf("expected closed body")
  202. }
  203. }
  204. func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
  205. tr := newFakeTransport()
  206. c := &simpleHTTPClient{transport: tr}
  207. donechan := make(chan struct{})
  208. ctx, cancel := context.WithCancel(context.Background())
  209. go func() {
  210. c.Do(ctx, &fakeAction{})
  211. close(donechan)
  212. }()
  213. // This should call CancelRequest and begin the cancellation process
  214. cancel()
  215. select {
  216. case <-donechan:
  217. t.Fatalf("simpleHTTPClient.Do should not have exited yet")
  218. default:
  219. }
  220. tr.finishCancel <- struct{}{}
  221. select {
  222. case <-donechan:
  223. //expected behavior
  224. return
  225. case <-time.After(time.Second):
  226. t.Fatalf("simpleHTTPClient.Do did not exit within 1s")
  227. }
  228. }
  229. func TestSimpleHTTPClientDoHeaderTimeout(t *testing.T) {
  230. tr := newFakeTransport()
  231. tr.finishCancel <- struct{}{}
  232. c := &simpleHTTPClient{transport: tr, headerTimeout: time.Millisecond}
  233. errc := make(chan error)
  234. go func() {
  235. _, _, err := c.Do(context.Background(), &fakeAction{})
  236. errc <- err
  237. }()
  238. select {
  239. case err := <-errc:
  240. if err == nil {
  241. t.Fatalf("expected non-nil error, got nil")
  242. }
  243. case <-time.After(time.Second):
  244. t.Fatalf("unexpected timeout when waiting for the test to finish")
  245. }
  246. }
  247. func TestHTTPClusterClientDo(t *testing.T) {
  248. fakeErr := errors.New("fake!")
  249. fakeURL := url.URL{}
  250. tests := []struct {
  251. client *httpClusterClient
  252. wantCode int
  253. wantErr error
  254. wantPinned int
  255. }{
  256. // first good response short-circuits Do
  257. {
  258. client: &httpClusterClient{
  259. endpoints: []url.URL{fakeURL, fakeURL},
  260. clientFactory: newStaticHTTPClientFactory(
  261. []staticHTTPResponse{
  262. {resp: http.Response{StatusCode: http.StatusTeapot}},
  263. {err: fakeErr},
  264. },
  265. ),
  266. rand: rand.New(rand.NewSource(0)),
  267. },
  268. wantCode: http.StatusTeapot,
  269. },
  270. // fall through to good endpoint if err is arbitrary
  271. {
  272. client: &httpClusterClient{
  273. endpoints: []url.URL{fakeURL, fakeURL},
  274. clientFactory: newStaticHTTPClientFactory(
  275. []staticHTTPResponse{
  276. {err: fakeErr},
  277. {resp: http.Response{StatusCode: http.StatusTeapot}},
  278. },
  279. ),
  280. rand: rand.New(rand.NewSource(0)),
  281. },
  282. wantCode: http.StatusTeapot,
  283. wantPinned: 1,
  284. },
  285. // context.Canceled short-circuits Do
  286. {
  287. client: &httpClusterClient{
  288. endpoints: []url.URL{fakeURL, fakeURL},
  289. clientFactory: newStaticHTTPClientFactory(
  290. []staticHTTPResponse{
  291. {err: context.Canceled},
  292. {resp: http.Response{StatusCode: http.StatusTeapot}},
  293. },
  294. ),
  295. rand: rand.New(rand.NewSource(0)),
  296. },
  297. wantErr: context.Canceled,
  298. },
  299. // return err if there are no endpoints
  300. {
  301. client: &httpClusterClient{
  302. endpoints: []url.URL{},
  303. clientFactory: newHTTPClientFactory(nil, nil, 0),
  304. rand: rand.New(rand.NewSource(0)),
  305. },
  306. wantErr: ErrNoEndpoints,
  307. },
  308. // return err if all endpoints return arbitrary errors
  309. {
  310. client: &httpClusterClient{
  311. endpoints: []url.URL{fakeURL, fakeURL},
  312. clientFactory: newStaticHTTPClientFactory(
  313. []staticHTTPResponse{
  314. {err: fakeErr},
  315. {err: fakeErr},
  316. },
  317. ),
  318. rand: rand.New(rand.NewSource(0)),
  319. },
  320. wantErr: &ClusterError{Errors: []error{fakeErr, fakeErr}},
  321. },
  322. // 500-level errors cause Do to fallthrough to next endpoint
  323. {
  324. client: &httpClusterClient{
  325. endpoints: []url.URL{fakeURL, fakeURL},
  326. clientFactory: newStaticHTTPClientFactory(
  327. []staticHTTPResponse{
  328. {resp: http.Response{StatusCode: http.StatusBadGateway}},
  329. {resp: http.Response{StatusCode: http.StatusTeapot}},
  330. },
  331. ),
  332. rand: rand.New(rand.NewSource(0)),
  333. },
  334. wantCode: http.StatusTeapot,
  335. wantPinned: 1,
  336. },
  337. }
  338. for i, tt := range tests {
  339. resp, _, err := tt.client.Do(context.Background(), nil)
  340. if !reflect.DeepEqual(tt.wantErr, err) {
  341. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  342. continue
  343. }
  344. if resp == nil {
  345. if tt.wantCode != 0 {
  346. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  347. }
  348. continue
  349. }
  350. if resp.StatusCode != tt.wantCode {
  351. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  352. continue
  353. }
  354. if tt.client.pinned != tt.wantPinned {
  355. t.Errorf("#%d: pinned=%d, want=%d", i, tt.client.pinned, tt.wantPinned)
  356. }
  357. }
  358. }
  359. func TestHTTPClusterClientDoDeadlineExceedContext(t *testing.T) {
  360. fakeURL := url.URL{}
  361. tr := newFakeTransport()
  362. tr.finishCancel <- struct{}{}
  363. c := &httpClusterClient{
  364. clientFactory: newHTTPClientFactory(tr, DefaultCheckRedirect, 0),
  365. endpoints: []url.URL{fakeURL},
  366. }
  367. errc := make(chan error)
  368. go func() {
  369. ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
  370. defer cancel()
  371. _, _, err := c.Do(ctx, &fakeAction{})
  372. errc <- err
  373. }()
  374. select {
  375. case err := <-errc:
  376. if err != context.DeadlineExceeded {
  377. t.Errorf("err = %+v, want %+v", err, context.DeadlineExceeded)
  378. }
  379. case <-time.After(time.Second):
  380. t.Fatalf("unexpected timeout when waiting for request to deadline exceed")
  381. }
  382. }
  383. type fakeCancelContext struct{}
  384. var fakeCancelContextError = errors.New("fake context canceled")
  385. func (f fakeCancelContext) Deadline() (time.Time, bool) { return time.Time{}, false }
  386. func (f fakeCancelContext) Done() <-chan struct{} {
  387. d := make(chan struct{}, 1)
  388. d <- struct{}{}
  389. return d
  390. }
  391. func (f fakeCancelContext) Err() error { return fakeCancelContextError }
  392. func (f fakeCancelContext) Value(key interface{}) interface{} { return 1 }
  393. func withTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
  394. return parent, func() { parent = nil }
  395. }
  396. func TestHTTPClusterClientDoCanceledContext(t *testing.T) {
  397. fakeURL := url.URL{}
  398. tr := newFakeTransport()
  399. tr.finishCancel <- struct{}{}
  400. c := &httpClusterClient{
  401. clientFactory: newHTTPClientFactory(tr, DefaultCheckRedirect, 0),
  402. endpoints: []url.URL{fakeURL},
  403. }
  404. errc := make(chan error)
  405. go func() {
  406. ctx, cancel := withTimeout(fakeCancelContext{}, time.Millisecond)
  407. cancel()
  408. _, _, err := c.Do(ctx, &fakeAction{})
  409. errc <- err
  410. }()
  411. select {
  412. case err := <-errc:
  413. if err != fakeCancelContextError {
  414. t.Errorf("err = %+v, want %+v", err, fakeCancelContextError)
  415. }
  416. case <-time.After(time.Second):
  417. t.Fatalf("unexpected timeout when waiting for request to fake context canceled")
  418. }
  419. }
  420. func TestRedirectedHTTPAction(t *testing.T) {
  421. act := &redirectedHTTPAction{
  422. action: &staticHTTPAction{
  423. request: http.Request{
  424. Method: "DELETE",
  425. URL: &url.URL{
  426. Scheme: "https",
  427. Host: "foo.example.com",
  428. Path: "/ping",
  429. },
  430. },
  431. },
  432. location: url.URL{
  433. Scheme: "https",
  434. Host: "bar.example.com",
  435. Path: "/pong",
  436. },
  437. }
  438. want := &http.Request{
  439. Method: "DELETE",
  440. URL: &url.URL{
  441. Scheme: "https",
  442. Host: "bar.example.com",
  443. Path: "/pong",
  444. },
  445. }
  446. got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
  447. if !reflect.DeepEqual(want, got) {
  448. t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
  449. }
  450. }
  451. func TestRedirectFollowingHTTPClient(t *testing.T) {
  452. tests := []struct {
  453. checkRedirect CheckRedirectFunc
  454. client httpClient
  455. wantCode int
  456. wantErr error
  457. }{
  458. // errors bubbled up
  459. {
  460. checkRedirect: func(int) error { return ErrTooManyRedirects },
  461. client: &multiStaticHTTPClient{
  462. responses: []staticHTTPResponse{
  463. {
  464. err: errors.New("fail!"),
  465. },
  466. },
  467. },
  468. wantErr: errors.New("fail!"),
  469. },
  470. // no need to follow redirect if none given
  471. {
  472. checkRedirect: func(int) error { return ErrTooManyRedirects },
  473. client: &multiStaticHTTPClient{
  474. responses: []staticHTTPResponse{
  475. {
  476. resp: http.Response{
  477. StatusCode: http.StatusTeapot,
  478. },
  479. },
  480. },
  481. },
  482. wantCode: http.StatusTeapot,
  483. },
  484. // redirects if less than max
  485. {
  486. checkRedirect: func(via int) error {
  487. if via >= 2 {
  488. return ErrTooManyRedirects
  489. }
  490. return nil
  491. },
  492. client: &multiStaticHTTPClient{
  493. responses: []staticHTTPResponse{
  494. {
  495. resp: http.Response{
  496. StatusCode: http.StatusTemporaryRedirect,
  497. Header: http.Header{"Location": []string{"http://example.com"}},
  498. },
  499. },
  500. {
  501. resp: http.Response{
  502. StatusCode: http.StatusTeapot,
  503. },
  504. },
  505. },
  506. },
  507. wantCode: http.StatusTeapot,
  508. },
  509. // succeed after reaching max redirects
  510. {
  511. checkRedirect: func(via int) error {
  512. if via >= 3 {
  513. return ErrTooManyRedirects
  514. }
  515. return nil
  516. },
  517. client: &multiStaticHTTPClient{
  518. responses: []staticHTTPResponse{
  519. {
  520. resp: http.Response{
  521. StatusCode: http.StatusTemporaryRedirect,
  522. Header: http.Header{"Location": []string{"http://example.com"}},
  523. },
  524. },
  525. {
  526. resp: http.Response{
  527. StatusCode: http.StatusTemporaryRedirect,
  528. Header: http.Header{"Location": []string{"http://example.com"}},
  529. },
  530. },
  531. {
  532. resp: http.Response{
  533. StatusCode: http.StatusTeapot,
  534. },
  535. },
  536. },
  537. },
  538. wantCode: http.StatusTeapot,
  539. },
  540. // fail if too many redirects
  541. {
  542. checkRedirect: func(via int) error {
  543. if via >= 2 {
  544. return ErrTooManyRedirects
  545. }
  546. return nil
  547. },
  548. client: &multiStaticHTTPClient{
  549. responses: []staticHTTPResponse{
  550. {
  551. resp: http.Response{
  552. StatusCode: http.StatusTemporaryRedirect,
  553. Header: http.Header{"Location": []string{"http://example.com"}},
  554. },
  555. },
  556. {
  557. resp: http.Response{
  558. StatusCode: http.StatusTemporaryRedirect,
  559. Header: http.Header{"Location": []string{"http://example.com"}},
  560. },
  561. },
  562. {
  563. resp: http.Response{
  564. StatusCode: http.StatusTeapot,
  565. },
  566. },
  567. },
  568. },
  569. wantErr: ErrTooManyRedirects,
  570. },
  571. // fail if Location header not set
  572. {
  573. checkRedirect: func(int) error { return ErrTooManyRedirects },
  574. client: &multiStaticHTTPClient{
  575. responses: []staticHTTPResponse{
  576. {
  577. resp: http.Response{
  578. StatusCode: http.StatusTemporaryRedirect,
  579. },
  580. },
  581. },
  582. },
  583. wantErr: errors.New("Location header not set"),
  584. },
  585. // fail if Location header is invalid
  586. {
  587. checkRedirect: func(int) error { return ErrTooManyRedirects },
  588. client: &multiStaticHTTPClient{
  589. responses: []staticHTTPResponse{
  590. {
  591. resp: http.Response{
  592. StatusCode: http.StatusTemporaryRedirect,
  593. Header: http.Header{"Location": []string{":"}},
  594. },
  595. },
  596. },
  597. },
  598. wantErr: errors.New("Location header not valid URL: :"),
  599. },
  600. // fail if redirects checked way too many times
  601. {
  602. checkRedirect: func(int) error { return nil },
  603. client: &staticHTTPClient{
  604. resp: http.Response{
  605. StatusCode: http.StatusTemporaryRedirect,
  606. Header: http.Header{"Location": []string{"http://example.com"}},
  607. },
  608. },
  609. wantErr: errTooManyRedirectChecks,
  610. },
  611. }
  612. for i, tt := range tests {
  613. client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect}
  614. resp, _, err := client.Do(context.Background(), nil)
  615. if !reflect.DeepEqual(tt.wantErr, err) {
  616. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  617. continue
  618. }
  619. if resp == nil {
  620. if tt.wantCode != 0 {
  621. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  622. }
  623. continue
  624. }
  625. if resp.StatusCode != tt.wantCode {
  626. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  627. continue
  628. }
  629. }
  630. }
  631. func TestDefaultCheckRedirect(t *testing.T) {
  632. tests := []struct {
  633. num int
  634. err error
  635. }{
  636. {0, nil},
  637. {5, nil},
  638. {10, nil},
  639. {11, ErrTooManyRedirects},
  640. {29, ErrTooManyRedirects},
  641. }
  642. for i, tt := range tests {
  643. err := DefaultCheckRedirect(tt.num)
  644. if !reflect.DeepEqual(tt.err, err) {
  645. t.Errorf("#%d: want=%#v got=%#v", i, tt.err, err)
  646. }
  647. }
  648. }
  649. func TestHTTPClusterClientSync(t *testing.T) {
  650. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  651. {
  652. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  653. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  654. },
  655. })
  656. hc := &httpClusterClient{
  657. clientFactory: cf,
  658. rand: rand.New(rand.NewSource(0)),
  659. }
  660. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  661. if err != nil {
  662. t.Fatalf("unexpected error during setup: %#v", err)
  663. }
  664. want := []string{"http://127.0.0.1:2379"}
  665. got := hc.Endpoints()
  666. if !reflect.DeepEqual(want, got) {
  667. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  668. }
  669. err = hc.Sync(context.Background())
  670. if err != nil {
  671. t.Fatalf("unexpected error during Sync: %#v", err)
  672. }
  673. want = []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"}
  674. got = hc.Endpoints()
  675. sort.Sort(sort.StringSlice(got))
  676. if !reflect.DeepEqual(want, got) {
  677. t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got)
  678. }
  679. err = hc.SetEndpoints([]string{"http://127.0.0.1:4009"})
  680. if err != nil {
  681. t.Fatalf("unexpected error during reset: %#v", err)
  682. }
  683. want = []string{"http://127.0.0.1:4009"}
  684. got = hc.Endpoints()
  685. if !reflect.DeepEqual(want, got) {
  686. t.Fatalf("incorrect endpoints post-reset: want=%#v got=%#v", want, got)
  687. }
  688. }
  689. func TestHTTPClusterClientSyncFail(t *testing.T) {
  690. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  691. {err: errors.New("fail!")},
  692. })
  693. hc := &httpClusterClient{
  694. clientFactory: cf,
  695. rand: rand.New(rand.NewSource(0)),
  696. }
  697. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  698. if err != nil {
  699. t.Fatalf("unexpected error during setup: %#v", err)
  700. }
  701. want := []string{"http://127.0.0.1:2379"}
  702. got := hc.Endpoints()
  703. if !reflect.DeepEqual(want, got) {
  704. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  705. }
  706. err = hc.Sync(context.Background())
  707. if err == nil {
  708. t.Fatalf("got nil error during Sync")
  709. }
  710. got = hc.Endpoints()
  711. if !reflect.DeepEqual(want, got) {
  712. t.Fatalf("incorrect endpoints after failed Sync: want=%#v got=%#v", want, got)
  713. }
  714. }
  715. func TestHTTPClusterClientAutoSyncCancelContext(t *testing.T) {
  716. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  717. {
  718. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  719. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  720. },
  721. })
  722. hc := &httpClusterClient{
  723. clientFactory: cf,
  724. rand: rand.New(rand.NewSource(0)),
  725. }
  726. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  727. if err != nil {
  728. t.Fatalf("unexpected error during setup: %#v", err)
  729. }
  730. ctx, cancel := context.WithCancel(context.Background())
  731. cancel()
  732. err = hc.AutoSync(ctx, time.Hour)
  733. if err != context.Canceled {
  734. t.Fatalf("incorrect error value: want=%v got=%v", context.Canceled, err)
  735. }
  736. }
  737. func TestHTTPClusterClientAutoSyncFail(t *testing.T) {
  738. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  739. {err: errors.New("fail!")},
  740. })
  741. hc := &httpClusterClient{
  742. clientFactory: cf,
  743. rand: rand.New(rand.NewSource(0)),
  744. }
  745. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  746. if err != nil {
  747. t.Fatalf("unexpected error during setup: %#v", err)
  748. }
  749. err = hc.AutoSync(context.Background(), time.Hour)
  750. if !strings.HasPrefix(err.Error(), ErrClusterUnavailable.Error()) {
  751. t.Fatalf("incorrect error value: want=%v got=%v", ErrClusterUnavailable, err)
  752. }
  753. }
  754. func TestHTTPClusterClientGetVersion(t *testing.T) {
  755. body := []byte(`{"etcdserver":"2.3.2","etcdcluster":"2.3.0"}`)
  756. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  757. {
  758. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Length": []string{"44"}}},
  759. body: body,
  760. },
  761. })
  762. hc := &httpClusterClient{
  763. clientFactory: cf,
  764. rand: rand.New(rand.NewSource(0)),
  765. }
  766. err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
  767. if err != nil {
  768. t.Fatalf("unexpected error during setup: %#v", err)
  769. }
  770. actual, err := hc.GetVersion(context.Background())
  771. if err != nil {
  772. t.Errorf("non-nil error: %#v", err)
  773. }
  774. expected := version.Versions{Server: "2.3.2", Cluster: "2.3.0"}
  775. if !reflect.DeepEqual(&expected, actual) {
  776. t.Errorf("incorrect Response: want=%#v got=%#v", expected, actual)
  777. }
  778. }
  779. // TestHTTPClusterClientSyncPinEndpoint tests that Sync() pins the endpoint when
  780. // it gets the exactly same member list as before.
  781. func TestHTTPClusterClientSyncPinEndpoint(t *testing.T) {
  782. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  783. {
  784. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  785. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  786. },
  787. {
  788. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  789. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  790. },
  791. {
  792. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  793. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  794. },
  795. })
  796. hc := &httpClusterClient{
  797. clientFactory: cf,
  798. rand: rand.New(rand.NewSource(0)),
  799. }
  800. err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
  801. if err != nil {
  802. t.Fatalf("unexpected error during setup: %#v", err)
  803. }
  804. pinnedEndpoint := hc.endpoints[hc.pinned]
  805. for i := 0; i < 3; i++ {
  806. err = hc.Sync(context.Background())
  807. if err != nil {
  808. t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
  809. }
  810. if g := hc.endpoints[hc.pinned]; g != pinnedEndpoint {
  811. t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, pinnedEndpoint)
  812. }
  813. }
  814. }
  815. // TestHTTPClusterClientSyncUnpinEndpoint tests that Sync() unpins the endpoint when
  816. // it gets a different member list than before.
  817. func TestHTTPClusterClientSyncUnpinEndpoint(t *testing.T) {
  818. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  819. {
  820. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  821. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  822. },
  823. {
  824. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  825. body: []byte(`{"members":[{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  826. },
  827. {
  828. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  829. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  830. },
  831. })
  832. hc := &httpClusterClient{
  833. clientFactory: cf,
  834. rand: rand.New(rand.NewSource(0)),
  835. }
  836. err := hc.SetEndpoints([]string{"http://127.0.0.1:4003", "http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"})
  837. if err != nil {
  838. t.Fatalf("unexpected error during setup: %#v", err)
  839. }
  840. wants := []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}
  841. for i := 0; i < 3; i++ {
  842. err = hc.Sync(context.Background())
  843. if err != nil {
  844. t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
  845. }
  846. if g := hc.endpoints[hc.pinned]; g.String() != wants[i] {
  847. t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, wants[i])
  848. }
  849. }
  850. }
  851. // TestHTTPClusterClientSyncPinLeaderEndpoint tests that Sync() pins the leader
  852. // when the selection mode is EndpointSelectionPrioritizeLeader
  853. func TestHTTPClusterClientSyncPinLeaderEndpoint(t *testing.T) {
  854. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  855. {
  856. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  857. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  858. },
  859. {
  860. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  861. body: []byte(`{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]}`),
  862. },
  863. {
  864. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  865. body: []byte(`{"members":[{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]},{"id":"42134f434382925","peerURLs":["http://127.0.0.1:2380","http://127.0.0.1:7001"],"name":"node1","clientURLs":["http://127.0.0.1:2379","http://127.0.0.1:4001"]},{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}]}`),
  866. },
  867. {
  868. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  869. body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}`),
  870. },
  871. })
  872. hc := &httpClusterClient{
  873. clientFactory: cf,
  874. rand: rand.New(rand.NewSource(0)),
  875. selectionMode: EndpointSelectionPrioritizeLeader,
  876. endpoints: []url.URL{{}}, // Need somewhere to pretend to send to initially
  877. }
  878. wants := []string{"http://127.0.0.1:4003", "http://127.0.0.1:4002"}
  879. for i, want := range wants {
  880. err := hc.Sync(context.Background())
  881. if err != nil {
  882. t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
  883. }
  884. pinned := hc.endpoints[hc.pinned].String()
  885. if pinned != want {
  886. t.Errorf("#%d: pinned endpoint = %v, want %v", i, pinned, want)
  887. }
  888. }
  889. }
  890. func TestHTTPClusterClientResetFail(t *testing.T) {
  891. tests := [][]string{
  892. // need at least one endpoint
  893. {},
  894. // urls must be valid
  895. {":"},
  896. }
  897. for i, tt := range tests {
  898. hc := &httpClusterClient{rand: rand.New(rand.NewSource(0))}
  899. err := hc.SetEndpoints(tt)
  900. if err == nil {
  901. t.Errorf("#%d: expected non-nil error", i)
  902. }
  903. }
  904. }
  905. func TestHTTPClusterClientResetPinRandom(t *testing.T) {
  906. round := 2000
  907. pinNum := 0
  908. for i := 0; i < round; i++ {
  909. hc := &httpClusterClient{rand: rand.New(rand.NewSource(int64(i)))}
  910. err := hc.SetEndpoints([]string{"http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"})
  911. if err != nil {
  912. t.Fatalf("#%d: reset error (%v)", i, err)
  913. }
  914. if hc.endpoints[hc.pinned].String() == "http://127.0.0.1:4001" {
  915. pinNum++
  916. }
  917. }
  918. min := 1.0/3.0 - 0.05
  919. max := 1.0/3.0 + 0.05
  920. if ratio := float64(pinNum) / float64(round); ratio > max || ratio < min {
  921. t.Errorf("pinned ratio = %v, want [%v, %v]", ratio, min, max)
  922. }
  923. }