launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	gc "launchpad.net/gocheck"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"launchpad.net/juju-core/charm"
    19  	envtesting "launchpad.net/juju-core/environs/testing"
    20  	jujutesting "launchpad.net/juju-core/juju/testing"
    21  	"launchpad.net/juju-core/state"
    22  	"launchpad.net/juju-core/state/api/params"
    23  	coretesting "launchpad.net/juju-core/testing"
    24  	jc "launchpad.net/juju-core/testing/checkers"
    25  	"launchpad.net/juju-core/utils"
    26  )
    27  
    28  type charmsSuite struct {
    29  	jujutesting.JujuConnSuite
    30  	userTag  string
    31  	password string
    32  }
    33  
    34  var _ = gc.Suite(&charmsSuite{})
    35  
    36  func (s *charmsSuite) SetUpTest(c *gc.C) {
    37  	s.JujuConnSuite.SetUpTest(c)
    38  	password, err := utils.RandomPassword()
    39  	c.Assert(err, gc.IsNil)
    40  	user, err := s.State.AddUser("joe", password)
    41  	c.Assert(err, gc.IsNil)
    42  	s.userTag = user.Tag()
    43  	s.password = password
    44  }
    45  
    46  func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
    47  	_, info, err := s.APIConn.Environ.StateInfo()
    48  	c.Assert(err, gc.IsNil)
    49  	uri := "http://" + info.Addrs[0] + "/charms"
    50  	_, err = s.sendRequest(c, "", "", "GET", uri, "", nil)
    51  	c.Assert(err, gc.ErrorMatches, `.*malformed HTTP response.*`)
    52  }
    53  
    54  func (s *charmsSuite) TestRequiresAuth(c *gc.C) {
    55  	resp, err := s.sendRequest(c, "", "", "GET", s.charmsURI(c, ""), "", nil)
    56  	c.Assert(err, gc.IsNil)
    57  	s.assertResponse(c, resp, http.StatusUnauthorized, "unauthorized", "")
    58  }
    59  
    60  func (s *charmsSuite) TestUploadRequiresPOST(c *gc.C) {
    61  	resp, err := s.authRequest(c, "GET", s.charmsURI(c, ""), "", nil)
    62  	c.Assert(err, gc.IsNil)
    63  	s.assertResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "GET"`, "")
    64  }
    65  
    66  func (s *charmsSuite) TestAuthRequiresUser(c *gc.C) {
    67  	// Add a machine and try to login.
    68  	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
    69  	c.Assert(err, gc.IsNil)
    70  	err = machine.SetProvisioned("foo", "fake_nonce", nil)
    71  	c.Assert(err, gc.IsNil)
    72  	password, err := utils.RandomPassword()
    73  	c.Assert(err, gc.IsNil)
    74  	err = machine.SetPassword(password)
    75  	c.Assert(err, gc.IsNil)
    76  
    77  	resp, err := s.sendRequest(c, machine.Tag(), password, "GET", s.charmsURI(c, ""), "", nil)
    78  	c.Assert(err, gc.IsNil)
    79  	s.assertResponse(c, resp, http.StatusUnauthorized, "unauthorized", "")
    80  
    81  	// Now try a user login.
    82  	resp, err = s.authRequest(c, "GET", s.charmsURI(c, ""), "", nil)
    83  	c.Assert(err, gc.IsNil)
    84  	s.assertResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "GET"`, "")
    85  }
    86  
    87  func (s *charmsSuite) TestUploadRequiresSeries(c *gc.C) {
    88  	resp, err := s.authRequest(c, "POST", s.charmsURI(c, ""), "", nil)
    89  	c.Assert(err, gc.IsNil)
    90  	s.assertResponse(c, resp, http.StatusBadRequest, "expected series= URL argument", "")
    91  }
    92  
    93  func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
    94  	// Create an empty file.
    95  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
    96  	c.Assert(err, gc.IsNil)
    97  
    98  	// Pretend we upload a zip by setting the Content-Type, so we can
    99  	// check the error at extraction time later.
   100  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name())
   101  	c.Assert(err, gc.IsNil)
   102  	s.assertResponse(c, resp, http.StatusBadRequest, "cannot open charm archive: zip: not a valid zip file", "")
   103  
   104  	// Now try with the default Content-Type.
   105  	resp, err = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), false, tempFile.Name())
   106  	c.Assert(err, gc.IsNil)
   107  	s.assertResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip, got: application/octet-stream", "")
   108  }
   109  
   110  func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) {
   111  	// Add the dummy charm with revision 1.
   112  	ch := coretesting.Charms.Bundle(c.MkDir(), "dummy")
   113  	curl := charm.MustParseURL(
   114  		fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
   115  	)
   116  	bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
   117  	c.Assert(err, gc.IsNil)
   118  	_, err = s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
   119  	c.Assert(err, gc.IsNil)
   120  
   121  	// Now try uploading the same revision and verify it gets bumped,
   122  	// and the BundleURL and BundleSha256 are calculated.
   123  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, ch.Path)
   124  	c.Assert(err, gc.IsNil)
   125  	expectedURL := charm.MustParseURL("local:quantal/dummy-2")
   126  	s.assertResponse(c, resp, http.StatusOK, "", expectedURL.String())
   127  	sch, err := s.State.Charm(expectedURL)
   128  	c.Assert(err, gc.IsNil)
   129  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   130  	c.Assert(sch.Revision(), gc.Equals, 2)
   131  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   132  	// No more checks for these two here, because they
   133  	// are verified in TestUploadRespectsLocalRevision.
   134  	c.Assert(sch.BundleURL(), gc.Not(gc.Equals), "")
   135  	c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
   136  }
   137  
   138  func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) {
   139  	// Make a dummy charm dir with revision 123.
   140  	dir := coretesting.Charms.ClonedDir(c.MkDir(), "dummy")
   141  	dir.SetDiskRevision(123)
   142  	// Now bundle the dir.
   143  	tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
   144  	c.Assert(err, gc.IsNil)
   145  	defer tempFile.Close()
   146  	defer os.Remove(tempFile.Name())
   147  	err = dir.BundleTo(tempFile)
   148  	c.Assert(err, gc.IsNil)
   149  
   150  	// Now try uploading it and ensure the revision persists.
   151  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name())
   152  	c.Assert(err, gc.IsNil)
   153  	expectedURL := charm.MustParseURL("local:quantal/dummy-123")
   154  	s.assertResponse(c, resp, http.StatusOK, "", expectedURL.String())
   155  	sch, err := s.State.Charm(expectedURL)
   156  	c.Assert(err, gc.IsNil)
   157  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   158  	c.Assert(sch.Revision(), gc.Equals, 123)
   159  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   160  
   161  	// First rewind the reader, which was reset but BundleTo() above.
   162  	_, err = tempFile.Seek(0, 0)
   163  	c.Assert(err, gc.IsNil)
   164  
   165  	// Finally, verify the SHA256 and uploaded URL.
   166  	expectedSHA256, _, err := utils.ReadSHA256(tempFile)
   167  	c.Assert(err, gc.IsNil)
   168  	name := charm.Quote(expectedURL.String())
   169  	storage, err := envtesting.GetEnvironStorage(s.State)
   170  	c.Assert(err, gc.IsNil)
   171  	expectedUploadURL, err := storage.URL(name)
   172  	c.Assert(err, gc.IsNil)
   173  
   174  	c.Assert(sch.BundleURL().String(), gc.Equals, expectedUploadURL)
   175  	c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
   176  
   177  	reader, err := storage.Get(name)
   178  	c.Assert(err, gc.IsNil)
   179  	defer reader.Close()
   180  	downloadedSHA256, _, err := utils.ReadSHA256(reader)
   181  	c.Assert(err, gc.IsNil)
   182  	c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
   183  }
   184  
   185  func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
   186  	// Make a clone of the dummy charm in a nested directory.
   187  	rootDir := c.MkDir()
   188  	dirPath := filepath.Join(rootDir, "subdir1", "subdir2")
   189  	err := os.MkdirAll(dirPath, 0755)
   190  	c.Assert(err, gc.IsNil)
   191  	dir := coretesting.Charms.ClonedDir(dirPath, "dummy")
   192  	// Now tweak the path the dir thinks it is in and bundle it.
   193  	dir.Path = rootDir
   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  	// Try reading it as a bundle - should fail due to nested dirs.
   202  	_, err = charm.ReadBundle(tempFile.Name())
   203  	c.Assert(err, gc.ErrorMatches, "bundle file not found: metadata.yaml")
   204  
   205  	// Now try uploading it - should succeeed and be repackaged.
   206  	resp, err := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), true, tempFile.Name())
   207  	c.Assert(err, gc.IsNil)
   208  	expectedURL := charm.MustParseURL("local:quantal/dummy-1")
   209  	s.assertResponse(c, resp, http.StatusOK, "", expectedURL.String())
   210  	sch, err := s.State.Charm(expectedURL)
   211  	c.Assert(err, gc.IsNil)
   212  	c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
   213  	c.Assert(sch.Revision(), gc.Equals, 1)
   214  	c.Assert(sch.IsUploaded(), jc.IsTrue)
   215  
   216  	// Get it from the storage and try to read it as a bundle - it
   217  	// should succeed, because it was repackaged during upload to
   218  	// strip nested dirs.
   219  	archiveName := strings.TrimPrefix(sch.BundleURL().RequestURI(), "/dummyenv/private/")
   220  	storage, err := envtesting.GetEnvironStorage(s.State)
   221  	c.Assert(err, gc.IsNil)
   222  	reader, err := storage.Get(archiveName)
   223  	c.Assert(err, gc.IsNil)
   224  	defer reader.Close()
   225  
   226  	data, err := ioutil.ReadAll(reader)
   227  	c.Assert(err, gc.IsNil)
   228  	downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded")
   229  	c.Assert(err, gc.IsNil)
   230  	defer downloadedFile.Close()
   231  	defer os.Remove(downloadedFile.Name())
   232  	err = ioutil.WriteFile(downloadedFile.Name(), data, 0644)
   233  	c.Assert(err, gc.IsNil)
   234  
   235  	bundle, err := charm.ReadBundle(downloadedFile.Name())
   236  	c.Assert(err, gc.IsNil)
   237  	c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision())
   238  	c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta())
   239  	c.Assert(bundle.Config(), jc.DeepEquals, sch.Config())
   240  }
   241  
   242  func (s *charmsSuite) charmsURI(c *gc.C, query string) string {
   243  	_, info, err := s.APIConn.Environ.StateInfo()
   244  	c.Assert(err, gc.IsNil)
   245  	return "https://" + info.Addrs[0] + "/charms" + query
   246  }
   247  
   248  func (s *charmsSuite) sendRequest(c *gc.C, tag, password, method, uri, contentType string, body io.Reader) (*http.Response, error) {
   249  	req, err := http.NewRequest(method, uri, body)
   250  	c.Assert(err, gc.IsNil)
   251  	if tag != "" && password != "" {
   252  		req.SetBasicAuth(tag, password)
   253  	}
   254  	if contentType != "" {
   255  		req.Header.Set("Content-Type", contentType)
   256  	}
   257  	return utils.GetNonValidatingHTTPClient().Do(req)
   258  }
   259  
   260  func (s *charmsSuite) authRequest(c *gc.C, method, uri, contentType string, body io.Reader) (*http.Response, error) {
   261  	return s.sendRequest(c, s.userTag, s.password, method, uri, contentType, body)
   262  }
   263  
   264  func (s *charmsSuite) uploadRequest(c *gc.C, uri string, asZip bool, path string) (*http.Response, error) {
   265  	contentType := "application/octet-stream"
   266  	if asZip {
   267  		contentType = "application/zip"
   268  	}
   269  
   270  	if path == "" {
   271  		return s.authRequest(c, "POST", uri, contentType, nil)
   272  	}
   273  
   274  	file, err := os.Open(path)
   275  	c.Assert(err, gc.IsNil)
   276  	defer file.Close()
   277  	return s.authRequest(c, "POST", uri, contentType, file)
   278  }
   279  
   280  func (s *charmsSuite) assertResponse(c *gc.C, resp *http.Response, expCode int, expError, expCharmURL string) {
   281  	body, err := ioutil.ReadAll(resp.Body)
   282  	defer resp.Body.Close()
   283  	c.Assert(err, gc.IsNil)
   284  	var jsonResponse params.CharmsResponse
   285  	err = json.Unmarshal(body, &jsonResponse)
   286  	c.Assert(err, gc.IsNil)
   287  	if expError != "" {
   288  		c.Check(jsonResponse.Error, gc.Matches, expError)
   289  		c.Check(jsonResponse.CharmURL, gc.Equals, "")
   290  	} else {
   291  		c.Check(jsonResponse.Error, gc.Equals, "")
   292  		c.Check(jsonResponse.CharmURL, gc.Equals, expCharmURL)
   293  	}
   294  	c.Check(resp.StatusCode, gc.Equals, expCode)
   295  }