client_test.go 25 KB

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