github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io/ioutil"
    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/params"
    25  	"github.com/juju/juju/state"
    26  	"github.com/juju/juju/state/storage"
    27  	"github.com/juju/juju/testcharms"
    28  )
    29  
    30  // charmsCommonSuite wraps authHttpSuite and adds
    31  // some helper methods suitable for working with the
    32  // charms endpoint.
    33  type charmsCommonSuite struct {
    34  	authHttpSuite
    35  }
    36  
    37  func (s *charmsCommonSuite) charmsURL(c *gc.C, query string) *url.URL {
    38  	uri := s.baseURL(c)
    39  	if s.modelUUID == "" {
    40  		uri.Path = "/charms"
    41  	} else {
    42  		uri.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
    43  	}
    44  	uri.RawQuery = query
    45  	return uri
    46  }
    47  
    48  func (s *charmsCommonSuite) charmsURI(c *gc.C, query string) string {
    49  	if query != "" && query[0] == '?' {
    50  		query = query[1:]
    51  	}
    52  	return s.charmsURL(c, query).String()
    53  }
    54  
    55  func (s *charmsCommonSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) {
    56  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
    57  	c.Check(charmResponse.Error, gc.Equals, "")
    58  	c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL)
    59  }
    60  
    61  func (s *charmsCommonSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) {
    62  	body := assertResponse(c, resp, http.StatusOK, expContentType)
    63  	c.Check(string(body), gc.Equals, expBody)
    64  }
    65  
    66  func (s *charmsCommonSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) {
    67  	charmResponse := s.assertResponse(c, resp, http.StatusOK)
    68  	c.Check(charmResponse.Error, gc.Equals, "")
    69  	c.Check(charmResponse.Files, gc.DeepEquals, expFiles)
    70  }
    71  
    72  func (s *charmsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
    73  	charmResponse := s.assertResponse(c, resp, expCode)
    74  	c.Check(charmResponse.Error, gc.Matches, expError)
    75  }
    76  
    77  func (s *charmsCommonSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse {
    78  	body := assertResponse(c, resp, expStatus, params.ContentTypeJSON)
    79  	var charmResponse params.CharmsResponse
    80  	err := json.Unmarshal(body, &charmResponse)
    81  	c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
    82  	return charmResponse
    83  }
    84  
    85  type charmsSuite struct {
    86  	charmsCommonSuite
    87  }
    88  
    89  var _ = gc.Suite(&charmsSuite{})
    90  
    91  func (s *charmsSuite) SetUpSuite(c *gc.C) {
    92  	// TODO(bogdanteleaga): Fix this on windows
    93  	if runtime.GOOS == "windows" {
    94  		c.Skip("bug 1403084: Skipping this on windows for now")
    95  	}
    96  	s.charmsCommonSuite.SetUpSuite(c)
    97  }
    98  
    99  func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
   100  	info := s.APIInfo(c)
   101  	uri := "http://" + info.Addrs[0] + "/charms"
   102  	s.sendRequest(c, httpRequestParams{
   103  		method:      "GET",
   104  		url:         uri,
   105  		expectError: `.*malformed HTTP response.*`,
   106  	})
   107  }
   108  
   109  func (s *charmsSuite) TestPOSTRequiresAuth(c *gc.C) {
   110  	resp := s.sendRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
   111  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "no credentials provided")
   112  }
   113  
   114  func (s *charmsSuite) TestGETDoesNotRequireAuth(c *gc.C) {
   115  	resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.charmsURI(c, "")})
   116  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected url=CharmURL query argument")
   117  }
   118  
   119  func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
   120  	resp := s.authRequest(c, httpRequestParams{method: "PUT", url: s.charmsURI(c, "")})
   121  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
   122  }
   123  
   124  func (s *charmsSuite) TestAuthRequiresUser(c *gc.C) {
   125  	// Add a machine and try to login.
   126  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
   127  	c.Assert(err, jc.ErrorIsNil)
   128  	err = machine.SetProvisioned("foo", "fake_nonce", nil)
   129  	c.Assert(err, jc.ErrorIsNil)
   130  	password, err := utils.RandomPassword()
   131  	c.Assert(err, jc.ErrorIsNil)
   132  	err = machine.SetPassword(password)
   133  	c.Assert(err, jc.ErrorIsNil)
   134  
   135  	resp := s.sendRequest(c, httpRequestParams{
   136  		tag:      machine.Tag().String(),
   137  		password: password,
   138  		method:   "POST",
   139  		url:      s.charmsURI(c, ""),
   140  		nonce:    "fake_nonce",
   141  	})
   142  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "invalid entity name or password")
   143  
   144  	// Now try a user login.
   145  	resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
   146  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument")
   147  }
   148  
   149  func (s *charmsSuite) TestUploadRequiresSeries(c *gc.C) {
   150  	resp := s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
   151  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument")
   152  }
   153  
   154  func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
   155  	// Create an empty file.
   156  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   157  	c.Assert(err, jc.ErrorIsNil)
   158  
   159  	// Pretend we upload a zip by setting the Content-Type, so we can
   160  	// check the error at extraction time later.
   161  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
   162  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "cannot open charm archive: zip: not a valid zip file")
   163  
   164  	// Now try with the default Content-Type.
   165  	resp = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/octet-stream", tempFile.Name())
   166  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip, got: application/octet-stream")
   167  }
   168  
   169  func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) {
   170  	// Add the dummy charm with revision 1.
   171  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   172  	curl := charm.MustParseURL(
   173  		fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
   174  	)
   175  	info := state.CharmInfo{
   176  		Charm:       ch,
   177  		ID:          curl,
   178  		StoragePath: "dummy-storage-path",
   179  		SHA256:      "dummy-1-sha256",
   180  	}
   181  	_, err := s.State.AddCharm(info)
   182  	c.Assert(err, jc.ErrorIsNil)
   183  
   184  	// Now try uploading the same revision and verify it gets bumped,
   185  	// and the BundleSha256 is calculated.
   186  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   187  	expectedURL := charm.MustParseURL("local:quantal/dummy-2")
   188  	s.assertUploadResponse(c, resp, expectedURL.String())
   189  	sch, err := s.State.Charm(expectedURL)
   190  	c.Assert(err, jc.ErrorIsNil)
   191  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   192  	c.Assert(sch.Revision(), gc.Equals, 2)
   193  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   194  	// No more checks for the hash here, because it is
   195  	// verified in TestUploadRespectsLocalRevision.
   196  	c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
   197  }
   198  
   199  func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) {
   200  	// Make a dummy charm dir with revision 123.
   201  	dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy")
   202  	dir.SetDiskRevision(123)
   203  	// Now bundle the dir.
   204  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   205  	c.Assert(err, jc.ErrorIsNil)
   206  	defer tempFile.Close()
   207  	defer os.Remove(tempFile.Name())
   208  	err = dir.ArchiveTo(tempFile)
   209  	c.Assert(err, jc.ErrorIsNil)
   210  
   211  	// Now try uploading it and ensure the revision persists.
   212  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
   213  	expectedURL := charm.MustParseURL("local:quantal/dummy-123")
   214  	s.assertUploadResponse(c, resp, expectedURL.String())
   215  	sch, err := s.State.Charm(expectedURL)
   216  	c.Assert(err, jc.ErrorIsNil)
   217  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   218  	c.Assert(sch.Revision(), gc.Equals, 123)
   219  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   220  
   221  	// First rewind the reader, which was reset but BundleTo() above.
   222  	_, err = tempFile.Seek(0, 0)
   223  	c.Assert(err, jc.ErrorIsNil)
   224  
   225  	// Finally, verify the SHA256.
   226  	expectedSHA256, _, err := utils.ReadSHA256(tempFile)
   227  	c.Assert(err, jc.ErrorIsNil)
   228  
   229  	c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
   230  
   231  	storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
   232  	reader, _, err := storage.Get(sch.StoragePath())
   233  	c.Assert(err, jc.ErrorIsNil)
   234  	defer reader.Close()
   235  	downloadedSHA256, _, err := utils.ReadSHA256(reader)
   236  	c.Assert(err, jc.ErrorIsNil)
   237  	c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
   238  }
   239  
   240  func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) {
   241  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   242  	// Backwards compatibility check, that we can upload charms to
   243  	// https://host:port/charms
   244  	url := s.charmsURL(c, "series=quantal")
   245  	url.Path = "/charms"
   246  	resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
   247  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   248  	s.assertUploadResponse(c, resp, expectedURL.String())
   249  }
   250  
   251  func (s *charmsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) {
   252  	// Check that we can upload charms to https://host:port/ModelUUID/charms
   253  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   254  	url := s.charmsURL(c, "series=quantal")
   255  	url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
   256  	resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
   257  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   258  	s.assertUploadResponse(c, resp, expectedURL.String())
   259  }
   260  
   261  func (s *charmsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) {
   262  	envState := s.setupOtherModel(c)
   263  	// Check that we can upload charms to https://host:port/ModelUUID/charms
   264  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   265  	url := s.charmsURL(c, "series=quantal")
   266  	url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
   267  	resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
   268  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   269  	s.assertUploadResponse(c, resp, expectedURL.String())
   270  }
   271  
   272  func (s *charmsSuite) TestUploadRejectsWrongModelUUIDPath(c *gc.C) {
   273  	// Check that we cannot upload charms to https://host:port/BADModelUUID/charms
   274  	url := s.charmsURL(c, "series=quantal")
   275  	url.Path = "/model/dead-beef-123456/charms"
   276  	resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()})
   277  	s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
   278  }
   279  
   280  func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
   281  	// Make a clone of the dummy charm in a nested directory.
   282  	rootDir := c.MkDir()
   283  	dirPath := filepath.Join(rootDir, "subdir1", "subdir2")
   284  	err := os.MkdirAll(dirPath, 0755)
   285  	c.Assert(err, jc.ErrorIsNil)
   286  	dir := testcharms.Repo.ClonedDir(dirPath, "dummy")
   287  	// Now tweak the path the dir thinks it is in and bundle it.
   288  	dir.Path = rootDir
   289  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   290  	c.Assert(err, jc.ErrorIsNil)
   291  	defer tempFile.Close()
   292  	defer os.Remove(tempFile.Name())
   293  	err = dir.ArchiveTo(tempFile)
   294  	c.Assert(err, jc.ErrorIsNil)
   295  
   296  	// Try reading it as a bundle - should fail due to nested dirs.
   297  	_, err = charm.ReadCharmArchive(tempFile.Name())
   298  	c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`)
   299  
   300  	// Now try uploading it - should succeeed and be repackaged.
   301  	resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
   302  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   303  	s.assertUploadResponse(c, resp, expectedURL.String())
   304  	sch, err := s.State.Charm(expectedURL)
   305  	c.Assert(err, jc.ErrorIsNil)
   306  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   307  	c.Assert(sch.Revision(), gc.Equals, 1)
   308  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   309  
   310  	// Get it from the storage and try to read it as a bundle - it
   311  	// should succeed, because it was repackaged during upload to
   312  	// strip nested dirs.
   313  	storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
   314  	reader, _, err := storage.Get(sch.StoragePath())
   315  	c.Assert(err, jc.ErrorIsNil)
   316  	defer reader.Close()
   317  
   318  	data, err := ioutil.ReadAll(reader)
   319  	c.Assert(err, jc.ErrorIsNil)
   320  	downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded")
   321  	c.Assert(err, jc.ErrorIsNil)
   322  	defer downloadedFile.Close()
   323  	defer os.Remove(downloadedFile.Name())
   324  	err = ioutil.WriteFile(downloadedFile.Name(), data, 0644)
   325  	c.Assert(err, jc.ErrorIsNil)
   326  
   327  	bundle, err := charm.ReadCharmArchive(downloadedFile.Name())
   328  	c.Assert(err, jc.ErrorIsNil)
   329  	c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision())
   330  	c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta())
   331  	c.Assert(bundle.Config(), jc.DeepEquals, sch.Config())
   332  }
   333  
   334  func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) {
   335  	uri := s.charmsURI(c, "?file=hooks/install")
   336  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   337  	s.assertErrorResponse(
   338  		c, resp, http.StatusBadRequest,
   339  		"expected url=CharmURL query argument",
   340  	)
   341  }
   342  
   343  func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) {
   344  	uri := s.charmsURI(c, "?url=local:precise/no-such")
   345  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   346  	s.assertErrorResponse(
   347  		c, resp, http.StatusNotFound,
   348  		`unable to retrieve and save the charm: cannot get charm from state: charm "local:precise/no-such" not found`,
   349  	)
   350  }
   351  
   352  func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) {
   353  	// Add the dummy charm.
   354  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   355  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   356  
   357  	// Ensure a 404 is returned for files not included in the charm.
   358  	for i, file := range []string{
   359  		"no-such-file", "..", "../../../etc/passwd", "hooks/delete",
   360  	} {
   361  		c.Logf("test %d: %s", i, file)
   362  		uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+file)
   363  		resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   364  		c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound)
   365  	}
   366  }
   367  
   368  func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) {
   369  	// Add the dummy charm.
   370  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   371  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   372  
   373  	// Ensure a 403 is returned if the requested file is a directory.
   374  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=hooks")
   375  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   376  	c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden)
   377  }
   378  
   379  func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) {
   380  	// Add the dummy charm.
   381  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   382  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   383  
   384  	// Ensure the file contents are properly returned.
   385  	for i, t := range []struct {
   386  		summary  string
   387  		file     string
   388  		response string
   389  	}{{
   390  		summary:  "relative path",
   391  		file:     "revision",
   392  		response: "1",
   393  	}, {
   394  		summary:  "exotic path",
   395  		file:     "./hooks/../revision",
   396  		response: "1",
   397  	}, {
   398  		summary:  "sub-directory path",
   399  		file:     "hooks/install",
   400  		response: "#!/bin/bash\necho \"Done!\"\n",
   401  	},
   402  	} {
   403  		c.Logf("test %d: %s", i, t.summary)
   404  		uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+t.file)
   405  		resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   406  		s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8")
   407  	}
   408  }
   409  
   410  func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) {
   411  	// Add the dummy charm.
   412  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   413  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   414  
   415  	data, err := ioutil.ReadFile(ch.Path)
   416  	c.Assert(err, jc.ErrorIsNil)
   417  
   418  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=*")
   419  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   420  	s.assertGetFileResponse(c, resp, string(data), "application/zip")
   421  }
   422  
   423  func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) {
   424  	// Backwards compatibility check, that we can GET from charms at
   425  	// https://host:port/charms
   426  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   427  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   428  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   429  	url.Path = "/charms"
   430  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   431  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   432  }
   433  
   434  func (s *charmsSuite) TestGetAllowsModelUUIDPath(c *gc.C) {
   435  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   436  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   437  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   438  	url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
   439  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   440  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   441  }
   442  
   443  func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) {
   444  	envState := s.setupOtherModel(c)
   445  
   446  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   447  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   448  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   449  	url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
   450  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   451  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   452  }
   453  
   454  func (s *charmsSuite) TestGetRejectsWrongModelUUIDPath(c *gc.C) {
   455  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   456  	url.Path = "/model/dead-beef-123456/charms"
   457  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
   458  	s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
   459  }
   460  
   461  func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) {
   462  	// Add the dummy charm.
   463  	ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
   464  	s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
   465  
   466  	// Ensure charm files are properly listed.
   467  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1")
   468  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   469  	manifest, err := ch.Manifest()
   470  	c.Assert(err, jc.ErrorIsNil)
   471  	expectedFiles := manifest.SortedValues()
   472  	s.assertGetFileListResponse(c, resp, expectedFiles)
   473  	ctype := resp.Header.Get("content-type")
   474  	c.Assert(ctype, gc.Equals, params.ContentTypeJSON)
   475  }
   476  
   477  func (s *charmsSuite) TestGetUsesCache(c *gc.C) {
   478  	// Add a fake charm archive in the cache directory.
   479  	cacheDir := filepath.Join(s.DataDir(), "charm-get-cache", s.State.ModelUUID())
   480  	err := os.MkdirAll(cacheDir, 0755)
   481  	c.Assert(err, jc.ErrorIsNil)
   482  
   483  	// Create and save a bundle in it.
   484  	charmDir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy")
   485  	testPath := filepath.Join(charmDir.Path, "utils.js")
   486  	contents := "// blah blah"
   487  	err = ioutil.WriteFile(testPath, []byte(contents), 0755)
   488  	c.Assert(err, jc.ErrorIsNil)
   489  	var buffer bytes.Buffer
   490  	err = charmDir.ArchiveTo(&buffer)
   491  	c.Assert(err, jc.ErrorIsNil)
   492  	charmArchivePath := filepath.Join(
   493  		cacheDir, charm.Quote("local:trusty/django-42")+".zip")
   494  	err = ioutil.WriteFile(charmArchivePath, buffer.Bytes(), 0644)
   495  	c.Assert(err, jc.ErrorIsNil)
   496  
   497  	// Ensure the cached contents are properly retrieved.
   498  	uri := s.charmsURI(c, "?url=local:trusty/django-42&file=utils.js")
   499  	resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
   500  	s.assertGetFileResponse(c, resp, contents, params.ContentTypeJS)
   501  }
   502  
   503  type charmsWithMacaroonsSuite struct {
   504  	charmsCommonSuite
   505  }
   506  
   507  var _ = gc.Suite(&charmsWithMacaroonsSuite{})
   508  
   509  func (s *charmsWithMacaroonsSuite) SetUpTest(c *gc.C) {
   510  	s.macaroonAuthEnabled = true
   511  	s.authHttpSuite.SetUpTest(c)
   512  }
   513  
   514  func (s *charmsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
   515  	resp := s.sendRequest(c, httpRequestParams{
   516  		method: "POST",
   517  		url:    s.charmsURI(c, ""),
   518  	})
   519  
   520  	charmResponse := s.assertResponse(c, resp, http.StatusUnauthorized)
   521  	c.Assert(charmResponse.Error, gc.Equals, "verification failed: no macaroons")
   522  	c.Assert(charmResponse.ErrorCode, gc.Equals, params.CodeDischargeRequired)
   523  	c.Assert(charmResponse.ErrorInfo, gc.NotNil)
   524  	c.Assert(charmResponse.ErrorInfo.Macaroon, gc.NotNil)
   525  }
   526  
   527  func (s *charmsWithMacaroonsSuite) TestCanPostWithDischargedMacaroon(c *gc.C) {
   528  	checkCount := 0
   529  	s.DischargerLogin = func() string {
   530  		checkCount++
   531  		return s.userTag.Id()
   532  	}
   533  	resp := s.sendRequest(c, httpRequestParams{
   534  		do:     s.doer(),
   535  		method: "POST",
   536  		url:    s.charmsURI(c, ""),
   537  	})
   538  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument")
   539  	c.Assert(checkCount, gc.Equals, 1)
   540  }
   541  
   542  // doer returns a Do function that can make a bakery request
   543  // appropriate for a charms endpoint.
   544  func (s *charmsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) {
   545  	return bakeryDo(nil, charmsBakeryGetError)
   546  }
   547  
   548  // charmsBakeryGetError implements a getError function
   549  // appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError
   550  // for the charms endpoint.
   551  func charmsBakeryGetError(resp *http.Response) error {
   552  	if resp.StatusCode != http.StatusUnauthorized {
   553  		return nil
   554  	}
   555  	data, err := ioutil.ReadAll(resp.Body)
   556  	if err != nil {
   557  		return errors.Annotatef(err, "cannot read body")
   558  	}
   559  	var charmResp params.CharmsResponse
   560  	if err := json.Unmarshal(data, &charmResp); err != nil {
   561  		return errors.Annotatef(err, "cannot unmarshal body")
   562  	}
   563  	errResp := &params.Error{
   564  		Message: charmResp.Error,
   565  		Code:    charmResp.ErrorCode,
   566  		Info:    charmResp.ErrorInfo,
   567  	}
   568  	if errResp.Code != params.CodeDischargeRequired {
   569  		return errResp
   570  	}
   571  	if errResp.Info == nil {
   572  		return errors.Annotatef(err, "no error info found in discharge-required response error")
   573  	}
   574  	// It's a discharge-required error, so make an appropriate httpbakery
   575  	// error from it.
   576  	return &httpbakery.Error{
   577  		Message: errResp.Message,
   578  		Code:    httpbakery.ErrDischargeRequired,
   579  		Info: &httpbakery.ErrorInfo{
   580  			Macaroon:     errResp.Info.Macaroon,
   581  			MacaroonPath: errResp.Info.MacaroonPath,
   582  		},
   583  	}
   584  }