github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/backup_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver_test
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"mime/multipart"
    14  	"net/http"
    15  	"net/textproto"
    16  
    17  	"github.com/juju/errors"
    18  	jc "github.com/juju/testing/checkers"
    19  	"github.com/juju/utils"
    20  	gc "gopkg.in/check.v1"
    21  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    22  
    23  	"github.com/juju/juju/apiserver"
    24  	apiserverbackups "github.com/juju/juju/apiserver/backups"
    25  	"github.com/juju/juju/apiserver/params"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/state/backups"
    28  	backupstesting "github.com/juju/juju/state/backups/testing"
    29  )
    30  
    31  type backupsCommonSuite struct {
    32  	authHTTPSuite
    33  	fake *backupstesting.FakeBackups
    34  }
    35  
    36  func (s *backupsCommonSuite) SetUpTest(c *gc.C) {
    37  	s.authHTTPSuite.SetUpTest(c)
    38  
    39  	s.fake = &backupstesting.FakeBackups{}
    40  	s.PatchValue(apiserver.NewBackups,
    41  		func(st *state.State) (backups.Backups, io.Closer) {
    42  			return s.fake, ioutil.NopCloser(nil)
    43  		},
    44  	)
    45  }
    46  
    47  func (s *backupsCommonSuite) backupURL(c *gc.C) string {
    48  	environ, err := s.State.Model()
    49  	c.Assert(err, jc.ErrorIsNil)
    50  	uri := s.baseURL(c)
    51  	uri.Path = fmt.Sprintf("/model/%s/backups", environ.UUID())
    52  	return uri.String()
    53  }
    54  
    55  func (s *backupsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, statusCode int, msg string) *params.Error {
    56  	body, err := ioutil.ReadAll(resp.Body)
    57  	c.Assert(err, jc.ErrorIsNil)
    58  
    59  	c.Assert(resp.StatusCode, gc.Equals, statusCode, gc.Commentf("body: %s", body))
    60  	c.Assert(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeJSON, gc.Commentf("body: %q", body))
    61  
    62  	var failure params.Error
    63  	err = json.Unmarshal(body, &failure)
    64  	c.Assert(err, jc.ErrorIsNil)
    65  	c.Assert(&failure, gc.ErrorMatches, msg, gc.Commentf("body: %s", body))
    66  	return &failure
    67  }
    68  
    69  type backupsSuite struct {
    70  	backupsCommonSuite
    71  }
    72  
    73  var _ = gc.Suite(&backupsSuite{})
    74  
    75  func (s *backupsSuite) TestRequiresAuth(c *gc.C) {
    76  	resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.backupURL(c)})
    77  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "no credentials provided")
    78  }
    79  
    80  func (s *backupsSuite) checkInvalidMethod(c *gc.C, method, url string) {
    81  	resp := s.authRequest(c, httpRequestParams{method: method, url: url})
    82  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "`+method+`"`)
    83  }
    84  
    85  func (s *backupsSuite) TestInvalidHTTPMethods(c *gc.C) {
    86  	url := s.backupURL(c)
    87  	for _, method := range []string{"POST", "DELETE", "OPTIONS"} {
    88  		c.Log("testing HTTP method: " + method)
    89  		s.checkInvalidMethod(c, method, url)
    90  	}
    91  }
    92  
    93  func (s *backupsSuite) TestAuthRequiresClientNotMachine(c *gc.C) {
    94  	// Add a machine and try to login.
    95  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
    96  	c.Assert(err, jc.ErrorIsNil)
    97  	err = machine.SetProvisioned("foo", "fake_nonce", nil)
    98  	c.Assert(err, jc.ErrorIsNil)
    99  	password, err := utils.RandomPassword()
   100  	c.Assert(err, jc.ErrorIsNil)
   101  	err = machine.SetPassword(password)
   102  	c.Assert(err, jc.ErrorIsNil)
   103  
   104  	resp := s.sendRequest(c, httpRequestParams{
   105  		tag:      machine.Tag().String(),
   106  		password: password,
   107  		method:   "GET",
   108  		url:      s.backupURL(c),
   109  		nonce:    "fake_nonce",
   110  	})
   111  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid")
   112  
   113  	// Now try a user login.
   114  	resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.backupURL(c)})
   115  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "POST"`)
   116  }
   117  
   118  type backupsWithMacaroonsSuite struct {
   119  	backupsCommonSuite
   120  }
   121  
   122  var _ = gc.Suite(&backupsWithMacaroonsSuite{})
   123  
   124  func (s *backupsWithMacaroonsSuite) SetUpTest(c *gc.C) {
   125  	s.macaroonAuthEnabled = true
   126  	s.backupsCommonSuite.SetUpTest(c)
   127  }
   128  
   129  func (s *backupsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
   130  	resp := s.sendRequest(c, httpRequestParams{
   131  		method:   "GET",
   132  		jsonBody: &params.BackupsDownloadArgs{"bad-id"},
   133  		url:      s.backupURL(c),
   134  	})
   135  
   136  	errResp := s.assertErrorResponse(c, resp, http.StatusUnauthorized, "verification failed: no macaroons")
   137  	c.Assert(errResp.Code, gc.Equals, params.CodeDischargeRequired)
   138  	c.Assert(errResp.Info, gc.NotNil)
   139  	c.Assert(errResp.Info.Macaroon, gc.NotNil)
   140  }
   141  
   142  func (s *backupsWithMacaroonsSuite) TestCanGetWithDischargedMacaroon(c *gc.C) {
   143  	checkCount := 0
   144  	s.DischargerLogin = func() string {
   145  		checkCount++
   146  		return s.userTag.Id()
   147  	}
   148  	s.fake.Error = errors.New("failed!")
   149  	resp := s.sendRequest(c, httpRequestParams{
   150  		do:       s.doer(),
   151  		method:   "GET",
   152  		jsonBody: &params.BackupsDownloadArgs{"bad-id"},
   153  		url:      s.backupURL(c),
   154  	})
   155  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!")
   156  	c.Assert(checkCount, gc.Equals, 1)
   157  }
   158  
   159  // doer returns a Do function that can make a bakery request
   160  // appropriate for a backups endpoint.
   161  func (s *backupsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) {
   162  	return bakeryDo(nil, backupsBakeryGetError)
   163  }
   164  
   165  // backupsBakeryGetError implements a getError function
   166  // appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError
   167  // for the backups endpoint.
   168  func backupsBakeryGetError(resp *http.Response) error {
   169  	if resp.StatusCode != http.StatusUnauthorized {
   170  		return nil
   171  	}
   172  	data, err := ioutil.ReadAll(resp.Body)
   173  	if err != nil {
   174  		return errors.Annotatef(err, "cannot read body")
   175  	}
   176  	var errResp params.Error
   177  	if err := json.Unmarshal(data, &errResp); err != nil {
   178  		return errors.Annotatef(err, "cannot unmarshal body")
   179  	}
   180  	if errResp.Code != params.CodeDischargeRequired {
   181  		return &errResp
   182  	}
   183  	if errResp.Info == nil {
   184  		return errors.Annotatef(err, "no error info found in discharge-required response error")
   185  	}
   186  	// It's a discharge-required error, so make an appropriate httpbakery
   187  	// error from it.
   188  	return &httpbakery.Error{
   189  		Message: errResp.Message,
   190  		Code:    httpbakery.ErrDischargeRequired,
   191  		Info: &httpbakery.ErrorInfo{
   192  			Macaroon:     errResp.Info.Macaroon,
   193  			MacaroonPath: errResp.Info.MacaroonPath,
   194  		},
   195  	}
   196  }
   197  
   198  type backupsDownloadSuite struct {
   199  	backupsCommonSuite
   200  }
   201  
   202  var _ = gc.Suite(&backupsDownloadSuite{})
   203  
   204  // sendValid sends a valid GET request to the backups endpoint
   205  // and returns the response and the expected contents of the
   206  // archive if the request succeeds.
   207  func (s *backupsDownloadSuite) sendValidGet(c *gc.C) (resp *http.Response, archiveBytes []byte) {
   208  	meta := backupstesting.NewMetadata()
   209  	archive, err := backupstesting.NewArchiveBasic(meta)
   210  	c.Assert(err, jc.ErrorIsNil)
   211  	archiveBytes = archive.Bytes()
   212  	s.fake.Meta = meta
   213  	s.fake.Archive = ioutil.NopCloser(archive)
   214  
   215  	return s.authRequest(c, httpRequestParams{
   216  		method:      "GET",
   217  		url:         s.backupURL(c),
   218  		contentType: params.ContentTypeJSON,
   219  		jsonBody: params.BackupsDownloadArgs{
   220  			ID: meta.ID(),
   221  		},
   222  	}), archiveBytes
   223  }
   224  
   225  func (s *backupsDownloadSuite) TestCalls(c *gc.C) {
   226  	resp, _ := s.sendValidGet(c)
   227  	defer resp.Body.Close()
   228  
   229  	c.Check(s.fake.Calls, gc.DeepEquals, []string{"Get"})
   230  	c.Check(s.fake.IDArg, gc.Equals, s.fake.Meta.ID())
   231  }
   232  
   233  func (s *backupsDownloadSuite) TestResponse(c *gc.C) {
   234  	resp, _ := s.sendValidGet(c)
   235  	defer resp.Body.Close()
   236  	meta := s.fake.Meta
   237  
   238  	c.Check(resp.StatusCode, gc.Equals, http.StatusOK)
   239  	expectedChecksum := base64.StdEncoding.EncodeToString([]byte(meta.Checksum()))
   240  	c.Check(resp.Header.Get("Digest"), gc.Equals, string(params.DigestSHA256)+"="+expectedChecksum)
   241  	c.Check(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeRaw)
   242  }
   243  
   244  func (s *backupsDownloadSuite) TestBody(c *gc.C) {
   245  	resp, archiveBytes := s.sendValidGet(c)
   246  	defer resp.Body.Close()
   247  
   248  	body, err := ioutil.ReadAll(resp.Body)
   249  	c.Assert(err, jc.ErrorIsNil)
   250  	c.Check(body, jc.DeepEquals, archiveBytes)
   251  }
   252  
   253  func (s *backupsDownloadSuite) TestErrorWhenGetFails(c *gc.C) {
   254  	s.fake.Error = errors.New("failed!")
   255  	resp, _ := s.sendValidGet(c)
   256  	defer resp.Body.Close()
   257  
   258  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!")
   259  }
   260  
   261  type backupsUploadSuite struct {
   262  	backupsCommonSuite
   263  	meta *backups.Metadata
   264  }
   265  
   266  var _ = gc.Suite(&backupsUploadSuite{})
   267  
   268  func (s *backupsUploadSuite) sendValid(c *gc.C, id string) *http.Response {
   269  	s.fake.Meta = backups.NewMetadata()
   270  	s.fake.Meta.SetID("<a new backup ID>")
   271  
   272  	var parts bytes.Buffer
   273  	writer := multipart.NewWriter(&parts)
   274  
   275  	// Set the metadata part.
   276  	s.meta = backups.NewMetadata()
   277  	metaResult := apiserverbackups.ResultFromMetadata(s.meta)
   278  	header := make(textproto.MIMEHeader)
   279  	header.Set("Content-Disposition", `form-data; name="metadata"`)
   280  	header.Set("Content-Type", params.ContentTypeJSON)
   281  	part, err := writer.CreatePart(header)
   282  	c.Assert(err, jc.ErrorIsNil)
   283  	err = json.NewEncoder(part).Encode(metaResult)
   284  	c.Assert(err, jc.ErrorIsNil)
   285  
   286  	// Set the attached part.
   287  	archive := bytes.NewBufferString("<compressed data>")
   288  	part, err = writer.CreateFormFile("attached", "juju-backup.tar.gz")
   289  	c.Assert(err, jc.ErrorIsNil)
   290  	_, err = io.Copy(part, archive)
   291  	c.Assert(err, jc.ErrorIsNil)
   292  
   293  	// Send the request.
   294  	ctype := writer.FormDataContentType()
   295  	return s.authRequest(c, httpRequestParams{method: "PUT", url: s.backupURL(c), contentType: ctype, body: &parts})
   296  }
   297  
   298  func (s *backupsUploadSuite) TestCalls(c *gc.C) {
   299  	resp := s.sendValid(c, "<a new backup ID>")
   300  	defer resp.Body.Close()
   301  
   302  	c.Check(s.fake.Calls, gc.DeepEquals, []string{"Add"})
   303  	c.Check(s.fake.ArchiveArg, gc.NotNil)
   304  	c.Check(s.fake.MetaArg, jc.DeepEquals, s.meta)
   305  }
   306  
   307  func (s *backupsUploadSuite) TestResponse(c *gc.C) {
   308  	resp := s.sendValid(c, "<a new backup ID>")
   309  	defer resp.Body.Close()
   310  
   311  	c.Check(resp.StatusCode, gc.Equals, http.StatusOK)
   312  	c.Check(resp.Header.Get("Content-Type"), gc.Equals, params.ContentTypeJSON)
   313  }
   314  
   315  func (s *backupsUploadSuite) TestBody(c *gc.C) {
   316  	resp := s.sendValid(c, "<a new backup ID>")
   317  	defer resp.Body.Close()
   318  	body, err := ioutil.ReadAll(resp.Body)
   319  	c.Assert(err, jc.ErrorIsNil)
   320  	var result params.BackupsUploadResult
   321  	err = json.Unmarshal(body, &result)
   322  	c.Assert(err, jc.ErrorIsNil)
   323  
   324  	c.Check(result.ID, gc.Equals, "<a new backup ID>")
   325  }
   326  
   327  func (s *backupsUploadSuite) TestErrorWhenGetFails(c *gc.C) {
   328  	s.fake.Error = errors.New("failed!")
   329  	resp := s.sendValid(c, "<a new backup ID>")
   330  	defer resp.Body.Close()
   331  
   332  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, "failed!")
   333  }