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 }