1
// Copyright 2014 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package legacy_test // import "gopkg.in/juju/charmstore.v5-unstable/internal/legacy"
17
jujutesting "github.com/juju/testing"
18
jc "github.com/juju/testing/checkers"
19
"github.com/juju/testing/httptesting"
20
gc "gopkg.in/check.v1"
21
"gopkg.in/juju/charm.v6-unstable"
22
"gopkg.in/juju/charmrepo.v2-unstable"
23
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
25
"gopkg.in/mgo.v2/bson"
27
"gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
28
"gopkg.in/juju/charmstore.v5-unstable/internal/legacy"
29
"gopkg.in/juju/charmstore.v5-unstable/internal/router"
30
"gopkg.in/juju/charmstore.v5-unstable/internal/storetesting"
31
"gopkg.in/juju/charmstore.v5-unstable/internal/storetesting/stats"
34
var serverParams = charmstore.ServerParams{
35
AuthUsername: "test-user",
36
AuthPassword: "test-password",
39
type APISuite struct {
40
jujutesting.IsolatedMgoSuite
41
srv *charmstore.Server
42
store *charmstore.Store
45
var _ = gc.Suite(&APISuite{})
47
func (s *APISuite) SetUpTest(c *gc.C) {
48
s.IsolatedMgoSuite.SetUpTest(c)
49
s.srv, s.store = newServer(c, s.Session, serverParams)
52
func (s *APISuite) TearDownTest(c *gc.C) {
54
s.store.Pool().Close()
56
s.IsolatedMgoSuite.TearDownTest(c)
59
func newServer(c *gc.C, session *mgo.Session, config charmstore.ServerParams) (*charmstore.Server, *charmstore.Store) {
60
db := session.DB("charmstore")
61
pool, err := charmstore.NewPool(db, nil, nil, config)
62
c.Assert(err, gc.IsNil)
63
srv, err := charmstore.NewServer(db, nil, config, map[string]charmstore.NewAPIHandlerFunc{"": legacy.NewAPIHandler})
64
c.Assert(err, gc.IsNil)
65
return srv, pool.Store()
68
func (s *APISuite) TestCharmArchive(c *gc.C) {
69
_, wordpress := s.addPublicCharm(c, "wordpress", "cs:precise/wordpress-0")
70
archiveBytes, err := ioutil.ReadFile(wordpress.Path)
71
c.Assert(err, gc.IsNil)
73
rec := httptesting.DoRequest(c, httptesting.DoRequestParams{
75
URL: "/charm/precise/wordpress-0",
77
c.Assert(rec.Code, gc.Equals, http.StatusOK)
78
c.Assert(rec.Body.Bytes(), gc.DeepEquals, archiveBytes)
79
c.Assert(rec.Header().Get("Content-Length"), gc.Equals, fmt.Sprint(len(rec.Body.Bytes())))
81
// Test with unresolved URL.
82
rec = httptesting.DoRequest(c, httptesting.DoRequestParams{
84
URL: "/charm/wordpress",
86
c.Assert(rec.Code, gc.Equals, http.StatusOK)
87
c.Assert(rec.Body.Bytes(), gc.DeepEquals, archiveBytes)
88
c.Assert(rec.Header().Get("Content-Length"), gc.Equals, fmt.Sprint(len(rec.Body.Bytes())))
90
// Check that the HTTP range logic is plugged in OK. If this
91
// is working, we assume that the whole thing is working OK,
92
// as net/http is well-tested.
93
rec = httptesting.DoRequest(c, httptesting.DoRequestParams{
95
URL: "/charm/precise/wordpress-0",
96
Header: http.Header{"Range": {"bytes=10-100"}},
98
c.Assert(rec.Code, gc.Equals, http.StatusPartialContent, gc.Commentf("body: %q", rec.Body.Bytes()))
99
c.Assert(rec.Body.Bytes(), gc.HasLen, 100-10+1)
100
c.Assert(rec.Body.Bytes(), gc.DeepEquals, archiveBytes[10:101])
103
func (s *APISuite) TestGetElidesSeriesFromMultiSeriesCharmMetadata(c *gc.C) {
104
_, ch := s.addPublicCharm(c, "multi-series", "cs:~charmers/multi-series-0")
105
rec := httptesting.DoRequest(c, httptesting.DoRequestParams{
107
URL: "/charm/~charmers/multi-series",
109
c.Assert(rec.Code, gc.Equals, http.StatusOK)
111
gotCh, err := charm.ReadCharmArchiveBytes(rec.Body.Bytes())
112
c.Assert(err, gc.IsNil)
117
c.Assert(gotCh.Meta(), jc.DeepEquals, chMeta)
120
func (s *APISuite) TestPostNotAllowed(c *gc.C) {
121
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
124
URL: "/charm/precise/wordpress",
125
ExpectStatus: http.StatusMethodNotAllowed,
126
ExpectBody: params.Error{
127
Code: params.ErrMethodNotAllowed,
128
Message: params.ErrMethodNotAllowed.Error(),
133
func (s *APISuite) TestCharmArchiveUnresolvedURL(c *gc.C) {
134
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
136
URL: "/charm/wordpress",
137
ExpectStatus: http.StatusNotFound,
138
ExpectBody: params.Error{
139
Code: params.ErrNotFound,
140
Message: `no matching charm or bundle for cs:wordpress`,
145
func (s *APISuite) TestCharmInfoNotFound(c *gc.C) {
146
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
148
URL: "/charm-info?charms=cs:precise/something-23",
149
ExpectStatus: http.StatusOK,
150
ExpectBody: map[string]charmrepo.InfoResponse{
151
"cs:precise/something-23": {
152
Errors: []string{"entry not found"},
158
func (s *APISuite) TestServeCharmInfo(c *gc.C) {
159
wordpressURL, wordpress := s.addPublicCharm(c, "wordpress", "cs:precise/wordpress-1")
160
hashSum := fileSHA256(c, wordpress.Path)
161
digest, err := json.Marshal("who@canonical.com-bzr-digest")
162
c.Assert(err, gc.IsNil)
167
extrainfo map[string][]byte
174
about: "full charm URL with digest extra info",
175
url: wordpressURL.String(),
176
extrainfo: map[string][]byte{
177
params.BzrDigestKey: digest,
179
canonical: "cs:precise/wordpress-1",
181
digest: "who@canonical.com-bzr-digest",
184
about: "full charm URL without digest extra info",
185
url: wordpressURL.String(),
186
canonical: "cs:precise/wordpress-1",
190
about: "partial charm URL with digest extra info",
192
extrainfo: map[string][]byte{
193
params.BzrDigestKey: digest,
195
canonical: "cs:precise/wordpress-1",
197
digest: "who@canonical.com-bzr-digest",
200
about: "partial charm URL without extra info",
202
canonical: "cs:precise/wordpress-1",
206
about: "invalid digest extra info",
208
extrainfo: map[string][]byte{
209
params.BzrDigestKey: []byte("[]"),
211
canonical: "cs:precise/wordpress-1",
214
err: `cannot unmarshal digest: json: cannot unmarshal array into Go value of type string`,
216
about: "charm not found",
217
url: "cs:precise/non-existent",
218
err: "entry not found",
220
about: "invalid charm URL",
222
err: `entry not found`,
224
about: "invalid charm schema",
225
url: "gopher:archie-server",
226
err: `entry not found`,
228
about: "invalid URL",
229
url: "/charm-info?charms=cs:not-found",
230
err: "entry not found",
233
for i, test := range tests {
234
c.Logf("test %d: %s", i, test.about)
235
err = s.store.UpdateEntity(wordpressURL, bson.D{{
236
"$set", bson.D{{"extrainfo", test.extrainfo}},
238
c.Assert(err, gc.IsNil)
239
expectInfo := charmrepo.InfoResponse{
240
CanonicalURL: test.canonical,
242
Revision: test.revision,
246
expectInfo.Errors = []string{test.err}
248
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
250
URL: "/charm-info?charms=" + test.url,
251
ExpectStatus: http.StatusOK,
252
ExpectBody: map[string]charmrepo.InfoResponse{
253
test.url: expectInfo,
259
func (s *APISuite) TestCharmInfoCounters(c *gc.C) {
260
if !storetesting.MongoJSEnabled() {
261
c.Skip("MongoDB JavaScript not available")
264
// Add two charms to the database, a promulgated one and a user owned one.
265
s.addPublicCharm(c, "wordpress", "cs:utopic/wordpress-42")
266
s.addPublicCharm(c, "wordpress", "cs:~who/trusty/wordpress-47")
268
requestInfo := func(id string, times int) {
269
for i := 0; i < times; i++ {
270
rec := httptesting.DoRequest(c, httptesting.DoRequestParams{
272
URL: "/charm-info?charms=" + id,
274
c.Assert(rec.Code, gc.Equals, http.StatusOK)
278
// Request charm info several times for the promulgated charm,
279
// the user owned one and a missing charm.
280
requestInfo("utopic/wordpress-42", 4)
281
requestInfo("~who/trusty/wordpress-47", 3)
282
requestInfo("precise/django-0", 2)
284
// The charm-info count for the promulgated charm has been updated.
285
key := []string{params.StatsCharmInfo, "utopic", "wordpress"}
286
stats.CheckCounterSum(c, s.store, key, false, 4)
288
// The charm-info count for the user owned charm has been updated.
289
key = []string{params.StatsCharmInfo, "trusty", "wordpress", "who"}
290
stats.CheckCounterSum(c, s.store, key, false, 3)
292
// The charm-missing count for the missing charm has been updated.
293
key = []string{params.StatsCharmMissing, "precise", "django"}
294
stats.CheckCounterSum(c, s.store, key, false, 2)
296
// The charm-info count for the missing charm is still zero.
297
key = []string{params.StatsCharmInfo, "precise", "django"}
298
stats.CheckCounterSum(c, s.store, key, false, 0)
301
func (s *APISuite) TestAPIInfoWithGatedCharm(c *gc.C) {
302
wordpressURL, _ := s.addPublicCharm(c, "wordpress", "cs:precise/wordpress-0")
303
s.store.SetPerms(&wordpressURL.URL, "stable.read", "bob")
304
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
306
URL: "/charm-info?charms=" + wordpressURL.URL.String(),
307
ExpectStatus: http.StatusOK,
308
ExpectBody: map[string]charmrepo.InfoResponse{
309
wordpressURL.URL.String(): {
310
Errors: []string{"entry not found"},
316
func fileSHA256(c *gc.C, path string) string {
317
f, err := os.Open(path)
318
c.Assert(err, gc.IsNil)
320
_, err = io.Copy(hash, f)
321
c.Assert(err, gc.IsNil)
322
return fmt.Sprintf("%x", hash.Sum(nil))
325
func (s *APISuite) TestCharmPackageGet(c *gc.C) {
326
wordpressURL, wordpress := s.addPublicCharm(c, "wordpress", "cs:precise/wordpress-0")
327
archiveBytes, err := ioutil.ReadFile(wordpress.Path)
328
c.Assert(err, gc.IsNil)
330
srv := httptest.NewServer(s.srv)
333
s.PatchValue(&charmrepo.CacheDir, c.MkDir())
334
s.PatchValue(&charmrepo.LegacyStore.BaseURL, srv.URL)
336
ch, err := charmrepo.LegacyStore.Get(&wordpressURL.URL)
337
c.Assert(err, gc.IsNil)
338
chArchive := ch.(*charm.CharmArchive)
340
data, err := ioutil.ReadFile(chArchive.Path)
341
c.Assert(err, gc.IsNil)
342
c.Assert(data, gc.DeepEquals, archiveBytes)
345
func (s *APISuite) TestCharmPackageCharmInfo(c *gc.C) {
346
wordpressURL, wordpress := s.addPublicCharm(c, "wordpress", "cs:precise/wordpress-0")
347
wordpressSHA256 := fileSHA256(c, wordpress.Path)
348
mysqlURL, mySQL := s.addPublicCharm(c, "wordpress", "cs:precise/mysql-2")
349
mysqlSHA256 := fileSHA256(c, mySQL.Path)
350
notFoundURL := charm.MustParseURL("cs:precise/not-found-3")
352
srv := httptest.NewServer(s.srv)
354
s.PatchValue(&charmrepo.LegacyStore.BaseURL, srv.URL)
356
resp, err := charmrepo.LegacyStore.Info(wordpressURL.PreferredURL(), mysqlURL.PreferredURL(), notFoundURL)
357
c.Assert(err, gc.IsNil)
358
c.Assert(resp, gc.HasLen, 3)
359
c.Assert(resp, jc.DeepEquals, []*charmrepo.InfoResponse{{
360
CanonicalURL: wordpressURL.String(),
361
Sha256: wordpressSHA256,
363
CanonicalURL: mysqlURL.String(),
367
Errors: []string{"charm not found: " + notFoundURL.String()},
371
var serverStatusTests = []struct {
375
{"/charm-info/any", 404},
376
{"/charm/bad-url", 404},
377
{"/charm/bad-series/wordpress", 404},
380
func (s *APISuite) TestServerStatus(c *gc.C) {
381
// TODO(rog) add tests from old TestServerStatus tests
382
// when we implement charm-info.
383
for i, test := range serverStatusTests {
384
c.Logf("test %d: %s", i, test.path)
385
resp := httptesting.DoRequest(c, httptesting.DoRequestParams{
389
c.Assert(resp.Code, gc.Equals, test.code, gc.Commentf("body: %s", resp.Body))
393
func (s *APISuite) addPublicCharm(c *gc.C, charmName, curl string) (*router.ResolvedURL, *charm.CharmArchive) {
394
rurl := &router.ResolvedURL{
395
URL: *charm.MustParseURL(curl),
396
PromulgatedRevision: -1,
398
if rurl.URL.User == "" {
399
rurl.URL.User = "charmers"
400
rurl.PromulgatedRevision = rurl.URL.Revision
402
archive := storetesting.Charms.CharmArchive(c.MkDir(), charmName)
403
err := s.store.AddCharmWithArchive(rurl, archive)
404
c.Assert(err, gc.IsNil)
409
func (s *APISuite) setPublic(c *gc.C, rurl *router.ResolvedURL) {
410
err := s.store.SetPerms(&rurl.URL, "stable.read", params.Everyone)
411
c.Assert(err, gc.IsNil)
412
err = s.store.Publish(rurl, nil, params.StableChannel)
413
c.Assert(err, gc.IsNil)
416
var serveCharmEventErrorsTests = []struct {
422
about: "invalid charm URL",
423
url: "no-such:charm",
424
err: `invalid charm URL: charm or bundle URL has invalid schema: "no-such:charm"`,
426
about: "revision specified",
427
url: "cs:utopic/django-42",
428
err: "got charm URL with revision: cs:utopic/django-42",
430
about: "charm not found",
431
url: "cs:trusty/django",
432
err: "entry not found",
434
about: "ignoring digest",
435
url: "precise/django-47@a-bzr-digest",
436
responseUrl: "precise/django-47",
437
err: "got charm URL with revision: cs:precise/django-47",
440
func (s *APISuite) TestServeCharmEventErrors(c *gc.C) {
441
for i, test := range serveCharmEventErrorsTests {
442
c.Logf("test %d: %s", i, test.about)
443
if test.responseUrl == "" {
444
test.responseUrl = test.url
446
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
448
URL: "/charm-event?charms=" + test.url,
449
ExpectStatus: http.StatusOK,
450
ExpectBody: map[string]charmrepo.EventResponse{
452
Errors: []string{test.err},
459
func (s *APISuite) TestServeCharmEvent(c *gc.C) {
460
// Add three charms to the charm store.
461
mysqlUrl, _ := s.addPublicCharm(c, "mysql", "cs:trusty/mysql-2")
462
riakUrl, _ := s.addPublicCharm(c, "riak", "cs:utopic/riak-3")
464
// Update the mysql charm with a valid digest extra-info.
465
s.addExtraInfoDigest(c, mysqlUrl, "who@canonical.com-bzr-digest")
467
// Update the riak charm with an invalid digest extra-info.
468
err := s.store.UpdateEntity(riakUrl, bson.D{{
469
"$set", bson.D{{"extrainfo", map[string][]byte{
470
params.BzrDigestKey: []byte(":"),
473
c.Assert(err, gc.IsNil)
475
// Retrieve the entities.
476
mysql, err := s.store.FindEntity(mysqlUrl, nil)
477
c.Assert(err, gc.IsNil)
478
riak, err := s.store.FindEntity(riakUrl, nil)
479
c.Assert(err, gc.IsNil)
484
expect map[string]*charmrepo.EventResponse
486
about: "valid digest",
487
query: "?charms=cs:trusty/mysql",
488
expect: map[string]*charmrepo.EventResponse{
491
Revision: mysql.Revision,
492
Time: mysql.UploadTime.UTC().Format(time.RFC3339),
493
Digest: "who@canonical.com-bzr-digest",
497
about: "invalid digest",
498
query: "?charms=cs:utopic/riak",
499
expect: map[string]*charmrepo.EventResponse{
502
Revision: riak.Revision,
503
Time: riak.UploadTime.UTC().Format(time.RFC3339),
504
Errors: []string{"cannot unmarshal digest: invalid character ':' looking for beginning of value"},
508
about: "partial charm URL",
509
query: "?charms=cs:mysql",
510
expect: map[string]*charmrepo.EventResponse{
513
Revision: mysql.Revision,
514
Time: mysql.UploadTime.UTC().Format(time.RFC3339),
515
Digest: "who@canonical.com-bzr-digest",
519
about: "digest in request",
520
query: "?charms=cs:trusty/mysql@my-digest",
521
expect: map[string]*charmrepo.EventResponse{
524
Revision: mysql.Revision,
525
Time: mysql.UploadTime.UTC().Format(time.RFC3339),
526
Digest: "who@canonical.com-bzr-digest",
530
about: "multiple charms",
531
query: "?charms=cs:mysql&charms=utopic/riak",
532
expect: map[string]*charmrepo.EventResponse{
535
Revision: mysql.Revision,
536
Time: mysql.UploadTime.UTC().Format(time.RFC3339),
537
Digest: "who@canonical.com-bzr-digest",
541
Revision: riak.Revision,
542
Time: riak.UploadTime.UTC().Format(time.RFC3339),
543
Errors: []string{"cannot unmarshal digest: invalid character ':' looking for beginning of value"},
548
for i, test := range tests {
549
c.Logf("test %d: %s", i, test.about)
550
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
552
URL: "/charm-event" + test.query,
553
ExpectStatus: http.StatusOK,
554
ExpectBody: test.expect,
559
func (s *APISuite) TestServeCharmEventDigestNotFound(c *gc.C) {
560
// Add a charm without a Bazaar digest.
561
url, _ := s.addPublicCharm(c, "wordpress", "cs:trusty/wordpress-42")
563
// Pretend the entity has been uploaded right now, and assume the test does
564
// not take more than two minutes to run.
565
s.updateUploadTime(c, url, time.Now())
566
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
568
URL: "/charm-event?charms=cs:trusty/wordpress",
569
ExpectStatus: http.StatusOK,
570
ExpectBody: map[string]charmrepo.EventResponse{
571
"cs:trusty/wordpress": {
572
Errors: []string{"entry not found"},
577
// Now change the entity upload time to be more than 2 minutes ago.
578
s.updateUploadTime(c, url, time.Now().Add(-121*time.Second))
579
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
581
URL: "/charm-event?charms=cs:trusty/wordpress",
582
ExpectStatus: http.StatusOK,
583
ExpectBody: map[string]charmrepo.EventResponse{
584
"cs:trusty/wordpress": {
585
Errors: []string{"digest not found: this can be due to an error while ingesting the entity"},
591
func (s *APISuite) TestServeCharmEventLastRevision(c *gc.C) {
592
// Add two revisions of the same charm.
593
url1, _ := s.addPublicCharm(c, "wordpress", "cs:trusty/wordpress-1")
594
url2, _ := s.addPublicCharm(c, "wordpress", "cs:trusty/wordpress-2")
596
// Update the resulting entities with Bazaar digests.
597
s.addExtraInfoDigest(c, url1, "digest-1")
598
s.addExtraInfoDigest(c, url2, "digest-2")
600
// Retrieve the most recent revision of the entity.
601
entity, err := s.store.FindEntity(url2, nil)
602
c.Assert(err, gc.IsNil)
604
// Ensure the last revision is correctly returned.
605
httptesting.AssertJSONCall(c, httptesting.JSONCallParams{
607
URL: "/charm-event?charms=wordpress",
608
ExpectStatus: http.StatusOK,
609
ExpectBody: map[string]*charmrepo.EventResponse{
613
Time: entity.UploadTime.UTC().Format(time.RFC3339),
620
func (s *APISuite) addExtraInfoDigest(c *gc.C, id *router.ResolvedURL, digest string) {
621
b, err := json.Marshal(digest)
622
c.Assert(err, gc.IsNil)
623
err = s.store.UpdateEntity(id, bson.D{{
624
"$set", bson.D{{"extrainfo", map[string][]byte{
625
params.BzrDigestKey: b,
628
c.Assert(err, gc.IsNil)
631
func (s *APISuite) updateUploadTime(c *gc.C, id *router.ResolvedURL, uploadTime time.Time) {
632
err := s.store.UpdateEntity(id, bson.D{{
633
"$set", bson.D{{"uploadtime", uploadTime}},
635
c.Assert(err, gc.IsNil)