github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/charms_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  	"encoding/json"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"mime"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"runtime"
    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/juju/charm.v6-unstable"
    22  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    23  
    24  	"github.com/juju/juju/apiserver"
    25  	"github.com/juju/juju/apiserver/params"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/state/storage"
    28  	"github.com/juju/juju/testcharms"
    29  	"github.com/juju/juju/testing/factory"
    30  )
    31  
    32  // charmsCommonSuite wraps authHTTPSuite and adds
    33  // some helper methods suitable for working with the
    34  // charms endpoint.
    35  type charmsCommonSuite struct {
    36  	authHTTPSuite
    37  }
    38  
    39  func (s *charmsCommonSuite) charmsURL(c *gc.C, query string) *url.URL {
    40  	uri := s.baseURL(c)
    41  	if s.modelUUID == "" {
    42  		uri.Path = "/charms"
    43  	} else {
    44  		uri.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
    45  	}
    46  	uri.RawQuery = query
    47  	return uri
    48  }
    49  
    50  func (s *charmsCommonSuite) charmsURI(c *gc.C, query string) string {
    51  	if query != "" && query[0] == '?' {
    52  		query = query[1:]
    53  	}
    54  	return s.charmsURL(c, query).String()
    55  }
    56  
    57  func (s *charmsCommonSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) {
    58  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
    59  	c.Check(charmResponse.Error, gc.Equals, "")
    60  	c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL)
    61  }
    62  
    63  func (s *charmsCommonSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) {
    64  	body := assertResponse(c, resp, http.StatusOK, expContentType)
    65  	c.Check(string(body), gc.Equals, expBody)
    66  }
    67  
    68  func (s *charmsCommonSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) {
    69  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
    70  	c.Check(charmResponse.Error, gc.Equals, "")
    71  	c.Check(charmResponse.Files, gc.DeepEquals, expFiles)
    72  }
    73  
    74  func (s *charmsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
    75  	charmResponse := s.assertResponse(c, resp, expCode)
    76  	c.Check(charmResponse.Error, gc.Matches, expError)
    77  }
    78  
    79  func (s *charmsCommonSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse {
    80  	body := assertResponse(c, resp, expStatus, params.ContentTypeJSON)
    81  	var charmResponse params.CharmsResponse
    82  	err := json.Unmarshal(body, &charmResponse)
    83  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
    84  	return charmResponse
    85  }
    86  
    87  func (s *charmsCommonSuite) setModelImporting(c *gc.C) {
    88  	model, err := s.State.Model()
    89  	c.Assert(err, jc.ErrorIsNil)
    90  	err = model.SetMigrationMode(state.MigrationModeImporting)
    91  	c.Assert(err, jc.ErrorIsNil)
    92  }
    93  
    94  type charmsSuite struct {
    95  	charmsCommonSuite
    96  }
    97  
    98  var _ = gc.Suite(&charmsSuite{})
    99  
   100  func (s *charmsSuite) SetUpSuite(c *gc.C) {
   101  	// TODO(bogdanteleaga): Fix this on windows
   102  	if runtime.GOOS == "windows" {
   103  		c.Skip("bug 1403084: Skipping this on windows for now")
   104  	}
   105  	s.charmsCommonSuite.SetUpSuite(c)
   106  }
   107  
   108  func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
   109  	info := s.APIInfo(c)
   110  	uri := "http://" + info.Addrs[0] + "/charms"
   111  	s.sendRequest(c, httpRequestParams{
   112  		method:      "GET",
   113  		url:         uri,
   114  		expectError: `.*malformed HTTP response.*`,
   115  	})
   116  }
   117  
   118  func (s *charmsSuite) TestPOSTRequiresAuth(c *gc.C) {
   119  	resp := s.sendRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
   120  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, ".*no credentials provided$")
   121  }
   122  
   123  func (s *charmsSuite) TestGETRequiresAuth(c *gc.C) {
   124  	resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.charmsURI(c, "")})
   125  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, ".*no credentials provided$")
   126  }
   127  
   128  func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
   129  	resp := s.authRequest(c, httpRequestParams{method: "PUT", url: s.charmsURI(c, "")})
   130  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
   131  }
   132  
   133  func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
   134  	// Add a machine and try to login.
   135  	machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
   136  		Nonce: "noncy",
   137  	})
   138  	resp := s.sendRequest(c, httpRequestParams{
   139  		tag:         machine.Tag().String(),
   140  		password:    password,
   141  		method:      "POST",
   142  		url:         s.charmsURI(c, ""),
   143  		nonce:       "noncy",
   144  		contentType: "foo/bar",
   145  	})
   146  	s.assertErrorResponse(c, resp, http.StatusInternalServerError, ".*tag kind machine not valid$")
   147  
   148  	// Now try a user login.
   149  	resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
   150  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
   151  }
   152  
   153  func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
   154  	// Create an empty file.
   155  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   156  	c.Assert(err, jc.ErrorIsNil)
   157  
   158  	// Pretend we upload a zip by setting the Content-Type, so we can
   159  	// check the error at extraction time later.
   160  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
   161  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*cannot open charm archive: zip: not a valid zip file$")
   162  
   163  	// Now try with the default Content-Type.
   164  	resp = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/octet-stream", tempFile.Name())
   165  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip, got: application/octet-stream$")
   166  }
   167  
   168  func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) {
   169  	// Add the dummy charm with revision 1.
   170  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   171  	curl := charm.MustParseURL(
   172  		fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
   173  	)
   174  	info := state.CharmInfo{
   175  		Charm:       ch,
   176  		ID:          curl,
   177  		StoragePath: "dummy-storage-path",
   178  		SHA256:      "dummy-1-sha256",
   179  	}
   180  	_, err := s.State.AddCharm(info)
   181  	c.Assert(err, jc.ErrorIsNil)
   182  
   183  	// Now try uploading the same revision and verify it gets bumped,
   184  	// and the BundleSha256 is calculated.
   185  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   186  	expectedURL := charm.MustParseURL("local:quantal/dummy-2")
   187  	s.assertUploadResponse(c, resp, expectedURL.String())
   188  	sch, err := s.State.Charm(expectedURL)
   189  	c.Assert(err, jc.ErrorIsNil)
   190  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   191  	c.Assert(sch.Revision(), gc.Equals, 2)
   192  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   193  	// No more checks for the hash here, because it is
   194  	// verified in TestUploadRespectsLocalRevision.
   195  	c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
   196  }
   197  
   198  func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) {
   199  	// Make a dummy charm dir with revision 123.
   200  	dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy")
   201  	dir.SetDiskRevision(123)
   202  	// Now bundle the dir.
   203  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   204  	c.Assert(err, jc.ErrorIsNil)
   205  	defer tempFile.Close()
   206  	defer os.Remove(tempFile.Name())
   207  	err = dir.ArchiveTo(tempFile)
   208  	c.Assert(err, jc.ErrorIsNil)
   209  
   210  	// Now try uploading it and ensure the revision persists.
   211  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
   212  	expectedURL := charm.MustParseURL("local:quantal/dummy-123")
   213  	s.assertUploadResponse(c, resp, expectedURL.String())
   214  	sch, err := s.State.Charm(expectedURL)
   215  	c.Assert(err, jc.ErrorIsNil)
   216  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   217  	c.Assert(sch.Revision(), gc.Equals, 123)
   218  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   219  
   220  	// First rewind the reader, which was reset but BundleTo() above.
   221  	_, err = tempFile.Seek(0, 0)
   222  	c.Assert(err, jc.ErrorIsNil)
   223  
   224  	// Finally, verify the SHA256.
   225  	expectedSHA256, _, err := utils.ReadSHA256(tempFile)
   226  	c.Assert(err, jc.ErrorIsNil)
   227  
   228  	c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
   229  
   230  	storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
   231  	reader, _, err := storage.Get(sch.StoragePath())
   232  	c.Assert(err, jc.ErrorIsNil)
   233  	defer reader.Close()
   234  	downloadedSHA256, _, err := utils.ReadSHA256(reader)
   235  	c.Assert(err, jc.ErrorIsNil)
   236  	c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
   237  }
   238  
   239  func (s *charmsSuite) TestUploadWithMultiSeriesCharm(c *gc.C) {
   240  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   241  	resp := s.uploadRequest(c, s.charmsURL(c, "").String(), "application/zip", ch.Path)
   242  	expectedURL := charm.MustParseURL("local:dummy-1")
   243  	s.assertUploadResponse(c, resp, expectedURL.String())
   244  }
   245  
   246  func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) {
   247  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   248  	// Backwards compatibility check, that we can upload charms to
   249  	// https://host:port/charms
   250  	url := s.charmsURL(c, "series=quantal")
   251  	url.Path = "/charms"
   252  	resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
   253  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   254  	s.assertUploadResponse(c, resp, expectedURL.String())
   255  }
   256  
   257  func (s *charmsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) {
   258  	// Check that we can upload charms to https://host:port/ModelUUID/charms
   259  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   260  	url := s.charmsURL(c, "series=quantal")
   261  	url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
   262  	resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
   263  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   264  	s.assertUploadResponse(c, resp, expectedURL.String())
   265  }
   266  
   267  func (s *charmsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) {
   268  	envState := s.setupOtherModel(c)
   269  	// Check that we can upload charms to https://host:port/ModelUUID/charms
   270  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   271  	url := s.charmsURL(c, "series=quantal")
   272  	url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
   273  	resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
   274  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   275  	s.assertUploadResponse(c, resp, expectedURL.String())
   276  }
   277  
   278  func (s *charmsSuite) TestUploadRejectsWrongModelUUIDPath(c *gc.C) {
   279  	// Check that we cannot upload charms to https://host:port/BADModelUUID/charms
   280  	url := s.charmsURL(c, "series=quantal")
   281  	url.Path = "/model/dead-beef-123456/charms"
   282  	resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()})
   283  	s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"$`)
   284  }
   285  
   286  func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
   287  	// Make a clone of the dummy charm in a nested directory.
   288  	rootDir := c.MkDir()
   289  	dirPath := filepath.Join(rootDir, "subdir1", "subdir2")
   290  	err := os.MkdirAll(dirPath, 0755)
   291  	c.Assert(err, jc.ErrorIsNil)
   292  	dir := testcharms.Repo.ClonedDir(dirPath, "dummy")
   293  	// Now tweak the path the dir thinks it is in and bundle it.
   294  	dir.Path = rootDir
   295  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   296  	c.Assert(err, jc.ErrorIsNil)
   297  	defer tempFile.Close()
   298  	defer os.Remove(tempFile.Name())
   299  	err = dir.ArchiveTo(tempFile)
   300  	c.Assert(err, jc.ErrorIsNil)
   301  
   302  	// Try reading it as a bundle - should fail due to nested dirs.
   303  	_, err = charm.ReadCharmArchive(tempFile.Name())
   304  	c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`)
   305  
   306  	// Now try uploading it - should succeeed and be repackaged.
   307  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
   308  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   309  	s.assertUploadResponse(c, resp, expectedURL.String())
   310  	sch, err := s.State.Charm(expectedURL)
   311  	c.Assert(err, jc.ErrorIsNil)
   312  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   313  	c.Assert(sch.Revision(), gc.Equals, 1)
   314  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   315  
   316  	// Get it from the storage and try to read it as a bundle - it
   317  	// should succeed, because it was repackaged during upload to
   318  	// strip nested dirs.
   319  	storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
   320  	reader, _, err := storage.Get(sch.StoragePath())
   321  	c.Assert(err, jc.ErrorIsNil)
   322  	defer reader.Close()
   323  
   324  	data, err := ioutil.ReadAll(reader)
   325  	c.Assert(err, jc.ErrorIsNil)
   326  	downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded")
   327  	c.Assert(err, jc.ErrorIsNil)
   328  	defer downloadedFile.Close()
   329  	defer os.Remove(downloadedFile.Name())
   330  	err = ioutil.WriteFile(downloadedFile.Name(), data, 0644)
   331  	c.Assert(err, jc.ErrorIsNil)
   332  
   333  	bundle, err := charm.ReadCharmArchive(downloadedFile.Name())
   334  	c.Assert(err, jc.ErrorIsNil)
   335  	c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision())
   336  	c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta())
   337  	c.Assert(bundle.Config(), jc.DeepEquals, sch.Config())
   338  }
   339  
   340  func (s *charmsSuite) TestNonLocalCharmUploadFailsIfNotMigrating(c *gc.C) {
   341  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   342  	curl := charm.MustParseURL(
   343  		fmt.Sprintf("cs:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
   344  	)
   345  	info := state.CharmInfo{
   346  		Charm:       ch,
   347  		ID:          curl,
   348  		StoragePath: "dummy-storage-path",
   349  		SHA256:      "dummy-1-sha256",
   350  	}
   351  	_, err := s.State.AddCharm(info)
   352  	c.Assert(err, jc.ErrorIsNil)
   353  
   354  	resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path)
   355  	s.assertErrorResponse(c, resp, 400, ".*cs charms may only be uploaded during model migration import$")
   356  }
   357  
   358  func (s *charmsSuite) TestNonLocalCharmUpload(c *gc.C) {
   359  	// Check that upload of charms with the "cs:" schema works (for
   360  	// model migrations).
   361  	s.setModelImporting(c)
   362  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   363  
   364  	resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path)
   365  
   366  	expectedURL := charm.MustParseURL("cs:quantal/dummy-1")
   367  	s.assertUploadResponse(c, resp, expectedURL.String())
   368  	sch, err := s.State.Charm(expectedURL)
   369  	c.Assert(err, jc.ErrorIsNil)
   370  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   371  	c.Assert(sch.Revision(), gc.Equals, 1)
   372  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   373  }
   374  
   375  func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) {
   376  	s.setModelImporting(c)
   377  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   378  
   379  	resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&name=dummy&revision=99"), "application/zip", ch.Path)
   380  
   381  	expectedURL := charm.MustParseURL("cs:dummy-99")
   382  	s.assertUploadResponse(c, resp, expectedURL.String())
   383  	sch, err := s.State.Charm(expectedURL)
   384  	c.Assert(err, jc.ErrorIsNil)
   385  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   386  	c.Assert(sch.Revision(), gc.Equals, 99)
   387  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   388  }
   389  
   390  func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) {
   391  	uri := s.charmsURI(c, "?file=hooks/install")
   392  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   393  	s.assertErrorResponse(
   394  		c, resp, http.StatusBadRequest,
   395  		".*expected url=CharmURL query argument$",
   396  	)
   397  }
   398  
   399  func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) {
   400  	uri := s.charmsURI(c, "?url=local:precise/no-such")
   401  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   402  	s.assertErrorResponse(
   403  		c, resp, http.StatusNotFound,
   404  		`.*cannot get charm from state: charm "local:precise/no-such" not found$`,
   405  	)
   406  }
   407  
   408  func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) {
   409  	// Add the dummy charm.
   410  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   411  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   412  
   413  	// Ensure a 404 is returned for files not included in the charm.
   414  	for i, file := range []string{
   415  		"no-such-file", "..", "../../../etc/passwd", "hooks/delete",
   416  	} {
   417  		c.Logf("test %d: %s", i, file)
   418  		uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+file)
   419  		resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   420  		c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound)
   421  	}
   422  }
   423  
   424  func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) {
   425  	// Add the dummy charm.
   426  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   427  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   428  
   429  	// Ensure a 403 is returned if the requested file is a directory.
   430  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=hooks")
   431  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   432  	c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden)
   433  }
   434  
   435  func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) {
   436  	// Add the dummy charm.
   437  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   438  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   439  
   440  	// Ensure the file contents are properly returned.
   441  	for i, t := range []struct {
   442  		summary  string
   443  		file     string
   444  		response string
   445  	}{{
   446  		summary:  "relative path",
   447  		file:     "revision",
   448  		response: "1",
   449  	}, {
   450  		summary:  "exotic path",
   451  		file:     "./hooks/../revision",
   452  		response: "1",
   453  	}, {
   454  		summary:  "sub-directory path",
   455  		file:     "hooks/install",
   456  		response: "#!/bin/bash\necho \"Done!\"\n",
   457  	},
   458  	} {
   459  		c.Logf("test %d: %s", i, t.summary)
   460  		uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+t.file)
   461  		resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   462  		s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8")
   463  	}
   464  }
   465  
   466  func (s *charmsSuite) TestGetCharmIcon(c *gc.C) {
   467  	// Upload the local charms.
   468  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "mysql")
   469  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   470  	ch = testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   471  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   472  
   473  	// Prepare the tests.
   474  	svgMimeType := mime.TypeByExtension(".svg")
   475  	iconPath := filepath.Join(testcharms.Repo.CharmDirPath("mysql"), "icon.svg")
   476  	icon, err := ioutil.ReadFile(iconPath)
   477  	c.Assert(err, jc.ErrorIsNil)
   478  	tests := []struct {
   479  		about      string
   480  		query      string
   481  		expectType string
   482  		expectBody string
   483  	}{{
   484  		about:      "icon found",
   485  		query:      "?url=local:quantal/mysql-1&file=icon.svg",
   486  		expectBody: string(icon),
   487  	}, {
   488  		about: "icon not found",
   489  		query: "?url=local:quantal/dummy-1&file=icon.svg",
   490  	}, {
   491  		about:      "default icon requested: icon found",
   492  		query:      "?url=local:quantal/mysql-1&icon=1",
   493  		expectBody: string(icon),
   494  	}, {
   495  		about:      "default icon requested: icon not found",
   496  		query:      "?url=local:quantal/dummy-1&icon=1",
   497  		expectBody: apiserver.DefaultIcon,
   498  	}, {
   499  		about:      "default icon request ignored",
   500  		query:      "?url=local:quantal/mysql-1&file=revision&icon=1",
   501  		expectType: "text/plain; charset=utf-8",
   502  		expectBody: "1",
   503  	}}
   504  
   505  	for i, test := range tests {
   506  		c.Logf("\ntest %d: %s", i, test.about)
   507  		uri := s.charmsURI(c, test.query)
   508  		resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   509  		if test.expectBody == "" {
   510  			s.assertErrorResponse(c, resp, http.StatusNotFound, ".*charm file not found$")
   511  			continue
   512  		}
   513  		if test.expectType == "" {
   514  			test.expectType = svgMimeType
   515  		}
   516  		s.assertGetFileResponse(c, resp, test.expectBody, test.expectType)
   517  	}
   518  }
   519  
   520  func (s *charmsSuite) TestGetWorksForControllerMachines(c *gc.C) {
   521  	// Make a controller machine.
   522  	const nonce = "noncey"
   523  	m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
   524  		Jobs:  []state.MachineJob{state.JobManageModel},
   525  		Nonce: nonce,
   526  	})
   527  
   528  	// Create a hosted model and upload a charm for it.
   529  	envState := s.setupOtherModel(c)
   530  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   531  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   532  
   533  	// Controller machine should be able to download the charm from
   534  	// the hosted model. This is required for controller workers which
   535  	// are acting on behalf of a particular hosted model.
   536  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   537  	url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
   538  	params := httpRequestParams{
   539  		method:   "GET",
   540  		url:      url.String(),
   541  		tag:      m.Tag().String(),
   542  		password: password,
   543  		nonce:    nonce,
   544  	}
   545  	resp := s.sendRequest(c, params)
   546  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   547  }
   548  
   549  func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) {
   550  	// Add the dummy charm.
   551  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   552  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   553  
   554  	data, err := ioutil.ReadFile(ch.Path)
   555  	c.Assert(err, jc.ErrorIsNil)
   556  
   557  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=*")
   558  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   559  	s.assertGetFileResponse(c, resp, string(data), "application/zip")
   560  }
   561  
   562  func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) {
   563  	// Backwards compatibility check, that we can GET from charms at
   564  	// https://host:port/charms
   565  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   566  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   567  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   568  	url.Path = "/charms"
   569  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   570  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   571  }
   572  
   573  func (s *charmsSuite) TestGetAllowsModelUUIDPath(c *gc.C) {
   574  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   575  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   576  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   577  	url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
   578  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   579  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   580  }
   581  
   582  func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) {
   583  	envState := s.setupOtherModel(c)
   584  
   585  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   586  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   587  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   588  	url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
   589  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   590  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   591  }
   592  
   593  func (s *charmsSuite) TestGetRejectsWrongModelUUIDPath(c *gc.C) {
   594  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   595  	url.Path = "/model/dead-beef-123456/charms"
   596  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   597  	s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"$`)
   598  }
   599  
   600  func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) {
   601  	// Add the dummy charm.
   602  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   603  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   604  
   605  	// Ensure charm files are properly listed.
   606  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1")
   607  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   608  	manifest, err := ch.Manifest()
   609  	c.Assert(err, jc.ErrorIsNil)
   610  	expectedFiles := manifest.SortedValues()
   611  	s.assertGetFileListResponse(c, resp, expectedFiles)
   612  	ctype := resp.Header.Get("content-type")
   613  	c.Assert(ctype, gc.Equals, params.ContentTypeJSON)
   614  }
   615  
   616  func (s *charmsSuite) TestNoTempFilesLeftBehind(c *gc.C) {
   617  	// Add the dummy charm.
   618  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   619  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   620  
   621  	// Download it.
   622  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=*")
   623  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   624  	assertResponse(c, resp, http.StatusOK, "application/zip")
   625  
   626  	// Ensure the tmp directory exists but nothing is in it.
   627  	files, err := ioutil.ReadDir(filepath.Join(s.DataDir(), "charm-get-tmp"))
   628  	c.Assert(err, jc.ErrorIsNil)
   629  	c.Check(files, gc.HasLen, 0)
   630  }
   631  
   632  func (s *charmsSuite) TestPOST_BadCharmNameErrorMessage(c *gc.C) {
   633  	url := s.charmsURL(c, "url=local:quantal/bad-name-1&file=revision")
   634  	url.Path = "/model/dead-beef-123456/charms"
   635  	resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()})
   636  	s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"$`)
   637  }
   638  
   639  type charmsWithMacaroonsSuite struct {
   640  	charmsCommonSuite
   641  }
   642  
   643  var _ = gc.Suite(&charmsWithMacaroonsSuite{})
   644  
   645  func (s *charmsWithMacaroonsSuite) SetUpTest(c *gc.C) {
   646  	s.macaroonAuthEnabled = true
   647  	s.authHTTPSuite.SetUpTest(c)
   648  }
   649  
   650  func (s *charmsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
   651  	resp := s.sendRequest(c, httpRequestParams{
   652  		method: "POST",
   653  		url:    s.charmsURI(c, ""),
   654  	})
   655  
   656  	charmResponse := s.assertResponse(c, resp, http.StatusUnauthorized)
   657  	c.Assert(charmResponse.Error, gc.Matches, ".*verification failed: no macaroons$")
   658  	c.Assert(charmResponse.ErrorCode, gc.Equals, params.CodeDischargeRequired)
   659  	c.Assert(charmResponse.ErrorInfo, gc.NotNil)
   660  	c.Assert(charmResponse.ErrorInfo.Macaroon, gc.NotNil)
   661  }
   662  
   663  func (s *charmsWithMacaroonsSuite) TestCanPostWithDischargedMacaroon(c *gc.C) {
   664  	checkCount := 0
   665  	s.DischargerLogin = func() string {
   666  		checkCount++
   667  		return s.userTag.Id()
   668  	}
   669  	resp := s.sendRequest(c, httpRequestParams{
   670  		do:          s.doer(),
   671  		method:      "POST",
   672  		url:         s.charmsURI(c, ""),
   673  		contentType: "foo/bar",
   674  	})
   675  	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
   676  	c.Assert(checkCount, gc.Equals, 1)
   677  }
   678  
   679  // doer returns a Do function that can make a bakery request
   680  // appropriate for a charms endpoint.
   681  func (s *charmsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) {
   682  	return bakeryDo(nil, charmsBakeryGetError)
   683  }
   684  
   685  // charmsBakeryGetError implements a getError function
   686  // appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError
   687  // for the charms endpoint.
   688  func charmsBakeryGetError(resp *http.Response) error {
   689  	if resp.StatusCode != http.StatusUnauthorized {
   690  		return nil
   691  	}
   692  	data, err := ioutil.ReadAll(resp.Body)
   693  	if err != nil {
   694  		return errors.Annotatef(err, "cannot read body")
   695  	}
   696  	var charmResp params.CharmsResponse
   697  	if err := json.Unmarshal(data, &charmResp); err != nil {
   698  		return errors.Annotatef(err, "cannot unmarshal body")
   699  	}
   700  	errResp := &params.Error{
   701  		Message: charmResp.Error,
   702  		Code:    charmResp.ErrorCode,
   703  		Info:    charmResp.ErrorInfo,
   704  	}
   705  	if errResp.Code != params.CodeDischargeRequired {
   706  		return errResp
   707  	}
   708  	if errResp.Info == nil {
   709  		return errors.Annotatef(err, "no error info found in discharge-required response error")
   710  	}
   711  	// It's a discharge-required error, so make an appropriate httpbakery
   712  	// error from it.
   713  	return &httpbakery.Error{
   714  		Message: errResp.Message,
   715  		Code:    httpbakery.ErrDischargeRequired,
   716  		Info: &httpbakery.ErrorInfo{
   717  			Macaroon:     errResp.Info.Macaroon,
   718  			MacaroonPath: errResp.Info.MacaroonPath,
   719  		},
   720  	}
   721  }