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

1.1.36 by Curtis C. Hovey
Import upstream version 1.24.6
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
}