client_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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 staticHTTPClient struct {
  27. resp http.Response
  28. err error
  29. }
  30. func (s *staticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  31. return &s.resp, nil, s.err
  32. }
  33. type staticHTTPAction struct {
  34. request http.Request
  35. }
  36. type staticHTTPResponse struct {
  37. resp http.Response
  38. err error
  39. }
  40. func (s *staticHTTPAction) HTTPRequest(url.URL) *http.Request {
  41. return &s.request
  42. }
  43. type multiStaticHTTPClient struct {
  44. responses []staticHTTPResponse
  45. cur int
  46. }
  47. func (s *multiStaticHTTPClient) Do(context.Context, httpAction) (*http.Response, []byte, error) {
  48. r := s.responses[s.cur]
  49. s.cur++
  50. return &r.resp, nil, r.err
  51. }
  52. func newStaticHTTPClientFactory(responses []staticHTTPResponse) httpClientFactory {
  53. var cur int
  54. return func(url.URL) httpClient {
  55. r := responses[cur]
  56. cur++
  57. return &staticHTTPClient{resp: r.resp, err: r.err}
  58. }
  59. }
  60. type fakeTransport struct {
  61. respchan chan *http.Response
  62. errchan chan error
  63. startCancel chan struct{}
  64. finishCancel chan struct{}
  65. }
  66. func newFakeTransport() *fakeTransport {
  67. return &fakeTransport{
  68. respchan: make(chan *http.Response, 1),
  69. errchan: make(chan error, 1),
  70. startCancel: make(chan struct{}, 1),
  71. finishCancel: make(chan struct{}, 1),
  72. }
  73. }
  74. func (t *fakeTransport) RoundTrip(*http.Request) (*http.Response, error) {
  75. select {
  76. case resp := <-t.respchan:
  77. return resp, nil
  78. case err := <-t.errchan:
  79. return nil, err
  80. case <-t.startCancel:
  81. // wait on finishCancel to simulate taking some amount of
  82. // time while calling CancelRequest
  83. <-t.finishCancel
  84. return nil, errors.New("cancelled")
  85. }
  86. }
  87. func (t *fakeTransport) CancelRequest(*http.Request) {
  88. t.startCancel <- struct{}{}
  89. }
  90. type fakeAction struct{}
  91. func (a *fakeAction) HTTPRequest(url.URL) *http.Request {
  92. return &http.Request{}
  93. }
  94. func TestSimpleHTTPClientDoSuccess(t *testing.T) {
  95. tr := newFakeTransport()
  96. c := &simpleHTTPClient{transport: tr}
  97. tr.respchan <- &http.Response{
  98. StatusCode: http.StatusTeapot,
  99. Body: ioutil.NopCloser(strings.NewReader("foo")),
  100. }
  101. resp, body, err := c.Do(context.Background(), &fakeAction{})
  102. if err != nil {
  103. t.Fatalf("incorrect error value: want=nil got=%v", err)
  104. }
  105. wantCode := http.StatusTeapot
  106. if wantCode != resp.StatusCode {
  107. t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode)
  108. }
  109. wantBody := []byte("foo")
  110. if !reflect.DeepEqual(wantBody, body) {
  111. t.Fatalf("invalid response body: want=%q got=%q", wantBody, body)
  112. }
  113. }
  114. func TestSimpleHTTPClientDoError(t *testing.T) {
  115. tr := newFakeTransport()
  116. c := &simpleHTTPClient{transport: tr}
  117. tr.errchan <- errors.New("fixture")
  118. _, _, err := c.Do(context.Background(), &fakeAction{})
  119. if err == nil {
  120. t.Fatalf("expected non-nil error, got nil")
  121. }
  122. }
  123. func TestSimpleHTTPClientDoCancelContext(t *testing.T) {
  124. tr := newFakeTransport()
  125. c := &simpleHTTPClient{transport: tr}
  126. tr.startCancel <- struct{}{}
  127. tr.finishCancel <- struct{}{}
  128. _, _, err := c.Do(context.Background(), &fakeAction{})
  129. if err == nil {
  130. t.Fatalf("expected non-nil error, got nil")
  131. }
  132. }
  133. func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) {
  134. tr := newFakeTransport()
  135. c := &simpleHTTPClient{transport: tr}
  136. donechan := make(chan struct{})
  137. ctx, cancel := context.WithCancel(context.Background())
  138. go func() {
  139. c.Do(ctx, &fakeAction{})
  140. close(donechan)
  141. }()
  142. // This should call CancelRequest and begin the cancellation process
  143. cancel()
  144. select {
  145. case <-donechan:
  146. t.Fatalf("simpleHTTPClient.Do should not have exited yet")
  147. default:
  148. }
  149. tr.finishCancel <- struct{}{}
  150. select {
  151. case <-donechan:
  152. //expected behavior
  153. return
  154. case <-time.After(time.Second):
  155. t.Fatalf("simpleHTTPClient.Do did not exit within 1s")
  156. }
  157. }
  158. func TestHTTPClusterClientDo(t *testing.T) {
  159. fakeErr := errors.New("fake!")
  160. fakeURL := url.URL{}
  161. tests := []struct {
  162. client *httpClusterClient
  163. wantCode int
  164. wantErr error
  165. }{
  166. // first good response short-circuits Do
  167. {
  168. client: &httpClusterClient{
  169. endpoints: []url.URL{fakeURL, fakeURL},
  170. clientFactory: newStaticHTTPClientFactory(
  171. []staticHTTPResponse{
  172. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  173. staticHTTPResponse{err: fakeErr},
  174. },
  175. ),
  176. },
  177. wantCode: http.StatusTeapot,
  178. },
  179. // fall through to good endpoint if err is arbitrary
  180. {
  181. client: &httpClusterClient{
  182. endpoints: []url.URL{fakeURL, fakeURL},
  183. clientFactory: newStaticHTTPClientFactory(
  184. []staticHTTPResponse{
  185. staticHTTPResponse{err: fakeErr},
  186. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  187. },
  188. ),
  189. },
  190. wantCode: http.StatusTeapot,
  191. },
  192. // ErrTimeout short-circuits Do
  193. {
  194. client: &httpClusterClient{
  195. endpoints: []url.URL{fakeURL, fakeURL},
  196. clientFactory: newStaticHTTPClientFactory(
  197. []staticHTTPResponse{
  198. staticHTTPResponse{err: ErrTimeout},
  199. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  200. },
  201. ),
  202. },
  203. wantErr: ErrTimeout,
  204. },
  205. // ErrCanceled short-circuits Do
  206. {
  207. client: &httpClusterClient{
  208. endpoints: []url.URL{fakeURL, fakeURL},
  209. clientFactory: newStaticHTTPClientFactory(
  210. []staticHTTPResponse{
  211. staticHTTPResponse{err: ErrCanceled},
  212. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  213. },
  214. ),
  215. },
  216. wantErr: ErrCanceled,
  217. },
  218. // return err if there are no endpoints
  219. {
  220. client: &httpClusterClient{
  221. endpoints: []url.URL{},
  222. clientFactory: newHTTPClientFactory(nil),
  223. },
  224. wantErr: ErrNoEndpoints,
  225. },
  226. // return err if all endpoints return arbitrary errors
  227. {
  228. client: &httpClusterClient{
  229. endpoints: []url.URL{fakeURL, fakeURL},
  230. clientFactory: newStaticHTTPClientFactory(
  231. []staticHTTPResponse{
  232. staticHTTPResponse{err: fakeErr},
  233. staticHTTPResponse{err: fakeErr},
  234. },
  235. ),
  236. },
  237. wantErr: fakeErr,
  238. },
  239. // 500-level errors cause Do to fallthrough to next endpoint
  240. {
  241. client: &httpClusterClient{
  242. endpoints: []url.URL{fakeURL, fakeURL},
  243. clientFactory: newStaticHTTPClientFactory(
  244. []staticHTTPResponse{
  245. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusBadGateway}},
  246. staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}},
  247. },
  248. ),
  249. },
  250. wantCode: http.StatusTeapot,
  251. },
  252. }
  253. for i, tt := range tests {
  254. resp, _, err := tt.client.Do(context.Background(), nil)
  255. if !reflect.DeepEqual(tt.wantErr, err) {
  256. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  257. continue
  258. }
  259. if resp == nil {
  260. if tt.wantCode != 0 {
  261. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  262. }
  263. continue
  264. }
  265. if resp.StatusCode != tt.wantCode {
  266. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  267. continue
  268. }
  269. }
  270. }
  271. func TestRedirectedHTTPAction(t *testing.T) {
  272. act := &redirectedHTTPAction{
  273. action: &staticHTTPAction{
  274. request: http.Request{
  275. Method: "DELETE",
  276. URL: &url.URL{
  277. Scheme: "https",
  278. Host: "foo.example.com",
  279. Path: "/ping",
  280. },
  281. },
  282. },
  283. location: url.URL{
  284. Scheme: "https",
  285. Host: "bar.example.com",
  286. Path: "/pong",
  287. },
  288. }
  289. want := &http.Request{
  290. Method: "DELETE",
  291. URL: &url.URL{
  292. Scheme: "https",
  293. Host: "bar.example.com",
  294. Path: "/pong",
  295. },
  296. }
  297. got := act.HTTPRequest(url.URL{Scheme: "http", Host: "baz.example.com", Path: "/pang"})
  298. if !reflect.DeepEqual(want, got) {
  299. t.Fatalf("HTTPRequest is %#v, want %#v", want, got)
  300. }
  301. }
  302. func TestRedirectFollowingHTTPClient(t *testing.T) {
  303. tests := []struct {
  304. max int
  305. client httpClient
  306. wantCode int
  307. wantErr error
  308. }{
  309. // errors bubbled up
  310. {
  311. max: 2,
  312. client: &multiStaticHTTPClient{
  313. responses: []staticHTTPResponse{
  314. staticHTTPResponse{
  315. err: errors.New("fail!"),
  316. },
  317. },
  318. },
  319. wantErr: errors.New("fail!"),
  320. },
  321. // no need to follow redirect if none given
  322. {
  323. max: 2,
  324. client: &multiStaticHTTPClient{
  325. responses: []staticHTTPResponse{
  326. staticHTTPResponse{
  327. resp: http.Response{
  328. StatusCode: http.StatusTeapot,
  329. },
  330. },
  331. },
  332. },
  333. wantCode: http.StatusTeapot,
  334. },
  335. // redirects if less than max
  336. {
  337. max: 2,
  338. client: &multiStaticHTTPClient{
  339. responses: []staticHTTPResponse{
  340. staticHTTPResponse{
  341. resp: http.Response{
  342. StatusCode: http.StatusTemporaryRedirect,
  343. Header: http.Header{"Location": []string{"http://example.com"}},
  344. },
  345. },
  346. staticHTTPResponse{
  347. resp: http.Response{
  348. StatusCode: http.StatusTeapot,
  349. },
  350. },
  351. },
  352. },
  353. wantCode: http.StatusTeapot,
  354. },
  355. // succeed after reaching max redirects
  356. {
  357. max: 2,
  358. client: &multiStaticHTTPClient{
  359. responses: []staticHTTPResponse{
  360. staticHTTPResponse{
  361. resp: http.Response{
  362. StatusCode: http.StatusTemporaryRedirect,
  363. Header: http.Header{"Location": []string{"http://example.com"}},
  364. },
  365. },
  366. staticHTTPResponse{
  367. resp: http.Response{
  368. StatusCode: http.StatusTemporaryRedirect,
  369. Header: http.Header{"Location": []string{"http://example.com"}},
  370. },
  371. },
  372. staticHTTPResponse{
  373. resp: http.Response{
  374. StatusCode: http.StatusTeapot,
  375. },
  376. },
  377. },
  378. },
  379. wantCode: http.StatusTeapot,
  380. },
  381. // fail at max+1 redirects
  382. {
  383. max: 1,
  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. wantErr: ErrTooManyRedirects,
  406. },
  407. // fail if Location header not set
  408. {
  409. max: 1,
  410. client: &multiStaticHTTPClient{
  411. responses: []staticHTTPResponse{
  412. staticHTTPResponse{
  413. resp: http.Response{
  414. StatusCode: http.StatusTemporaryRedirect,
  415. },
  416. },
  417. },
  418. },
  419. wantErr: errors.New("Location header not set"),
  420. },
  421. // fail if Location header is invalid
  422. {
  423. max: 1,
  424. client: &multiStaticHTTPClient{
  425. responses: []staticHTTPResponse{
  426. staticHTTPResponse{
  427. resp: http.Response{
  428. StatusCode: http.StatusTemporaryRedirect,
  429. Header: http.Header{"Location": []string{":"}},
  430. },
  431. },
  432. },
  433. },
  434. wantErr: errors.New("Location header not valid URL: :"),
  435. },
  436. }
  437. for i, tt := range tests {
  438. client := &redirectFollowingHTTPClient{client: tt.client, max: tt.max}
  439. resp, _, err := client.Do(context.Background(), nil)
  440. if !reflect.DeepEqual(tt.wantErr, err) {
  441. t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr)
  442. continue
  443. }
  444. if resp == nil {
  445. if tt.wantCode != 0 {
  446. t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode)
  447. }
  448. continue
  449. }
  450. if resp.StatusCode != tt.wantCode {
  451. t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode)
  452. continue
  453. }
  454. }
  455. }