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 }