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