go.etcd.io/etcd@v3.3.27+incompatible/client/keys_test.go (about)

     1  // Copyright 2015 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package client
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"net/url"
    24  	"reflect"
    25  	"testing"
    26  	"time"
    27  )
    28  
    29  func TestV2KeysURLHelper(t *testing.T) {
    30  	tests := []struct {
    31  		endpoint url.URL
    32  		prefix   string
    33  		key      string
    34  		want     url.URL
    35  	}{
    36  		// key is empty, no problem
    37  		{
    38  			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
    39  			prefix:   "",
    40  			key:      "",
    41  			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
    42  		},
    43  
    44  		// key is joined to path
    45  		{
    46  			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"},
    47  			prefix:   "",
    48  			key:      "/foo/bar",
    49  			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys/foo/bar"},
    50  		},
    51  
    52  		// key is joined to path when path is empty
    53  		{
    54  			endpoint: url.URL{Scheme: "http", Host: "example.com", Path: ""},
    55  			prefix:   "",
    56  			key:      "/foo/bar",
    57  			want:     url.URL{Scheme: "http", Host: "example.com", Path: "/foo/bar"},
    58  		},
    59  
    60  		// Host field carries through with port
    61  		{
    62  			endpoint: url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
    63  			prefix:   "",
    64  			key:      "",
    65  			want:     url.URL{Scheme: "http", Host: "example.com:8080", Path: "/v2/keys"},
    66  		},
    67  
    68  		// Scheme carries through
    69  		{
    70  			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
    71  			prefix:   "",
    72  			key:      "",
    73  			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/v2/keys"},
    74  		},
    75  		// Prefix is applied
    76  		{
    77  			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
    78  			prefix:   "/bar",
    79  			key:      "/baz",
    80  			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz"},
    81  		},
    82  		// Prefix is joined to path
    83  		{
    84  			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
    85  			prefix:   "/bar",
    86  			key:      "",
    87  			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar"},
    88  		},
    89  		// Keep trailing slash
    90  		{
    91  			endpoint: url.URL{Scheme: "https", Host: "example.com", Path: "/foo"},
    92  			prefix:   "/bar",
    93  			key:      "/baz/",
    94  			want:     url.URL{Scheme: "https", Host: "example.com", Path: "/foo/bar/baz/"},
    95  		},
    96  	}
    97  
    98  	for i, tt := range tests {
    99  		got := v2KeysURL(tt.endpoint, tt.prefix, tt.key)
   100  		if tt.want != *got {
   101  			t.Errorf("#%d: want=%#v, got=%#v", i, tt.want, *got)
   102  		}
   103  	}
   104  }
   105  
   106  func TestGetAction(t *testing.T) {
   107  	ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}
   108  	baseWantURL := &url.URL{
   109  		Scheme: "http",
   110  		Host:   "example.com",
   111  		Path:   "/v2/keys/foo/bar",
   112  	}
   113  	wantHeader := http.Header{}
   114  
   115  	tests := []struct {
   116  		recursive bool
   117  		sorted    bool
   118  		quorum    bool
   119  		wantQuery string
   120  	}{
   121  		{
   122  			recursive: false,
   123  			sorted:    false,
   124  			quorum:    false,
   125  			wantQuery: "quorum=false&recursive=false&sorted=false",
   126  		},
   127  		{
   128  			recursive: true,
   129  			sorted:    false,
   130  			quorum:    false,
   131  			wantQuery: "quorum=false&recursive=true&sorted=false",
   132  		},
   133  		{
   134  			recursive: false,
   135  			sorted:    true,
   136  			quorum:    false,
   137  			wantQuery: "quorum=false&recursive=false&sorted=true",
   138  		},
   139  		{
   140  			recursive: true,
   141  			sorted:    true,
   142  			quorum:    false,
   143  			wantQuery: "quorum=false&recursive=true&sorted=true",
   144  		},
   145  		{
   146  			recursive: false,
   147  			sorted:    false,
   148  			quorum:    true,
   149  			wantQuery: "quorum=true&recursive=false&sorted=false",
   150  		},
   151  	}
   152  
   153  	for i, tt := range tests {
   154  		f := getAction{
   155  			Key:       "/foo/bar",
   156  			Recursive: tt.recursive,
   157  			Sorted:    tt.sorted,
   158  			Quorum:    tt.quorum,
   159  		}
   160  		got := *f.HTTPRequest(ep)
   161  
   162  		wantURL := baseWantURL
   163  		wantURL.RawQuery = tt.wantQuery
   164  
   165  		err := assertRequest(got, "GET", wantURL, wantHeader, nil)
   166  		if err != nil {
   167  			t.Errorf("#%d: %v", i, err)
   168  		}
   169  	}
   170  }
   171  
   172  func TestWaitAction(t *testing.T) {
   173  	ep := url.URL{Scheme: "http", Host: "example.com", Path: "/v2/keys"}
   174  	baseWantURL := &url.URL{
   175  		Scheme: "http",
   176  		Host:   "example.com",
   177  		Path:   "/v2/keys/foo/bar",
   178  	}
   179  	wantHeader := http.Header{}
   180  
   181  	tests := []struct {
   182  		waitIndex uint64
   183  		recursive bool
   184  		wantQuery string
   185  	}{
   186  		{
   187  			recursive: false,
   188  			waitIndex: uint64(0),
   189  			wantQuery: "recursive=false&wait=true&waitIndex=0",
   190  		},
   191  		{
   192  			recursive: false,
   193  			waitIndex: uint64(12),
   194  			wantQuery: "recursive=false&wait=true&waitIndex=12",
   195  		},
   196  		{
   197  			recursive: true,
   198  			waitIndex: uint64(12),
   199  			wantQuery: "recursive=true&wait=true&waitIndex=12",
   200  		},
   201  	}
   202  
   203  	for i, tt := range tests {
   204  		f := waitAction{
   205  			Key:       "/foo/bar",
   206  			WaitIndex: tt.waitIndex,
   207  			Recursive: tt.recursive,
   208  		}
   209  		got := *f.HTTPRequest(ep)
   210  
   211  		wantURL := baseWantURL
   212  		wantURL.RawQuery = tt.wantQuery
   213  
   214  		err := assertRequest(got, "GET", wantURL, wantHeader, nil)
   215  		if err != nil {
   216  			t.Errorf("#%d: unexpected error: %#v", i, err)
   217  		}
   218  	}
   219  }
   220  
   221  func TestSetAction(t *testing.T) {
   222  	wantHeader := http.Header(map[string][]string{
   223  		"Content-Type": {"application/x-www-form-urlencoded"},
   224  	})
   225  
   226  	tests := []struct {
   227  		act      setAction
   228  		wantURL  string
   229  		wantBody string
   230  	}{
   231  		// default prefix
   232  		{
   233  			act: setAction{
   234  				Prefix: defaultV2KeysPrefix,
   235  				Key:    "foo",
   236  			},
   237  			wantURL:  "http://example.com/v2/keys/foo",
   238  			wantBody: "value=",
   239  		},
   240  
   241  		// non-default prefix
   242  		{
   243  			act: setAction{
   244  				Prefix: "/pfx",
   245  				Key:    "foo",
   246  			},
   247  			wantURL:  "http://example.com/pfx/foo",
   248  			wantBody: "value=",
   249  		},
   250  
   251  		// no prefix
   252  		{
   253  			act: setAction{
   254  				Key: "foo",
   255  			},
   256  			wantURL:  "http://example.com/foo",
   257  			wantBody: "value=",
   258  		},
   259  
   260  		// Key with path separators
   261  		{
   262  			act: setAction{
   263  				Prefix: defaultV2KeysPrefix,
   264  				Key:    "foo/bar/baz",
   265  			},
   266  			wantURL:  "http://example.com/v2/keys/foo/bar/baz",
   267  			wantBody: "value=",
   268  		},
   269  
   270  		// Key with leading slash, Prefix with trailing slash
   271  		{
   272  			act: setAction{
   273  				Prefix: "/foo/",
   274  				Key:    "/bar",
   275  			},
   276  			wantURL:  "http://example.com/foo/bar",
   277  			wantBody: "value=",
   278  		},
   279  
   280  		// Key with trailing slash
   281  		{
   282  			act: setAction{
   283  				Key: "/foo/",
   284  			},
   285  			wantURL:  "http://example.com/foo/",
   286  			wantBody: "value=",
   287  		},
   288  
   289  		// Value is set
   290  		{
   291  			act: setAction{
   292  				Key:   "foo",
   293  				Value: "baz",
   294  			},
   295  			wantURL:  "http://example.com/foo",
   296  			wantBody: "value=baz",
   297  		},
   298  
   299  		// PrevExist set, but still ignored
   300  		{
   301  			act: setAction{
   302  				Key:       "foo",
   303  				PrevExist: PrevIgnore,
   304  			},
   305  			wantURL:  "http://example.com/foo",
   306  			wantBody: "value=",
   307  		},
   308  
   309  		// PrevExist set to true
   310  		{
   311  			act: setAction{
   312  				Key:       "foo",
   313  				PrevExist: PrevExist,
   314  			},
   315  			wantURL:  "http://example.com/foo?prevExist=true",
   316  			wantBody: "value=",
   317  		},
   318  
   319  		// PrevExist set to false
   320  		{
   321  			act: setAction{
   322  				Key:       "foo",
   323  				PrevExist: PrevNoExist,
   324  			},
   325  			wantURL:  "http://example.com/foo?prevExist=false",
   326  			wantBody: "value=",
   327  		},
   328  
   329  		// PrevValue is urlencoded
   330  		{
   331  			act: setAction{
   332  				Key:       "foo",
   333  				PrevValue: "bar baz",
   334  			},
   335  			wantURL:  "http://example.com/foo?prevValue=bar+baz",
   336  			wantBody: "value=",
   337  		},
   338  
   339  		// PrevIndex is set
   340  		{
   341  			act: setAction{
   342  				Key:       "foo",
   343  				PrevIndex: uint64(12),
   344  			},
   345  			wantURL:  "http://example.com/foo?prevIndex=12",
   346  			wantBody: "value=",
   347  		},
   348  
   349  		// TTL is set
   350  		{
   351  			act: setAction{
   352  				Key: "foo",
   353  				TTL: 3 * time.Minute,
   354  			},
   355  			wantURL:  "http://example.com/foo",
   356  			wantBody: "ttl=180&value=",
   357  		},
   358  
   359  		// Refresh is set
   360  		{
   361  			act: setAction{
   362  				Key:     "foo",
   363  				TTL:     3 * time.Minute,
   364  				Refresh: true,
   365  			},
   366  			wantURL:  "http://example.com/foo",
   367  			wantBody: "refresh=true&ttl=180&value=",
   368  		},
   369  
   370  		// Dir is set
   371  		{
   372  			act: setAction{
   373  				Key: "foo",
   374  				Dir: true,
   375  			},
   376  			wantURL:  "http://example.com/foo?dir=true",
   377  			wantBody: "",
   378  		},
   379  		// Dir is set with a value
   380  		{
   381  			act: setAction{
   382  				Key:   "foo",
   383  				Value: "bar",
   384  				Dir:   true,
   385  			},
   386  			wantURL:  "http://example.com/foo?dir=true",
   387  			wantBody: "",
   388  		},
   389  		// Dir is set with PrevExist set to true
   390  		{
   391  			act: setAction{
   392  				Key:       "foo",
   393  				PrevExist: PrevExist,
   394  				Dir:       true,
   395  			},
   396  			wantURL:  "http://example.com/foo?dir=true&prevExist=true",
   397  			wantBody: "",
   398  		},
   399  		// Dir is set with PrevValue
   400  		{
   401  			act: setAction{
   402  				Key:       "foo",
   403  				PrevValue: "bar",
   404  				Dir:       true,
   405  			},
   406  			wantURL:  "http://example.com/foo?dir=true",
   407  			wantBody: "",
   408  		},
   409  		// NoValueOnSuccess is set
   410  		{
   411  			act: setAction{
   412  				Key:              "foo",
   413  				NoValueOnSuccess: true,
   414  			},
   415  			wantURL:  "http://example.com/foo?noValueOnSuccess=true",
   416  			wantBody: "value=",
   417  		},
   418  	}
   419  
   420  	for i, tt := range tests {
   421  		u, err := url.Parse(tt.wantURL)
   422  		if err != nil {
   423  			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
   424  		}
   425  
   426  		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
   427  		if err := assertRequest(*got, "PUT", u, wantHeader, []byte(tt.wantBody)); err != nil {
   428  			t.Errorf("#%d: %v", i, err)
   429  		}
   430  	}
   431  }
   432  
   433  func TestCreateInOrderAction(t *testing.T) {
   434  	wantHeader := http.Header(map[string][]string{
   435  		"Content-Type": {"application/x-www-form-urlencoded"},
   436  	})
   437  
   438  	tests := []struct {
   439  		act      createInOrderAction
   440  		wantURL  string
   441  		wantBody string
   442  	}{
   443  		// default prefix
   444  		{
   445  			act: createInOrderAction{
   446  				Prefix: defaultV2KeysPrefix,
   447  				Dir:    "foo",
   448  			},
   449  			wantURL:  "http://example.com/v2/keys/foo",
   450  			wantBody: "value=",
   451  		},
   452  
   453  		// non-default prefix
   454  		{
   455  			act: createInOrderAction{
   456  				Prefix: "/pfx",
   457  				Dir:    "foo",
   458  			},
   459  			wantURL:  "http://example.com/pfx/foo",
   460  			wantBody: "value=",
   461  		},
   462  
   463  		// no prefix
   464  		{
   465  			act: createInOrderAction{
   466  				Dir: "foo",
   467  			},
   468  			wantURL:  "http://example.com/foo",
   469  			wantBody: "value=",
   470  		},
   471  
   472  		// Key with path separators
   473  		{
   474  			act: createInOrderAction{
   475  				Prefix: defaultV2KeysPrefix,
   476  				Dir:    "foo/bar/baz",
   477  			},
   478  			wantURL:  "http://example.com/v2/keys/foo/bar/baz",
   479  			wantBody: "value=",
   480  		},
   481  
   482  		// Key with leading slash, Prefix with trailing slash
   483  		{
   484  			act: createInOrderAction{
   485  				Prefix: "/foo/",
   486  				Dir:    "/bar",
   487  			},
   488  			wantURL:  "http://example.com/foo/bar",
   489  			wantBody: "value=",
   490  		},
   491  
   492  		// Key with trailing slash
   493  		{
   494  			act: createInOrderAction{
   495  				Dir: "/foo/",
   496  			},
   497  			wantURL:  "http://example.com/foo/",
   498  			wantBody: "value=",
   499  		},
   500  
   501  		// Value is set
   502  		{
   503  			act: createInOrderAction{
   504  				Dir:   "foo",
   505  				Value: "baz",
   506  			},
   507  			wantURL:  "http://example.com/foo",
   508  			wantBody: "value=baz",
   509  		},
   510  		// TTL is set
   511  		{
   512  			act: createInOrderAction{
   513  				Dir: "foo",
   514  				TTL: 3 * time.Minute,
   515  			},
   516  			wantURL:  "http://example.com/foo",
   517  			wantBody: "ttl=180&value=",
   518  		},
   519  	}
   520  
   521  	for i, tt := range tests {
   522  		u, err := url.Parse(tt.wantURL)
   523  		if err != nil {
   524  			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
   525  		}
   526  
   527  		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
   528  		if err := assertRequest(*got, "POST", u, wantHeader, []byte(tt.wantBody)); err != nil {
   529  			t.Errorf("#%d: %v", i, err)
   530  		}
   531  	}
   532  }
   533  
   534  func TestDeleteAction(t *testing.T) {
   535  	wantHeader := http.Header(map[string][]string{
   536  		"Content-Type": {"application/x-www-form-urlencoded"},
   537  	})
   538  
   539  	tests := []struct {
   540  		act     deleteAction
   541  		wantURL string
   542  	}{
   543  		// default prefix
   544  		{
   545  			act: deleteAction{
   546  				Prefix: defaultV2KeysPrefix,
   547  				Key:    "foo",
   548  			},
   549  			wantURL: "http://example.com/v2/keys/foo",
   550  		},
   551  
   552  		// non-default prefix
   553  		{
   554  			act: deleteAction{
   555  				Prefix: "/pfx",
   556  				Key:    "foo",
   557  			},
   558  			wantURL: "http://example.com/pfx/foo",
   559  		},
   560  
   561  		// no prefix
   562  		{
   563  			act: deleteAction{
   564  				Key: "foo",
   565  			},
   566  			wantURL: "http://example.com/foo",
   567  		},
   568  
   569  		// Key with path separators
   570  		{
   571  			act: deleteAction{
   572  				Prefix: defaultV2KeysPrefix,
   573  				Key:    "foo/bar/baz",
   574  			},
   575  			wantURL: "http://example.com/v2/keys/foo/bar/baz",
   576  		},
   577  
   578  		// Key with leading slash, Prefix with trailing slash
   579  		{
   580  			act: deleteAction{
   581  				Prefix: "/foo/",
   582  				Key:    "/bar",
   583  			},
   584  			wantURL: "http://example.com/foo/bar",
   585  		},
   586  
   587  		// Key with trailing slash
   588  		{
   589  			act: deleteAction{
   590  				Key: "/foo/",
   591  			},
   592  			wantURL: "http://example.com/foo/",
   593  		},
   594  
   595  		// Recursive set to true
   596  		{
   597  			act: deleteAction{
   598  				Key:       "foo",
   599  				Recursive: true,
   600  			},
   601  			wantURL: "http://example.com/foo?recursive=true",
   602  		},
   603  
   604  		// PrevValue is urlencoded
   605  		{
   606  			act: deleteAction{
   607  				Key:       "foo",
   608  				PrevValue: "bar baz",
   609  			},
   610  			wantURL: "http://example.com/foo?prevValue=bar+baz",
   611  		},
   612  
   613  		// PrevIndex is set
   614  		{
   615  			act: deleteAction{
   616  				Key:       "foo",
   617  				PrevIndex: uint64(12),
   618  			},
   619  			wantURL: "http://example.com/foo?prevIndex=12",
   620  		},
   621  	}
   622  
   623  	for i, tt := range tests {
   624  		u, err := url.Parse(tt.wantURL)
   625  		if err != nil {
   626  			t.Errorf("#%d: unable to use wantURL fixture: %v", i, err)
   627  		}
   628  
   629  		got := tt.act.HTTPRequest(url.URL{Scheme: "http", Host: "example.com"})
   630  		if err := assertRequest(*got, "DELETE", u, wantHeader, nil); err != nil {
   631  			t.Errorf("#%d: %v", i, err)
   632  		}
   633  	}
   634  }
   635  
   636  func assertRequest(got http.Request, wantMethod string, wantURL *url.URL, wantHeader http.Header, wantBody []byte) error {
   637  	if wantMethod != got.Method {
   638  		return fmt.Errorf("want.Method=%#v got.Method=%#v", wantMethod, got.Method)
   639  	}
   640  
   641  	if !reflect.DeepEqual(wantURL, got.URL) {
   642  		return fmt.Errorf("want.URL=%#v got.URL=%#v", wantURL, got.URL)
   643  	}
   644  
   645  	if !reflect.DeepEqual(wantHeader, got.Header) {
   646  		return fmt.Errorf("want.Header=%#v got.Header=%#v", wantHeader, got.Header)
   647  	}
   648  
   649  	if got.Body == nil {
   650  		if wantBody != nil {
   651  			return fmt.Errorf("want.Body=%v got.Body=%v", wantBody, got.Body)
   652  		}
   653  	} else {
   654  		if wantBody == nil {
   655  			return fmt.Errorf("want.Body=%v got.Body=%s", wantBody, got.Body)
   656  		}
   657  		gotBytes, err := ioutil.ReadAll(got.Body)
   658  		if err != nil {
   659  			return err
   660  		}
   661  
   662  		if !reflect.DeepEqual(wantBody, gotBytes) {
   663  			return fmt.Errorf("want.Body=%s got.Body=%s", wantBody, gotBytes)
   664  		}
   665  	}
   666  
   667  	return nil
   668  }
   669  
   670  func TestUnmarshalSuccessfulResponse(t *testing.T) {
   671  	var expiration time.Time
   672  	expiration.UnmarshalText([]byte("2015-04-07T04:40:23.044979686Z"))
   673  
   674  	tests := []struct {
   675  		indexHdr     string
   676  		clusterIDHdr string
   677  		body         string
   678  		wantRes      *Response
   679  		wantErr      bool
   680  	}{
   681  		// Neither PrevNode or Node
   682  		{
   683  			indexHdr: "1",
   684  			body:     `{"action":"delete"}`,
   685  			wantRes:  &Response{Action: "delete", Index: 1},
   686  			wantErr:  false,
   687  		},
   688  
   689  		// PrevNode
   690  		{
   691  			indexHdr: "15",
   692  			body:     `{"action":"delete", "prevNode": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
   693  			wantRes: &Response{
   694  				Action: "delete",
   695  				Index:  15,
   696  				Node:   nil,
   697  				PrevNode: &Node{
   698  					Key:           "/foo",
   699  					Value:         "bar",
   700  					ModifiedIndex: 12,
   701  					CreatedIndex:  10,
   702  				},
   703  			},
   704  			wantErr: false,
   705  		},
   706  
   707  		// Node
   708  		{
   709  			indexHdr: "15",
   710  			body:     `{"action":"get", "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10, "ttl": 10, "expiration": "2015-04-07T04:40:23.044979686Z"}}`,
   711  			wantRes: &Response{
   712  				Action: "get",
   713  				Index:  15,
   714  				Node: &Node{
   715  					Key:           "/foo",
   716  					Value:         "bar",
   717  					ModifiedIndex: 12,
   718  					CreatedIndex:  10,
   719  					TTL:           10,
   720  					Expiration:    &expiration,
   721  				},
   722  				PrevNode: nil,
   723  			},
   724  			wantErr: false,
   725  		},
   726  
   727  		// Node Dir
   728  		{
   729  			indexHdr:     "15",
   730  			clusterIDHdr: "abcdef",
   731  			body:         `{"action":"get", "node": {"key": "/foo", "dir": true, "modifiedIndex": 12, "createdIndex": 10}}`,
   732  			wantRes: &Response{
   733  				Action: "get",
   734  				Index:  15,
   735  				Node: &Node{
   736  					Key:           "/foo",
   737  					Dir:           true,
   738  					ModifiedIndex: 12,
   739  					CreatedIndex:  10,
   740  				},
   741  				PrevNode:  nil,
   742  				ClusterID: "abcdef",
   743  			},
   744  			wantErr: false,
   745  		},
   746  
   747  		// PrevNode and Node
   748  		{
   749  			indexHdr: "15",
   750  			body:     `{"action":"update", "prevNode": {"key": "/foo", "value": "baz", "modifiedIndex": 10, "createdIndex": 10}, "node": {"key": "/foo", "value": "bar", "modifiedIndex": 12, "createdIndex": 10}}`,
   751  			wantRes: &Response{
   752  				Action: "update",
   753  				Index:  15,
   754  				PrevNode: &Node{
   755  					Key:           "/foo",
   756  					Value:         "baz",
   757  					ModifiedIndex: 10,
   758  					CreatedIndex:  10,
   759  				},
   760  				Node: &Node{
   761  					Key:           "/foo",
   762  					Value:         "bar",
   763  					ModifiedIndex: 12,
   764  					CreatedIndex:  10,
   765  				},
   766  			},
   767  			wantErr: false,
   768  		},
   769  
   770  		// Garbage in body
   771  		{
   772  			indexHdr: "",
   773  			body:     `garbage`,
   774  			wantRes:  nil,
   775  			wantErr:  true,
   776  		},
   777  
   778  		// non-integer index
   779  		{
   780  			indexHdr: "poo",
   781  			body:     `{}`,
   782  			wantRes:  nil,
   783  			wantErr:  true,
   784  		},
   785  	}
   786  
   787  	for i, tt := range tests {
   788  		h := make(http.Header)
   789  		h.Add("X-Etcd-Index", tt.indexHdr)
   790  		res, err := unmarshalSuccessfulKeysResponse(h, []byte(tt.body))
   791  		if tt.wantErr != (err != nil) {
   792  			t.Errorf("#%d: wantErr=%t, err=%v", i, tt.wantErr, err)
   793  		}
   794  
   795  		if (res == nil) != (tt.wantRes == nil) {
   796  			t.Errorf("#%d: received res=%#v, but expected res=%#v", i, res, tt.wantRes)
   797  			continue
   798  		} else if tt.wantRes == nil {
   799  			// expected and successfully got nil response
   800  			continue
   801  		}
   802  
   803  		if res.Action != tt.wantRes.Action {
   804  			t.Errorf("#%d: Action=%s, expected %s", i, res.Action, tt.wantRes.Action)
   805  		}
   806  		if res.Index != tt.wantRes.Index {
   807  			t.Errorf("#%d: Index=%d, expected %d", i, res.Index, tt.wantRes.Index)
   808  		}
   809  		if !reflect.DeepEqual(res.Node, tt.wantRes.Node) {
   810  			t.Errorf("#%d: Node=%v, expected %v", i, res.Node, tt.wantRes.Node)
   811  		}
   812  	}
   813  }
   814  
   815  func TestUnmarshalFailedKeysResponse(t *testing.T) {
   816  	body := []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`)
   817  
   818  	wantErr := Error{
   819  		Code:    100,
   820  		Message: "Key not found",
   821  		Cause:   "/foo",
   822  		Index:   uint64(18),
   823  	}
   824  
   825  	gotErr := unmarshalFailedKeysResponse(body)
   826  	if !reflect.DeepEqual(wantErr, gotErr) {
   827  		t.Errorf("unexpected error: want=%#v got=%#v", wantErr, gotErr)
   828  	}
   829  }
   830  
   831  func TestUnmarshalFailedKeysResponseBadJSON(t *testing.T) {
   832  	err := unmarshalFailedKeysResponse([]byte(`{"er`))
   833  	if err == nil {
   834  		t.Errorf("got nil error")
   835  	} else if _, ok := err.(Error); ok {
   836  		t.Errorf("error is of incorrect type *Error: %#v", err)
   837  	}
   838  }
   839  
   840  func TestHTTPWatcherNextWaitAction(t *testing.T) {
   841  	initAction := waitAction{
   842  		Prefix:    "/pants",
   843  		Key:       "/foo/bar",
   844  		Recursive: true,
   845  		WaitIndex: 19,
   846  	}
   847  
   848  	client := &actionAssertingHTTPClient{
   849  		t:   t,
   850  		act: &initAction,
   851  		resp: http.Response{
   852  			StatusCode: http.StatusOK,
   853  			Header:     http.Header{"X-Etcd-Index": []string{"42"}},
   854  		},
   855  		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}}`),
   856  	}
   857  
   858  	wantResponse := &Response{
   859  		Action:   "update",
   860  		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(21)},
   861  		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
   862  		Index:    uint64(42),
   863  	}
   864  
   865  	wantNextWait := waitAction{
   866  		Prefix:    "/pants",
   867  		Key:       "/foo/bar",
   868  		Recursive: true,
   869  		WaitIndex: 22,
   870  	}
   871  
   872  	watcher := &httpWatcher{
   873  		client:   client,
   874  		nextWait: initAction,
   875  	}
   876  
   877  	resp, err := watcher.Next(context.Background())
   878  	if err != nil {
   879  		t.Errorf("non-nil error: %#v", err)
   880  	}
   881  
   882  	if !reflect.DeepEqual(wantResponse, resp) {
   883  		t.Errorf("received incorrect Response: want=%#v got=%#v", wantResponse, resp)
   884  	}
   885  
   886  	if !reflect.DeepEqual(wantNextWait, watcher.nextWait) {
   887  		t.Errorf("nextWait incorrect: want=%#v got=%#v", wantNextWait, watcher.nextWait)
   888  	}
   889  }
   890  
   891  func TestHTTPWatcherNextFail(t *testing.T) {
   892  	tests := []httpClient{
   893  		// generic HTTP client failure
   894  		&staticHTTPClient{
   895  			err: errors.New("fail!"),
   896  		},
   897  
   898  		// unusable status code
   899  		&staticHTTPClient{
   900  			resp: http.Response{
   901  				StatusCode: http.StatusTeapot,
   902  			},
   903  		},
   904  
   905  		// etcd Error response
   906  		&staticHTTPClient{
   907  			resp: http.Response{
   908  				StatusCode: http.StatusNotFound,
   909  			},
   910  			body: []byte(`{"errorCode":100,"message":"Key not found","cause":"/foo","index":18}`),
   911  		},
   912  	}
   913  
   914  	for i, tt := range tests {
   915  		act := waitAction{
   916  			Prefix:    "/pants",
   917  			Key:       "/foo/bar",
   918  			Recursive: true,
   919  			WaitIndex: 19,
   920  		}
   921  
   922  		watcher := &httpWatcher{
   923  			client:   tt,
   924  			nextWait: act,
   925  		}
   926  
   927  		resp, err := watcher.Next(context.Background())
   928  		if err == nil {
   929  			t.Errorf("#%d: expected non-nil error", i)
   930  		}
   931  		if resp != nil {
   932  			t.Errorf("#%d: expected nil Response, got %#v", i, resp)
   933  		}
   934  		if !reflect.DeepEqual(act, watcher.nextWait) {
   935  			t.Errorf("#%d: nextWait changed: want=%#v got=%#v", i, act, watcher.nextWait)
   936  		}
   937  	}
   938  }
   939  
   940  func TestHTTPKeysAPIWatcherAction(t *testing.T) {
   941  	tests := []struct {
   942  		key  string
   943  		opts *WatcherOptions
   944  		want waitAction
   945  	}{
   946  		{
   947  			key:  "/foo",
   948  			opts: nil,
   949  			want: waitAction{
   950  				Key:       "/foo",
   951  				Recursive: false,
   952  				WaitIndex: 0,
   953  			},
   954  		},
   955  
   956  		{
   957  			key: "/foo",
   958  			opts: &WatcherOptions{
   959  				Recursive:  false,
   960  				AfterIndex: 0,
   961  			},
   962  			want: waitAction{
   963  				Key:       "/foo",
   964  				Recursive: false,
   965  				WaitIndex: 0,
   966  			},
   967  		},
   968  
   969  		{
   970  			key: "/foo",
   971  			opts: &WatcherOptions{
   972  				Recursive:  true,
   973  				AfterIndex: 0,
   974  			},
   975  			want: waitAction{
   976  				Key:       "/foo",
   977  				Recursive: true,
   978  				WaitIndex: 0,
   979  			},
   980  		},
   981  
   982  		{
   983  			key: "/foo",
   984  			opts: &WatcherOptions{
   985  				Recursive:  false,
   986  				AfterIndex: 19,
   987  			},
   988  			want: waitAction{
   989  				Key:       "/foo",
   990  				Recursive: false,
   991  				WaitIndex: 20,
   992  			},
   993  		},
   994  	}
   995  
   996  	for i, tt := range tests {
   997  		kAPI := &httpKeysAPI{
   998  			client: &staticHTTPClient{err: errors.New("fail!")},
   999  		}
  1000  
  1001  		want := &httpWatcher{
  1002  			client:   &staticHTTPClient{err: errors.New("fail!")},
  1003  			nextWait: tt.want,
  1004  		}
  1005  
  1006  		got := kAPI.Watcher(tt.key, tt.opts)
  1007  		if !reflect.DeepEqual(want, got) {
  1008  			t.Errorf("#%d: incorrect watcher: want=%#v got=%#v", i, want, got)
  1009  		}
  1010  	}
  1011  }
  1012  
  1013  func TestHTTPKeysAPISetAction(t *testing.T) {
  1014  	tests := []struct {
  1015  		key        string
  1016  		value      string
  1017  		opts       *SetOptions
  1018  		wantAction httpAction
  1019  	}{
  1020  		// nil SetOptions
  1021  		{
  1022  			key:   "/foo",
  1023  			value: "bar",
  1024  			opts:  nil,
  1025  			wantAction: &setAction{
  1026  				Key:       "/foo",
  1027  				Value:     "bar",
  1028  				PrevValue: "",
  1029  				PrevIndex: 0,
  1030  				PrevExist: PrevIgnore,
  1031  				TTL:       0,
  1032  			},
  1033  		},
  1034  		// empty SetOptions
  1035  		{
  1036  			key:   "/foo",
  1037  			value: "bar",
  1038  			opts:  &SetOptions{},
  1039  			wantAction: &setAction{
  1040  				Key:       "/foo",
  1041  				Value:     "bar",
  1042  				PrevValue: "",
  1043  				PrevIndex: 0,
  1044  				PrevExist: PrevIgnore,
  1045  				TTL:       0,
  1046  			},
  1047  		},
  1048  		// populated SetOptions
  1049  		{
  1050  			key:   "/foo",
  1051  			value: "bar",
  1052  			opts: &SetOptions{
  1053  				PrevValue: "baz",
  1054  				PrevIndex: 13,
  1055  				PrevExist: PrevExist,
  1056  				TTL:       time.Minute,
  1057  				Dir:       true,
  1058  			},
  1059  			wantAction: &setAction{
  1060  				Key:       "/foo",
  1061  				Value:     "bar",
  1062  				PrevValue: "baz",
  1063  				PrevIndex: 13,
  1064  				PrevExist: PrevExist,
  1065  				TTL:       time.Minute,
  1066  				Dir:       true,
  1067  			},
  1068  		},
  1069  	}
  1070  
  1071  	for i, tt := range tests {
  1072  		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
  1073  		kAPI := httpKeysAPI{client: client}
  1074  		kAPI.Set(context.Background(), tt.key, tt.value, tt.opts)
  1075  	}
  1076  }
  1077  
  1078  func TestHTTPKeysAPISetError(t *testing.T) {
  1079  	tests := []httpClient{
  1080  		// generic HTTP client failure
  1081  		&staticHTTPClient{
  1082  			err: errors.New("fail!"),
  1083  		},
  1084  
  1085  		// unusable status code
  1086  		&staticHTTPClient{
  1087  			resp: http.Response{
  1088  				StatusCode: http.StatusTeapot,
  1089  			},
  1090  		},
  1091  
  1092  		// etcd Error response
  1093  		&staticHTTPClient{
  1094  			resp: http.Response{
  1095  				StatusCode: http.StatusInternalServerError,
  1096  			},
  1097  			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
  1098  		},
  1099  	}
  1100  
  1101  	for i, tt := range tests {
  1102  		kAPI := httpKeysAPI{client: tt}
  1103  		resp, err := kAPI.Set(context.Background(), "/foo", "bar", nil)
  1104  		if err == nil {
  1105  			t.Errorf("#%d: received nil error", i)
  1106  		}
  1107  		if resp != nil {
  1108  			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
  1109  		}
  1110  	}
  1111  }
  1112  
  1113  func TestHTTPKeysAPISetResponse(t *testing.T) {
  1114  	client := &staticHTTPClient{
  1115  		resp: http.Response{
  1116  			StatusCode: http.StatusOK,
  1117  			Header:     http.Header{"X-Etcd-Index": []string{"21"}},
  1118  		},
  1119  		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}}`),
  1120  	}
  1121  
  1122  	wantResponse := &Response{
  1123  		Action:   "set",
  1124  		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(21), ModifiedIndex: uint64(21)},
  1125  		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
  1126  		Index:    uint64(21),
  1127  	}
  1128  
  1129  	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
  1130  	resp, err := kAPI.Set(context.Background(), "/foo/bar/baz", "snarf", nil)
  1131  	if err != nil {
  1132  		t.Errorf("non-nil error: %#v", err)
  1133  	}
  1134  	if !reflect.DeepEqual(wantResponse, resp) {
  1135  		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
  1136  	}
  1137  }
  1138  
  1139  func TestHTTPKeysAPIGetAction(t *testing.T) {
  1140  	tests := []struct {
  1141  		key        string
  1142  		opts       *GetOptions
  1143  		wantAction httpAction
  1144  	}{
  1145  		// nil GetOptions
  1146  		{
  1147  			key:  "/foo",
  1148  			opts: nil,
  1149  			wantAction: &getAction{
  1150  				Key:       "/foo",
  1151  				Sorted:    false,
  1152  				Recursive: false,
  1153  			},
  1154  		},
  1155  		// empty GetOptions
  1156  		{
  1157  			key:  "/foo",
  1158  			opts: &GetOptions{},
  1159  			wantAction: &getAction{
  1160  				Key:       "/foo",
  1161  				Sorted:    false,
  1162  				Recursive: false,
  1163  			},
  1164  		},
  1165  		// populated GetOptions
  1166  		{
  1167  			key: "/foo",
  1168  			opts: &GetOptions{
  1169  				Sort:      true,
  1170  				Recursive: true,
  1171  				Quorum:    true,
  1172  			},
  1173  			wantAction: &getAction{
  1174  				Key:       "/foo",
  1175  				Sorted:    true,
  1176  				Recursive: true,
  1177  				Quorum:    true,
  1178  			},
  1179  		},
  1180  	}
  1181  
  1182  	for i, tt := range tests {
  1183  		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
  1184  		kAPI := httpKeysAPI{client: client}
  1185  		kAPI.Get(context.Background(), tt.key, tt.opts)
  1186  	}
  1187  }
  1188  
  1189  func TestHTTPKeysAPIGetError(t *testing.T) {
  1190  	tests := []httpClient{
  1191  		// generic HTTP client failure
  1192  		&staticHTTPClient{
  1193  			err: errors.New("fail!"),
  1194  		},
  1195  
  1196  		// unusable status code
  1197  		&staticHTTPClient{
  1198  			resp: http.Response{
  1199  				StatusCode: http.StatusTeapot,
  1200  			},
  1201  		},
  1202  
  1203  		// etcd Error response
  1204  		&staticHTTPClient{
  1205  			resp: http.Response{
  1206  				StatusCode: http.StatusInternalServerError,
  1207  			},
  1208  			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
  1209  		},
  1210  	}
  1211  
  1212  	for i, tt := range tests {
  1213  		kAPI := httpKeysAPI{client: tt}
  1214  		resp, err := kAPI.Get(context.Background(), "/foo", nil)
  1215  		if err == nil {
  1216  			t.Errorf("#%d: received nil error", i)
  1217  		}
  1218  		if resp != nil {
  1219  			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
  1220  		}
  1221  	}
  1222  }
  1223  
  1224  func TestHTTPKeysAPIGetResponse(t *testing.T) {
  1225  	client := &staticHTTPClient{
  1226  		resp: http.Response{
  1227  			StatusCode: http.StatusOK,
  1228  			Header:     http.Header{"X-Etcd-Index": []string{"42"}},
  1229  		},
  1230  		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}]}}`),
  1231  	}
  1232  
  1233  	wantResponse := &Response{
  1234  		Action: "get",
  1235  		Node: &Node{
  1236  			Key: "/pants/foo/bar",
  1237  			Nodes: []*Node{
  1238  				{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: 21, ModifiedIndex: 25},
  1239  			},
  1240  			CreatedIndex:  uint64(19),
  1241  			ModifiedIndex: uint64(25),
  1242  		},
  1243  		Index: uint64(42),
  1244  	}
  1245  
  1246  	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
  1247  	resp, err := kAPI.Get(context.Background(), "/foo/bar", &GetOptions{Recursive: true})
  1248  	if err != nil {
  1249  		t.Errorf("non-nil error: %#v", err)
  1250  	}
  1251  	if !reflect.DeepEqual(wantResponse, resp) {
  1252  		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
  1253  	}
  1254  }
  1255  
  1256  func TestHTTPKeysAPIDeleteAction(t *testing.T) {
  1257  	tests := []struct {
  1258  		key        string
  1259  		opts       *DeleteOptions
  1260  		wantAction httpAction
  1261  	}{
  1262  		// nil DeleteOptions
  1263  		{
  1264  			key:  "/foo",
  1265  			opts: nil,
  1266  			wantAction: &deleteAction{
  1267  				Key:       "/foo",
  1268  				PrevValue: "",
  1269  				PrevIndex: 0,
  1270  				Recursive: false,
  1271  			},
  1272  		},
  1273  		// empty DeleteOptions
  1274  		{
  1275  			key:  "/foo",
  1276  			opts: &DeleteOptions{},
  1277  			wantAction: &deleteAction{
  1278  				Key:       "/foo",
  1279  				PrevValue: "",
  1280  				PrevIndex: 0,
  1281  				Recursive: false,
  1282  			},
  1283  		},
  1284  		// populated DeleteOptions
  1285  		{
  1286  			key: "/foo",
  1287  			opts: &DeleteOptions{
  1288  				PrevValue: "baz",
  1289  				PrevIndex: 13,
  1290  				Recursive: true,
  1291  			},
  1292  			wantAction: &deleteAction{
  1293  				Key:       "/foo",
  1294  				PrevValue: "baz",
  1295  				PrevIndex: 13,
  1296  				Recursive: true,
  1297  			},
  1298  		},
  1299  	}
  1300  
  1301  	for i, tt := range tests {
  1302  		client := &actionAssertingHTTPClient{t: t, num: i, act: tt.wantAction}
  1303  		kAPI := httpKeysAPI{client: client}
  1304  		kAPI.Delete(context.Background(), tt.key, tt.opts)
  1305  	}
  1306  }
  1307  
  1308  func TestHTTPKeysAPIDeleteError(t *testing.T) {
  1309  	tests := []httpClient{
  1310  		// generic HTTP client failure
  1311  		&staticHTTPClient{
  1312  			err: errors.New("fail!"),
  1313  		},
  1314  
  1315  		// unusable status code
  1316  		&staticHTTPClient{
  1317  			resp: http.Response{
  1318  				StatusCode: http.StatusTeapot,
  1319  			},
  1320  		},
  1321  
  1322  		// etcd Error response
  1323  		&staticHTTPClient{
  1324  			resp: http.Response{
  1325  				StatusCode: http.StatusInternalServerError,
  1326  			},
  1327  			body: []byte(`{"errorCode":300,"message":"Raft internal error","cause":"/foo","index":18}`),
  1328  		},
  1329  	}
  1330  
  1331  	for i, tt := range tests {
  1332  		kAPI := httpKeysAPI{client: tt}
  1333  		resp, err := kAPI.Delete(context.Background(), "/foo", nil)
  1334  		if err == nil {
  1335  			t.Errorf("#%d: received nil error", i)
  1336  		}
  1337  		if resp != nil {
  1338  			t.Errorf("#%d: received non-nil Response: %#v", i, resp)
  1339  		}
  1340  	}
  1341  }
  1342  
  1343  func TestHTTPKeysAPIDeleteResponse(t *testing.T) {
  1344  	client := &staticHTTPClient{
  1345  		resp: http.Response{
  1346  			StatusCode: http.StatusOK,
  1347  			Header:     http.Header{"X-Etcd-Index": []string{"22"}},
  1348  		},
  1349  		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}}`),
  1350  	}
  1351  
  1352  	wantResponse := &Response{
  1353  		Action:   "delete",
  1354  		Node:     &Node{Key: "/pants/foo/bar/baz", Value: "snarf", CreatedIndex: uint64(19), ModifiedIndex: uint64(22)},
  1355  		PrevNode: &Node{Key: "/pants/foo/bar/baz", Value: "snazz", CreatedIndex: uint64(19), ModifiedIndex: uint64(20)},
  1356  		Index:    uint64(22),
  1357  	}
  1358  
  1359  	kAPI := &httpKeysAPI{client: client, prefix: "/pants"}
  1360  	resp, err := kAPI.Delete(context.Background(), "/foo/bar/baz", nil)
  1361  	if err != nil {
  1362  		t.Errorf("non-nil error: %#v", err)
  1363  	}
  1364  	if !reflect.DeepEqual(wantResponse, resp) {
  1365  		t.Errorf("incorrect Response: want=%#v got=%#v", wantResponse, resp)
  1366  	}
  1367  }
  1368  
  1369  func TestHTTPKeysAPICreateAction(t *testing.T) {
  1370  	act := &setAction{
  1371  		Key:       "/foo",
  1372  		Value:     "bar",
  1373  		PrevExist: PrevNoExist,
  1374  		PrevIndex: 0,
  1375  		PrevValue: "",
  1376  		TTL:       0,
  1377  	}
  1378  
  1379  	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
  1380  	kAPI.Create(context.Background(), "/foo", "bar")
  1381  }
  1382  
  1383  func TestHTTPKeysAPICreateInOrderAction(t *testing.T) {
  1384  	act := &createInOrderAction{
  1385  		Dir:   "/foo",
  1386  		Value: "bar",
  1387  		TTL:   0,
  1388  	}
  1389  	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
  1390  	kAPI.CreateInOrder(context.Background(), "/foo", "bar", nil)
  1391  }
  1392  
  1393  func TestHTTPKeysAPIUpdateAction(t *testing.T) {
  1394  	act := &setAction{
  1395  		Key:       "/foo",
  1396  		Value:     "bar",
  1397  		PrevExist: PrevExist,
  1398  		PrevIndex: 0,
  1399  		PrevValue: "",
  1400  		TTL:       0,
  1401  	}
  1402  
  1403  	kAPI := httpKeysAPI{client: &actionAssertingHTTPClient{t: t, act: act}}
  1404  	kAPI.Update(context.Background(), "/foo", "bar")
  1405  }
  1406  
  1407  func TestNodeTTLDuration(t *testing.T) {
  1408  	tests := []struct {
  1409  		node *Node
  1410  		want time.Duration
  1411  	}{
  1412  		{
  1413  			node: &Node{TTL: 0},
  1414  			want: 0,
  1415  		},
  1416  		{
  1417  			node: &Node{TTL: 97},
  1418  			want: 97 * time.Second,
  1419  		},
  1420  	}
  1421  
  1422  	for i, tt := range tests {
  1423  		got := tt.node.TTLDuration()
  1424  		if tt.want != got {
  1425  			t.Errorf("#%d: incorrect duration: want=%v got=%v", i, tt.want, got)
  1426  		}
  1427  	}
  1428  }