func TestHTTPKeysAPIGetResponse(t *testing.T) { client := &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusOK, Header: http.Header{"X-Etcd-Index": []string{"42"}}, }, body: []byte(`{"action":"get","node":{"key":"/pants/foo/bar","modifiedIndex":25,"createdIndex":19,"nodes":[{"key":"/pants/foo/bar/baz","value":"snarf","createdIndex":21,"modifiedIndex":25}]}}`), } wantResponse := &Response{ Action: "get", Node: &Node{ Key: "/pants/foo/bar", Nodes: []*Node{ &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: 21, ModifiedIndex: 25}, }, CreatedIndex: uint64(19), ModifiedIndex: uint64(25), }, Index: uint64(42), } kAPI := &httpKeysAPI{client: client, prefix: "/pants"} resp, err := kAPI.Get(context.Background(), "/foo/bar", &GetOptions{Recursive: true}) if err != nil { t.Errorf("non-nil error: %#v", err) } if !reflect.DeepEqual(wantResponse, resp) { t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp) } }
func TestHTTPMembersAPIListError(t *testing.T) { tests := []httpClient{ // generic httpClient failure &staticHTTPClient{err: errors.New("fail!")}, // unrecognized HTTP status code &staticHTTPClient{ resp: http.Response{StatusCode: http.StatusTeapot}, }, // fail to unmarshal body on StatusOK &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusOK, }, body: []byte(`[{"id":"XX`), }, } for i, tt := range tests { mAPI := &httpMembersAPI{client: tt} ms, err := mAPI.List(context.Background()) if err == nil { t.Errorf("#%d: got nil err", i) } if ms != nil { t.Errorf("#%d: got non-nil Member slice", i) } } }
func TestSimpleHTTPClientDoCancelContextResponseBodyClosed(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} // create an already-cancelled context ctx, cancel := context.WithCancel(context.Background()) cancel() body := &checkableReadCloser{ReadCloser: ioutil.NopCloser(strings.NewReader("foo"))} go func() { // wait for CancelRequest to be called, informing us that simpleHTTPClient // knows the context is already timed out <-tr.startCancel tr.respchan <- &http.Response{Body: body} tr.finishCancel <- struct{}{} }() _, _, err := c.Do(ctx, &fakeAction{}) if err == nil { t.Fatalf("expected non-nil error, got nil") } if !body.closed { t.Fatalf("expected closed body") } }
func TestHTTPKeysAPIDeleteResponse(t *testing.T) { client := &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusOK, Header: http.Header{"X-Etcd-Index": []string{"22"}}, }, body: []byte(`{"action":"delete","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":22,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`), } wantResponse := &Response{ Action: "delete", Node: &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(22)}, PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)}, Index: uint64(22), } kAPI := &httpKeysAPI{client: client, prefix: "/pants"} resp, err := kAPI.Delete(context.Background(), "/foo/bar/baz", nil) if err != nil { t.Errorf("non-nil error: %#v", err) } if !reflect.DeepEqual(wantResponse, resp) { t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp) } }
func TestHTTPKeysAPIDeleteError(t *testing.T) { tests := []httpClient{ // generic HTTP client failure &staticHTTPClient{ err: errors.New("fail!"), }, // unusable status code &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusTeapot, }, }, // etcd Error response &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusInternalServerError, }, body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`), }, } for i, tt := range tests { kAPI := httpKeysAPI{client: tt} resp, err := kAPI.Delete(context.Background(), "/foo", nil) if err == nil { t.Errorf("#%d: received nil error", i) } if resp != nil { t.Errorf("#%d: received non-nil Response: %#v", i, resp) } } }
func TestHTTPMembersAPIAddSuccess(t *testing.T) { wantAction := &membersAPIActionAdd{ peerURLs: types.URLs([]url.URL{ {Scheme: "http", Host: "127.0.0.1:7002"}, }), } mAPI := &httpMembersAPI{ client: &actionAssertingHTTPClient{ t: t, act: wantAction, resp: http.Response{ StatusCode: http.StatusCreated, }, body: []byte(`{"id":"94088180e21eb87b","peerURLs":["http://127.0.0.1:7002"]}`), }, } wantResponseMember := &Member{ ID: "94088180e21eb87b", PeerURLs: []string{"http://127.0.0.1:7002"}, } m, err := mAPI.Add(context.Background(), "http://127.0.0.1:7002") if err != nil { t.Errorf("got non-nil err: %#v", err) } if !reflect.DeepEqual(wantResponseMember, m) { t.Errorf("incorrect Member: want=%#v got=%#v", wantResponseMember, m) } }
func TestSimpleHTTPClientDoCancelContextWaitForRoundTrip(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} donechan := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) go func() { c.Do(ctx, &fakeAction{}) close(donechan) }() // This should call CancelRequest and begin the cancellation process cancel() select { case <-donechan: t.Fatalf("simpleHTTPClient.Do should not have exited yet") default: } tr.finishCancel <- struct{}{} select { case <-donechan: //expected behavior return case <-time.After(time.Second): t.Fatalf("simpleHTTPClient.Do did not exit within 1s") } }
func TestSimpleHTTPClientDoCancelContextResponseBodyClosed(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} // create an already-cancelled context ctx, cancel := context.WithCancel(context.Background()) cancel() body := &checkableReadCloser{ReadCloser: ioutil.NopCloser(strings.NewReader("foo"))} go func() { // wait that simpleHTTPClient knows the context is already timed out, // and calls CancelRequest testutil.WaitSchedule() // response is returned before cancel effects tr.respchan <- &http.Response{Body: body} }() _, _, err := c.Do(ctx, &fakeAction{}) if err == nil { t.Fatalf("expected non-nil error, got nil") } if !body.closed { t.Fatalf("expected closed body") } }
func TestHTTPMembersAPIListSuccess(t *testing.T) { wantAction := &membersAPIActionList{} mAPI := &httpMembersAPI{ client: &actionAssertingHTTPClient{ t: t, act: wantAction, resp: http.Response{ StatusCode: http.StatusOK, }, body: []byte(`{"members":[{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}]}`), }, } wantResponseMembers := []Member{ { ID: "94088180e21eb87b", Name: "node2", PeerURLs: []string{"http://127.0.0.1:7002"}, ClientURLs: []string{"http://127.0.0.1:4002"}, }, } m, err := mAPI.List(context.Background()) if err != nil { t.Errorf("got non-nil err: %#v", err) } if !reflect.DeepEqual(wantResponseMembers, m) { t.Errorf("incorrect Members: want=%#v got=%#v", wantResponseMembers, m) } }
func TestHTTPClusterClientSyncFail(t *testing.T) { cf := newStaticHTTPClientFactory([]staticHTTPResponse{ staticHTTPResponse{err: errors.New("fail!")}, }) hc := &httpClusterClient{clientFactory: cf} err := hc.reset([]string{"http://127.0.0.1:2379"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } want := []string{"http://127.0.0.1:2379"} got := hc.Endpoints() if !reflect.DeepEqual(want, got) { t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got) } err = hc.Sync(context.Background()) if err == nil { t.Fatalf("got nil error during Sync") } got = hc.Endpoints() if !reflect.DeepEqual(want, got) { t.Fatalf("incorrect endpoints after failed Sync: want=%#v got=%#v", want, got) } }
func watch(kAPI etcd.KeysAPI, key string, stop chan struct{}) (res *etcd.Response) { for res == nil { select { case <-stop: log.Debugf("Gracefully closing etcd watch loop: key=%s", key) return default: opts := &etcd.WatcherOptions{ AfterIndex: 0, Recursive: true, } watcher := kAPI.Watcher(key, opts) log.Debugf("Creating etcd watcher: %s", key) var err error res, err = watcher.Next(context.Background()) if err != nil { log.Errorf("etcd watcher %v returned error: %v", key, err) } } // Let's not slam the etcd server in the event that we know // an unexpected error occurred. time.Sleep(time.Second) } return }
func TestHTTPKeysAPICreateInOrderAction(t *testing.T) { act := &createInOrderAction{ Dir: "/foo", Value: "bar", TTL: 0, } kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}} kAPI.CreateInOrder(context.Background(), "/foo", "bar", nil) }
func TestSimpleHTTPClientDoError(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} tr.errchan <- errors.New("fixture") _, _, err := c.Do(context.Background(), &fakeAction{}) if err == nil { t.Fatalf("expected non-nil error, got nil") } }
func TestSimpleHTTPClientDoCancelContext(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} tr.startCancel <- struct{}{} tr.finishCancel <- struct{}{} _, _, err := c.Do(context.Background(), &fakeAction{}) if err == nil { t.Fatalf("expected non-nil error, got nil") } }
func TestHTTPKeysAPIDeleteAction(t *testing.T) { tests := []struct { key string value string opts *DeleteOptions wantAction httpAction }{ // nil DeleteOptions { key: "/foo", opts: nil, wantAction: &deleteAction{ Key: "/foo", PrevValue: "", PrevIndex: 0, Recursive: false, }, }, // empty DeleteOptions { key: "/foo", opts: &DeleteOptions{}, wantAction: &deleteAction{ Key: "/foo", PrevValue: "", PrevIndex: 0, Recursive: false, }, }, // populated DeleteOptions { key: "/foo", opts: &DeleteOptions{ PrevValue: "baz", PrevIndex: 13, Recursive: true, }, wantAction: &deleteAction{ Key: "/foo", PrevValue: "baz", PrevIndex: 13, Recursive: true, }, }, } for i, tt := range tests { client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction} kAPI := httpKeysAPI{client: client} kAPI.Delete(context.Background(), tt.key, tt.opts) } }
func ExampleWithTimeout() { // Pass a context with a timeout to tell a blocking function that it // should abandon its work after the timeout elapses. ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond) select { case <-time.After(200 * time.Millisecond): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) // prints "context deadline exceeded" } // Output: // context deadline exceeded }
func TestHTTPKeysAPIUpdateAction(t *testing.T) { act := &setAction{ Key: "/foo", Value: "bar", PrevExist: PrevExist, PrevIndex: 0, PrevValue: "", TTL: 0, } kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}} kAPI.Update(context.Background(), "/foo", "bar") }
func TestHTTPWatcherNextWaitAction(t *testing.T) { initAction := waitAction{ Prefix: "/pants", Key: "/foo/bar", Recursive: true, WaitIndex: 19, } client := &actionAssertingHTTPClient{ t: t, act: &initAction, resp: http.Response{ StatusCode: http.StatusOK, Header: http.Header{"X-Etcd-Index": []string{"42"}}, }, body: []byte(`{"action":"update","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":19},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`), } wantResponse := &Response{ Action: "update", Node: &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(21)}, PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)}, Index: uint64(42), } wantNextWait := waitAction{ Prefix: "/pants", Key: "/foo/bar", Recursive: true, WaitIndex: 22, } watcher := &httpWatcher{ client: client, nextWait: initAction, } resp, err := watcher.Next(context.Background()) if err != nil { t.Errorf("non-nil error: %#v", err) } if !reflect.DeepEqual(wantResponse, resp) { t.Errorf("received incorrect Response: want=%#v got=%#v", wantResponse, resp) } if !reflect.DeepEqual(wantNextWait, watcher.nextWait) { t.Errorf("nextWait incorrect: want=%#v got=%#v", wantNextWait, watcher.nextWait) } }
func TestHTTPKeysAPIGetAction(t *testing.T) { tests := []struct { key string opts *GetOptions wantAction httpAction }{ // nil GetOptions { key: "/foo", opts: nil, wantAction: &getAction{ Key: "/foo", Sorted: false, Recursive: false, }, }, // empty GetOptions { key: "/foo", opts: &GetOptions{}, wantAction: &getAction{ Key: "/foo", Sorted: false, Recursive: false, }, }, // populated GetOptions { key: "/foo", opts: &GetOptions{ Sort: true, Recursive: true, Quorum: true, }, wantAction: &getAction{ Key: "/foo", Sorted: true, Recursive: true, Quorum: true, }, }, } for i, tt := range tests { client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction} kAPI := httpKeysAPI{client: client} kAPI.Get(context.Background(), tt.key, tt.opts) } }
func TestHTTPWatcherNextFail(t *testing.T) { tests := []httpClient{ // generic HTTP client failure &staticHTTPClient{ err: errors.New("fail!"), }, // unusable status code &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusTeapot, }, }, // etcd Error response &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusNotFound, }, body: []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`), }, } for i, tt := range tests { act := waitAction{ Prefix: "/pants", Key: "/foo/bar", Recursive: true, WaitIndex: 19, } watcher := &httpWatcher{ client: tt, nextWait: act, } resp, err := watcher.Next(context.Background()) if err == nil { t.Errorf("#%d: expected non-nil error", i) } if resp != nil { t.Errorf("#%d: expected nil Response, got %#v", i, resp) } if !reflect.DeepEqual(act, watcher.nextWait) { t.Errorf("#%d: nextWait changed: want=%#v got=%#v", i, act, watcher.nextWait) } } }
func TestHTTPMembersAPIRemoveSuccess(t *testing.T) { wantAction := &membersAPIActionRemove{ memberID: "94088180e21eb87b", } mAPI := &httpMembersAPI{ client: &actionAssertingHTTPClient{ t: t, act: wantAction, resp: http.Response{ StatusCode: http.StatusNoContent, }, }, } if err := mAPI.Remove(context.Background(), "94088180e21eb87b"); err != nil { t.Errorf("got non-nil err: %#v", err) } }
func TestHTTPClusterClientSync(t *testing.T) { cf := newStaticHTTPClientFactory([]staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{StatusCode: http.StatusOK, Header: http.Header{"Content-Type": []string{"application/json"}}}, 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"]}]}`), }, }) hc := &httpClusterClient{clientFactory: cf} err := hc.reset([]string{"http://127.0.0.1:2379"}) if err != nil { t.Fatalf("unexpected error during setup: %#v", err) } want := []string{"http://127.0.0.1:2379"} got := hc.Endpoints() if !reflect.DeepEqual(want, got) { t.Fatalf("incorrect endpoints: want=%#v got=%#v", want, got) } err = hc.Sync(context.Background()) if err != nil { t.Fatalf("unexpected error during Sync: %#v", err) } 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"} got = hc.Endpoints() if !reflect.DeepEqual(want, got) { t.Fatalf("incorrect endpoints post-Sync: want=%#v got=%#v", want, got) } err = hc.reset([]string{"http://127.0.0.1:4009"}) if err != nil { t.Fatalf("unexpected error during reset: %#v", err) } want = []string{"http://127.0.0.1:4009"} got = hc.Endpoints() if !reflect.DeepEqual(want, got) { t.Fatalf("incorrect endpoints post-reset: want=%#v got=%#v", want, got) } }
func TestHTTPMembersAPIRemoveFail(t *testing.T) { tests := []httpClient{ // generic error &staticHTTPClient{ err: errors.New("fail!"), }, // unexpected HTTP status code &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusInternalServerError, }, }, } for i, tt := range tests { mAPI := &httpMembersAPI{client: tt} if err := mAPI.Remove(context.Background(), "94088180e21eb87b"); err == nil { t.Errorf("#%d: got nil err", i) } } }
func TestSimpleHTTPClientDoCancelContextResponseBodyClosedWithBlockingBody(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} ctx, cancel := context.WithCancel(context.Background()) body := &checkableReadCloser{ReadCloser: &blockingBody{c: make(chan struct{})}} go func() { tr.respchan <- &http.Response{Body: body} time.Sleep(2 * time.Millisecond) // cancel after the body is received cancel() }() _, _, err := c.Do(ctx, &fakeAction{}) if err == nil { t.Fatalf("expected non-nil error, got nil") } if !body.closed { t.Fatalf("expected closed body") } }
func TestSimpleHTTPClientDoSuccess(t *testing.T) { tr := newFakeTransport() c := &simpleHTTPClient{transport: tr} tr.respchan <- &http.Response{ StatusCode: http.StatusTeapot, Body: ioutil.NopCloser(strings.NewReader("foo")), } resp, body, err := c.Do(context.Background(), &fakeAction{}) if err != nil { t.Fatalf("incorrect error value: want=nil got=%v", err) } wantCode := http.StatusTeapot if wantCode != resp.StatusCode { t.Fatalf("invalid response code: want=%d got=%d", wantCode, resp.StatusCode) } wantBody := []byte("foo") if !reflect.DeepEqual(wantBody, body) { t.Fatalf("invalid response body: want=%q got=%q", wantBody, body) } }
func TestHTTPMembersAPIAddError(t *testing.T) { okPeer := "http://example.com:2379" tests := []struct { peerURL string client httpClient // if wantErr == nil, assert that the returned error is non-nil // if wantErr != nil, assert that the returned error matches wantErr error }{ // malformed peer URL { peerURL: ":", }, // generic httpClient failure { peerURL: okPeer, client: &staticHTTPClient{err: errors.New("fail!")}, }, // unrecognized HTTP status code { peerURL: okPeer, client: &staticHTTPClient{ resp: http.Response{StatusCode: http.StatusTeapot}, }, }, // unmarshal body into membersError on StatusConflict { peerURL: okPeer, client: &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusConflict, }, body: []byte(`{"message":"fail!"}`), }, wantErr: membersError{Message: "fail!"}, }, // fail to unmarshal body on StatusConflict { peerURL: okPeer, client: &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusConflict, }, body: []byte(`{"`), }, }, // fail to unmarshal body on StatusCreated { peerURL: okPeer, client: &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusCreated, }, body: []byte(`{"id":"XX`), }, }, } for i, tt := range tests { mAPI := &httpMembersAPI{client: tt.client} m, err := mAPI.Add(context.Background(), tt.peerURL) if err == nil { t.Errorf("#%d: got nil err", i) } if tt.wantErr != nil && !reflect.DeepEqual(tt.wantErr, err) { t.Errorf("#%d: incorrect error: want=%#v got=%#v", i, tt.wantErr, err) } if m != nil { t.Errorf("#%d: got non-nil Member", i) } } }
func TestHTTPKeysAPISetAction(t *testing.T) { tests := []struct { key string value string opts *SetOptions wantAction httpAction }{ // nil SetOptions { key: "/foo", value: "bar", opts: nil, wantAction: &setAction{ Key: "/foo", Value: "bar", PrevValue: "", PrevIndex: 0, PrevExist: PrevIgnore, TTL: 0, }, }, // empty SetOptions { key: "/foo", value: "bar", opts: &SetOptions{}, wantAction: &setAction{ Key: "/foo", Value: "bar", PrevValue: "", PrevIndex: 0, PrevExist: PrevIgnore, TTL: 0, }, }, // populated SetOptions { key: "/foo", value: "bar", opts: &SetOptions{ PrevValue: "baz", PrevIndex: 13, PrevExist: PrevExist, TTL: time.Minute, Dir: true, }, wantAction: &setAction{ Key: "/foo", Value: "bar", PrevValue: "baz", PrevIndex: 13, PrevExist: PrevExist, TTL: time.Minute, Dir: true, }, }, } for i, tt := range tests { client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction} kAPI := httpKeysAPI{client: client} kAPI.Set(context.Background(), tt.key, tt.value, tt.opts) } }
func TestHTTPClusterClientDo(t *testing.T) { fakeErr := errors.New("fake!") fakeURL := url.URL{} tests := []struct { client *httpClusterClient wantCode int wantErr error }{ // first good response short-circuits Do { client: &httpClusterClient{ endpoints: []url.URL{fakeURL, fakeURL}, clientFactory: newStaticHTTPClientFactory( []staticHTTPResponse{ staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}}, staticHTTPResponse{err: fakeErr}, }, ), }, wantCode: http.StatusTeapot, }, // fall through to good endpoint if err is arbitrary { client: &httpClusterClient{ endpoints: []url.URL{fakeURL, fakeURL}, clientFactory: newStaticHTTPClientFactory( []staticHTTPResponse{ staticHTTPResponse{err: fakeErr}, staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}}, }, ), }, wantCode: http.StatusTeapot, }, // context.DeadlineExceeded short-circuits Do { client: &httpClusterClient{ endpoints: []url.URL{fakeURL, fakeURL}, clientFactory: newStaticHTTPClientFactory( []staticHTTPResponse{ staticHTTPResponse{err: context.DeadlineExceeded}, staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}}, }, ), }, wantErr: context.DeadlineExceeded, }, // context.Canceled short-circuits Do { client: &httpClusterClient{ endpoints: []url.URL{fakeURL, fakeURL}, clientFactory: newStaticHTTPClientFactory( []staticHTTPResponse{ staticHTTPResponse{err: context.Canceled}, staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}}, }, ), }, wantErr: context.Canceled, }, // return err if there are no endpoints { client: &httpClusterClient{ endpoints: []url.URL{}, clientFactory: newHTTPClientFactory(nil, nil), }, wantErr: ErrNoEndpoints, }, // return err if all endpoints return arbitrary errors { client: &httpClusterClient{ endpoints: []url.URL{fakeURL, fakeURL}, clientFactory: newStaticHTTPClientFactory( []staticHTTPResponse{ staticHTTPResponse{err: fakeErr}, staticHTTPResponse{err: fakeErr}, }, ), }, wantErr: fakeErr, }, // 500-level errors cause Do to fallthrough to next endpoint { client: &httpClusterClient{ endpoints: []url.URL{fakeURL, fakeURL}, clientFactory: newStaticHTTPClientFactory( []staticHTTPResponse{ staticHTTPResponse{resp: http.Response{StatusCode: http.StatusBadGateway}}, staticHTTPResponse{resp: http.Response{StatusCode: http.StatusTeapot}}, }, ), }, wantCode: http.StatusTeapot, }, } for i, tt := range tests { resp, _, err := tt.client.Do(context.Background(), nil) if !reflect.DeepEqual(tt.wantErr, err) { t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr) continue } if resp == nil { if tt.wantCode != 0 { t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode) } continue } if resp.StatusCode != tt.wantCode { t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode) continue } } }
func TestRedirectFollowingHTTPClient(t *testing.T) { tests := []struct { checkRedirect CheckRedirectFunc client httpClient wantCode int wantErr error }{ // errors bubbled up { checkRedirect: func(int) error { return ErrTooManyRedirects }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ err: errors.New("fail!"), }, }, }, wantErr: errors.New("fail!"), }, // no need to follow redirect if none given { checkRedirect: func(int) error { return ErrTooManyRedirects }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTeapot, }, }, }, }, wantCode: http.StatusTeapot, }, // redirects if less than max { checkRedirect: func(via int) error { if via >= 2 { return ErrTooManyRedirects } return nil }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"http://example.com"}}, }, }, staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTeapot, }, }, }, }, wantCode: http.StatusTeapot, }, // succeed after reaching max redirects { checkRedirect: func(via int) error { if via >= 3 { return ErrTooManyRedirects } return nil }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"http://example.com"}}, }, }, staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"http://example.com"}}, }, }, staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTeapot, }, }, }, }, wantCode: http.StatusTeapot, }, // fail if too many redirects { checkRedirect: func(via int) error { if via >= 2 { return ErrTooManyRedirects } return nil }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"http://example.com"}}, }, }, staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"http://example.com"}}, }, }, staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTeapot, }, }, }, }, wantErr: ErrTooManyRedirects, }, // fail if Location header not set { checkRedirect: func(int) error { return ErrTooManyRedirects }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, }, }, }, }, wantErr: errors.New("Location header not set"), }, // fail if Location header is invalid { checkRedirect: func(int) error { return ErrTooManyRedirects }, client: &multiStaticHTTPClient{ responses: []staticHTTPResponse{ staticHTTPResponse{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{":"}}, }, }, }, }, wantErr: errors.New("Location header not valid URL: :"), }, // fail if redirects checked way too many times { checkRedirect: func(int) error { return nil }, client: &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusTemporaryRedirect, Header: http.Header{"Location": []string{"http://example.com"}}, }, }, wantErr: errTooManyRedirectChecks, }, } for i, tt := range tests { client := &redirectFollowingHTTPClient{client: tt.client, checkRedirect: tt.checkRedirect} resp, _, err := client.Do(context.Background(), nil) if !reflect.DeepEqual(tt.wantErr, err) { t.Errorf("#%d: got err=%v, want=%v", i, err, tt.wantErr) continue } if resp == nil { if tt.wantCode != 0 { t.Errorf("#%d: resp is nil, want=%d", i, tt.wantCode) } continue } if resp.StatusCode != tt.wantCode { t.Errorf("#%d: resp code=%d, want=%d", i, resp.StatusCode, tt.wantCode) continue } } }
func (r *EtcdRegistry) ctx() context.Context { ctx, _ := context.WithTimeout(context.Background(), r.reqTimeout) return ctx }