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 := ¶ms.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 }