blob: 0e190cf59816cba5c219e018779f6bd81cd795e0 [file] [log] [blame]
// 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 {
kAPI := &httpKeysAPI{
client: &staticHTTPClient{err: errors.New("fail!")},
}
want := &httpWatcher{
client: &staticHTTPClient{err: errors.New("fail!")},
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)
}
}
}