Make client.Request more testable, break coupling with RESTClient

Moves polling to a function provided by the RESTClient, not innate
to Request. Moves doRequest from RESTClient to Request for clarity.
This commit is contained in:
Clayton Coleman 2014-10-28 22:48:13 -04:00
parent 1da5c444e8
commit eac933eb44
5 changed files with 249 additions and 171 deletions

View File

@ -603,6 +603,7 @@ type Status struct {
type StatusDetails struct { type StatusDetails struct {
// The ID attribute of the resource associated with the status StatusReason // The ID attribute of the resource associated with the status StatusReason
// (when there is a single ID which can be described). // (when there is a single ID which can be described).
// TODO: replace with Name
ID string `json:"id,omitempty" yaml:"id,omitempty"` ID string `json:"id,omitempty" yaml:"id,omitempty"`
// The kind attribute of the resource associated with the status StatusReason. // The kind attribute of the resource associated with the status StatusReason.
// On some operations may differ from the requested resource Kind. // On some operations may differ from the requested resource Kind.

View File

@ -28,32 +28,65 @@ import (
"time" "time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json"
"github.com/golang/glog"
) )
// specialParams lists parameters that are handled specially and which users of Request // specialParams lists parameters that are handled specially and which users of Request
// are therefore not allowed to set manually. // are therefore not allowed to set manually.
var specialParams = util.NewStringSet("sync", "timeout") 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. // 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 // Any errors are stored until the end of your call, so you only have to
// check once. // check once.
type Request struct { type Request struct {
c *RESTClient // required
err error client HTTPClient
verb string verb string
path string baseURL *url.URL
body io.Reader codec runtime.Codec
params map[string]string
selector labels.Selector // optional, will be invoked if the server returns a 202 to decide
timeout time.Duration // whether to poll.
sync bool poller PollFunc
pollPeriod time.Duration
// 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. // 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) r.err = fmt.Errorf("must set %v through the corresponding function, not directly.", paramName)
return r return r
} }
if r.params == nil {
r.params = make(map[string]string)
}
r.params[paramName] = value r.params[paramName] = value
return r return r
} }
@ -172,7 +208,7 @@ func (r *Request) Body(obj interface{}) *Request {
case io.Reader: case io.Reader:
r.body = t r.body = t
case runtime.Object: case runtime.Object:
data, err := r.c.Codec.Encode(t) data, err := r.codec.Encode(t)
if err != nil { if err != nil {
r.err = err r.err = err
return r return r
@ -184,21 +220,23 @@ func (r *Request) Body(obj interface{}) *Request {
return r return r
} }
// PollPeriod sets the poll period. // NoPoll indicates a server "working" response should be returned as an error
// If the server sends back a "working" status message, then repeatedly poll the server func (r *Request) NoPoll() *Request {
// to see if the operation has completed yet, waiting 'd' between each poll. return r.Poller(nil)
// 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 { // 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 { if r.err != nil {
return r return r
} }
r.pollPeriod = d r.poller = poller
return r return r
} }
func (r *Request) finalURL() string { func (r *Request) finalURL() string {
finalURL := *r.c.baseURL finalURL := *r.baseURL
finalURL.Path = r.path finalURL.Path = r.path
query := url.Values{} query := url.Values{}
for key, value := range r.params { for key, value := range r.params {
@ -227,18 +265,18 @@ func (r *Request) Watch() (watch.Interface, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
client := r.c.Client client := r.client
if client == nil { if client == nil {
client = http.DefaultClient client = http.DefaultClient
} }
response, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Got status: %v", response.StatusCode) 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. // 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 { if err != nil {
return nil, err return nil, err
} }
client := r.c.Client client := r.client
if client == nil { if client == nil {
client = http.DefaultClient client = http.DefaultClient
} }
response, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err 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 { func (r *Request) Do() Result {
client := r.client
if client == nil {
client = http.DefaultClient
}
for { for {
if r.err != nil { if r.err != nil {
return Result{err: r.err} return Result{err: r.err}
} }
req, err := http.NewRequest(r.verb, r.finalURL(), r.body) req, err := http.NewRequest(r.verb, r.finalURL(), r.body)
if err != nil { if err != nil {
return Result{err: err} return Result{err: err}
} }
respBody, created, err := r.c.doRequest(req)
resp, err := client.Do(req)
if err != nil { if err != nil {
if s, ok := err.(APIStatus); ok { return Result{err: err}
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{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(). // Result contains the result of calling Request.Do().
type Result struct { type Result struct {
body []byte body []byte

View File

@ -22,6 +22,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@ -29,6 +30,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "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/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
@ -38,6 +40,41 @@ import (
watchjson "github.com/GoogleCloudPlatform/kubernetes/pkg/watch/json" 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) { func TestDoRequestNewWay(t *testing.T) {
reqBody := "request body" reqBody := "request body"
expectedObj := &api.Service{Port: 12345} expectedObj := &api.Service{Port: 12345}
@ -48,6 +85,7 @@ func TestDoRequestNewWay(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
defer testServer.Close()
c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"}) c := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta2", Username: "user", Password: "pass"})
obj, err := c.Verb("POST"). obj, err := c.Verb("POST").
Path("foo/bar"). 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{}) c := NewOrDie(&Config{})
r := c.Get() r := c.Get()
if r.pollPeriod == 0 { if c.PollPeriod == 0 {
t.Errorf("polling should be on by default") t.Errorf("polling should be on by default")
} }
r.PollPeriod(time.Hour) if r.poller == nil {
if r.pollPeriod != time.Hour { t.Errorf("polling should be on by default")
t.Errorf("'PollPeriod' doesn't work") }
r.NoPoll()
if r.poller != nil {
t.Errorf("'NoPoll' doesn't work")
} }
} }
@ -374,6 +415,16 @@ func TestPolling(t *testing.T) {
callNumber := 0 callNumber := 0
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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]) data, err := v1beta1.Codec.Encode(objects[callNumber])
if err != nil { if err != nil {
t.Errorf("Unexpected encode error") 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 := NewOrDie(&Config{Host: testServer.URL, Version: "v1beta1", Username: "user", Password: "pass"})
c.PollPeriod = 1 * time.Millisecond
trials := []func(){ trials := []func(){
func() { func() {
// Check that we do indeed poll when asked to. // 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 { if err != nil {
t.Errorf("Unexpected error: %v %#v", err, err) t.Errorf("Unexpected error: %v %#v", err, err)
return return
@ -402,7 +453,7 @@ func TestPolling(t *testing.T) {
}, },
func() { func() {
// Check that we don't poll when asked not to. // 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 { if err == nil {
t.Errorf("Unexpected non error: %v", obj) t.Errorf("Unexpected non error: %v", obj)
return return

View File

@ -17,16 +17,13 @@ limitations under the License.
package client package client
import ( import (
"fmt"
"io/ioutil"
"net/http"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/golang/glog"
) )
// RESTClient imposes common Kubernetes API conventions on a set of resource paths. // 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 // Set specific behavior of the client. If not set http.DefaultClient will be
// used. // 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 Sync bool
PollPeriod time.Duration PollPeriod time.Duration
@ -68,56 +69,13 @@ func NewRESTClient(baseURL *url.URL, c runtime.Codec) *RESTClient {
Codec: c, Codec: c,
// Make asynchronous requests by default // Make asynchronous requests by default
// TODO: flip me to the default
Sync: false, Sync: false,
// Poll frequently when asynchronous requests are provided // Poll frequently when asynchronous requests are provided
PollPeriod: time.Second * 2, 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). // Verb begins a request with a verb (GET, POST, PUT, DELETE).
// //
// Example usage of RESTClient's request building interface: // Example usage of RESTClient's request building interface:
@ -136,15 +94,11 @@ func (c *RESTClient) Verb(verb string) *Request {
// if c.Client != nil { // if c.Client != nil {
// timeout = c.Client.Timeout // timeout = c.Client.Timeout
// } // }
return &Request{ poller := c.Poller
verb: verb, if poller == nil {
c: c, poller = c.DefaultPoll
path: c.baseURL.Path,
sync: c.Sync,
timeout: c.Timeout,
params: map[string]string{},
pollPeriod: c.PollPeriod,
} }
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"). // 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. // PollFor makes a request to do a single poll of the completion of the given operation.
func (c *RESTClient) PollFor(operationID string) *Request { func (c *RESTClient) Operation(name string) *Request {
return c.Get().Path("operations").Path(operationID).Sync(false).PollPeriod(0) 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
} }

View File

@ -19,7 +19,6 @@ package client
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"reflect" "reflect"
"testing" "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) { func TestDoRequestBearer(t *testing.T) {
status := &api.Status{Status: api.StatusWorking} status := &api.Status{Status: api.StatusWorking}
expectedBody, _ := latest.Codec.Encode(status) expectedBody, _ := latest.Codec.Encode(status)
@ -139,12 +108,15 @@ func TestDoRequestBearer(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) 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"}) c, err := RESTClientFor(&Config{Host: testServer.URL, BearerToken: "test"})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) 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" { if fakeHandler.RequestReceived.Header.Get("Authorization") != "Bearer test" {
t.Errorf("Request is missing authorization header: %#v", *request) t.Errorf("Request is missing authorization header: %#v", *request)
} }
@ -159,18 +131,17 @@ func TestDoRequestAccepted(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test", Version: testapi.Version()})
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "test"})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
body, _, err := c.doRequest(request) body, err := c.Get().Path("test").Do().Raw()
if fakeHandler.RequestReceived.Header["Authorization"] == nil {
t.Errorf("Request is missing authorization header: %#v", *request)
}
if err == nil { if err == nil {
t.Fatalf("Unexpected non-error") 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) se, ok := err.(APIStatus)
if !ok { if !ok {
t.Fatalf("Unexpected kind of error: %#v", err) 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) t.Errorf("Unexpected status: %#v %#v", se.Status(), status)
} }
if body != nil { 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) { func TestDoRequestAcceptedSuccess(t *testing.T) {
@ -193,17 +164,16 @@ func TestDoRequestAcceptedSuccess(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass", Version: testapi.Version()})
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
body, _, err := c.doRequest(request) body, err := c.Get().Path("test").Do().Raw()
if fakeHandler.RequestReceived.Header["Authorization"] == nil {
t.Errorf("Request is missing authorization header: %#v", *request)
}
if err != nil { 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) statusOut, err := latest.Codec.Decode(body)
if err != nil { if err != nil {
@ -212,7 +182,7 @@ func TestDoRequestAcceptedSuccess(t *testing.T) {
if !reflect.DeepEqual(status, statusOut) { if !reflect.DeepEqual(status, statusOut) {
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", 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) { func TestDoRequestFailed(t *testing.T) {
@ -224,12 +194,11 @@ func TestDoRequestFailed(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil)
c, err := RESTClientFor(&Config{Host: testServer.URL}) c, err := RESTClientFor(&Config{Host: testServer.URL})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
body, _, err := c.doRequest(request) body, err := c.Get().Do().Raw()
if err == nil || body != nil { if err == nil || body != nil {
t.Errorf("unexpected non-error: %#v", body) t.Errorf("unexpected non-error: %#v", body)
} }
@ -252,12 +221,12 @@ func TestDoRequestCreated(t *testing.T) {
T: t, T: t,
} }
testServer := httptest.NewServer(&fakeHandler) testServer := httptest.NewServer(&fakeHandler)
request, _ := http.NewRequest("GET", testServer.URL+"/foo/bar", nil) c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass", Version: testapi.Version()})
c, err := RESTClientFor(&Config{Host: testServer.URL, Username: "user", Password: "pass"})
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) 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 { if err != nil {
t.Errorf("Unexpected error %#v", err) t.Errorf("Unexpected error %#v", err)
} }
@ -271,5 +240,5 @@ func TestDoRequestCreated(t *testing.T) {
if !reflect.DeepEqual(status, statusOut) { if !reflect.DeepEqual(status, statusOut) {
t.Errorf("Unexpected mis-match. Expected %#v. Saw %#v", 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)
} }