Skip to content

Commit

Permalink
Move as much API HTTP code as possible into its own package.
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsnowcurrently committed Oct 28, 2014
1 parent 7a2c4b9 commit 837f41f
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 111 deletions.
48 changes: 13 additions & 35 deletions api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,31 @@ import (
"crypto/tls"
"net/http"
"net/url"
"path"

"github.com/juju/errors"
"github.com/juju/utils"

"github.com/juju/juju/api/base"
apihttp "github.com/juju/juju/api/http"
)

var _ base.HTTPRequestBuilder = (*State)(nil)
var _ base.HTTPCaller = (*State)(nil)
var _ apihttp.Client = (*State)(nil)

// HTTPClient sends an HTTP request, returning the subsequent response.
type HTTPClient interface {
Do(*http.Request) (*http.Response, error)
}

var newHTTPClient = func(state *State) HTTPClient {
return state.NewHTTPClient()
var newHTTPClient = func(s *State) apihttp.HTTPClient {
return s.NewHTTPClient()
}

// NewHTTPClient returns an HTTP client initialized based on State.
func (s *State) NewHTTPClient() HTTPClient {
// For reference, call utils.GetNonValidatingHTTPClient() to get a
// non-validating client.
func (s *State) NewHTTPClient() *http.Client {
// for reference:
// Call utils.GetNonValidatingHTTPClient() to get a non-validating client.
httpclient := utils.GetValidatingHTTPClient()
tlsconfig := tls.Config{RootCAs: s.certPool, ServerName: "anything"}
httpclient.Transport = utils.NewHttpTLSTransport(&tlsconfig)
return httpclient
}

// NewHTTPRequest returns a new API-supporting HTTP request based on State.
func (s *State) NewHTTPRequest(method, path string) (*base.HTTPRequest, error) {
func (s *State) NewHTTPRequest(method, path string) (*apihttp.Request, error) {
baseURL, err := url.Parse(s.serverRoot)
if err != nil {
return nil, errors.Annotatef(err, "while parsing base URL (%s)", s.serverRoot)
Expand All @@ -50,31 +43,16 @@ func (s *State) NewHTTPRequest(method, path string) (*base.HTTPRequest, error) {
}
uuid := tag.Id()

req, err := newHTTPRequest(method, baseURL, path, uuid, s.tag, s.password)
if err != nil {
return nil, errors.Trace(err)
}

return &base.HTTPRequest{*req}, nil
}

func newHTTPRequest(method string, URL *url.URL, pth, uuid, tag, pw string) (*http.Request, error) {
URL.Path = path.Join("/environment", uuid, pth)
req, err := http.NewRequest(method, URL.String(), nil)
if err != nil {
return nil, errors.Annotate(err, "while building HTTP request")
}
req.SetBasicAuth(tag, pw)
return req, nil
req, err := apihttp.NewRequest(method, baseURL, path, uuid, s.tag, s.password)
return req, errors.Trace(err)
}

// SendHTTPRequest sends the request using the HTTP client derived from
// State.
func (s *State) SendHTTPRequest(req *base.HTTPRequest) (*http.Response, error) {
// SendHTTPRequest sends the request using the HTTP client derived from State.
func (s *State) SendHTTPRequest(req *apihttp.Request) (*http.Response, error) {
httpclient := newHTTPClient(s)
resp, err := httpclient.Do(&req.Request)
if err != nil {
return nil, errors.Annotate(err, "error when sending HTTP request")
return nil, errors.Annotate(err, "while sending HTTP request")
}
return resp, nil
}
32 changes: 32 additions & 0 deletions api/http/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package http

import (
"net/http"
)

// Request is a wrapper around an HTTP request that has been
// prepared for use in API HTTP calls.
type Request struct {
http.Request
}

// HTTPClient is an API-specific HTTP client.
type HTTPClient interface {
// Do sends the HTTP request, returning the subsequent response.
Do(req *http.Request) (*http.Response, error)
}

// Client exposes direct HTTP request functionality for the juju state
// API. This is significant for upload and download of files, which the
// websockets-based RPC does not support.
type Client interface {
// NewHTTPRequest returns a new API-specific HTTP request. Callers
// should finish the request (setting headers, body) before sending.
NewHTTPRequest(method, path string) (*Request, error)
// SendHTTPRequest returns the HTTP response from the API server.
// The caller is then responsible for handling the response.
SendHTTPRequest(req *Request) (*http.Response, error)
}
14 changes: 14 additions & 0 deletions api/http/package_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.

package http_test

import (
"testing"

gc "gopkg.in/check.v1"
)

func TestPackage(t *testing.T) {
gc.TestingT(t)
}
23 changes: 23 additions & 0 deletions api/http/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package http

import (
"net/http"
"net/url"
"path"

"github.com/juju/errors"
)

// NewRequest returns a new HTTP request suitable for the API.
func NewRequest(method string, baseURL *url.URL, pth, uuid, tag, pw string) (*Request, error) {
baseURL.Path = path.Join("/environment", uuid, pth)
req, err := http.NewRequest(method, baseURL.String(), nil)
if err != nil {
return nil, errors.Annotate(err, "while building HTTP request")
}
req.SetBasicAuth(tag, pw)
return &Request{*req}, nil
}
35 changes: 35 additions & 0 deletions api/http/request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package http_test

import (
"net/url"

gc "gopkg.in/check.v1"

apihttp "github.com/juju/juju/api/http"
apihttptesting "github.com/juju/juju/api/http/testing"
)

type requestSuite struct {
apihttptesting.BaseSuite
}

var _ = gc.Suite(&requestSuite{})

func (s *requestSuite) SetUpTest(c *gc.C) {
s.BaseSuite.SetUpTest(c)
}

func (s *requestSuite) TestNewRequestSuccess(c *gc.C) {
baseURL, err := url.Parse("https://localhost:8080/")
c.Assert(err, gc.IsNil)
uuid := "abcd-efedcb-012345-6789"
tag := "machine-0"
pw := "secure"
req, err := apihttp.NewRequest("GET", baseURL, "somefacade", uuid, tag, pw)
c.Assert(err, gc.IsNil)

s.CheckRequest(c, req, "GET", tag, pw, "localhost", "somefacade")
}
27 changes: 2 additions & 25 deletions api/base/http.go → api/http/response.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package base
package http

import (
"encoding/json"
Expand All @@ -13,33 +13,10 @@ import (
"github.com/juju/juju/apiserver/params"
)

// HTTPRequest is a wrapper around an HTTP request that has been
// prepared for use in API HTTP calls.
type HTTPRequest struct {
http.Request
}

// HTTPRequestBuilder facilitates creating HTTP requests suitable for
// use with API HTTP calls (see HTTPCaller).
type HTTPRequestBuilder interface {
// NewHTTPRequest returns a new API-specific HTTP request. Callers
// should finish the request (setting headers, body) before sending.
NewHTTPRequest(method, path string) (*HTTPRequest, error)
}

// HTTPCaller exposes direct HTTP request functionality for the API.
// This is significant for upload and download of files, which the
// websockets-based RPC does not support.
type HTTPCaller interface {
// SendHTTPRequest returns the HTTP response from the API server.
// The caller is then responsible for handling the response.
SendHTTPRequest(req *HTTPRequest) (*http.Response, error)
}

// CheckHTTPResponse returns the failure serialized in the response
// body. If there is no failure (an OK status code), it simply returns
// nil.
func CheckHTTPResponse(resp *http.Response) error {
func CheckResponse(resp *http.Response) error {
if resp.StatusCode == http.StatusOK {
return nil
}
Expand Down
55 changes: 55 additions & 0 deletions api/http/testing/fakes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package testing

import (
"bytes"
"io/ioutil"
"net/http"

gc "gopkg.in/check.v1"
)

// FakeHTTPClient is used in testing in place of an actual http.Client.
type FakeHTTPClient struct {
// Error is the error that will be returned for any calls.
Error error
// Response is the response returned from calls.
Response *http.Response

// Calls is the record of which methods were called.
Calls []string
// RequestArg is the request that was passed to a call.
RequestArg *http.Request
}

// NewFakeHTTPClient returns a fake with Response set to an OK status,
// no headers, and no body.
func NewFakeHTTPClient() *FakeHTTPClient {
resp := http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: ioutil.NopCloser(&bytes.Buffer{}),
}

fake := FakeHTTPClient{
Response: &resp,
}
return &fake
}

// CheckCalled checks that the Do was called once with the request and
// returned the correct value.
func (f *FakeHTTPClient) CheckCalled(c *gc.C, req *http.Request, resp *http.Response) {
c.Check(f.Calls, gc.DeepEquals, []string{"Do"})
c.Check(f.RequestArg, gc.Equals, req)
c.Check(resp, gc.Equals, f.Response)
}

// Do fakes the behavior of http.Client.Do().
func (f *FakeHTTPClient) Do(req *http.Request) (*http.Response, error) {
f.Calls = append(f.Calls, "Do")
f.RequestArg = req
return f.Response, f.Error
}
39 changes: 39 additions & 0 deletions api/http/testing/suite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package testing

import (
"encoding/base64"

gc "gopkg.in/check.v1"

apihttp "github.com/juju/juju/api/http"
"github.com/juju/juju/testing"
)

// BaseSuite provides basic testing capability for API HTTP tests.
type BaseSuite struct {
testing.BaseSuite
// Fake is the fake HTTP client used in tests.
Fake *FakeHTTPClient
}

func (s *BaseSuite) SetUpTest(c *gc.C) {
s.BaseSuite.SetUpTest(c)

s.Fake = NewFakeHTTPClient()
}

func (s *BaseSuite) CheckRequest(c *gc.C, req *apihttp.Request, method, user, pw, host, pth string) {
// Only check API-related request fields.

c.Check(req.Method, gc.Equals, method)

url := `https://` + host + `:\d+/environment/[-0-9a-f]+/` + pth
c.Check(req.URL.String(), gc.Matches, url)

c.Assert(req.Header, gc.HasLen, 1)
auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + pw))
c.Check(req.Header.Get("Authorization"), gc.Equals, "Basic "+auth)
}
Loading

0 comments on commit 837f41f

Please sign in to comment.