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 }