~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/apiserver/charms_test.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
        "github.com/juju/juju/testing/factory"
 
29
)
 
30
 
 
31
// charmsCommonSuite wraps authHttpSuite and adds
 
32
// some helper methods suitable for working with the
 
33
// charms endpoint.
 
34
type charmsCommonSuite struct {
 
35
        authHttpSuite
 
36
}
 
37
 
 
38
func (s *charmsCommonSuite) charmsURL(c *gc.C, query string) *url.URL {
 
39
        uri := s.baseURL(c)
 
40
        if s.modelUUID == "" {
 
41
                uri.Path = "/charms"
 
42
        } else {
 
43
                uri.Path = fmt.Sprintf("/model/%s/charms", s.modelUUID)
 
44
        }
 
45
        uri.RawQuery = query
 
46
        return uri
 
47
}
 
48
 
 
49
func (s *charmsCommonSuite) charmsURI(c *gc.C, query string) string {
 
50
        if query != "" && query[0] == '?' {
 
51
                query = query[1:]
 
52
        }
 
53
        return s.charmsURL(c, query).String()
 
54
}
 
55
 
 
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)
 
60
}
 
61
 
 
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)
 
65
}
 
66
 
 
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)
 
71
}
 
72
 
 
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)
 
76
}
 
77
 
 
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))
 
83
        return charmResponse
 
84
}
 
85
 
 
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)
 
91
}
 
92
 
 
93
type charmsSuite struct {
 
94
        charmsCommonSuite
 
95
}
 
96
 
 
97
var _ = gc.Suite(&charmsSuite{})
 
98
 
 
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")
 
103
        }
 
104
        s.charmsCommonSuite.SetUpSuite(c)
 
105
}
 
106
 
 
107
func (s *charmsSuite) TestCharmsServedSecurely(c *gc.C) {
 
108
        info := s.APIInfo(c)
 
109
        uri := "http://" + info.Addrs[0] + "/charms"
 
110
        s.sendRequest(c, httpRequestParams{
 
111
                method:      "GET",
 
112
                url:         uri,
 
113
                expectError: `.*malformed HTTP response.*`,
 
114
        })
 
115
}
 
116
 
 
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")
 
120
}
 
121
 
 
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")
 
125
}
 
126
 
 
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"`)
 
130
}
 
131
 
 
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{
 
135
                Nonce: "noncy",
 
136
        })
 
137
        resp := s.sendRequest(c, httpRequestParams{
 
138
                tag:         machine.Tag().String(),
 
139
                password:    password,
 
140
                method:      "POST",
 
141
                url:         s.charmsURI(c, ""),
 
142
                nonce:       "noncy",
 
143
                contentType: "foo/bar",
 
144
        })
 
145
        s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid")
 
146
 
 
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.+")
 
150
}
 
151
 
 
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)
 
156
 
 
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")
 
161
 
 
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")
 
165
}
 
166
 
 
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()),
 
172
        )
 
173
        info := state.CharmInfo{
 
174
                Charm:       ch,
 
175
                ID:          curl,
 
176
                StoragePath: "dummy-storage-path",
 
177
                SHA256:      "dummy-1-sha256",
 
178
        }
 
179
        _, err := s.State.AddCharm(info)
 
180
        c.Assert(err, jc.ErrorIsNil)
 
181
 
 
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), "")
 
195
}
 
196
 
 
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)
 
208
 
 
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)
 
218
 
 
219
        // First rewind the reader, which was reset but BundleTo() above.
 
220
        _, err = tempFile.Seek(0, 0)
 
221
        c.Assert(err, jc.ErrorIsNil)
 
222
 
 
223
        // Finally, verify the SHA256.
 
224
        expectedSHA256, _, err := utils.ReadSHA256(tempFile)
 
225
        c.Assert(err, jc.ErrorIsNil)
 
226
 
 
227
        c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
 
228
 
 
229
        storage := storage.NewStorage(s.State.ModelUUID(), s.State.MongoSession())
 
230
        reader, _, err := storage.Get(sch.StoragePath())
 
231
        c.Assert(err, jc.ErrorIsNil)
 
232
        defer reader.Close()
 
233
        downloadedSHA256, _, err := utils.ReadSHA256(reader)
 
234
        c.Assert(err, jc.ErrorIsNil)
 
235
        c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
 
236
}
 
237
 
 
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())
 
243
}
 
244
 
 
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")
 
250
        url.Path = "/charms"
 
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())
 
254
}
 
255
 
 
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())
 
264
}
 
265
 
 
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())
 
275
}
 
276
 
 
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"`)
 
283
}
 
284
 
 
285
func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) {
 
286
        // Make a clone of the dummy charm in a nested directory.
 
287
        rootDir := c.MkDir()
 
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.
 
293
        dir.Path = rootDir
 
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)
 
300
 
 
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`)
 
304
 
 
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)
 
314
 
 
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)
 
321
        defer reader.Close()
 
322
 
 
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)
 
331
 
 
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())
 
337
}
 
338
 
 
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()),
 
343
        )
 
344
        info := state.CharmInfo{
 
345
                Charm:       ch,
 
346
                ID:          curl,
 
347
                StoragePath: "dummy-storage-path",
 
348
                SHA256:      "dummy-1-sha256",
 
349
        }
 
350
        _, err := s.State.AddCharm(info)
 
351
        c.Assert(err, jc.ErrorIsNil)
 
352
 
 
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")
 
355
}
 
356
 
 
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")
 
362
 
 
363
        resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&series=quantal"), "application/zip", ch.Path)
 
364
 
 
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)
 
372
}
 
373
 
 
374
func (s *charmsSuite) TestNonLocalCharmUploadWithRevisionOverride(c *gc.C) {
 
375
        s.setModelImporting(c)
 
376
        ch := testcharms.Repo.CharmArchive(c.MkDir(), "dummy")
 
377
 
 
378
        resp := s.uploadRequest(c, s.charmsURI(c, "?schema=cs&revision=99"), "application/zip", ch.Path)
 
379
 
 
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)
 
387
}
 
388
 
 
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",
 
395
        )
 
396
}
 
397
 
 
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`,
 
404
        )
 
405
}
 
406
 
 
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)
 
411
 
 
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",
 
415
        } {
 
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)
 
420
        }
 
421
}
 
422
 
 
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)
 
427
 
 
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)
 
432
}
 
433
 
 
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)
 
438
 
 
439
        // Ensure the file contents are properly returned.
 
440
        for i, t := range []struct {
 
441
                summary  string
 
442
                file     string
 
443
                response string
 
444
        }{{
 
445
                summary:  "relative path",
 
446
                file:     "revision",
 
447
                response: "1",
 
448
        }, {
 
449
                summary:  "exotic path",
 
450
                file:     "./hooks/../revision",
 
451
                response: "1",
 
452
        }, {
 
453
                summary:  "sub-directory path",
 
454
                file:     "hooks/install",
 
455
                response: "#!/bin/bash\necho \"Done!\"\n",
 
456
        },
 
457
        } {
 
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")
 
462
        }
 
463
}
 
464
 
 
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},
 
470
                Nonce: nonce,
 
471
        })
 
472
 
 
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)
 
477
 
 
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{
 
484
                method:   "GET",
 
485
                url:      url.String(),
 
486
                tag:      m.Tag().String(),
 
487
                password: password,
 
488
                nonce:    nonce,
 
489
        }
 
490
        resp := s.sendRequest(c, params)
 
491
        s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
 
492
}
 
493
 
 
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)
 
498
 
 
499
        data, err := ioutil.ReadFile(ch.Path)
 
500
        c.Assert(err, jc.ErrorIsNil)
 
501
 
 
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")
 
505
}
 
506
 
 
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")
 
513
        url.Path = "/charms"
 
514
        resp := s.authRequest(c, httpRequestParams{method: "GET", url: url.String()})
 
515
        s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8")
 
516
}
 
517
 
 
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")
 
525
}
 
526
 
 
527
func (s *charmsSuite) TestGetAllowsOtherEnvironment(c *gc.C) {
 
528
        envState := s.setupOtherModel(c)
 
529
 
 
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")
 
536
}
 
537
 
 
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"`)
 
543
}
 
544
 
 
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)
 
549
 
 
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)
 
559
}
 
560
 
 
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)
 
566
 
 
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)
 
580
 
 
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)
 
585
}
 
586
 
 
587
type charmsWithMacaroonsSuite struct {
 
588
        charmsCommonSuite
 
589
}
 
590
 
 
591
var _ = gc.Suite(&charmsWithMacaroonsSuite{})
 
592
 
 
593
func (s *charmsWithMacaroonsSuite) SetUpTest(c *gc.C) {
 
594
        s.macaroonAuthEnabled = true
 
595
        s.authHttpSuite.SetUpTest(c)
 
596
}
 
597
 
 
598
func (s *charmsWithMacaroonsSuite) TestWithNoBasicAuthReturnsDischargeRequiredError(c *gc.C) {
 
599
        resp := s.sendRequest(c, httpRequestParams{
 
600
                method: "POST",
 
601
                url:    s.charmsURI(c, ""),
 
602
        })
 
603
 
 
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)
 
609
}
 
610
 
 
611
func (s *charmsWithMacaroonsSuite) TestCanPostWithDischargedMacaroon(c *gc.C) {
 
612
        checkCount := 0
 
613
        s.DischargerLogin = func() string {
 
614
                checkCount++
 
615
                return s.userTag.Id()
 
616
        }
 
617
        resp := s.sendRequest(c, httpRequestParams{
 
618
                do:          s.doer(),
 
619
                method:      "POST",
 
620
                url:         s.charmsURI(c, ""),
 
621
                contentType: "foo/bar",
 
622
        })
 
623
        s.assertErrorResponse(c, resp, http.StatusBadRequest, "expected Content-Type: application/zip.+")
 
624
        c.Assert(checkCount, gc.Equals, 1)
 
625
}
 
626
 
 
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)
 
631
}
 
632
 
 
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 {
 
638
                return nil
 
639
        }
 
640
        data, err := ioutil.ReadAll(resp.Body)
 
641
        if err != nil {
 
642
                return errors.Annotatef(err, "cannot read body")
 
643
        }
 
644
        var charmResp params.CharmsResponse
 
645
        if err := json.Unmarshal(data, &charmResp); err != nil {
 
646
                return errors.Annotatef(err, "cannot unmarshal body")
 
647
        }
 
648
        errResp := &params.Error{
 
649
                Message: charmResp.Error,
 
650
                Code:    charmResp.ErrorCode,
 
651
                Info:    charmResp.ErrorInfo,
 
652
        }
 
653
        if errResp.Code != params.CodeDischargeRequired {
 
654
                return errResp
 
655
        }
 
656
        if errResp.Info == nil {
 
657
                return errors.Annotatef(err, "no error info found in discharge-required response error")
 
658
        }
 
659
        // It's a discharge-required error, so make an appropriate httpbakery
 
660
        // error from it.
 
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,
 
667
                },
 
668
        }
 
669
}