github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/charm/repository/charmhub_test.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package repository
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/url"
    10  	"time"
    11  
    12  	"github.com/juju/charm/v12"
    13  	charmresource "github.com/juju/charm/v12/resource"
    14  	"github.com/juju/collections/set"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/testing"
    17  	jc "github.com/juju/testing/checkers"
    18  	"github.com/juju/utils/v3/hash"
    19  	"go.uber.org/mock/gomock"
    20  	gc "gopkg.in/check.v1"
    21  
    22  	"github.com/juju/juju/charmhub"
    23  	"github.com/juju/juju/charmhub/transport"
    24  	"github.com/juju/juju/core/arch"
    25  	corecharm "github.com/juju/juju/core/charm"
    26  	"github.com/juju/juju/core/charm/repository/mocks"
    27  )
    28  
    29  var (
    30  	expRefreshFields = set.NewStrings(
    31  		"download", "id", "license", "name", "publisher", "resources",
    32  		"revision", "summary", "type", "version", "bases", "config-yaml",
    33  		"metadata-yaml",
    34  	).SortedValues()
    35  )
    36  
    37  type charmHubRepositorySuite struct {
    38  	testing.IsolationSuite
    39  
    40  	client *mocks.MockCharmHubClient
    41  	logger *mocks.MockLogger
    42  }
    43  
    44  var _ = gc.Suite(&charmHubRepositorySuite{})
    45  
    46  func (s *charmHubRepositorySuite) TestResolveForDeploy(c *gc.C) {
    47  	defer s.setupMocks(c).Finish()
    48  	s.expectCharmRefreshInstallOneFromChannel(c)
    49  	// The origin.ID should never be saved to the origin during
    50  	// ResolveWithPreferredChannel.  That is done during the file
    51  	// download only.
    52  	s.testResolve(c, "")
    53  }
    54  
    55  func (s *charmHubRepositorySuite) TestResolveForUpgrade(c *gc.C) {
    56  	defer s.setupMocks(c).Finish()
    57  	cfg, err := charmhub.RefreshOne("instance-key", "charmCHARMcharmCHARMcharmCHARM01", 16, "latest/stable", charmhub.RefreshBase{
    58  		Architecture: arch.DefaultArchitecture,
    59  	})
    60  	c.Assert(err, jc.ErrorIsNil)
    61  	s.expectCharmRefresh(c, cfg)
    62  	// If the origin has an ID, ensure it's kept thru the call
    63  	// to ResolveWithPreferredChannel.
    64  	s.testResolve(c, "charmCHARMcharmCHARMcharmCHARM01")
    65  }
    66  
    67  func (s *charmHubRepositorySuite) testResolve(c *gc.C, id string) {
    68  	curl := charm.MustParseURL("ch:wordpress")
    69  	rev := 16
    70  	channel := corecharm.MustParseChannel("latest/stable")
    71  	origin := corecharm.Origin{
    72  		Source:   "charm-hub",
    73  		ID:       id,
    74  		Revision: &rev,
    75  		Platform: corecharm.Platform{
    76  			Architecture: arch.DefaultArchitecture,
    77  			OS:           "ubuntu",
    78  			Channel:      "20.04",
    79  		},
    80  		Channel: &channel,
    81  	}
    82  	if id != "" {
    83  		origin.InstanceKey = "instance-key"
    84  	}
    85  
    86  	obtainedCurl, obtainedOrigin, obtainedBases, err := s.newClient().ResolveWithPreferredChannel("wordpress", origin)
    87  	c.Assert(err, jc.ErrorIsNil)
    88  
    89  	curl.Revision = rev
    90  
    91  	origin.Type = "charm"
    92  	origin.Revision = &curl.Revision
    93  	origin.Platform.Architecture = arch.DefaultArchitecture
    94  	origin.Platform.OS = "ubuntu"
    95  	origin.Platform.Channel = "20.04"
    96  
    97  	expected := s.expectedCURL(curl, 16, arch.DefaultArchitecture, "focal")
    98  
    99  	c.Assert(obtainedCurl, jc.DeepEquals, expected)
   100  	c.Assert(obtainedOrigin, jc.DeepEquals, origin)
   101  	c.Assert(obtainedBases, jc.SameContents, []corecharm.Platform{{OS: "ubuntu", Channel: "20.04", Architecture: "amd64"}})
   102  }
   103  
   104  func (s *charmHubRepositorySuite) TestResolveFillsInEmptyTrack(c *gc.C) {
   105  	defer s.setupMocks(c).Finish()
   106  	s.expectCharmRefreshInstallOneFromChannel(c)
   107  
   108  	channel := corecharm.MustParseChannel("stable")
   109  	origin := corecharm.Origin{
   110  		Source: "charm-hub",
   111  		Platform: corecharm.Platform{
   112  			Architecture: arch.DefaultArchitecture,
   113  			OS:           "ubuntu",
   114  			Channel:      "20.04",
   115  		},
   116  		Channel: &channel,
   117  	}
   118  	_, obtainedOrigin, _, err := s.newClient().ResolveWithPreferredChannel("wordpress", origin)
   119  	c.Assert(err, jc.ErrorIsNil)
   120  	c.Assert(obtainedOrigin.Channel.Track, gc.Equals, "latest")
   121  }
   122  
   123  func (s *charmHubRepositorySuite) TestResolveWithChannel(c *gc.C) {
   124  	defer s.setupMocks(c).Finish()
   125  	s.expectCharmRefreshInstallOneFromChannel(c)
   126  
   127  	curl := charm.MustParseURL("ch:wordpress")
   128  	origin := corecharm.Origin{
   129  		Source: "charm-hub",
   130  		Platform: corecharm.Platform{
   131  			Architecture: arch.DefaultArchitecture,
   132  			OS:           "ubuntu",
   133  			Channel:      "20.04",
   134  		},
   135  		Channel: &charm.Channel{
   136  			Track: "latest",
   137  			Risk:  "stable",
   138  		},
   139  	}
   140  
   141  	obtainedCurl, obtainedOrigin, obtainedBases, err := s.newClient().ResolveWithPreferredChannel("wordpress", origin)
   142  	c.Assert(err, jc.ErrorIsNil)
   143  
   144  	curl.Revision = 16
   145  
   146  	origin.Type = "charm"
   147  	origin.Revision = &curl.Revision
   148  	origin.Channel = &charm.Channel{
   149  		Track: "latest",
   150  		Risk:  "stable",
   151  	}
   152  	origin.Platform.Architecture = arch.DefaultArchitecture
   153  	origin.Platform.OS = "ubuntu"
   154  	origin.Platform.Channel = "20.04"
   155  
   156  	expected := s.expectedCURL(curl, 16, arch.DefaultArchitecture, "focal")
   157  
   158  	c.Assert(obtainedCurl, jc.DeepEquals, expected)
   159  	c.Assert(obtainedOrigin, jc.DeepEquals, origin)
   160  	c.Assert(obtainedBases, jc.SameContents, []corecharm.Platform{{OS: "ubuntu", Channel: "20.04", Architecture: "amd64"}})
   161  }
   162  
   163  func (s *charmHubRepositorySuite) TestResolveWithoutBase(c *gc.C) {
   164  	defer s.setupMocks(c).Finish()
   165  	s.expectCharmRefreshInstallOneFromChannel(c)
   166  
   167  	curl := charm.MustParseURL("ch:wordpress")
   168  	origin := corecharm.Origin{
   169  		Source: "charm-hub",
   170  		Platform: corecharm.Platform{
   171  			Architecture: arch.DefaultArchitecture,
   172  		},
   173  	}
   174  
   175  	obtainedCurl, obtainedOrigin, obtainedBases, err := s.newClient().ResolveWithPreferredChannel("wordpress", origin)
   176  	c.Assert(err, jc.ErrorIsNil)
   177  
   178  	curl.Revision = 16
   179  
   180  	origin.Type = "charm"
   181  	origin.Revision = &curl.Revision
   182  	origin.Channel = &charm.Channel{
   183  		Track: "latest",
   184  		Risk:  "stable",
   185  	}
   186  	origin.Platform.Architecture = arch.DefaultArchitecture
   187  
   188  	expected := s.expectedCURL(curl, 16, arch.DefaultArchitecture, "")
   189  
   190  	c.Assert(obtainedCurl, jc.DeepEquals, expected)
   191  	c.Assert(obtainedOrigin, jc.DeepEquals, origin)
   192  	c.Assert(obtainedBases, jc.SameContents, []corecharm.Platform{})
   193  }
   194  
   195  func (s *charmHubRepositorySuite) TestResolveForDeployWithRevisionSuccess(c *gc.C) {
   196  	defer s.setupMocks(c).Finish()
   197  	s.expectCharmRefreshInstallOneByRevisionResources(c)
   198  
   199  	revision := 16
   200  	curl := charm.MustParseURL("ch:wordpress")
   201  	origin := corecharm.Origin{
   202  		Source: "charm-hub",
   203  		Platform: corecharm.Platform{
   204  			Architecture: arch.DefaultArchitecture,
   205  			OS:           "ubuntu",
   206  			Channel:      "20.04",
   207  		},
   208  		Revision: &revision,
   209  		Channel: &charm.Channel{
   210  			Track: "latest",
   211  			Risk:  "stable",
   212  		},
   213  	}
   214  	arg := corecharm.CharmID{URL: curl, Origin: origin}
   215  
   216  	obtainedData, err := s.newClient().ResolveForDeploy(arg)
   217  	c.Assert(err, jc.ErrorIsNil)
   218  
   219  	curl.Revision = revision
   220  
   221  	expectedOrigin := origin
   222  	expectedOrigin.Type = "charm"
   223  	expectedOrigin.Revision = &revision
   224  
   225  	expected := s.expectedCURL(curl, 16, arch.DefaultArchitecture, "focal")
   226  
   227  	c.Assert(obtainedData.URL, jc.DeepEquals, expected)
   228  	c.Assert(obtainedData.EssentialMetadata.ResolvedOrigin, jc.DeepEquals, expectedOrigin)
   229  }
   230  
   231  func (s *charmHubRepositorySuite) TestResolveForDeploySuccessChooseBase(c *gc.C) {
   232  	defer s.setupMocks(c).Finish()
   233  	s.expectedRefreshInvalidPlatformError(c)
   234  	s.expectCharmRefreshInstallOneFromChannelFullBase(c)
   235  
   236  	curl := charm.MustParseURL("ch:wordpress")
   237  	origin := corecharm.Origin{
   238  		Source: "charm-hub",
   239  		Platform: corecharm.Platform{
   240  			Architecture: arch.DefaultArchitecture,
   241  		},
   242  		Channel: &charm.Channel{
   243  			Track: "latest",
   244  			Risk:  "stable",
   245  		},
   246  	}
   247  	arg := corecharm.CharmID{URL: curl, Origin: origin}
   248  
   249  	obtainedData, err := s.newClient().ResolveForDeploy(arg)
   250  	c.Assert(err, jc.ErrorIsNil)
   251  
   252  	curl.Revision = 16
   253  
   254  	expectedOrigin := origin
   255  	expectedOrigin.Type = "charm"
   256  	expectedOrigin.Revision = &curl.Revision
   257  	expectedOrigin.Platform.OS = "ubuntu"
   258  	expectedOrigin.Platform.Channel = "20.04"
   259  
   260  	expected := s.expectedCURL(curl, 16, arch.DefaultArchitecture, "focal")
   261  
   262  	c.Assert(obtainedData.URL, jc.DeepEquals, expected)
   263  	c.Assert(obtainedData.EssentialMetadata.ResolvedOrigin, jc.DeepEquals, expectedOrigin)
   264  	c.Assert(obtainedData.Resources, gc.HasLen, 1)
   265  	foundResource := obtainedData.Resources["wal-e"]
   266  	c.Assert(foundResource.Name, gc.Equals, "wal-e")
   267  	c.Assert(foundResource.Path, gc.Equals, "wal-e.snap")
   268  	c.Assert(foundResource.Revision, gc.Equals, 5)
   269  }
   270  func (s *charmHubRepositorySuite) TestResolveWithBundles(c *gc.C) {
   271  	defer s.setupMocks(c).Finish()
   272  	s.expectBundleRefresh(c)
   273  
   274  	curl := charm.MustParseURL("ch:core-kubernetes")
   275  	origin := corecharm.Origin{
   276  		Source: "charm-hub",
   277  		Platform: corecharm.Platform{
   278  			Architecture: arch.DefaultArchitecture,
   279  		},
   280  	}
   281  
   282  	obtainedCurl, obtainedOrigin, obtainedBases, err := s.newClient().ResolveWithPreferredChannel("core-kubernetes", origin)
   283  	c.Assert(err, jc.ErrorIsNil)
   284  
   285  	curl.Revision = 17
   286  
   287  	origin.Type = "bundle"
   288  	origin.Revision = &curl.Revision
   289  	origin.Channel = &charm.Channel{
   290  		Track: "latest",
   291  		Risk:  "stable",
   292  	}
   293  	origin.Platform.Architecture = arch.DefaultArchitecture
   294  
   295  	expected := s.expectedCURL(curl, 17, arch.DefaultArchitecture, "")
   296  
   297  	c.Assert(obtainedCurl, jc.DeepEquals, expected)
   298  	c.Assert(obtainedOrigin, jc.DeepEquals, origin)
   299  	c.Assert(obtainedBases, jc.SameContents, []corecharm.Platform{})
   300  }
   301  
   302  func (s *charmHubRepositorySuite) TestResolveInvalidPlatformError(c *gc.C) {
   303  	defer s.setupMocks(c).Finish()
   304  	s.expectedRefreshInvalidPlatformError(c)
   305  	s.expectCharmRefreshInstallOneFromChannel(c)
   306  
   307  	curl := charm.MustParseURL("ch:wordpress")
   308  	origin := corecharm.Origin{
   309  		Source: "charm-hub",
   310  		Platform: corecharm.Platform{
   311  			Architecture: arch.DefaultArchitecture,
   312  		},
   313  	}
   314  
   315  	obtainedCurl, obtainedOrigin, obtainedBases, err := s.newClient().ResolveWithPreferredChannel("wordpress", origin)
   316  	c.Assert(err, jc.ErrorIsNil)
   317  
   318  	curl.Revision = 16
   319  
   320  	origin.Type = "charm"
   321  	origin.Revision = &curl.Revision
   322  	origin.Channel = &charm.Channel{
   323  		Track: "latest",
   324  		Risk:  "stable",
   325  	}
   326  	origin.Platform.Architecture = arch.DefaultArchitecture
   327  	origin.Platform.OS = "ubuntu"
   328  	origin.Platform.Channel = "20.04"
   329  
   330  	expected := s.expectedCURL(curl, 16, arch.DefaultArchitecture, "focal")
   331  
   332  	c.Assert(obtainedCurl, jc.DeepEquals, expected)
   333  	c.Assert(obtainedOrigin, jc.DeepEquals, origin)
   334  	c.Assert(obtainedBases, jc.SameContents, []corecharm.Platform{{OS: "ubuntu", Channel: "20.04", Architecture: "amd64"}})
   335  }
   336  
   337  func (s *charmHubRepositorySuite) TestResolveRevisionNotFoundErrorWithNoSeries(c *gc.C) {
   338  	defer s.setupMocks(c).Finish()
   339  	s.expectedRefreshRevisionNotFoundError(c)
   340  
   341  	origin := corecharm.Origin{
   342  		Source: "charm-hub",
   343  		Platform: corecharm.Platform{
   344  			Architecture: arch.DefaultArchitecture,
   345  		},
   346  	}
   347  
   348  	_, _, _, err := s.newClient().ResolveWithPreferredChannel("wordpress", origin)
   349  	c.Assert(err, gc.ErrorMatches,
   350  		`(?m)selecting releases: charm or bundle not found in the charm's default channel, base "amd64"
   351  available releases are:
   352    channel "latest/stable": available bases are: ubuntu@20.04`)
   353  }
   354  
   355  func (s *charmHubRepositorySuite) TestResolveRevisionNotFoundError(c *gc.C) {
   356  	defer s.setupMocks(c).Finish()
   357  	s.expectedRefreshRevisionNotFoundError(c)
   358  
   359  	origin := corecharm.Origin{
   360  		Source: "charm-hub",
   361  		Platform: corecharm.Platform{
   362  			Architecture: arch.DefaultArchitecture,
   363  			OS:           "ubuntu",
   364  			Channel:      "18.04",
   365  		},
   366  	}
   367  
   368  	repo := NewCharmHubRepository(s.logger, s.client)
   369  	_, _, _, err := repo.ResolveWithPreferredChannel("wordpress", origin)
   370  	c.Assert(err, gc.ErrorMatches,
   371  		`(?m)selecting releases: charm or bundle not found in the charm's default channel, base "amd64/ubuntu/18.04"
   372  available releases are:
   373    channel "latest/stable": available bases are: ubuntu@20.04`)
   374  }
   375  
   376  func (s *charmHubRepositorySuite) TestDownloadCharm(c *gc.C) {
   377  	defer s.setupMocks(c).Finish()
   378  
   379  	requestedOrigin := corecharm.Origin{
   380  		Source: "charm-hub",
   381  		Platform: corecharm.Platform{
   382  			Architecture: arch.DefaultArchitecture,
   383  			OS:           "ubuntu",
   384  			Channel:      "20.04",
   385  		},
   386  		Channel: &charm.Channel{
   387  			Track: "latest",
   388  			Risk:  "stable",
   389  		},
   390  	}
   391  	resolvedOrigin := corecharm.Origin{
   392  		Source: "charm-hub",
   393  		ID:     "charmCHARMcharmCHARMcharmCHARM01",
   394  		Hash:   "SHA256 hash",
   395  		Platform: corecharm.Platform{
   396  			Architecture: arch.DefaultArchitecture,
   397  			OS:           "ubuntu",
   398  			Channel:      "20.04",
   399  		},
   400  		Channel: &charm.Channel{
   401  			Track: "latest",
   402  			Risk:  "stable",
   403  		},
   404  	}
   405  
   406  	resolvedURL, err := url.Parse("ch:amd64/focal/wordpress-42")
   407  	c.Assert(err, jc.ErrorIsNil)
   408  	resolvedArchive := new(charm.CharmArchive)
   409  
   410  	s.expectCharmRefreshInstallOneFromChannel(c)
   411  	s.client.EXPECT().DownloadAndRead(context.TODO(), resolvedURL, "/tmp/foo").Return(resolvedArchive, nil)
   412  
   413  	gotArchive, gotOrigin, err := s.newClient().DownloadCharm("wordpress", requestedOrigin, "/tmp/foo")
   414  	c.Assert(err, jc.ErrorIsNil)
   415  	c.Assert(gotArchive, gc.Equals, resolvedArchive) // note: we are using gc.Equals to check the pointers here.
   416  	c.Assert(gotOrigin, gc.DeepEquals, resolvedOrigin)
   417  }
   418  
   419  func (s *charmHubRepositorySuite) TestGetDownloadURL(c *gc.C) {
   420  	defer s.setupMocks(c).Finish()
   421  
   422  	requestedOrigin := corecharm.Origin{
   423  		Source: "charm-hub",
   424  		Platform: corecharm.Platform{
   425  			Architecture: arch.DefaultArchitecture,
   426  			OS:           "ubuntu",
   427  			Channel:      "20.04",
   428  		},
   429  		Channel: &charm.Channel{
   430  			Track: "latest",
   431  			Risk:  "stable",
   432  		},
   433  	}
   434  	resolvedOrigin := corecharm.Origin{
   435  		Source: "charm-hub",
   436  		ID:     "charmCHARMcharmCHARMcharmCHARM01",
   437  		Hash:   "SHA256 hash",
   438  		Platform: corecharm.Platform{
   439  			Architecture: arch.DefaultArchitecture,
   440  			OS:           "ubuntu",
   441  			Channel:      "20.04",
   442  		},
   443  		Channel: &charm.Channel{
   444  			Track: "latest",
   445  			Risk:  "stable",
   446  		},
   447  	}
   448  
   449  	resolvedURL, err := url.Parse("ch:amd64/focal/wordpress-42")
   450  	c.Assert(err, jc.ErrorIsNil)
   451  
   452  	s.expectCharmRefreshInstallOneFromChannel(c)
   453  
   454  	gotURL, gotOrigin, err := s.newClient().GetDownloadURL("wordpress", requestedOrigin)
   455  	c.Assert(err, jc.ErrorIsNil)
   456  	c.Assert(gotURL, gc.DeepEquals, resolvedURL)
   457  	c.Assert(gotOrigin, gc.DeepEquals, resolvedOrigin)
   458  }
   459  
   460  func (s *charmHubRepositorySuite) TestGetEssentialMetadata(c *gc.C) {
   461  	defer s.setupMocks(c).Finish()
   462  
   463  	requestedOrigin := corecharm.Origin{
   464  		Source: "charm-hub",
   465  		Platform: corecharm.Platform{
   466  			Architecture: arch.DefaultArchitecture,
   467  			OS:           "ubuntu",
   468  			Channel:      "20.04",
   469  		},
   470  		Channel: &charm.Channel{
   471  			Track: "latest",
   472  			Risk:  "stable",
   473  		},
   474  	}
   475  
   476  	s.expectCharmRefreshInstallOneFromChannel(c) // resolve the origin
   477  	s.expectCharmRefreshInstallOneFromChannel(c) // refresh and get metadata
   478  
   479  	got, err := s.newClient().GetEssentialMetadata(corecharm.MetadataRequest{
   480  		CharmName: "wordpress",
   481  		Origin:    requestedOrigin,
   482  	})
   483  
   484  	c.Assert(err, jc.ErrorIsNil)
   485  	c.Assert(got, gc.HasLen, 1)
   486  	c.Assert(got[0].Meta.Name, gc.Equals, "wordpress")
   487  	c.Assert(got[0].Config.Options["blog-title"], gc.Not(gc.IsNil))
   488  	c.Assert(got[0].Manifest.Bases, gc.HasLen, 1)
   489  	c.Assert(got[0].ResolvedOrigin.ID, gc.Equals, "", gc.Commentf("ID is only added after charm download"))
   490  	c.Assert(got[0].ResolvedOrigin.Hash, gc.Equals, "", gc.Commentf("Hash is only added after charm download"))
   491  }
   492  
   493  func (s *charmHubRepositorySuite) TestResolveResources(c *gc.C) {
   494  	defer s.setupMocks(c).Finish()
   495  	s.expectRefresh(true)
   496  	s.expectListResourceRevisions(2)
   497  
   498  	result, err := s.newClient().ResolveResources([]charmresource.Resource{{
   499  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   500  		Origin:      charmresource.OriginUpload,
   501  		Revision:    1,
   502  		Fingerprint: fp(c),
   503  		Size:        0,
   504  	}, {
   505  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   506  		Origin:      charmresource.OriginStore,
   507  		Revision:    2,
   508  		Fingerprint: fp(c),
   509  		Size:        0,
   510  	}}, charmID())
   511  	c.Assert(err, jc.ErrorIsNil)
   512  	c.Assert(result, gc.DeepEquals, []charmresource.Resource{{
   513  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   514  		Origin:      charmresource.OriginUpload,
   515  		Revision:    1,
   516  		Fingerprint: fp(c),
   517  		Size:        0,
   518  	}, {
   519  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   520  		Origin:      charmresource.OriginStore,
   521  		Revision:    2,
   522  		Fingerprint: fp(c),
   523  		Size:        0,
   524  	}})
   525  }
   526  
   527  func (s *charmHubRepositorySuite) TestResolveResourcesFromStore(c *gc.C) {
   528  	defer s.setupMocks(c).Finish()
   529  	s.expectRefresh(false)
   530  	s.expectListResourceRevisions(1)
   531  
   532  	id := charmID()
   533  	id.Origin.ID = ""
   534  	result, err := s.newClient().ResolveResources([]charmresource.Resource{{
   535  		Meta:     charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   536  		Origin:   charmresource.OriginStore,
   537  		Revision: 1,
   538  		Size:     0,
   539  	}}, id)
   540  	c.Assert(err, jc.ErrorIsNil)
   541  	c.Assert(result, gc.DeepEquals, []charmresource.Resource{{
   542  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   543  		Origin:      charmresource.OriginStore,
   544  		Revision:    1,
   545  		Fingerprint: fp(c),
   546  		Size:        0,
   547  	}})
   548  }
   549  
   550  func (s *charmHubRepositorySuite) TestResolveResourcesFromStoreNoRevision(c *gc.C) {
   551  	defer s.setupMocks(c).Finish()
   552  	s.expectRefreshWithRevision(1, true)
   553  
   554  	result, err := s.newClient().ResolveResources([]charmresource.Resource{{
   555  		Meta:     charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   556  		Origin:   charmresource.OriginStore,
   557  		Revision: -1,
   558  		Size:     0,
   559  	}}, charmID())
   560  	c.Assert(err, jc.ErrorIsNil)
   561  	c.Assert(result, gc.DeepEquals, []charmresource.Resource{{
   562  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   563  		Origin:      charmresource.OriginStore,
   564  		Revision:    1,
   565  		Fingerprint: fp(c),
   566  		Size:        0,
   567  	}})
   568  }
   569  
   570  func (s *charmHubRepositorySuite) TestResolveResourcesNoMatchingRevision(c *gc.C) {
   571  	defer s.setupMocks(c).Finish()
   572  	s.expectRefresh(true)
   573  	s.expectRefreshWithRevision(99, true)
   574  	s.expectListResourceRevisions(3)
   575  
   576  	_, err := s.newClient().ResolveResources([]charmresource.Resource{{
   577  		Meta:     charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   578  		Origin:   charmresource.OriginStore,
   579  		Revision: 1,
   580  		Size:     0,
   581  	}}, charmID())
   582  	c.Assert(err, gc.ErrorMatches, `charm resource "wal-e" at revision 1 not found`)
   583  }
   584  
   585  func (s *charmHubRepositorySuite) TestResolveResourcesUpload(c *gc.C) {
   586  	defer s.setupMocks(c).Finish()
   587  	s.expectRefresh(false)
   588  
   589  	id := charmID()
   590  	id.Origin.ID = ""
   591  	result, err := s.newClient().ResolveResources([]charmresource.Resource{{
   592  		Meta:     charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   593  		Origin:   charmresource.OriginUpload,
   594  		Revision: 3,
   595  		Fingerprint: charmresource.Fingerprint{
   596  			Fingerprint: hash.Fingerprint{}},
   597  		Size: 0,
   598  	}}, id)
   599  	c.Assert(err, jc.ErrorIsNil)
   600  	c.Assert(result, gc.DeepEquals, []charmresource.Resource{{
   601  		Meta:     charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   602  		Origin:   charmresource.OriginUpload,
   603  		Revision: 3,
   604  		Fingerprint: charmresource.Fingerprint{
   605  			Fingerprint: hash.Fingerprint{}},
   606  		Size: 0,
   607  	}})
   608  }
   609  
   610  func (s *charmHubRepositorySuite) TestResourceInfo(c *gc.C) {
   611  	defer s.setupMocks(c).Finish()
   612  	s.expectRefreshWithRevision(25, false)
   613  
   614  	curl := charm.MustParseURL("ch:amd64/focal/ubuntu-19")
   615  	rev := curl.Revision
   616  	channel := corecharm.MustParseChannel("stable")
   617  	origin := corecharm.Origin{
   618  		Source:   corecharm.CharmHub,
   619  		Type:     "charm",
   620  		Revision: &rev,
   621  		Channel:  &channel,
   622  		Platform: corecharm.Platform{
   623  			OS:           "ubuntu",
   624  			Channel:      "20.04",
   625  			Architecture: "amd64",
   626  		},
   627  	}
   628  
   629  	result, err := s.newClient().resourceInfo(curl, origin, "wal-e", 25)
   630  	c.Assert(err, jc.ErrorIsNil)
   631  	c.Assert(result, gc.DeepEquals, charmresource.Resource{
   632  		Meta:        charmresource.Meta{Name: "wal-e", Type: 1, Path: "wal-e.snap", Description: "WAL-E Snap Package"},
   633  		Origin:      charmresource.OriginStore,
   634  		Revision:    25,
   635  		Fingerprint: fp(c),
   636  		Size:        0,
   637  	})
   638  }
   639  
   640  func (s *charmHubRepositorySuite) expectCharmRefreshInstallOneFromChannel(c *gc.C) {
   641  	cfg, err := charmhub.InstallOneFromChannel("wordpress", "latest/stable", charmhub.RefreshBase{
   642  		Architecture: arch.DefaultArchitecture,
   643  	})
   644  	c.Assert(err, jc.ErrorIsNil)
   645  	s.expectCharmRefresh(c, cfg)
   646  }
   647  
   648  func (s *charmHubRepositorySuite) expectCharmRefresh(c *gc.C, cfg charmhub.RefreshConfig) {
   649  	s.client.EXPECT().Refresh(gomock.Any(), RefreshConfigMatcher{c: c, Config: cfg}).DoAndReturn(func(ctx context.Context, cfg charmhub.RefreshConfig) ([]transport.RefreshResponse, error) {
   650  		id := charmhub.ExtractConfigInstanceKey(cfg)
   651  
   652  		return []transport.RefreshResponse{{
   653  			ID:          "charmCHARMcharmCHARMcharmCHARM01",
   654  			InstanceKey: id,
   655  			Entity: transport.RefreshEntity{
   656  				Type:     transport.CharmType,
   657  				ID:       "charmCHARMcharmCHARMcharmCHARM01",
   658  				Name:     "wordpress",
   659  				Revision: 16,
   660  				Download: transport.Download{
   661  					HashSHA256: "SHA256 hash",
   662  					HashSHA384: "SHA384 hash",
   663  					Size:       42,
   664  					URL:        "ch:amd64/focal/wordpress-42",
   665  				},
   666  				//
   667  				Bases: []transport.Base{
   668  					{
   669  						Name:         "ubuntu",
   670  						Architecture: "amd64",
   671  						Channel:      "20.04",
   672  					},
   673  				},
   674  				MetadataYAML: `
   675  name: wordpress
   676  summary: Blog engine
   677  description: Blog engine
   678  `[1:],
   679  				ConfigYAML: `
   680  options:
   681    blog-title: {default: My Title, description: A descriptive title used for the blog., type: string}
   682  `[1:],
   683  			},
   684  			EffectiveChannel: "latest/stable",
   685  		}}, nil
   686  	})
   687  }
   688  
   689  func (s *charmHubRepositorySuite) expectBundleRefresh(c *gc.C) {
   690  	cfg, err := charmhub.InstallOneFromChannel("core-kubernetes", "latest/stable", charmhub.RefreshBase{
   691  		Architecture: arch.DefaultArchitecture,
   692  	})
   693  	c.Assert(err, jc.ErrorIsNil)
   694  	s.client.EXPECT().Refresh(gomock.Any(), RefreshConfigMatcher{c: c, Config: cfg}).DoAndReturn(func(ctx context.Context, cfg charmhub.RefreshConfig) ([]transport.RefreshResponse, error) {
   695  		id := charmhub.ExtractConfigInstanceKey(cfg)
   696  
   697  		return []transport.RefreshResponse{{
   698  			ID:          "bundleBUNDLEbundleBUNDLE01",
   699  			InstanceKey: id,
   700  			Entity: transport.RefreshEntity{
   701  				Type:     transport.BundleType,
   702  				ID:       "bundleBUNDLEbundleBUNDLE01",
   703  				Name:     "core-kubernetes",
   704  				Revision: 17,
   705  			},
   706  			EffectiveChannel: "latest/stable",
   707  		}}, nil
   708  	})
   709  }
   710  
   711  func (s *charmHubRepositorySuite) expectedRefreshInvalidPlatformError(c *gc.C) {
   712  	s.client.EXPECT().Refresh(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, cfg charmhub.RefreshConfig) ([]transport.RefreshResponse, error) {
   713  		id := charmhub.ExtractConfigInstanceKey(cfg)
   714  
   715  		return []transport.RefreshResponse{{
   716  			ID:          "charmCHARMcharmCHARMcharmCHARM01",
   717  			InstanceKey: id,
   718  			Error: &transport.APIError{
   719  				Code:    transport.ErrorCodeInvalidCharmBase,
   720  				Message: "invalid charm platform",
   721  				Extra: transport.APIErrorExtra{
   722  					DefaultBases: []transport.Base{{
   723  						Architecture: "amd64",
   724  						Name:         "ubuntu",
   725  						Channel:      "20.04",
   726  					}},
   727  				},
   728  			},
   729  		}}, nil
   730  	})
   731  }
   732  
   733  func (s *charmHubRepositorySuite) expectedRefreshRevisionNotFoundError(c *gc.C) {
   734  	s.client.EXPECT().Refresh(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, cfg charmhub.RefreshConfig) ([]transport.RefreshResponse, error) {
   735  		id := charmhub.ExtractConfigInstanceKey(cfg)
   736  
   737  		return []transport.RefreshResponse{{
   738  			ID:          "charmCHARMcharmCHARMcharmCHARM01",
   739  			InstanceKey: id,
   740  			Error: &transport.APIError{
   741  				Code:    transport.ErrorCodeRevisionNotFound,
   742  				Message: "revision not found",
   743  				Extra: transport.APIErrorExtra{
   744  					Releases: []transport.Release{{
   745  						Base: transport.Base{
   746  							Architecture: "amd64",
   747  							Name:         "ubuntu",
   748  							Channel:      "20.04",
   749  						},
   750  						Channel: "stable",
   751  					}},
   752  				},
   753  			},
   754  		}}, nil
   755  	})
   756  }
   757  
   758  func (s *charmHubRepositorySuite) expectCharmRefreshInstallOneFromChannelFullBase(c *gc.C) {
   759  	cfg, err := charmhub.InstallOneFromChannel("wordpress", "latest/stable", charmhub.RefreshBase{
   760  		Architecture: arch.DefaultArchitecture, Name: "ubuntu", Channel: "20.04",
   761  	})
   762  	c.Assert(err, jc.ErrorIsNil)
   763  	s.expectCharmRefreshFullWithResources(c, cfg)
   764  }
   765  
   766  func (s *charmHubRepositorySuite) expectCharmRefreshInstallOneByRevisionResources(c *gc.C) {
   767  	cfg, err := charmhub.InstallOneFromRevision("wordpress", 16)
   768  	c.Assert(err, jc.ErrorIsNil)
   769  	s.expectCharmRefresh(c, cfg)
   770  }
   771  
   772  func (s *charmHubRepositorySuite) expectCharmRefreshFullWithResources(c *gc.C, cfg charmhub.RefreshConfig) {
   773  	s.client.EXPECT().Refresh(gomock.Any(), RefreshConfigMatcher{c: c, Config: cfg}).DoAndReturn(func(ctx context.Context, cfg charmhub.RefreshConfig) ([]transport.RefreshResponse, error) {
   774  		id := charmhub.ExtractConfigInstanceKey(cfg)
   775  		return []transport.RefreshResponse{{
   776  			ID:          "charmCHARMcharmCHARMcharmCHARM01",
   777  			InstanceKey: id,
   778  			Entity: transport.RefreshEntity{
   779  				Type:     transport.CharmType,
   780  				ID:       "charmCHARMcharmCHARMcharmCHARM01",
   781  				Name:     "wordpress",
   782  				Revision: 16,
   783  				Download: transport.Download{
   784  					HashSHA256: "SHA256 hash",
   785  					HashSHA384: "SHA384 hash",
   786  					Size:       42,
   787  					URL:        "ch:amd64/focal/wordpress-42",
   788  				},
   789  				//
   790  				Bases: []transport.Base{
   791  					{
   792  						Name:         "ubuntu",
   793  						Architecture: "amd64",
   794  						Channel:      "20.04",
   795  					},
   796  				},
   797  				MetadataYAML: `
   798  name: wordpress
   799  summary: Blog engine
   800  description: Blog engine
   801  `[1:],
   802  				ConfigYAML: `
   803  options:
   804    blog-title: {default: My Title, description: A descriptive title used for the blog., type: string}
   805  `[1:],
   806  				Resources: []transport.ResourceRevision{
   807  					resourceRevision(5),
   808  				},
   809  			},
   810  			EffectiveChannel: "latest/stable",
   811  		}}, nil
   812  	})
   813  }
   814  
   815  func (s *charmHubRepositorySuite) setupMocks(c *gc.C) *gomock.Controller {
   816  	ctrl := gomock.NewController(c)
   817  	s.client = mocks.NewMockCharmHubClient(ctrl)
   818  	s.logger = mocks.NewMockLogger(ctrl)
   819  	s.logger.EXPECT().Tracef(gomock.Any(), gomock.Any()).AnyTimes()
   820  	return ctrl
   821  }
   822  
   823  func (s *charmHubRepositorySuite) expectedCURL(curl *charm.URL, revision int, arch string, series string) *charm.URL {
   824  	return curl.WithRevision(revision).WithArchitecture(arch).WithSeries(series)
   825  }
   826  
   827  func (s *charmHubRepositorySuite) newClient() *CharmHubRepository {
   828  	return NewCharmHubRepository(s.logger, s.client)
   829  }
   830  
   831  func (s *charmHubRepositorySuite) expectRefresh(id bool) {
   832  	s.expectRefreshWithRevision(0, id)
   833  }
   834  
   835  func (s *charmHubRepositorySuite) expectRefreshWithRevision(rev int, id bool) {
   836  	resp := []transport.RefreshResponse{
   837  		{
   838  			Entity: transport.RefreshEntity{
   839  				CreatedAt: time.Date(2020, 7, 7, 9, 39, 44, 132000000, time.UTC),
   840  				Download:  transport.Download{HashSHA256: "c97e1efc5367d2fdcfdf29f4a2243b13765cc9cbdfad19627a29ac903c01ae63", Size: 5487460, URL: "https://api.staging.charmhub.io/api/v1/charms/download/jmeJLrjWpJX9OglKSeUHCwgyaCNuoQjD_208.charm"},
   841  				ID:        "jmeJLrjWpJX9OglKSeUHCwgyaCNuoQjD",
   842  				Name:      "ubuntu",
   843  				Resources: []transport.ResourceRevision{
   844  					resourceRevision(rev),
   845  				},
   846  				Revision: 19,
   847  				Summary:  "PostgreSQL object-relational SQL database (supported version)",
   848  				Version:  "208",
   849  			},
   850  			EffectiveChannel: "latest/stable",
   851  			Error:            (*transport.APIError)(nil),
   852  			Name:             "postgresql",
   853  			Result:           "download",
   854  		},
   855  	}
   856  	s.client.EXPECT().Refresh(gomock.Any(), charmhubConfigMatcher{id: id}).Return(resp, nil)
   857  }
   858  
   859  // charmhubConfigMatcher matches only the charm IDs and revisions of a
   860  // charmhub.RefreshMany config.
   861  type charmhubConfigMatcher struct {
   862  	id bool
   863  }
   864  
   865  func (m charmhubConfigMatcher) Matches(x interface{}) bool {
   866  	config, ok := x.(charmhub.RefreshConfig)
   867  	if !ok {
   868  		return false
   869  	}
   870  	h, err := config.Build()
   871  	if err != nil {
   872  		return false
   873  	}
   874  	if m.id && h.Actions[0].ID != nil && *h.Actions[0].ID == "meshuggah" {
   875  		return true
   876  	}
   877  	if !m.id && h.Actions[0].Name != nil && *h.Actions[0].Name == "ubuntu" {
   878  		return true
   879  	}
   880  	return false
   881  }
   882  
   883  func (m charmhubConfigMatcher) String() string {
   884  	if m.id {
   885  		return "match id"
   886  	}
   887  	return "match name"
   888  }
   889  
   890  func (s *charmHubRepositorySuite) expectListResourceRevisions(rev int) {
   891  	resp := []transport.ResourceRevision{
   892  		resourceRevision(rev),
   893  	}
   894  	s.client.EXPECT().ListResourceRevisions(gomock.Any(), gomock.Any(), gomock.Any()).Return(resp, nil)
   895  }
   896  
   897  func fp(c *gc.C) charmresource.Fingerprint {
   898  	fp, err := charmresource.ParseFingerprint("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b")
   899  	c.Assert(err, jc.ErrorIsNil)
   900  	return fp
   901  }
   902  
   903  func charmID() corecharm.CharmID {
   904  	curl := charm.MustParseURL("ubuntu")
   905  	channel, _ := charm.ParseChannel("stable")
   906  	return corecharm.CharmID{
   907  		URL: curl,
   908  		Origin: corecharm.Origin{
   909  			ID:      "meshuggah",
   910  			Source:  corecharm.CharmHub,
   911  			Channel: &channel,
   912  			Platform: corecharm.Platform{
   913  				OS:           "ubuntu",
   914  				Channel:      "20.04",
   915  				Architecture: "amd64",
   916  			},
   917  		}}
   918  }
   919  
   920  func resourceRevision(rev int) transport.ResourceRevision {
   921  	return transport.ResourceRevision{
   922  		Download: transport.Download{
   923  			HashSHA384: "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b",
   924  			Size:       0,
   925  			URL:        "https://api.staging.charmhub.io/api/v1/resources/download/charm_jmeJLrjWpJX9OglKSeUHCwgyaCNuoQjD.wal-e_0",
   926  		},
   927  		Name:        "wal-e",
   928  		Revision:    rev,
   929  		Type:        "file",
   930  		Filename:    "wal-e.snap",
   931  		Description: "WAL-E Snap Package",
   932  	}
   933  }
   934  
   935  // RefreshConfigMatcher is required so that we do check somethings going into
   936  // the refresh method. As instanceKey is private we don't know what it is until
   937  // it's called.
   938  type RefreshConfigMatcher struct {
   939  	c      *gc.C
   940  	Config charmhub.RefreshConfig
   941  }
   942  
   943  func (m RefreshConfigMatcher) Matches(x interface{}) bool {
   944  	rc, ok := x.(charmhub.RefreshConfig)
   945  	if !ok {
   946  		return false
   947  	}
   948  
   949  	cb, err := m.Config.Build()
   950  	m.c.Assert(err, jc.ErrorIsNil)
   951  
   952  	rcb, err := rc.Build()
   953  	m.c.Assert(err, jc.ErrorIsNil)
   954  	m.c.Assert(len(cb.Actions), gc.Equals, len(rcb.Actions))
   955  
   956  	if cb.Actions[0].ID == nil && rcb.Actions[0].ID == nil {
   957  		return true
   958  	}
   959  	return cb.Actions[0].ID != nil && rcb.Actions[0].ID != nil && *cb.Actions[0].ID == *rcb.Actions[0].ID
   960  }
   961  
   962  func (RefreshConfigMatcher) String() string {
   963  	return "is refresh config"
   964  }
   965  
   966  type refreshConfigSuite struct {
   967  	testing.IsolationSuite
   968  }
   969  
   970  var _ = gc.Suite(&refreshConfigSuite{})
   971  
   972  func (refreshConfigSuite) TestRefreshByChannel(c *gc.C) {
   973  	name := "wordpress"
   974  	platform := corecharm.MustParsePlatform("amd64/ubuntu/focal")
   975  	channel := corecharm.MustParseChannel("latest/stable").Normalize()
   976  	origin := corecharm.Origin{
   977  		Platform: platform,
   978  		Channel:  &channel,
   979  	}
   980  
   981  	cfg, err := refreshConfig(name, origin)
   982  	c.Assert(err, jc.ErrorIsNil)
   983  
   984  	ch := channel.String()
   985  	instanceKey := charmhub.ExtractConfigInstanceKey(cfg)
   986  
   987  	build, err := cfg.Build()
   988  	c.Assert(err, jc.ErrorIsNil)
   989  	c.Assert(build, gc.DeepEquals, transport.RefreshRequest{
   990  		Actions: []transport.RefreshRequestAction{{
   991  			Action:      "install",
   992  			InstanceKey: instanceKey,
   993  			Name:        &name,
   994  			Channel:     &ch,
   995  			Base: &transport.Base{
   996  				Name:         "ubuntu",
   997  				Channel:      "20.04",
   998  				Architecture: "amd64",
   999  			},
  1000  		}},
  1001  		Context: []transport.RefreshRequestContext{},
  1002  		Fields:  expRefreshFields,
  1003  	})
  1004  }
  1005  
  1006  func (refreshConfigSuite) TestRefreshByChannelVersion(c *gc.C) {
  1007  	name := "wordpress"
  1008  	// 'mistakenly' include a risk in the platform
  1009  	platform := corecharm.MustParsePlatform("amd64/ubuntu/20.10/stable")
  1010  	channel := corecharm.MustParseChannel("latest/stable").Normalize()
  1011  	origin := corecharm.Origin{
  1012  		Platform: platform,
  1013  		Channel:  &channel,
  1014  	}
  1015  
  1016  	cfg, err := refreshConfig(name, origin)
  1017  	c.Assert(err, jc.ErrorIsNil)
  1018  
  1019  	ch := channel.String()
  1020  	instanceKey := charmhub.ExtractConfigInstanceKey(cfg)
  1021  
  1022  	build, err := cfg.Build()
  1023  	c.Assert(err, jc.ErrorIsNil)
  1024  	c.Assert(build, gc.DeepEquals, transport.RefreshRequest{
  1025  		Actions: []transport.RefreshRequestAction{{
  1026  			Action:      "install",
  1027  			InstanceKey: instanceKey,
  1028  			Name:        &name,
  1029  			Channel:     &ch,
  1030  			Base: &transport.Base{
  1031  				Name:         "ubuntu",
  1032  				Channel:      "20.10",
  1033  				Architecture: "amd64",
  1034  			},
  1035  		}},
  1036  		Context: []transport.RefreshRequestContext{},
  1037  		Fields:  expRefreshFields,
  1038  	})
  1039  }
  1040  
  1041  func (refreshConfigSuite) TestRefreshByRevision(c *gc.C) {
  1042  	revision := 1
  1043  	name := "wordpress"
  1044  	platform := corecharm.MustParsePlatform("amd64/ubuntu/focal")
  1045  	origin := corecharm.Origin{
  1046  		Platform: platform,
  1047  		Revision: &revision,
  1048  	}
  1049  
  1050  	cfg, err := refreshConfig(name, origin)
  1051  	c.Assert(err, jc.ErrorIsNil)
  1052  
  1053  	instanceKey := charmhub.ExtractConfigInstanceKey(cfg)
  1054  
  1055  	build, err := cfg.Build()
  1056  	c.Assert(err, jc.ErrorIsNil)
  1057  	c.Assert(build, gc.DeepEquals, transport.RefreshRequest{
  1058  		Actions: []transport.RefreshRequestAction{{
  1059  			Action:      "install",
  1060  			InstanceKey: instanceKey,
  1061  			Name:        &name,
  1062  			Revision:    &revision,
  1063  		}},
  1064  		Context: []transport.RefreshRequestContext{},
  1065  		Fields:  expRefreshFields,
  1066  	})
  1067  }
  1068  
  1069  func (refreshConfigSuite) TestRefreshByID(c *gc.C) {
  1070  	id := "aaabbbccc"
  1071  	revision := 1
  1072  	platform := corecharm.MustParsePlatform("amd64/ubuntu/focal")
  1073  	channel := corecharm.MustParseChannel("stable")
  1074  	origin := corecharm.Origin{
  1075  		Type:        transport.CharmType.String(),
  1076  		ID:          id,
  1077  		Platform:    platform,
  1078  		Revision:    &revision,
  1079  		Channel:     &channel,
  1080  		InstanceKey: "instance-key",
  1081  	}
  1082  
  1083  	cfg, err := refreshConfig("wordpress", origin)
  1084  	c.Assert(err, jc.ErrorIsNil)
  1085  
  1086  	instanceKey := charmhub.ExtractConfigInstanceKey(cfg)
  1087  
  1088  	build, err := cfg.Build()
  1089  	c.Assert(err, jc.ErrorIsNil)
  1090  	c.Assert(build, gc.DeepEquals, transport.RefreshRequest{
  1091  		Actions: []transport.RefreshRequestAction{{
  1092  			Action:      "refresh",
  1093  			InstanceKey: instanceKey,
  1094  			ID:          &id,
  1095  		}},
  1096  		Context: []transport.RefreshRequestContext{{
  1097  			InstanceKey: instanceKey,
  1098  			ID:          id,
  1099  			Revision:    revision,
  1100  			Base: transport.Base{
  1101  				Name:         "ubuntu",
  1102  				Channel:      "20.04",
  1103  				Architecture: "amd64",
  1104  			},
  1105  			TrackingChannel: channel.String(),
  1106  		}},
  1107  		Fields: expRefreshFields,
  1108  	})
  1109  }
  1110  
  1111  type selectNextBaseSuite struct {
  1112  	testing.IsolationSuite
  1113  	logger *mocks.MockLogger
  1114  }
  1115  
  1116  var _ = gc.Suite(&selectNextBaseSuite{})
  1117  
  1118  func (*selectNextBaseSuite) TestSelectNextBaseWithNoBases(c *gc.C) {
  1119  	repo := new(CharmHubRepository)
  1120  	_, err := repo.selectNextBases(nil, corecharm.Origin{})
  1121  	c.Assert(err, gc.ErrorMatches, `no bases available`)
  1122  }
  1123  
  1124  func (*selectNextBaseSuite) TestSelectNextBaseWithInvalidBases(c *gc.C) {
  1125  	repo := new(CharmHubRepository)
  1126  	_, err := repo.selectNextBases([]transport.Base{{
  1127  		Architecture: "all",
  1128  	}}, corecharm.Origin{
  1129  		Platform: corecharm.Platform{
  1130  			Architecture: "amd64",
  1131  		},
  1132  	})
  1133  	c.Assert(err, gc.ErrorMatches, `bases matching architecture "amd64" not found`)
  1134  }
  1135  
  1136  func (*selectNextBaseSuite) TestSelectNextBaseWithInvalidBaseChannel(c *gc.C) {
  1137  	repo := new(CharmHubRepository)
  1138  	_, err := repo.selectNextBases([]transport.Base{{
  1139  		Architecture: "amd64",
  1140  	}}, corecharm.Origin{
  1141  		Platform: corecharm.Platform{
  1142  			Architecture: "amd64",
  1143  			OS:           "ubuntu",
  1144  		},
  1145  	})
  1146  	c.Assert(errors.IsNotValid(err), jc.IsTrue)
  1147  }
  1148  
  1149  func (*selectNextBaseSuite) TestSelectNextBaseWithInvalidOS(c *gc.C) {
  1150  	repo := new(CharmHubRepository)
  1151  	_, err := repo.selectNextBases([]transport.Base{{
  1152  		Architecture: "amd64",
  1153  	}}, corecharm.Origin{
  1154  		Platform: corecharm.Platform{
  1155  			Architecture: "amd64",
  1156  			OS:           "ubuntu",
  1157  		},
  1158  	})
  1159  	c.Assert(errors.IsNotValid(err), jc.IsTrue)
  1160  }
  1161  
  1162  func (*selectNextBaseSuite) TestSelectNextBaseWithValidBases(c *gc.C) {
  1163  	repo := new(CharmHubRepository)
  1164  	platform, err := repo.selectNextBases([]transport.Base{{
  1165  		Architecture: "amd64",
  1166  		Name:         "ubuntu",
  1167  		Channel:      "20.04",
  1168  	}}, corecharm.Origin{
  1169  		Platform: corecharm.Platform{
  1170  			Architecture: "amd64",
  1171  			OS:           "ubuntu",
  1172  			Channel:      "20.04",
  1173  		},
  1174  	})
  1175  	c.Assert(err, jc.ErrorIsNil)
  1176  	c.Assert(platform, gc.DeepEquals, []corecharm.Platform{{
  1177  		Architecture: "amd64",
  1178  		OS:           "ubuntu",
  1179  		Channel:      "20.04",
  1180  	}})
  1181  }
  1182  
  1183  func (*selectNextBaseSuite) TestSelectNextBaseWithValidBasesWithSeries(c *gc.C) {
  1184  	repo := new(CharmHubRepository)
  1185  	platform, err := repo.selectNextBases([]transport.Base{{
  1186  		Architecture: "amd64",
  1187  		Name:         "ubuntu",
  1188  		Channel:      "focal",
  1189  	}, {
  1190  		Architecture: "amd64",
  1191  		Name:         "ubuntu",
  1192  		Channel:      "20.04",
  1193  	}}, corecharm.Origin{
  1194  		Platform: corecharm.Platform{
  1195  			Architecture: "amd64",
  1196  			OS:           "ubuntu",
  1197  			Channel:      "20.04",
  1198  		},
  1199  	})
  1200  	c.Assert(err, jc.ErrorIsNil)
  1201  	c.Assert(platform, gc.DeepEquals, []corecharm.Platform{{
  1202  		Architecture: "amd64",
  1203  		OS:           "ubuntu",
  1204  		Channel:      "20.04",
  1205  	}})
  1206  }
  1207  
  1208  func (*selectNextBaseSuite) TestSelectNextBaseWithCentosBase(c *gc.C) {
  1209  	repo := new(CharmHubRepository)
  1210  	platform, err := repo.selectNextBases([]transport.Base{{
  1211  		Architecture: "amd64",
  1212  		Name:         "centos",
  1213  		Channel:      "7",
  1214  	}}, corecharm.Origin{
  1215  		Platform: corecharm.Platform{
  1216  			Architecture: "amd64",
  1217  			OS:           "ubuntu",
  1218  			Channel:      "20.04",
  1219  		},
  1220  	})
  1221  	c.Assert(err, jc.ErrorIsNil)
  1222  	c.Assert(platform, gc.DeepEquals, []corecharm.Platform{{
  1223  		Architecture: "amd64",
  1224  		OS:           "centos",
  1225  		Channel:      "7",
  1226  	}})
  1227  }
  1228  
  1229  func (*selectNextBaseSuite) TestSelectNextBasesFromReleasesNoReleasesError(c *gc.C) {
  1230  	channel := corecharm.MustParseChannel("stable/foo")
  1231  	repo := new(CharmHubRepository)
  1232  	err := repo.handleRevisionNotFound([]transport.Release{}, corecharm.Origin{
  1233  		Channel: &channel,
  1234  	})
  1235  	c.Assert(err, gc.ErrorMatches, `no releases available`)
  1236  }
  1237  
  1238  func (*selectNextBaseSuite) TestSelectNextBasesFromReleasesAmbiguousMatchError(c *gc.C) {
  1239  	channel := corecharm.MustParseChannel("stable/foo")
  1240  	repo := new(CharmHubRepository)
  1241  	err := repo.handleRevisionNotFound([]transport.Release{
  1242  		{},
  1243  	}, corecharm.Origin{
  1244  		Channel: &channel,
  1245  	})
  1246  	c.Assert(err, gc.ErrorMatches, fmt.Sprintf(`ambiguous arch and series with channel %q. specify both arch and series along with channel`, channel.String()))
  1247  }
  1248  
  1249  func (s *selectNextBaseSuite) TestSelectNextBasesFromReleasesSuggestionError(c *gc.C) {
  1250  	defer s.setupMocks(c).Finish()
  1251  	repo := NewCharmHubRepository(s.logger, nil)
  1252  
  1253  	channel := corecharm.MustParseChannel("stable")
  1254  	err := repo.handleRevisionNotFound([]transport.Release{{
  1255  		Base: transport.Base{
  1256  			Name:         "os",
  1257  			Channel:      "series",
  1258  			Architecture: "arch",
  1259  		},
  1260  		Channel: "stable",
  1261  	}}, corecharm.Origin{
  1262  		Channel: &channel,
  1263  	})
  1264  	c.Assert(err, gc.ErrorMatches, `charm or bundle not found for channel "stable", base ""`)
  1265  }
  1266  
  1267  func (s *selectNextBaseSuite) TestSelectNextBasesFromReleasesSuggestion(c *gc.C) {
  1268  	defer s.setupMocks(c).Finish()
  1269  	repo := NewCharmHubRepository(s.logger, nil)
  1270  	err := repo.handleRevisionNotFound([]transport.Release{{
  1271  		Base: transport.Base{
  1272  			Name:         "ubuntu",
  1273  			Channel:      "20.04",
  1274  			Architecture: "arch",
  1275  		},
  1276  		Channel: "stable",
  1277  	}}, corecharm.Origin{
  1278  		Platform: corecharm.Platform{
  1279  			Architecture: "arch",
  1280  		},
  1281  	})
  1282  	c.Assert(err, gc.ErrorMatches,
  1283  		`charm or bundle not found in the charm's default channel, base "arch"
  1284  available releases are:
  1285    channel "latest/stable": available bases are: ubuntu@20.04`)
  1286  }
  1287  
  1288  func (s *selectNextBaseSuite) setupMocks(c *gc.C) *gomock.Controller {
  1289  	ctrl := gomock.NewController(c)
  1290  	s.logger = mocks.NewMockLogger(ctrl)
  1291  	s.logger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
  1292  	s.logger.EXPECT().Tracef(gomock.Any(), gomock.Any()).AnyTimes()
  1293  	return ctrl
  1294  }
  1295  
  1296  type composeSuggestionsSuite struct {
  1297  	testing.IsolationSuite
  1298  	logger *mocks.MockLogger
  1299  }
  1300  
  1301  var _ = gc.Suite(&composeSuggestionsSuite{})
  1302  
  1303  func (s *composeSuggestionsSuite) TestNoReleases(c *gc.C) {
  1304  	defer s.setupMocks(c).Finish()
  1305  	repo := NewCharmHubRepository(s.logger, nil)
  1306  	suggestions := repo.composeSuggestions([]transport.Release{}, corecharm.Origin{})
  1307  	c.Assert(suggestions, gc.DeepEquals, []string(nil))
  1308  }
  1309  
  1310  func (s *composeSuggestionsSuite) TestNoMatchingArch(c *gc.C) {
  1311  	defer s.setupMocks(c).Finish()
  1312  	repo := NewCharmHubRepository(s.logger, nil)
  1313  	suggestions := repo.composeSuggestions([]transport.Release{{
  1314  		Base: transport.Base{
  1315  			Name:         "os",
  1316  			Channel:      "series",
  1317  			Architecture: "arch",
  1318  		},
  1319  		Channel: "stable",
  1320  	}}, corecharm.Origin{})
  1321  	c.Assert(suggestions, gc.DeepEquals, []string(nil))
  1322  }
  1323  
  1324  func (s *composeSuggestionsSuite) TestSuggestion(c *gc.C) {
  1325  	defer s.setupMocks(c).Finish()
  1326  	repo := NewCharmHubRepository(s.logger, nil)
  1327  	suggestions := repo.composeSuggestions([]transport.Release{{
  1328  		Base: transport.Base{
  1329  			Name:         "ubuntu",
  1330  			Channel:      "20.04",
  1331  			Architecture: "arch",
  1332  		},
  1333  		Channel: "stable",
  1334  	}}, corecharm.Origin{
  1335  		Platform: corecharm.Platform{
  1336  			Architecture: "arch",
  1337  		},
  1338  	})
  1339  	c.Assert(suggestions, gc.DeepEquals, []string{
  1340  		`channel "latest/stable": available bases are: ubuntu@20.04`,
  1341  	})
  1342  }
  1343  
  1344  func (s *composeSuggestionsSuite) TestSuggestionWithRisk(c *gc.C) {
  1345  	defer s.setupMocks(c).Finish()
  1346  	repo := NewCharmHubRepository(s.logger, nil)
  1347  	suggestions := repo.composeSuggestions([]transport.Release{{
  1348  		Base: transport.Base{
  1349  			Name:         "ubuntu",
  1350  			Channel:      "20.04/stable",
  1351  			Architecture: "arch",
  1352  		},
  1353  		Channel: "stable",
  1354  	}}, corecharm.Origin{
  1355  		Platform: corecharm.Platform{
  1356  			Architecture: "arch",
  1357  		},
  1358  	})
  1359  	c.Assert(suggestions, gc.DeepEquals, []string{
  1360  		`channel "latest/stable": available bases are: ubuntu@20.04`,
  1361  	})
  1362  }
  1363  
  1364  func (s *composeSuggestionsSuite) TestMultipleSuggestion(c *gc.C) {
  1365  	defer s.setupMocks(c).Finish()
  1366  	repo := NewCharmHubRepository(s.logger, nil)
  1367  	suggestions := repo.composeSuggestions([]transport.Release{{
  1368  		Base: transport.Base{
  1369  			Name:         "ubuntu",
  1370  			Channel:      "20.04",
  1371  			Architecture: "c",
  1372  		},
  1373  		Channel: "stable",
  1374  	}, {
  1375  		Base: transport.Base{
  1376  			Name:         "ubuntu",
  1377  			Channel:      "18.04",
  1378  			Architecture: "c",
  1379  		},
  1380  		Channel: "stable",
  1381  	}, {
  1382  		Base: transport.Base{
  1383  			Name:         "ubuntu",
  1384  			Channel:      "18.04",
  1385  			Architecture: "all",
  1386  		},
  1387  		Channel: "2.0/stable",
  1388  	}, {
  1389  		Base: transport.Base{
  1390  			Name:         "g",
  1391  			Channel:      "h",
  1392  			Architecture: "i",
  1393  		},
  1394  		Channel: "stable",
  1395  	}}, corecharm.Origin{
  1396  		Platform: corecharm.Platform{
  1397  			Architecture: "c",
  1398  		},
  1399  	})
  1400  	c.Assert(suggestions, jc.SameContents, []string{
  1401  		`channel "latest/stable": available bases are: ubuntu@20.04, ubuntu@18.04`,
  1402  		`channel "2.0/stable": available bases are: ubuntu@18.04`,
  1403  	})
  1404  }
  1405  
  1406  func (s *composeSuggestionsSuite) TestCentosSuggestion(c *gc.C) {
  1407  	defer s.setupMocks(c).Finish()
  1408  	repo := NewCharmHubRepository(s.logger, nil)
  1409  	suggestions := repo.composeSuggestions([]transport.Release{{
  1410  		Base: transport.Base{
  1411  			Name:         "centos",
  1412  			Channel:      "7",
  1413  			Architecture: "c",
  1414  		},
  1415  		Channel: "stable",
  1416  	}}, corecharm.Origin{
  1417  		Platform: corecharm.Platform{
  1418  			Architecture: "c",
  1419  		},
  1420  	})
  1421  	c.Assert(suggestions, gc.DeepEquals, []string{
  1422  		`channel "latest/stable": available bases are: centos@7`,
  1423  	})
  1424  }
  1425  
  1426  func (s *composeSuggestionsSuite) setupMocks(c *gc.C) *gomock.Controller {
  1427  	ctrl := gomock.NewController(c)
  1428  	s.logger = mocks.NewMockLogger(ctrl)
  1429  	s.logger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
  1430  	s.logger.EXPECT().Tracef(gomock.Any(), gomock.Any()).AnyTimes()
  1431  	return ctrl
  1432  }