// 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" "errors" "fmt" "io/ioutil" "net/http" "net/url" "reflect" "testing" "time" ) func TestV2KeysURLHelper(t *testing.T) { tests := []struct { endpoint url.URL prefix string key string want url.URL }{ // key is empty, no problem { endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}, prefix: "", key: "", want: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}, }, // key is joined to path { endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}, prefix: "", key: "/foo/bar", want: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar"}, }, // key is joined to path when path is empty { endpoint: url.URL{Scheme: "http", Host: "example.com", Path: ""}, prefix: "", key: "/foo/bar", want: url.URL{Scheme: "http", Host: "example.com", Path: "/foo/bar"}, }, // Host field carries through with port { endpoint: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"}, prefix: "", key: "", want: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"}, }, // Scheme carries through { endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"}, prefix: "", key: "", want: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"}, }, // Prefix is applied { endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"}, prefix: "/bar", key: "/baz", want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz"}, }, // Prefix is joined to path { endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"}, prefix: "/bar", key: "", want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar"}, }, // Keep trailing slash { endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"}, prefix: "/bar", key: "/baz/", want: url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz/"}, }, } for i, tt := range tests { got := v2KeysURL(tt.endpoint, tt.prefix, tt.key) if tt.want != *got { t.Errorf("#%d: want=%#v, got=%#v", i, tt.want, *got) } } } func TestGetAction(t *testing.T) { ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"} baseWantURL := &url.URL{ Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar", } wantHeader := http.Header{} tests := []struct { recursive bool sorted bool quorum bool wantQuery string }{ { recursive: false, sorted: false, quorum: false, wantQuery: "quorum=false&recursive=false&sorted=false", }, { recursive: true, sorted: false, quorum: false, wantQuery: "quorum=false&recursive=true&sorted=false", }, { recursive: false, sorted: true, quorum: false, wantQuery: "quorum=false&recursive=false&sorted=true", }, { recursive: true, sorted: true, quorum: false, wantQuery: "quorum=false&recursive=true&sorted=true", }, { recursive: false, sorted: false, quorum: true, wantQuery: "quorum=true&recursive=false&sorted=false", }, } for i, tt := range tests { f := getAction{ Key: "/foo/bar", Recursive: tt.recursive, Sorted: tt.sorted, Quorum: tt.quorum, } got := *f.HTTPRequest(ep) wantURL := baseWantURL wantURL.RawQuery = tt.wantQuery err := assertRequest(got, "GET", wantURL, wantHeader, nil) if err != nil { t.Errorf("#%d: %v", i, err) } } } func TestWaitAction(t *testing.T) { ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"} baseWantURL := &url.URL{ Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar", } wantHeader := http.Header{} tests := []struct { waitIndex uint64 recursive bool wantQuery string }{ { recursive: false, waitIndex: uint64(0), wantQuery: "recursive=false&wait=true&waitIndex=0", }, { recursive: false, waitIndex: uint64(12), wantQuery: "recursive=false&wait=true&waitIndex=12", }, { recursive: true, waitIndex: uint64(12), wantQuery: "recursive=true&wait=true&waitIndex=12", }, } for i, tt := range tests { f := waitAction{ Key: "/foo/bar", WaitIndex: tt.waitIndex, Recursive: tt.recursive, } got := *f.HTTPRequest(ep) wantURL := baseWantURL wantURL.RawQuery = tt.wantQuery err := assertRequest(got, "GET", wantURL, wantHeader, nil) if err != nil { t.Errorf("#%d: unexpected error: %#v", i, err) } } } func TestSetAction(t *testing.T) { wantHeader := http.Header(map[string][]string{ "Content-Type": {"application/x-www-form-urlencoded"}, }) tests := []struct { act setAction wantURL string wantBody string }{ // default prefix { act: setAction{ Prefix: defaultV2KeysPrefix, Key: "foo", }, wantURL: "http://example.com/v2/keys/foo", wantBody: "value=", }, // non-default prefix { act: setAction{ Prefix: "/pfx", Key: "foo", }, wantURL: "http://example.com/pfx/foo", wantBody: "value=", }, // no prefix { act: setAction{ Key: "foo", }, wantURL: "http://example.com/foo", wantBody: "value=", }, // Key with path separators { act: setAction{ Prefix: defaultV2KeysPrefix, Key: "foo/bar/baz", }, wantURL: "http://example.com/v2/keys/foo/bar/baz", wantBody: "value=", }, // Key with leading slash, Prefix with trailing slash { act: setAction{ Prefix: "/foo/", Key: "/bar", }, wantURL: "http://example.com/foo/bar", wantBody: "value=", }, // Key with trailing slash { act: setAction{ Key: "/foo/", }, wantURL: "http://example.com/foo/", wantBody: "value=", }, // Value is set { act: setAction{ Key: "foo", Value: "baz", }, wantURL: "http://example.com/foo", wantBody: "value=baz", }, // PrevExist set, but still ignored { act: setAction{ Key: "foo", PrevExist: PrevIgnore, }, wantURL: "http://example.com/foo", wantBody: "value=", }, // PrevExist set to true { act: setAction{ Key: "foo", PrevExist: PrevExist, }, wantURL: "http://example.com/foo?prevExist=true", wantBody: "value=", }, // PrevExist set to false { act: setAction{ Key: "foo", PrevExist: PrevNoExist, }, wantURL: "http://example.com/foo?prevExist=false", wantBody: "value=", }, // PrevValue is urlencoded { act: setAction{ Key: "foo", PrevValue: "bar baz", }, wantURL: "http://example.com/foo?prevValue=bar+baz", wantBody: "value=", }, // PrevIndex is set { act: setAction{ Key: "foo", PrevIndex: uint64(12), }, wantURL: "http://example.com/foo?prevIndex=12", wantBody: "value=", }, // TTL is set { act: setAction{ Key: "foo", TTL: 3 * time.Minute, }, wantURL: "http://example.com/foo", wantBody: "ttl=180&value=", }, // Refresh is set { act: setAction{ Key: "foo", TTL: 3 * time.Minute, Refresh: true, }, wantURL: "http://example.com/foo", wantBody: "refresh=true&ttl=180&value=", }, // Dir is set { act: setAction{ Key: "foo", Dir: true, }, wantURL: "http://example.com/foo?dir=true", wantBody: "", }, // Dir is set with a value { act: setAction{ Key: "foo", Value: "bar", Dir: true, }, wantURL: "http://example.com/foo?dir=true", wantBody: "", }, // Dir is set with PrevExist set to true { act: setAction{ Key: "foo", PrevExist: PrevExist, Dir: true, }, wantURL: "http://example.com/foo?dir=true&prevExist=true", wantBody: "", }, // Dir is set with PrevValue { act: setAction{ Key: "foo", PrevValue: "bar", Dir: true, }, wantURL: "http://example.com/foo?dir=true", wantBody: "", }, // NoValueOnSuccess is set { act: setAction{ Key: "foo", NoValueOnSuccess: true, }, wantURL: "http://example.com/foo?noValueOnSuccess=true", wantBody: "value=", }, } for i, tt := range tests { u, err := url.Parse(tt.wantURL) if err != nil { t.Errorf("#%d: unable to use wantURL fixture: %v", i, err) } got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"}) if err := assertRequest(*got, "PUT", u, wantHeader, []byte(tt.wantBody)); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestCreateInOrderAction(t *testing.T) { wantHeader := http.Header(map[string][]string{ "Content-Type": {"application/x-www-form-urlencoded"}, }) tests := []struct { act createInOrderAction wantURL string wantBody string }{ // default prefix { act: createInOrderAction{ Prefix: defaultV2KeysPrefix, Dir: "foo", }, wantURL: "http://example.com/v2/keys/foo", wantBody: "value=", }, // non-default prefix { act: createInOrderAction{ Prefix: "/pfx", Dir: "foo", }, wantURL: "http://example.com/pfx/foo", wantBody: "value=", }, // no prefix { act: createInOrderAction{ Dir: "foo", }, wantURL: "http://example.com/foo", wantBody: "value=", }, // Key with path separators { act: createInOrderAction{ Prefix: defaultV2KeysPrefix, Dir: "foo/bar/baz", }, wantURL: "http://example.com/v2/keys/foo/bar/baz", wantBody: "value=", }, // Key with leading slash, Prefix with trailing slash { act: createInOrderAction{ Prefix: "/foo/", Dir: "/bar", }, wantURL: "http://example.com/foo/bar", wantBody: "value=", }, // Key with trailing slash { act: createInOrderAction{ Dir: "/foo/", }, wantURL: "http://example.com/foo/", wantBody: "value=", }, // Value is set { act: createInOrderAction{ Dir: "foo", Value: "baz", }, wantURL: "http://example.com/foo", wantBody: "value=baz", }, // TTL is set { act: createInOrderAction{ Dir: "foo", TTL: 3 * time.Minute, }, wantURL: "http://example.com/foo", wantBody: "ttl=180&value=", }, } for i, tt := range tests { u, err := url.Parse(tt.wantURL) if err != nil { t.Errorf("#%d: unable to use wantURL fixture: %v", i, err) } got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"}) if err := assertRequest(*got, "POST", u, wantHeader, []byte(tt.wantBody)); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestDeleteAction(t *testing.T) { wantHeader := http.Header(map[string][]string{ "Content-Type": {"application/x-www-form-urlencoded"}, }) tests := []struct { act deleteAction wantURL string }{ // default prefix { act: deleteAction{ Prefix: defaultV2KeysPrefix, Key: "foo", }, wantURL: "http://example.com/v2/keys/foo", }, // non-default prefix { act: deleteAction{ Prefix: "/pfx", Key: "foo", }, wantURL: "http://example.com/pfx/foo", }, // no prefix { act: deleteAction{ Key: "foo", }, wantURL: "http://example.com/foo", }, // Key with path separators { act: deleteAction{ Prefix: defaultV2KeysPrefix, Key: "foo/bar/baz", }, wantURL: "http://example.com/v2/keys/foo/bar/baz", }, // Key with leading slash, Prefix with trailing slash { act: deleteAction{ Prefix: "/foo/", Key: "/bar", }, wantURL: "http://example.com/foo/bar", }, // Key with trailing slash { act: deleteAction{ Key: "/foo/", }, wantURL: "http://example.com/foo/", }, // Recursive set to true { act: deleteAction{ Key: "foo", Recursive: true, }, wantURL: "http://example.com/foo?recursive=true", }, // PrevValue is urlencoded { act: deleteAction{ Key: "foo", PrevValue: "bar baz", }, wantURL: "http://example.com/foo?prevValue=bar+baz", }, // PrevIndex is set { act: deleteAction{ Key: "foo", PrevIndex: uint64(12), }, wantURL: "http://example.com/foo?prevIndex=12", }, } for i, tt := range tests { u, err := url.Parse(tt.wantURL) if err != nil { t.Errorf("#%d: unable to use wantURL fixture: %v", i, err) } got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"}) if err := assertRequest(*got, "DELETE", u, wantHeader, nil); err != nil { t.Errorf("#%d: %v", i, err) } } } func assertRequest(got http.Request, wantMethod string, wantURL *url.URL, wantHeader http.Header, wantBody []byte) error { if wantMethod != got.Method { return fmt.Errorf("want.Method=%#v got.Method=%#v", wantMethod, got.Method) } if !reflect.DeepEqual(wantURL, got.URL) { return fmt.Errorf("want.URL=%#v got.URL=%#v", wantURL, got.URL) } if !reflect.DeepEqual(wantHeader, got.Header) { return fmt.Errorf("want.Header=%#v got.Header=%#v", wantHeader, got.Header) } if got.Body == nil { if wantBody != nil { return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, got.Body) } } else { if wantBody == nil { return fmt.Errorf("want.Body=%v got.Body=%s", wantBody, got.Body) } gotBytes, err := ioutil.ReadAll(got.Body) if err != nil { return err } if !reflect.DeepEqual(wantBody, gotBytes) { return fmt.Errorf("want.Body=%s got.Body=%s", wantBody, gotBytes) } } return nil } func TestUnmarshalSuccessfulResponse(t *testing.T) { var expiration time.Time expiration.UnmarshalText([]byte("2015-04-07T04:40:23.044979686Z")) tests := []struct { indexHdr string clusterIDHdr string body string wantRes *Response wantErr bool }{ // Neither PrevNode or Node { indexHdr: "1", body: `{"action":"delete"}`, wantRes: &Response{Action: "delete", Index: 1}, wantErr: false, }, // PrevNode { indexHdr: "15", body: `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`, wantRes: &Response{ Action: "delete", Index: 15, Node: nil, PrevNode: &Node{ Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10, }, }, wantErr: false, }, // Node { indexHdr: "15", body: `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`, wantRes: &Response{ Action: "get", Index: 15, Node: &Node{ Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10, TTL: 10, Expiration: &expiration, }, PrevNode: nil, }, wantErr: false, }, // Node Dir { indexHdr: "15", clusterIDHdr: "abcdef", body: `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`, wantRes: &Response{ Action: "get", Index: 15, Node: &Node{ Key: "/foo", Dir: true, ModifiedIndex: 12, CreatedIndex: 10, }, PrevNode: nil, ClusterID: "abcdef", }, wantErr: false, }, // PrevNode and Node { indexHdr: "15", body: `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`, wantRes: &Response{ Action: "update", Index: 15, PrevNode: &Node{ Key: "/foo", Value: "baz", ModifiedIndex: 10, CreatedIndex: 10, }, Node: &Node{ Key: "/foo", Value: "bar", ModifiedIndex: 12, CreatedIndex: 10, }, }, wantErr: false, }, // Garbage in body { indexHdr: "", body: `garbage`, wantRes: nil, wantErr: true, }, // non-integer index { indexHdr: "poo", body: `{}`, wantRes: nil, wantErr: true, }, } for i, tt := range tests { h := make(http.Header) h.Add("X-Etcd-Index", tt.indexHdr) res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body)) if tt.wantErr != (err != nil) { t.Errorf("#%d: wantErr=%t, err=%v", i, tt.wantErr, err) } if (res == nil) != (tt.wantRes == nil) { t.Errorf("#%d: received res=%#v, but expected res=%#v", i, res, tt.wantRes) continue } else if tt.wantRes == nil { // expected and successfully got nil response continue } if res.Action != tt.wantRes.Action { t.Errorf("#%d: Action=%s, expected %s", i, res.Action, tt.wantRes.Action) } if res.Index != tt.wantRes.Index { t.Errorf("#%d: Index=%d, expected %d", i, res.Index, tt.wantRes.Index) } if !reflect.DeepEqual(res.Node, tt.wantRes.Node) { t.Errorf("#%d: Node=%v, expected %v", i, res.Node, tt.wantRes.Node) } } } func TestUnmarshalFailedKeysResponse(t *testing.T) { body := []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`) wantErr := Error{ Code: 100, Message: "Key not found", Cause: "/foo", Index: uint64(18), } gotErr := unmarshalFailedKeysResponse(body) if !reflect.DeepEqual(wantErr, gotErr) { t.Errorf("unexpected error: want=%#v got=%#v", wantErr, gotErr) } } func TestUnmarshalFailedKeysResponseBadJSON(t *testing.T) { err := unmarshalFailedKeysResponse([]byte(`{"er`)) if err == nil { t.Errorf("got nil error") } else if _, ok := err.(Error); ok { t.Errorf("error is of incorrect type *Error: %#v", err) } } 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 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 TestHTTPKeysAPIWatcherAction(t *testing.T) { tests := []struct { key string opts *WatcherOptions want waitAction }{ { key: "/foo", opts: nil, want: waitAction{ Key: "/foo", Recursive: false, WaitIndex: 0, }, }, { key: "/foo", opts: &WatcherOptions{ Recursive: false, AfterIndex: 0, }, want: waitAction{ Key: "/foo", Recursive: false, WaitIndex: 0, }, }, { key: "/foo", opts: &WatcherOptions{ Recursive: true, AfterIndex: 0, }, want: waitAction{ Key: "/foo", Recursive: true, WaitIndex: 0, }, }, { key: "/foo", opts: &WatcherOptions{ Recursive: false, AfterIndex: 19, }, want: waitAction{ Key: "/foo", Recursive: false, WaitIndex: 20, }, }, } for i, tt := range tests { testError := errors.New("fail!") kAPI := &httpKeysAPI{ client: &staticHTTPClient{err: testError}, } want := &httpWatcher{ client: &staticHTTPClient{err: testError}, nextWait: tt.want, } got := kAPI.Watcher(tt.key, tt.opts) if !reflect.DeepEqual(want, got) { t.Errorf("#%d: incorrect watcher: want=%#v got=%#v", i, want, got) } } } 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 TestHTTPKeysAPISetError(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.Set(context.Background(), "/foo", "bar", 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 TestHTTPKeysAPISetResponse(t *testing.T) { client := &staticHTTPClient{ resp: http.Response{ StatusCode: http.StatusOK, Header: http.Header{"X-Etcd-Index": []string{"21"}}, }, body: []byte(`{"action":"set","node":{"key":"/pants/foo/bar/baz","value":"snarf","modifiedIndex":21,"createdIndex":21},"prevNode":{"key":"/pants/foo/bar/baz","value":"snazz","modifiedIndex":20,"createdIndex":19}}`), } wantResponse := &Response{ Action: "set", Node: &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(21), ModifiedIndex: uint64(21)}, PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)}, Index: uint64(21), } kAPI := &httpKeysAPI{client: client, prefix: "/pants"} resp, err := kAPI.Set(context.Background(), "/foo/bar/baz", "snarf", 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 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 TestHTTPKeysAPIGetError(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.Get(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 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{ {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 TestHTTPKeysAPIDeleteAction(t *testing.T) { tests := []struct { key 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 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 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 TestHTTPKeysAPICreateAction(t *testing.T) { act := &setAction{ Key: "/foo", Value: "bar", PrevExist: PrevNoExist, PrevIndex: 0, PrevValue: "", TTL: 0, } kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}} kAPI.Create(context.Background(), "/foo", "bar") } 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 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 TestNodeTTLDuration(t *testing.T) { tests := []struct { node *Node want time.Duration }{ { node: &Node{TTL: 0}, want: 0, }, { node: &Node{TTL: 97}, want: 97 * time.Second, }, } for i, tt := range tests { got := tt.node.TTLDuration() if tt.want != got { t.Errorf("#%d: incorrect duration: want=%v got=%v", i, tt.want, got) } } }