client_test.go 31 KB

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