1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the LGPLv3, see LICENCE.client file for details.
21
jujutesting "github.com/juju/testing"
22
jc "github.com/juju/testing/checkers"
23
"github.com/juju/utils"
24
gc "gopkg.in/check.v1"
26
"gopkg.in/juju/charm.v5"
27
"gopkg.in/macaroon-bakery.v0/bakery/checkers"
28
"gopkg.in/macaroon-bakery.v0/bakerytest"
29
"gopkg.in/macaroon-bakery.v0/httpbakery"
32
"gopkg.in/juju/charmstore.v4"
33
"gopkg.in/juju/charmstore.v4/csclient"
34
"gopkg.in/juju/charmstore.v4/internal/storetesting"
35
"gopkg.in/juju/charmstore.v4/params"
38
var charmRepo = storetesting.Charms
40
// Define fake attributes to be used in tests.
41
var fakeReader, fakeHash, fakeSize = func() (io.ReadSeeker, string, int64) {
42
content := []byte("fake content")
45
return bytes.NewReader(content), fmt.Sprintf("%x", h.Sum(nil)), int64(len(content))
49
jujutesting.IsolatedMgoSuite
50
client *csclient.Client
52
serverParams charmstore.ServerParams
53
discharge func(cond, arg string) ([]checkers.Caveat, error)
56
var _ = gc.Suite(&suite{})
58
func (s *suite) SetUpTest(c *gc.C) {
59
s.IsolatedMgoSuite.SetUpTest(c)
60
s.startServer(c, s.Session)
61
s.client = csclient.New(csclient.Params{
63
User: s.serverParams.AuthUsername,
64
Password: s.serverParams.AuthPassword,
68
func (s *suite) TearDownTest(c *gc.C) {
70
s.IsolatedMgoSuite.TearDownTest(c)
73
func (s *suite) startServer(c *gc.C, session *mgo.Session) {
74
s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
75
return nil, fmt.Errorf("no discharge")
78
discharger := bakerytest.NewDischarger(nil, func(_ *http.Request, cond, arg string) ([]checkers.Caveat, error) {
79
return s.discharge(cond, arg)
82
serverParams := charmstore.ServerParams{
83
AuthUsername: "test-user",
84
AuthPassword: "test-password",
85
IdentityLocation: discharger.Service.Location(),
86
PublicKeyLocator: discharger,
89
db := session.DB("charmstore")
90
handler, err := charmstore.NewServer(db, nil, "", serverParams, charmstore.V4)
91
c.Assert(err, gc.IsNil)
92
s.srv = httptest.NewServer(handler)
93
s.serverParams = serverParams
97
func (s *suite) TestDefaultServerURL(c *gc.C) {
98
// Add a charm used for tests.
99
err := s.client.UploadCharmWithRevision(
100
charm.MustParseReference("~charmers/vivid/testing-wordpress-42"),
101
charmRepo.CharmDir("wordpress"),
104
c.Assert(err, gc.IsNil)
106
// Patch the default server URL.
107
s.PatchValue(&csclient.ServerURL, s.srv.URL)
109
// Instantiate a client using the default server URL.
110
client := csclient.New(csclient.Params{
111
User: s.serverParams.AuthUsername,
112
Password: s.serverParams.AuthPassword,
114
c.Assert(client.ServerURL(), gc.Equals, s.srv.URL)
116
// Check that the request succeeds.
117
err = client.Get("/vivid/testing-wordpress-42/expand-id", nil)
118
c.Assert(err, gc.IsNil)
121
func (s *suite) TestSetHTTPHeader(c *gc.C) {
122
var header http.Header
123
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
128
sendRequest := func(client *csclient.Client) {
129
req, err := http.NewRequest("GET", "", nil)
130
c.Assert(err, jc.ErrorIsNil)
131
_, err = client.Do(req, "/")
132
c.Assert(err, jc.ErrorIsNil)
134
client := csclient.New(csclient.Params{
138
// Make a first request without custom headers.
140
defaultHeaderLen := len(header)
142
// Make a second request adding a couple of custom headers.
143
h := make(http.Header)
147
client.SetHTTPHeader(h)
149
c.Assert(header, gc.HasLen, defaultHeaderLen+len(h))
150
c.Assert(header.Get("k1"), gc.Equals, "v1")
151
c.Assert(header[http.CanonicalHeaderKey("k2")], jc.DeepEquals, []string{"v2", "v3"})
153
// Make a third request without custom headers.
154
client.SetHTTPHeader(nil)
156
c.Assert(header, gc.HasLen, defaultHeaderLen)
159
var getTests = []struct {
163
expectResult interface{}
165
expectErrorCode params.ErrorCode
168
path: "/wordpress/expand-id",
169
expectResult: []params.ExpandedId{{
170
Id: "cs:utopic/wordpress-42",
173
about: "success with nil result",
174
path: "/wordpress/expand-id",
177
about: "non-absolute path",
179
expectError: `path "wordpress" is not absolute`,
181
about: "URL parse error",
182
path: "/wordpress/%zz",
183
expectError: `parse .*: invalid URL escape "%zz"`,
185
about: "result with error code",
187
expectError: "not found",
188
expectErrorCode: params.ErrNotFound,
191
func (s *suite) TestGet(c *gc.C) {
192
ch := charmRepo.CharmDir("wordpress")
193
url := charm.MustParseReference("~charmers/utopic/wordpress-42")
194
err := s.client.UploadCharmWithRevision(url, ch, 42)
195
c.Assert(err, gc.IsNil)
197
for i, test := range getTests {
198
c.Logf("test %d: %s", i, test.about)
201
var result json.RawMessage
202
var resultPtr interface{}
206
err = s.client.Get(test.path, resultPtr)
208
// Check the response.
209
if test.expectError != "" {
210
c.Assert(err, gc.ErrorMatches, test.expectError, gc.Commentf("error is %T; %#v", err, err))
211
c.Assert(result, gc.IsNil)
212
cause := errgo.Cause(err)
213
if code, ok := cause.(params.ErrorCode); ok {
214
c.Assert(code, gc.Equals, test.expectErrorCode)
216
c.Assert(test.expectErrorCode, gc.Equals, params.ErrorCode(""))
220
c.Assert(err, gc.IsNil)
221
if test.expectResult != nil {
222
c.Assert(string(result), jc.JSONEquals, test.expectResult)
227
var putErrorTests = []struct {
232
expectErrorCode params.ErrorCode
234
about: "bad JSON val",
235
path: "/~charmers/utopic/wordpress-42/meta/extra-info/foo",
237
expectError: `cannot marshal PUT body: json: unsupported type: chan int`,
239
about: "non-absolute path",
241
expectError: `path "wordpress" is not absolute`,
243
about: "URL parse error",
244
path: "/wordpress/%zz",
245
expectError: `parse .*: invalid URL escape "%zz"`,
247
about: "result with error code",
249
expectError: "not found",
250
expectErrorCode: params.ErrNotFound,
253
func (s *suite) TestPutError(c *gc.C) {
254
err := s.client.UploadCharmWithRevision(
255
charm.MustParseReference("~charmers/utopic/wordpress-42"),
256
charmRepo.CharmDir("wordpress"),
258
c.Assert(err, gc.IsNil)
260
for i, test := range putErrorTests {
261
c.Logf("test %d: %s", i, test.about)
262
err := s.client.Put(test.path, test.val)
263
c.Assert(err, gc.ErrorMatches, test.expectError)
264
cause := errgo.Cause(err)
265
if code, ok := cause.(params.ErrorCode); ok {
266
c.Assert(code, gc.Equals, test.expectErrorCode)
268
c.Assert(test.expectErrorCode, gc.Equals, params.ErrorCode(""))
273
func (s *suite) TestPutSuccess(c *gc.C) {
274
err := s.client.UploadCharmWithRevision(
275
charm.MustParseReference("~charmers/utopic/wordpress-42"),
276
charmRepo.CharmDir("wordpress"),
278
c.Assert(err, gc.IsNil)
280
perms := []string{"bob"}
281
err = s.client.Put("/~charmers/utopic/wordpress-42/meta/perm/read", perms)
282
c.Assert(err, gc.IsNil)
284
err = s.client.Get("/~charmers/utopic/wordpress-42/meta/perm/read", &got)
285
c.Assert(err, gc.IsNil)
286
c.Assert(got, jc.DeepEquals, perms)
289
func (s *suite) TestGetArchive(c *gc.C) {
290
key := s.checkGetArchive(c)
292
// Check that the downloads count for the entity has been updated.
293
s.checkCharmDownloads(c, key, 1)
296
func (s *suite) TestGetArchiveWithStatsDisabled(c *gc.C) {
297
s.client.DisableStats()
298
key := s.checkGetArchive(c)
300
// Check that the downloads count for the entity has not been updated.
301
s.checkCharmDownloads(c, key, 0)
304
var checkDownloadsAttempt = utils.AttemptStrategy{
305
Total: 1 * time.Second,
306
Delay: 100 * time.Millisecond,
309
func (s *suite) checkCharmDownloads(c *gc.C, key string, expect int64) {
311
for a := checkDownloadsAttempt.Start(); a.Next(); {
312
count := s.statsForKey(c, key)
314
// Wait for a couple of iterations to make sure that it's stable.
315
if stableCount++; stableCount >= 2 {
322
c.Errorf("unexpected download count for %s, got %d, want %d", key, count, expect)
327
func (s *suite) statsForKey(c *gc.C, key string) int64 {
328
var result []params.Statistic
329
err := s.client.Get("/stats/counter/"+key, &result)
330
c.Assert(err, gc.IsNil)
331
c.Assert(result, gc.HasLen, 1)
332
return result[0].Count
335
func (s *suite) checkGetArchive(c *gc.C) string {
336
ch := charmRepo.CharmArchive(c.MkDir(), "wordpress")
338
// Open the archive and calculate its hash and size.
339
r, expectHash, expectSize := archiveHashAndSize(c, ch.Path)
342
url := charm.MustParseReference("~charmers/utopic/wordpress-42")
343
err := s.client.UploadCharmWithRevision(url, ch, 42)
344
c.Assert(err, gc.IsNil)
346
rb, id, hash, size, err := s.client.GetArchive(url)
347
c.Assert(err, gc.IsNil)
349
c.Assert(id, jc.DeepEquals, url)
350
c.Assert(hash, gc.Equals, expectHash)
351
c.Assert(size, gc.Equals, expectSize)
354
size, err = io.Copy(h, rb)
355
c.Assert(err, gc.IsNil)
356
c.Assert(size, gc.Equals, expectSize)
357
c.Assert(fmt.Sprintf("%x", h.Sum(nil)), gc.Equals, expectHash)
359
// Return the stats key for the archive download.
360
keys := []string{params.StatsArchiveDownload, "utopic", "wordpress", "charmers", "42"}
361
return strings.Join(keys, ":")
364
func (s *suite) TestGetArchiveErrorNotFound(c *gc.C) {
365
url := charm.MustParseReference("no-such")
366
r, id, hash, size, err := s.client.GetArchive(url)
367
c.Assert(err, gc.ErrorMatches, `cannot get archive: no matching charm or bundle for "cs:no-such"`)
368
c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
369
c.Assert(r, gc.IsNil)
370
c.Assert(id, gc.IsNil)
371
c.Assert(hash, gc.Equals, "")
372
c.Assert(size, gc.Equals, int64(0))
375
var getArchiveWithBadResponseTests = []struct {
377
response *http.Response
381
about: "http client Get failure",
382
error: errgo.New("round trip failure"),
383
expectError: "cannot get archive: Get .*: round trip failure",
385
about: "no entity id header",
386
response: &http.Response{
393
params.ContentHashHeader: {fakeHash},
395
Body: ioutil.NopCloser(strings.NewReader("")),
396
ContentLength: fakeSize,
398
expectError: "no " + params.EntityIdHeader + " header found in response",
400
about: "invalid entity id header",
401
response: &http.Response{
408
params.ContentHashHeader: {fakeHash},
409
params.EntityIdHeader: {"no:such"},
411
Body: ioutil.NopCloser(strings.NewReader("")),
412
ContentLength: fakeSize,
414
expectError: `invalid entity id found in response: charm URL has invalid schema: "no:such"`,
416
about: "partial entity id header",
417
response: &http.Response{
424
params.ContentHashHeader: {fakeHash},
425
params.EntityIdHeader: {"django-42"},
427
Body: ioutil.NopCloser(strings.NewReader("")),
428
ContentLength: fakeSize,
430
expectError: `archive get returned not fully qualified entity id "cs:django-42"`,
432
about: "no hash header",
433
response: &http.Response{
440
params.EntityIdHeader: {"cs:utopic/django-42"},
442
Body: ioutil.NopCloser(strings.NewReader("")),
443
ContentLength: fakeSize,
445
expectError: "no " + params.ContentHashHeader + " header found in response",
447
about: "no content length",
448
response: &http.Response{
455
params.ContentHashHeader: {fakeHash},
456
params.EntityIdHeader: {"cs:utopic/django-42"},
458
Body: ioutil.NopCloser(strings.NewReader("")),
461
expectError: "no content length found in response",
464
func (s *suite) TestGetArchiveWithBadResponse(c *gc.C) {
465
id := charm.MustParseReference("wordpress")
466
for i, test := range getArchiveWithBadResponseTests {
467
c.Logf("test %d: %s", i, test.about)
468
cl := csclient.New(csclient.Params{
469
URL: "http://0.1.2.3",
470
HTTPClient: &http.Client{
471
Transport: &cannedRoundTripper{
477
_, _, _, _, err := cl.GetArchive(id)
478
c.Assert(err, gc.ErrorMatches, test.expectError)
482
func (s *suite) TestUploadArchiveWithCharm(c *gc.C) {
483
path := charmRepo.CharmArchivePath(c.MkDir(), "wordpress")
486
s.checkUploadArchive(c, path, "~charmers/utopic/wordpress", "cs:~charmers/utopic/wordpress-0")
488
// Posting the same archive a second time does not change its resulting id.
489
s.checkUploadArchive(c, path, "~charmers/utopic/wordpress", "cs:~charmers/utopic/wordpress-0")
491
// Posting a different archive to the same URL increases the resulting id
493
path = charmRepo.CharmArchivePath(c.MkDir(), "mysql")
494
s.checkUploadArchive(c, path, "~charmers/utopic/wordpress", "cs:~charmers/utopic/wordpress-1")
497
func (s *suite) prepareBundleCharms(c *gc.C) {
498
// Add the charms required by the wordpress-simple bundle to the store.
499
err := s.client.UploadCharmWithRevision(
500
charm.MustParseReference("~charmers/utopic/wordpress-42"),
501
charmRepo.CharmArchive(c.MkDir(), "wordpress"),
504
c.Assert(err, gc.IsNil)
505
err = s.client.UploadCharmWithRevision(
506
charm.MustParseReference("~charmers/utopic/mysql-47"),
507
charmRepo.CharmArchive(c.MkDir(), "mysql"),
510
c.Assert(err, gc.IsNil)
513
func (s *suite) TestUploadArchiveWithBundle(c *gc.C) {
514
s.prepareBundleCharms(c)
515
path := charmRepo.BundleArchivePath(c.MkDir(), "wordpress-simple")
517
s.checkUploadArchive(c, path, "~charmers/bundle/wordpress-simple", "cs:~charmers/bundle/wordpress-simple-0")
520
var uploadArchiveWithBadResponseTests = []struct {
522
response *http.Response
526
about: "http client Post failure",
527
error: errgo.New("round trip failure"),
528
expectError: "cannot post archive: Post .*: round trip failure",
530
about: "invalid JSON in body",
531
response: &http.Response{
537
Body: ioutil.NopCloser(strings.NewReader("no id here")),
540
expectError: `cannot unmarshal response "no id here": .*`,
543
func (s *suite) TestUploadArchiveWithBadResponse(c *gc.C) {
544
id := charm.MustParseReference("trusty/wordpress")
545
for i, test := range uploadArchiveWithBadResponseTests {
546
c.Logf("test %d: %s", i, test.about)
547
cl := csclient.New(csclient.Params{
548
URL: "http://0.1.2.3",
550
HTTPClient: &http.Client{
551
Transport: &cannedRoundTripper{
557
id, err := csclient.UploadArchive(cl, id, fakeReader, fakeHash, fakeSize, -1)
558
c.Assert(id, gc.IsNil)
559
c.Assert(err, gc.ErrorMatches, test.expectError)
563
func (s *suite) TestUploadArchiveWithNoSeries(c *gc.C) {
564
id, err := csclient.UploadArchive(
566
charm.MustParseReference("wordpress"),
567
fakeReader, fakeHash, fakeSize, -1)
568
c.Assert(id, gc.IsNil)
569
c.Assert(err, gc.ErrorMatches, `no series specified in "cs:wordpress"`)
572
func (s *suite) TestUploadArchiveWithServerError(c *gc.C) {
573
path := charmRepo.CharmArchivePath(c.MkDir(), "wordpress")
574
body, hash, size := archiveHashAndSize(c, path)
577
// Send an invalid hash so that the server returns an error.
578
url := charm.MustParseReference("~charmers/trusty/wordpress")
579
id, err := csclient.UploadArchive(s.client, url, body, hash+"mismatch", size, -1)
580
c.Assert(id, gc.IsNil)
581
c.Assert(err, gc.ErrorMatches, "cannot post archive: cannot put archive blob: hash mismatch")
584
func (s *suite) checkUploadArchive(c *gc.C, path, url, expectId string) {
585
// Open the archive and calculate its hash and size.
586
body, hash, size := archiveHashAndSize(c, path)
590
id, err := csclient.UploadArchive(s.client, charm.MustParseReference(url), body, hash, size, -1)
591
c.Assert(err, gc.IsNil)
592
c.Assert(id.String(), gc.Equals, expectId)
594
// Ensure the entity has been properly added to the db.
595
r, resultingId, resultingHash, resultingSize, err := s.client.GetArchive(id)
596
c.Assert(err, gc.IsNil)
598
c.Assert(resultingId, gc.DeepEquals, id)
599
c.Assert(resultingHash, gc.Equals, hash)
600
c.Assert(resultingSize, gc.Equals, size)
603
func archiveHashAndSize(c *gc.C, path string) (r csclient.ReadSeekCloser, hash string, size int64) {
604
f, err := os.Open(path)
605
c.Assert(err, gc.IsNil)
607
size, err = io.Copy(h, f)
608
c.Assert(err, gc.IsNil)
609
_, err = f.Seek(0, 0)
610
c.Assert(err, gc.IsNil)
611
return f, fmt.Sprintf("%x", h.Sum(nil)), size
614
func (s *suite) TestUploadCharmDir(c *gc.C) {
615
ch := charmRepo.CharmDir("wordpress")
616
id, err := s.client.UploadCharm(charm.MustParseReference("~charmers/utopic/wordpress"), ch)
617
c.Assert(err, gc.IsNil)
618
c.Assert(id.String(), gc.Equals, "cs:~charmers/utopic/wordpress-0")
619
s.checkUploadCharm(c, id, ch)
622
func (s *suite) TestUploadCharmArchive(c *gc.C) {
623
ch := charmRepo.CharmArchive(c.MkDir(), "wordpress")
624
id, err := s.client.UploadCharm(charm.MustParseReference("~charmers/trusty/wordpress"), ch)
625
c.Assert(err, gc.IsNil)
626
c.Assert(id.String(), gc.Equals, "cs:~charmers/trusty/wordpress-0")
627
s.checkUploadCharm(c, id, ch)
630
func (s *suite) TestUploadCharmArchiveWithRevision(c *gc.C) {
631
id := charm.MustParseReference("~charmers/trusty/wordpress-42")
632
err := s.client.UploadCharmWithRevision(
634
charmRepo.CharmDir("wordpress"),
637
c.Assert(err, gc.IsNil)
638
ch := charmRepo.CharmArchive(c.MkDir(), "wordpress")
639
s.checkUploadCharm(c, id, ch)
642
s.checkUploadCharm(c, id, ch)
645
func (s *suite) TestUploadCharmArchiveWithUnwantedRevision(c *gc.C) {
646
ch := charmRepo.CharmDir("wordpress")
647
_, err := s.client.UploadCharm(charm.MustParseReference("~charmers/bundle/wp-20"), ch)
648
c.Assert(err, gc.ErrorMatches, `revision specified in "cs:~charmers/bundle/wp-20", but should not be specified`)
651
func (s *suite) TestUploadCharmErrorUnknownType(c *gc.C) {
652
ch := charmRepo.CharmDir("wordpress")
656
id, err := s.client.UploadCharm(charm.MustParseReference("~charmers/trusty/wordpress"), unknown)
657
c.Assert(err, gc.ErrorMatches, `cannot open charm archive: cannot get the archive for entity type .*`)
658
c.Assert(id, gc.IsNil)
661
func (s *suite) TestUploadCharmErrorOpenArchive(c *gc.C) {
662
// Since the internal code path is shared between charms and bundles, just
663
// using a charm for this test also exercises the same failure for bundles.
664
ch := charmRepo.CharmArchive(c.MkDir(), "wordpress")
665
ch.Path = "no-such-file"
666
id, err := s.client.UploadCharm(charm.MustParseReference("trusty/wordpress"), ch)
667
c.Assert(err, gc.ErrorMatches, `cannot open charm archive: open no-such-file: no such file or directory`)
668
c.Assert(id, gc.IsNil)
671
func (s *suite) TestUploadCharmErrorArchiveTo(c *gc.C) {
672
// Since the internal code path is shared between charms and bundles, just
673
// using a charm for this test also exercises the same failure for bundles.
674
id, err := s.client.UploadCharm(charm.MustParseReference("trusty/wordpress"), failingArchiverTo{})
675
c.Assert(err, gc.ErrorMatches, `cannot open charm archive: cannot create entity archive: bad wolf`)
676
c.Assert(id, gc.IsNil)
679
type failingArchiverTo struct {
683
func (failingArchiverTo) ArchiveTo(io.Writer) error {
684
return errgo.New("bad wolf")
687
func (s *suite) checkUploadCharm(c *gc.C, id *charm.Reference, ch charm.Charm) {
688
r, _, _, _, err := s.client.GetArchive(id)
689
c.Assert(err, gc.IsNil)
690
data, err := ioutil.ReadAll(r)
691
c.Assert(err, gc.IsNil)
692
result, err := charm.ReadCharmArchiveBytes(data)
693
c.Assert(err, gc.IsNil)
694
// Comparing the charm metadata is sufficient for ensuring the result is
695
// the same charm previously uploaded.
696
c.Assert(result.Meta(), jc.DeepEquals, ch.Meta())
699
func (s *suite) TestUploadBundleDir(c *gc.C) {
700
s.prepareBundleCharms(c)
701
b := charmRepo.BundleDir("wordpress-simple")
702
id, err := s.client.UploadBundle(charm.MustParseReference("~charmers/bundle/wordpress-simple"), b)
703
c.Assert(err, gc.IsNil)
704
c.Assert(id.String(), gc.Equals, "cs:~charmers/bundle/wordpress-simple-0")
705
s.checkUploadBundle(c, id, b)
708
func (s *suite) TestUploadBundleArchive(c *gc.C) {
709
s.prepareBundleCharms(c)
710
path := charmRepo.BundleArchivePath(c.MkDir(), "wordpress-simple")
711
b, err := charm.ReadBundleArchive(path)
712
c.Assert(err, gc.IsNil)
713
id, err := s.client.UploadBundle(charm.MustParseReference("~charmers/bundle/wp"), b)
714
c.Assert(err, gc.IsNil)
715
c.Assert(id.String(), gc.Equals, "cs:~charmers/bundle/wp-0")
716
s.checkUploadBundle(c, id, b)
719
func (s *suite) TestUploadBundleArchiveWithUnwantedRevision(c *gc.C) {
720
s.prepareBundleCharms(c)
721
path := charmRepo.BundleArchivePath(c.MkDir(), "wordpress-simple")
722
b, err := charm.ReadBundleArchive(path)
723
c.Assert(err, gc.IsNil)
724
_, err = s.client.UploadBundle(charm.MustParseReference("~charmers/bundle/wp-20"), b)
725
c.Assert(err, gc.ErrorMatches, `revision specified in "cs:~charmers/bundle/wp-20", but should not be specified`)
728
func (s *suite) TestUploadBundleArchiveWithRevision(c *gc.C) {
729
s.prepareBundleCharms(c)
730
path := charmRepo.BundleArchivePath(c.MkDir(), "wordpress-simple")
731
b, err := charm.ReadBundleArchive(path)
732
c.Assert(err, gc.IsNil)
733
id := charm.MustParseReference("~charmers/bundle/wp-22")
734
err = s.client.UploadBundleWithRevision(id, b, 34)
735
c.Assert(err, gc.IsNil)
736
s.checkUploadBundle(c, id, b)
739
s.checkUploadBundle(c, id, b)
742
func (s *suite) TestUploadBundleErrorUploading(c *gc.C) {
743
// Uploading without specifying the series should return an error.
744
// Note that the possible upload errors are already extensively exercised
745
// as part of the client.uploadArchive tests.
746
id, err := s.client.UploadBundle(
747
charm.MustParseReference("~charmers/wordpress-simple"),
748
charmRepo.BundleDir("wordpress-simple"),
750
c.Assert(err, gc.ErrorMatches, `no series specified in "cs:~charmers/wordpress-simple"`)
751
c.Assert(id, gc.IsNil)
754
func (s *suite) TestUploadBundleErrorUnknownType(c *gc.C) {
755
b := charmRepo.BundleDir("wordpress-simple")
759
id, err := s.client.UploadBundle(charm.MustParseReference("bundle/wordpress"), unknown)
760
c.Assert(err, gc.ErrorMatches, `cannot open bundle archive: cannot get the archive for entity type .*`)
761
c.Assert(id, gc.IsNil)
764
func (s *suite) checkUploadBundle(c *gc.C, id *charm.Reference, b charm.Bundle) {
765
r, _, _, _, err := s.client.GetArchive(id)
766
c.Assert(err, gc.IsNil)
767
data, err := ioutil.ReadAll(r)
768
c.Assert(err, gc.IsNil)
769
result, err := charm.ReadBundleArchiveBytes(data)
770
c.Assert(err, gc.IsNil)
771
// Comparing the bundle data is sufficient for ensuring the result is
772
// the same bundle previously uploaded.
773
c.Assert(result.Data(), jc.DeepEquals, b.Data())
776
func (s *suite) TestDoAuthorization(c *gc.C) {
777
// Add a charm to be deleted.
778
err := s.client.UploadCharmWithRevision(
779
charm.MustParseReference("~charmers/utopic/wordpress-42"),
780
charmRepo.CharmArchive(c.MkDir(), "wordpress"),
783
c.Assert(err, gc.IsNil)
785
// Check that when we use incorrect authorization,
786
// we get an error trying to delete the charm
787
client := csclient.New(csclient.Params{
789
User: s.serverParams.AuthUsername,
790
Password: "bad password",
792
req, err := http.NewRequest("DELETE", "", nil)
793
c.Assert(err, gc.IsNil)
794
_, err = client.Do(req, "/~charmers/utopic/wordpress-42/archive")
795
c.Assert(err, gc.ErrorMatches, "invalid user name or password")
796
c.Assert(errgo.Cause(err), gc.Equals, params.ErrUnauthorized)
798
// Check that it's still there.
799
err = s.client.Get("/~charmers/utopic/wordpress-42/expand-id", nil)
800
c.Assert(err, gc.IsNil)
802
// Then check that when we use the correct authorization,
803
// the delete succeeds.
804
client = csclient.New(csclient.Params{
806
User: s.serverParams.AuthUsername,
807
Password: s.serverParams.AuthPassword,
809
req, err = http.NewRequest("DELETE", "", nil)
810
c.Assert(err, gc.IsNil)
811
resp, err := client.Do(req, "/~charmers/utopic/wordpress-42/archive")
812
c.Assert(err, gc.IsNil)
815
// Check that it's now really gone.
816
err = s.client.Get("/utopic/wordpress-42/expand-id", nil)
817
c.Assert(err, gc.ErrorMatches, `no matching charm or bundle for "cs:utopic/wordpress-42"`)
820
var getWithBadResponseTests = []struct {
823
response *http.Response
827
about: "http client Get failure",
828
error: errgo.New("round trip failure"),
829
expectError: "Get .*: round trip failure",
831
about: "body read error",
832
response: &http.Response{
838
Body: ioutil.NopCloser(&errorReader{"body read error"}),
841
expectError: "cannot read response body: body read error",
843
about: "badly formatted json response",
844
response: &http.Response{
850
Body: ioutil.NopCloser(strings.NewReader("bad")),
853
expectError: `cannot unmarshal response "bad": .*`,
855
about: "badly formatted json error",
856
response: &http.Response{
857
Status: "404 Not found",
862
Body: ioutil.NopCloser(strings.NewReader("bad")),
865
expectError: `cannot unmarshal error response "bad": .*`,
867
about: "error response with empty message",
868
response: &http.Response{
869
Status: "404 Not found",
874
Body: ioutil.NopCloser(bytes.NewReader(mustMarshalJSON(¶ms.Error{
879
expectError: "error response with empty message .*",
882
func (s *suite) TestGetWithBadResponse(c *gc.C) {
883
for i, test := range getWithBadResponseTests {
884
c.Logf("test %d: %s", i, test.about)
885
cl := csclient.New(csclient.Params{
886
URL: "http://0.1.2.3",
887
HTTPClient: &http.Client{
888
Transport: &cannedRoundTripper{
894
var result interface{}
895
err := cl.Get("/foo", &result)
896
c.Assert(err, gc.ErrorMatches, test.expectError)
900
var hyphenateTests = []struct {
908
expect: "hello-there",
911
expect: "hello-http",
914
expect: "hello-http",
917
expect: "hellothere",
919
val: "Long4Camel32WithDigits45",
920
expect: "long4-camel32-with-digits45",
922
// The result here is equally dubious, but Go identifiers
923
// should not contain underscores.
924
val: "With_Dubious_Underscore",
925
expect: "with_-dubious_-underscore",
928
func (s *suite) TestHyphenate(c *gc.C) {
929
for i, test := range hyphenateTests {
930
c.Logf("test %d. %q", i, test.val)
931
c.Assert(csclient.Hyphenate(test.val), gc.Equals, test.expect)
935
func (s *suite) TestDo(c *gc.C) {
936
// Do is tested fairly comprehensively (but indirectly)
937
// in TestGet, so just a trivial smoke test here.
938
url := charm.MustParseReference("~charmers/utopic/wordpress-42")
939
err := s.client.UploadCharmWithRevision(
941
charmRepo.CharmArchive(c.MkDir(), "wordpress"),
944
c.Assert(err, gc.IsNil)
945
err = s.client.PutExtraInfo(url, map[string]interface{}{
948
c.Assert(err, gc.IsNil)
950
req, _ := http.NewRequest("GET", "", nil)
951
resp, err := s.client.Do(req, "/wordpress/meta/extra-info/foo")
952
c.Assert(err, gc.IsNil)
953
defer resp.Body.Close()
954
data, err := ioutil.ReadAll(resp.Body)
955
c.Assert(err, gc.IsNil)
956
c.Assert(string(data), gc.Equals, `"bar"`)
959
var metaBadTypeTests = []struct {
964
expectError: "expected pointer, not string",
967
expectError: `expected pointer to struct, not \*string`,
969
result: new(struct{ Embed }),
970
expectError: "anonymous fields not supported",
972
expectError: "expected valid result pointer, not nil",
975
func (s *suite) TestMetaBadType(c *gc.C) {
976
id := charm.MustParseReference("wordpress")
977
for _, test := range metaBadTypeTests {
978
_, err := s.client.Meta(id, test.result)
979
c.Assert(err, gc.ErrorMatches, test.expectError)
986
func (s *suite) TestMeta(c *gc.C) {
987
ch := charmRepo.CharmDir("wordpress")
988
url := charm.MustParseReference("~charmers/utopic/wordpress-42")
989
purl := charm.MustParseReference("utopic/wordpress-42")
990
err := s.client.UploadCharmWithRevision(url, ch, 42)
991
c.Assert(err, gc.IsNil)
993
// Put some extra-info.
994
err = s.client.PutExtraInfo(url, map[string]interface{}{
997
c.Assert(err, gc.IsNil)
1002
expectResult interface{}
1004
expectErrorCode params.ErrorCode
1007
id: "utopic/wordpress",
1008
expectResult: &struct{}{},
1010
about: "single field",
1011
id: "utopic/wordpress",
1012
expectResult: &struct {
1013
CharmMetadata *charm.Meta
1015
CharmMetadata: ch.Meta(),
1018
about: "three fields",
1020
expectResult: &struct {
1021
CharmMetadata *charm.Meta
1022
CharmConfig *charm.Config
1023
ExtraInfo map[string]string
1025
CharmMetadata: ch.Meta(),
1026
CharmConfig: ch.Config(),
1027
ExtraInfo: map[string]string{"attr": "value"},
1030
about: "tagged field",
1032
expectResult: &struct {
1033
Foo *charm.Meta `csclient:"charm-metadata"`
1034
Attr string `csclient:"extra-info/attr"`
1040
about: "id not found",
1042
expectResult: &struct{}{},
1043
expectError: `cannot get "/bogus/meta/any": no matching charm or bundle for "cs:bogus"`,
1044
expectErrorCode: params.ErrNotFound,
1046
about: "unmarshal into invalid type",
1048
expectResult: new(struct {
1049
CharmMetadata []string
1051
expectError: `cannot unmarshal charm-metadata: json: cannot unmarshal object into Go value of type \[]string`,
1053
about: "unmarshal into struct with unexported fields",
1055
expectResult: &struct {
1057
CharmMetadata *charm.Meta
1058
// Embedded anonymous fields don't get tagged as unexported
1059
// due to https://code.google.com/p/go/issues/detail?id=7247
1060
// TODO fix in go 1.5.
1063
CharmMetadata: ch.Meta(),
1066
about: "metadata not appropriate for charm",
1068
expectResult: &struct {
1069
CharmMetadata *charm.Meta
1070
BundleMetadata *charm.BundleData
1072
CharmMetadata: ch.Meta(),
1075
for i, test := range tests {
1076
c.Logf("test %d: %s", i, test.about)
1077
// Make a result value of the same type as the expected result,
1079
result := reflect.New(reflect.TypeOf(test.expectResult).Elem()).Interface()
1080
id, err := s.client.Meta(charm.MustParseReference(test.id), result)
1081
if test.expectError != "" {
1082
c.Assert(err, gc.ErrorMatches, test.expectError)
1083
if code, ok := errgo.Cause(err).(params.ErrorCode); ok {
1084
c.Assert(code, gc.Equals, test.expectErrorCode)
1086
c.Assert(test.expectErrorCode, gc.Equals, params.ErrorCode(""))
1088
c.Assert(id, gc.IsNil)
1091
c.Assert(err, gc.IsNil)
1092
c.Assert(id, jc.DeepEquals, purl)
1093
c.Assert(result, jc.DeepEquals, test.expectResult)
1097
func (s *suite) TestPutExtraInfo(c *gc.C) {
1098
ch := charmRepo.CharmDir("wordpress")
1099
url := charm.MustParseReference("~charmers/utopic/wordpress-42")
1100
err := s.client.UploadCharmWithRevision(url, ch, 42)
1101
c.Assert(err, gc.IsNil)
1103
// Put some info in.
1104
info := map[string]interface{}{
1106
"attr2": []interface{}{"one", "two"},
1108
err = s.client.PutExtraInfo(url, info)
1109
c.Assert(err, gc.IsNil)
1111
// Verify that we get it back OK.
1113
ExtraInfo map[string]interface{}
1115
_, err = s.client.Meta(url, &val)
1116
c.Assert(err, gc.IsNil)
1117
c.Assert(val.ExtraInfo, jc.DeepEquals, info)
1119
// Put some more in.
1120
err = s.client.PutExtraInfo(url, map[string]interface{}{
1123
c.Assert(err, gc.IsNil)
1125
// Verify that we get all the previous results and the new value.
1126
info["attr3"] = "three"
1127
_, err = s.client.Meta(url, &val)
1128
c.Assert(err, gc.IsNil)
1129
c.Assert(val.ExtraInfo, jc.DeepEquals, info)
1132
func (s *suite) TestPutExtraInfoWithError(c *gc.C) {
1133
err := s.client.PutExtraInfo(charm.MustParseReference("wordpress"), map[string]interface{}{"attr": "val"})
1134
c.Assert(err, gc.ErrorMatches, `no matching charm or bundle for "cs:wordpress"`)
1135
c.Assert(errgo.Cause(err), gc.Equals, params.ErrNotFound)
1138
type errorReader struct {
1142
func (e *errorReader) Read(buf []byte) (int, error) {
1143
return 0, errgo.New(e.error)
1146
type cannedRoundTripper struct {
1151
func (r *cannedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
1152
return r.resp, r.error
1155
func mustMarshalJSON(x interface{}) []byte {
1156
data, err := json.Marshal(x)
1163
func (s *suite) TestLog(c *gc.C) {
1166
level params.LogLevel
1168
urls []*charm.Reference
1170
typ: params.IngestionType,
1171
level: params.InfoLevel,
1172
message: "ingestion info",
1175
typ: params.LegacyStatisticsType,
1176
level: params.ErrorLevel,
1177
message: "statistics error",
1178
urls: []*charm.Reference{
1179
charm.MustParseReference("cs:mysql"),
1180
charm.MustParseReference("cs:wordpress"),
1184
for _, log := range logs {
1185
err := s.client.Log(log.typ, log.level, log.message, log.urls...)
1186
c.Assert(err, gc.IsNil)
1188
var result []*params.LogResponse
1189
err := s.client.Get("/log", &result)
1190
c.Assert(err, gc.IsNil)
1191
c.Assert(result, gc.HasLen, len(logs))
1192
for i, l := range result {
1193
c.Assert(l.Type, gc.Equals, logs[len(logs)-(1+i)].typ)
1194
c.Assert(l.Level, gc.Equals, logs[len(logs)-(1+i)].level)
1196
err := json.Unmarshal([]byte(l.Data), &msg)
1197
c.Assert(err, gc.IsNil)
1198
c.Assert(msg, gc.Equals, logs[len(logs)-(1+i)].message)
1199
c.Assert(l.URLs, jc.DeepEquals, logs[len(logs)-(1+i)].urls)
1203
func (s *suite) TestMacaroonAuthorization(c *gc.C) {
1204
ch := charmRepo.CharmDir("wordpress")
1205
curl := charm.MustParseReference("~charmers/utopic/wordpress-42")
1206
purl := charm.MustParseReference("utopic/wordpress-42")
1207
err := s.client.UploadCharmWithRevision(curl, ch, 42)
1208
c.Assert(err, gc.IsNil)
1210
err = s.client.Put("/"+curl.Path()+"/meta/perm/read", []string{"bob"})
1211
c.Assert(err, gc.IsNil)
1213
// Create a client without basic auth credentials
1214
client := csclient.New(csclient.Params{
1218
var result struct{ IdRevision struct{ Revision int } }
1219
// TODO 2015-01-23: once supported, rewrite the test using POST requests.
1220
_, err = client.Meta(purl, &result)
1221
c.Assert(err, gc.ErrorMatches, `cannot get "/utopic/wordpress-42/meta/any\?include=id-revision": cannot get discharge from ".*": third party refused discharge: cannot discharge: no discharge`)
1222
c.Assert(httpbakery.IsDischargeError(errgo.Cause(err)), gc.Equals, true)
1224
s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
1225
return []checkers.Caveat{checkers.DeclaredCaveat("username", "bob")}, nil
1227
_, err = client.Meta(curl, &result)
1228
c.Assert(err, gc.IsNil)
1229
c.Assert(result.IdRevision.Revision, gc.Equals, curl.Revision)
1231
visitURL := "http://0.1.2.3/visitURL"
1232
s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
1233
return nil, &httpbakery.Error{
1234
Code: httpbakery.ErrInteractionRequired,
1235
Message: "interaction required",
1236
Info: &httpbakery.ErrorInfo{
1238
WaitURL: "http://0.1.2.3/waitURL",
1242
client = csclient.New(csclient.Params{
1244
VisitWebPage: func(vurl *url.URL) error {
1245
c.Check(vurl.String(), gc.Equals, visitURL)
1246
return fmt.Errorf("stopping interaction")
1249
_, err = client.Meta(purl, &result)
1250
c.Assert(err, gc.ErrorMatches, `cannot get "/utopic/wordpress-42/meta/any\?include=id-revision": cannot get discharge from ".*": cannot start interactive session: stopping interaction`)
1251
c.Assert(result.IdRevision.Revision, gc.Equals, curl.Revision)
1252
c.Assert(httpbakery.IsInteractionError(errgo.Cause(err)), gc.Equals, true)
1255
func (s *suite) TestLogin(c *gc.C) {
1256
ch := charmRepo.CharmDir("wordpress")
1257
url := charm.MustParseReference("~charmers/utopic/wordpress-42")
1258
purl := charm.MustParseReference("utopic/wordpress-42")
1259
err := s.client.UploadCharmWithRevision(url, ch, 42)
1260
c.Assert(err, gc.IsNil)
1262
err = s.client.Put("/"+url.Path()+"/meta/perm/read", []string{"bob"})
1263
c.Assert(err, gc.IsNil)
1264
client := csclient.New(csclient.Params{
1268
var result struct{ IdRevision struct{ Revision int } }
1269
_, err = client.Meta(purl, &result)
1270
c.Assert(err, gc.NotNil)
1272
// Try logging in when the discharger fails.
1273
err = client.Login()
1274
c.Assert(err, gc.ErrorMatches, `cannot discharge login macaroon: cannot get discharge from ".*": third party refused discharge: cannot discharge: no discharge`)
1276
// Allow the discharge.
1277
s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
1278
return []checkers.Caveat{checkers.DeclaredCaveat("username", "bob")}, nil
1280
err = client.Login()
1281
c.Assert(err, gc.IsNil)
1283
// Change discharge so that we're sure the cookies are being
1284
// used rather than the discharge mechanism.
1285
s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
1286
return nil, fmt.Errorf("no discharge")
1289
// Check that the request still works.
1290
_, err = client.Meta(purl, &result)
1291
c.Assert(err, gc.IsNil)
1292
c.Assert(result.IdRevision.Revision, gc.Equals, url.Revision)