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