client_test.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  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. "github.com/coreos/etcd/pkg/testutil"
  30. "github.com/coreos/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 !reflect.DeepEqual(tt.wantErr, err) {
  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 fakeCancelContextError = 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 fakeCancelContextError }
  412. func (f fakeCancelContext) Value(key interface{}) interface{} { return 1 }
  413. func withTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
  414. return parent, func() { parent = nil }
  415. }
  416. func TestHTTPClusterClientDoCanceledContext(t *testing.T) {
  417. fakeURL := url.URL{}
  418. tr := newFakeTransport()
  419. tr.finishCancel <- struct{}{}
  420. c := &httpClusterClient{
  421. clientFactory: newHTTPClientFactory(tr, DefaultCheckRedirect, 0),
  422. endpoints: []url.URL{fakeURL},
  423. }
  424. errc := make(chan error)
  425. go func() {
  426. ctx, cancel := withTimeout(fakeCancelContext{}, time.Millisecond)
  427. cancel()
  428. _, _, err := c.Do(ctx, &fakeAction{})
  429. errc <- err
  430. }()
  431. select {
  432. case err := <-errc:
  433. if err != fakeCancelContextError {
  434. t.Errorf("err = %+v, want %+v", err, fakeCancelContextError)
  435. }
  436. case <-time.After(time.Second):
  437. t.Fatalf("unexpected timeout when waiting for request to fake context canceled")
  438. }
  439. }
  440. func TestRedirectedHTTPAction(t *testing.T) {
  441. act := &redirectedHTTPAction{
  442. action: &staticHTTPAction{
  443. request: http.Request{
  444. Method: "DELETE",
  445. URL: &url.URL{
  446. Scheme: "https",
  447. Host: "foo.example.com",
  448. Path: "/ping",
  449. },
  450. },
  451. },
  452. location: url.URL{
  453. Scheme: "https",
  454. Host: "bar.example.com",
  455. Path: "/pong",
  456. },
  457. }
  458. want := &http.Request{
  459. Method: "DELETE",
  460. URL: &url.URL{
  461. Scheme: "https",
  462. Host: "bar.example.com",
  463. Path: "/pong",
  464. },
  465. }
  466. got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
  467. if !reflect.DeepEqual(want, got) {
  468. t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
  469. }
  470. }
  471. func TestRedirectFollowingHTTPClient(t *testing.T) {
  472. tests := []struct {
  473. checkRedirect CheckRedirectFunc
  474. client httpClient
  475. wantCode int
  476. wantErr error
  477. }{
  478. // errors bubbled up
  479. {
  480. checkRedirect: func(int) error { return ErrTooManyRedirects },
  481. client: &multiStaticHTTPClient{
  482. responses: []staticHTTPResponse{
  483. {
  484. err: errors.New("fail!"),
  485. },
  486. },
  487. },
  488. wantErr: errors.New("fail!"),
  489. },
  490. // no need to follow redirect if none given
  491. {
  492. checkRedirect: func(int) error { return ErrTooManyRedirects },
  493. client: &multiStaticHTTPClient{
  494. responses: []staticHTTPResponse{
  495. {
  496. resp: http.Response{
  497. StatusCode: http.StatusTeapot,
  498. },
  499. },
  500. },
  501. },
  502. wantCode: http.StatusTeapot,
  503. },
  504. // redirects if less than max
  505. {
  506. checkRedirect: func(via int) error {
  507. if via >= 2 {
  508. return ErrTooManyRedirects
  509. }
  510. return nil
  511. },
  512. client: &multiStaticHTTPClient{
  513. responses: []staticHTTPResponse{
  514. {
  515. resp: http.Response{
  516. StatusCode: http.StatusTemporaryRedirect,
  517. Header: http.Header{"Location": []string{"http://example.com"}},
  518. },
  519. },
  520. {
  521. resp: http.Response{
  522. StatusCode: http.StatusTeapot,
  523. },
  524. },
  525. },
  526. },
  527. wantCode: http.StatusTeapot,
  528. },
  529. // succeed after reaching max redirects
  530. {
  531. checkRedirect: func(via int) error {
  532. if via >= 3 {
  533. return ErrTooManyRedirects
  534. }
  535. return nil
  536. },
  537. client: &multiStaticHTTPClient{
  538. responses: []staticHTTPResponse{
  539. {
  540. resp: http.Response{
  541. StatusCode: http.StatusTemporaryRedirect,
  542. Header: http.Header{"Location": []string{"http://example.com"}},
  543. },
  544. },
  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.StatusTeapot,
  554. },
  555. },
  556. },
  557. },
  558. wantCode: http.StatusTeapot,
  559. },
  560. // fail if too many redirects
  561. {
  562. checkRedirect: func(via int) error {
  563. if via >= 2 {
  564. return ErrTooManyRedirects
  565. }
  566. return nil
  567. },
  568. client: &multiStaticHTTPClient{
  569. responses: []staticHTTPResponse{
  570. {
  571. resp: http.Response{
  572. StatusCode: http.StatusTemporaryRedirect,
  573. Header: http.Header{"Location": []string{"http://example.com"}},
  574. },
  575. },
  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.StatusTeapot,
  585. },
  586. },
  587. },
  588. },
  589. wantErr: ErrTooManyRedirects,
  590. },
  591. // fail if Location header not set
  592. {
  593. checkRedirect: func(int) error { return ErrTooManyRedirects },
  594. client: &multiStaticHTTPClient{
  595. responses: []staticHTTPResponse{
  596. {
  597. resp: http.Response{
  598. StatusCode: http.StatusTemporaryRedirect,
  599. },
  600. },
  601. },
  602. },
  603. wantErr: errors.New("Location header not set"),
  604. },
  605. // fail if Location header is invalid
  606. {
  607. checkRedirect: func(int) error { return ErrTooManyRedirects },
  608. client: &multiStaticHTTPClient{
  609. responses: []staticHTTPResponse{
  610. {
  611. resp: http.Response{
  612. StatusCode: http.StatusTemporaryRedirect,
  613. Header: http.Header{"Location": []string{":"}},
  614. },
  615. },
  616. },
  617. },
  618. wantErr: errors.New("Location header not valid URL: :"),
  619. },
  620. // fail if redirects checked way too many times
  621. {
  622. checkRedirect: func(int) error { return nil },
  623. client: &staticHTTPClient{
  624. resp: http.Response{
  625. StatusCode: http.StatusTemporaryRedirect,
  626. Header: http.Header{"Location": []string{"http://example.com"}},
  627. },
  628. },
  629. wantErr: errTooManyRedirectChecks,
  630. },
  631. }
  632. for i, tt := range tests {
  633. client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect}
  634. resp, _, err := client.Do(context.Background(), nil)
  635. if !reflect.DeepEqual(tt.wantErr, err) {
  636. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  637. continue
  638. }
  639. if resp == nil {
  640. if tt.wantCode != 0 {
  641. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  642. }
  643. continue
  644. }
  645. if resp.StatusCode != tt.wantCode {
  646. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  647. continue
  648. }
  649. }
  650. }
  651. func TestDefaultCheckRedirect(t *testing.T) {
  652. tests := []struct {
  653. num int
  654. err error
  655. }{
  656. {0, nil},
  657. {5, nil},
  658. {10, nil},
  659. {11, ErrTooManyRedirects},
  660. {29, ErrTooManyRedirects},
  661. }
  662. for i, tt := range tests {
  663. err := DefaultCheckRedirect(tt.num)
  664. if !reflect.DeepEqual(tt.err, err) {
  665. t.Errorf("#%d: want=%#v got=%#v", i, tt.err, err)
  666. }
  667. }
  668. }
  669. func TestHTTPClusterClientSync(t *testing.T) {
  670. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  671. {
  672. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  673. 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"]}]}`),
  674. },
  675. })
  676. hc := &httpClusterClient{
  677. clientFactory: cf,
  678. rand: rand.New(rand.NewSource(0)),
  679. }
  680. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  681. if err != nil {
  682. t.Fatalf("unexpected error during setup: %#v", err)
  683. }
  684. want := []string{"http://127.0.0.1:2379"}
  685. got := hc.Endpoints()
  686. if !reflect.DeepEqual(want, got) {
  687. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  688. }
  689. err = hc.Sync(context.Background())
  690. if err != nil {
  691. t.Fatalf("unexpected error during Sync: %#v", err)
  692. }
  693. 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"}
  694. got = hc.Endpoints()
  695. sort.Sort(sort.StringSlice(got))
  696. if !reflect.DeepEqual(want, got) {
  697. t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got)
  698. }
  699. err = hc.SetEndpoints([]string{"http://127.0.0.1:4009"})
  700. if err != nil {
  701. t.Fatalf("unexpected error during reset: %#v", err)
  702. }
  703. want = []string{"http://127.0.0.1:4009"}
  704. got = hc.Endpoints()
  705. if !reflect.DeepEqual(want, got) {
  706. t.Fatalf("incorrect endpoints post-reset: want=%#v got=%#v", want, got)
  707. }
  708. }
  709. func TestHTTPClusterClientSyncFail(t *testing.T) {
  710. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  711. {err: errors.New("fail!")},
  712. })
  713. hc := &httpClusterClient{
  714. clientFactory: cf,
  715. rand: rand.New(rand.NewSource(0)),
  716. }
  717. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  718. if err != nil {
  719. t.Fatalf("unexpected error during setup: %#v", err)
  720. }
  721. want := []string{"http://127.0.0.1:2379"}
  722. got := hc.Endpoints()
  723. if !reflect.DeepEqual(want, got) {
  724. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  725. }
  726. err = hc.Sync(context.Background())
  727. if err == nil {
  728. t.Fatalf("got nil error during Sync")
  729. }
  730. got = hc.Endpoints()
  731. if !reflect.DeepEqual(want, got) {
  732. t.Fatalf("incorrect endpoints after failed Sync: want=%#v got=%#v", want, got)
  733. }
  734. }
  735. func TestHTTPClusterClientAutoSyncCancelContext(t *testing.T) {
  736. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  737. {
  738. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  739. 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"]}]}`),
  740. },
  741. })
  742. hc := &httpClusterClient{
  743. clientFactory: cf,
  744. rand: rand.New(rand.NewSource(0)),
  745. }
  746. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  747. if err != nil {
  748. t.Fatalf("unexpected error during setup: %#v", err)
  749. }
  750. ctx, cancel := context.WithCancel(context.Background())
  751. cancel()
  752. err = hc.AutoSync(ctx, time.Hour)
  753. if err != context.Canceled {
  754. t.Fatalf("incorrect error value: want=%v got=%v", context.Canceled, err)
  755. }
  756. }
  757. func TestHTTPClusterClientAutoSyncFail(t *testing.T) {
  758. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  759. {err: errors.New("fail!")},
  760. })
  761. hc := &httpClusterClient{
  762. clientFactory: cf,
  763. rand: rand.New(rand.NewSource(0)),
  764. }
  765. err := hc.SetEndpoints([]string{"http://127.0.0.1:2379"})
  766. if err != nil {
  767. t.Fatalf("unexpected error during setup: %#v", err)
  768. }
  769. err = hc.AutoSync(context.Background(), time.Hour)
  770. if !strings.HasPrefix(err.Error(), ErrClusterUnavailable.Error()) {
  771. t.Fatalf("incorrect error value: want=%v got=%v", ErrClusterUnavailable, err)
  772. }
  773. }
  774. func TestHTTPClusterClientGetVersion(t *testing.T) {
  775. body := []byte(`{"etcdserver":"2.3.2","etcdcluster":"2.3.0"}`)
  776. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  777. {
  778. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Length": []string{"44"}}},
  779. body: body,
  780. },
  781. })
  782. hc := &httpClusterClient{
  783. clientFactory: cf,
  784. rand: rand.New(rand.NewSource(0)),
  785. }
  786. 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"})
  787. if err != nil {
  788. t.Fatalf("unexpected error during setup: %#v", err)
  789. }
  790. actual, err := hc.GetVersion(context.Background())
  791. if err != nil {
  792. t.Errorf("non-nil error: %#v", err)
  793. }
  794. expected := version.Versions{Server: "2.3.2", Cluster: "2.3.0"}
  795. if !reflect.DeepEqual(&expected, actual) {
  796. t.Errorf("incorrect Response: want=%#v got=%#v", expected, actual)
  797. }
  798. }
  799. // TestHTTPClusterClientSyncPinEndpoint tests that Sync() pins the endpoint when
  800. // it gets the exactly same member list as before.
  801. func TestHTTPClusterClientSyncPinEndpoint(t *testing.T) {
  802. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  803. {
  804. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  805. 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"]}]}`),
  806. },
  807. {
  808. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  809. 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"]}]}`),
  810. },
  811. {
  812. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  813. 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"]}]}`),
  814. },
  815. })
  816. hc := &httpClusterClient{
  817. clientFactory: cf,
  818. rand: rand.New(rand.NewSource(0)),
  819. }
  820. 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"})
  821. if err != nil {
  822. t.Fatalf("unexpected error during setup: %#v", err)
  823. }
  824. pinnedEndpoint := hc.endpoints[hc.pinned]
  825. for i := 0; i < 3; i++ {
  826. err = hc.Sync(context.Background())
  827. if err != nil {
  828. t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
  829. }
  830. if g := hc.endpoints[hc.pinned]; g != pinnedEndpoint {
  831. t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, pinnedEndpoint)
  832. }
  833. }
  834. }
  835. // TestHTTPClusterClientSyncUnpinEndpoint tests that Sync() unpins the endpoint when
  836. // it gets a different member list than before.
  837. func TestHTTPClusterClientSyncUnpinEndpoint(t *testing.T) {
  838. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  839. {
  840. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  841. 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"]}]}`),
  842. },
  843. {
  844. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  845. 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"]}]}`),
  846. },
  847. {
  848. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  849. 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"]}]}`),
  850. },
  851. })
  852. hc := &httpClusterClient{
  853. clientFactory: cf,
  854. rand: rand.New(rand.NewSource(0)),
  855. }
  856. 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"})
  857. if err != nil {
  858. t.Fatalf("unexpected error during setup: %#v", err)
  859. }
  860. wants := []string{"http://127.0.0.1:2379", "http://127.0.0.1:4001", "http://127.0.0.1:4002"}
  861. for i := 0; i < 3; i++ {
  862. err = hc.Sync(context.Background())
  863. if err != nil {
  864. t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
  865. }
  866. if g := hc.endpoints[hc.pinned]; g.String() != wants[i] {
  867. t.Errorf("#%d: pinned endpoint = %v, want %v", i, g, wants[i])
  868. }
  869. }
  870. }
  871. // TestHTTPClusterClientSyncPinLeaderEndpoint tests that Sync() pins the leader
  872. // when the selection mode is EndpointSelectionPrioritizeLeader
  873. func TestHTTPClusterClientSyncPinLeaderEndpoint(t *testing.T) {
  874. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  875. {
  876. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  877. 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"]}]}`),
  878. },
  879. {
  880. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  881. body: []byte(`{"id":"2745e2525fce8fe","peerURLs":["http://127.0.0.1:7003"],"name":"node3","clientURLs":["http://127.0.0.1:4003"]}`),
  882. },
  883. {
  884. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  885. 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"]}]}`),
  886. },
  887. {
  888. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  889. body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"],"name":"node2","clientURLs":["http://127.0.0.1:4002"]}`),
  890. },
  891. })
  892. hc := &httpClusterClient{
  893. clientFactory: cf,
  894. rand: rand.New(rand.NewSource(0)),
  895. selectionMode: EndpointSelectionPrioritizeLeader,
  896. endpoints: []url.URL{{}}, // Need somewhere to pretend to send to initially
  897. }
  898. wants := []string{"http://127.0.0.1:4003", "http://127.0.0.1:4002"}
  899. for i, want := range wants {
  900. err := hc.Sync(context.Background())
  901. if err != nil {
  902. t.Fatalf("#%d: unexpected error during Sync: %#v", i, err)
  903. }
  904. pinned := hc.endpoints[hc.pinned].String()
  905. if pinned != want {
  906. t.Errorf("#%d: pinned endpoint = %v, want %v", i, pinned, want)
  907. }
  908. }
  909. }
  910. func TestHTTPClusterClientResetFail(t *testing.T) {
  911. tests := [][]string{
  912. // need at least one endpoint
  913. {},
  914. // urls must be valid
  915. {":"},
  916. }
  917. for i, tt := range tests {
  918. hc := &httpClusterClient{rand: rand.New(rand.NewSource(0))}
  919. err := hc.SetEndpoints(tt)
  920. if err == nil {
  921. t.Errorf("#%d: expected non-nil error", i)
  922. }
  923. }
  924. }
  925. func TestHTTPClusterClientResetPinRandom(t *testing.T) {
  926. round := 2000
  927. pinNum := 0
  928. for i := 0; i < round; i++ {
  929. hc := &httpClusterClient{rand: rand.New(rand.NewSource(int64(i)))}
  930. err := hc.SetEndpoints([]string{"http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"})
  931. if err != nil {
  932. t.Fatalf("#%d: reset error (%v)", i, err)
  933. }
  934. if hc.endpoints[hc.pinned].String() == "http://127.0.0.1:4001" {
  935. pinNum++
  936. }
  937. }
  938. min := 1.0/3.0 - 0.05
  939. max := 1.0/3.0 + 0.05
  940. if ratio := float64(pinNum) / float64(round); ratio > max || ratio < min {
  941. t.Errorf("pinned ratio = %v, want [%v, %v]", ratio, min, max)
  942. }
  943. }