github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/applicationoffers_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state_test
     5  
     6  import (
     7  	"github.com/juju/charm/v12"
     8  	"github.com/juju/errors"
     9  	"github.com/juju/names/v5"
    10  	jc "github.com/juju/testing/checkers"
    11  	jujutxn "github.com/juju/txn/v3"
    12  	gc "gopkg.in/check.v1"
    13  
    14  	"github.com/juju/juju/core/crossmodel"
    15  	"github.com/juju/juju/core/permission"
    16  	"github.com/juju/juju/core/status"
    17  	"github.com/juju/juju/state"
    18  	statetesting "github.com/juju/juju/state/testing"
    19  	"github.com/juju/juju/testing"
    20  	"github.com/juju/juju/testing/factory"
    21  )
    22  
    23  type applicationOffersSuite struct {
    24  	ConnSuite
    25  	mysql *state.Application
    26  }
    27  
    28  var _ = gc.Suite(&applicationOffersSuite{})
    29  
    30  func (s *applicationOffersSuite) SetUpTest(c *gc.C) {
    31  	s.ConnSuite.SetUpTest(c)
    32  	ch := s.AddTestingCharm(c, "mysql")
    33  	s.mysql = s.AddTestingApplication(c, "mysql", ch)
    34  }
    35  
    36  func (s *applicationOffersSuite) createDefaultOffer(c *gc.C) crossmodel.ApplicationOffer {
    37  	eps := map[string]string{"db": "server", "db-admin": "server-admin"}
    38  	sd := state.NewApplicationOffers(s.State)
    39  	owner := s.Factory.MakeUser(c, nil)
    40  	offerArgs := crossmodel.AddApplicationOfferArgs{
    41  		OfferName:              "hosted-mysql",
    42  		ApplicationName:        "mysql",
    43  		ApplicationDescription: "mysql is a db server",
    44  		Endpoints:              eps,
    45  		Owner:                  owner.Name(),
    46  	}
    47  	offer, err := sd.AddOffer(offerArgs)
    48  	c.Assert(err, jc.ErrorIsNil)
    49  	return *offer
    50  }
    51  
    52  func (s *applicationOffersSuite) TestDetectingOfferConnections(c *gc.C) {
    53  	connected, err := state.ApplicationHasConnectedOffers(s.State, "mysql")
    54  	c.Assert(err, jc.ErrorIsNil)
    55  	c.Check(connected, jc.IsFalse)
    56  
    57  	offer := s.createDefaultOffer(c)
    58  	connected, err = state.ApplicationHasConnectedOffers(s.State, "mysql")
    59  	c.Assert(err, jc.ErrorIsNil)
    60  	c.Check(connected, jc.IsFalse)
    61  
    62  	s.addOfferConnection(c, offer.OfferUUID)
    63  	connected, err = state.ApplicationHasConnectedOffers(s.State, "mysql")
    64  	c.Assert(err, jc.ErrorIsNil)
    65  	c.Check(connected, jc.IsTrue)
    66  }
    67  
    68  func (s *applicationOffersSuite) TestEndpoints(c *gc.C) {
    69  	offer := s.createDefaultOffer(c)
    70  	_, err := state.ApplicationOfferEndpoint(offer, "foo")
    71  	c.Assert(err, gc.ErrorMatches, `relation "foo" on application offer "mysql" not found`)
    72  
    73  	serverEP, err := state.ApplicationOfferEndpoint(offer, "server")
    74  	c.Assert(err, jc.ErrorIsNil)
    75  	c.Assert(serverEP, gc.DeepEquals, state.Endpoint{
    76  		ApplicationName: "mysql",
    77  		Relation: charm.Relation{
    78  			Interface: "mysql",
    79  			Name:      "server",
    80  			Role:      charm.RoleProvider,
    81  			Scope:     charm.ScopeGlobal,
    82  		},
    83  	})
    84  }
    85  
    86  func (s *applicationOffersSuite) TestRemove(c *gc.C) {
    87  	offer := s.createDefaultOffer(c)
    88  	r := s.State.RemoteEntities()
    89  	_, err := r.ExportLocalEntity(names.NewApplicationTag(offer.OfferName))
    90  	c.Assert(err, jc.ErrorIsNil)
    91  
    92  	sd := state.NewApplicationOffers(s.State)
    93  	err = sd.Remove(offer.OfferName, false)
    94  	c.Assert(err, jc.ErrorIsNil)
    95  	_, err = sd.ApplicationOffer(offer.OfferName)
    96  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
    97  
    98  	_, err = r.GetToken(names.NewApplicationTag(offer.OfferName))
    99  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   100  
   101  	userPerms, err := s.State.GetOfferUsers(offer.OfferUUID)
   102  	c.Assert(err, jc.ErrorIsNil)
   103  	c.Assert(userPerms, gc.HasLen, 0)
   104  }
   105  
   106  func (s *applicationOffersSuite) TestAddApplicationOffer(c *gc.C) {
   107  	eps := map[string]string{"db": "server", "db-admin": "server-admin"}
   108  	sd := state.NewApplicationOffers(s.State)
   109  	owner := s.Factory.MakeUser(c, nil)
   110  	args := crossmodel.AddApplicationOfferArgs{
   111  		OfferName:              "hosted-mysql",
   112  		ApplicationName:        "mysql",
   113  		ApplicationDescription: "mysql is a db server",
   114  		Endpoints:              eps,
   115  		Owner:                  owner.Name(),
   116  		HasRead:                []string{"everyone@external"},
   117  	}
   118  	offer, err := sd.AddOffer(args)
   119  	c.Assert(err, jc.ErrorIsNil)
   120  	expectedOffer, err := sd.ApplicationOffer(offer.OfferName)
   121  	c.Assert(err, jc.ErrorIsNil)
   122  	c.Assert(*offer, jc.DeepEquals, *expectedOffer)
   123  
   124  	access, err := s.State.GetOfferAccess(offer.OfferUUID, owner.UserTag())
   125  	c.Assert(err, jc.ErrorIsNil)
   126  	c.Assert(access, gc.Equals, permission.AdminAccess)
   127  
   128  	access, err = s.State.GetOfferAccess(offer.OfferUUID, names.NewUserTag("everyone@external"))
   129  	c.Assert(err, jc.ErrorIsNil)
   130  	c.Assert(access, gc.Equals, permission.ReadAccess)
   131  }
   132  
   133  func (s *applicationOffersSuite) TestAddApplicationOfferInvalidApplication(c *gc.C) {
   134  	sd := state.NewApplicationOffers(s.State)
   135  	owner := s.Factory.MakeUser(c, nil)
   136  	args := crossmodel.AddApplicationOfferArgs{
   137  		OfferName:              "hosted-mysql",
   138  		ApplicationName:        "invalid",
   139  		ApplicationDescription: "mysql is a db server",
   140  		Endpoints:              map[string]string{"db": "server", "db-admin": "server-admin"},
   141  		Owner:                  owner.Name(),
   142  		HasRead:                []string{"everyone@external"},
   143  	}
   144  	_, err := sd.AddOffer(args)
   145  	c.Assert(err, gc.ErrorMatches, `cannot add application offer "hosted-mysql": application "invalid" not found`)
   146  }
   147  
   148  func (s *applicationOffersSuite) TestAddApplicationOfferBadEndpoints(c *gc.C) {
   149  	eps := map[string]string{"db": "server", "db-admin": "admin"}
   150  	sd := state.NewApplicationOffers(s.State)
   151  	owner := s.Factory.MakeUser(c, nil)
   152  	args := crossmodel.AddApplicationOfferArgs{
   153  		OfferName:              "hosted-mysql",
   154  		ApplicationName:        "mysql",
   155  		ApplicationDescription: "mysql is a db server",
   156  		Endpoints:              eps,
   157  		Owner:                  owner.Name(),
   158  	}
   159  	_, err := sd.AddOffer(args)
   160  	c.Assert(err, gc.ErrorMatches, `.*application "mysql" has no "admin" relation`)
   161  
   162  	// Fix the endpoints and try again.
   163  	// There was a bug where this failed so we test it.
   164  	eps = map[string]string{"db": "server", "db-admin": "server-admin"}
   165  	args.Endpoints = eps
   166  	_, err = sd.AddOffer(args)
   167  	c.Assert(err, jc.ErrorIsNil)
   168  }
   169  
   170  func (s *applicationOffersSuite) TestFaillAddApplicationOfferNonGlobalEndpoint(c *gc.C) {
   171  	s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress"))
   172  	// logging-dir is a container scoped relation.
   173  	eps := map[string]string{"logging-dir": "logging-dir"}
   174  	sd := state.NewApplicationOffers(s.State)
   175  	owner := s.Factory.MakeUser(c, nil)
   176  	args := crossmodel.AddApplicationOfferArgs{
   177  		OfferName:       "offer-name",
   178  		ApplicationName: "local-wordpress",
   179  		Endpoints:       eps,
   180  		Owner:           owner.Name(),
   181  	}
   182  	_, err := sd.AddOffer(args)
   183  	c.Assert(err, gc.ErrorMatches, `.*can only offer endpoints with global scope, provided scope "container".*`)
   184  }
   185  
   186  func (s *applicationOffersSuite) TestListOffersNone(c *gc.C) {
   187  	sd := state.NewApplicationOffers(s.State)
   188  	offers, err := sd.ListOffers()
   189  	c.Assert(err, jc.ErrorIsNil)
   190  	c.Assert(len(offers), gc.Equals, 0)
   191  }
   192  
   193  func (s *applicationOffersSuite) createOffer(c *gc.C, name, description string) (crossmodel.ApplicationOffer, string) {
   194  	eps := map[string]string{
   195  		"db": "server",
   196  	}
   197  	sd := state.NewApplicationOffers(s.State)
   198  	owner := s.Factory.MakeUser(c, nil)
   199  	offerArgs := crossmodel.AddApplicationOfferArgs{
   200  		OfferName:              name,
   201  		ApplicationName:        "mysql",
   202  		ApplicationDescription: description,
   203  		Endpoints:              eps,
   204  		Owner:                  owner.Name(),
   205  	}
   206  	offer, err := sd.AddOffer(offerArgs)
   207  	c.Assert(err, jc.ErrorIsNil)
   208  	return *offer, owner.Name()
   209  }
   210  
   211  func (s *applicationOffersSuite) TestApplicationOffer(c *gc.C) {
   212  	sd := state.NewApplicationOffers(s.State)
   213  	expectedOffer := s.createDefaultOffer(c)
   214  	offer, err := sd.ApplicationOffer("hosted-mysql")
   215  	c.Assert(err, jc.ErrorIsNil)
   216  	c.Assert(*offer, jc.DeepEquals, expectedOffer)
   217  }
   218  
   219  func (s *applicationOffersSuite) TestApplicationOfferForUUID(c *gc.C) {
   220  	sd := state.NewApplicationOffers(s.State)
   221  	expectedOffer := s.createDefaultOffer(c)
   222  	offer, err := sd.ApplicationOfferForUUID(expectedOffer.OfferUUID)
   223  	c.Assert(err, jc.ErrorIsNil)
   224  	c.Assert(*offer, jc.DeepEquals, expectedOffer)
   225  }
   226  
   227  func (s *applicationOffersSuite) TestAllApplicationOffers(c *gc.C) {
   228  	eps := map[string]string{"db": "server", "db-admin": "server-admin"}
   229  	sd := state.NewApplicationOffers(s.State)
   230  	owner := s.Factory.MakeUser(c, nil)
   231  	anOffer := s.createDefaultOffer(c)
   232  	args := crossmodel.AddApplicationOfferArgs{
   233  		OfferName:              "another-mysql",
   234  		ApplicationName:        "mysql",
   235  		ApplicationDescription: "mysql is a db server",
   236  		Endpoints:              eps,
   237  		Owner:                  owner.Name(),
   238  		HasRead:                []string{"everyone@external"},
   239  	}
   240  	anotherOffer, err := sd.AddOffer(args)
   241  	c.Assert(err, jc.ErrorIsNil)
   242  
   243  	offers, err := sd.AllApplicationOffers()
   244  	c.Assert(err, jc.ErrorIsNil)
   245  	// Ensure ordering doesn't matter.
   246  	offersMap := make(map[string]*crossmodel.ApplicationOffer)
   247  	for _, offer := range offers {
   248  		offersMap[offer.OfferName] = offer
   249  	}
   250  	c.Assert(offersMap, jc.DeepEquals, map[string]*crossmodel.ApplicationOffer{
   251  		anOffer.OfferName:      &anOffer,
   252  		anotherOffer.OfferName: anotherOffer,
   253  	})
   254  }
   255  
   256  func (s *applicationOffersSuite) TestListOffersAll(c *gc.C) {
   257  	sd := state.NewApplicationOffers(s.State)
   258  	offer := s.createDefaultOffer(c)
   259  	offers, err := sd.ListOffers()
   260  	c.Assert(err, jc.ErrorIsNil)
   261  	c.Assert(len(offers), gc.Equals, 1)
   262  	c.Assert(offers[0], jc.DeepEquals, offer)
   263  }
   264  
   265  func (s *applicationOffersSuite) TestListOffersOneFilter(c *gc.C) {
   266  	sd := state.NewApplicationOffers(s.State)
   267  	offer, _ := s.createOffer(c, "offer1", "description for offer1")
   268  	s.createOffer(c, "offer2", "description for offer2")
   269  	s.createOffer(c, "offer3", "description for offer3")
   270  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   271  		OfferName:       "offer1",
   272  		ApplicationName: "mysql",
   273  		Endpoints: []crossmodel.EndpointFilterTerm{{
   274  			Interface: "mysql",
   275  		}},
   276  	})
   277  	c.Assert(err, jc.ErrorIsNil)
   278  	c.Assert(len(offers), gc.Equals, 1)
   279  	c.Assert(offers[0], jc.DeepEquals, offer)
   280  }
   281  
   282  func (s *applicationOffersSuite) TestListOffersExact(c *gc.C) {
   283  	sd := state.NewApplicationOffers(s.State)
   284  	offer, _ := s.createOffer(c, "offer1", "description for offer1")
   285  	s.createOffer(c, "offer2", "description for offer2")
   286  	s.createOffer(c, "offer3", "description for offer3")
   287  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   288  		OfferName: "^offer1$",
   289  	})
   290  	c.Assert(err, jc.ErrorIsNil)
   291  	c.Assert(len(offers), gc.Equals, 1)
   292  	c.Assert(offers[0], jc.DeepEquals, offer)
   293  	offers, err = sd.ListOffers(crossmodel.ApplicationOfferFilter{
   294  		OfferName: "^offer$",
   295  	})
   296  	c.Assert(err, jc.ErrorIsNil)
   297  	c.Assert(len(offers), gc.Equals, 0)
   298  }
   299  
   300  func (s *applicationOffersSuite) TestListOffersFilterExcludes(c *gc.C) {
   301  	sd := state.NewApplicationOffers(s.State)
   302  	s.createOffer(c, "offer1", "description for offer1")
   303  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   304  		Endpoints: []crossmodel.EndpointFilterTerm{{
   305  			Interface: "db2",
   306  		}},
   307  	})
   308  	c.Assert(err, jc.ErrorIsNil)
   309  	c.Assert(len(offers), gc.Equals, 0)
   310  }
   311  
   312  func (s *applicationOffersSuite) TestListOffersManyFilters(c *gc.C) {
   313  	sd := state.NewApplicationOffers(s.State)
   314  	offer, _ := s.createOffer(c, "offer1", "description for offer1")
   315  	offer2, _ := s.createOffer(c, "offer2", "description for offer2")
   316  	s.createOffer(c, "offer3", "description for offer3")
   317  	offers, err := sd.ListOffers(
   318  		crossmodel.ApplicationOfferFilter{
   319  			OfferName:       "offer1",
   320  			ApplicationName: "mysql",
   321  		},
   322  		crossmodel.ApplicationOfferFilter{
   323  			OfferName:              "offer2",
   324  			ApplicationDescription: "offer2",
   325  		},
   326  	)
   327  	c.Assert(err, jc.ErrorIsNil)
   328  	c.Assert(len(offers), gc.Equals, 2)
   329  	c.Assert(offers, jc.DeepEquals, []crossmodel.ApplicationOffer{offer, offer2})
   330  }
   331  
   332  func (s *applicationOffersSuite) TestListOffersFilterDescriptionRegexp(c *gc.C) {
   333  	sd := state.NewApplicationOffers(s.State)
   334  	s.createOffer(c, "offer1", "description for offer1")
   335  	offer, _ := s.createOffer(c, "offer2", "description for offer2")
   336  	s.createOffer(c, "offer3", "description for offer3")
   337  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   338  		ApplicationDescription: "for offer2",
   339  	})
   340  	c.Assert(err, jc.ErrorIsNil)
   341  	c.Assert(len(offers), gc.Equals, 1)
   342  	c.Assert(offers[0], jc.DeepEquals, offer)
   343  }
   344  
   345  func (s *applicationOffersSuite) TestListOffersFilterOfferNameRegexp(c *gc.C) {
   346  	sd := state.NewApplicationOffers(s.State)
   347  	offer, _ := s.createOffer(c, "hosted-offer1", "description for offer1")
   348  	s.createOffer(c, "offer2", "description for offer2")
   349  	s.createOffer(c, "offer3", "description for offer3")
   350  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   351  		OfferName: "offer1",
   352  	})
   353  	c.Assert(err, jc.ErrorIsNil)
   354  	c.Assert(len(offers), gc.Equals, 1)
   355  	c.Assert(offers[0], jc.DeepEquals, offer)
   356  }
   357  
   358  func (s *applicationOffersSuite) TestListOffersAllowedConsumersOwner(c *gc.C) {
   359  	sd := state.NewApplicationOffers(s.State)
   360  	offer, owner := s.createOffer(c, "offer1", "description for offer1")
   361  	s.createOffer(c, "offer2", "description for offer2")
   362  	s.createOffer(c, "offer3", "description for offer3")
   363  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   364  		AllowedConsumers: []string{owner, "mary"},
   365  	})
   366  	c.Assert(err, jc.ErrorIsNil)
   367  	c.Assert(len(offers), gc.Equals, 1)
   368  	c.Assert(offers[0], jc.DeepEquals, offer)
   369  }
   370  
   371  func (s *applicationOffersSuite) TestListOffersAllowedConsumers(c *gc.C) {
   372  	sd := state.NewApplicationOffers(s.State)
   373  	offer, _ := s.createOffer(c, "offer1", "description for offer1")
   374  	offer2, _ := s.createOffer(c, "offer2", "description for offer2")
   375  	s.createOffer(c, "offer3", "description for offer3")
   376  	s.Factory.MakeUser(c, &factory.UserParams{Name: "mary"})
   377  
   378  	mary := names.NewUserTag("mary")
   379  	err := s.State.CreateOfferAccess(
   380  		names.NewApplicationOfferTag(offer.OfferUUID), mary, permission.ConsumeAccess)
   381  	c.Assert(err, jc.ErrorIsNil)
   382  	err = s.State.CreateOfferAccess(
   383  		names.NewApplicationOfferTag(offer2.OfferUUID), mary, permission.ReadAccess)
   384  	c.Assert(err, jc.ErrorIsNil)
   385  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   386  		AllowedConsumers: []string{"mary"},
   387  	})
   388  	c.Assert(err, jc.ErrorIsNil)
   389  	c.Assert(len(offers), gc.Equals, 1)
   390  	c.Assert(offers[0], jc.DeepEquals, offer)
   391  }
   392  
   393  func (s *applicationOffersSuite) TestListOffersConnectedUsers(c *gc.C) {
   394  	sd := state.NewApplicationOffers(s.State)
   395  	offer, _ := s.createOffer(c, "offer1", "description for offer1")
   396  	s.createOffer(c, "offer2", "description for offer2")
   397  	s.createOffer(c, "offer3", "description for offer3")
   398  	s.Factory.MakeUser(c, &factory.UserParams{Name: "mary"})
   399  
   400  	_, err := s.State.AddOfferConnection(state.AddOfferConnectionParams{
   401  		SourceModelUUID: testing.ModelTag.Id(),
   402  		Username:        "mary",
   403  		OfferUUID:       offer.OfferUUID,
   404  	})
   405  	c.Assert(err, jc.ErrorIsNil)
   406  	offers, err := sd.ListOffers(crossmodel.ApplicationOfferFilter{
   407  		ConnectedUsers: []string{"mary"},
   408  	})
   409  	c.Assert(err, jc.ErrorIsNil)
   410  	c.Assert(len(offers), gc.Equals, 1)
   411  	c.Assert(offers[0], jc.DeepEquals, offer)
   412  }
   413  
   414  func (s *applicationOffersSuite) TestAddApplicationOfferDuplicate(c *gc.C) {
   415  	sd := state.NewApplicationOffers(s.State)
   416  	owner := s.Factory.MakeUser(c, nil)
   417  	_, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   418  		OfferName:       "hosted-mysql",
   419  		ApplicationName: "mysql",
   420  		Owner:           owner.Name(),
   421  	})
   422  	c.Assert(err, jc.ErrorIsNil)
   423  	_, err = sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   424  		OfferName:       "hosted-mysql",
   425  		ApplicationName: "mysql",
   426  		Owner:           owner.Name(),
   427  	})
   428  	c.Assert(err, gc.ErrorMatches, `cannot add application offer "hosted-mysql": application offer already exists`)
   429  }
   430  
   431  func (s *applicationOffersSuite) TestAddApplicationOfferDuplicateAddedAfterInitial(c *gc.C) {
   432  	// Check that a record with a URL conflict cannot be added if
   433  	// there is no conflict initially but a record is added
   434  	// before the transaction is run.
   435  	sd := state.NewApplicationOffers(s.State)
   436  	owner := s.Factory.MakeUser(c, nil)
   437  	defer state.SetBeforeHooks(c, s.State, func() {
   438  		_, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   439  			OfferName:       "hosted-mysql",
   440  			ApplicationName: "mysql",
   441  			Owner:           owner.Name(),
   442  		})
   443  		c.Assert(err, jc.ErrorIsNil)
   444  	}).Check()
   445  	_, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   446  		OfferName:       "hosted-mysql",
   447  		ApplicationName: "mysql",
   448  		Owner:           owner.Name(),
   449  	})
   450  	c.Assert(err, gc.ErrorMatches, `cannot add application offer "hosted-mysql": application offer already exists`)
   451  }
   452  
   453  func (s *applicationOffersSuite) TestUpdateApplicationOffer(c *gc.C) {
   454  	sd := state.NewApplicationOffers(s.State)
   455  	owner := s.Factory.MakeUser(c, nil)
   456  	original, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   457  		OfferName:       "hosted-mysql",
   458  		ApplicationName: "mysql",
   459  		Owner:           owner.Name(),
   460  	})
   461  	c.Assert(err, jc.ErrorIsNil)
   462  	offer, err := sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   463  		OfferName:              "hosted-mysql",
   464  		ApplicationName:        "mysql",
   465  		ApplicationDescription: "a better database",
   466  		Owner:                  owner.Name(),
   467  	})
   468  	c.Assert(err, jc.ErrorIsNil)
   469  	c.Assert(offer, jc.DeepEquals, &crossmodel.ApplicationOffer{
   470  		OfferName:              "hosted-mysql",
   471  		OfferUUID:              original.OfferUUID,
   472  		ApplicationName:        "mysql",
   473  		ApplicationDescription: "a better database",
   474  		Endpoints:              map[string]charm.Relation{},
   475  	})
   476  	assertOffersRef(c, s.State, "mysql", 1)
   477  }
   478  
   479  func (s *applicationOffersSuite) TestUpdateApplicationOfferDifferentApp(c *gc.C) {
   480  	sd := state.NewApplicationOffers(s.State)
   481  	owner := s.Factory.MakeUser(c, nil)
   482  	original, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   483  		OfferName:       "hosted-mysql",
   484  		ApplicationName: "mysql",
   485  		Owner:           owner.Name(),
   486  	})
   487  	c.Assert(err, jc.ErrorIsNil)
   488  	s.Factory.MakeApplication(c, &factory.ApplicationParams{Name: "foo"})
   489  	offer, err := sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   490  		OfferName:       "hosted-mysql",
   491  		ApplicationName: "foo",
   492  		Owner:           owner.Name(),
   493  	})
   494  	c.Assert(err, jc.ErrorIsNil)
   495  	c.Assert(offer, jc.DeepEquals, &crossmodel.ApplicationOffer{
   496  		OfferName:       "hosted-mysql",
   497  		OfferUUID:       original.OfferUUID,
   498  		ApplicationName: "foo",
   499  		Endpoints:       map[string]charm.Relation{},
   500  	})
   501  	assertNoOffersRef(c, s.State, "mysql")
   502  	assertOffersRef(c, s.State, "foo", 1)
   503  }
   504  
   505  func (s *applicationOffersSuite) TestUpdateApplicationOfferNotFound(c *gc.C) {
   506  	sd := state.NewApplicationOffers(s.State)
   507  	owner := s.Factory.MakeUser(c, nil)
   508  	_, err := sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   509  		OfferName:       "hosted-mysql",
   510  		ApplicationName: "mysql",
   511  		Owner:           owner.Name(),
   512  	})
   513  	c.Assert(err, gc.ErrorMatches, `cannot update application offer "hosted-mysql": offer "hosted-mysql" not found`)
   514  }
   515  
   516  func (s *applicationOffersSuite) TestUpdateApplicationOfferRemovedAfterInitial(c *gc.C) {
   517  	// Check that a record with a URL conflict cannot be added if
   518  	// there is no conflict initially but a record is added
   519  	// before the transaction is run.
   520  	sd := state.NewApplicationOffers(s.State)
   521  	owner := s.Factory.MakeUser(c, nil)
   522  	_, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   523  		OfferName:       "hosted-mysql",
   524  		ApplicationName: "mysql",
   525  		Owner:           owner.Name(),
   526  	})
   527  	c.Assert(err, jc.ErrorIsNil)
   528  	defer state.SetBeforeHooks(c, s.State, func() {
   529  		err := sd.Remove("hosted-mysql", false)
   530  		c.Assert(err, jc.ErrorIsNil)
   531  	}).Check()
   532  	_, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   533  		OfferName:       "hosted-mysql",
   534  		ApplicationName: "mysql",
   535  		Owner:           owner.Name(),
   536  	})
   537  	c.Assert(err, gc.ErrorMatches, `cannot update application offer "hosted-mysql": offer "hosted-mysql" not found`)
   538  }
   539  
   540  func (s *applicationOffersSuite) addOfferConnection(c *gc.C, offerUUID string) *state.RemoteApplication {
   541  	app, err := s.State.AddRemoteApplication(state.AddRemoteApplicationParams{
   542  		Name:            "wordpress",
   543  		SourceModel:     testing.ModelTag,
   544  		IsConsumerProxy: true,
   545  		Endpoints: []charm.Relation{{
   546  			Interface: "mysql",
   547  			Name:      "server",
   548  			Role:      charm.RoleRequirer,
   549  			Scope:     charm.ScopeGlobal,
   550  		}}})
   551  	c.Assert(err, jc.ErrorIsNil)
   552  	eps, err := s.State.InferEndpoints("wordpress", "mysql")
   553  	c.Assert(err, jc.ErrorIsNil)
   554  	rel, err := s.State.AddRelation(eps...)
   555  	c.Assert(err, jc.ErrorIsNil)
   556  
   557  	_, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{
   558  		OfferUUID:       offerUUID,
   559  		RelationId:      rel.Id(),
   560  		RelationKey:     rel.Tag().Id(),
   561  		Username:        "admin",
   562  		SourceModelUUID: testing.ModelTag.Id(),
   563  	})
   564  	c.Assert(err, jc.ErrorIsNil)
   565  
   566  	return app
   567  }
   568  
   569  func (s *applicationOffersSuite) TestUpdateApplicationOfferRemovingEndpointsInUse(c *gc.C) {
   570  	owner := s.Factory.MakeUser(c, nil).Name()
   571  	sd := state.NewApplicationOffers(s.State)
   572  	offer, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   573  		OfferName:       "hosted-mysql",
   574  		ApplicationName: "mysql",
   575  		Owner:           owner,
   576  		Endpoints: map[string]string{
   577  			"server":       "server",
   578  			"server-admin": "server-admin",
   579  		},
   580  	})
   581  	c.Assert(err, jc.ErrorIsNil)
   582  
   583  	_, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{
   584  		SourceModelUUID: testing.ModelTag.Id(),
   585  		RelationId:      1,
   586  		RelationKey:     "remote:server mysql:server",
   587  		Username:        "admin",
   588  		OfferUUID:       offer.OfferUUID,
   589  	})
   590  	c.Assert(err, jc.ErrorIsNil)
   591  
   592  	_, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   593  		OfferName:       "hosted-mysql",
   594  		ApplicationName: "mysql",
   595  		Owner:           owner,
   596  		Endpoints: map[string]string{
   597  			// We are attempting to remove the "server" endpoint
   598  			// from the offer which is currently connected to an
   599  			// active consumer
   600  			"server-admin": "server-admin",
   601  		},
   602  	})
   603  	c.Assert(err, gc.ErrorMatches, `cannot update application offer "hosted-mysql": application endpoint "server" has active consumers`)
   604  }
   605  
   606  func (s *applicationOffersSuite) TestUpdateApplicationOfferInvalidApplication(c *gc.C) {
   607  	owner := s.Factory.MakeUser(c, nil).Name()
   608  	sd := state.NewApplicationOffers(s.State)
   609  
   610  	originalOffer, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   611  		OfferName:       "myoffer",
   612  		Owner:           owner,
   613  		ApplicationName: "mysql",
   614  		Endpoints: map[string]string{
   615  			"db": "server",
   616  		},
   617  	})
   618  	c.Assert(err, jc.ErrorIsNil)
   619  	c.Assert(originalOffer, jc.DeepEquals, &crossmodel.ApplicationOffer{
   620  		OfferName:       "myoffer",
   621  		OfferUUID:       originalOffer.OfferUUID,
   622  		ApplicationName: "mysql",
   623  		Endpoints: map[string]charm.Relation{
   624  			"db": {
   625  				Name:      "server",
   626  				Role:      "provider",
   627  				Interface: "mysql",
   628  				Scope:     "global",
   629  			},
   630  		},
   631  	})
   632  
   633  	_, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   634  		OfferName:       "myoffer",
   635  		Owner:           owner,
   636  		ApplicationName: "invalid",
   637  		Endpoints: map[string]string{
   638  			"invalid-endpoint": "invalid-endpoint",
   639  		},
   640  	})
   641  	c.Assert(err, gc.ErrorMatches, `cannot update application offer "myoffer": application "invalid" not found`)
   642  
   643  	newOffer, err := sd.ApplicationOffer("myoffer")
   644  	c.Assert(err, jc.ErrorIsNil)
   645  	c.Check(newOffer, jc.DeepEquals, originalOffer)
   646  }
   647  
   648  // regression test for https://bugs.launchpad.net/juju/+bug/1954830
   649  func (s *applicationOffersSuite) TestUpdateApplicationOfferInvalidEndpoint(c *gc.C) {
   650  	owner := s.Factory.MakeUser(c, nil).Name()
   651  	sd := state.NewApplicationOffers(s.State)
   652  
   653  	originalOffer, err := sd.AddOffer(crossmodel.AddApplicationOfferArgs{
   654  		OfferName:       "myoffer",
   655  		Owner:           owner,
   656  		ApplicationName: "mysql",
   657  		Endpoints: map[string]string{
   658  			"db": "server",
   659  		},
   660  	})
   661  	c.Assert(err, jc.ErrorIsNil)
   662  	c.Assert(originalOffer, jc.DeepEquals, &crossmodel.ApplicationOffer{
   663  		OfferName:       "myoffer",
   664  		OfferUUID:       originalOffer.OfferUUID,
   665  		ApplicationName: "mysql",
   666  		Endpoints: map[string]charm.Relation{
   667  			"db": {
   668  				Name:      "server",
   669  				Role:      "provider",
   670  				Interface: "mysql",
   671  				Scope:     "global",
   672  			},
   673  		},
   674  	})
   675  
   676  	_, err = sd.UpdateOffer(crossmodel.AddApplicationOfferArgs{
   677  		OfferName:       "myoffer",
   678  		Owner:           owner,
   679  		ApplicationName: "mysql",
   680  		Endpoints: map[string]string{
   681  			"invalid-endpoint": "invalid-endpoint",
   682  		},
   683  	})
   684  	c.Assert(err, gc.ErrorMatches, `cannot update application offer "myoffer": getting relation endpoint for relation "invalid-endpoint" and application "mysql": application "mysql" has no "invalid-endpoint" relation`)
   685  
   686  	newOffer, err := sd.ApplicationOffer("myoffer")
   687  	c.Assert(err, jc.ErrorIsNil)
   688  	c.Check(newOffer, jc.DeepEquals, originalOffer)
   689  }
   690  
   691  func (s *applicationOffersSuite) TestRemoveOffersSucceedsWithZeroConnections(c *gc.C) {
   692  	s.createDefaultOffer(c)
   693  	ao := state.NewApplicationOffers(s.State)
   694  	err := ao.Remove("hosted-mysql", false)
   695  	c.Assert(err, jc.ErrorIsNil)
   696  	_, err = ao.ApplicationOffer("hosted-mysql")
   697  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   698  
   699  	err = s.mysql.Refresh()
   700  	c.Assert(err, jc.ErrorIsNil)
   701  	assertNoOffersRef(c, s.State, "mysql")
   702  }
   703  
   704  func (s *applicationOffersSuite) TestRemoveApplicationSucceedsWithZeroConnections(c *gc.C) {
   705  	s.createDefaultOffer(c)
   706  
   707  	err := s.mysql.Destroy()
   708  	c.Assert(err, jc.ErrorIsNil)
   709  	err = s.mysql.Refresh()
   710  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   711  	assertNoOffersRef(c, s.State, "mysql")
   712  }
   713  
   714  func (s *applicationOffersSuite) TestRemoveApplicationSucceedsWithZeroConnectionsRace(c *gc.C) {
   715  	addOffer := func() {
   716  		s.createDefaultOffer(c)
   717  	}
   718  	defer state.SetBeforeHooks(c, s.State, addOffer).Check()
   719  	err := s.mysql.Destroy()
   720  	c.Assert(err, jc.ErrorIsNil)
   721  	err = s.mysql.Refresh()
   722  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   723  	assertNoOffersRef(c, s.State, "mysql")
   724  }
   725  
   726  func (s *applicationOffersSuite) TestRemoveApplicationFailsWithOfferWithConnections(c *gc.C) {
   727  	offer := s.createDefaultOffer(c)
   728  	s.addOfferConnection(c, offer.OfferUUID)
   729  
   730  	err := s.mysql.Destroy()
   731  	c.Assert(err, gc.ErrorMatches, `cannot destroy application "mysql": application is used by 1 consumer`)
   732  	err = s.mysql.Refresh()
   733  	c.Assert(err, jc.ErrorIsNil)
   734  	assertOffersRef(c, s.State, "mysql", 1)
   735  }
   736  
   737  func (s *applicationOffersSuite) TestRemoveApplicationFailsWithOfferWithConnectionsRace(c *gc.C) {
   738  	addConnectedOffer := func() {
   739  		offer := s.createDefaultOffer(c)
   740  		s.addOfferConnection(c, offer.OfferUUID)
   741  	}
   742  	defer state.SetBeforeHooks(c, s.State, addConnectedOffer).Check()
   743  	err := s.mysql.Destroy()
   744  	c.Assert(err, gc.ErrorMatches, `cannot destroy application "mysql": application is used by 1 consumer`)
   745  	err = s.mysql.Refresh()
   746  	c.Assert(err, jc.ErrorIsNil)
   747  	assertOffersRef(c, s.State, "mysql", 1)
   748  }
   749  
   750  func (s *applicationOffersSuite) TestRemoveOffersFailsWithConnections(c *gc.C) {
   751  	offer := s.createDefaultOffer(c)
   752  	s.addOfferConnection(c, offer.OfferUUID)
   753  	ao := state.NewApplicationOffers(s.State)
   754  	err := ao.Remove("hosted-mysql", false)
   755  	c.Assert(err, gc.ErrorMatches, `cannot delete application offer "hosted-mysql": offer has 1 relation`)
   756  }
   757  
   758  func (s *applicationOffersSuite) TestRemoveOffersFailsWithConnectionsRace(c *gc.C) {
   759  	offer := s.createDefaultOffer(c)
   760  	ao := state.NewApplicationOffers(s.State)
   761  	addOfferConnection := func() {
   762  		c.Logf("adding connection to %s", offer.OfferUUID)
   763  		s.addOfferConnection(c, offer.OfferUUID)
   764  	}
   765  	defer state.SetBeforeHooks(c, s.State, addOfferConnection).Check()
   766  
   767  	err := ao.Remove("hosted-mysql", false)
   768  	c.Assert(err, gc.ErrorMatches, `cannot delete application offer "hosted-mysql": offer has 1 relation`)
   769  }
   770  
   771  func (s *applicationOffersSuite) TestRemoveOffersSucceedsWhenLocalRelationAdded(c *gc.C) {
   772  	offer := s.createDefaultOffer(c)
   773  	s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress"))
   774  	_, err := s.State.Application(offer.ApplicationName)
   775  	c.Assert(err, jc.ErrorIsNil)
   776  	eps, err := s.State.InferEndpoints("local-wordpress", "mysql")
   777  	c.Assert(err, jc.ErrorIsNil)
   778  	_, err = s.State.AddRelation(eps...)
   779  	c.Assert(err, jc.ErrorIsNil)
   780  	ao := state.NewApplicationOffers(s.State)
   781  
   782  	err = ao.Remove(offer.OfferName, false)
   783  	c.Assert(err, jc.ErrorIsNil)
   784  	_, err = ao.ApplicationOffer("hosted-mysql")
   785  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   786  }
   787  
   788  func (s *applicationOffersSuite) assertInScope(c *gc.C, relUnit *state.RelationUnit, inScope bool) {
   789  	ok, err := relUnit.InScope()
   790  	c.Assert(err, jc.ErrorIsNil)
   791  	c.Assert(ok, gc.Equals, inScope)
   792  }
   793  
   794  func (s *applicationOffersSuite) TestRemoveOffersWithConnectionsForce(c *gc.C) {
   795  	offer := s.createDefaultOffer(c)
   796  	rwordpress, err := s.State.AddRemoteApplication(state.AddRemoteApplicationParams{
   797  		Name:            "remote-wordpress",
   798  		SourceModel:     names.NewModelTag("source-model"),
   799  		IsConsumerProxy: true,
   800  		Endpoints: []charm.Relation{{
   801  			Interface: "mysql",
   802  			Limit:     1,
   803  			Name:      "db",
   804  			Role:      charm.RoleRequirer,
   805  			Scope:     charm.ScopeGlobal,
   806  		}},
   807  	})
   808  	c.Assert(err, jc.ErrorIsNil)
   809  	wordpressEP, err := rwordpress.Endpoint("db")
   810  	c.Assert(err, jc.ErrorIsNil)
   811  
   812  	mysql, err := s.State.Application("mysql")
   813  	c.Assert(err, jc.ErrorIsNil)
   814  	mysqlUnit, err := mysql.AddUnit(state.AddUnitParams{})
   815  	c.Assert(err, jc.ErrorIsNil)
   816  	mysqlEP, err := mysql.Endpoint("server")
   817  	c.Assert(err, jc.ErrorIsNil)
   818  
   819  	rel, err := s.State.AddRelation(wordpressEP, mysqlEP)
   820  	c.Assert(err, jc.ErrorIsNil)
   821  	mysqlru, err := rel.Unit(mysqlUnit)
   822  	c.Assert(err, jc.ErrorIsNil)
   823  	err = mysqlru.EnterScope(nil)
   824  	c.Assert(err, jc.ErrorIsNil)
   825  	s.assertInScope(c, mysqlru, true)
   826  
   827  	wpru, err := rel.RemoteUnit("remote-wordpress/0")
   828  	c.Assert(err, jc.ErrorIsNil)
   829  	err = wpru.EnterScope(nil)
   830  	c.Assert(err, jc.ErrorIsNil)
   831  	s.assertInScope(c, wpru, true)
   832  
   833  	_, err = s.State.AddOfferConnection(state.AddOfferConnectionParams{
   834  		OfferUUID:       offer.OfferUUID,
   835  		RelationId:      rel.Id(),
   836  		RelationKey:     rel.Tag().Id(),
   837  		Username:        "admin",
   838  		SourceModelUUID: testing.ModelTag.Id(),
   839  	})
   840  	c.Assert(err, jc.ErrorIsNil)
   841  
   842  	ao := state.NewApplicationOffers(s.State)
   843  
   844  	err = ao.Remove("hosted-mysql", true)
   845  	c.Assert(err, jc.ErrorIsNil)
   846  	_, err = ao.ApplicationOffer("hosted-mysql")
   847  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   848  	conn, err := s.State.OfferConnections(offer.OfferUUID)
   849  	c.Assert(err, jc.ErrorIsNil)
   850  	c.Assert(conn, gc.HasLen, 1)
   851  	offerRel, err := s.State.Relation(conn[0].RelationId())
   852  	c.Assert(err, jc.ErrorIsNil)
   853  	c.Assert(offerRel.Life(), gc.Equals, state.Dying)
   854  	s.assertInScope(c, wpru, false)
   855  	s.assertInScope(c, mysqlru, true)
   856  	err = rwordpress.Refresh()
   857  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   858  }
   859  
   860  func (s *applicationOffersSuite) TestRemoveOneOfferSameApplication(c *gc.C) {
   861  	offer, owner := s.createOffer(c, "hosted-mysql", "offer one")
   862  	sd := state.NewApplicationOffers(s.State)
   863  	offerArgs := crossmodel.AddApplicationOfferArgs{
   864  		OfferName:              "mysql-admin",
   865  		ApplicationName:        "mysql",
   866  		ApplicationDescription: "mysql admin",
   867  		Endpoints:              map[string]string{"db-admin": "server-admin"},
   868  		Owner:                  owner,
   869  	}
   870  	offer2, err := sd.AddOffer(offerArgs)
   871  	c.Assert(err, jc.ErrorIsNil)
   872  
   873  	s.addOfferConnection(c, offer.OfferUUID)
   874  	ao := state.NewApplicationOffers(s.State)
   875  
   876  	err = ao.Remove(offer2.OfferName, false)
   877  	c.Assert(err, jc.ErrorIsNil)
   878  	_, err = ao.ApplicationOffer("mysql-admin")
   879  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   880  
   881  	// The other offer is unaffected.
   882  	appOffer, err := ao.ApplicationOffer("hosted-mysql")
   883  	c.Assert(err, jc.ErrorIsNil)
   884  	conn, err := s.State.OfferConnections(appOffer.OfferUUID)
   885  	c.Assert(err, jc.ErrorIsNil)
   886  	c.Assert(conn, gc.HasLen, 1)
   887  }
   888  
   889  func (s *applicationOffersSuite) TestRemovingApplicationFailsRace(c *gc.C) {
   890  	s.createDefaultOffer(c)
   891  	wp := s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress"))
   892  	eps, err := s.State.InferEndpoints(wp.Name(), s.mysql.Name())
   893  	c.Assert(err, jc.ErrorIsNil)
   894  
   895  	addRelation := func() {
   896  		_, err := s.State.AddRelation(eps...)
   897  		c.Assert(err, jc.ErrorIsNil)
   898  	}
   899  
   900  	rmRelations := func() {
   901  		rels, err := s.State.AllRelations()
   902  		c.Assert(err, jc.ErrorIsNil)
   903  
   904  		for _, rel := range rels {
   905  			err = rel.Destroy()
   906  			c.Assert(err, jc.ErrorIsNil)
   907  			err = s.mysql.Refresh()
   908  			c.Assert(err, jc.ErrorIsNil)
   909  		}
   910  	}
   911  
   912  	state.SetMaxTxnAttempts(c, s.State, 3)
   913  	bumpTxnRevno := jujutxn.TestHook{Before: addRelation, After: rmRelations}
   914  	defer state.SetTestHooks(c, s.State, bumpTxnRevno, bumpTxnRevno, bumpTxnRevno).Check()
   915  
   916  	err = s.mysql.Destroy()
   917  	c.Assert(err, jc.Satisfies, errors.IsNotSupported)
   918  	c.Assert(err, gc.ErrorMatches, "cannot destroy application.*")
   919  	s.mysql.Refresh()
   920  	assertOffersRef(c, s.State, "mysql", 1)
   921  }
   922  
   923  func (s *applicationOffersSuite) TestRemoveOffersWithConnectionsRace(c *gc.C) {
   924  	// Create a local wordpress application to relate to the local mysql,
   925  	// to show that we count remote relations correctly.
   926  	s.AddTestingApplication(c, "local-wordpress", s.AddTestingCharm(c, "wordpress"))
   927  	eps, err := s.State.InferEndpoints("local-wordpress", "mysql")
   928  	c.Assert(err, jc.ErrorIsNil)
   929  	localRel, err := s.State.AddRelation(eps...)
   930  	c.Assert(err, jc.ErrorIsNil)
   931  
   932  	ao := state.NewApplicationOffers(s.State)
   933  	offer := s.createDefaultOffer(c)
   934  	addOfferConnection := func() {
   935  		// Remove the local relation and add a remote relation,
   936  		// so that the relation count remains stable. We should
   937  		// be checking the *remote* relation count.
   938  		c.Assert(localRel.Destroy(), jc.ErrorIsNil)
   939  		s.addOfferConnection(c, offer.OfferUUID)
   940  	}
   941  	defer state.SetBeforeHooks(c, s.State, addOfferConnection).Check()
   942  
   943  	err = ao.Remove(offer.OfferName, false)
   944  	c.Assert(err, gc.ErrorMatches, `cannot delete application offer "hosted-mysql": offer has 1 relation`)
   945  }
   946  
   947  func (s *applicationOffersSuite) TestWatchOfferStatus(c *gc.C) {
   948  	ao := state.NewApplicationOffers(s.State)
   949  	offer, err := ao.AddOffer(crossmodel.AddApplicationOfferArgs{
   950  		OfferName:       "hosted-mysql",
   951  		ApplicationName: "mysql",
   952  		Owner:           s.Owner.Id(),
   953  	})
   954  	c.Assert(err, jc.ErrorIsNil)
   955  
   956  	// Ensure that all the creation events have flowed through the system.
   957  	s.WaitForModelWatchersIdle(c, s.Model.UUID())
   958  
   959  	w, err := s.State.WatchOfferStatus(offer.OfferUUID)
   960  	c.Assert(err, jc.ErrorIsNil)
   961  
   962  	defer statetesting.AssertStop(c, w)
   963  	wc := statetesting.NewNotifyWatcherC(c, w)
   964  	// Initial event.
   965  	wc.AssertOneChange()
   966  
   967  	app, err := s.State.Application(offer.ApplicationName)
   968  	c.Assert(err, jc.ErrorIsNil)
   969  	err = app.SetStatus(status.StatusInfo{
   970  		Status:  status.Waiting,
   971  		Message: "waiting for replication",
   972  	})
   973  	c.Assert(err, jc.ErrorIsNil)
   974  	wc.AssertOneChange()
   975  
   976  	u := s.Factory.MakeUnit(c, &factory.UnitParams{
   977  		Application: app,
   978  	})
   979  	wc.AssertOneChange()
   980  	err = u.SetStatus(status.StatusInfo{
   981  		Status: status.Blocked,
   982  	})
   983  	c.Assert(err, jc.ErrorIsNil)
   984  	wc.AssertOneChange()
   985  	err = u.Destroy()
   986  	c.Assert(err, jc.ErrorIsNil)
   987  	wc.AssertOneChange()
   988  
   989  	err = ao.Remove(offer.OfferName, false)
   990  	c.Assert(err, jc.ErrorIsNil)
   991  	_, err = ao.ApplicationOffer("hosted-mysql")
   992  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   993  	err = app.Destroy()
   994  	c.Assert(err, jc.ErrorIsNil)
   995  	wc.AssertOneChange()
   996  }
   997  
   998  func (s *applicationOffersSuite) TestWatchOffer(c *gc.C) {
   999  	ao := state.NewApplicationOffers(s.State)
  1000  	offer, err := ao.AddOffer(crossmodel.AddApplicationOfferArgs{
  1001  		OfferName:       "hosted-mysql",
  1002  		ApplicationName: "mysql",
  1003  		Owner:           s.Owner.Id(),
  1004  	})
  1005  	c.Assert(err, jc.ErrorIsNil)
  1006  
  1007  	anotherOffer, err := ao.AddOffer(crossmodel.AddApplicationOfferArgs{
  1008  		OfferName:       "hosted-postgresql",
  1009  		ApplicationName: "mysql",
  1010  		Owner:           s.Owner.Id(),
  1011  	})
  1012  	c.Assert(err, jc.ErrorIsNil)
  1013  
  1014  	// Ensure that all the creation events have flowed through the system.
  1015  	s.WaitForModelWatchersIdle(c, s.Model.UUID())
  1016  
  1017  	w := s.State.WatchOffer(offer.OfferName)
  1018  
  1019  	defer statetesting.AssertStop(c, w)
  1020  	wc := statetesting.NewNotifyWatcherC(c, w)
  1021  	// Initial event.
  1022  	wc.AssertOneChange()
  1023  
  1024  	err = ao.Remove(offer.OfferName, false)
  1025  	c.Assert(err, jc.ErrorIsNil)
  1026  	wc.AssertOneChange()
  1027  
  1028  	err = ao.Remove(anotherOffer.OfferName, false)
  1029  	c.Assert(err, jc.ErrorIsNil)
  1030  	wc.AssertNoChange()
  1031  }