github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/applicationoffers/applicationoffers_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package applicationoffers_test
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
    11  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
    12  	"github.com/juju/charm/v12"
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names/v5"
    16  	jc "github.com/juju/testing/checkers"
    17  	"github.com/juju/utils/v3"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/apiserver/common"
    21  	"github.com/juju/juju/apiserver/common/crossmodel"
    22  	apiservererrors "github.com/juju/juju/apiserver/errors"
    23  	"github.com/juju/juju/apiserver/facades/client/applicationoffers"
    24  	apiservertesting "github.com/juju/juju/apiserver/testing"
    25  	jujucrossmodel "github.com/juju/juju/core/crossmodel"
    26  	"github.com/juju/juju/core/network"
    27  	"github.com/juju/juju/core/permission"
    28  	"github.com/juju/juju/environs"
    29  	"github.com/juju/juju/rpc/params"
    30  	"github.com/juju/juju/state"
    31  	"github.com/juju/juju/testing"
    32  )
    33  
    34  type applicationOffersSuite struct {
    35  	baseSuite
    36  	api *applicationoffers.OffersAPIv5
    37  }
    38  
    39  var _ = gc.Suite(&applicationOffersSuite{})
    40  
    41  func (s *applicationOffersSuite) SetUpTest(c *gc.C) {
    42  	s.baseSuite.SetUpTest(c)
    43  	s.applicationOffers = &stubApplicationOffers{
    44  		// Ensure that calls to "Offer" made by the test suite call
    45  		// AddOffer by default.
    46  		applicationOffer: func(string) (*jujucrossmodel.ApplicationOffer, error) {
    47  			return nil, errors.NotFoundf("offer")
    48  		},
    49  	}
    50  	getApplicationOffers := func(interface{}) jujucrossmodel.ApplicationOffers {
    51  		return s.applicationOffers
    52  	}
    53  
    54  	resources := common.NewResources()
    55  	_ = resources.RegisterNamed("dataDir", common.StringResource(c.MkDir()))
    56  
    57  	getEnviron := func(modelUUID string) (environs.Environ, error) {
    58  		return s.env, nil
    59  	}
    60  	var err error
    61  	s.bakery = &mockBakeryService{caveats: make(map[string][]checkers.Caveat)}
    62  	thirdPartyKey := bakery.MustGenerateKey()
    63  	s.authContext, err = crossmodel.NewAuthContext(
    64  		s.mockState, thirdPartyKey,
    65  		crossmodel.NewOfferBakeryForTest(s.bakery, clock.WallClock),
    66  	)
    67  	c.Assert(err, jc.ErrorIsNil)
    68  	api, err := applicationoffers.CreateOffersAPI(
    69  		getApplicationOffers, getEnviron, getFakeControllerInfo,
    70  		s.mockState, s.mockStatePool, s.authorizer, resources, s.authContext,
    71  	)
    72  	c.Assert(err, jc.ErrorIsNil)
    73  	s.api = api
    74  }
    75  
    76  func (s *applicationOffersSuite) assertOffer(c *gc.C, expectedErr error) {
    77  	applicationName := "test"
    78  	s.addApplication(c, applicationName)
    79  	one := params.AddApplicationOffer{
    80  		ModelTag:        testing.ModelTag.String(),
    81  		OfferName:       "offer-test",
    82  		ApplicationName: applicationName,
    83  		Endpoints:       map[string]string{"db": "db"},
    84  		OwnerTag:        "user-fred",
    85  	}
    86  	all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one}}
    87  	s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) {
    88  		c.Assert(offer.OfferName, gc.Equals, one.OfferName)
    89  		c.Assert(offer.ApplicationName, gc.Equals, one.ApplicationName)
    90  		c.Assert(offer.ApplicationDescription, gc.Equals, "A pretty popular blog engine")
    91  		c.Assert(offer.Owner, gc.Equals, "fred")
    92  		c.Assert(offer.HasRead, gc.DeepEquals, []string{"everyone@external"})
    93  		return &jujucrossmodel.ApplicationOffer{}, nil
    94  	}
    95  	ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}}
    96  	s.mockState.applications = map[string]crossmodel.Application{
    97  		applicationName: &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}},
    98  	}
    99  	s.mockState.spaces["myspace"] = &mockSpace{
   100  		name:       "myspace",
   101  		providerId: "juju-space-myspace",
   102  		subnets: network.SubnetInfos{
   103  			{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", AvailabilityZones: []string{"az1"}},
   104  		},
   105  	}
   106  	s.env.spaceInfo = &environs.ProviderSpaceInfo{
   107  		SpaceInfo: network.SpaceInfo{
   108  			ID:         "1",
   109  			Name:       "myspace",
   110  			ProviderId: "juju-space-myspace",
   111  			Subnets: []network.SubnetInfo{{
   112  				CIDR:              "4.3.2.0/24",
   113  				ProviderId:        "juju-subnet-1",
   114  				AvailabilityZones: []string{"az1"},
   115  			}},
   116  		},
   117  	}
   118  
   119  	errs, err := s.api.Offer(all)
   120  	c.Assert(err, jc.ErrorIsNil)
   121  	c.Assert(errs.Results, gc.HasLen, len(all.Offers))
   122  	if expectedErr != nil {
   123  		c.Assert(errs.Results[0].Error, gc.ErrorMatches, expectedErr.Error())
   124  		return
   125  	}
   126  	c.Assert(errs.Results[0].Error, gc.IsNil)
   127  	s.applicationOffers.CheckCallNames(c, offerCall, addOffersBackendCall)
   128  }
   129  
   130  func (s *applicationOffersSuite) TestOffer(c *gc.C) {
   131  	s.authorizer.Tag = names.NewUserTag("admin")
   132  	s.assertOffer(c, nil)
   133  }
   134  
   135  func (s *applicationOffersSuite) TestAddOfferUpdatesExistingOffer(c *gc.C) {
   136  	s.authorizer.Tag = names.NewUserTag("admin")
   137  	applicationName := "test"
   138  	s.addApplication(c, applicationName)
   139  	one := params.AddApplicationOffer{
   140  		ModelTag:        testing.ModelTag.String(),
   141  		OfferName:       "offer-test",
   142  		ApplicationName: applicationName,
   143  		Endpoints:       map[string]string{"db": "db"},
   144  	}
   145  	all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one}}
   146  	s.applicationOffers.applicationOffer = func(name string) (*jujucrossmodel.ApplicationOffer, error) {
   147  		c.Assert(name, gc.Equals, one.OfferName)
   148  		return &jujucrossmodel.ApplicationOffer{}, nil
   149  	}
   150  	s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) {
   151  		return nil, errors.BadRequestf("unexpected call to AddOffer; expected a call to UpdateOffer instead")
   152  	}
   153  	s.applicationOffers.updateOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) {
   154  		c.Assert(offer.OfferName, gc.Equals, one.OfferName)
   155  		c.Assert(offer.ApplicationName, gc.Equals, one.ApplicationName)
   156  		c.Assert(offer.ApplicationDescription, gc.Equals, "A pretty popular blog engine")
   157  		c.Assert(offer.Owner, gc.Equals, "admin")
   158  		c.Assert(offer.HasRead, gc.DeepEquals, []string{"everyone@external"})
   159  		return &jujucrossmodel.ApplicationOffer{}, nil
   160  	}
   161  	ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}}
   162  	s.mockState.applications = map[string]crossmodel.Application{
   163  		applicationName: &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}},
   164  	}
   165  	errs, err := s.api.Offer(all)
   166  	c.Assert(err, jc.ErrorIsNil)
   167  	c.Assert(errs.Results, gc.HasLen, len(all.Offers))
   168  	c.Assert(errs.Results[0].Error, gc.IsNil)
   169  	s.applicationOffers.CheckCallNames(c, offerCall, updateOfferBackendCall)
   170  }
   171  
   172  func (s *applicationOffersSuite) TestOfferPermission(c *gc.C) {
   173  	s.authorizer.Tag = names.NewUserTag("mary")
   174  	s.assertOffer(c, apiservererrors.ErrPerm)
   175  }
   176  
   177  func (s *applicationOffersSuite) TestOfferSomeFail(c *gc.C) {
   178  	s.authorizer.Tag = names.NewUserTag("admin")
   179  	s.addApplication(c, "one")
   180  	s.addApplication(c, "two")
   181  	s.addApplication(c, "paramsfail")
   182  	one := params.AddApplicationOffer{
   183  		ModelTag:        testing.ModelTag.String(),
   184  		OfferName:       "offer-one",
   185  		ApplicationName: "one",
   186  		Endpoints:       map[string]string{"db": "db"},
   187  	}
   188  	bad := params.AddApplicationOffer{
   189  		ModelTag:        testing.ModelTag.String(),
   190  		OfferName:       "offer-bad",
   191  		ApplicationName: "notthere",
   192  		Endpoints:       map[string]string{"db": "db"},
   193  	}
   194  	bad2 := params.AddApplicationOffer{
   195  		ModelTag:        testing.ModelTag.String(),
   196  		OfferName:       "offer-bad",
   197  		ApplicationName: "paramsfail",
   198  		Endpoints:       map[string]string{"db": "db"},
   199  	}
   200  	two := params.AddApplicationOffer{
   201  		ModelTag:        testing.ModelTag.String(),
   202  		OfferName:       "offer-two",
   203  		ApplicationName: "two",
   204  		Endpoints:       map[string]string{"db": "db"},
   205  	}
   206  	all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one, bad, bad2, two}}
   207  	s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) {
   208  		if offer.ApplicationName == "paramsfail" {
   209  			return nil, errors.New("params fail")
   210  		}
   211  		return &jujucrossmodel.ApplicationOffer{}, nil
   212  	}
   213  	ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}}
   214  	s.mockState.applications = map[string]crossmodel.Application{
   215  		"one":        &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}},
   216  		"two":        &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}},
   217  		"paramsfail": &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}},
   218  	}
   219  
   220  	errs, err := s.api.Offer(all)
   221  	c.Assert(err, jc.ErrorIsNil)
   222  	c.Assert(errs.Results, gc.HasLen, len(all.Offers))
   223  	c.Assert(errs.Results[0].Error, gc.IsNil)
   224  	c.Assert(errs.Results[3].Error, gc.IsNil)
   225  	c.Assert(errs.Results[1].Error, gc.ErrorMatches, `getting offered application notthere: application "notthere" not found`)
   226  	c.Assert(errs.Results[2].Error, gc.ErrorMatches, `params fail`)
   227  	s.applicationOffers.CheckCallNames(c, offerCall, addOffersBackendCall, offerCall, addOffersBackendCall, offerCall, addOffersBackendCall)
   228  }
   229  
   230  func (s *applicationOffersSuite) TestOfferError(c *gc.C) {
   231  	s.authorizer.Tag = names.NewUserTag("admin")
   232  	applicationName := "test"
   233  	s.addApplication(c, applicationName)
   234  	one := params.AddApplicationOffer{
   235  		ModelTag:        testing.ModelTag.String(),
   236  		OfferName:       "offer-test",
   237  		ApplicationName: applicationName,
   238  		Endpoints:       map[string]string{"db": "db"},
   239  	}
   240  	all := params.AddApplicationOffers{Offers: []params.AddApplicationOffer{one}}
   241  
   242  	msg := "fail"
   243  
   244  	s.applicationOffers.addOffer = func(offer jujucrossmodel.AddApplicationOfferArgs) (*jujucrossmodel.ApplicationOffer, error) {
   245  		return nil, errors.New(msg)
   246  	}
   247  	ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}}
   248  	s.mockState.applications = map[string]crossmodel.Application{
   249  		applicationName: &mockApplication{charm: ch, bindings: map[string]string{"db": "myspace"}},
   250  	}
   251  
   252  	errs, err := s.api.Offer(all)
   253  	c.Assert(err, jc.ErrorIsNil)
   254  	c.Assert(errs.Results, gc.HasLen, len(all.Offers))
   255  	c.Assert(errs.Results[0].Error, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg))
   256  	s.applicationOffers.CheckCallNames(c, offerCall, addOffersBackendCall)
   257  }
   258  
   259  func (s *applicationOffersSuite) assertList(c *gc.C, offerUUID string, expectedErr error, expectedCIDRS []string) {
   260  	s.mockState.users["mary"] = &mockUser{"mary"}
   261  	_ = s.mockState.CreateOfferAccess(
   262  		names.NewApplicationOfferTag(offerUUID),
   263  		names.NewUserTag("mary"), permission.ConsumeAccess)
   264  	filter := params.OfferFilters{
   265  		Filters: []params.OfferFilter{
   266  			{
   267  				OwnerName:       "fred@external",
   268  				ModelName:       "prod",
   269  				OfferName:       "hosted-db2",
   270  				ApplicationName: "test",
   271  			},
   272  		},
   273  	}
   274  	found, err := s.api.ListApplicationOffers(filter)
   275  	if expectedErr != nil {
   276  		c.Assert(errors.Cause(err), gc.ErrorMatches, expectedErr.Error())
   277  		return
   278  	}
   279  	c.Assert(err, jc.ErrorIsNil)
   280  
   281  	expectedOfferDetails := []params.ApplicationOfferAdminDetailsV5{
   282  		{
   283  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   284  				SourceModelTag:         testing.ModelTag.String(),
   285  				ApplicationDescription: "description",
   286  				OfferName:              "hosted-db2",
   287  				OfferUUID:              offerUUID,
   288  				OfferURL:               "fred@external/prod.hosted-db2",
   289  				Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
   290  				Users: []params.OfferUserDetails{
   291  					{UserName: "admin", DisplayName: "", Access: "admin"},
   292  					{UserName: "mary", DisplayName: "mary", Access: "consume"},
   293  				},
   294  			},
   295  			ApplicationName: "test",
   296  			CharmURL:        "ch:db2-2",
   297  			Connections: []params.OfferConnection{{
   298  				SourceModelTag: testing.ModelTag.String(),
   299  				RelationId:     1,
   300  				Endpoint:       "db",
   301  				Username:       "fred@external",
   302  				Status:         params.EntityStatus{Status: "joined"},
   303  				IngressSubnets: expectedCIDRS,
   304  			}},
   305  		},
   306  	}
   307  	c.Assert(found, jc.DeepEquals, params.QueryApplicationOffersResultsV5{
   308  		expectedOfferDetails,
   309  	})
   310  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
   311  	if s.mockState.model.modelType == state.ModelTypeCAAS {
   312  		s.env.stub.CheckNoCalls(c)
   313  		return
   314  	}
   315  }
   316  
   317  func (s *applicationOffersSuite) TestList(c *gc.C) {
   318  	s.authorizer.Tag = names.NewUserTag("admin")
   319  	offerUUID := s.setupOffers(c, "test", false)
   320  	s.assertList(c, offerUUID, nil, []string{"192.168.1.0/32", "10.0.0.0/8"})
   321  }
   322  
   323  func (s *applicationOffersSuite) TestListCAAS(c *gc.C) {
   324  	s.authorizer.Tag = names.NewUserTag("admin")
   325  	offerUUID := s.setupOffers(c, "test", false)
   326  	s.mockState.model.modelType = state.ModelTypeCAAS
   327  	s.assertList(c, offerUUID, nil, []string{"192.168.1.0/32", "10.0.0.0/8"})
   328  }
   329  
   330  func (s *applicationOffersSuite) TestListNoRelationNetworks(c *gc.C) {
   331  	s.authorizer.Tag = names.NewUserTag("admin")
   332  	s.mockState.relationNetworks = nil
   333  	offerUUID := s.setupOffers(c, "test", false)
   334  	s.assertList(c, offerUUID, nil, nil)
   335  }
   336  
   337  func (s *applicationOffersSuite) TestListPermission(c *gc.C) {
   338  	offerUUID := s.setupOffers(c, "test", false)
   339  	s.assertList(c, offerUUID, apiservererrors.ErrPerm, nil)
   340  }
   341  
   342  func (s *applicationOffersSuite) TestListError(c *gc.C) {
   343  	s.setupOffers(c, "test", false)
   344  	s.authorizer.Tag = names.NewUserTag("admin")
   345  	filter := params.OfferFilters{
   346  		Filters: []params.OfferFilter{
   347  			{
   348  				OwnerName:       "fred@external",
   349  				ModelName:       "prod",
   350  				OfferName:       "hosted-db2",
   351  				ApplicationName: "test",
   352  			},
   353  		},
   354  	}
   355  	msg := "fail"
   356  
   357  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
   358  		return nil, errors.New(msg)
   359  	}
   360  
   361  	_, err := s.api.ListApplicationOffers(filter)
   362  	c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg))
   363  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
   364  }
   365  
   366  func (s *applicationOffersSuite) TestListFilterRequiresModel(c *gc.C) {
   367  	s.setupOffers(c, "test", false)
   368  	filter := params.OfferFilters{
   369  		Filters: []params.OfferFilter{
   370  			{
   371  				OfferName:       "hosted-db2",
   372  				ApplicationName: "test",
   373  			},
   374  		},
   375  	}
   376  	_, err := s.api.ListApplicationOffers(filter)
   377  	c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name")
   378  }
   379  
   380  func (s *applicationOffersSuite) TestListRequiresFilter(c *gc.C) {
   381  	s.setupOffers(c, "test", false)
   382  	_, err := s.api.ListApplicationOffers(params.OfferFilters{})
   383  	c.Assert(err, gc.ErrorMatches, "at least one offer filter is required")
   384  }
   385  
   386  func (s *applicationOffersSuite) assertShow(c *gc.C, url, offerUUID string, expected []params.ApplicationOfferResult) {
   387  	s.setupOffersForUUID(c, offerUUID, "", false)
   388  	s.mockState.users["mary"] = &mockUser{"mary"}
   389  	_ = s.mockState.CreateOfferAccess(
   390  		names.NewApplicationOfferTag(offerUUID),
   391  		names.NewUserTag("mary"), permission.ConsumeAccess)
   392  	filter := params.OfferURLs{[]string{url}, bakery.LatestVersion}
   393  
   394  	found, err := s.api.ApplicationOffers(filter)
   395  	c.Assert(err, jc.ErrorIsNil)
   396  	c.Assert(found.Results, jc.DeepEquals, expected)
   397  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
   398  	if len(expected) > 0 {
   399  		return
   400  	}
   401  	s.env.stub.CheckCallNames(c, "ProviderSpaceInfo")
   402  	s.env.stub.CheckCall(c, 0, "ProviderSpaceInfo", &network.SpaceInfo{
   403  		Name:       "myspace",
   404  		ProviderId: "juju-space-myspace",
   405  		Subnets: []network.SubnetInfo{{
   406  			CIDR:              "4.3.2.0/24",
   407  			ProviderId:        "juju-subnet-1",
   408  			AvailabilityZones: []string{"az1"},
   409  		}},
   410  	})
   411  }
   412  
   413  func (s *applicationOffersSuite) TestShow(c *gc.C) {
   414  	offerUUID := utils.MustNewUUID().String()
   415  	expected := []params.ApplicationOfferResult{{
   416  		Result: &params.ApplicationOfferAdminDetailsV5{
   417  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   418  				SourceModelTag:         testing.ModelTag.String(),
   419  				ApplicationDescription: "description",
   420  				OfferURL:               "fred@external/prod.hosted-db2",
   421  				OfferName:              "hosted-db2",
   422  				OfferUUID:              offerUUID,
   423  				Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
   424  				Users: []params.OfferUserDetails{
   425  					{UserName: "fred@external", DisplayName: "", Access: "admin"},
   426  					{UserName: "mary", DisplayName: "mary", Access: "consume"},
   427  				},
   428  			},
   429  			ApplicationName: "test",
   430  			CharmURL:        "ch:db2-2",
   431  			Connections: []params.OfferConnection{{
   432  				SourceModelTag: "model-deadbeef-0bad-400d-8000-4b1d0d06f00d",
   433  				RelationId:     1, Username: "fred@external", Endpoint: "db",
   434  				Status:         params.EntityStatus{Status: "joined"},
   435  				IngressSubnets: []string{"192.168.1.0/32", "10.0.0.0/8"},
   436  			}},
   437  		},
   438  	}}
   439  	s.authorizer.Tag = names.NewUserTag("admin")
   440  	expected[0].Result.Users[0].UserName = "admin"
   441  	s.assertShow(c, "fred@external/prod.hosted-db2", offerUUID, expected)
   442  	// Again with an unqualified model path.
   443  	s.mockState.AdminTag = names.NewUserTag("fred@external")
   444  	s.authorizer.AdminTag = s.mockState.AdminTag
   445  	s.authorizer.Tag = s.mockState.AdminTag
   446  	expected[0].Result.Users[0].UserName = "fred@external"
   447  	s.applicationOffers.ResetCalls()
   448  	s.assertShow(c, "prod.hosted-db2", offerUUID, expected)
   449  }
   450  
   451  func (s *applicationOffersSuite) TestShowNoPermission(c *gc.C) {
   452  	offerUUID := utils.MustNewUUID().String()
   453  	s.mockState.users["someone"] = &mockUser{"someone"}
   454  	user := names.NewUserTag("someone")
   455  	offer := names.NewApplicationOfferTag(offerUUID)
   456  	err := s.mockState.CreateOfferAccess(offer, user, permission.NoAccess)
   457  	c.Assert(err, jc.ErrorIsNil)
   458  
   459  	s.authorizer.Tag = user
   460  	expected := []params.ApplicationOfferResult{{
   461  		Error: apiservererrors.ServerError(errors.NotFoundf("application offer %q", "fred@external/prod.hosted-db2")),
   462  	}}
   463  	s.assertShow(c, "fred@external/prod.hosted-db2", offerUUID, expected)
   464  }
   465  
   466  func (s *applicationOffersSuite) TestShowPermission(c *gc.C) {
   467  	offerUUID := utils.MustNewUUID().String()
   468  	user := names.NewUserTag("someone")
   469  	s.authorizer.Tag = user
   470  	expected := []params.ApplicationOfferResult{{
   471  		Result: &params.ApplicationOfferAdminDetailsV5{
   472  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   473  				SourceModelTag:         testing.ModelTag.String(),
   474  				ApplicationDescription: "description",
   475  				OfferURL:               "fred@external/prod.hosted-db2",
   476  				OfferName:              "hosted-db2",
   477  				OfferUUID:              offerUUID,
   478  				Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
   479  				Users: []params.OfferUserDetails{
   480  					{UserName: "someone", DisplayName: "someone", Access: "read"},
   481  				},
   482  			},
   483  		}}}
   484  	s.mockState.users[user.Name()] = &mockUser{user.Name()}
   485  	_ = s.mockState.CreateOfferAccess(names.NewApplicationOfferTag(offerUUID), user, permission.ReadAccess)
   486  	s.assertShow(c, "fred@external/prod.hosted-db2", offerUUID, expected)
   487  }
   488  
   489  func (s *applicationOffersSuite) TestShowError(c *gc.C) {
   490  	url := "fred@external/prod.hosted-db2"
   491  	filter := params.OfferURLs{[]string{url}, bakery.LatestVersion}
   492  	msg := "fail"
   493  
   494  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
   495  		return nil, errors.New(msg)
   496  	}
   497  	s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
   498  
   499  	_, err := s.api.ApplicationOffers(filter)
   500  	c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg))
   501  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
   502  }
   503  
   504  func (s *applicationOffersSuite) TestShowNotFound(c *gc.C) {
   505  	urls := []string{"fred@external/prod.hosted-db2"}
   506  	filter := params.OfferURLs{urls, bakery.LatestVersion}
   507  
   508  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
   509  		return nil, nil
   510  	}
   511  	s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
   512  
   513  	found, err := s.api.ApplicationOffers(filter)
   514  	c.Assert(err, jc.ErrorIsNil)
   515  	c.Assert(found.Results, gc.HasLen, 1)
   516  	c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred@external/prod.hosted-db2" not found`)
   517  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
   518  }
   519  
   520  func (s *applicationOffersSuite) TestShowRejectsEndpoints(c *gc.C) {
   521  	urls := []string{"fred@external/prod.hosted-db2:db"}
   522  	filter := params.OfferURLs{urls, bakery.LatestVersion}
   523  	s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
   524  
   525  	found, err := s.api.ApplicationOffers(filter)
   526  	c.Assert(err, jc.ErrorIsNil)
   527  	c.Assert(found.Results, gc.HasLen, 1)
   528  	c.Assert(found.Results[0].Error.Message, gc.Equals, `saas application "fred@external/prod.hosted-db2:db" shouldn't include endpoint`)
   529  }
   530  
   531  func (s *applicationOffersSuite) TestShowErrorMsgMultipleURLs(c *gc.C) {
   532  	urls := []string{"fred@external/prod.hosted-mysql", "fred@external/test.hosted-db2"}
   533  	filter := params.OfferURLs{urls, bakery.LatestVersion}
   534  
   535  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
   536  		return nil, nil
   537  	}
   538  	s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
   539  	anotherModel := &mockModel{uuid: "uuid2", name: "test", owner: "fred@external", modelType: state.ModelTypeIAAS}
   540  	s.mockStatePool.st["uuid2"] = &mockState{
   541  		modelUUID: "uuid2",
   542  		model:     anotherModel,
   543  	}
   544  	s.mockState.allmodels = []applicationoffers.Model{s.mockState.model, anotherModel}
   545  
   546  	found, err := s.api.ApplicationOffers(filter)
   547  	c.Assert(err, jc.ErrorIsNil)
   548  	c.Assert(found.Results, gc.HasLen, 2)
   549  	c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred@external/prod.hosted-mysql" not found`)
   550  	c.Assert(found.Results[1].Error.Error(), gc.Matches, `application offer "fred@external/test.hosted-db2" not found`)
   551  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall)
   552  }
   553  
   554  func (s *applicationOffersSuite) TestShowFoundMultiple(c *gc.C) {
   555  	name := "test"
   556  	url := "fred@external/prod.hosted-" + name
   557  	anOffer := jujucrossmodel.ApplicationOffer{
   558  		ApplicationName:        name,
   559  		ApplicationDescription: "description",
   560  		OfferName:              "hosted-" + name,
   561  		OfferUUID:              "hosted-" + name + "-uuid",
   562  		Endpoints:              map[string]charm.Relation{"db": {Name: "db"}},
   563  	}
   564  
   565  	name2 := "testagain"
   566  	url2 := "mary/test.hosted-" + name2
   567  	anOffer2 := jujucrossmodel.ApplicationOffer{
   568  		ApplicationName:        name2,
   569  		ApplicationDescription: "description2",
   570  		OfferName:              "hosted-" + name2,
   571  		OfferUUID:              "hosted-" + name2 + "-uuid",
   572  		Endpoints:              map[string]charm.Relation{"db2": {Name: "db2"}},
   573  	}
   574  
   575  	filter := params.OfferURLs{[]string{url, url2}, bakery.LatestVersion}
   576  
   577  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
   578  		c.Assert(filters, gc.HasLen, 1)
   579  		if filters[0].OfferName == "hosted-test" {
   580  			return []jujucrossmodel.ApplicationOffer{anOffer}, nil
   581  		}
   582  		return []jujucrossmodel.ApplicationOffer{anOffer2}, nil
   583  	}
   584  	ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}}
   585  	s.mockState.applications = map[string]crossmodel.Application{
   586  		"test": &mockApplication{
   587  			charm: ch, curl: "ch:db2-2", bindings: map[string]string{"db": "myspace"}},
   588  	}
   589  
   590  	model := &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
   591  	anotherModel := &mockModel{uuid: "uuid2", name: "test", owner: "mary", modelType: state.ModelTypeIAAS}
   592  
   593  	s.mockState.model = model
   594  	s.mockState.allmodels = []applicationoffers.Model{model, anotherModel}
   595  	s.mockState.spaces["myspace"] = &mockSpace{
   596  		name:       "myspace",
   597  		providerId: "juju-space-myspace",
   598  		subnets: network.SubnetInfos{
   599  			{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", AvailabilityZones: []string{"az1"}},
   600  		},
   601  	}
   602  	s.env.spaceInfo = &environs.ProviderSpaceInfo{
   603  		SpaceInfo: network.SpaceInfo{
   604  			ID:         "1",
   605  			Name:       "myspace",
   606  			ProviderId: "juju-space-myspace",
   607  			Subnets: []network.SubnetInfo{{
   608  				CIDR:              "4.3.2.0/24",
   609  				ProviderId:        "juju-subnet-1",
   610  				AvailabilityZones: []string{"az1"},
   611  			}},
   612  		},
   613  	}
   614  
   615  	user := names.NewUserTag("someone")
   616  	s.authorizer.Tag = user
   617  	s.mockState.users[user.Name()] = &mockUser{user.Name()}
   618  	_ = s.mockState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-test-uuid"), user, permission.ReadAccess)
   619  
   620  	anotherState := &mockState{
   621  		modelUUID:   "uuid2",
   622  		users:       make(map[string]applicationoffers.User),
   623  		accessPerms: make(map[offerAccess]permission.Access),
   624  		spaces:      make(map[string]applicationoffers.Space),
   625  		model:       anotherModel,
   626  	}
   627  	anotherState.applications = map[string]crossmodel.Application{
   628  		"testagain": &mockApplication{
   629  			charm: ch, curl: "ch:mysql-2", bindings: map[string]string{"db2": "anotherspace"}},
   630  	}
   631  	anotherState.spaces["anotherspace"] = &mockSpace{
   632  		name:       "anotherspace",
   633  		providerId: "juju-space-myspace",
   634  		subnets: network.SubnetInfos{
   635  			{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", AvailabilityZones: []string{"az1"}},
   636  		},
   637  	}
   638  	anotherState.users[user.Name()] = &mockUser{user.Name()}
   639  	_ = anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-testagain-uuid"), user, permission.ConsumeAccess)
   640  	s.mockStatePool.st["uuid2"] = anotherState
   641  
   642  	found, err := s.api.ApplicationOffers(filter)
   643  	c.Assert(err, jc.ErrorIsNil)
   644  	var results []params.ApplicationOfferAdminDetailsV5
   645  	for _, r := range found.Results {
   646  		c.Assert(r.Error, gc.IsNil)
   647  		results = append(results, *r.Result)
   648  	}
   649  	c.Assert(results, jc.DeepEquals, []params.ApplicationOfferAdminDetailsV5{
   650  		{
   651  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   652  				SourceModelTag:         testing.ModelTag.String(),
   653  				ApplicationDescription: "description",
   654  				OfferName:              "hosted-" + name,
   655  				OfferUUID:              "hosted-" + name + "-uuid",
   656  				OfferURL:               url,
   657  				Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
   658  				Users: []params.OfferUserDetails{
   659  					{UserName: "someone", DisplayName: "someone", Access: "read"},
   660  				},
   661  			},
   662  		}, {
   663  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   664  				SourceModelTag:         "model-uuid2",
   665  				ApplicationDescription: "description2",
   666  				OfferName:              "hosted-" + name2,
   667  				OfferUUID:              "hosted-" + name2 + "-uuid",
   668  				OfferURL:               url2,
   669  				Endpoints:              []params.RemoteEndpoint{{Name: "db2"}},
   670  				Users: []params.OfferUserDetails{
   671  					{UserName: "someone", DisplayName: "someone", Access: "consume"},
   672  				}},
   673  		},
   674  	})
   675  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall)
   676  }
   677  
   678  func (s *applicationOffersSuite) assertFind(c *gc.C, expected []params.ApplicationOfferAdminDetailsV5) {
   679  	filter := params.OfferFilters{
   680  		Filters: []params.OfferFilter{
   681  			{
   682  				OfferName: "hosted-db2",
   683  				Endpoints: []params.EndpointFilterAttributes{{
   684  					Interface: "db2",
   685  				}},
   686  			},
   687  		},
   688  	}
   689  	found, err := s.api.FindApplicationOffers(filter)
   690  	c.Assert(err, jc.ErrorIsNil)
   691  	c.Assert(found, jc.DeepEquals, params.QueryApplicationOffersResultsV5{
   692  		Results: expected,
   693  	})
   694  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
   695  	if len(expected) == 0 {
   696  		return
   697  	}
   698  }
   699  
   700  func (s *applicationOffersSuite) TestFind(c *gc.C) {
   701  	offerUUID := s.setupOffers(c, "", true)
   702  	s.authorizer.Tag = names.NewUserTag("admin")
   703  	expected := []params.ApplicationOfferAdminDetailsV5{
   704  		{
   705  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   706  				SourceModelTag:         testing.ModelTag.String(),
   707  				ApplicationDescription: "description",
   708  				OfferName:              "hosted-db2",
   709  				OfferUUID:              offerUUID,
   710  				OfferURL:               "fred@external/prod.hosted-db2",
   711  				Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
   712  				Users: []params.OfferUserDetails{
   713  					{UserName: "admin", DisplayName: "", Access: "admin"},
   714  				}},
   715  			ApplicationName: "test",
   716  			CharmURL:        "ch:db2-2",
   717  			Connections: []params.OfferConnection{{
   718  				SourceModelTag: "model-deadbeef-0bad-400d-8000-4b1d0d06f00d",
   719  				RelationId:     1, Username: "fred@external", Endpoint: "db",
   720  				Status:         params.EntityStatus{Status: "joined"},
   721  				IngressSubnets: []string{"192.168.1.0/32", "10.0.0.0/8"},
   722  			}},
   723  		},
   724  	}
   725  	s.assertFind(c, expected)
   726  }
   727  
   728  func (s *applicationOffersSuite) TestFindNoPermission(c *gc.C) {
   729  	s.mockState.users["someone"] = &mockUser{"someone"}
   730  	user := names.NewUserTag("someone")
   731  	offer := names.NewApplicationOfferTag(utils.MustNewUUID().String())
   732  	err := s.mockState.CreateOfferAccess(offer, user, permission.NoAccess)
   733  	c.Assert(err, jc.ErrorIsNil)
   734  
   735  	s.setupOffers(c, "", true)
   736  	s.authorizer.Tag = names.NewUserTag("someone")
   737  	s.assertFind(c, []params.ApplicationOfferAdminDetailsV5{})
   738  }
   739  
   740  func (s *applicationOffersSuite) TestFindPermission(c *gc.C) {
   741  	offerUUID := s.setupOffers(c, "", true)
   742  	user := names.NewUserTag("someone")
   743  	s.authorizer.Tag = user
   744  	expected := []params.ApplicationOfferAdminDetailsV5{
   745  		{
   746  			ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   747  				SourceModelTag:         testing.ModelTag.String(),
   748  				ApplicationDescription: "description",
   749  				OfferName:              "hosted-db2",
   750  				OfferUUID:              offerUUID,
   751  				OfferURL:               "fred@external/prod.hosted-db2",
   752  				Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
   753  				Users: []params.OfferUserDetails{
   754  					{UserName: "someone", DisplayName: "someone", Access: "read"},
   755  				}},
   756  		},
   757  	}
   758  	s.mockState.users[user.Name()] = &mockUser{user.Name()}
   759  	_ = s.mockState.CreateOfferAccess(names.NewApplicationOfferTag(offerUUID), user, permission.ReadAccess)
   760  	s.assertFind(c, expected)
   761  }
   762  
   763  func (s *applicationOffersSuite) TestFindFiltersRequireModel(c *gc.C) {
   764  	s.setupOffers(c, "", true)
   765  	filter := params.OfferFilters{
   766  		Filters: []params.OfferFilter{
   767  			{
   768  				OfferName:       "hosted-db2",
   769  				ApplicationName: "test",
   770  			}, {
   771  				OfferName:       "hosted-mysql",
   772  				ApplicationName: "test",
   773  			},
   774  		},
   775  	}
   776  	_, err := s.api.FindApplicationOffers(filter)
   777  	c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name")
   778  }
   779  
   780  func (s *applicationOffersSuite) TestFindRequiresFilter(c *gc.C) {
   781  	s.setupOffers(c, "", true)
   782  	_, err := s.api.FindApplicationOffers(params.OfferFilters{})
   783  	c.Assert(err, gc.ErrorMatches, "at least one offer filter is required")
   784  }
   785  
   786  func (s *applicationOffersSuite) TestFindMulti(c *gc.C) {
   787  	oneOfferUUID := utils.MustNewUUID().String()
   788  	twoOfferUUID := utils.MustNewUUID().String()
   789  	db2Offer := jujucrossmodel.ApplicationOffer{
   790  		OfferName:              "hosted-db2",
   791  		OfferUUID:              oneOfferUUID,
   792  		ApplicationName:        "db2",
   793  		ApplicationDescription: "db2 description",
   794  		Endpoints:              map[string]charm.Relation{"db": {Name: "db2"}},
   795  	}
   796  	mysqlOffer := jujucrossmodel.ApplicationOffer{
   797  		OfferName:              "hosted-mysql",
   798  		OfferUUID:              twoOfferUUID,
   799  		ApplicationName:        "mysql",
   800  		ApplicationDescription: "mysql description",
   801  		Endpoints:              map[string]charm.Relation{"db": {Name: "mysql"}},
   802  	}
   803  	postgresqlOffer := jujucrossmodel.ApplicationOffer{
   804  		OfferName:              "hosted-postgresql",
   805  		OfferUUID:              "hosted-postgresql-uuid",
   806  		ApplicationName:        "postgresql",
   807  		ApplicationDescription: "postgresql description",
   808  		Endpoints:              map[string]charm.Relation{"db": {Name: "postgresql"}},
   809  	}
   810  	// Include an offer with bad data to ensure it is ignored.
   811  	offerAppNotFound := jujucrossmodel.ApplicationOffer{
   812  		OfferName:       "badoffer",
   813  		ApplicationName: "missing",
   814  	}
   815  
   816  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
   817  		var result []jujucrossmodel.ApplicationOffer
   818  		for _, f := range filters {
   819  			switch f.OfferName {
   820  			case "hosted-db2":
   821  				result = append(result, db2Offer)
   822  			case "hosted-mysql":
   823  				result = append(result, mysqlOffer)
   824  			case "hosted-postgresql":
   825  				result = append(result, postgresqlOffer)
   826  			default:
   827  				result = append(result, offerAppNotFound)
   828  			}
   829  		}
   830  		return result, nil
   831  	}
   832  	ch := &mockCharm{meta: &charm.Meta{Description: "A pretty popular blog engine"}}
   833  	s.mockState.applications = map[string]crossmodel.Application{
   834  		"db2": &mockApplication{
   835  			name:  "db2",
   836  			charm: ch,
   837  			curl:  "ch:db2-2",
   838  			bindings: map[string]string{
   839  				"db2": "myspace",
   840  			},
   841  		},
   842  	}
   843  	s.mockState.model = &mockModel{
   844  		uuid:      testing.ModelTag.Id(),
   845  		name:      "prod",
   846  		owner:     "fred@external",
   847  		modelType: state.ModelTypeIAAS,
   848  	}
   849  	s.mockState.spaces["myspace"] = &mockSpace{
   850  		name:       "myspace",
   851  		providerId: "juju-space-myspace",
   852  		subnets: network.SubnetInfos{
   853  			{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", AvailabilityZones: []string{"az1"}},
   854  		},
   855  	}
   856  	s.env.spaceInfo = &environs.ProviderSpaceInfo{
   857  		SpaceInfo: network.SpaceInfo{
   858  			ID:         "1",
   859  			Name:       "myspace",
   860  			ProviderId: "juju-space-myspace",
   861  			Subnets: []network.SubnetInfo{{
   862  				CIDR:              "4.3.2.0/24",
   863  				ProviderId:        "juju-subnet-1",
   864  				AvailabilityZones: []string{"az1"},
   865  			}},
   866  		},
   867  	}
   868  
   869  	user := names.NewUserTag("someone")
   870  	s.authorizer.Tag = user
   871  	s.mockState.users[user.Name()] = &mockUser{user.Name()}
   872  	_ = s.mockState.CreateOfferAccess(names.NewApplicationOfferTag(oneOfferUUID), user, permission.ConsumeAccess)
   873  
   874  	anotherState := &mockState{
   875  		modelUUID:   "uuid2",
   876  		users:       make(map[string]applicationoffers.User),
   877  		accessPerms: make(map[offerAccess]permission.Access),
   878  		spaces:      make(map[string]applicationoffers.Space),
   879  	}
   880  	s.mockStatePool.st["uuid2"] = anotherState
   881  	anotherState.applications = map[string]crossmodel.Application{
   882  		"mysql": &mockApplication{
   883  			name:  "mysql",
   884  			charm: ch,
   885  			curl:  "ch:mysql-2",
   886  			bindings: map[string]string{
   887  				"mysql": "anotherspace",
   888  			},
   889  		},
   890  		"postgresql": &mockApplication{
   891  			charm: ch,
   892  			curl:  "ch:postgresql-2",
   893  			bindings: map[string]string{
   894  				"postgresql": "anotherspace",
   895  			},
   896  		},
   897  	}
   898  	anotherState.spaces["anotherspace"] = &mockSpace{
   899  		name:       "anotherspace",
   900  		providerId: "juju-space-anotherspace",
   901  		subnets: network.SubnetInfos{
   902  			{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", AvailabilityZones: []string{"az1"}},
   903  		},
   904  	}
   905  	anotherState.model = &mockModel{
   906  		uuid:      "uuid2",
   907  		name:      "another",
   908  		owner:     "mary",
   909  		modelType: state.ModelTypeIAAS,
   910  	}
   911  	s.mockState.relations["hosted-mysql:server wordpress:db"] = &mockRelation{
   912  		id: 1,
   913  		endpoint: state.Endpoint{
   914  			ApplicationName: "mysql",
   915  			Relation: charm.Relation{
   916  				Name:      "server",
   917  				Interface: "mysql",
   918  				Role:      "provider",
   919  			},
   920  		},
   921  	}
   922  	s.mockState.connections = []applicationoffers.OfferConnection{
   923  		&mockOfferConnection{
   924  			username:    "fred@external",
   925  			modelUUID:   testing.ModelTag.Id(),
   926  			relationKey: "hosted-db2:db wordpress:db",
   927  			relationId:  1,
   928  		},
   929  	}
   930  	anotherState.users[user.Name()] = &mockUser{user.Name()}
   931  	_ = anotherState.CreateOfferAccess(names.NewApplicationOfferTag(twoOfferUUID), user, permission.ReadAccess)
   932  	_ = anotherState.CreateOfferAccess(names.NewApplicationOfferTag("hosted-postgresql-uuid"), user, permission.AdminAccess)
   933  
   934  	s.mockState.allmodels = []applicationoffers.Model{
   935  		s.mockState.model,
   936  		anotherState.model,
   937  	}
   938  
   939  	filter := params.OfferFilters{
   940  		Filters: []params.OfferFilter{
   941  			{
   942  				OfferName: "hosted-db2",
   943  				OwnerName: "fred@external",
   944  				ModelName: "prod",
   945  			},
   946  			{
   947  				OfferName: "hosted-mysql",
   948  				OwnerName: "mary",
   949  				ModelName: "another",
   950  			},
   951  			{
   952  				OfferName: "hosted-postgresql",
   953  				OwnerName: "mary",
   954  				ModelName: "another",
   955  			},
   956  			{
   957  				OfferName: "badoffer",
   958  				OwnerName: "mary",
   959  				ModelName: "another",
   960  			},
   961  		},
   962  	}
   963  	found, err := s.api.FindApplicationOffers(filter)
   964  	c.Assert(err, jc.ErrorIsNil)
   965  	c.Assert(found, jc.DeepEquals, params.QueryApplicationOffersResultsV5{
   966  		[]params.ApplicationOfferAdminDetailsV5{
   967  			{
   968  				ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   969  					SourceModelTag:         testing.ModelTag.String(),
   970  					ApplicationDescription: "db2 description",
   971  					OfferName:              "hosted-db2",
   972  					OfferUUID:              oneOfferUUID,
   973  					OfferURL:               "fred@external/prod.hosted-db2",
   974  					Endpoints: []params.RemoteEndpoint{
   975  						{Name: "db"},
   976  					},
   977  					Users: []params.OfferUserDetails{
   978  						{UserName: "someone", DisplayName: "someone", Access: "consume"},
   979  					},
   980  				},
   981  			},
   982  			{
   983  				ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   984  					SourceModelTag:         "model-uuid2",
   985  					ApplicationDescription: "mysql description",
   986  					OfferName:              "hosted-mysql",
   987  					OfferUUID:              twoOfferUUID,
   988  					OfferURL:               "mary/another.hosted-mysql",
   989  					Endpoints: []params.RemoteEndpoint{
   990  						{Name: "db"},
   991  					},
   992  					Users: []params.OfferUserDetails{
   993  						{UserName: "someone", DisplayName: "someone", Access: "read"},
   994  					},
   995  				},
   996  			},
   997  			{
   998  				ApplicationOfferDetailsV5: params.ApplicationOfferDetailsV5{
   999  					SourceModelTag:         "model-uuid2",
  1000  					ApplicationDescription: "postgresql description",
  1001  					OfferName:              "hosted-postgresql",
  1002  					OfferUUID:              "hosted-postgresql-uuid",
  1003  					OfferURL:               "mary/another.hosted-postgresql",
  1004  					Endpoints:              []params.RemoteEndpoint{{Name: "db"}},
  1005  					Users: []params.OfferUserDetails{
  1006  						{UserName: "someone", DisplayName: "someone", Access: "admin"},
  1007  					},
  1008  				},
  1009  				CharmURL: "ch:postgresql-2",
  1010  			},
  1011  		},
  1012  	})
  1013  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall, listOffersBackendCall)
  1014  }
  1015  
  1016  func (s *applicationOffersSuite) TestFindError(c *gc.C) {
  1017  	filter := params.OfferFilters{
  1018  		Filters: []params.OfferFilter{
  1019  			{
  1020  				OfferName:       "hosted-db2",
  1021  				ApplicationName: "test",
  1022  			},
  1023  		},
  1024  	}
  1025  	msg := "fail"
  1026  
  1027  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
  1028  		return nil, errors.New(msg)
  1029  	}
  1030  	s.mockState.model = &mockModel{uuid: testing.ModelTag.Id(), name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
  1031  
  1032  	_, err := s.api.FindApplicationOffers(filter)
  1033  	c.Assert(err, gc.ErrorMatches, fmt.Sprintf(".*%v.*", msg))
  1034  	s.applicationOffers.CheckCallNames(c, listOffersBackendCall)
  1035  }
  1036  
  1037  func (s *applicationOffersSuite) TestFindMissingModelInMultipleFilters(c *gc.C) {
  1038  	filter := params.OfferFilters{
  1039  		Filters: []params.OfferFilter{
  1040  			{
  1041  				OfferName:       "hosted-db2",
  1042  				ApplicationName: "test",
  1043  			},
  1044  			{
  1045  				OfferName:       "hosted-mysql",
  1046  				ApplicationName: "test",
  1047  			},
  1048  		},
  1049  	}
  1050  
  1051  	s.applicationOffers.listOffers = func(filters ...jujucrossmodel.ApplicationOfferFilter) ([]jujucrossmodel.ApplicationOffer, error) {
  1052  		panic("should not be called")
  1053  	}
  1054  
  1055  	_, err := s.api.FindApplicationOffers(filter)
  1056  	c.Assert(err, gc.ErrorMatches, "application offer filter must specify a model name")
  1057  	s.applicationOffers.CheckCallNames(c)
  1058  }
  1059  
  1060  type consumeSuite struct {
  1061  	baseSuite
  1062  	api *applicationoffers.OffersAPIv5
  1063  }
  1064  
  1065  var _ = gc.Suite(&consumeSuite{})
  1066  
  1067  func (s *consumeSuite) SetUpTest(c *gc.C) {
  1068  	s.baseSuite.SetUpTest(c)
  1069  	s.bakery = &mockBakeryService{caveats: make(map[string][]checkers.Caveat)}
  1070  	getApplicationOffers := func(st interface{}) jujucrossmodel.ApplicationOffers {
  1071  		return &mockApplicationOffers{st: st.(*mockState)}
  1072  	}
  1073  
  1074  	resources := common.NewResources()
  1075  	err := resources.RegisterNamed("dataDir", common.StringResource(c.MkDir()))
  1076  	c.Assert(err, jc.ErrorIsNil)
  1077  
  1078  	getEnviron := func(modelUUID string) (environs.Environ, error) {
  1079  		return s.env, nil
  1080  	}
  1081  	thirdPartyKey := bakery.MustGenerateKey()
  1082  	s.authContext, err = crossmodel.NewAuthContext(
  1083  		s.mockState, thirdPartyKey,
  1084  		crossmodel.NewOfferBakeryForTest(s.bakery, clock.WallClock),
  1085  	)
  1086  	c.Assert(err, jc.ErrorIsNil)
  1087  	api, err := applicationoffers.CreateOffersAPI(
  1088  		getApplicationOffers, getEnviron, getFakeControllerInfo,
  1089  		s.mockState, s.mockStatePool, s.authorizer, resources, s.authContext,
  1090  	)
  1091  	c.Assert(err, jc.ErrorIsNil)
  1092  	s.api = api
  1093  }
  1094  
  1095  func (s *consumeSuite) TestConsumeDetailsRejectsEndpoints(c *gc.C) {
  1096  	results, err := s.api.GetConsumeDetails(params.ConsumeOfferDetailsArg{
  1097  		OfferURLs: params.OfferURLs{
  1098  			OfferURLs: []string{"fred@external/prod.application:db"},
  1099  		}})
  1100  	c.Assert(err, jc.ErrorIsNil)
  1101  	c.Assert(results.Results, gc.HasLen, 1)
  1102  	c.Assert(results.Results[0].Error != nil, jc.IsTrue)
  1103  	c.Assert(results.Results[0].Error.Message, gc.Equals, `saas application "fred@external/prod.application:db" shouldn't include endpoint`)
  1104  }
  1105  
  1106  func (s *consumeSuite) TestConsumeDetailsNoPermission(c *gc.C) {
  1107  	offerUUID := s.setupOffer()
  1108  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1109  	st.(*mockState).users["someone"] = &mockUser{"someone"}
  1110  	apiUser := names.NewUserTag("someone")
  1111  	offer := names.NewApplicationOfferTag(offerUUID)
  1112  	err := st.CreateOfferAccess(offer, apiUser, permission.NoAccess)
  1113  	c.Assert(err, jc.ErrorIsNil)
  1114  
  1115  	s.authorizer.Tag = apiUser
  1116  	results, err := s.api.GetConsumeDetails(params.ConsumeOfferDetailsArg{
  1117  		OfferURLs: params.OfferURLs{
  1118  			OfferURLs: []string{"fred@external/prod.hosted-mysql"},
  1119  		}})
  1120  	c.Assert(err, jc.ErrorIsNil)
  1121  	expected := []params.ConsumeOfferDetailsResult{{
  1122  		Error: apiservererrors.ServerError(errors.NotFoundf("application offer %q", "fred@external/prod.hosted-mysql")),
  1123  	}}
  1124  	c.Assert(results.Results, jc.DeepEquals, expected)
  1125  }
  1126  
  1127  func (s *consumeSuite) TestConsumeDetailsWithPermission(c *gc.C) {
  1128  	s.assertConsumeDetailsWithPermission(c,
  1129  		func(authorizer *apiservertesting.FakeAuthorizer, apiUser names.UserTag) string {
  1130  			authorizer.HasConsumeTag = apiUser
  1131  			authorizer.Tag = apiUser
  1132  			return ""
  1133  		},
  1134  	)
  1135  }
  1136  
  1137  func (s *consumeSuite) TestConsumeDetailsSpecifiedUserHasPermission(c *gc.C) {
  1138  	s.assertConsumeDetailsWithPermission(c,
  1139  		func(authorizer *apiservertesting.FakeAuthorizer, apiUser names.UserTag) string {
  1140  			authorizer.HasConsumeTag = apiUser
  1141  			controllerAdmin := names.NewUserTag("superuser-joe")
  1142  			authorizer.Tag = controllerAdmin
  1143  			return apiUser.String()
  1144  		},
  1145  	)
  1146  }
  1147  
  1148  func (s *consumeSuite) TestConsumeDetailsSpecifiedUserHasNoPermissionButSuperUserLoggedIn(c *gc.C) {
  1149  	s.assertConsumeDetailsWithPermission(c,
  1150  		func(authorizer *apiservertesting.FakeAuthorizer, apiUser names.UserTag) string {
  1151  			controllerAdmin := names.NewUserTag("superuser-joe")
  1152  			authorizer.Tag = controllerAdmin
  1153  			return apiUser.String()
  1154  		},
  1155  	)
  1156  }
  1157  
  1158  func (s *consumeSuite) assertConsumeDetailsWithPermission(
  1159  	c *gc.C, configAuthorizer func(*apiservertesting.FakeAuthorizer, names.UserTag) string,
  1160  ) {
  1161  	offerUUID := s.setupOffer()
  1162  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1163  	st.(*mockState).users["someone"] = &mockUser{"someone"}
  1164  	apiUser := names.NewUserTag("someone")
  1165  
  1166  	userTag := configAuthorizer(s.authorizer, apiUser)
  1167  	offer := names.NewApplicationOfferTag(offerUUID)
  1168  	err := st.CreateOfferAccess(offer, apiUser, permission.ConsumeAccess)
  1169  	c.Assert(err, jc.ErrorIsNil)
  1170  
  1171  	results, err := s.api.GetConsumeDetails(params.ConsumeOfferDetailsArg{
  1172  		UserTag: userTag,
  1173  		OfferURLs: params.OfferURLs{
  1174  			OfferURLs: []string{"fred@external/prod.hosted-mysql"},
  1175  		}},
  1176  	)
  1177  	c.Assert(err, jc.ErrorIsNil)
  1178  	c.Assert(results.Results, gc.HasLen, 1)
  1179  	c.Assert(results.Results[0].Error, gc.IsNil)
  1180  	c.Assert(results.Results[0].Offer, jc.DeepEquals, &params.ApplicationOfferDetailsV5{
  1181  		SourceModelTag:         "model-deadbeef-0bad-400d-8000-4b1d0d06f00d",
  1182  		OfferURL:               "fred@external/prod.hosted-mysql",
  1183  		OfferName:              "hosted-mysql",
  1184  		OfferUUID:              offerUUID,
  1185  		ApplicationDescription: "a database",
  1186  		Endpoints:              []params.RemoteEndpoint{{Name: "server", Role: "provider", Interface: "mysql"}},
  1187  		Users: []params.OfferUserDetails{
  1188  			{UserName: "someone", DisplayName: "someone", Access: "consume"},
  1189  		},
  1190  	})
  1191  	c.Assert(results.Results[0].ControllerInfo, jc.DeepEquals, &params.ExternalControllerInfo{
  1192  		ControllerTag: testing.ControllerTag.String(),
  1193  		Addrs:         []string{"192.168.1.1:17070"},
  1194  		CACert:        testing.CACert,
  1195  	})
  1196  	c.Assert(results.Results[0].Macaroon.Id(), jc.DeepEquals, []byte("id"))
  1197  
  1198  	cav := s.bakery.caveats[string(results.Results[0].Macaroon.Id())]
  1199  	c.Check(cav, gc.HasLen, 4)
  1200  	c.Check(strings.HasPrefix(cav[0].Condition, "time-before "), jc.IsTrue)
  1201  	c.Check(cav[1].Condition, gc.Equals, "declared source-model-uuid deadbeef-0bad-400d-8000-4b1d0d06f00d")
  1202  	c.Check(cav[2].Condition, gc.Equals, "declared username someone")
  1203  	c.Check(cav[3].Condition, gc.Equals, "declared offer-uuid "+offerUUID)
  1204  }
  1205  
  1206  func (s *consumeSuite) TestConsumeDetailsNonAdminSpecifiedUser(c *gc.C) {
  1207  	offerUUID := s.setupOffer()
  1208  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1209  	st.(*mockState).users["someone"] = &mockUser{"someone"}
  1210  	apiUser := names.NewUserTag("someone")
  1211  	offer := names.NewApplicationOfferTag(offerUUID)
  1212  	err := st.CreateOfferAccess(offer, apiUser, permission.ConsumeAccess)
  1213  	c.Assert(err, jc.ErrorIsNil)
  1214  
  1215  	s.authorizer.Tag = names.NewUserTag("joe-blow")
  1216  	_, err = s.api.GetConsumeDetails(params.ConsumeOfferDetailsArg{
  1217  		UserTag: apiUser.String(),
  1218  		OfferURLs: params.OfferURLs{
  1219  			OfferURLs: []string{"fred@external/prod.hosted-mysql"},
  1220  		}})
  1221  	c.Assert(errors.Is(err, apiservererrors.ErrPerm), jc.IsTrue)
  1222  }
  1223  
  1224  func (s *consumeSuite) TestConsumeDetailsDefaultEndpoint(c *gc.C) {
  1225  	offerUUID := s.setupOffer()
  1226  
  1227  	st := s.mockStatePool.st[testing.ModelTag.Id()].(*mockState)
  1228  	st.users["someone"] = &mockUser{"someone"}
  1229  	delete(st.applications["mysql"].(*mockApplication).bindings, "database")
  1230  
  1231  	// Add a default endpoint for the application.
  1232  	st.spaces["default-endpoint"] = &mockSpace{
  1233  		name: "default-endpoint",
  1234  	}
  1235  	st.applications["mysql"].(*mockApplication).bindings[""] = "default-endpoint"
  1236  
  1237  	apiUser := names.NewUserTag("someone")
  1238  	offer := names.NewApplicationOfferTag(offerUUID)
  1239  	err := st.CreateOfferAccess(offer, apiUser, permission.ConsumeAccess)
  1240  	c.Assert(err, jc.ErrorIsNil)
  1241  
  1242  	s.authorizer.Tag = apiUser
  1243  	s.authorizer.HasConsumeTag = apiUser
  1244  	results, err := s.api.GetConsumeDetails(params.ConsumeOfferDetailsArg{
  1245  		OfferURLs: params.OfferURLs{
  1246  			OfferURLs: []string{"fred@external/prod.hosted-mysql"},
  1247  		}},
  1248  	)
  1249  
  1250  	c.Assert(err, jc.ErrorIsNil)
  1251  	c.Assert(results.Results, gc.HasLen, 1)
  1252  	c.Assert(results.Results[0].Error, gc.IsNil)
  1253  	c.Assert(results.Results[0].Offer, jc.DeepEquals, &params.ApplicationOfferDetailsV5{
  1254  		SourceModelTag:         "model-deadbeef-0bad-400d-8000-4b1d0d06f00d",
  1255  		OfferURL:               "fred@external/prod.hosted-mysql",
  1256  		OfferName:              "hosted-mysql",
  1257  		OfferUUID:              offerUUID,
  1258  		ApplicationDescription: "a database",
  1259  		Endpoints:              []params.RemoteEndpoint{{Name: "server", Role: "provider", Interface: "mysql"}},
  1260  		Users: []params.OfferUserDetails{
  1261  			{UserName: "someone", DisplayName: "someone", Access: "consume"},
  1262  		},
  1263  	})
  1264  }
  1265  
  1266  func (s *consumeSuite) setupOffer() string {
  1267  	modelUUID := testing.ModelTag.Id()
  1268  	offerName := "hosted-mysql"
  1269  
  1270  	model := &mockModel{uuid: modelUUID, name: "prod", owner: "fred@external", modelType: state.ModelTypeIAAS}
  1271  	s.mockState.allmodels = []applicationoffers.Model{model}
  1272  	st := &mockState{
  1273  		modelUUID:         modelUUID,
  1274  		model:             model,
  1275  		applications:      make(map[string]crossmodel.Application),
  1276  		applicationOffers: make(map[string]jujucrossmodel.ApplicationOffer),
  1277  		users:             make(map[string]applicationoffers.User),
  1278  		accessPerms:       make(map[offerAccess]permission.Access),
  1279  		spaces:            make(map[string]applicationoffers.Space),
  1280  		relations:         make(map[string]crossmodel.Relation),
  1281  	}
  1282  	s.mockStatePool.st[modelUUID] = st
  1283  	anOffer := jujucrossmodel.ApplicationOffer{
  1284  		ApplicationName:        "mysql",
  1285  		ApplicationDescription: "a database",
  1286  		OfferName:              offerName,
  1287  		OfferUUID:              utils.MustNewUUID().String(),
  1288  		Endpoints: map[string]charm.Relation{
  1289  			"server": {Name: "database", Interface: "mysql", Role: "provider", Scope: "global"}},
  1290  	}
  1291  	st.applicationOffers[offerName] = anOffer
  1292  	st.applications["mysql"] = &mockApplication{
  1293  		name:     "mysql",
  1294  		charm:    &mockCharm{meta: &charm.Meta{Description: "A pretty popular database"}},
  1295  		bindings: map[string]string{"database": "myspace"},
  1296  		endpoints: []state.Endpoint{
  1297  			{Relation: charm.Relation{Name: "juju-info", Role: "provider", Interface: "juju-info", Limit: 0, Scope: "global"}},
  1298  			{Relation: charm.Relation{Name: "server", Role: "provider", Interface: "mysql", Limit: 0, Scope: "global"}},
  1299  			{Relation: charm.Relation{Name: "server-admin", Role: "provider", Interface: "mysql-root", Limit: 0, Scope: "global"}}},
  1300  	}
  1301  	st.spaces["myspace"] = &mockSpace{
  1302  		name:       "myspace",
  1303  		providerId: "juju-space-myspace",
  1304  		subnets: network.SubnetInfos{
  1305  			{CIDR: "4.3.2.0/24", ProviderId: "juju-subnet-1", AvailabilityZones: []string{"az1"}},
  1306  		},
  1307  	}
  1308  	s.env.spaceInfo = &environs.ProviderSpaceInfo{
  1309  		SpaceInfo: network.SpaceInfo{
  1310  			ID:         "1",
  1311  			Name:       "myspace",
  1312  			ProviderId: "juju-space-myspace",
  1313  			Subnets: []network.SubnetInfo{{
  1314  				CIDR:              "4.3.2.0/24",
  1315  				ProviderId:        "juju-subnet-1",
  1316  				AvailabilityZones: []string{"az1"},
  1317  			}},
  1318  		},
  1319  	}
  1320  	return anOffer.OfferUUID
  1321  }
  1322  
  1323  func (s *consumeSuite) TestRemoteApplicationInfo(c *gc.C) {
  1324  	offerUUID := s.setupOffer()
  1325  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1326  	st.(*mockState).users["foobar"] = &mockUser{"foobar"}
  1327  
  1328  	// Give user permission to see the offer.
  1329  	user := names.NewUserTag("foobar")
  1330  	offer := names.NewApplicationOfferTag(offerUUID)
  1331  	err := st.CreateOfferAccess(offer, user, permission.ConsumeAccess)
  1332  	c.Assert(err, jc.ErrorIsNil)
  1333  
  1334  	s.authorizer.Tag = user
  1335  	results, err := s.api.RemoteApplicationInfo(params.OfferURLs{
  1336  		OfferURLs: []string{"fred@external/prod.hosted-mysql", "fred@external/prod.unknown"},
  1337  	})
  1338  	c.Assert(err, jc.ErrorIsNil)
  1339  	c.Assert(results.Results, gc.HasLen, 2)
  1340  	c.Assert(results.Results[0].Error, gc.IsNil)
  1341  	c.Assert(results.Results, jc.DeepEquals, []params.RemoteApplicationInfoResult{
  1342  		{Result: &params.RemoteApplicationInfo{
  1343  			ModelTag:         testing.ModelTag.String(),
  1344  			Name:             "hosted-mysql",
  1345  			Description:      "a database",
  1346  			OfferURL:         "fred@external/prod.hosted-mysql",
  1347  			SourceModelLabel: "prod",
  1348  			IconURLPath:      "rest/1.0/remote-application/hosted-mysql/icon",
  1349  			Endpoints: []params.RemoteEndpoint{
  1350  				{Name: "server", Role: "provider", Interface: "mysql"}},
  1351  		}},
  1352  		{
  1353  			Error: &params.Error{Message: `application offer "unknown" not found`, Code: "not found"},
  1354  		},
  1355  	})
  1356  }
  1357  
  1358  func (s *consumeSuite) TestDestroyOffersNoForceV2(c *gc.C) {
  1359  	s.assertDestroyOffersNoForce(c, s.api)
  1360  }
  1361  
  1362  type destroyOffers interface {
  1363  	DestroyOffers(args params.DestroyApplicationOffers) (params.ErrorResults, error)
  1364  }
  1365  
  1366  func (s *consumeSuite) assertDestroyOffersNoForce(c *gc.C, api destroyOffers) {
  1367  	s.setupOffer()
  1368  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1369  	st.(*mockState).users["foobar"] = &mockUser{"foobar"}
  1370  	st.(*mockState).connections = []applicationoffers.OfferConnection{
  1371  		&mockOfferConnection{
  1372  			username:    "fred@external",
  1373  			modelUUID:   testing.ModelTag.Id(),
  1374  			relationKey: "hosted-db2:db wordpress:db",
  1375  			relationId:  1,
  1376  		},
  1377  	}
  1378  
  1379  	s.authorizer.Tag = names.NewUserTag("admin")
  1380  	results, err := s.api.DestroyOffers(params.DestroyApplicationOffers{
  1381  		OfferURLs: []string{
  1382  			"fred@external/prod.hosted-mysql"},
  1383  	})
  1384  	c.Assert(err, jc.ErrorIsNil)
  1385  	c.Assert(results.Results, gc.HasLen, 1)
  1386  	c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{
  1387  		{
  1388  			Error: &params.Error{Message: `offer has 1 relations`},
  1389  		},
  1390  	})
  1391  
  1392  	urls := []string{"fred@external/prod.hosted-db2"}
  1393  	filter := params.OfferURLs{urls, bakery.LatestVersion}
  1394  	found, err := s.api.ApplicationOffers(filter)
  1395  	c.Assert(err, jc.ErrorIsNil)
  1396  	c.Assert(found.Results, gc.HasLen, 1)
  1397  	c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred@external/prod.hosted-db2" not found`)
  1398  }
  1399  
  1400  func (s *consumeSuite) TestDestroyOffersForce(c *gc.C) {
  1401  	s.setupOffer()
  1402  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1403  	st.(*mockState).users["foobar"] = &mockUser{"foobar"}
  1404  	st.(*mockState).connections = []applicationoffers.OfferConnection{
  1405  		&mockOfferConnection{
  1406  			username:    "fred@external",
  1407  			modelUUID:   testing.ModelTag.Id(),
  1408  			relationKey: "hosted-db2:db wordpress:db",
  1409  			relationId:  1,
  1410  		},
  1411  	}
  1412  
  1413  	s.authorizer.Tag = names.NewUserTag("admin")
  1414  	results, err := s.api.DestroyOffers(params.DestroyApplicationOffers{
  1415  		Force: true,
  1416  		OfferURLs: []string{
  1417  			"fred@external/prod.hosted-mysql", "fred@external/prod.unknown", "garbage/badmodel.someoffer", "badmodel.someoffer"},
  1418  	})
  1419  	c.Assert(err, jc.ErrorIsNil)
  1420  	c.Assert(results.Results, gc.HasLen, 4)
  1421  	c.Assert(results.Results[0].Error, gc.IsNil)
  1422  	c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{
  1423  		{},
  1424  		{
  1425  			Error: &params.Error{Message: `application offer "unknown" not found`, Code: "not found"},
  1426  		}, {
  1427  			Error: &params.Error{Message: `model "garbage/badmodel" not found`, Code: "not found"},
  1428  		}, {
  1429  			Error: &params.Error{Message: `model "admin/badmodel" not found`, Code: "not found"},
  1430  		},
  1431  	})
  1432  
  1433  	urls := []string{"fred@external/prod.hosted-db2"}
  1434  	filter := params.OfferURLs{urls, bakery.LatestVersion}
  1435  	found, err := s.api.ApplicationOffers(filter)
  1436  	c.Assert(err, jc.ErrorIsNil)
  1437  	c.Assert(found.Results, gc.HasLen, 1)
  1438  	c.Assert(found.Results[0].Error.Error(), gc.Matches, `application offer "fred@external/prod.hosted-db2" not found`)
  1439  }
  1440  
  1441  func (s *consumeSuite) TestDestroyOffersPermission(c *gc.C) {
  1442  	s.setupOffer()
  1443  	s.authorizer.Tag = names.NewUserTag("mary")
  1444  	st := s.mockStatePool.st[testing.ModelTag.Id()]
  1445  	st.(*mockState).users["foobar"] = &mockUser{"foobar"}
  1446  
  1447  	results, err := s.api.DestroyOffers(params.DestroyApplicationOffers{
  1448  		OfferURLs: []string{"fred@external/prod.hosted-mysql"},
  1449  	})
  1450  	c.Assert(err, jc.ErrorIsNil)
  1451  	c.Assert(results.Results, gc.HasLen, 1)
  1452  	c.Assert(results.Results[0].Error, gc.ErrorMatches, apiservererrors.ErrPerm.Error())
  1453  }