| // Copyright 2015 The etcd Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package client |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "net/http" |
| "net/url" |
| "reflect" |
| "testing" |
| |
| "github.com/coreos/etcd/pkg/types" |
| ) |
| |
| func TestMembersAPIActionList(t *testing.T) { |
| ep := url.URL{Scheme: "http", Host: "example.com"} |
| act := &membersAPIActionList{} |
| |
| wantURL := &url.URL{ |
| Scheme: "http", |
| Host: "example.com", |
| Path: "/v2/members", |
| } |
| |
| got := *act.HTTPRequest(ep) |
| err := assertRequest(got, "GET", wantURL, http.Header{}, nil) |
| if err != nil { |
| t.Error(err.Error()) |
| } |
| } |
| |
| func TestMembersAPIActionAdd(t *testing.T) { |
| ep := url.URL{Scheme: "http", Host: "example.com"} |
| act := &membersAPIActionAdd{ |
| peerURLs: types.URLs([]url.URL{ |
| {Scheme: "https", Host: "127.0.0.1:8081"}, |
| {Scheme: "http", Host: "127.0.0.1:8080"}, |
| }), |
| } |
| |
| wantURL := &url.URL{ |
| Scheme: "http", |
| Host: "example.com", |
| Path: "/v2/members", |
| } |
| wantHeader := http.Header{ |
| "Content-Type": []string{"application/json"}, |
| } |
| wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`) |
| |
| got := *act.HTTPRequest(ep) |
| err := assertRequest(got, "POST", wantURL, wantHeader, wantBody) |
| if err != nil { |
| t.Error(err.Error()) |
| } |
| } |
| |
| func TestMembersAPIActionUpdate(t *testing.T) { |
| ep := url.URL{Scheme: "http", Host: "example.com"} |
| act := &membersAPIActionUpdate{ |
| memberID: "0xabcd", |
| peerURLs: types.URLs([]url.URL{ |
| {Scheme: "https", Host: "127.0.0.1:8081"}, |
| {Scheme: "http", Host: "127.0.0.1:8080"}, |
| }), |
| } |
| |
| wantURL := &url.URL{ |
| Scheme: "http", |
| Host: "example.com", |
| Path: "/v2/members/0xabcd", |
| } |
| wantHeader := http.Header{ |
| "Content-Type": []string{"application/json"}, |
| } |
| wantBody := []byte(`{"peerURLs":["https://127.0.0.1:8081","http://127.0.0.1:8080"]}`) |
| |
| got := *act.HTTPRequest(ep) |
| err := assertRequest(got, "PUT", wantURL, wantHeader, wantBody) |
| if err != nil { |
| t.Error(err.Error()) |
| } |
| } |
| |
| func TestMembersAPIActionRemove(t *testing.T) { |
| ep := url.URL{Scheme: "http", Host: "example.com"} |
| act := &membersAPIActionRemove{memberID: "XXX"} |
| |
| wantURL := &url.URL{ |
| Scheme: "http", |
| Host: "example.com", |
| Path: "/v2/members/XXX", |
| } |
| |
| got := *act.HTTPRequest(ep) |
| err := assertRequest(got, "DELETE", wantURL, http.Header{}, nil) |
| if err != nil { |
| t.Error(err.Error()) |
| } |
| } |
| |
| func TestMembersAPIActionLeader(t *testing.T) { |
| ep := url.URL{Scheme: "http", Host: "example.com"} |
| act := &membersAPIActionLeader{} |
| |
| wantURL := &url.URL{ |
| Scheme: "http", |
| Host: "example.com", |
| Path: "/v2/members/leader", |
| } |
| |
| got := *act.HTTPRequest(ep) |
| err := assertRequest(got, "GET", wantURL, http.Header{}, nil) |
| if err != nil { |
| t.Error(err.Error()) |
| } |
| } |
| |
| func TestAssertStatusCode(t *testing.T) { |
| if err := assertStatusCode(404, 400); err == nil { |
| t.Errorf("assertStatusCode failed to detect conflict in 400 vs 404") |
| } |
| |
| if err := assertStatusCode(404, 400, 404); err != nil { |
| t.Errorf("assertStatusCode found conflict in (404,400) vs 400: %v", err) |
| } |
| } |
| |
| func TestV2MembersURL(t *testing.T) { |
| got := v2MembersURL(url.URL{ |
| Scheme: "http", |
| Host: "foo.example.com:4002", |
| Path: "/pants", |
| }) |
| want := &url.URL{ |
| Scheme: "http", |
| Host: "foo.example.com:4002", |
| Path: "/pants/v2/members", |
| } |
| |
| if !reflect.DeepEqual(want, got) { |
| t.Fatalf("v2MembersURL got %#v, want %#v", got, want) |
| } |
| } |
| |
| func TestMemberUnmarshal(t *testing.T) { |
| tests := []struct { |
| body []byte |
| wantMember Member |
| wantError bool |
| }{ |
| // no URLs, just check ID & Name |
| { |
| body: []byte(`{"id": "c", "name": "dungarees"}`), |
| wantMember: Member{ID: "c", Name: "dungarees", PeerURLs: nil, ClientURLs: nil}, |
| }, |
| |
| // both client and peer URLs |
| { |
| body: []byte(`{"peerURLs": ["http://127.0.0.1:2379"], "clientURLs": ["http://127.0.0.1:2379"]}`), |
| wantMember: Member{ |
| PeerURLs: []string{ |
| "http://127.0.0.1:2379", |
| }, |
| ClientURLs: []string{ |
| "http://127.0.0.1:2379", |
| }, |
| }, |
| }, |
| |
| // multiple peer URLs |
| { |
| body: []byte(`{"peerURLs": ["http://127.0.0.1:2379", "https://example.com"]}`), |
| wantMember: Member{ |
| PeerURLs: []string{ |
| "http://127.0.0.1:2379", |
| "https://example.com", |
| }, |
| ClientURLs: nil, |
| }, |
| }, |
| |
| // multiple client URLs |
| { |
| body: []byte(`{"clientURLs": ["http://127.0.0.1:2379", "https://example.com"]}`), |
| wantMember: Member{ |
| PeerURLs: nil, |
| ClientURLs: []string{ |
| "http://127.0.0.1:2379", |
| "https://example.com", |
| }, |
| }, |
| }, |
| |
| // invalid JSON |
| { |
| body: []byte(`{"peerU`), |
| wantError: true, |
| }, |
| } |
| |
| for i, tt := range tests { |
| got := Member{} |
| err := json.Unmarshal(tt.body, &got) |
| if tt.wantError != (err != nil) { |
| t.Errorf("#%d: want error %t, got %v", i, tt.wantError, err) |
| continue |
| } |
| |
| if !reflect.DeepEqual(tt.wantMember, got) { |
| t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.wantMember, got) |
| } |
| } |
| } |
| |
| func TestMemberCollectionUnmarshalFail(t *testing.T) { |
| mc := &memberCollection{} |
| if err := mc.UnmarshalJSON([]byte(`{`)); err == nil { |
| t.Errorf("got nil error") |
| } |
| } |
| |
| func TestMemberCollectionUnmarshal(t *testing.T) { |
| tests := []struct { |
| body []byte |
| want memberCollection |
| }{ |
| { |
| body: []byte(`{}`), |
| want: memberCollection([]Member{}), |
| }, |
| { |
| body: []byte(`{"members":[]}`), |
| want: memberCollection([]Member{}), |
| }, |
| { |
| 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"]}]}`), |
| want: memberCollection( |
| []Member{ |
| { |
| ID: "2745e2525fce8fe", |
| Name: "node3", |
| PeerURLs: []string{ |
| "http://127.0.0.1:7003", |
| }, |
| ClientURLs: []string{ |
| "http://127.0.0.1:4003", |
| }, |
| }, |
| { |
| ID: "42134f434382925", |
| Name: "node1", |
| PeerURLs: []string{ |
| "http://127.0.0.1:2380", |
| "http://127.0.0.1:7001", |
| }, |
| ClientURLs: []string{ |
| "http://127.0.0.1:2379", |
| "http://127.0.0.1:4001", |
| }, |
| }, |
| { |
| ID: "94088180e21eb87b", |
| Name: "node2", |
| PeerURLs: []string{ |
| "http://127.0.0.1:7002", |
| }, |
| ClientURLs: []string{ |
| "http://127.0.0.1:4002", |
| }, |
| }, |
| }, |
| ), |
| }, |
| } |
| |
| for i, tt := range tests { |
| var got memberCollection |
| err := json.Unmarshal(tt.body, &got) |
| if err != nil { |
| t.Errorf("#%d: unexpected error: %v", i, err) |
| continue |
| } |
| |
| if !reflect.DeepEqual(tt.want, got) { |
| t.Errorf("#%d: incorrect output: want=%#v, got=%#v", i, tt.want, got) |
| } |
| } |
| } |
| |
| func TestMemberCreateRequestMarshal(t *testing.T) { |
| req := memberCreateOrUpdateRequest{ |
| PeerURLs: types.URLs([]url.URL{ |
| {Scheme: "http", Host: "127.0.0.1:8081"}, |
| {Scheme: "https", Host: "127.0.0.1:8080"}, |
| }), |
| } |
| want := []byte(`{"peerURLs":["http://127.0.0.1:8081","https://127.0.0.1:8080"]}`) |
| |
| got, err := json.Marshal(&req) |
| if err != nil { |
| t.Fatalf("Marshal returned unexpected err=%v", err) |
| } |
| |
| if !reflect.DeepEqual(want, got) { |
| t.Fatalf("Failed to marshal memberCreateRequest: want=%s, got=%s", want, got) |
| } |
| } |
| |
| 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 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 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 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 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 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 TestHTTPMembersAPILeaderSuccess(t *testing.T) { |
| wantAction := &membersAPIActionLeader{} |
| mAPI := &httpMembersAPI{ |
| client: &actionAssertingHTTPClient{ |
| t: t, |
| act: wantAction, |
| resp: http.Response{ |
| StatusCode: http.StatusOK, |
| }, |
| body: []byte(`{"id":"94088180e21eb87b","name":"node2","peerURLs":["http://127.0.0.1:7002"],"clientURLs":["http://127.0.0.1:4002"]}`), |
| }, |
| } |
| |
| wantResponseMember := &Member{ |
| ID: "94088180e21eb87b", |
| Name: "node2", |
| PeerURLs: []string{"http://127.0.0.1:7002"}, |
| ClientURLs: []string{"http://127.0.0.1:4002"}, |
| } |
| |
| m, err := mAPI.Leader(context.Background()) |
| if err != nil { |
| t.Errorf("err = %v, want %v", err, nil) |
| } |
| if !reflect.DeepEqual(wantResponseMember, m) { |
| t.Errorf("incorrect member: member = %v, want %v", wantResponseMember, m) |
| } |
| } |
| |
| func TestHTTPMembersAPILeaderError(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} |
| m, err := mAPI.Leader(context.Background()) |
| if err == nil { |
| t.Errorf("#%d: err = nil, want not nil", i) |
| } |
| if m != nil { |
| t.Errorf("member slice = %v, want nil", m) |
| } |
| } |
| } |