~sinzui/ubuntu/wily/juju-core/wily-1.24.7

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v4/csclient/csclient_test.go

  • Committer: Package Import Robot
  • Author(s): Curtis C. Hovey
  • Date: 2015-09-22 15:27:01 UTC
  • mfrom: (1.1.36)
  • Revision ID: package-import@ubuntu.com-20150922152701-lzq2yhn2uaahrdqu
Tags: 1.24.6-0ubuntu1
* New upstream release (LP: #1481556).
* d/copyright updated for Juju 1.24.6 (Last verified commit changes).
* d/tests/* Run tests with upstart when Juju version before 1.23.
* Prefer gccgo-5 for ppc64el and arm64 in build-deps.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2015 Canonical Ltd.
 
2
// Licensed under the LGPLv3, see LICENCE.client file for details.
 
3
 
 
4
package csclient_test
 
5
 
 
6
import (
 
7
        "bytes"
 
8
        "crypto/sha512"
 
9
        "encoding/json"
 
10
        "fmt"
 
11
        "io"
 
12
        "io/ioutil"
 
13
        "net/http"
 
14
        "net/http/httptest"
 
15
        "net/url"
 
16
        "os"
 
17
        "reflect"
 
18
        "strings"
 
19
        "time"
 
20
 
 
21
        jujutesting "github.com/juju/testing"
 
22
        jc "github.com/juju/testing/checkers"
 
23
        "github.com/juju/utils"
 
24
        gc "gopkg.in/check.v1"
 
25
        "gopkg.in/errgo.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"
 
30
        "gopkg.in/mgo.v2"
 
31
 
 
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"
 
36
)
 
37
 
 
38
var charmRepo = storetesting.Charms
 
39
 
 
40
// Define fake attributes to be used in tests.
 
41
var fakeReader, fakeHash, fakeSize = func() (io.ReadSeeker, string, int64) {
 
42
        content := []byte("fake content")
 
43
        h := sha512.New384()
 
44
        h.Write(content)
 
45
        return bytes.NewReader(content), fmt.Sprintf("%x", h.Sum(nil)), int64(len(content))
 
46
}()
 
47
 
 
48
type suite struct {
 
49
        jujutesting.IsolatedMgoSuite
 
50
        client       *csclient.Client
 
51
        srv          *httptest.Server
 
52
        serverParams charmstore.ServerParams
 
53
        discharge    func(cond, arg string) ([]checkers.Caveat, error)
 
54
}
 
55
 
 
56
var _ = gc.Suite(&suite{})
 
57
 
 
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{
 
62
                URL:      s.srv.URL,
 
63
                User:     s.serverParams.AuthUsername,
 
64
                Password: s.serverParams.AuthPassword,
 
65
        })
 
66
}
 
67
 
 
68
func (s *suite) TearDownTest(c *gc.C) {
 
69
        s.srv.Close()
 
70
        s.IsolatedMgoSuite.TearDownTest(c)
 
71
}
 
72
 
 
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")
 
76
        }
 
77
 
 
78
        discharger := bakerytest.NewDischarger(nil, func(_ *http.Request, cond, arg string) ([]checkers.Caveat, error) {
 
79
                return s.discharge(cond, arg)
 
80
        })
 
81
 
 
82
        serverParams := charmstore.ServerParams{
 
83
                AuthUsername:     "test-user",
 
84
                AuthPassword:     "test-password",
 
85
                IdentityLocation: discharger.Service.Location(),
 
86
                PublicKeyLocator: discharger,
 
87
        }
 
88
 
 
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
 
94
 
 
95
}
 
96
 
 
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"),
 
102
                42,
 
103
        )
 
104
        c.Assert(err, gc.IsNil)
 
105
 
 
106
        // Patch the default server URL.
 
107
        s.PatchValue(&csclient.ServerURL, s.srv.URL)
 
108
 
 
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,
 
113
        })
 
114
        c.Assert(client.ServerURL(), gc.Equals, s.srv.URL)
 
115
 
 
116
        // Check that the request succeeds.
 
117
        err = client.Get("/vivid/testing-wordpress-42/expand-id", nil)
 
118
        c.Assert(err, gc.IsNil)
 
119
}
 
120
 
 
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) {
 
124
                header = req.Header
 
125
        }))
 
126
        defer srv.Close()
 
127
 
 
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)
 
133
        }
 
134
        client := csclient.New(csclient.Params{
 
135
                URL: srv.URL,
 
136
        })
 
137
 
 
138
        // Make a first request without custom headers.
 
139
        sendRequest(client)
 
140
        defaultHeaderLen := len(header)
 
141
 
 
142
        // Make a second request adding a couple of custom headers.
 
143
        h := make(http.Header)
 
144
        h.Set("k1", "v1")
 
145
        h.Add("k2", "v2")
 
146
        h.Add("k2", "v3")
 
147
        client.SetHTTPHeader(h)
 
148
        sendRequest(client)
 
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"})
 
152
 
 
153
        // Make a third request without custom headers.
 
154
        client.SetHTTPHeader(nil)
 
155
        sendRequest(client)
 
156
        c.Assert(header, gc.HasLen, defaultHeaderLen)
 
157
}
 
158
 
 
159
var getTests = []struct {
 
160
        about           string
 
161
        path            string
 
162
        nilResult       bool
 
163
        expectResult    interface{}
 
164
        expectError     string
 
165
        expectErrorCode params.ErrorCode
 
166
}{{
 
167
        about: "success",
 
168
        path:  "/wordpress/expand-id",
 
169
        expectResult: []params.ExpandedId{{
 
170
                Id: "cs:utopic/wordpress-42",
 
171
        }},
 
172
}, {
 
173
        about:     "success with nil result",
 
174
        path:      "/wordpress/expand-id",
 
175
        nilResult: true,
 
176
}, {
 
177
        about:       "non-absolute path",
 
178
        path:        "wordpress",
 
179
        expectError: `path "wordpress" is not absolute`,
 
180
}, {
 
181
        about:       "URL parse error",
 
182
        path:        "/wordpress/%zz",
 
183
        expectError: `parse .*: invalid URL escape "%zz"`,
 
184
}, {
 
185
        about:           "result with error code",
 
186
        path:            "/blahblah",
 
187
        expectError:     "not found",
 
188
        expectErrorCode: params.ErrNotFound,
 
189
}}
 
190
 
 
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)
 
196
 
 
197
        for i, test := range getTests {
 
198
                c.Logf("test %d: %s", i, test.about)
 
199
 
 
200
                // Send the request.
 
201
                var result json.RawMessage
 
202
                var resultPtr interface{}
 
203
                if !test.nilResult {
 
204
                        resultPtr = &result
 
205
                }
 
206
                err = s.client.Get(test.path, resultPtr)
 
207
 
 
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)
 
215
                        } else {
 
216
                                c.Assert(test.expectErrorCode, gc.Equals, params.ErrorCode(""))
 
217
                        }
 
218
                        continue
 
219
                }
 
220
                c.Assert(err, gc.IsNil)
 
221
                if test.expectResult != nil {
 
222
                        c.Assert(string(result), jc.JSONEquals, test.expectResult)
 
223
                }
 
224
        }
 
225
}
 
226
 
 
227
var putErrorTests = []struct {
 
228
        about           string
 
229
        path            string
 
230
        val             interface{}
 
231
        expectError     string
 
232
        expectErrorCode params.ErrorCode
 
233
}{{
 
234
        about:       "bad JSON val",
 
235
        path:        "/~charmers/utopic/wordpress-42/meta/extra-info/foo",
 
236
        val:         make(chan int),
 
237
        expectError: `cannot marshal PUT body: json: unsupported type: chan int`,
 
238
}, {
 
239
        about:       "non-absolute path",
 
240
        path:        "wordpress",
 
241
        expectError: `path "wordpress" is not absolute`,
 
242
}, {
 
243
        about:       "URL parse error",
 
244
        path:        "/wordpress/%zz",
 
245
        expectError: `parse .*: invalid URL escape "%zz"`,
 
246
}, {
 
247
        about:           "result with error code",
 
248
        path:            "/blahblah",
 
249
        expectError:     "not found",
 
250
        expectErrorCode: params.ErrNotFound,
 
251
}}
 
252
 
 
253
func (s *suite) TestPutError(c *gc.C) {
 
254
        err := s.client.UploadCharmWithRevision(
 
255
                charm.MustParseReference("~charmers/utopic/wordpress-42"),
 
256
                charmRepo.CharmDir("wordpress"),
 
257
                42)
 
258
        c.Assert(err, gc.IsNil)
 
259
 
 
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)
 
267
                } else {
 
268
                        c.Assert(test.expectErrorCode, gc.Equals, params.ErrorCode(""))
 
269
                }
 
270
        }
 
271
}
 
272
 
 
273
func (s *suite) TestPutSuccess(c *gc.C) {
 
274
        err := s.client.UploadCharmWithRevision(
 
275
                charm.MustParseReference("~charmers/utopic/wordpress-42"),
 
276
                charmRepo.CharmDir("wordpress"),
 
277
                42)
 
278
        c.Assert(err, gc.IsNil)
 
279
 
 
280
        perms := []string{"bob"}
 
281
        err = s.client.Put("/~charmers/utopic/wordpress-42/meta/perm/read", perms)
 
282
        c.Assert(err, gc.IsNil)
 
283
        var got []string
 
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)
 
287
}
 
288
 
 
289
func (s *suite) TestGetArchive(c *gc.C) {
 
290
        key := s.checkGetArchive(c)
 
291
 
 
292
        // Check that the downloads count for the entity has been updated.
 
293
        s.checkCharmDownloads(c, key, 1)
 
294
}
 
295
 
 
296
func (s *suite) TestGetArchiveWithStatsDisabled(c *gc.C) {
 
297
        s.client.DisableStats()
 
298
        key := s.checkGetArchive(c)
 
299
 
 
300
        // Check that the downloads count for the entity has not been updated.
 
301
        s.checkCharmDownloads(c, key, 0)
 
302
}
 
303
 
 
304
var checkDownloadsAttempt = utils.AttemptStrategy{
 
305
        Total: 1 * time.Second,
 
306
        Delay: 100 * time.Millisecond,
 
307
}
 
308
 
 
309
func (s *suite) checkCharmDownloads(c *gc.C, key string, expect int64) {
 
310
        stableCount := 0
 
311
        for a := checkDownloadsAttempt.Start(); a.Next(); {
 
312
                count := s.statsForKey(c, key)
 
313
                if count == expect {
 
314
                        // Wait for a couple of iterations to make sure that it's stable.
 
315
                        if stableCount++; stableCount >= 2 {
 
316
                                return
 
317
                        }
 
318
                } else {
 
319
                        stableCount = 0
 
320
                }
 
321
                if !a.HasNext() {
 
322
                        c.Errorf("unexpected download count for %s, got %d, want %d", key, count, expect)
 
323
                }
 
324
        }
 
325
}
 
326
 
 
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
 
333
}
 
334
 
 
335
func (s *suite) checkGetArchive(c *gc.C) string {
 
336
        ch := charmRepo.CharmArchive(c.MkDir(), "wordpress")
 
337
 
 
338
        // Open the archive and calculate its hash and size.
 
339
        r, expectHash, expectSize := archiveHashAndSize(c, ch.Path)
 
340
        r.Close()
 
341
 
 
342
        url := charm.MustParseReference("~charmers/utopic/wordpress-42")
 
343
        err := s.client.UploadCharmWithRevision(url, ch, 42)
 
344
        c.Assert(err, gc.IsNil)
 
345
 
 
346
        rb, id, hash, size, err := s.client.GetArchive(url)
 
347
        c.Assert(err, gc.IsNil)
 
348
        defer rb.Close()
 
349
        c.Assert(id, jc.DeepEquals, url)
 
350
        c.Assert(hash, gc.Equals, expectHash)
 
351
        c.Assert(size, gc.Equals, expectSize)
 
352
 
 
353
        h := sha512.New384()
 
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)
 
358
 
 
359
        // Return the stats key for the archive download.
 
360
        keys := []string{params.StatsArchiveDownload, "utopic", "wordpress", "charmers", "42"}
 
361
        return strings.Join(keys, ":")
 
362
}
 
363
 
 
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))
 
373
}
 
374
 
 
375
var getArchiveWithBadResponseTests = []struct {
 
376
        about       string
 
377
        response    *http.Response
 
378
        error       error
 
379
        expectError string
 
380
}{{
 
381
        about:       "http client Get failure",
 
382
        error:       errgo.New("round trip failure"),
 
383
        expectError: "cannot get archive: Get .*: round trip failure",
 
384
}, {
 
385
        about: "no entity id header",
 
386
        response: &http.Response{
 
387
                Status:     "200 OK",
 
388
                StatusCode: 200,
 
389
                Proto:      "HTTP/1.0",
 
390
                ProtoMajor: 1,
 
391
                ProtoMinor: 0,
 
392
                Header: http.Header{
 
393
                        params.ContentHashHeader: {fakeHash},
 
394
                },
 
395
                Body:          ioutil.NopCloser(strings.NewReader("")),
 
396
                ContentLength: fakeSize,
 
397
        },
 
398
        expectError: "no " + params.EntityIdHeader + " header found in response",
 
399
}, {
 
400
        about: "invalid entity id header",
 
401
        response: &http.Response{
 
402
                Status:     "200 OK",
 
403
                StatusCode: 200,
 
404
                Proto:      "HTTP/1.0",
 
405
                ProtoMajor: 1,
 
406
                ProtoMinor: 0,
 
407
                Header: http.Header{
 
408
                        params.ContentHashHeader: {fakeHash},
 
409
                        params.EntityIdHeader:    {"no:such"},
 
410
                },
 
411
                Body:          ioutil.NopCloser(strings.NewReader("")),
 
412
                ContentLength: fakeSize,
 
413
        },
 
414
        expectError: `invalid entity id found in response: charm URL has invalid schema: "no:such"`,
 
415
}, {
 
416
        about: "partial entity id header",
 
417
        response: &http.Response{
 
418
                Status:     "200 OK",
 
419
                StatusCode: 200,
 
420
                Proto:      "HTTP/1.0",
 
421
                ProtoMajor: 1,
 
422
                ProtoMinor: 0,
 
423
                Header: http.Header{
 
424
                        params.ContentHashHeader: {fakeHash},
 
425
                        params.EntityIdHeader:    {"django-42"},
 
426
                },
 
427
                Body:          ioutil.NopCloser(strings.NewReader("")),
 
428
                ContentLength: fakeSize,
 
429
        },
 
430
        expectError: `archive get returned not fully qualified entity id "cs:django-42"`,
 
431
}, {
 
432
        about: "no hash header",
 
433
        response: &http.Response{
 
434
                Status:     "200 OK",
 
435
                StatusCode: 200,
 
436
                Proto:      "HTTP/1.0",
 
437
                ProtoMajor: 1,
 
438
                ProtoMinor: 0,
 
439
                Header: http.Header{
 
440
                        params.EntityIdHeader: {"cs:utopic/django-42"},
 
441
                },
 
442
                Body:          ioutil.NopCloser(strings.NewReader("")),
 
443
                ContentLength: fakeSize,
 
444
        },
 
445
        expectError: "no " + params.ContentHashHeader + " header found in response",
 
446
}, {
 
447
        about: "no content length",
 
448
        response: &http.Response{
 
449
                Status:     "200 OK",
 
450
                StatusCode: 200,
 
451
                Proto:      "HTTP/1.0",
 
452
                ProtoMajor: 1,
 
453
                ProtoMinor: 0,
 
454
                Header: http.Header{
 
455
                        params.ContentHashHeader: {fakeHash},
 
456
                        params.EntityIdHeader:    {"cs:utopic/django-42"},
 
457
                },
 
458
                Body:          ioutil.NopCloser(strings.NewReader("")),
 
459
                ContentLength: -1,
 
460
        },
 
461
        expectError: "no content length found in response",
 
462
}}
 
463
 
 
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{
 
472
                                        resp:  test.response,
 
473
                                        error: test.error,
 
474
                                },
 
475
                        },
 
476
                })
 
477
                _, _, _, _, err := cl.GetArchive(id)
 
478
                c.Assert(err, gc.ErrorMatches, test.expectError)
 
479
        }
 
480
}
 
481
 
 
482
func (s *suite) TestUploadArchiveWithCharm(c *gc.C) {
 
483
        path := charmRepo.CharmArchivePath(c.MkDir(), "wordpress")
 
484
 
 
485
        // Post the archive.
 
486
        s.checkUploadArchive(c, path, "~charmers/utopic/wordpress", "cs:~charmers/utopic/wordpress-0")
 
487
 
 
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")
 
490
 
 
491
        // Posting a different archive to the same URL increases the resulting id
 
492
        // revision.
 
493
        path = charmRepo.CharmArchivePath(c.MkDir(), "mysql")
 
494
        s.checkUploadArchive(c, path, "~charmers/utopic/wordpress", "cs:~charmers/utopic/wordpress-1")
 
495
}
 
496
 
 
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"),
 
502
                42,
 
503
        )
 
504
        c.Assert(err, gc.IsNil)
 
505
        err = s.client.UploadCharmWithRevision(
 
506
                charm.MustParseReference("~charmers/utopic/mysql-47"),
 
507
                charmRepo.CharmArchive(c.MkDir(), "mysql"),
 
508
                47,
 
509
        )
 
510
        c.Assert(err, gc.IsNil)
 
511
}
 
512
 
 
513
func (s *suite) TestUploadArchiveWithBundle(c *gc.C) {
 
514
        s.prepareBundleCharms(c)
 
515
        path := charmRepo.BundleArchivePath(c.MkDir(), "wordpress-simple")
 
516
        // Post the archive.
 
517
        s.checkUploadArchive(c, path, "~charmers/bundle/wordpress-simple", "cs:~charmers/bundle/wordpress-simple-0")
 
518
}
 
519
 
 
520
var uploadArchiveWithBadResponseTests = []struct {
 
521
        about       string
 
522
        response    *http.Response
 
523
        error       error
 
524
        expectError string
 
525
}{{
 
526
        about:       "http client Post failure",
 
527
        error:       errgo.New("round trip failure"),
 
528
        expectError: "cannot post archive: Post .*: round trip failure",
 
529
}, {
 
530
        about: "invalid JSON in body",
 
531
        response: &http.Response{
 
532
                Status:        "200 OK",
 
533
                StatusCode:    200,
 
534
                Proto:         "HTTP/1.0",
 
535
                ProtoMajor:    1,
 
536
                ProtoMinor:    0,
 
537
                Body:          ioutil.NopCloser(strings.NewReader("no id here")),
 
538
                ContentLength: 0,
 
539
        },
 
540
        expectError: `cannot unmarshal response "no id here": .*`,
 
541
}}
 
542
 
 
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",
 
549
                        User: "bob",
 
550
                        HTTPClient: &http.Client{
 
551
                                Transport: &cannedRoundTripper{
 
552
                                        resp:  test.response,
 
553
                                        error: test.error,
 
554
                                },
 
555
                        },
 
556
                })
 
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)
 
560
        }
 
561
}
 
562
 
 
563
func (s *suite) TestUploadArchiveWithNoSeries(c *gc.C) {
 
564
        id, err := csclient.UploadArchive(
 
565
                s.client,
 
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"`)
 
570
}
 
571
 
 
572
func (s *suite) TestUploadArchiveWithServerError(c *gc.C) {
 
573
        path := charmRepo.CharmArchivePath(c.MkDir(), "wordpress")
 
574
        body, hash, size := archiveHashAndSize(c, path)
 
575
        defer body.Close()
 
576
 
 
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")
 
582
}
 
583
 
 
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)
 
587
        defer body.Close()
 
588
 
 
589
        // Post the archive.
 
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)
 
593
 
 
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)
 
597
        defer r.Close()
 
598
        c.Assert(resultingId, gc.DeepEquals, id)
 
599
        c.Assert(resultingHash, gc.Equals, hash)
 
600
        c.Assert(resultingSize, gc.Equals, size)
 
601
}
 
602
 
 
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)
 
606
        h := sha512.New384()
 
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
 
612
}
 
613
 
 
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)
 
620
}
 
621
 
 
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)
 
628
}
 
629
 
 
630
func (s *suite) TestUploadCharmArchiveWithRevision(c *gc.C) {
 
631
        id := charm.MustParseReference("~charmers/trusty/wordpress-42")
 
632
        err := s.client.UploadCharmWithRevision(
 
633
                id,
 
634
                charmRepo.CharmDir("wordpress"),
 
635
                10,
 
636
        )
 
637
        c.Assert(err, gc.IsNil)
 
638
        ch := charmRepo.CharmArchive(c.MkDir(), "wordpress")
 
639
        s.checkUploadCharm(c, id, ch)
 
640
        id.User = ""
 
641
        id.Revision = 10
 
642
        s.checkUploadCharm(c, id, ch)
 
643
}
 
644
 
 
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`)
 
649
}
 
650
 
 
651
func (s *suite) TestUploadCharmErrorUnknownType(c *gc.C) {
 
652
        ch := charmRepo.CharmDir("wordpress")
 
653
        unknown := struct {
 
654
                charm.Charm
 
655
        }{ch}
 
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)
 
659
}
 
660
 
 
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)
 
669
}
 
670
 
 
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)
 
677
}
 
678
 
 
679
type failingArchiverTo struct {
 
680
        charm.Charm
 
681
}
 
682
 
 
683
func (failingArchiverTo) ArchiveTo(io.Writer) error {
 
684
        return errgo.New("bad wolf")
 
685
}
 
686
 
 
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())
 
697
}
 
698
 
 
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)
 
706
}
 
707
 
 
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)
 
717
}
 
718
 
 
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`)
 
726
}
 
727
 
 
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)
 
737
        id.User = ""
 
738
        id.Revision = 34
 
739
        s.checkUploadBundle(c, id, b)
 
740
}
 
741
 
 
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"),
 
749
        )
 
750
        c.Assert(err, gc.ErrorMatches, `no series specified in "cs:~charmers/wordpress-simple"`)
 
751
        c.Assert(id, gc.IsNil)
 
752
}
 
753
 
 
754
func (s *suite) TestUploadBundleErrorUnknownType(c *gc.C) {
 
755
        b := charmRepo.BundleDir("wordpress-simple")
 
756
        unknown := struct {
 
757
                charm.Bundle
 
758
        }{b}
 
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)
 
762
}
 
763
 
 
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())
 
774
}
 
775
 
 
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"),
 
781
                42,
 
782
        )
 
783
        c.Assert(err, gc.IsNil)
 
784
 
 
785
        // Check that when we use incorrect authorization,
 
786
        // we get an error trying to delete the charm
 
787
        client := csclient.New(csclient.Params{
 
788
                URL:      s.srv.URL,
 
789
                User:     s.serverParams.AuthUsername,
 
790
                Password: "bad password",
 
791
        })
 
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)
 
797
 
 
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)
 
801
 
 
802
        // Then check that when we use the correct authorization,
 
803
        // the delete succeeds.
 
804
        client = csclient.New(csclient.Params{
 
805
                URL:      s.srv.URL,
 
806
                User:     s.serverParams.AuthUsername,
 
807
                Password: s.serverParams.AuthPassword,
 
808
        })
 
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)
 
813
        resp.Body.Close()
 
814
 
 
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"`)
 
818
}
 
819
 
 
820
var getWithBadResponseTests = []struct {
 
821
        about       string
 
822
        error       error
 
823
        response    *http.Response
 
824
        responseErr error
 
825
        expectError string
 
826
}{{
 
827
        about:       "http client Get failure",
 
828
        error:       errgo.New("round trip failure"),
 
829
        expectError: "Get .*: round trip failure",
 
830
}, {
 
831
        about: "body read error",
 
832
        response: &http.Response{
 
833
                Status:        "200 OK",
 
834
                StatusCode:    200,
 
835
                Proto:         "HTTP/1.0",
 
836
                ProtoMajor:    1,
 
837
                ProtoMinor:    0,
 
838
                Body:          ioutil.NopCloser(&errorReader{"body read error"}),
 
839
                ContentLength: -1,
 
840
        },
 
841
        expectError: "cannot read response body: body read error",
 
842
}, {
 
843
        about: "badly formatted json response",
 
844
        response: &http.Response{
 
845
                Status:        "200 OK",
 
846
                StatusCode:    200,
 
847
                Proto:         "HTTP/1.0",
 
848
                ProtoMajor:    1,
 
849
                ProtoMinor:    0,
 
850
                Body:          ioutil.NopCloser(strings.NewReader("bad")),
 
851
                ContentLength: -1,
 
852
        },
 
853
        expectError: `cannot unmarshal response "bad": .*`,
 
854
}, {
 
855
        about: "badly formatted json error",
 
856
        response: &http.Response{
 
857
                Status:        "404 Not found",
 
858
                StatusCode:    404,
 
859
                Proto:         "HTTP/1.0",
 
860
                ProtoMajor:    1,
 
861
                ProtoMinor:    0,
 
862
                Body:          ioutil.NopCloser(strings.NewReader("bad")),
 
863
                ContentLength: -1,
 
864
        },
 
865
        expectError: `cannot unmarshal error response "bad": .*`,
 
866
}, {
 
867
        about: "error response with empty message",
 
868
        response: &http.Response{
 
869
                Status:     "404 Not found",
 
870
                StatusCode: 404,
 
871
                Proto:      "HTTP/1.0",
 
872
                ProtoMajor: 1,
 
873
                ProtoMinor: 0,
 
874
                Body: ioutil.NopCloser(bytes.NewReader(mustMarshalJSON(&params.Error{
 
875
                        Code: "foo",
 
876
                }))),
 
877
                ContentLength: -1,
 
878
        },
 
879
        expectError: "error response with empty message .*",
 
880
}}
 
881
 
 
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{
 
889
                                        resp:  test.response,
 
890
                                        error: test.error,
 
891
                                },
 
892
                        },
 
893
                })
 
894
                var result interface{}
 
895
                err := cl.Get("/foo", &result)
 
896
                c.Assert(err, gc.ErrorMatches, test.expectError)
 
897
        }
 
898
}
 
899
 
 
900
var hyphenateTests = []struct {
 
901
        val    string
 
902
        expect string
 
903
}{{
 
904
        val:    "Hello",
 
905
        expect: "hello",
 
906
}, {
 
907
        val:    "HelloThere",
 
908
        expect: "hello-there",
 
909
}, {
 
910
        val:    "HelloHTTP",
 
911
        expect: "hello-http",
 
912
}, {
 
913
        val:    "helloHTTP",
 
914
        expect: "hello-http",
 
915
}, {
 
916
        val:    "hellothere",
 
917
        expect: "hellothere",
 
918
}, {
 
919
        val:    "Long4Camel32WithDigits45",
 
920
        expect: "long4-camel32-with-digits45",
 
921
}, {
 
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",
 
926
}}
 
927
 
 
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)
 
932
        }
 
933
}
 
934
 
 
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(
 
940
                url,
 
941
                charmRepo.CharmArchive(c.MkDir(), "wordpress"),
 
942
                42,
 
943
        )
 
944
        c.Assert(err, gc.IsNil)
 
945
        err = s.client.PutExtraInfo(url, map[string]interface{}{
 
946
                "foo": "bar",
 
947
        })
 
948
        c.Assert(err, gc.IsNil)
 
949
 
 
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"`)
 
957
}
 
958
 
 
959
var metaBadTypeTests = []struct {
 
960
        result      interface{}
 
961
        expectError string
 
962
}{{
 
963
        result:      "",
 
964
        expectError: "expected pointer, not string",
 
965
}, {
 
966
        result:      new(string),
 
967
        expectError: `expected pointer to struct, not \*string`,
 
968
}, {
 
969
        result:      new(struct{ Embed }),
 
970
        expectError: "anonymous fields not supported",
 
971
}, {
 
972
        expectError: "expected valid result pointer, not nil",
 
973
}}
 
974
 
 
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)
 
980
        }
 
981
}
 
982
 
 
983
type Embed struct{}
 
984
type embed struct{}
 
985
 
 
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)
 
992
 
 
993
        // Put some extra-info.
 
994
        err = s.client.PutExtraInfo(url, map[string]interface{}{
 
995
                "attr": "value",
 
996
        })
 
997
        c.Assert(err, gc.IsNil)
 
998
 
 
999
        tests := []struct {
 
1000
                about           string
 
1001
                id              string
 
1002
                expectResult    interface{}
 
1003
                expectError     string
 
1004
                expectErrorCode params.ErrorCode
 
1005
        }{{
 
1006
                about:        "no fields",
 
1007
                id:           "utopic/wordpress",
 
1008
                expectResult: &struct{}{},
 
1009
        }, {
 
1010
                about: "single field",
 
1011
                id:    "utopic/wordpress",
 
1012
                expectResult: &struct {
 
1013
                        CharmMetadata *charm.Meta
 
1014
                }{
 
1015
                        CharmMetadata: ch.Meta(),
 
1016
                },
 
1017
        }, {
 
1018
                about: "three fields",
 
1019
                id:    "wordpress",
 
1020
                expectResult: &struct {
 
1021
                        CharmMetadata *charm.Meta
 
1022
                        CharmConfig   *charm.Config
 
1023
                        ExtraInfo     map[string]string
 
1024
                }{
 
1025
                        CharmMetadata: ch.Meta(),
 
1026
                        CharmConfig:   ch.Config(),
 
1027
                        ExtraInfo:     map[string]string{"attr": "value"},
 
1028
                },
 
1029
        }, {
 
1030
                about: "tagged field",
 
1031
                id:    "wordpress",
 
1032
                expectResult: &struct {
 
1033
                        Foo  *charm.Meta `csclient:"charm-metadata"`
 
1034
                        Attr string      `csclient:"extra-info/attr"`
 
1035
                }{
 
1036
                        Foo:  ch.Meta(),
 
1037
                        Attr: "value",
 
1038
                },
 
1039
        }, {
 
1040
                about:           "id not found",
 
1041
                id:              "bogus",
 
1042
                expectResult:    &struct{}{},
 
1043
                expectError:     `cannot get "/bogus/meta/any": no matching charm or bundle for "cs:bogus"`,
 
1044
                expectErrorCode: params.ErrNotFound,
 
1045
        }, {
 
1046
                about: "unmarshal into invalid type",
 
1047
                id:    "wordpress",
 
1048
                expectResult: new(struct {
 
1049
                        CharmMetadata []string
 
1050
                }),
 
1051
                expectError: `cannot unmarshal charm-metadata: json: cannot unmarshal object into Go value of type \[]string`,
 
1052
        }, {
 
1053
                about: "unmarshal into struct with unexported fields",
 
1054
                id:    "wordpress",
 
1055
                expectResult: &struct {
 
1056
                        unexported    int
 
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.
 
1061
                        // embed
 
1062
                }{
 
1063
                        CharmMetadata: ch.Meta(),
 
1064
                },
 
1065
        }, {
 
1066
                about: "metadata not appropriate for charm",
 
1067
                id:    "wordpress",
 
1068
                expectResult: &struct {
 
1069
                        CharmMetadata  *charm.Meta
 
1070
                        BundleMetadata *charm.BundleData
 
1071
                }{
 
1072
                        CharmMetadata: ch.Meta(),
 
1073
                },
 
1074
        }}
 
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,
 
1078
                // but empty.
 
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)
 
1085
                        } else {
 
1086
                                c.Assert(test.expectErrorCode, gc.Equals, params.ErrorCode(""))
 
1087
                        }
 
1088
                        c.Assert(id, gc.IsNil)
 
1089
                        continue
 
1090
                }
 
1091
                c.Assert(err, gc.IsNil)
 
1092
                c.Assert(id, jc.DeepEquals, purl)
 
1093
                c.Assert(result, jc.DeepEquals, test.expectResult)
 
1094
        }
 
1095
}
 
1096
 
 
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)
 
1102
 
 
1103
        // Put some info in.
 
1104
        info := map[string]interface{}{
 
1105
                "attr1": "value1",
 
1106
                "attr2": []interface{}{"one", "two"},
 
1107
        }
 
1108
        err = s.client.PutExtraInfo(url, info)
 
1109
        c.Assert(err, gc.IsNil)
 
1110
 
 
1111
        // Verify that we get it back OK.
 
1112
        var val struct {
 
1113
                ExtraInfo map[string]interface{}
 
1114
        }
 
1115
        _, err = s.client.Meta(url, &val)
 
1116
        c.Assert(err, gc.IsNil)
 
1117
        c.Assert(val.ExtraInfo, jc.DeepEquals, info)
 
1118
 
 
1119
        // Put some more in.
 
1120
        err = s.client.PutExtraInfo(url, map[string]interface{}{
 
1121
                "attr3": "three",
 
1122
        })
 
1123
        c.Assert(err, gc.IsNil)
 
1124
 
 
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)
 
1130
}
 
1131
 
 
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)
 
1136
}
 
1137
 
 
1138
type errorReader struct {
 
1139
        error string
 
1140
}
 
1141
 
 
1142
func (e *errorReader) Read(buf []byte) (int, error) {
 
1143
        return 0, errgo.New(e.error)
 
1144
}
 
1145
 
 
1146
type cannedRoundTripper struct {
 
1147
        resp  *http.Response
 
1148
        error error
 
1149
}
 
1150
 
 
1151
func (r *cannedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 
1152
        return r.resp, r.error
 
1153
}
 
1154
 
 
1155
func mustMarshalJSON(x interface{}) []byte {
 
1156
        data, err := json.Marshal(x)
 
1157
        if err != nil {
 
1158
                panic(err)
 
1159
        }
 
1160
        return data
 
1161
}
 
1162
 
 
1163
func (s *suite) TestLog(c *gc.C) {
 
1164
        logs := []struct {
 
1165
                typ     params.LogType
 
1166
                level   params.LogLevel
 
1167
                message string
 
1168
                urls    []*charm.Reference
 
1169
        }{{
 
1170
                typ:     params.IngestionType,
 
1171
                level:   params.InfoLevel,
 
1172
                message: "ingestion info",
 
1173
                urls:    nil,
 
1174
        }, {
 
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"),
 
1181
                },
 
1182
        }}
 
1183
 
 
1184
        for _, log := range logs {
 
1185
                err := s.client.Log(log.typ, log.level, log.message, log.urls...)
 
1186
                c.Assert(err, gc.IsNil)
 
1187
        }
 
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)
 
1195
                var msg string
 
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)
 
1200
        }
 
1201
}
 
1202
 
 
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)
 
1209
 
 
1210
        err = s.client.Put("/"+curl.Path()+"/meta/perm/read", []string{"bob"})
 
1211
        c.Assert(err, gc.IsNil)
 
1212
 
 
1213
        // Create a client without basic auth credentials
 
1214
        client := csclient.New(csclient.Params{
 
1215
                URL: s.srv.URL,
 
1216
        })
 
1217
 
 
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)
 
1223
 
 
1224
        s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
 
1225
                return []checkers.Caveat{checkers.DeclaredCaveat("username", "bob")}, nil
 
1226
        }
 
1227
        _, err = client.Meta(curl, &result)
 
1228
        c.Assert(err, gc.IsNil)
 
1229
        c.Assert(result.IdRevision.Revision, gc.Equals, curl.Revision)
 
1230
 
 
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{
 
1237
                                VisitURL: visitURL,
 
1238
                                WaitURL:  "http://0.1.2.3/waitURL",
 
1239
                        }}
 
1240
        }
 
1241
 
 
1242
        client = csclient.New(csclient.Params{
 
1243
                URL: s.srv.URL,
 
1244
                VisitWebPage: func(vurl *url.URL) error {
 
1245
                        c.Check(vurl.String(), gc.Equals, visitURL)
 
1246
                        return fmt.Errorf("stopping interaction")
 
1247
                }})
 
1248
 
 
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)
 
1253
}
 
1254
 
 
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)
 
1261
 
 
1262
        err = s.client.Put("/"+url.Path()+"/meta/perm/read", []string{"bob"})
 
1263
        c.Assert(err, gc.IsNil)
 
1264
        client := csclient.New(csclient.Params{
 
1265
                URL: s.srv.URL,
 
1266
        })
 
1267
 
 
1268
        var result struct{ IdRevision struct{ Revision int } }
 
1269
        _, err = client.Meta(purl, &result)
 
1270
        c.Assert(err, gc.NotNil)
 
1271
 
 
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`)
 
1275
 
 
1276
        // Allow the discharge.
 
1277
        s.discharge = func(cond, arg string) ([]checkers.Caveat, error) {
 
1278
                return []checkers.Caveat{checkers.DeclaredCaveat("username", "bob")}, nil
 
1279
        }
 
1280
        err = client.Login()
 
1281
        c.Assert(err, gc.IsNil)
 
1282
 
 
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")
 
1287
        }
 
1288
 
 
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)
 
1293
}