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

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver_test
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"net/url"
    14  	"path"
    15  	"strings"
    16  
    17  	"github.com/juju/errors"
    18  	jc "github.com/juju/testing/checkers"
    19  	"github.com/juju/utils"
    20  	"github.com/juju/utils/arch"
    21  	"github.com/juju/utils/series"
    22  	"github.com/juju/version"
    23  	gc "gopkg.in/check.v1"
    24  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    25  
    26  	apiauthentication "github.com/juju/juju/api/authentication"
    27  	apitesting "github.com/juju/juju/api/testing"
    28  	commontesting "github.com/juju/juju/apiserver/common/testing"
    29  	"github.com/juju/juju/apiserver/params"
    30  	envtesting "github.com/juju/juju/environs/testing"
    31  	envtools "github.com/juju/juju/environs/tools"
    32  	toolstesting "github.com/juju/juju/environs/tools/testing"
    33  	"github.com/juju/juju/state"
    34  	"github.com/juju/juju/state/binarystorage"
    35  	"github.com/juju/juju/testing"
    36  	"github.com/juju/juju/testing/factory"
    37  	coretools "github.com/juju/juju/tools"
    38  	jujuversion "github.com/juju/juju/version"
    39  )
    40  
    41  // charmsCommonSuite wraps authHTTPSuite and adds
    42  // some helper methods suitable for working with the
    43  // tools endpoint.
    44  type toolsCommonSuite struct {
    45  	authHTTPSuite
    46  }
    47  
    48  func (s *toolsCommonSuite) toolsURL(c *gc.C, query string) *url.URL {
    49  	uri := s.baseURL(c)
    50  	uri.Path = fmt.Sprintf("/model/%s/tools", s.modelUUID)
    51  	uri.RawQuery = query
    52  	return uri
    53  }
    54  
    55  func (s *toolsCommonSuite) toolsURI(c *gc.C, query string) string {
    56  	if query != "" && query[0] == '?' {
    57  		query = query[1:]
    58  	}
    59  	return s.toolsURL(c, query).String()
    60  }
    61  
    62  func (s *toolsCommonSuite) downloadRequest(c *gc.C, version version.Binary, uuid string) *http.Response {
    63  	url := s.toolsURL(c, "")
    64  	if uuid == "" {
    65  		url.Path = fmt.Sprintf("/tools/%s", version)
    66  	} else {
    67  		url.Path = fmt.Sprintf("/model/%s/tools/%s", uuid, version)
    68  	}
    69  	return s.sendRequest(c, httpRequestParams{method: "GET", url: url.String()})
    70  }
    71  
    72  func (s *toolsCommonSuite) assertUploadResponse(c *gc.C, resp *http.Response, agentTools *coretools.Tools) {
    73  	toolsResponse := s.assertResponse(c, resp, http.StatusOK)
    74  	c.Check(toolsResponse.Error, gc.IsNil)
    75  	c.Check(toolsResponse.ToolsList, jc.DeepEquals, coretools.List{agentTools})
    76  }
    77  
    78  func (s *toolsCommonSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) {
    79  	body := assertResponse(c, resp, http.StatusOK, expContentType)
    80  	c.Check(string(body), gc.Equals, expBody)
    81  }
    82  
    83  func (s *toolsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
    84  	toolsResponse := s.assertResponse(c, resp, expCode)
    85  	c.Assert(toolsResponse.Error, gc.NotNil)
    86  	c.Assert(toolsResponse.Error.Message, gc.Matches, expError)
    87  }
    88  
    89  func (s *toolsCommonSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.ToolsResult {
    90  	body := assertResponse(c, resp, expStatus, params.ContentTypeJSON)
    91  	var toolsResponse params.ToolsResult
    92  	err := json.Unmarshal(body, &toolsResponse)
    93  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
    94  	return toolsResponse
    95  }
    96  
    97  type toolsSuite struct {
    98  	toolsCommonSuite
    99  	commontesting.BlockHelper
   100  }
   101  
   102  var _ = gc.Suite(&toolsSuite{})
   103  
   104  func (s *toolsSuite) SetUpTest(c *gc.C) {
   105  	s.toolsCommonSuite.SetUpTest(c)
   106  	s.BlockHelper = commontesting.NewBlockHelper(s.APIState)
   107  	s.AddCleanup(func(*gc.C) { s.BlockHelper.Close() })
   108  }
   109  
   110  func (s *toolsSuite) TestToolsUploadedSecurely(c *gc.C) {
   111  	info := s.APIInfo(c)
   112  	uri := "http://" + info.Addrs[0] + "/tools"
   113  	s.sendRequest(c, httpRequestParams{
   114  		method:      "PUT",
   115  		url:         uri,
   116  		expectError: `.*malformed HTTP response.*`,
   117  	})
   118  }
   119  
   120  func (s *toolsSuite) TestRequiresAuth(c *gc.C) {
   121  	resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.toolsURI(c, "")})
   122  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "no credentials provided")
   123  }
   124  
   125  func (s *toolsSuite) TestRequiresPOST(c *gc.C) {
   126  	resp := s.authRequest(c, httpRequestParams{method: "PUT", url: s.toolsURI(c, "")})
   127  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
   128  }
   129  
   130  func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) {
   131  	// Add a machine and try to login.
   132  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
   133  	c.Assert(err, jc.ErrorIsNil)
   134  	err = machine.SetProvisioned("foo", "fake_nonce", nil)
   135  	c.Assert(err, jc.ErrorIsNil)
   136  	password, err := utils.RandomPassword()
   137  	c.Assert(err, jc.ErrorIsNil)
   138  	err = machine.SetPassword(password)
   139  	c.Assert(err, jc.ErrorIsNil)
   140  
   141  	resp := s.sendRequest(c, httpRequestParams{tag: machine.Tag().String(), password: password, method: "POST", url: s.toolsURI(c, "")})
   142  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "machine 0 not provisioned")
   143  
   144  	// Now try a user login.
   145  	resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.toolsURI(c, "")})
   146  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
   147  }
   148  
   149  func (s *toolsSuite) TestUploadRequiresVersion(c *gc.C) {
   150  	resp := s.authRequest(c, httpRequestParams{method: "POST", url: s.toolsURI(c, "")})
   151  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
   152  }
   153  
   154  func (s *toolsSuite) TestUploadFailsWithNoTools(c *gc.C) {
   155  	// Create an empty file.
   156  	tempFile, err := ioutil.TempFile(c.MkDir(), "tools")
   157  	c.Assert(err, jc.ErrorIsNil)
   158  
   159  	resp := s.uploadRequest(c, s.toolsURI(c, "?binaryVersion=1.18.0-quantal-amd64"), "application/x-tar-gz", tempFile.Name())
   160  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "no tools uploaded")
   161  }
   162  
   163  func (s *toolsSuite) TestUploadFailsWithInvalidContentType(c *gc.C) {
   164  	// Create an empty file.
   165  	tempFile, err := ioutil.TempFile(c.MkDir(), "tools")
   166  	c.Assert(err, jc.ErrorIsNil)
   167  
   168  	// Now try with the default Content-Type.
   169  	resp := s.uploadRequest(c, s.toolsURI(c, "?binaryVersion=1.18.0-quantal-amd64"), "application/octet-stream", tempFile.Name())
   170  	s.assertErrorResponse(
   171  		c, resp, http.StatusBadRequest, "expected Content-Type: application/x-tar-gz, got: application/octet-stream")
   172  }
   173  
   174  func (s *toolsSuite) setupToolsForUpload(c *gc.C) (coretools.List, version.Binary, string) {
   175  	localStorage := c.MkDir()
   176  	vers := version.MustParseBinary("1.9.0-quantal-amd64")
   177  	versionStrings := []string{vers.String()}
   178  	expectedTools := toolstesting.MakeToolsWithCheckSum(c, localStorage, "released", versionStrings)
   179  	toolsFile := envtools.StorageName(vers, "released")
   180  	return expectedTools, vers, path.Join(localStorage, toolsFile)
   181  }
   182  
   183  func (s *toolsSuite) TestUpload(c *gc.C) {
   184  	// Make some fake tools.
   185  	expectedTools, v, toolPath := s.setupToolsForUpload(c)
   186  	vers := v.String()
   187  	// Now try uploading them.
   188  	resp := s.uploadRequest(
   189  		c, s.toolsURI(c, "?binaryVersion="+vers), "application/x-tar-gz", toolPath)
   190  
   191  	// Check the response.
   192  	expectedTools[0].URL = fmt.Sprintf("%s/model/%s/tools/%s", s.baseURL(c), s.State.ModelUUID(), vers)
   193  	s.assertUploadResponse(c, resp, expectedTools[0])
   194  
   195  	// Check the contents.
   196  	metadata, uploadedData := s.getToolsFromStorage(c, s.State, vers)
   197  	expectedData, err := ioutil.ReadFile(toolPath)
   198  	c.Assert(err, jc.ErrorIsNil)
   199  	c.Assert(uploadedData, gc.DeepEquals, expectedData)
   200  	allMetadata := s.getToolsMetadataFromStorage(c, s.State)
   201  	c.Assert(allMetadata, jc.DeepEquals, []binarystorage.Metadata{metadata})
   202  }
   203  
   204  func (s *toolsSuite) TestBlockUpload(c *gc.C) {
   205  	// Make some fake tools.
   206  	_, v, toolPath := s.setupToolsForUpload(c)
   207  	vers := v.String()
   208  	// Block all changes.
   209  	s.BlockAllChanges(c, "TestUpload")
   210  	// Now try uploading them.
   211  	resp := s.uploadRequest(
   212  		c, s.toolsURI(c, "?binaryVersion="+vers), "application/x-tar-gz", toolPath)
   213  	toolsResponse := s.assertResponse(c, resp, http.StatusBadRequest)
   214  	s.AssertBlocked(c, toolsResponse.Error, "TestUpload")
   215  
   216  	// Check the contents.
   217  	storage, err := s.State.ToolsStorage()
   218  	c.Assert(err, jc.ErrorIsNil)
   219  	defer storage.Close()
   220  	_, _, err = storage.Open(vers)
   221  	c.Assert(errors.IsNotFound(err), jc.IsTrue)
   222  }
   223  
   224  func (s *toolsSuite) TestUploadAllowsTopLevelPath(c *gc.C) {
   225  	// Backwards compatibility check, that we can upload tools to
   226  	// https://host:port/tools
   227  	expectedTools, vers, toolPath := s.setupToolsForUpload(c)
   228  	url := s.toolsURL(c, "binaryVersion="+vers.String())
   229  	url.Path = "/tools"
   230  	resp := s.uploadRequest(c, url.String(), "application/x-tar-gz", toolPath)
   231  	// Check the response.
   232  	expectedTools[0].URL = fmt.Sprintf("%s/model/%s/tools/%s", s.baseURL(c), s.State.ModelUUID(), vers)
   233  	s.assertUploadResponse(c, resp, expectedTools[0])
   234  }
   235  
   236  func (s *toolsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) {
   237  	// Check that we can upload tools to https://host:port/ModelUUID/tools
   238  	expectedTools, vers, toolPath := s.setupToolsForUpload(c)
   239  	url := s.toolsURL(c, "binaryVersion="+vers.String())
   240  	url.Path = fmt.Sprintf("/model/%s/tools", s.State.ModelUUID())
   241  	resp := s.uploadRequest(c, url.String(), "application/x-tar-gz", toolPath)
   242  	// Check the response.
   243  	expectedTools[0].URL = fmt.Sprintf("%s/model/%s/tools/%s", s.baseURL(c), s.State.ModelUUID(), vers)
   244  	s.assertUploadResponse(c, resp, expectedTools[0])
   245  }
   246  
   247  func (s *toolsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) {
   248  	envState := s.setupOtherModel(c)
   249  	// Check that we can upload tools to https://host:port/ModelUUID/tools
   250  	expectedTools, vers, toolPath := s.setupToolsForUpload(c)
   251  	url := s.toolsURL(c, "binaryVersion="+vers.String())
   252  	url.Path = fmt.Sprintf("/model/%s/tools", envState.ModelUUID())
   253  	resp := s.uploadRequest(c, url.String(), "application/x-tar-gz", toolPath)
   254  	// Check the response.
   255  	expectedTools[0].URL = fmt.Sprintf("%s/model/%s/tools/%s", s.baseURL(c), envState.ModelUUID(), vers)
   256  	s.assertUploadResponse(c, resp, expectedTools[0])
   257  }
   258  
   259  func (s *toolsSuite) TestUploadRejectsWrongModelUUIDPath(c *gc.C) {
   260  	// Check that we cannot access the tools at https://host:port/BADModelUUID/tools
   261  	url := s.toolsURL(c, "")
   262  	url.Path = "/model/dead-beef-123456/tools"
   263  	resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()})
   264  	s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
   265  }
   266  
   267  func (s *toolsSuite) TestUploadSeriesExpanded(c *gc.C) {
   268  	// Make some fake tools.
   269  	expectedTools, v, toolPath := s.setupToolsForUpload(c)
   270  	vers := v.String()
   271  	// Now try uploading them. The tools will be cloned for
   272  	// each additional series specified.
   273  	params := "?binaryVersion=" + vers + "&series=quantal,precise"
   274  	resp := s.uploadRequest(c, s.toolsURI(c, params), "application/x-tar-gz", toolPath)
   275  	c.Assert(resp.StatusCode, gc.Equals, http.StatusOK)
   276  
   277  	// Check the response.
   278  	info := s.APIInfo(c)
   279  	expectedTools[0].URL = fmt.Sprintf("%s/model/%s/tools/%s", s.baseURL(c), info.ModelTag.Id(), vers)
   280  	s.assertUploadResponse(c, resp, expectedTools[0])
   281  
   282  	// Check the contents.
   283  	storage, err := s.State.ToolsStorage()
   284  	c.Assert(err, jc.ErrorIsNil)
   285  	defer storage.Close()
   286  	expectedData, err := ioutil.ReadFile(toolPath)
   287  	c.Assert(err, jc.ErrorIsNil)
   288  	for _, series := range []string{"precise", "quantal"} {
   289  		v.Series = series
   290  		_, r, err := storage.Open(v.String())
   291  		c.Assert(err, jc.ErrorIsNil)
   292  		uploadedData, err := ioutil.ReadAll(r)
   293  		r.Close()
   294  		c.Assert(err, jc.ErrorIsNil)
   295  		c.Assert(uploadedData, gc.DeepEquals, expectedData)
   296  	}
   297  
   298  	// ensure other series *aren't* there.
   299  	v.Series = "trusty"
   300  	_, err = storage.Metadata(v.String())
   301  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   302  }
   303  
   304  func (s *toolsSuite) TestDownloadModelUUIDPath(c *gc.C) {
   305  	v := version.Binary{
   306  		Number: jujuversion.Current,
   307  		Arch:   arch.HostArch(),
   308  		Series: series.HostSeries(),
   309  	}
   310  	tools := s.storeFakeTools(c, s.State, "abc", binarystorage.Metadata{
   311  		Version: v.String(),
   312  		Size:    3,
   313  		SHA256:  "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
   314  	})
   315  	s.testDownload(c, tools, s.State.ModelUUID())
   316  }
   317  
   318  func (s *toolsSuite) TestDownloadOtherModelUUIDPath(c *gc.C) {
   319  	envState := s.setupOtherModel(c)
   320  	v := version.Binary{
   321  		Number: jujuversion.Current,
   322  		Arch:   arch.HostArch(),
   323  		Series: series.HostSeries(),
   324  	}
   325  	tools := s.storeFakeTools(c, envState, "abc", binarystorage.Metadata{
   326  		Version: v.String(),
   327  		Size:    3,
   328  		SHA256:  "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
   329  	})
   330  	s.testDownload(c, tools, envState.ModelUUID())
   331  }
   332  
   333  func (s *toolsSuite) TestDownloadTopLevelPath(c *gc.C) {
   334  	v := version.Binary{
   335  		Number: jujuversion.Current,
   336  		Arch:   arch.HostArch(),
   337  		Series: series.HostSeries(),
   338  	}
   339  	tools := s.storeFakeTools(c, s.State, "abc", binarystorage.Metadata{
   340  		Version: v.String(),
   341  		Size:    3,
   342  		SHA256:  "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
   343  	})
   344  	s.testDownload(c, tools, "")
   345  }
   346  
   347  func (s *toolsSuite) TestDownloadFetchesAndCaches(c *gc.C) {
   348  	// The tools are not in binarystorage, so the download request causes
   349  	// the API server to search for the tools in simplestreams, fetch
   350  	// them, and then cache them in binarystorage.
   351  	vers := version.MustParseBinary("1.23.0-trusty-amd64")
   352  	stor := s.DefaultToolsStorage
   353  	envtesting.RemoveTools(c, stor, "released")
   354  	tools := envtesting.AssertUploadFakeToolsVersions(c, stor, "released", "released", vers)[0]
   355  	data := s.testDownload(c, tools, "")
   356  
   357  	metadata, cachedData := s.getToolsFromStorage(c, s.State, tools.Version.String())
   358  	c.Assert(metadata.Size, gc.Equals, tools.Size)
   359  	c.Assert(metadata.SHA256, gc.Equals, tools.SHA256)
   360  	c.Assert(string(cachedData), gc.Equals, string(data))
   361  }
   362  
   363  func (s *toolsSuite) TestDownloadFetchesAndVerifiesSize(c *gc.C) {
   364  	// Upload fake tools, then upload over the top so the SHA256 hash does not match.
   365  	s.PatchValue(&jujuversion.Current, testing.FakeVersionNumber)
   366  	stor := s.DefaultToolsStorage
   367  	envtesting.RemoveTools(c, stor, "released")
   368  	current := version.Binary{
   369  		Number: jujuversion.Current,
   370  		Arch:   arch.HostArch(),
   371  		Series: series.HostSeries(),
   372  	}
   373  	tools := envtesting.AssertUploadFakeToolsVersions(c, stor, "released", "released", current)[0]
   374  	err := stor.Put(envtools.StorageName(tools.Version, "released"), strings.NewReader("!"), 1)
   375  	c.Assert(err, jc.ErrorIsNil)
   376  
   377  	resp := s.downloadRequest(c, tools.Version, "")
   378  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "error fetching tools: size mismatch for .*")
   379  	s.assertToolsNotStored(c, tools.Version.String())
   380  }
   381  
   382  func (s *toolsSuite) TestDownloadFetchesAndVerifiesHash(c *gc.C) {
   383  	// Upload fake tools, then upload over the top so the SHA256 hash does not match.
   384  	s.PatchValue(&jujuversion.Current, testing.FakeVersionNumber)
   385  	stor := s.DefaultToolsStorage
   386  	envtesting.RemoveTools(c, stor, "released")
   387  	current := version.Binary{
   388  		Number: jujuversion.Current,
   389  		Arch:   arch.HostArch(),
   390  		Series: series.HostSeries(),
   391  	}
   392  	tools := envtesting.AssertUploadFakeToolsVersions(c, stor, "released", "released", current)[0]
   393  	sameSize := strings.Repeat("!", int(tools.Size))
   394  	err := stor.Put(envtools.StorageName(tools.Version, "released"), strings.NewReader(sameSize), tools.Size)
   395  	c.Assert(err, jc.ErrorIsNil)
   396  
   397  	resp := s.downloadRequest(c, tools.Version, "")
   398  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "error fetching tools: hash mismatch for .*")
   399  	s.assertToolsNotStored(c, tools.Version.String())
   400  }
   401  
   402  func (s *toolsSuite) storeFakeTools(c *gc.C, st *state.State, content string, metadata binarystorage.Metadata) *coretools.Tools {
   403  	storage, err := st.ToolsStorage()
   404  	c.Assert(err, jc.ErrorIsNil)
   405  	defer storage.Close()
   406  	err = storage.Add(strings.NewReader(content), metadata)
   407  	c.Assert(err, jc.ErrorIsNil)
   408  	return &coretools.Tools{
   409  		Version: version.MustParseBinary(metadata.Version),
   410  		Size:    metadata.Size,
   411  		SHA256:  metadata.SHA256,
   412  	}
   413  }
   414  
   415  func (s *toolsSuite) getToolsFromStorage(c *gc.C, st *state.State, vers string) (binarystorage.Metadata, []byte) {
   416  	storage, err := st.ToolsStorage()
   417  	c.Assert(err, jc.ErrorIsNil)
   418  	defer storage.Close()
   419  	metadata, r, err := storage.Open(vers)
   420  	c.Assert(err, jc.ErrorIsNil)
   421  	data, err := ioutil.ReadAll(r)
   422  	r.Close()
   423  	c.Assert(err, jc.ErrorIsNil)
   424  	return metadata, data
   425  }
   426  
   427  func (s *toolsSuite) getToolsMetadataFromStorage(c *gc.C, st *state.State) []binarystorage.Metadata {
   428  	storage, err := st.ToolsStorage()
   429  	c.Assert(err, jc.ErrorIsNil)
   430  	defer storage.Close()
   431  	metadata, err := storage.AllMetadata()
   432  	c.Assert(err, jc.ErrorIsNil)
   433  	return metadata
   434  }
   435  
   436  func (s *toolsSuite) assertToolsNotStored(c *gc.C, vers string) {
   437  	storage, err := s.State.ToolsStorage()
   438  	c.Assert(err, jc.ErrorIsNil)
   439  	defer storage.Close()
   440  	_, err = storage.Metadata(vers)
   441  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   442  }
   443  
   444  func (s *toolsSuite) testDownload(c *gc.C, tools *coretools.Tools, uuid string) []byte {
   445  	resp := s.downloadRequest(c, tools.Version, uuid)
   446  	defer resp.Body.Close()
   447  	data, err := ioutil.ReadAll(resp.Body)
   448  	c.Assert(err, jc.ErrorIsNil)
   449  	c.Assert(data, gc.HasLen, int(tools.Size))
   450  
   451  	hash := sha256.New()
   452  	hash.Write(data)
   453  	c.Assert(fmt.Sprintf("%x", hash.Sum(nil)), gc.Equals, tools.SHA256)
   454  	return data
   455  }
   456  
   457  func (s *toolsSuite) TestDownloadRejectsWrongModelUUIDPath(c *gc.C) {
   458  	current := version.Binary{
   459  		Number: jujuversion.Current,
   460  		Arch:   arch.HostArch(),
   461  		Series: series.HostSeries(),
   462  	}
   463  	resp := s.downloadRequest(c, current, "dead-beef-123456")
   464  	s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
   465  }
   466  
   467  type toolsWithMacaroonsSuite struct {
   468  	toolsCommonSuite
   469  }
   470  
   471  var _ = gc.Suite(&toolsWithMacaroonsSuite{})
   472  
   473  func (s *toolsWithMacaroonsSuite) SetUpTest(c *gc.C) {
   474  	s.macaroonAuthEnabled = true
   475  	s.toolsCommonSuite.SetUpTest(c)
   476  }
   477  
   478  func (s *toolsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
   479  	resp := s.sendRequest(c, httpRequestParams{
   480  		method: "POST",
   481  		url:    s.toolsURI(c, ""),
   482  	})
   483  
   484  	charmResponse := s.assertResponse(c, resp, http.StatusUnauthorized)
   485  	c.Assert(charmResponse.Error, gc.NotNil)
   486  	c.Assert(charmResponse.Error.Message, gc.Equals, "verification failed: no macaroons")
   487  	c.Assert(charmResponse.Error.Code, gc.Equals, params.CodeDischargeRequired)
   488  	c.Assert(charmResponse.Error.Info, gc.NotNil)
   489  	c.Assert(charmResponse.Error.Info.Macaroon, gc.NotNil)
   490  }
   491  
   492  func (s *toolsWithMacaroonsSuite) TestCanPostWithDischargedMacaroon(c *gc.C) {
   493  	checkCount := 0
   494  	s.DischargerLogin = func() string {
   495  		checkCount++
   496  		return s.userTag.Id()
   497  	}
   498  	resp := s.sendRequest(c, httpRequestParams{
   499  		do:     s.doer(),
   500  		method: "POST",
   501  		url:    s.toolsURI(c, ""),
   502  	})
   503  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
   504  	c.Assert(checkCount, gc.Equals, 1)
   505  }
   506  
   507  func (s *toolsWithMacaroonsSuite) TestCanPostWithLocalLogin(c *gc.C) {
   508  	// Create a new local user that we can log in as
   509  	// using macaroon authentication.
   510  	const password = "hunter2"
   511  	user := s.Factory.MakeUser(c, &factory.UserParams{Password: password})
   512  
   513  	// Install a "web-page" visitor that deals with the interaction
   514  	// method that Juju controllers support for authenticating local
   515  	// users. Note: the use of httpbakery.NewMultiVisitor is necessary
   516  	// to trigger httpbakery to query the authentication methods and
   517  	// bypass browser authentication.
   518  	var prompted bool
   519  	jar := apitesting.NewClearableCookieJar()
   520  	client := utils.GetNonValidatingHTTPClient()
   521  	client.Jar = jar
   522  	bakeryClient := httpbakery.NewClient()
   523  	bakeryClient.Client = client
   524  	bakeryClient.WebPageVisitor = httpbakery.NewMultiVisitor(apiauthentication.NewVisitor(
   525  		user.UserTag().Id(),
   526  		func(username string) (string, error) {
   527  			c.Assert(username, gc.Equals, user.UserTag().Id())
   528  			prompted = true
   529  			return password, nil
   530  		},
   531  	))
   532  	bakeryDo := func(req *http.Request) (*http.Response, error) {
   533  		var body io.ReadSeeker
   534  		if req.Body != nil {
   535  			body = req.Body.(io.ReadSeeker)
   536  			req.Body = nil
   537  		}
   538  		return bakeryClient.DoWithBodyAndCustomError(req, body, bakeryGetError)
   539  	}
   540  
   541  	resp := s.sendRequest(c, httpRequestParams{
   542  		method:   "POST",
   543  		url:      s.toolsURI(c, ""),
   544  		tag:      user.UserTag().String(),
   545  		password: "", // no password forces macaroon usage
   546  		do:       bakeryDo,
   547  	})
   548  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
   549  	c.Assert(prompted, jc.IsTrue)
   550  }
   551  
   552  // doer returns a Do function that can make a bakery request
   553  // appropriate for a charms endpoint.
   554  func (s *toolsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) {
   555  	return bakeryDo(nil, bakeryGetError)
   556  }