client_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. // Copyright 2015 CoreOS, Inc.
  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/ioutil"
  18. "net/http"
  19. "net/url"
  20. "reflect"
  21. "strings"
  22. "testing"
  23. "time"
  24. "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
  25. )
  26. type actionAssertingHTTPClient struct {
  27. t *testing.T
  28. num int
  29. act httpAction
  30. resp http.Response
  31. body []byte
  32. err error
  33. }
  34. func (a *actionAssertingHTTPClient) Do(_ context.Context, act httpAction) (*http.Response, []byte, error) {
  35. if !reflect.DeepEqual(a.act, act) {
  36. a.t.Errorf("#%d: unexpected httpAction: want=%#v got=%#v", a.num, a.act, act)
  37. }
  38. return &a.resp, a.body, a.err
  39. }
  40. type staticHTTPClient struct {
  41. resp http.Response
  42. body []byte
  43. err error
  44. }
  45. func (s *staticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  46. return &s.resp, s.body, s.err
  47. }
  48. type staticHTTPAction struct {
  49. request http.Request
  50. }
  51. func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
  52. return &s.request
  53. }
  54. type staticHTTPResponse struct {
  55. resp http.Response
  56. body []byte
  57. err error
  58. }
  59. type multiStaticHTTPClient struct {
  60. responses []staticHTTPResponse
  61. cur int
  62. }
  63. func (s *multiStaticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  64. r := s.responses[s.cur]
  65. s.cur++
  66. return &r.resp, r.body, r.err
  67. }
  68. func newStaticHTTPClientFactory(responses []staticHTTPResponse) httpClientFactory {
  69. var cur int
  70. return func(url.URL) httpClient {
  71. r := responses[cur]
  72. cur++
  73. return &staticHTTPClient{resp: r.resp, body: r.body, err: r.err}
  74. }
  75. }
  76. type fakeTransport struct {
  77. respchan chan *http.Response
  78. errchan chan error
  79. startCancel chan struct{}
  80. finishCancel chan struct{}
  81. }
  82. func newFakeTransport() *fakeTransport {
  83. return &fakeTransport{
  84. respchan: make(chan *http.Response, 1),
  85. errchan: make(chan error, 1),
  86. startCancel: make(chan struct{}, 1),
  87. finishCancel: make(chan struct{}, 1),
  88. }
  89. }
  90. func (t *fakeTransport) RoundTrip(*http.Request) (*http.Response, error) {
  91. select {
  92. case resp := <-t.respchan:
  93. return resp, nil
  94. case err := <-t.errchan:
  95. return nil, err
  96. case <-t.startCancel:
  97. // wait on finishCancel to simulate taking some amount of
  98. // time while calling CancelRequest
  99. <-t.finishCancel
  100. return nil, errors.New("cancelled")
  101. }
  102. }
  103. func (t *fakeTransport) CancelRequest(*http.Request) {
  104. t.startCancel <- struct{}{}
  105. }
  106. type fakeAction struct{}
  107. func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
  108. return &http.Request{}
  109. }
  110. func TestSimpleHTTPClientDoSuccess(t *testing.T) {
  111. tr := newFakeTransport()
  112. c := &simpleHTTPClient{transport: tr}
  113. tr.respchan <- &http.Response{
  114. StatusCode: http.StatusTeapot,
  115. Body: ioutil.NopCloser(strings.NewReader("foo")),
  116. }
  117. resp, body, err := c.Do(context.Background(), &fakeAction{})
  118. if err != nil {
  119. t.Fatalf("incorrect error value: want=nil got=%v", err)
  120. }
  121. wantCode := http.StatusTeapot
  122. if wantCode != resp.StatusCode {
  123. t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
  124. }
  125. wantBody := []byte("foo")
  126. if !reflect.DeepEqual(wantBody, body) {
  127. t.Fatalf("invalid response body: want=%q got=%q", wantBody, body)
  128. }
  129. }
  130. func TestSimpleHTTPClientDoError(t *testing.T) {
  131. tr := newFakeTransport()
  132. c := &simpleHTTPClient{transport: tr}
  133. tr.errchan <- errors.New("fixture")
  134. _, _, err := c.Do(context.Background(), &fakeAction{})
  135. if err == nil {
  136. t.Fatalf("expected non-nil error, got nil")
  137. }
  138. }
  139. func TestSimpleHTTPClientDoCancelContext(t *testing.T) {
  140. tr := newFakeTransport()
  141. c := &simpleHTTPClient{transport: tr}
  142. tr.startCancel <- struct{}{}
  143. tr.finishCancel <- struct{}{}
  144. _, _, err := c.Do(context.Background(), &fakeAction{})
  145. if err == nil {
  146. t.Fatalf("expected non-nil error, got nil")
  147. }
  148. }
  149. func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
  150. tr := newFakeTransport()
  151. c := &simpleHTTPClient{transport: tr}
  152. donechan := make(chan struct{})
  153. ctx, cancel := context.WithCancel(context.Background())
  154. go func() {
  155. c.Do(ctx, &fakeAction{})
  156. close(donechan)
  157. }()
  158. // This should call CancelRequest and begin the cancellation process
  159. cancel()
  160. select {
  161. case <-donechan:
  162. t.Fatalf("simpleHTTPClient.Do should not have exited yet")
  163. default:
  164. }
  165. tr.finishCancel <- struct{}{}
  166. select {
  167. case <-donechan:
  168. //expected behavior
  169. return
  170. case <-time.After(time.Second):
  171. t.Fatalf("simpleHTTPClient.Do did not exit within 1s")
  172. }
  173. }
  174. func TestHTTPClusterClientDo(t *testing.T) {
  175. fakeErr := errors.New("fake!")
  176. fakeURL := url.URL{}
  177. tests := []struct {
  178. client *httpClusterClient
  179. wantCode int
  180. wantErr error
  181. }{
  182. // first good response short-circuits Do
  183. {
  184. client: &httpClusterClient{
  185. endpoints: []url.URL{fakeURL, fakeURL},
  186. clientFactory: newStaticHTTPClientFactory(
  187. []staticHTTPResponse{
  188. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  189. staticHTTPResponse{err: fakeErr},
  190. },
  191. ),
  192. },
  193. wantCode: http.StatusTeapot,
  194. },
  195. // fall through to good endpoint if err is arbitrary
  196. {
  197. client: &httpClusterClient{
  198. endpoints: []url.URL{fakeURL, fakeURL},
  199. clientFactory: newStaticHTTPClientFactory(
  200. []staticHTTPResponse{
  201. staticHTTPResponse{err: fakeErr},
  202. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  203. },
  204. ),
  205. },
  206. wantCode: http.StatusTeapot,
  207. },
  208. // context.DeadlineExceeded short-circuits Do
  209. {
  210. client: &httpClusterClient{
  211. endpoints: []url.URL{fakeURL, fakeURL},
  212. clientFactory: newStaticHTTPClientFactory(
  213. []staticHTTPResponse{
  214. staticHTTPResponse{err: context.DeadlineExceeded},
  215. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  216. },
  217. ),
  218. },
  219. wantErr: context.DeadlineExceeded,
  220. },
  221. // context.Canceled short-circuits Do
  222. {
  223. client: &httpClusterClient{
  224. endpoints: []url.URL{fakeURL, fakeURL},
  225. clientFactory: newStaticHTTPClientFactory(
  226. []staticHTTPResponse{
  227. staticHTTPResponse{err: context.Canceled},
  228. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  229. },
  230. ),
  231. },
  232. wantErr: context.Canceled,
  233. },
  234. // return err if there are no endpoints
  235. {
  236. client: &httpClusterClient{
  237. endpoints: []url.URL{},
  238. clientFactory: newHTTPClientFactory(nil, nil),
  239. },
  240. wantErr: ErrNoEndpoints,
  241. },
  242. // return err if all endpoints return arbitrary errors
  243. {
  244. client: &httpClusterClient{
  245. endpoints: []url.URL{fakeURL, fakeURL},
  246. clientFactory: newStaticHTTPClientFactory(
  247. []staticHTTPResponse{
  248. staticHTTPResponse{err: fakeErr},
  249. staticHTTPResponse{err: fakeErr},
  250. },
  251. ),
  252. },
  253. wantErr: fakeErr,
  254. },
  255. // 500-level errors cause Do to fallthrough to next endpoint
  256. {
  257. client: &httpClusterClient{
  258. endpoints: []url.URL{fakeURL, fakeURL},
  259. clientFactory: newStaticHTTPClientFactory(
  260. []staticHTTPResponse{
  261. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusBadGateway}},
  262. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  263. },
  264. ),
  265. },
  266. wantCode: http.StatusTeapot,
  267. },
  268. }
  269. for i, tt := range tests {
  270. resp, _, err := tt.client.Do(context.Background(), nil)
  271. if !reflect.DeepEqual(tt.wantErr, err) {
  272. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  273. continue
  274. }
  275. if resp == nil {
  276. if tt.wantCode != 0 {
  277. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  278. }
  279. continue
  280. }
  281. if resp.StatusCode != tt.wantCode {
  282. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  283. continue
  284. }
  285. }
  286. }
  287. func TestRedirectedHTTPAction(t *testing.T) {
  288. act := &redirectedHTTPAction{
  289. action: &staticHTTPAction{
  290. request: http.Request{
  291. Method: "DELETE",
  292. URL: &url.URL{
  293. Scheme: "https",
  294. Host: "foo.example.com",
  295. Path: "/ping",
  296. },
  297. },
  298. },
  299. location: url.URL{
  300. Scheme: "https",
  301. Host: "bar.example.com",
  302. Path: "/pong",
  303. },
  304. }
  305. want := &http.Request{
  306. Method: "DELETE",
  307. URL: &url.URL{
  308. Scheme: "https",
  309. Host: "bar.example.com",
  310. Path: "/pong",
  311. },
  312. }
  313. got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
  314. if !reflect.DeepEqual(want, got) {
  315. t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
  316. }
  317. }
  318. func TestRedirectFollowingHTTPClient(t *testing.T) {
  319. tests := []struct {
  320. checkRedirect CheckRedirectFunc
  321. client httpClient
  322. wantCode int
  323. wantErr error
  324. }{
  325. // errors bubbled up
  326. {
  327. checkRedirect: func(int) error { return ErrTooManyRedirects },
  328. client: &multiStaticHTTPClient{
  329. responses: []staticHTTPResponse{
  330. staticHTTPResponse{
  331. err: errors.New("fail!"),
  332. },
  333. },
  334. },
  335. wantErr: errors.New("fail!"),
  336. },
  337. // no need to follow redirect if none given
  338. {
  339. checkRedirect: func(int) error { return ErrTooManyRedirects },
  340. client: &multiStaticHTTPClient{
  341. responses: []staticHTTPResponse{
  342. staticHTTPResponse{
  343. resp: http.Response{
  344. StatusCode: http.StatusTeapot,
  345. },
  346. },
  347. },
  348. },
  349. wantCode: http.StatusTeapot,
  350. },
  351. // redirects if less than max
  352. {
  353. checkRedirect: func(via int) error {
  354. if via >= 2 {
  355. return ErrTooManyRedirects
  356. }
  357. return nil
  358. },
  359. client: &multiStaticHTTPClient{
  360. responses: []staticHTTPResponse{
  361. staticHTTPResponse{
  362. resp: http.Response{
  363. StatusCode: http.StatusTemporaryRedirect,
  364. Header: http.Header{"Location": []string{"http://example.com"}},
  365. },
  366. },
  367. staticHTTPResponse{
  368. resp: http.Response{
  369. StatusCode: http.StatusTeapot,
  370. },
  371. },
  372. },
  373. },
  374. wantCode: http.StatusTeapot,
  375. },
  376. // succeed after reaching max redirects
  377. {
  378. checkRedirect: func(via int) error {
  379. if via >= 3 {
  380. return ErrTooManyRedirects
  381. }
  382. return nil
  383. },
  384. client: &multiStaticHTTPClient{
  385. responses: []staticHTTPResponse{
  386. staticHTTPResponse{
  387. resp: http.Response{
  388. StatusCode: http.StatusTemporaryRedirect,
  389. Header: http.Header{"Location": []string{"http://example.com"}},
  390. },
  391. },
  392. staticHTTPResponse{
  393. resp: http.Response{
  394. StatusCode: http.StatusTemporaryRedirect,
  395. Header: http.Header{"Location": []string{"http://example.com"}},
  396. },
  397. },
  398. staticHTTPResponse{
  399. resp: http.Response{
  400. StatusCode: http.StatusTeapot,
  401. },
  402. },
  403. },
  404. },
  405. wantCode: http.StatusTeapot,
  406. },
  407. // fail if too many redirects
  408. {
  409. checkRedirect: func(via int) error {
  410. if via >= 2 {
  411. return ErrTooManyRedirects
  412. }
  413. return nil
  414. },
  415. client: &multiStaticHTTPClient{
  416. responses: []staticHTTPResponse{
  417. staticHTTPResponse{
  418. resp: http.Response{
  419. StatusCode: http.StatusTemporaryRedirect,
  420. Header: http.Header{"Location": []string{"http://example.com"}},
  421. },
  422. },
  423. staticHTTPResponse{
  424. resp: http.Response{
  425. StatusCode: http.StatusTemporaryRedirect,
  426. Header: http.Header{"Location": []string{"http://example.com"}},
  427. },
  428. },
  429. staticHTTPResponse{
  430. resp: http.Response{
  431. StatusCode: http.StatusTeapot,
  432. },
  433. },
  434. },
  435. },
  436. wantErr: ErrTooManyRedirects,
  437. },
  438. // fail if Location header not set
  439. {
  440. checkRedirect: func(int) error { return ErrTooManyRedirects },
  441. client: &multiStaticHTTPClient{
  442. responses: []staticHTTPResponse{
  443. staticHTTPResponse{
  444. resp: http.Response{
  445. StatusCode: http.StatusTemporaryRedirect,
  446. },
  447. },
  448. },
  449. },
  450. wantErr: errors.New("Location header not set"),
  451. },
  452. // fail if Location header is invalid
  453. {
  454. checkRedirect: func(int) error { return ErrTooManyRedirects },
  455. client: &multiStaticHTTPClient{
  456. responses: []staticHTTPResponse{
  457. staticHTTPResponse{
  458. resp: http.Response{
  459. StatusCode: http.StatusTemporaryRedirect,
  460. Header: http.Header{"Location": []string{":"}},
  461. },
  462. },
  463. },
  464. },
  465. wantErr: errors.New("Location header not valid URL: :"),
  466. },
  467. }
  468. for i, tt := range tests {
  469. client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect}
  470. resp, _, err := client.Do(context.Background(), nil)
  471. if !reflect.DeepEqual(tt.wantErr, err) {
  472. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  473. continue
  474. }
  475. if resp == nil {
  476. if tt.wantCode != 0 {
  477. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  478. }
  479. continue
  480. }
  481. if resp.StatusCode != tt.wantCode {
  482. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  483. continue
  484. }
  485. }
  486. }
  487. func TestDefaultCheckRedirect(t *testing.T) {
  488. tests := []struct {
  489. num int
  490. err error
  491. }{
  492. {0, nil},
  493. {5, nil},
  494. {10, nil},
  495. {11, ErrTooManyRedirects},
  496. {29, ErrTooManyRedirects},
  497. }
  498. for i, tt := range tests {
  499. err := DefaultCheckRedirect(tt.num)
  500. if !reflect.DeepEqual(tt.err, err) {
  501. t.Errorf("#%d: want=%#v got=%#v", i, tt.err, err)
  502. }
  503. }
  504. }
  505. func TestHTTPClusterClientSync(t *testing.T) {
  506. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  507. staticHTTPResponse{
  508. resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}},
  509. 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"]}]}`),
  510. },
  511. })
  512. hc := &httpClusterClient{clientFactory: cf}
  513. err := hc.reset([]string{"http://127.0.0.1:4001"})
  514. if err != nil {
  515. t.Fatalf("unexpected error during setup: %#v", err)
  516. }
  517. want := []string{"http://127.0.0.1:4001"}
  518. got := hc.Endpoints()
  519. if !reflect.DeepEqual(want, got) {
  520. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  521. }
  522. err = hc.Sync(context.Background())
  523. if err != nil {
  524. t.Fatalf("unexpected error during Sync: %#v", err)
  525. }
  526. want = []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"}
  527. got = hc.Endpoints()
  528. if !reflect.DeepEqual(want, got) {
  529. t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got)
  530. }
  531. err = hc.reset([]string{"http://127.0.0.1:4009"})
  532. if err != nil {
  533. t.Fatalf("unexpected error during reset: %#v", err)
  534. }
  535. want = []string{"http://127.0.0.1:4009"}
  536. got = hc.Endpoints()
  537. if !reflect.DeepEqual(want, got) {
  538. t.Fatalf("incorrect endpoints post-reset: want=%#v got=%#v", want, got)
  539. }
  540. }
  541. func TestHTTPClusterClientSyncFail(t *testing.T) {
  542. cf := newStaticHTTPClientFactory([]staticHTTPResponse{
  543. staticHTTPResponse{err: errors.New("fail!")},
  544. })
  545. hc := &httpClusterClient{clientFactory: cf}
  546. err := hc.reset([]string{"http://127.0.0.1:4001"})
  547. if err != nil {
  548. t.Fatalf("unexpected error during setup: %#v", err)
  549. }
  550. want := []string{"http://127.0.0.1:4001"}
  551. got := hc.Endpoints()
  552. if !reflect.DeepEqual(want, got) {
  553. t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got)
  554. }
  555. err = hc.Sync(context.Background())
  556. if err == nil {
  557. t.Fatalf("got nil error during Sync")
  558. }
  559. got = hc.Endpoints()
  560. if !reflect.DeepEqual(want, got) {
  561. t.Fatalf("incorrect endpoints after failed Sync: want=%#v got=%#v", want, got)
  562. }
  563. }