github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/controller/crossmodelrelations/crossmodelrelations_test.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package crossmodelrelations_test
     5  
     6  import (
     7  	"bytes"
     8  	"regexp"
     9  
    10  	"github.com/juju/testing"
    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  	"gopkg.in/macaroon.v2-unstable"
    17  
    18  	"github.com/juju/juju/apiserver/common"
    19  	commoncrossmodel "github.com/juju/juju/apiserver/common/crossmodel"
    20  	"github.com/juju/juju/apiserver/common/firewall"
    21  	"github.com/juju/juju/apiserver/facade"
    22  	"github.com/juju/juju/apiserver/facades/controller/crossmodelrelations"
    23  	"github.com/juju/juju/apiserver/params"
    24  	apiservertesting "github.com/juju/juju/apiserver/testing"
    25  	"github.com/juju/juju/core/crossmodel"
    26  	"github.com/juju/juju/core/status"
    27  	"github.com/juju/juju/state"
    28  	coretesting "github.com/juju/juju/testing"
    29  )
    30  
    31  var _ = gc.Suite(&crossmodelRelationsSuite{})
    32  
    33  type crossmodelRelationsSuite struct {
    34  	coretesting.BaseSuite
    35  
    36  	resources     *common.Resources
    37  	authorizer    *apiservertesting.FakeAuthorizer
    38  	st            *mockState
    39  	mockStatePool *mockStatePool
    40  	bakery        *mockBakeryService
    41  	authContext   *commoncrossmodel.AuthContext
    42  	api           *crossmodelrelations.CrossModelRelationsAPI
    43  
    44  	watchedRelations params.Entities
    45  	watchedOffers    []string
    46  }
    47  
    48  func (s *crossmodelRelationsSuite) SetUpTest(c *gc.C) {
    49  	s.BaseSuite.SetUpTest(c)
    50  
    51  	s.bakery = &mockBakeryService{}
    52  	s.resources = common.NewResources()
    53  	s.AddCleanup(func(_ *gc.C) { s.resources.StopAll() })
    54  
    55  	s.authorizer = &apiservertesting.FakeAuthorizer{
    56  		Tag:        names.NewMachineTag("0"),
    57  		Controller: true,
    58  	}
    59  
    60  	s.st = newMockState()
    61  	s.mockStatePool = &mockStatePool{map[string]commoncrossmodel.Backend{coretesting.ModelTag.Id(): s.st}}
    62  	fw := &mockFirewallState{}
    63  	egressAddressWatcher := func(_ facade.Resources, fws firewall.State, relations params.Entities) (params.StringsWatchResults, error) {
    64  		c.Assert(fw, gc.Equals, fws)
    65  		s.watchedRelations = relations
    66  		return params.StringsWatchResults{Results: make([]params.StringsWatchResult, len(relations.Entities))}, nil
    67  	}
    68  	relationStatusWatcher := func(st crossmodelrelations.CrossModelRelationsState, tag names.RelationTag) (state.StringsWatcher, error) {
    69  		c.Assert(s.st, gc.Equals, st)
    70  		s.watchedRelations = params.Entities{Entities: []params.Entity{{Tag: tag.String()}}}
    71  		w := &mockRelationStatusWatcher{changes: make(chan []string, 1)}
    72  		w.changes <- []string{"db2:db django:db"}
    73  		return w, nil
    74  	}
    75  	offerStatusWatcher := func(st crossmodelrelations.CrossModelRelationsState, offerUUID string) (crossmodelrelations.OfferWatcher, error) {
    76  		c.Assert(s.st, gc.Equals, st)
    77  		s.watchedOffers = []string{offerUUID}
    78  		w := &mockOfferStatusWatcher{offerUUID: offerUUID, changes: make(chan struct{}, 1)}
    79  		w.changes <- struct{}{}
    80  		return w, nil
    81  	}
    82  	var err error
    83  	s.authContext, err = commoncrossmodel.NewAuthContext(s.mockStatePool, s.bakery, s.bakery)
    84  	c.Assert(err, jc.ErrorIsNil)
    85  	api, err := crossmodelrelations.NewCrossModelRelationsAPI(
    86  		s.st, fw, s.resources, s.authorizer, s.authContext, egressAddressWatcher, relationStatusWatcher, offerStatusWatcher)
    87  	c.Assert(err, jc.ErrorIsNil)
    88  	s.api = api
    89  }
    90  
    91  func (s *crossmodelRelationsSuite) assertPublishRelationsChanges(c *gc.C, life params.Life, suspendedReason string, forceCleanup bool) {
    92  	s.st.remoteApplications["db2"] = &mockRemoteApplication{}
    93  	s.st.remoteEntities[names.NewApplicationTag("db2")] = "token-db2"
    94  	rel := newMockRelation(1)
    95  	ru1 := newMockRelationUnit()
    96  	ru2 := newMockRelationUnit()
    97  	rel.units["db2/1"] = ru1
    98  	rel.units["db2/2"] = ru2
    99  	s.st.relations["db2:db django:db"] = rel
   100  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   101  		offerUUID:       "hosted-db2-uuid",
   102  		sourcemodelUUID: "source-model-uuid",
   103  		relationKey:     "db2:db django:db",
   104  		relationId:      1,
   105  	}
   106  	s.st.remoteEntities[names.NewRelationTag("db2:db django:db")] = "token-db2:db django:db"
   107  	mac, err := s.bakery.NewMacaroon(
   108  		[]checkers.Caveat{
   109  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   110  			checkers.DeclaredCaveat("relation-key", "db2:db django:db"),
   111  			checkers.DeclaredCaveat("username", "mary"),
   112  		})
   113  
   114  	c.Assert(err, jc.ErrorIsNil)
   115  	suspended := true
   116  	results, err := s.api.PublishRelationChanges(params.RemoteRelationsChanges{
   117  		Changes: []params.RemoteRelationChangeEvent{
   118  			{
   119  				Life:             life,
   120  				ForceCleanup:     &forceCleanup,
   121  				Suspended:        &suspended,
   122  				SuspendedReason:  suspendedReason,
   123  				ApplicationToken: "token-db2",
   124  				RelationToken:    "token-db2:db django:db",
   125  				ChangedUnits: []params.RemoteRelationUnitChange{{
   126  					UnitId:   1,
   127  					Settings: map[string]interface{}{"foo": "bar"},
   128  				}},
   129  				DepartedUnits: []int{2},
   130  				Macaroons:     macaroon.Slice{mac},
   131  			},
   132  		},
   133  	})
   134  	c.Assert(err, jc.ErrorIsNil)
   135  	err = results.Combine()
   136  	c.Assert(err, jc.ErrorIsNil)
   137  	expected := []testing.StubCall{
   138  		{"GetRemoteEntity", []interface{}{"token-db2:db django:db"}},
   139  		{"KeyRelation", []interface{}{"db2:db django:db"}},
   140  		{"GetRemoteEntity", []interface{}{"token-db2"}},
   141  	}
   142  	if life == params.Alive {
   143  		c.Assert(rel.status, gc.Equals, status.Suspending)
   144  		if suspendedReason == "" {
   145  			c.Assert(rel.message, gc.Equals, "suspending after update from remote model")
   146  		} else {
   147  			c.Assert(rel.message, gc.Equals, suspendedReason)
   148  		}
   149  	} else {
   150  		c.Assert(rel.status, gc.Equals, status.Status(""))
   151  		c.Assert(rel.message, gc.Equals, "")
   152  		expected = append(expected, testing.StubCall{
   153  			"RemoteApplication", []interface{}{"db2"},
   154  		})
   155  	}
   156  	s.st.CheckCalls(c, expected)
   157  	if forceCleanup {
   158  		ru1.CheckCalls(c, []testing.StubCall{
   159  			{"LeaveScope", []interface{}{}},
   160  		})
   161  	} else {
   162  		ru1.CheckCalls(c, []testing.StubCall{
   163  			{"InScope", []interface{}{}},
   164  			{"EnterScope", []interface{}{map[string]interface{}{"foo": "bar"}}},
   165  		})
   166  	}
   167  	ru2.CheckCalls(c, []testing.StubCall{
   168  		{"LeaveScope", []interface{}{}},
   169  	})
   170  }
   171  
   172  func (s *crossmodelRelationsSuite) TestPublishRelationsChanges(c *gc.C) {
   173  	s.assertPublishRelationsChanges(c, params.Alive, "", false)
   174  }
   175  
   176  func (s *crossmodelRelationsSuite) TestPublishRelationsChangesWithSuspendedReason(c *gc.C) {
   177  	s.assertPublishRelationsChanges(c, params.Alive, "reason", false)
   178  }
   179  
   180  func (s *crossmodelRelationsSuite) TestPublishRelationsChangesDyingWhileSuspended(c *gc.C) {
   181  	s.assertPublishRelationsChanges(c, params.Dying, "", false)
   182  }
   183  
   184  func (s *crossmodelRelationsSuite) TestPublishRelationsChangesDyingForceCleanup(c *gc.C) {
   185  	s.assertPublishRelationsChanges(c, params.Dying, "", true)
   186  }
   187  
   188  func (s *crossmodelRelationsSuite) assertRegisterRemoteRelations(c *gc.C) {
   189  	app := &mockApplication{}
   190  	app.eps = []state.Endpoint{{
   191  		ApplicationName: "offeredapp",
   192  		Relation:        charm.Relation{Name: "local"},
   193  	}}
   194  	s.st.applications["offeredapp"] = app
   195  	s.st.offers = map[string]*crossmodel.ApplicationOffer{
   196  		"offer-uuid": {
   197  			OfferUUID:       "offer-uuid",
   198  			OfferName:       "offered",
   199  			ApplicationName: "offeredapp",
   200  		}}
   201  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   202  		offerUUID:       "offer-uuid",
   203  		sourcemodelUUID: "source-model-uuid",
   204  		relationKey:     "db2:db django:db",
   205  		relationId:      1,
   206  	}
   207  	mac, err := s.bakery.NewMacaroon(
   208  		[]checkers.Caveat{
   209  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   210  			checkers.DeclaredCaveat("offer-uuid", "offer-uuid"),
   211  			checkers.DeclaredCaveat("username", "mary"),
   212  		})
   213  
   214  	c.Assert(err, jc.ErrorIsNil)
   215  	results, err := s.api.RegisterRemoteRelations(params.RegisterRemoteRelationArgs{
   216  		Relations: []params.RegisterRemoteRelationArg{{
   217  			ApplicationToken:  "app-token",
   218  			SourceModelTag:    coretesting.ModelTag.String(),
   219  			RelationToken:     "rel-token",
   220  			RemoteEndpoint:    params.RemoteEndpoint{Name: "remote"},
   221  			OfferUUID:         "offer-uuid",
   222  			LocalEndpointName: "local",
   223  			Macaroons:         macaroon.Slice{mac},
   224  		}}})
   225  	c.Assert(err, jc.ErrorIsNil)
   226  	c.Assert(results.Results, gc.HasLen, 1)
   227  	result := results.Results[0]
   228  	c.Assert(result.Error, gc.IsNil)
   229  	c.Check(result.Result.Token, gc.Equals, "token-offered")
   230  	declared := checkers.InferDeclared(macaroon.Slice{result.Result.Macaroon})
   231  	c.Assert(declared, jc.DeepEquals, checkers.Declared{
   232  		"source-model-uuid": "deadbeef-0bad-400d-8000-4b1d0d06f00d",
   233  		"relation-key":      "offeredapp:local remote-apptoken:remote",
   234  		"username":          "mary",
   235  		"offer-uuid":        "offer-uuid",
   236  	})
   237  	cav := result.Result.Macaroon.Caveats()
   238  	c.Check(cav, gc.HasLen, 5)
   239  	c.Check(bytes.HasPrefix(cav[0].Id, []byte("time-before ")), jc.IsTrue)
   240  	c.Check(cav[1].Id, jc.DeepEquals, []byte("declared source-model-uuid deadbeef-0bad-400d-8000-4b1d0d06f00d"))
   241  	c.Check(cav[2].Id, jc.DeepEquals, []byte("declared offer-uuid offer-uuid"))
   242  	c.Check(cav[3].Id, jc.DeepEquals, []byte("declared username mary"))
   243  	c.Check(cav[4].Id, jc.DeepEquals, []byte("declared relation-key offeredapp:local remote-apptoken:remote"))
   244  
   245  	expectedRemoteApp := s.st.remoteApplications["remote-apptoken"]
   246  	expectedRemoteApp.Stub = testing.Stub{} // don't care about api calls
   247  	c.Check(expectedRemoteApp, jc.DeepEquals, &mockRemoteApplication{
   248  		sourceModelUUID: coretesting.ModelTag.Id(), consumerproxy: true})
   249  	expectedRel := s.st.relations["offeredapp:local remote-apptoken:remote"]
   250  	expectedRel.Stub = testing.Stub{} // don't care about api calls
   251  	c.Check(expectedRel, jc.DeepEquals, &mockRelation{id: 0, key: "offeredapp:local remote-apptoken:remote"})
   252  	c.Check(s.st.remoteEntities, gc.HasLen, 2)
   253  	c.Check(s.st.remoteEntities[names.NewApplicationTag("offered")], gc.Equals, "token-offered")
   254  	c.Check(s.st.remoteEntities[names.NewRelationTag("offeredapp:local remote-apptoken:remote")], gc.Equals, "rel-token")
   255  	c.Assert(s.st.offerConnections, gc.HasLen, 1)
   256  	offerConnection := s.st.offerConnections[0]
   257  	c.Assert(offerConnection, jc.DeepEquals, &mockOfferConnection{
   258  		sourcemodelUUID: coretesting.ModelTag.Id(),
   259  		relationId:      0,
   260  		relationKey:     "offeredapp:local remote-apptoken:remote",
   261  		username:        "mary",
   262  		offerUUID:       "offer-uuid",
   263  	})
   264  }
   265  
   266  func (s *crossmodelRelationsSuite) TestRegisterRemoteRelations(c *gc.C) {
   267  	s.assertRegisterRemoteRelations(c)
   268  }
   269  
   270  func (s *crossmodelRelationsSuite) TestRegisterRemoteRelationsIdempotent(c *gc.C) {
   271  	s.assertRegisterRemoteRelations(c)
   272  	s.assertRegisterRemoteRelations(c)
   273  }
   274  
   275  func (s *crossmodelRelationsSuite) TestRelationUnitSettings(c *gc.C) {
   276  	djangoRelationUnit := newMockRelationUnit()
   277  	djangoRelationUnit.settings["key"] = "value"
   278  	db2Relation := newMockRelation(123)
   279  	db2Relation.units["django/0"] = djangoRelationUnit
   280  	s.st.relations["db2:db django:db"] = db2Relation
   281  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   282  		offerUUID:       "hosted-db2-uuid",
   283  		sourcemodelUUID: "source-model-uuid",
   284  		relationKey:     "db2:db django:db",
   285  		relationId:      1,
   286  	}
   287  	s.st.remoteEntities[names.NewRelationTag("db2:db django:db")] = "token-db2"
   288  	mac, err := s.bakery.NewMacaroon(
   289  		[]checkers.Caveat{
   290  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   291  			checkers.DeclaredCaveat("relation-key", "db2:db django:db"),
   292  			checkers.DeclaredCaveat("username", "mary"),
   293  		})
   294  
   295  	c.Assert(err, jc.ErrorIsNil)
   296  	result, err := s.api.RelationUnitSettings(params.RemoteRelationUnits{
   297  		RelationUnits: []params.RemoteRelationUnit{{
   298  			RelationToken: "token-db2",
   299  			Unit:          "unit-django-0",
   300  			Macaroons:     macaroon.Slice{mac},
   301  		}},
   302  	})
   303  	c.Assert(err, jc.ErrorIsNil)
   304  	c.Assert(result.Results, jc.DeepEquals, []params.SettingsResult{{Settings: params.Settings{"key": "value"}}})
   305  	s.st.CheckCalls(c, []testing.StubCall{
   306  		{"GetRemoteEntity", []interface{}{"token-db2"}},
   307  		{"KeyRelation", []interface{}{"db2:db django:db"}},
   308  	})
   309  }
   310  
   311  func (s *crossmodelRelationsSuite) TestPublishIngressNetworkChanges(c *gc.C) {
   312  	s.st.remoteApplications["db2"] = &mockRemoteApplication{}
   313  	rel := newMockRelation(1)
   314  	rel.key = "db2:db django:db"
   315  	s.st.relations["db2:db django:db"] = rel
   316  	s.st.remoteEntities[names.NewApplicationTag("db2")] = "token-db2"
   317  	s.st.remoteEntities[names.NewRelationTag("db2:db django:db")] = "token-db2:db django:db"
   318  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   319  		offerUUID:       "hosted-db2-uuid",
   320  		sourcemodelUUID: "source-model-uuid",
   321  		relationKey:     "db2:db django:db",
   322  		relationId:      1,
   323  	}
   324  	mac, err := s.bakery.NewMacaroon(
   325  		[]checkers.Caveat{
   326  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   327  			checkers.DeclaredCaveat("relation-key", "db2:db django:db"),
   328  			checkers.DeclaredCaveat("username", "mary"),
   329  		})
   330  
   331  	c.Assert(err, jc.ErrorIsNil)
   332  	results, err := s.api.PublishIngressNetworkChanges(params.IngressNetworksChanges{
   333  		Changes: []params.IngressNetworksChangeEvent{
   334  			{
   335  				ApplicationToken: "token-db2",
   336  				RelationToken:    "token-db2:db django:db",
   337  				Networks:         []string{"1.2.3.4/32"},
   338  				Macaroons:        macaroon.Slice{mac},
   339  			},
   340  		},
   341  	})
   342  	c.Assert(err, jc.ErrorIsNil)
   343  	err = results.Combine()
   344  	c.Assert(err, jc.ErrorIsNil)
   345  	c.Assert(s.st.ingressNetworks[rel.key], jc.DeepEquals, []string{"1.2.3.4/32"})
   346  	s.st.CheckCalls(c, []testing.StubCall{
   347  		{"GetRemoteEntity", []interface{}{"token-db2:db django:db"}},
   348  		{"KeyRelation", []interface{}{"db2:db django:db"}},
   349  	})
   350  }
   351  
   352  func (s *crossmodelRelationsSuite) TestPublishIngressNetworkChangesRejected(c *gc.C) {
   353  	s.st.remoteApplications["db2"] = &mockRemoteApplication{}
   354  	s.st.relations["db2:db django:db"] = newMockRelation(1)
   355  	s.st.remoteEntities[names.NewApplicationTag("db2")] = "token-db2"
   356  	s.st.remoteEntities[names.NewRelationTag("db2:db django:db")] = "token-db2:db django:db"
   357  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   358  		offerUUID:       "hosted-db2-uuid",
   359  		sourcemodelUUID: "source-model-uuid",
   360  		relationKey:     "db2:db django:db",
   361  		relationId:      1,
   362  	}
   363  	mac, err := s.bakery.NewMacaroon(
   364  		[]checkers.Caveat{
   365  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   366  			checkers.DeclaredCaveat("relation-key", "db2:db django:db"),
   367  			checkers.DeclaredCaveat("username", "mary"),
   368  		})
   369  
   370  	c.Assert(err, jc.ErrorIsNil)
   371  	s.st.firewallRules[state.JujuApplicationOfferRule] = &state.FirewallRule{WhitelistCIDRs: []string{"10.1.1.1/8"}}
   372  	results, err := s.api.PublishIngressNetworkChanges(params.IngressNetworksChanges{
   373  		Changes: []params.IngressNetworksChangeEvent{
   374  			{
   375  				ApplicationToken: "token-db2",
   376  				RelationToken:    "token-db2:db django:db",
   377  				Networks:         []string{"1.2.3.4/32"},
   378  				Macaroons:        macaroon.Slice{mac},
   379  			},
   380  		},
   381  	})
   382  	c.Assert(err, jc.ErrorIsNil)
   383  	err = results.Combine()
   384  	c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta("subnet 1.2.3.4/32 not in firewall whitelist"))
   385  	s.st.CheckCalls(c, []testing.StubCall{
   386  		{"GetRemoteEntity", []interface{}{"token-db2:db django:db"}},
   387  		{"KeyRelation", []interface{}{"db2:db django:db"}},
   388  	})
   389  }
   390  
   391  func (s *crossmodelRelationsSuite) TestWatchEgressAddressesForRelations(c *gc.C) {
   392  	s.st.remoteEntities[names.NewRelationTag("db2:db django:db")] = "token-db2:db django:db"
   393  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   394  		offerUUID:       "hosted-db2-uuid",
   395  		sourcemodelUUID: "source-model-uuid",
   396  		relationKey:     "db2:db django:db",
   397  		relationId:      1,
   398  	}
   399  	mac, err := s.bakery.NewMacaroon(
   400  		[]checkers.Caveat{
   401  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   402  			checkers.DeclaredCaveat("relation-key", "db2:db django:db"),
   403  			checkers.DeclaredCaveat("username", "mary"),
   404  		})
   405  
   406  	c.Assert(err, jc.ErrorIsNil)
   407  	args := params.RemoteEntityArgs{
   408  		Args: []params.RemoteEntityArg{
   409  			{
   410  				Token:     "token-mysql:db django:db",
   411  				Macaroons: macaroon.Slice{mac},
   412  			},
   413  			{
   414  				Token:     "token-db2:db django:db",
   415  				Macaroons: macaroon.Slice{mac},
   416  			},
   417  			{
   418  				Token:     "token-postgresql:db django:db",
   419  				Macaroons: macaroon.Slice{mac},
   420  			},
   421  		},
   422  	}
   423  	results, err := s.api.WatchEgressAddressesForRelations(args)
   424  	c.Assert(err, jc.ErrorIsNil)
   425  	c.Assert(results.Results, gc.HasLen, len(args.Args))
   426  	c.Assert(results.Results[0].Error.ErrorCode(), gc.Equals, params.CodeNotFound)
   427  	c.Assert(results.Results[1].Error, gc.IsNil)
   428  	c.Assert(results.Results[2].Error.ErrorCode(), gc.Equals, params.CodeNotFound)
   429  	c.Assert(s.watchedRelations, jc.DeepEquals, params.Entities{
   430  		Entities: []params.Entity{{Tag: "relation-db2.db#django.db"}}},
   431  	)
   432  	s.st.CheckCalls(c, []testing.StubCall{
   433  		{"GetRemoteEntity", []interface{}{"token-mysql:db django:db"}},
   434  		{"GetRemoteEntity", []interface{}{"token-db2:db django:db"}},
   435  		{"GetRemoteEntity", []interface{}{"token-postgresql:db django:db"}},
   436  	})
   437  	// TODO(wallyworld) - add mre tests when implementation finished
   438  }
   439  
   440  func (s *crossmodelRelationsSuite) TestWatchRelationsStatus(c *gc.C) {
   441  	s.st.remoteEntities[names.NewRelationTag("db2:db django:db")] = "token-db2:db django:db"
   442  	rel := newMockRelation(1)
   443  	s.st.relations["db2:db django:db"] = rel
   444  	s.st.offerConnectionsByKey["db2:db django:db"] = &mockOfferConnection{
   445  		offerUUID:       "hosted-db2-uuid",
   446  		sourcemodelUUID: "source-model-uuid",
   447  		relationKey:     "db2:db django:db",
   448  		relationId:      1,
   449  	}
   450  	mac, err := s.bakery.NewMacaroon(
   451  		[]checkers.Caveat{
   452  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   453  			checkers.DeclaredCaveat("relation-key", "db2:db django:db"),
   454  			checkers.DeclaredCaveat("username", "mary"),
   455  		})
   456  
   457  	c.Assert(err, jc.ErrorIsNil)
   458  	args := params.RemoteEntityArgs{
   459  		Args: []params.RemoteEntityArg{
   460  			{
   461  				Token:     "token-mysql:db django:db",
   462  				Macaroons: macaroon.Slice{mac},
   463  			},
   464  			{
   465  				Token:     "token-db2:db django:db",
   466  				Macaroons: macaroon.Slice{mac},
   467  			},
   468  			{
   469  				Token:     "token-postgresql:db django:db",
   470  				Macaroons: macaroon.Slice{mac},
   471  			},
   472  		},
   473  	}
   474  	results, err := s.api.WatchRelationsSuspendedStatus(args)
   475  	c.Assert(err, jc.ErrorIsNil)
   476  	c.Assert(results.Results, gc.HasLen, len(args.Args))
   477  	c.Assert(results.Results[0].Error.ErrorCode(), gc.Equals, params.CodeNotFound)
   478  	c.Assert(results.Results[1].Error, gc.IsNil)
   479  	c.Assert(results.Results[2].Error.ErrorCode(), gc.Equals, params.CodeNotFound)
   480  	c.Assert(s.watchedRelations, jc.DeepEquals, params.Entities{
   481  		Entities: []params.Entity{{Tag: "relation-db2.db#django.db"}}},
   482  	)
   483  	s.st.CheckCalls(c, []testing.StubCall{
   484  		{"GetRemoteEntity", []interface{}{"token-mysql:db django:db"}},
   485  		{"GetRemoteEntity", []interface{}{"token-db2:db django:db"}},
   486  		{"KeyRelation", []interface{}{"db2:db django:db"}},
   487  		{"GetRemoteEntity", []interface{}{"token-postgresql:db django:db"}},
   488  	})
   489  }
   490  
   491  func (s *crossmodelRelationsSuite) TestWatchOfferStatus(c *gc.C) {
   492  	s.st.offers["mysql-uuid"] = &crossmodel.ApplicationOffer{
   493  		OfferName: "hosted-mysql", OfferUUID: "mysql-uuid", ApplicationName: "mysql"}
   494  	app := &mockApplication{}
   495  	s.st.applications["mysql"] = app
   496  	s.st.remoteEntities[names.NewApplicationOfferTag("hosted-mysql")] = "token-hosted-mysql"
   497  	mac, err := s.bakery.NewMacaroon(
   498  		[]checkers.Caveat{
   499  			checkers.DeclaredCaveat("source-model-uuid", s.st.ModelUUID()),
   500  			checkers.DeclaredCaveat("offer-uuid", "mysql-uuid"),
   501  			checkers.DeclaredCaveat("username", "mary"),
   502  		})
   503  
   504  	c.Assert(err, jc.ErrorIsNil)
   505  	args := params.OfferArgs{
   506  		Args: []params.OfferArg{
   507  			{
   508  				OfferUUID: "db2-uuid",
   509  				Macaroons: macaroon.Slice{mac},
   510  			},
   511  			{
   512  				OfferUUID: "mysql-uuid",
   513  				Macaroons: macaroon.Slice{mac},
   514  			},
   515  			{
   516  				OfferUUID: "postgresql-uuid",
   517  				Macaroons: macaroon.Slice{mac},
   518  			},
   519  		},
   520  	}
   521  	results, err := s.api.WatchOfferStatus(args)
   522  	c.Assert(err, jc.ErrorIsNil)
   523  	c.Assert(results.Results, gc.HasLen, len(args.Args))
   524  	c.Assert(results.Results[0].Error.ErrorCode(), gc.Equals, params.CodeUnauthorized)
   525  	c.Assert(results.Results[1].Error, gc.IsNil)
   526  	c.Assert(results.Results[2].Error.ErrorCode(), gc.Equals, params.CodeUnauthorized)
   527  	c.Assert(s.watchedOffers, jc.DeepEquals, []string{"mysql-uuid"})
   528  	s.st.CheckCalls(c, []testing.StubCall{
   529  		{"Application", []interface{}{"mysql"}},
   530  	})
   531  	app.CheckCalls(c, []testing.StubCall{
   532  		{"Status", nil},
   533  	})
   534  }