http_test.go 11 KB

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