1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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"
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
"github.com/juju/juju/testing/factory"
31
// charmsCommonSuite wraps authHttpSuite and adds
32
// some helper methods suitable for working with the
34
type charmsCommonSuite struct {
38
func (s *charmsCommonSuite) charmsURL(c *gc.C, query string) *url.URL {
40
if s.modelUUID == "" {
43
uri.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
49
func (s *charmsCommonSuite) charmsURI(c *gc.C, query string) string {
50
if query != "" && query[0] == '?' {
53
return s.charmsURL(c, query).String()
56
func (s *charmsCommonSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) {
57
charmResponse := s.assertResponse(c, resp, http.StatusOK)
58
c.Check(charmResponse.Error, gc.Equals, "")
59
c.Check(charmResponse.CharmURL, gc.Equals, expCharmURL)
62
func (s *charmsCommonSuite) assertGetFileResponse(c *gc.C, resp *http.Response, expBody, expContentType string) {
63
body := assertResponse(c, resp, http.StatusOK, expContentType)
64
c.Check(string(body), gc.Equals, expBody)
67
func (s *charmsCommonSuite) assertGetFileListResponse(c *gc.C, resp *http.Response, expFiles []string) {
68
charmResponse := s.assertResponse(c, resp, http.StatusOK)
69
c.Check(charmResponse.Error, gc.Equals, "")
70
c.Check(charmResponse.Files, gc.DeepEquals, expFiles)
73
func (s *charmsCommonSuite) assertErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) {
74
charmResponse := s.assertResponse(c, resp, expCode)
75
c.Check(charmResponse.Error, gc.Matches, expError)
78
func (s *charmsCommonSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.CharmsResponse {
79
body := assertResponse(c, resp, expStatus, params.ContentTypeJSON)
80
var charmResponse params.CharmsResponse
81
err := json.Unmarshal(body, &charmResponse)
82
c.Assert(err, jc.ErrorIsNil, gc.Commentf("body: %s", body))
86
func (s *charmsCommonSuite) setModelImporting(c *gc.C) {
87
model, err := s.State.Model()
88
c.Assert(err, jc.ErrorIsNil)
89
err = model.SetMigrationMode(state.MigrationModeImporting)
90
c.Assert(err, jc.ErrorIsNil)
93
type charmsSuite struct {
97
var _ = gc.Suite(&charmsSuite{})
99
func (s *charmsSuite) SetUpSuite(c *gc.C) {
100
// TODO(bogdanteleaga): Fix this on windows
101
if runtime.GOOS == "windows" {
102
c.Skip("bug 1403084: Skipping this on windows for now")
104
s.charmsCommonSuite.SetUpSuite(c)
107
func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
109
uri := "http://" + info.Addrs[0] + "/charms"
110
s.sendRequest(c, httpRequestParams{
113
expectError: `.*malformed HTTP response.*`,
117
func (s *charmsSuite) TestPOSTRequiresAuth(c *gc.C) {
118
resp := s.sendRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
119
s.assertErrorResponse(c, resp, http.StatusUnauthorized, "no credentials provided")
122
func (s *charmsSuite) TestGETRequiresAuth(c *gc.C) {
123
resp := s.sendRequest(c, httpRequestParams{method: "GET", url: s.charmsURI(c, "")})
124
s.assertErrorResponse(c, resp, http.StatusUnauthorized, "no credentials provided")
127
func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
128
resp := s.authRequest(c, httpRequestParams{method: "PUT", url: s.charmsURI(c, "")})
129
s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
132
func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
133
// Add a machine and try to login.
134
machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
137
resp := s.sendRequest(c, httpRequestParams{
138
tag: machine.Tag().String(),
141
url: s.charmsURI(c, ""),
143
contentType: "foo/bar",
145
s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid")
147
// Now try a user login.
148
resp = s.authRequest(c, httpRequestParams{method: "POST", url: s.charmsURI(c, "")})
149
s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip.+")
152
func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
153
// Create an empty file.
154
tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
155
c.Assert(err, jc.ErrorIsNil)
157
// Pretend we upload a zip by setting the Content-Type, so we can
158
// check the error at extraction time later.
159
resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
160
s.assertErrorResponse(c, resp, http.StatusBadRequest, "cannot open charm archive: zip: not a valid zip file")
162
// Now try with the default Content-Type.
163
resp = s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/octet-stream", tempFile.Name())
164
s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip, got: application/octet-stream")
167
func (s *charmsSuite) TestUploadBumpsRevision(c *gc.C) {
168
// Add the dummy charm with revision 1.
169
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
170
curl := charm.MustParseURL(
171
fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
173
info := state.CharmInfo{
176
StoragePath: "dummy-storage-path",
177
SHA256: "dummy-1-sha256",
179
_, err := s.State.AddCharm(info)
180
c.Assert(err, jc.ErrorIsNil)
182
// Now try uploading the same revision and verify it gets bumped,
183
// and the BundleSha256 is calculated.
184
resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
185
expectedURL := charm.MustParseURL("local:quantal/dummy-2")
186
s.assertUploadResponse(c, resp, expectedURL.String())
187
sch, err := s.State.Charm(expectedURL)
188
c.Assert(err, jc.ErrorIsNil)
189
c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
190
c.Assert(sch.Revision(), gc.Equals, 2)
191
c.Assert(sch.IsUploaded(), jc.IsTrue)
192
// No more checks for the hash here, because it is
193
// verified in TestUploadRespectsLocalRevision.
194
c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
197
func (s *charmsSuite) TestUploadRespectsLocalRevision(c *gc.C) {
198
// Make a dummy charm dir with revision 123.
199
dir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy")
200
dir.SetDiskRevision(123)
201
// Now bundle the dir.
202
tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
203
c.Assert(err, jc.ErrorIsNil)
204
defer tempFile.Close()
205
defer os.Remove(tempFile.Name())
206
err = dir.ArchiveTo(tempFile)
207
c.Assert(err, jc.ErrorIsNil)
209
// Now try uploading it and ensure the revision persists.
210
resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
211
expectedURL := charm.MustParseURL("local:quantal/dummy-123")
212
s.assertUploadResponse(c, resp, expectedURL.String())
213
sch, err := s.State.Charm(expectedURL)
214
c.Assert(err, jc.ErrorIsNil)
215
c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
216
c.Assert(sch.Revision(), gc.Equals, 123)
217
c.Assert(sch.IsUploaded(), jc.IsTrue)
219
// First rewind the reader, which was reset but BundleTo() above.
220
_, err = tempFile.Seek(0, 0)
221
c.Assert(err, jc.ErrorIsNil)
223
// Finally, verify the SHA256.
224
expectedSHA256, _, err := utils.ReadSHA256(tempFile)
225
c.Assert(err, jc.ErrorIsNil)
227
c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
229
storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
230
reader, _, err := storage.Get(sch.StoragePath())
231
c.Assert(err, jc.ErrorIsNil)
233
downloadedSHA256, _, err := utils.ReadSHA256(reader)
234
c.Assert(err, jc.ErrorIsNil)
235
c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
238
func (s *charmsSuite) TestUploadWithMultiSeriesCharm(c *gc.C) {
239
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
240
resp := s.uploadRequest(c, s.charmsURL(c, "").String(), "application/zip", ch.Path)
241
expectedURL := charm.MustParseURL("local:dummy-1")
242
s.assertUploadResponse(c, resp, expectedURL.String())
245
func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) {
246
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
247
// Backwards compatibility check, that we can upload charms to
248
// https://host:port/charms
249
url := s.charmsURL(c, "series=quantal")
251
resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
252
expectedURL := charm.MustParseURL("local:quantal/dummy-1")
253
s.assertUploadResponse(c, resp, expectedURL.String())
256
func (s *charmsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) {
257
// Check that we can upload charms to https://host:port/ModelUUID/charms
258
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
259
url := s.charmsURL(c, "series=quantal")
260
url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
261
resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
262
expectedURL := charm.MustParseURL("local:quantal/dummy-1")
263
s.assertUploadResponse(c, resp, expectedURL.String())
266
func (s *charmsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) {
267
envState := s.setupOtherModel(c)
268
// Check that we can upload charms to https://host:port/ModelUUID/charms
269
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
270
url := s.charmsURL(c, "series=quantal")
271
url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
272
resp := s.uploadRequest(c, url.String(), "application/zip", ch.Path)
273
expectedURL := charm.MustParseURL("local:quantal/dummy-1")
274
s.assertUploadResponse(c, resp, expectedURL.String())
277
func (s *charmsSuite) TestUploadRejectsWrongModelUUIDPath(c *gc.C) {
278
// Check that we cannot upload charms to https://host:port/BADModelUUID/charms
279
url := s.charmsURL(c, "series=quantal")
280
url.Path = "/model/dead-beef-123456/charms"
281
resp := s.authRequest(c, httpRequestParams{method: "POST", url: url.String()})
282
s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
285
func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
286
// Make a clone of the dummy charm in a nested directory.
288
dirPath := filepath.Join(rootDir, "subdir1", "subdir2")
289
err := os.MkdirAll(dirPath, 0755)
290
c.Assert(err, jc.ErrorIsNil)
291
dir := testcharms.Repo.ClonedDir(dirPath, "dummy")
292
// Now tweak the path the dir thinks it is in and bundle it.
294
tempFile, err := ioutil.TempFile(c.MkDir(), "charm")
295
c.Assert(err, jc.ErrorIsNil)
296
defer tempFile.Close()
297
defer os.Remove(tempFile.Name())
298
err = dir.ArchiveTo(tempFile)
299
c.Assert(err, jc.ErrorIsNil)
301
// Try reading it as a bundle - should fail due to nested dirs.
302
_, err = charm.ReadCharmArchive(tempFile.Name())
303
c.Assert(err, gc.ErrorMatches, `archive file "metadata.yaml" not found`)
305
// Now try uploading it - should succeeed and be repackaged.
306
resp := s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", tempFile.Name())
307
expectedURL := charm.MustParseURL("local:quantal/dummy-1")
308
s.assertUploadResponse(c, resp, expectedURL.String())
309
sch, err := s.State.Charm(expectedURL)
310
c.Assert(err, jc.ErrorIsNil)
311
c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
312
c.Assert(sch.Revision(), gc.Equals, 1)
313
c.Assert(sch.IsUploaded(), jc.IsTrue)
315
// Get it from the storage and try to read it as a bundle - it
316
// should succeed, because it was repackaged during upload to
317
// strip nested dirs.
318
storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
319
reader, _, err := storage.Get(sch.StoragePath())
320
c.Assert(err, jc.ErrorIsNil)
323
data, err := ioutil.ReadAll(reader)
324
c.Assert(err, jc.ErrorIsNil)
325
downloadedFile, err := ioutil.TempFile(c.MkDir(), "downloaded")
326
c.Assert(err, jc.ErrorIsNil)
327
defer downloadedFile.Close()
328
defer os.Remove(downloadedFile.Name())
329
err = ioutil.WriteFile(downloadedFile.Name(), data, 0644)
330
c.Assert(err, jc.ErrorIsNil)
332
bundle, err := charm.ReadCharmArchive(downloadedFile.Name())
333
c.Assert(err, jc.ErrorIsNil)
334
c.Assert(bundle.Revision(), jc.DeepEquals, sch.Revision())
335
c.Assert(bundle.Meta(), jc.DeepEquals, sch.Meta())
336
c.Assert(bundle.Config(), jc.DeepEquals, sch.Config())
339
func (s *charmsSuite) TestNonLocalCharmUploadFailsIfNotMigrating(c *gc.C) {
340
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
341
curl := charm.MustParseURL(
342
fmt.Sprintf("cs:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
344
info := state.CharmInfo{
347
StoragePath: "dummy-storage-path",
348
SHA256: "dummy-1-sha256",
350
_, err := s.State.AddCharm(info)
351
c.Assert(err, jc.ErrorIsNil)
353
resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path)
354
s.assertErrorResponse(c, resp, 400, "cs charms may only be uploaded during model migration import")
357
func (s *charmsSuite) TestNonLocalCharmUpload(c *gc.C) {
358
// Check that upload of charms with the "cs:" schema works (for
359
// model migrations).
360
s.setModelImporting(c)
361
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
363
resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path)
365
expectedURL := charm.MustParseURL("cs:quantal/dummy-1")
366
s.assertUploadResponse(c, resp, expectedURL.String())
367
sch, err := s.State.Charm(expectedURL)
368
c.Assert(err, jc.ErrorIsNil)
369
c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
370
c.Assert(sch.Revision(), gc.Equals, 1)
371
c.Assert(sch.IsUploaded(), jc.IsTrue)
374
func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) {
375
s.setModelImporting(c)
376
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
378
resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&revision=99"), "application/zip", ch.Path)
380
expectedURL := charm.MustParseURL("cs:dummy-99")
381
s.assertUploadResponse(c, resp, expectedURL.String())
382
sch, err := s.State.Charm(expectedURL)
383
c.Assert(err, jc.ErrorIsNil)
384
c.Assert(sch.URL(), gc.DeepEquals, expectedURL)
385
c.Assert(sch.Revision(), gc.Equals, 99)
386
c.Assert(sch.IsUploaded(), jc.IsTrue)
389
func (s *charmsSuite) TestGetRequiresCharmURL(c *gc.C) {
390
uri := s.charmsURI(c, "?file=hooks/install")
391
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
392
s.assertErrorResponse(
393
c, resp, http.StatusBadRequest,
394
"expected url=CharmURL query argument",
398
func (s *charmsSuite) TestGetFailsWithInvalidCharmURL(c *gc.C) {
399
uri := s.charmsURI(c, "?url=local:precise/no-such")
400
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
401
s.assertErrorResponse(
402
c, resp, http.StatusNotFound,
403
`unable to retrieve and save the charm: cannot get charm from state: charm "local:precise/no-such" not found`,
407
func (s *charmsSuite) TestGetReturnsNotFoundWhenMissing(c *gc.C) {
408
// Add the dummy charm.
409
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
410
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
412
// Ensure a 404 is returned for files not included in the charm.
413
for i, file := range []string{
414
"no-such-file", "..", "../../../etc/passwd", "hooks/delete",
416
c.Logf("test %d: %s", i, file)
417
uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+file)
418
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
419
c.Assert(resp.StatusCode, gc.Equals, http.StatusNotFound)
423
func (s *charmsSuite) TestGetReturnsForbiddenWithDirectory(c *gc.C) {
424
// Add the dummy charm.
425
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
426
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
428
// Ensure a 403 is returned if the requested file is a directory.
429
uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=hooks")
430
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
431
c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden)
434
func (s *charmsSuite) TestGetReturnsFileContents(c *gc.C) {
435
// Add the dummy charm.
436
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
437
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
439
// Ensure the file contents are properly returned.
440
for i, t := range []struct {
445
summary: "relative path",
449
summary: "exotic path",
450
file: "./hooks/../revision",
453
summary: "sub-directory path",
454
file: "hooks/install",
455
response: "#!/bin/bash\necho \"Done!\"\n",
458
c.Logf("test %d: %s", i, t.summary)
459
uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file="+t.file)
460
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
461
s.assertGetFileResponse(c, resp, t.response, "text/plain; charset=utf-8")
465
func (s *charmsSuite) TestGetWorksForControllerMachines(c *gc.C) {
466
// Make a controller machine.
467
const nonce = "noncey"
468
m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
469
Jobs: []state.MachineJob{state.JobManageModel},
473
// Create a hosted model and upload a charm for it.
474
envState := s.setupOtherModel(c)
475
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
476
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
478
// Controller machine should be able to download the charm from
479
// the hosted model. This is required for controller workers which
480
// are acting on behalf of a particular hosted model.
481
url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
482
url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
483
params := httpRequestParams{
486
tag: m.Tag().String(),
490
resp := s.sendRequest(c, params)
491
s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
494
func (s *charmsSuite) TestGetStarReturnsArchiveBytes(c *gc.C) {
495
// Add the dummy charm.
496
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
497
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
499
data, err := ioutil.ReadFile(ch.Path)
500
c.Assert(err, jc.ErrorIsNil)
502
uri := s.charmsURI(c, "?url=local:quantal/dummy-1&file=*")
503
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
504
s.assertGetFileResponse(c, resp, string(data), "application/zip")
507
func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) {
508
// Backwards compatibility check, that we can GET from charms at
509
// https://host:port/charms
510
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
511
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
512
url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
514
resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
515
s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
518
func (s *charmsSuite) TestGetAllowsModelUUIDPath(c *gc.C) {
519
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
520
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
521
url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
522
url.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
523
resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
524
s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
527
func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) {
528
envState := s.setupOtherModel(c)
530
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
531
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
532
url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
533
url.Path = fmt.Sprintf("/model/%s/charms", envState.ModelUUID())
534
resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
535
s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
538
func (s *charmsSuite) TestGetRejectsWrongModelUUIDPath(c *gc.C) {
539
url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision")
540
url.Path = "/model/dead-beef-123456/charms"
541
resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
542
s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown model: "dead-beef-123456"`)
545
func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) {
546
// Add the dummy charm.
547
ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
548
s.uploadRequest(c, s.charmsURI(c, "?series=quantal"), "application/zip", ch.Path)
550
// Ensure charm files are properly listed.
551
uri := s.charmsURI(c, "?url=local:quantal/dummy-1")
552
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
553
manifest, err := ch.Manifest()
554
c.Assert(err, jc.ErrorIsNil)
555
expectedFiles := manifest.SortedValues()
556
s.assertGetFileListResponse(c, resp, expectedFiles)
557
ctype := resp.Header.Get("content-type")
558
c.Assert(ctype, gc.Equals, params.ContentTypeJSON)
561
func (s *charmsSuite) TestGetUsesCache(c *gc.C) {
562
// Add a fake charm archive in the cache directory.
563
cacheDir := filepath.Join(s.DataDir(), "charm-get-cache", s.State.ModelUUID())
564
err := os.MkdirAll(cacheDir, 0755)
565
c.Assert(err, jc.ErrorIsNil)
567
// Create and save a bundle in it.
568
charmDir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy")
569
testPath := filepath.Join(charmDir.Path, "utils.js")
570
contents := "// blah blah"
571
err = ioutil.WriteFile(testPath, []byte(contents), 0755)
572
c.Assert(err, jc.ErrorIsNil)
573
var buffer bytes.Buffer
574
err = charmDir.ArchiveTo(&buffer)
575
c.Assert(err, jc.ErrorIsNil)
576
charmArchivePath := filepath.Join(
577
cacheDir, charm.Quote("local:trusty/django-42")+".zip")
578
err = ioutil.WriteFile(charmArchivePath, buffer.Bytes(), 0644)
579
c.Assert(err, jc.ErrorIsNil)
581
// Ensure the cached contents are properly retrieved.
582
uri := s.charmsURI(c, "?url=local:trusty/django-42&file=utils.js")
583
resp := s.authRequest(c, httpRequestParams{method: "GET", url: uri})
584
s.assertGetFileResponse(c, resp, contents, params.ContentTypeJS)
587
type charmsWithMacaroonsSuite struct {
591
var _ = gc.Suite(&charmsWithMacaroonsSuite{})
593
func (s *charmsWithMacaroonsSuite) SetUpTest(c *gc.C) {
594
s.macaroonAuthEnabled = true
595
s.authHttpSuite.SetUpTest(c)
598
func (s *charmsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
599
resp := s.sendRequest(c, httpRequestParams{
601
url: s.charmsURI(c, ""),
604
charmResponse := s.assertResponse(c, resp, http.StatusUnauthorized)
605
c.Assert(charmResponse.Error, gc.Equals, "verification failed: no macaroons")
606
c.Assert(charmResponse.ErrorCode, gc.Equals, params.CodeDischargeRequired)
607
c.Assert(charmResponse.ErrorInfo, gc.NotNil)
608
c.Assert(charmResponse.ErrorInfo.Macaroon, gc.NotNil)
611
func (s *charmsWithMacaroonsSuite) TestCanPostWithDischargedMacaroon(c *gc.C) {
613
s.DischargerLogin = func() string {
615
return s.userTag.Id()
617
resp := s.sendRequest(c, httpRequestParams{
620
url: s.charmsURI(c, ""),
621
contentType: "foo/bar",
623
s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip.+")
624
c.Assert(checkCount, gc.Equals, 1)
627
// doer returns a Do function that can make a bakery request
628
// appropriate for a charms endpoint.
629
func (s *charmsWithMacaroonsSuite) doer() func(*http.Request) (*http.Response, error) {
630
return bakeryDo(nil, charmsBakeryGetError)
633
// charmsBakeryGetError implements a getError function
634
// appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError
635
// for the charms endpoint.
636
func charmsBakeryGetError(resp *http.Response) error {
637
if resp.StatusCode != http.StatusUnauthorized {
640
data, err := ioutil.ReadAll(resp.Body)
642
return errors.Annotatef(err, "cannot read body")
644
var charmResp params.CharmsResponse
645
if err := json.Unmarshal(data, &charmResp); err != nil {
646
return errors.Annotatef(err, "cannot unmarshal body")
648
errResp := ¶ms.Error{
649
Message: charmResp.Error,
650
Code: charmResp.ErrorCode,
651
Info: charmResp.ErrorInfo,
653
if errResp.Code != params.CodeDischargeRequired {
656
if errResp.Info == nil {
657
return errors.Annotatef(err, "no error info found in discharge-required response error")
659
// It's a discharge-required error, so make an appropriate httpbakery
661
return &httpbakery.Error{
662
Message: errResp.Message,
663
Code: httpbakery.ErrDischargeRequired,
664
Info: &httpbakery.ErrorInfo{
665
Macaroon: errResp.Info.Macaroon,
666
MacaroonPath: errResp.Info.MacaroonPath,