diff --git a/pkg/api/types.go b/pkg/api/types.go index 210a4c35c61..86c19a9e137 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -603,6 +603,7 @@ type Status struct { type StatusDetails struct { // The ID attribute of the resource associated with the status StatusReason // (when there is a single ID which can be described). + // TODO: replace with Name ID string `json:"id,omitempty" yaml:"id,omitempty"` // The kind attribute of the resource associated with the status StatusReason. // On some operations may differ from the requested resource Kind. diff --git a/pkg/client/request.go b/pkg/client/request.go index 965e5846872..a20928df588 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -28,32 +28,65 @@ import ( "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" - "github.com/golang/glog" ) // specialParams lists parameters that are handled specially and which users of Request // are therefore not allowed to set manually. var specialParams = util.NewStringSet("sync", "timeout") +// PollFunc is called when a server operation returns 202 accepted. The name of the +// operation is extracted from the response and passed to this function. Return a +// request to retrieve the result of the operation, or false for the second argument +// if polling should end. +type PollFunc func(name string) (*Request, bool) + +// HTTPClient is an interface for testing a request object. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + // Request allows for building up a request to a server in a chained fashion. // Any errors are stored until the end of your call, so you only have to // check once. type Request struct { - c *RESTClient - err error - verb string - path string - body io.Reader - params map[string]string - selector labels.Selector - timeout time.Duration - sync bool - pollPeriod time.Duration + // required + client HTTPClient + verb string + baseURL *url.URL + codec runtime.Codec + + // optional, will be invoked if the server returns a 202 to decide + // whether to poll. + poller PollFunc + + // accessible via method setters + path string + params map[string]string + selector labels.Selector + sync bool + timeout time.Duration + + // output + err error + body io.Reader +} + +// NewRequest creates a new request with the core attributes. +func NewRequest(client HTTPClient, verb string, baseURL *url.URL, codec runtime.Codec) *Request { + return &Request{ + client: client, + verb: verb, + baseURL: baseURL, + codec: codec, + + path: baseURL.Path, + } } // Path appends an item to the request path. You must call Path at least once. @@ -135,6 +168,9 @@ func (r *Request) setParam(paramName, value string) *Request { r.err = fmt.Errorf("must set %v through the corresponding function, not directly.", paramName) return r } + if r.params == nil { + r.params = make(map[string]string) + } r.params[paramName] = value return r } @@ -172,7 +208,7 @@ func (r *Request) Body(obj interface{}) *Request { case io.Reader: r.body = t case runtime.Object: - data, err := r.c.Codec.Encode(t) + data, err := r.codec.Encode(t) if err != nil { r.err = err return r @@ -184,21 +220,23 @@ func (r *Request) Body(obj interface{}) *Request { return r } -// PollPeriod sets the poll period. -// If the server sends back a "working" status message, then repeatedly poll the server -// to see if the operation has completed yet, waiting 'd' between each poll. -// If you want to handle the "working" status yourself (it'll be delivered as StatusErr), -// set d to 0 to turn off this behavior. -func (r *Request) PollPeriod(d time.Duration) *Request { +// NoPoll indicates a server "working" response should be returned as an error +func (r *Request) NoPoll() *Request { + return r.Poller(nil) +} + +// Poller indicates this request should use the specify poll function to determine whether +// a server "working" response should be retried. +func (r *Request) Poller(poller PollFunc) *Request { if r.err != nil { return r } - r.pollPeriod = d + r.poller = poller return r } func (r *Request) finalURL() string { - finalURL := *r.c.baseURL + finalURL := *r.baseURL finalURL.Path = r.path query := url.Values{} for key, value := range r.params { @@ -227,18 +265,18 @@ func (r *Request) Watch() (watch.Interface, error) { if err != nil { return nil, err } - client := r.c.Client + client := r.client if client == nil { client = http.DefaultClient } - response, err := client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, err } - if response.StatusCode != http.StatusOK { - return nil, fmt.Errorf("Got status: %v", response.StatusCode) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Got status: %v", resp.StatusCode) } - return watch.NewStreamWatcher(watchjson.NewDecoder(response.Body, r.c.Codec)), nil + return watch.NewStreamWatcher(watchjson.NewDecoder(resp.Body, r.codec)), nil } // Stream formats and executes the request, and offers streaming of the response. @@ -251,51 +289,106 @@ func (r *Request) Stream() (io.ReadCloser, error) { if err != nil { return nil, err } - client := r.c.Client + client := r.client if client == nil { client = http.DefaultClient } - response, err := client.Do(req) + resp, err := client.Do(req) if err != nil { return nil, err } - return response.Body, nil + return resp.Body, nil } -// Do formats and executes the request. Returns the API object received, or an error. +// Do formats and executes the request. Returns a Result object for easy response +// processing. Handles polling the server in the event a continuation was sent. func (r *Request) Do() Result { + client := r.client + if client == nil { + client = http.DefaultClient + } + for { if r.err != nil { return Result{err: r.err} } + req, err := http.NewRequest(r.verb, r.finalURL(), r.body) if err != nil { return Result{err: err} } - respBody, created, err := r.c.doRequest(req) + + resp, err := client.Do(req) if err != nil { - if s, ok := err.(APIStatus); ok { - status := s.Status() - if status.Status == api.StatusWorking && r.pollPeriod != 0 { - if status.Details != nil { - id := status.Details.ID - if len(id) > 0 { - glog.Infof("Waiting for completion of /operations/%s", id) - time.Sleep(r.pollPeriod) - // Make a poll request - pollOp := r.c.PollFor(id).PollPeriod(r.pollPeriod) - // Could also say "return r.Do()" but this way doesn't grow the callstack. - r = pollOp - continue - } - } - } - } + return Result{err: err} } - return Result{respBody, created, err, r.c.Codec} + + respBody, created, err := r.transformResponse(resp, req) + if poll, ok := r.shouldPoll(err); ok { + r = poll + continue + } + + return Result{respBody, created, err, r.codec} } } +// shouldPoll checks the server error for an incomplete operation +// and if found returns a request that would check the response. +// If no polling is necessary or possible, it will return false. +func (r *Request) shouldPoll(err error) (*Request, bool) { + if err == nil || r.poller == nil { + return nil, false + } + apistatus, ok := err.(APIStatus) + if !ok { + return nil, false + } + status := apistatus.Status() + if status.Status != api.StatusWorking { + return nil, false + } + if status.Details == nil || len(status.Details.ID) == 0 { + return nil, false + } + return r.poller(status.Details.ID) +} + +// transformResponse converts an API response into a structured API object. +func (r *Request) transformResponse(resp *http.Response, req *http.Request) ([]byte, bool, error) { + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, false, err + } + + // Did the server give us a status response? + isStatusResponse := false + var status api.Status + if err := r.codec.DecodeInto(body, &status); err == nil && status.Status != "" { + isStatusResponse = true + } + + switch { + case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent: + if !isStatusResponse { + return nil, false, fmt.Errorf("request [%#v] failed (%d) %s: %s", req, resp.StatusCode, resp.Status, string(body)) + } + return nil, false, errors.FromObject(&status) + } + + // If the server gave us a status back, look at what it was. + if isStatusResponse && status.Status != api.StatusSuccess { + // "Working" requests need to be handled specially. + // "Failed" requests are clearly just an error and it makes sense to return them as such. + return nil, false, errors.FromObject(&status) + } + + created := resp.StatusCode == http.StatusCreated + return body, created, err +} + // Result contains the result of calling Request.Do(). type Result struct { body []byte diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index 9183c820353..9d18fd25766 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "net/url" "reflect" "strings" "testing" @@ -29,6 +30,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" @@ -38,6 +40,41 @@ import ( watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" ) +func TestTransformResponse(t *testing.T) { + invalid := []byte("aaaaa") + uri, _ := url.Parse("http://localhost") + testCases := []struct { + Response *http.Response + Data []byte + Created bool + Error bool + }{ + {Response: &http.Response{StatusCode: 200}, Data: []byte{}}, + {Response: &http.Response{StatusCode: 201}, Data: []byte{}, Created: true}, + {Response: &http.Response{StatusCode: 199}, Error: true}, + {Response: &http.Response{StatusCode: 500}, Error: true}, + {Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid}, + {Response: &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(invalid))}, Data: invalid}, + } + for i, test := range testCases { + r := NewRequest(nil, "", uri, testapi.Codec()) + if test.Response.Body == nil { + test.Response.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) + } + response, created, err := r.transformResponse(test.Response, &http.Request{}) + hasErr := err != nil + if hasErr != test.Error { + t.Errorf("%d: unexpected error: %f %v", i, test.Error, err) + } + if !(test.Data == nil && response == nil) && !reflect.DeepEqual(test.Data, response) { + t.Errorf("%d: unexpected response: %#v %#v", i, test.Data, response) + } + if test.Created != created { + t.Errorf("%d: expected created %f, got %f", i, test.Created, created) + } + } +} + func TestDoRequestNewWay(t *testing.T) { reqBody := "request body" expectedObj := &api.Service{Port: 12345} @@ -48,6 +85,7 @@ func TestDoRequestNewWay(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) + defer testServer.Close() c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"}) obj, err := c.Verb("POST"). Path("foo/bar"). @@ -351,15 +389,18 @@ func TestBody(t *testing.T) { } } -func TestSetPollPeriod(t *testing.T) { +func TestSetPoller(t *testing.T) { c := NewOrDie(&Config{}) r := c.Get() - if r.pollPeriod == 0 { + if c.PollPeriod == 0 { t.Errorf("polling should be on by default") } - r.PollPeriod(time.Hour) - if r.pollPeriod != time.Hour { - t.Errorf("'PollPeriod' doesn't work") + if r.poller == nil { + t.Errorf("polling should be on by default") + } + r.NoPoll() + if r.poller != nil { + t.Errorf("'NoPoll' doesn't work") } } @@ -374,6 +415,16 @@ func TestPolling(t *testing.T) { callNumber := 0 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if callNumber == 0 { + if r.URL.Path != "/api/v1beta1/" { + t.Fatalf("unexpected request URL path %s", r.URL.Path) + } + } else { + if r.URL.Path != "/api/v1beta1/operations/1234" { + t.Fatalf("unexpected request URL path %s", r.URL.Path) + } + } + t.Logf("About to write %d", callNumber) data, err := v1beta1.Codec.Encode(objects[callNumber]) if err != nil { t.Errorf("Unexpected encode error") @@ -383,11 +434,11 @@ func TestPolling(t *testing.T) { })) c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"}) - + c.PollPeriod = 1 * time.Millisecond trials := []func(){ func() { // Check that we do indeed poll when asked to. - obj, err := c.Get().PollPeriod(5 * time.Millisecond).Do().Get() + obj, err := c.Get().Do().Get() if err != nil { t.Errorf("Unexpected error: %v %#v", err, err) return @@ -402,7 +453,7 @@ func TestPolling(t *testing.T) { }, func() { // Check that we don't poll when asked not to. - obj, err := c.Get().PollPeriod(0).Do().Get() + obj, err := c.Get().NoPoll().Do().Get() if err == nil { t.Errorf("Unexpected non error: %v", obj) return diff --git a/pkg/client/restclient.go b/pkg/client/restclient.go index caa510fc928..9b8d4b542a7 100644 --- a/pkg/client/restclient.go +++ b/pkg/client/restclient.go @@ -17,16 +17,13 @@ limitations under the License. package client import ( - "fmt" - "io/ioutil" - "net/http" "net/url" "strings" "time" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + + "github.com/golang/glog" ) // RESTClient imposes common Kubernetes API conventions on a set of resource paths. @@ -45,7 +42,11 @@ type RESTClient struct { // Set specific behavior of the client. If not set http.DefaultClient will be // used. - Client *http.Client + Client HTTPClient + + // Set the poll behavior of this client. If not set the DefaultPoll method will + // be called. + Poller PollFunc Sync bool PollPeriod time.Duration @@ -68,56 +69,13 @@ func NewRESTClient(baseURL *url.URL, c runtime.Codec) *RESTClient { Codec: c, // Make asynchronous requests by default - // TODO: flip me to the default Sync: false, + // Poll frequently when asynchronous requests are provided PollPeriod: time.Second * 2, } } -// doRequest executes a request against a server -func (c *RESTClient) doRequest(request *http.Request) ([]byte, bool, error) { - client := c.Client - if client == nil { - client = http.DefaultClient - } - - response, err := client.Do(request) - if err != nil { - return nil, false, err - } - defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return body, false, err - } - - // Did the server give us a status response? - isStatusResponse := false - var status api.Status - if err := c.Codec.DecodeInto(body, &status); err == nil && status.Status != "" { - isStatusResponse = true - } - - switch { - case response.StatusCode < http.StatusOK || response.StatusCode > http.StatusPartialContent: - if !isStatusResponse { - return nil, false, fmt.Errorf("request [%#v] failed (%d) %s: %s", request, response.StatusCode, response.Status, string(body)) - } - return nil, false, errors.FromObject(&status) - } - - // If the server gave us a status back, look at what it was. - if isStatusResponse && status.Status != api.StatusSuccess { - // "Working" requests need to be handled specially. - // "Failed" requests are clearly just an error and it makes sense to return them as such. - return nil, false, errors.FromObject(&status) - } - - created := response.StatusCode == http.StatusCreated - return body, created, err -} - // Verb begins a request with a verb (GET, POST, PUT, DELETE). // // Example usage of RESTClient's request building interface: @@ -136,15 +94,11 @@ func (c *RESTClient) Verb(verb string) *Request { // if c.Client != nil { // timeout = c.Client.Timeout // } - return &Request{ - verb: verb, - c: c, - path: c.baseURL.Path, - sync: c.Sync, - timeout: c.Timeout, - params: map[string]string{}, - pollPeriod: c.PollPeriod, + poller := c.Poller + if poller == nil { + poller = c.DefaultPoll } + return NewRequest(c.Client, verb, c.baseURL, c.Codec).Poller(poller).Sync(c.Sync).Timeout(c.Timeout) } // Post begins a POST request. Short for c.Verb("POST"). @@ -168,6 +122,16 @@ func (c *RESTClient) Delete() *Request { } // PollFor makes a request to do a single poll of the completion of the given operation. -func (c *RESTClient) PollFor(operationID string) *Request { - return c.Get().Path("operations").Path(operationID).Sync(false).PollPeriod(0) +func (c *RESTClient) Operation(name string) *Request { + return c.Get().Path("operations").Path(name).Sync(false).NoPoll() +} + +func (c *RESTClient) DefaultPoll(name string) (*Request, bool) { + if c.PollPeriod == 0 { + return nil, false + } + glog.Infof("Waiting for completion of operation %s", name) + time.Sleep(c.PollPeriod) + // Make a poll request + return c.Operation(name).Poller(c.DefaultPoll), true } diff --git a/pkg/client/restclient_test.go b/pkg/client/restclient_test.go index 63b742b1655..75c48bf9f32 100644 --- a/pkg/client/restclient_test.go +++ b/pkg/client/restclient_test.go @@ -19,7 +19,6 @@ package client import ( "net/http" "net/http/httptest" - "net/url" "reflect" "testing" @@ -100,36 +99,6 @@ func TestValidatesHostParameter(t *testing.T) { } } -func TestDoRequest(t *testing.T) { - invalid := "aaaaa" - uri, _ := url.Parse("http://localhost") - testClients := []testClient{ - {Request: testRequest{Method: "GET", Path: "good"}, Response: Response{StatusCode: 200}}, - {Request: testRequest{Method: "GET", Path: "good"}, Response: Response{StatusCode: 201}, Created: true}, - {Request: testRequest{Method: "GET", Path: "bad%ZZ"}, Error: true}, - {Request: testRequest{Method: "GET", Path: "error"}, Response: Response{StatusCode: 500}, Error: true}, - {Request: testRequest{Method: "POST", Path: "faildecode"}, Response: Response{StatusCode: 200, RawBody: &invalid}}, - {Request: testRequest{Method: "GET", Path: "failread"}, Response: Response{StatusCode: 200, RawBody: &invalid}}, - {Client: &Client{&RESTClient{baseURL: uri, Codec: testapi.Codec()}}, Request: testRequest{Method: "GET", Path: "nocertificate"}, Error: true}, - } - for _, c := range testClients { - client := c.Setup() - prefix := *client.baseURL - prefix.Path += c.Request.Path - request := &http.Request{ - Method: c.Request.Method, - Header: make(http.Header), - URL: &prefix, - } - response, created, err := client.doRequest(request) - if c.Created != created { - t.Errorf("expected created %f, got %f", c.Created, created) - } - //t.Logf("dorequest: %#v\n%#v\n%v", request.URL, response, err) - c.ValidateRaw(t, response, err) - } -} - func TestDoRequestBearer(t *testing.T) { status := &api.Status{Status: api.StatusWorking} expectedBody, _ := latest.Codec.Encode(status) @@ -139,12 +108,15 @@ func TestDoRequestBearer(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) + request, _ := http.NewRequest("GET", testServer.URL, nil) c, err := RESTClientFor(&Config{Host: testServer.URL, BearerToken: "test"}) if err != nil { t.Fatalf("unexpected error: %v", err) } - c.doRequest(request) + err = c.Get().Do().Error() + if err == nil { + t.Fatalf("unexpected non-error: %v", err) + } if fakeHandler.RequestReceived.Header.Get("Authorization") != "Bearer test" { t.Errorf("Request is missing authorization header: %#v", *request) } @@ -159,18 +131,17 @@ func TestDoRequestAccepted(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) - c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test"}) + c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test", Version: testapi.Version()}) if err != nil { t.Fatalf("unexpected error: %v", err) } - body, _, err := c.doRequest(request) - if fakeHandler.RequestReceived.Header["Authorization"] == nil { - t.Errorf("Request is missing authorization header: %#v", *request) - } + body, err := c.Get().Path("test").Do().Raw() if err == nil { t.Fatalf("Unexpected non-error") } + if fakeHandler.RequestReceived.Header["Authorization"] == nil { + t.Errorf("Request is missing authorization header: %#v", fakeHandler.RequestReceived) + } se, ok := err.(APIStatus) if !ok { t.Fatalf("Unexpected kind of error: %#v", err) @@ -179,9 +150,9 @@ func TestDoRequestAccepted(t *testing.T) { t.Errorf("Unexpected status: %#v %#v", se.Status(), status) } if body != nil { - t.Errorf("Expected nil body, but saw: '%s'", body) + t.Errorf("Expected nil body, but saw: '%s'", string(body)) } - fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil) + fakeHandler.ValidateRequest(t, "/"+testapi.Version()+"/test", "GET", nil) } func TestDoRequestAcceptedSuccess(t *testing.T) { @@ -193,17 +164,16 @@ func TestDoRequestAcceptedSuccess(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) - c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"}) + c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass", Version: testapi.Version()}) if err != nil { t.Fatalf("unexpected error: %v", err) } - body, _, err := c.doRequest(request) - if fakeHandler.RequestReceived.Header["Authorization"] == nil { - t.Errorf("Request is missing authorization header: %#v", *request) - } + body, err := c.Get().Path("test").Do().Raw() if err != nil { - t.Errorf("Unexpected error %#v", err) + t.Fatalf("unexpected error: %v", err) + } + if fakeHandler.RequestReceived.Header["Authorization"] == nil { + t.Errorf("Request is missing authorization header: %#v", fakeHandler.RequestReceived) } statusOut, err := latest.Codec.Decode(body) if err != nil { @@ -212,7 +182,7 @@ func TestDoRequestAcceptedSuccess(t *testing.T) { if !reflect.DeepEqual(status, statusOut) { t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut) } - fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil) + fakeHandler.ValidateRequest(t, "/"+testapi.Version()+"/test", "GET", nil) } func TestDoRequestFailed(t *testing.T) { @@ -224,12 +194,11 @@ func TestDoRequestFailed(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) c, err := RESTClientFor(&Config{Host: testServer.URL}) if err != nil { t.Fatalf("unexpected error: %v", err) } - body, _, err := c.doRequest(request) + body, err := c.Get().Do().Raw() if err == nil || body != nil { t.Errorf("unexpected non-error: %#v", body) } @@ -252,12 +221,12 @@ func TestDoRequestCreated(t *testing.T) { T: t, } testServer := httptest.NewServer(&fakeHandler) - request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) - c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"}) + c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass", Version: testapi.Version()}) if err != nil { t.Fatalf("unexpected error: %v", err) } - body, created, err := c.doRequest(request) + created := false + body, err := c.Get().Path("test").Do().WasCreated(&created).Raw() if err != nil { t.Errorf("Unexpected error %#v", err) } @@ -271,5 +240,5 @@ func TestDoRequestCreated(t *testing.T) { if !reflect.DeepEqual(status, statusOut) { t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", status, statusOut) } - fakeHandler.ValidateRequest(t, "/foo/bar", "GET", nil) + fakeHandler.ValidateRequest(t, "/"+testapi.Version()+"/test", "GET", nil) }