github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/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"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	jc "github.com/juju/testing/checkers"
    19  	"github.com/juju/utils"
    20  	gc "launchpad.net/gocheck"
    21  
    22  	"github.com/juju/juju/charm"
    23  	charmtesting "github.com/juju/juju/charm/testing"
    24  	"github.com/juju/juju/environs"
    25  	jujutesting "github.com/juju/juju/juju/testing"
    26  	"github.com/juju/juju/state"
    27  	"github.com/juju/juju/state/api/params"
    28  )
    29  
    30  type authHttpSuite struct {
    31  	jujutesting.JujuConnSuite
    32  	userTag            string
    33  	password           string
    34  	archiveContentType string
    35  }
    36  
    37  func (s *authHttpSuite) SetUpTest(c *gc.C) {
    38  	s.JujuConnSuite.SetUpTest(c)
    39  	user := s.AddUser(c, "joe")
    40  	s.userTag = user.Tag()
    41  	s.password = "password"
    42  }
    43  
    44  func (s *authHttpSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) {
    45  	req, err := http.NewRequest(method, uri, body)
    46  	c.Assert(err, gc.IsNil)
    47  	if tag != "" && password != "" {
    48  		req.SetBasicAuth(tag, password)
    49  	}
    50  	if contentType != "" {
    51  		req.Header.Set("Content-Type", contentType)
    52  	}
    53  	return utils.GetNonValidatingHTTPClient().Do(req)
    54  }
    55  
    56  func (s *authHttpSuite) baseURL(c *gc.C) *url.URL {
    57  	_, info, err := s.APIConn.Environ.StateInfo()
    58  	c.Assert(err, gc.IsNil)
    59  	return &url.URL{
    60  		Scheme: "https",
    61  		Host:   info.Addrs[0],
    62  		Path:   "",
    63  	}
    64  }
    65  
    66  func (s *authHttpSuite) authRequest(c *gc.C, method, uri, contentType string, body io.Reader) (*http.Response, error) {
    67  	return s.sendRequest(c, s.userTag, s.password, method, uri, contentType, body)
    68  }
    69  
    70  func (s *authHttpSuite) uploadRequest(c *gc.C, uri string, asZip bool, path string) (*http.Response, error) {
    71  	contentType := "application/octet-stream"
    72  	if asZip {
    73  		contentType = s.archiveContentType
    74  	}
    75  
    76  	if path == "" {
    77  		return s.authRequest(c, "POST", uri, contentType, nil)
    78  	}
    79  
    80  	file, err := os.Open(path)
    81  	c.Assert(err, gc.IsNil)
    82  	defer file.Close()
    83  	return s.authRequest(c, "POST", uri, contentType, file)
    84  }
    85  
    86  type charmsSuite struct {
    87  	authHttpSuite
    88  }
    89  
    90  var _ = gc.Suite(&charmsSuite{})
    91  
    92  func (s *charmsSuite) SetUpSuite(c *gc.C) {
    93  	s.JujuConnSuite.SetUpSuite(c)
    94  	s.archiveContentType = "application/zip"
    95  }
    96  
    97  func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
    98  	_, info, err := s.APIConn.Environ.StateInfo()
    99  	c.Assert(err, gc.IsNil)
   100  	uri := "http://" + info.Addrs[0] + "/charms"
   101  	_, err = s.sendRequest(c, "", "", "GET", uri, "", nil)
   102  	c.Assert(err, gc.ErrorMatches, `.*malformed HTTP response.*`)
   103  }
   104  
   105  func (s *charmsSuite) TestRequiresAuth(c *gc.C) {
   106  	resp, err := s.sendRequest(c, "", "", "GET", s.charmsURI(c, ""), "", nil)
   107  	c.Assert(err, gc.IsNil)
   108  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "unauthorized")
   109  }
   110  
   111  func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
   112  	resp, err := s.authRequest(c, "PUT", s.charmsURI(c, ""), "", nil)
   113  	c.Assert(err, gc.IsNil)
   114  	s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
   115  }
   116  
   117  func (s *charmsSuite) TestAuthRequiresUser(c *gc.C) {
   118  	// Add a machine and try to login.
   119  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
   120  	c.Assert(err, gc.IsNil)
   121  	err = machine.SetProvisioned("foo", "fake_nonce", nil)
   122  	c.Assert(err, gc.IsNil)
   123  	password, err := utils.RandomPassword()
   124  	c.Assert(err, gc.IsNil)
   125  	err = machine.SetPassword(password)
   126  	c.Assert(err, gc.IsNil)
   127  
   128  	resp, err := s.sendRequest(c, machine.Tag(), password, "GET", s.charmsURI(c, ""), "", nil)
   129  	c.Assert(err, gc.IsNil)
   130  	s.assertErrorResponse(c, resp, http.StatusUnauthorized, "unauthorized")
   131  
   132  	// Now try a user login.
   133  	resp, err = s.authRequest(c, "GET", s.charmsURI(c, ""), "", nil)
   134  	c.Assert(err, gc.IsNil)
   135  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected url=CharmURL query argument")
   136  }
   137  
   138  func (s *charmsSuite) TestUploadRequiresSeries(c *gc.C) {
   139  	resp, err := s.authRequest(c, "POST", s.charmsURI(c, ""), "", nil)
   140  	c.Assert(err, gc.IsNil)
   141  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected series=URL argument")
   142  }
   143  
   144  func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
   145  	// Create an empty file.
   146  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   147  	c.Assert(err, gc.IsNil)
   148  
   149  	// Pretend we upload a zip by setting the Content-Type, so we can
   150  	// check the error at extraction time later.
   151  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name())
   152  	c.Assert(err, gc.IsNil)
   153  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "cannot open charm archive: zip: not a valid zip file")
   154  
   155  	// Now try with the default Content-Type.
   156  	resp, err = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), false, tempFile.Name())
   157  	c.Assert(err, gc.IsNil)
   158  	s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip, got: application/octet-stream")
   159  }
   160  
   161  func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) {
   162  	// Add the dummy charm with revision 1.
   163  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   164  	curl := charm.MustParseURL(
   165  		fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
   166  	)
   167  	bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
   168  	c.Assert(err, gc.IsNil)
   169  	_, err = s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
   170  	c.Assert(err, gc.IsNil)
   171  
   172  	// Now try uploading the same revision and verify it gets bumped,
   173  	// and the BundleURL and BundleSha256 are calculated.
   174  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   175  	c.Assert(err, gc.IsNil)
   176  	expectedURL := charm.MustParseURL("local:quantal/dummy-2")
   177  	s.assertUploadResponse(c, resp, expectedURL.String())
   178  	sch, err := s.State.Charm(expectedURL)
   179  	c.Assert(err, gc.IsNil)
   180  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   181  	c.Assert(sch.Revision(), gc.Equals, 2)
   182  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   183  	// No more checks for these two here, because they
   184  	// are verified in TestUploadRespectsLocalRevision.
   185  	c.Assert(sch.BundleURL(), gc.Not(gc.Equals), "")
   186  	c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
   187  }
   188  
   189  func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) {
   190  	// Make a dummy charm dir with revision 123.
   191  	dir := charmtesting.Charms.ClonedDir(c.MkDir(), "dummy")
   192  	dir.SetDiskRevision(123)
   193  	// Now bundle the dir.
   194  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   195  	c.Assert(err, gc.IsNil)
   196  	defer tempFile.Close()
   197  	defer os.Remove(tempFile.Name())
   198  	err = dir.BundleTo(tempFile)
   199  	c.Assert(err, gc.IsNil)
   200  
   201  	// Now try uploading it and ensure the revision persists.
   202  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name())
   203  	c.Assert(err, gc.IsNil)
   204  	expectedURL := charm.MustParseURL("local:quantal/dummy-123")
   205  	s.assertUploadResponse(c, resp, expectedURL.String())
   206  	sch, err := s.State.Charm(expectedURL)
   207  	c.Assert(err, gc.IsNil)
   208  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   209  	c.Assert(sch.Revision(), gc.Equals, 123)
   210  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   211  
   212  	// First rewind the reader, which was reset but BundleTo() above.
   213  	_, err = tempFile.Seek(0, 0)
   214  	c.Assert(err, gc.IsNil)
   215  
   216  	// Finally, verify the SHA256 and uploaded URL.
   217  	expectedSHA256, _, err := utils.ReadSHA256(tempFile)
   218  	c.Assert(err, gc.IsNil)
   219  	name := charm.Quote(expectedURL.String())
   220  	storage, err := environs.GetStorage(s.State)
   221  	c.Assert(err, gc.IsNil)
   222  	expectedUploadURL, err := storage.URL(name)
   223  	c.Assert(err, gc.IsNil)
   224  
   225  	c.Assert(sch.BundleURL().String(), gc.Equals, expectedUploadURL)
   226  	c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
   227  
   228  	reader, err := storage.Get(name)
   229  	c.Assert(err, gc.IsNil)
   230  	defer reader.Close()
   231  	downloadedSHA256, _, err := utils.ReadSHA256(reader)
   232  	c.Assert(err, gc.IsNil)
   233  	c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
   234  }
   235  
   236  func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) {
   237  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   238  	// Backwards compatibility check, that we can upload charms to
   239  	// https://host:port/charms
   240  	url := s.charmsURL(c, "series=quantal")
   241  	url.Path = "/charms"
   242  	resp, err := s.uploadRequest(c, url.String(), true, ch.Path)
   243  	c.Assert(err, gc.IsNil)
   244  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   245  	s.assertUploadResponse(c, resp, expectedURL.String())
   246  }
   247  
   248  func (s *charmsSuite) TestUploadAllowsEnvUUIDPath(c *gc.C) {
   249  	// Check that we can upload charms to https://host:port/ENVUUID/charms
   250  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   251  	environ, err := s.State.Environment()
   252  	c.Assert(err, gc.IsNil)
   253  	url := s.charmsURL(c, "series=quantal")
   254  	url.Path = fmt.Sprintf("/environment/%s/charms", environ.UUID())
   255  	resp, err := s.uploadRequest(c, url.String(), true, ch.Path)
   256  	c.Assert(err, gc.IsNil)
   257  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   258  	s.assertUploadResponse(c, resp, expectedURL.String())
   259  }
   260  
   261  func (s *charmsSuite) TestUploadRejectsWrongEnvUUIDPath(c *gc.C) {
   262  	// Check that we cannot upload charms to https://host:port/BADENVUUID/charms
   263  	url := s.charmsURL(c, "series=quantal")
   264  	url.Path = "/environment/dead-beef-123456/charms"
   265  	resp, err := s.authRequest(c, "POST", url.String(), "", nil)
   266  	c.Assert(err, gc.IsNil)
   267  	s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown environment: "dead-beef-123456"`)
   268  }
   269  
   270  func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
   271  	// Make a clone of the dummy charm in a nested directory.
   272  	rootDir := c.MkDir()
   273  	dirPath := filepath.Join(rootDir, "subdir1", "subdir2")
   274  	err := os.MkdirAll(dirPath, 0755)
   275  	c.Assert(err, gc.IsNil)
   276  	dir := charmtesting.Charms.ClonedDir(dirPath, "dummy")
   277  	// Now tweak the path the dir thinks it is in and bundle it.
   278  	dir.Path = rootDir
   279  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   280  	c.Assert(err, gc.IsNil)
   281  	defer tempFile.Close()
   282  	defer os.Remove(tempFile.Name())
   283  	err = dir.BundleTo(tempFile)
   284  	c.Assert(err, gc.IsNil)
   285  
   286  	// Try reading it as a bundle - should fail due to nested dirs.
   287  	_, err = charm.ReadBundle(tempFile.Name())
   288  	c.Assert(err, gc.ErrorMatches, "bundle file not found: metadata.yaml")
   289  
   290  	// Now try uploading it - should succeeed and be repackaged.
   291  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name())
   292  	c.Assert(err, gc.IsNil)
   293  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   294  	s.assertUploadResponse(c, resp, expectedURL.String())
   295  	sch, err := s.State.Charm(expectedURL)
   296  	c.Assert(err, gc.IsNil)
   297  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   298  	c.Assert(sch.Revision(), gc.Equals, 1)
   299  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   300  
   301  	// Get it from the storage and try to read it as a bundle - it
   302  	// should succeed, because it was repackaged during upload to
   303  	// strip nested dirs.
   304  	archiveName := strings.TrimPrefix(sch.BundleURL().RequestURI(), "/dummyenv/private/")
   305  	storage, err := environs.GetStorage(s.State)
   306  	c.Assert(err, gc.IsNil)
   307  	reader, err := storage.Get(archiveName)
   308  	c.Assert(err, gc.IsNil)
   309  	defer reader.Close()
   310  
   311  	data, err := ioutil.ReadAll(reader)
   312  	c.Assert(err, gc.IsNil)
   313  	downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded")
   314  	c.Assert(err, gc.IsNil)
   315  	defer downloadedFile.Close()
   316  	defer os.Remove(downloadedFile.Name())
   317  	err = ioutil.WriteFile(downloadedFile.Name(), data, 0644)
   318  	c.Assert(err, gc.IsNil)
   319  
   320  	bundle, err := charm.ReadBundle(downloadedFile.Name())
   321  	c.Assert(err, gc.IsNil)
   322  	c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision())
   323  	c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta())
   324  	c.Assert(bundle.Config(), jc.DeepEquals, sch.Config())
   325  }
   326  
   327  func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) {
   328  	uri := s.charmsURI(c, "?file=hooks/install")
   329  	resp, err := s.authRequest(c, "GET", uri, "", nil)
   330  	c.Assert(err, gc.IsNil)
   331  	s.assertErrorResponse(
   332  		c, resp, http.StatusBadRequest,
   333  		"expected url=CharmURL query argument",
   334  	)
   335  }
   336  
   337  func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) {
   338  	uri := s.charmsURI(c, "?url=local:precise/no-such")
   339  	resp, err := s.authRequest(c, "GET", uri, "", nil)
   340  	c.Assert(err, gc.IsNil)
   341  	s.assertErrorResponse(
   342  		c, resp, http.StatusBadRequest,
   343  		"unable to retrieve and save the charm: charm not found in the provider storage: .*",
   344  	)
   345  }
   346  
   347  func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) {
   348  	// Add the dummy charm.
   349  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   350  	_, err := s.uploadRequest(
   351  		c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   352  	c.Assert(err, gc.IsNil)
   353  
   354  	// Ensure a 404 is returned for files not included in the charm.
   355  	for i, file := range []string{
   356  		"no-such-file", "..", "../../../etc/passwd", "hooks/delete",
   357  	} {
   358  		c.Logf("test %d: %s", i, file)
   359  		uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+file)
   360  		resp, err := s.authRequest(c, "GET", uri, "", nil)
   361  		c.Assert(err, gc.IsNil)
   362  		c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound)
   363  	}
   364  }
   365  
   366  func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) {
   367  	// Add the dummy charm.
   368  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   369  	_, err := s.uploadRequest(
   370  		c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   371  	c.Assert(err, gc.IsNil)
   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, err := s.authRequest(c, "GET", uri, "", nil)
   376  	c.Assert(err, gc.IsNil)
   377  	c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden)
   378  }
   379  
   380  func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) {
   381  	// Add the dummy charm.
   382  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   383  	_, err := s.uploadRequest(
   384  		c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   385  	c.Assert(err, gc.IsNil)
   386  
   387  	// Ensure the file contents are properly returned.
   388  	for i, t := range []struct {
   389  		summary  string
   390  		file     string
   391  		response string
   392  	}{{
   393  		summary:  "relative path",
   394  		file:     "revision",
   395  		response: "1",
   396  	}, {
   397  		summary:  "exotic path",
   398  		file:     "./hooks/../revision",
   399  		response: "1",
   400  	}, {
   401  		summary:  "sub-directory path",
   402  		file:     "hooks/install",
   403  		response: "#!/bin/bash\necho \"Done!\"\n",
   404  	},
   405  	} {
   406  		c.Logf("test %d: %s", i, t.summary)
   407  		uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+t.file)
   408  		resp, err := s.authRequest(c, "GET", uri, "", nil)
   409  		c.Assert(err, gc.IsNil)
   410  		s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8")
   411  	}
   412  }
   413  
   414  func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) {
   415  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   416  	_, err := s.uploadRequest(
   417  		c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   418  	c.Assert(err, gc.IsNil)
   419  	// Backwards compatibility check, that we can GET from charms at
   420  	// https://host:port/charms
   421  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   422  	url.Path = "/charms"
   423  	resp, err := s.authRequest(c, "GET", url.String(), "", nil)
   424  	c.Assert(err, gc.IsNil)
   425  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   426  }
   427  
   428  func (s *charmsSuite) TestGetAllowsEnvUUIDPath(c *gc.C) {
   429  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   430  	_, err := s.uploadRequest(
   431  		c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   432  	c.Assert(err, gc.IsNil)
   433  	// Check that we can GET from charms at https://host:port/ENVUUID/charms
   434  	environ, err := s.State.Environment()
   435  	c.Assert(err, gc.IsNil)
   436  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   437  	url.Path = fmt.Sprintf("/environment/%s/charms", environ.UUID())
   438  	resp, err := s.authRequest(c, "GET", url.String(), "", nil)
   439  	c.Assert(err, gc.IsNil)
   440  	s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
   441  }
   442  
   443  func (s *charmsSuite) TestGetRejectsWrongEnvUUIDPath(c *gc.C) {
   444  	// Check that we cannot upload charms to https://host:port/BADENVUUID/charms
   445  	url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
   446  	url.Path = "/environment/dead-beef-123456/charms"
   447  	resp, err := s.authRequest(c, "GET", url.String(), "", nil)
   448  	c.Assert(err, gc.IsNil)
   449  	s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown environment: "dead-beef-123456"`)
   450  }
   451  
   452  func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) {
   453  	// Add the dummy charm.
   454  	ch := charmtesting.Charms.Bundle(c.MkDir(), "dummy")
   455  	_, err := s.uploadRequest(
   456  		c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   457  	c.Assert(err, gc.IsNil)
   458  
   459  	// Ensure charm files are properly listed.
   460  	uri := s.charmsURI(c, "?url=local:quantal/dummy-1")
   461  	resp, err := s.authRequest(c, "GET", uri, "", nil)
   462  	c.Assert(err, gc.IsNil)
   463  	manifest, err := ch.Manifest()
   464  	c.Assert(err, gc.IsNil)
   465  	expectedFiles := manifest.SortedValues()
   466  	s.assertGetFileListResponse(c, resp, expectedFiles)
   467  	ctype := resp.Header.Get("content-type")
   468  	c.Assert(ctype, gc.Equals, "application/json")
   469  }
   470  
   471  func (s *charmsSuite) TestGetUsesCache(c *gc.C) {
   472  	// Add a fake charm archive in the cache directory.
   473  	cacheDir := filepath.Join(s.DataDir(), "charm-get-cache")
   474  	err := os.MkdirAll(cacheDir, 0755)
   475  	c.Assert(err, gc.IsNil)
   476  
   477  	// Create and save a bundle in it.
   478  	charmDir := charmtesting.Charms.ClonedDir(c.MkDir(), "dummy")
   479  	testPath := filepath.Join(charmDir.Path, "utils.js")
   480  	contents := "// blah blah"
   481  	err = ioutil.WriteFile(testPath, []byte(contents), 0755)
   482  	c.Assert(err, gc.IsNil)
   483  	var buffer bytes.Buffer
   484  	err = charmDir.BundleTo(&buffer)
   485  	c.Assert(err, gc.IsNil)
   486  	charmArchivePath := filepath.Join(
   487  		cacheDir, charm.Quote("local:trusty/django-42")+".zip")
   488  	err = ioutil.WriteFile(charmArchivePath, buffer.Bytes(), 0644)
   489  	c.Assert(err, gc.IsNil)
   490  
   491  	// Ensure the cached contents are properly retrieved.
   492  	uri := s.charmsURI(c, "?url=local:trusty/django-42&file=utils.js")
   493  	resp, err := s.authRequest(c, "GET", uri, "", nil)
   494  	c.Assert(err, gc.IsNil)
   495  	s.assertGetFileResponse(c, resp, contents, "application/javascript")
   496  }
   497  
   498  func (s *charmsSuite) charmsURL(c *gc.C, query string) *url.URL {
   499  	uri := s.baseURL(c)
   500  	uri.Path += "/charms"
   501  	uri.RawQuery = query
   502  	return uri
   503  }
   504  
   505  func (s *charmsSuite) charmsURI(c *gc.C, query string) string {
   506  	if query != "" && query[0] == '?' {
   507  		query = query[1:]
   508  	}
   509  	return s.charmsURL(c, query).String()
   510  }
   511  
   512  func (s *charmsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) {
   513  	body := assertResponse(c, resp, http.StatusOK, "application/json")
   514  	charmResponse := jsonResponse(c, body)
   515  	c.Check(charmResponse.Error, gc.Equals, "")
   516  	c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL)
   517  }
   518  
   519  func (s *charmsSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) {
   520  	body := assertResponse(c, resp, http.StatusOK, expContentType)
   521  	c.Check(string(body), gc.Equals, expBody)
   522  }
   523  
   524  func (s *charmsSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) {
   525  	body := assertResponse(c, resp, http.StatusOK, "application/json")
   526  	charmResponse := jsonResponse(c, body)
   527  	c.Check(charmResponse.Error, gc.Equals, "")
   528  	c.Check(charmResponse.Files, gc.DeepEquals, expFiles)
   529  }
   530  
   531  func (s *charmsSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
   532  	body := assertResponse(c, resp, expCode, "application/json")
   533  	c.Check(jsonResponse(c, body).Error, gc.Matches, expError)
   534  }
   535  
   536  func assertResponse(c *gc.C, resp *http.Response, expCode int, expContentType string) []byte {
   537  	c.Check(resp.StatusCode, gc.Equals, expCode)
   538  	body, err := ioutil.ReadAll(resp.Body)
   539  	defer resp.Body.Close()
   540  	c.Assert(err, gc.IsNil)
   541  	ctype := resp.Header.Get("Content-Type")
   542  	c.Assert(ctype, gc.Equals, expContentType)
   543  	return body
   544  }
   545  
   546  func jsonResponse(c *gc.C, body []byte) (jsonResponse params.CharmsResponse) {
   547  	err := json.Unmarshal(body, &jsonResponse)
   548  	c.Assert(err, gc.IsNil)
   549  	return
   550  }