// 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 integration import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "strings" "testing" "time" "go.etcd.io/etcd/pkg/testutil" "go.etcd.io/etcd/pkg/transport" ) func TestV2Set(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() v := url.Values{} v.Set("value", "bar") vAndNoValue := url.Values{} vAndNoValue.Set("value", "bar") vAndNoValue.Set("noValueOnSuccess", "true") tests := []struct { relativeURL string value url.Values wStatus int w string }{ { "/v2/keys/foo/bar", v, http.StatusCreated, `{"action":"set","node":{"key":"/foo/bar","value":"bar","modifiedIndex":4,"createdIndex":4}}`, }, { "/v2/keys/foodir?dir=true", url.Values{}, http.StatusCreated, `{"action":"set","node":{"key":"/foodir","dir":true,"modifiedIndex":5,"createdIndex":5}}`, }, { "/v2/keys/fooempty", url.Values(map[string][]string{"value": {""}}), http.StatusCreated, `{"action":"set","node":{"key":"/fooempty","value":"","modifiedIndex":6,"createdIndex":6}}`, }, { "/v2/keys/foo/novalue", vAndNoValue, http.StatusCreated, `{"action":"set"}`, }, } for i, tt := range tests { resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) if err != nil { t.Errorf("#%d: err = %v, want nil", i, err) } g := string(tc.ReadBody(resp)) w := tt.w + "\n" if g != w { t.Errorf("#%d: body = %v, want %v", i, g, w) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } } } func TestV2CreateUpdate(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() tests := []struct { relativeURL string value url.Values wStatus int w map[string]interface{} }{ // key with ttl { "/v2/keys/ttl/foo", url.Values(map[string][]string{"value": {"XXX"}, "ttl": {"20"}}), http.StatusCreated, map[string]interface{}{ "node": map[string]interface{}{ "value": "XXX", "ttl": float64(20), }, }, }, // key with bad ttl { "/v2/keys/ttl/foo", url.Values(map[string][]string{"value": {"XXX"}, "ttl": {"bad_ttl"}}), http.StatusBadRequest, map[string]interface{}{ "errorCode": float64(202), "message": "The given TTL in POST form is not a number", }, }, // create key { "/v2/keys/create/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}}), http.StatusCreated, map[string]interface{}{ "node": map[string]interface{}{ "value": "XXX", }, }, }, // created key failed { "/v2/keys/create/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(105), "message": "Key already exists", "cause": "/create/foo", }, }, // update the newly created key with ttl { "/v2/keys/create/foo", url.Values(map[string][]string{"value": {"YYY"}, "prevExist": {"true"}, "ttl": {"20"}}), http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "value": "YYY", "ttl": float64(20), }, "action": "update", }, }, // update the ttl to none { "/v2/keys/create/foo", url.Values(map[string][]string{"value": {"ZZZ"}, "prevExist": {"true"}}), http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "value": "ZZZ", }, "action": "update", }, }, // update on a non-existing key { "/v2/keys/nonexist", url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"true"}}), http.StatusNotFound, map[string]interface{}{ "errorCode": float64(100), "message": "Key not found", "cause": "/nonexist", }, }, // create with no value on success { "/v2/keys/create/novalue", url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}, "noValueOnSuccess": {"true"}}), http.StatusCreated, map[string]interface{}{}, }, // update with no value on success { "/v2/keys/create/novalue", url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"true"}, "noValueOnSuccess": {"true"}}), http.StatusOK, map[string]interface{}{}, }, // created key failed with no value on success { "/v2/keys/create/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevExist": {"false"}, "noValueOnSuccess": {"true"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(105), "message": "Key already exists", "cause": "/create/foo", }, }, } for i, tt := range tests { resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) if err != nil { t.Fatalf("#%d: put err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2CAS(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() tests := []struct { relativeURL string value url.Values wStatus int w map[string]interface{} }{ { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"XXX"}}), http.StatusCreated, nil, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"4"}}), http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "value": "YYY", "modifiedIndex": float64(5), }, "action": "compareAndSwap", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"10"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[10 != 5]", "index": float64(5), }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"bad_index"}}), http.StatusBadRequest, map[string]interface{}{ "errorCode": float64(203), "message": "The given index in POST form is not a number", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"ZZZ"}, "prevValue": {"YYY"}}), http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "value": "ZZZ", }, "action": "compareAndSwap", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[bad_value != ZZZ]", }, }, // prevValue is required { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {""}}), http.StatusBadRequest, map[string]interface{}{ "errorCode": float64(201), }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}, "prevIndex": {"100"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[bad_value != ZZZ] [100 != 6]", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"ZZZ"}, "prevIndex": {"100"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[100 != 6]", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"XXX"}, "prevValue": {"bad_value"}, "prevIndex": {"6"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[bad_value != ZZZ]", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"6"}, "noValueOnSuccess": {"true"}}), http.StatusOK, map[string]interface{}{ "action": "compareAndSwap", }, }, { "/v2/keys/cas/foo", url.Values(map[string][]string{"value": {"YYY"}, "prevIndex": {"10"}, "noValueOnSuccess": {"true"}}), http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[10 != 7]", "index": float64(7), }, }, } for i, tt := range tests { resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) if err != nil { t.Fatalf("#%d: put err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2Delete(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() v := url.Values{} v.Set("value", "XXX") r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v) if err != nil { t.Error(err) } r.Body.Close() r, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/emptydir?dir=true"), v) if err != nil { t.Error(err) } r.Body.Close() r, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foodir/bar?dir=true"), v) if err != nil { t.Error(err) } r.Body.Close() tests := []struct { relativeURL string wStatus int w map[string]interface{} }{ { "/v2/keys/foo", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo", }, "prevNode": map[string]interface{}{ "key": "/foo", "value": "XXX", }, "action": "delete", }, }, { "/v2/keys/emptydir", http.StatusForbidden, map[string]interface{}{ "errorCode": float64(102), "message": "Not a file", "cause": "/emptydir", }, }, { "/v2/keys/emptydir?dir=true", http.StatusOK, nil, }, { "/v2/keys/foodir?dir=true", http.StatusForbidden, map[string]interface{}{ "errorCode": float64(108), "message": "Directory not empty", "cause": "/foodir", }, }, { "/v2/keys/foodir?recursive=true", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foodir", "dir": true, }, "prevNode": map[string]interface{}{ "key": "/foodir", "dir": true, }, "action": "delete", }, }, } for i, tt := range tests { resp, err := tc.DeleteForm(fmt.Sprintf("%s%s", u, tt.relativeURL), nil) if err != nil { t.Fatalf("#%d: delete err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2CAD(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() v := url.Values{} v.Set("value", "XXX") r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo"), v) if err != nil { t.Error(err) } r.Body.Close() r, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foovalue"), v) if err != nil { t.Error(err) } r.Body.Close() tests := []struct { relativeURL string wStatus int w map[string]interface{} }{ { "/v2/keys/foo?prevIndex=100", http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[100 != 4]", }, }, { "/v2/keys/foo?prevIndex=bad_index", http.StatusBadRequest, map[string]interface{}{ "errorCode": float64(203), "message": "The given index in POST form is not a number", }, }, { "/v2/keys/foo?prevIndex=4", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo", "modifiedIndex": float64(6), }, "action": "compareAndDelete", }, }, { "/v2/keys/foovalue?prevValue=YYY", http.StatusPreconditionFailed, map[string]interface{}{ "errorCode": float64(101), "message": "Compare failed", "cause": "[YYY != XXX]", }, }, { "/v2/keys/foovalue?prevValue=", http.StatusBadRequest, map[string]interface{}{ "errorCode": float64(201), "cause": `"prevValue" cannot be empty`, }, }, { "/v2/keys/foovalue?prevValue=XXX", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foovalue", "modifiedIndex": float64(7), }, "action": "compareAndDelete", }, }, } for i, tt := range tests { resp, err := tc.DeleteForm(fmt.Sprintf("%s%s", u, tt.relativeURL), nil) if err != nil { t.Fatalf("#%d: delete err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2Unique(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() tests := []struct { relativeURL string value url.Values wStatus int w map[string]interface{} }{ { "/v2/keys/foo", url.Values(map[string][]string{"value": {"XXX"}}), http.StatusCreated, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo/00000000000000000004", "value": "XXX", }, "action": "create", }, }, { "/v2/keys/foo", url.Values(map[string][]string{"value": {"XXX"}}), http.StatusCreated, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo/00000000000000000005", "value": "XXX", }, "action": "create", }, }, { "/v2/keys/bar", url.Values(map[string][]string{"value": {"XXX"}}), http.StatusCreated, map[string]interface{}{ "node": map[string]interface{}{ "key": "/bar/00000000000000000006", "value": "XXX", }, "action": "create", }, }, } for i, tt := range tests { resp, err := tc.PostForm(fmt.Sprintf("%s%s", u, tt.relativeURL), tt.value) if err != nil { t.Fatalf("#%d: post err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2Get(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() v := url.Values{} v.Set("value", "XXX") r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar/zar"), v) if err != nil { t.Error(err) } r.Body.Close() tests := []struct { relativeURL string wStatus int w map[string]interface{} }{ { "/v2/keys/foo/bar/zar", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo/bar/zar", "value": "XXX", }, "action": "get", }, }, { "/v2/keys/foo", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo", "dir": true, "nodes": []interface{}{ map[string]interface{}{ "key": "/foo/bar", "dir": true, "createdIndex": float64(4), "modifiedIndex": float64(4), }, }, }, "action": "get", }, }, { "/v2/keys/foo?recursive=true", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo", "dir": true, "nodes": []interface{}{ map[string]interface{}{ "key": "/foo/bar", "dir": true, "createdIndex": float64(4), "modifiedIndex": float64(4), "nodes": []interface{}{ map[string]interface{}{ "key": "/foo/bar/zar", "value": "XXX", "createdIndex": float64(4), "modifiedIndex": float64(4), }, }, }, }, }, "action": "get", }, }, } for i, tt := range tests { resp, err := tc.Get(fmt.Sprintf("%s%s", u, tt.relativeURL)) if err != nil { t.Fatalf("#%d: get err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if resp.Header.Get("Content-Type") != "application/json" { t.Errorf("#%d: header = %v, want %v", i, resp.Header.Get("Content-Type"), "application/json") } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2QuorumGet(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() v := url.Values{} v.Set("value", "XXX") r, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar/zar?quorum=true"), v) if err != nil { t.Error(err) } r.Body.Close() tests := []struct { relativeURL string wStatus int w map[string]interface{} }{ { "/v2/keys/foo/bar/zar", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo/bar/zar", "value": "XXX", }, "action": "get", }, }, { "/v2/keys/foo", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo", "dir": true, "nodes": []interface{}{ map[string]interface{}{ "key": "/foo/bar", "dir": true, "createdIndex": float64(4), "modifiedIndex": float64(4), }, }, }, "action": "get", }, }, { "/v2/keys/foo?recursive=true", http.StatusOK, map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo", "dir": true, "nodes": []interface{}{ map[string]interface{}{ "key": "/foo/bar", "dir": true, "createdIndex": float64(4), "modifiedIndex": float64(4), "nodes": []interface{}{ map[string]interface{}{ "key": "/foo/bar/zar", "value": "XXX", "createdIndex": float64(4), "modifiedIndex": float64(4), }, }, }, }, }, "action": "get", }, }, } for i, tt := range tests { resp, err := tc.Get(fmt.Sprintf("%s%s", u, tt.relativeURL)) if err != nil { t.Fatalf("#%d: get err = %v, want nil", i, err) } if resp.StatusCode != tt.wStatus { t.Errorf("#%d: status = %d, want %d", i, resp.StatusCode, tt.wStatus) } if resp.Header.Get("Content-Type") != "application/json" { t.Errorf("#%d: header = %v, want %v", i, resp.Header.Get("Content-Type"), "application/json") } if err := checkBody(tc.ReadBodyJSON(resp), tt.w); err != nil { t.Errorf("#%d: %v", i, err) } } } func TestV2Watch(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() watchResp, err := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar?wait=true")) if err != nil { t.Fatalf("watch err = %v, want nil", err) } // Set a value. v := url.Values{} v.Set("value", "XXX") resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v) if err != nil { t.Fatalf("put err = %v, want nil", err) } resp.Body.Close() body := tc.ReadBodyJSON(watchResp) w := map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo/bar", "value": "XXX", "modifiedIndex": float64(4), }, "action": "set", } if err := checkBody(body, w); err != nil { t.Error(err) } } func TestV2WatchWithIndex(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() var body map[string]interface{} c := make(chan bool, 1) go func() { resp, err := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar?wait=true&waitIndex=5")) if err != nil { t.Errorf("watch err = %v, want nil", err) } body = tc.ReadBodyJSON(resp) c <- true }() select { case <-c: t.Fatal("should not get the watch result") case <-time.After(time.Millisecond): } // Set a value (before given index). v := url.Values{} v.Set("value", "XXX") resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v) if err != nil { t.Fatalf("put err = %v, want nil", err) } resp.Body.Close() select { case <-c: t.Fatal("should not get the watch result") case <-time.After(time.Millisecond): } // Set a value (before given index). resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar"), v) if err != nil { t.Fatalf("put err = %v, want nil", err) } resp.Body.Close() select { case <-c: case <-time.After(time.Second): t.Fatal("cannot get watch result") } w := map[string]interface{}{ "node": map[string]interface{}{ "key": "/foo/bar", "value": "XXX", "modifiedIndex": float64(5), }, "action": "set", } if err := checkBody(body, w); err != nil { t.Error(err) } } func TestV2WatchKeyInDir(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() var body map[string]interface{} c := make(chan bool) // Create an expiring directory v := url.Values{} v.Set("dir", "true") v.Set("ttl", "1") resp, err := tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir"), v) if err != nil { t.Fatalf("put err = %v, want nil", err) } resp.Body.Close() // Create a permanent node within the directory v = url.Values{} v.Set("value", "XXX") resp, err = tc.PutForm(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir/bar"), v) if err != nil { t.Fatalf("put err = %v, want nil", err) } resp.Body.Close() go func() { // Expect a notification when watching the node resp, err := tc.Get(fmt.Sprintf("%s%s", u, "/v2/keys/keyindir/bar?wait=true")) if err != nil { t.Errorf("watch err = %v, want nil", err) } body = tc.ReadBodyJSON(resp) c <- true }() select { case <-c: // 1s ttl + 0.5s sync delay + 1.5s disk and network delay // We set that long disk and network delay because travis may be slow // when do system calls. case <-time.After(3 * time.Second): t.Fatal("timed out waiting for watch result") } w := map[string]interface{}{ "node": map[string]interface{}{ "key": "/keyindir", }, "action": "expire", } if err := checkBody(body, w); err != nil { t.Error(err) } } func TestV2Head(t *testing.T) { defer testutil.AfterTest(t) cl := NewCluster(t, 1) cl.Launch(t) defer cl.Terminate(t) u := cl.URL(0) tc := NewTestClient() v := url.Values{} v.Set("value", "XXX") fullURL := fmt.Sprintf("%s%s", u, "/v2/keys/foo/bar") resp, err := tc.Head(fullURL) if err != nil { t.Fatalf("head err = %v, want nil", err) } resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusNotFound) } if resp.ContentLength <= 0 { t.Errorf("ContentLength = %d, want > 0", resp.ContentLength) } resp, err = tc.PutForm(fullURL, v) if err != nil { t.Fatalf("put err = %v, want nil", err) } resp.Body.Close() resp, err = tc.Head(fullURL) if err != nil { t.Fatalf("head err = %v, want nil", err) } resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK) } if resp.ContentLength <= 0 { t.Errorf("ContentLength = %d, want > 0", resp.ContentLength) } } func checkBody(body map[string]interface{}, w map[string]interface{}) error { if body["node"] != nil { if w["node"] != nil { wn := w["node"].(map[string]interface{}) n := body["node"].(map[string]interface{}) for k := range n { if wn[k] == nil { delete(n, k) } } body["node"] = n } if w["prevNode"] != nil { wn := w["prevNode"].(map[string]interface{}) n := body["prevNode"].(map[string]interface{}) for k := range n { if wn[k] == nil { delete(n, k) } } body["prevNode"] = n } } for k, v := range w { g := body[k] if !reflect.DeepEqual(g, v) { return fmt.Errorf("%v = %+v, want %+v", k, g, v) } } return nil } type testHttpClient struct { *http.Client } // Creates a new HTTP client with KeepAlive disabled. func NewTestClient() *testHttpClient { tr, _ := transport.NewTransport(transport.TLSInfo{}, time.Second) tr.DisableKeepAlives = true return &testHttpClient{&http.Client{Transport: tr}} } // Reads the body from the response and closes it. func (t *testHttpClient) ReadBody(resp *http.Response) []byte { if resp == nil { return []byte{} } body, _ := ioutil.ReadAll(resp.Body) resp.Body.Close() return body } // Reads the body from the response and parses it as JSON. func (t *testHttpClient) ReadBodyJSON(resp *http.Response) map[string]interface{} { m := make(map[string]interface{}) b := t.ReadBody(resp) if err := json.Unmarshal(b, &m); err != nil { panic(fmt.Sprintf("HTTP body JSON parse error: %v: %s", err, string(b))) } return m } func (t *testHttpClient) Head(url string) (*http.Response, error) { return t.send("HEAD", url, "application/json", nil) } func (t *testHttpClient) Get(url string) (*http.Response, error) { return t.send("GET", url, "application/json", nil) } func (t *testHttpClient) Post(url string, bodyType string, body io.Reader) (*http.Response, error) { return t.send("POST", url, bodyType, body) } func (t *testHttpClient) PostForm(url string, data url.Values) (*http.Response, error) { return t.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } func (t *testHttpClient) Put(url string, bodyType string, body io.Reader) (*http.Response, error) { return t.send("PUT", url, bodyType, body) } func (t *testHttpClient) PutForm(url string, data url.Values) (*http.Response, error) { return t.Put(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } func (t *testHttpClient) Delete(url string, bodyType string, body io.Reader) (*http.Response, error) { return t.send("DELETE", url, bodyType, body) } func (t *testHttpClient) DeleteForm(url string, data url.Values) (*http.Response, error) { return t.Delete(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) } func (t *testHttpClient) send(method string, url string, bodyType string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", bodyType) return t.Do(req) }