github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/state/txns_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "errors" 8 9 jc "github.com/juju/testing/checkers" 10 jujutxn "github.com/juju/txn" 11 gc "gopkg.in/check.v1" 12 "gopkg.in/mgo.v2/bson" 13 "gopkg.in/mgo.v2/txn" 14 15 "github.com/juju/juju/testing" 16 ) 17 18 type MultiModelRunnerSuite struct { 19 testing.BaseSuite 20 multiModelRunner jujutxn.Runner 21 testRunner *recordingRunner 22 } 23 24 var _ = gc.Suite(&MultiModelRunnerSuite{}) 25 26 // A fixed attempt counter value used to verify this is passed through 27 // in Run() 28 const ( 29 testTxnAttempt = 42 30 modelUUID = "uuid" 31 ) 32 33 func (s *MultiModelRunnerSuite) SetUpTest(c *gc.C) { 34 s.BaseSuite.SetUpTest(c) 35 s.testRunner = &recordingRunner{} 36 s.multiModelRunner = &multiModelRunner{ 37 rawRunner: s.testRunner, 38 modelUUID: modelUUID, 39 schema: collectionSchema{ 40 logsC: {}, 41 machinesC: {}, 42 modelsC: {global: true}, 43 "other": {global: true}, 44 "raw": {rawAccess: true}, 45 }, 46 } 47 } 48 49 type testDoc struct { 50 DocID string `bson:"_id"` 51 Id string `bson:"thingid"` 52 ModelUUID string `bson:"model-uuid"` 53 } 54 55 // An alternative machine document to test that fields are matched by 56 // struct tag. 57 type altTestDoc struct { 58 Identifier string `bson:"_id"` 59 Model string `bson:"model-uuid"` 60 } 61 62 type multiModelRunnerTestCase struct { 63 label string 64 input txn.Op 65 expected txn.Op 66 } 67 68 // Test cases are returned by a function because transaction 69 // operations are modified in place and can't be safely reused by 70 // multiple tests. 71 func getTestCases() []multiModelRunnerTestCase { 72 return []multiModelRunnerTestCase{ 73 { 74 "ops for non-multi env collections are left alone", 75 txn.Op{ 76 C: "other", 77 Id: "whatever", 78 Insert: bson.M{"_id": "whatever"}, 79 }, 80 txn.Op{ 81 C: "other", 82 Id: "whatever", 83 Insert: bson.M{"_id": "whatever"}, 84 }, 85 }, { 86 "env UUID added to doc", 87 txn.Op{ 88 C: machinesC, 89 Id: "0", 90 Insert: &testDoc{ 91 DocID: "0", 92 Id: "0", 93 }, 94 }, 95 txn.Op{ 96 C: machinesC, 97 Id: "uuid:0", 98 Insert: bson.D{ 99 {"_id", "uuid:0"}, 100 {"thingid", "0"}, 101 {"model-uuid", "uuid"}, 102 }, 103 }, 104 }, { 105 "fields matched by struct tag, not field name", 106 txn.Op{ 107 C: machinesC, 108 Id: "2", 109 Insert: &altTestDoc{ 110 Identifier: "2", 111 Model: "", 112 }, 113 }, 114 txn.Op{ 115 C: machinesC, 116 Id: "uuid:2", 117 Insert: bson.D{ 118 {"_id", "uuid:2"}, 119 {"model-uuid", "uuid"}, 120 }, 121 }, 122 }, { 123 "doc passed as struct value", 124 txn.Op{ 125 C: machinesC, 126 Id: "3", 127 // Passed by value 128 Insert: testDoc{ 129 DocID: "3", 130 Id: "3", 131 }, 132 }, 133 txn.Op{ 134 C: machinesC, 135 Id: "uuid:3", 136 Insert: bson.D{ 137 {"_id", "uuid:3"}, 138 {"thingid", "3"}, 139 {"model-uuid", "uuid"}, 140 }, 141 }, 142 }, { 143 "document passed as bson.D", 144 txn.Op{ 145 C: machinesC, 146 Id: "4", 147 Insert: bson.D{{"_id", "4"}}, 148 }, 149 txn.Op{ 150 C: machinesC, 151 Id: "uuid:4", 152 Insert: bson.D{ 153 {"_id", "uuid:4"}, 154 {"model-uuid", "uuid"}, 155 }, 156 }, 157 }, { 158 "document passed as bson.M", 159 txn.Op{ 160 C: machinesC, 161 Id: "5", 162 Insert: bson.M{"_id": "5"}, 163 }, 164 txn.Op{ 165 C: machinesC, 166 Id: "uuid:5", 167 Insert: bson.D{ 168 {"_id", "uuid:5"}, 169 {"model-uuid", "uuid"}, 170 }, 171 }, 172 }, { 173 "document passed as map[string]interface{}", 174 txn.Op{ 175 C: machinesC, 176 Id: "5", 177 Insert: map[string]interface{}{}, 178 }, 179 txn.Op{ 180 C: machinesC, 181 Id: "uuid:5", 182 Insert: bson.D{ 183 {"model-uuid", "uuid"}, 184 }, 185 }, 186 }, { 187 "bson.D $set with struct update", 188 txn.Op{ 189 C: machinesC, 190 Id: "1", 191 Update: bson.D{{"$set", &testDoc{ 192 DocID: "1", 193 Id: "1", 194 }}}, 195 }, 196 txn.Op{ 197 C: machinesC, 198 Id: "uuid:1", 199 Update: bson.D{{"$set", 200 bson.D{ 201 {"_id", "uuid:1"}, 202 {"thingid", "1"}, 203 {"model-uuid", "uuid"}, 204 }, 205 }}, 206 }, 207 }, { 208 "bson.D $set with bson.D update", 209 txn.Op{ 210 C: machinesC, 211 Id: "1", 212 Update: bson.D{ 213 {"$set", bson.D{ 214 {"_id", "1"}, 215 {"foo", "bar"}, 216 }}, 217 {"$other", "op"}, 218 }, 219 }, 220 txn.Op{ 221 C: machinesC, 222 Id: "uuid:1", 223 Update: bson.D{ 224 {"$set", bson.D{ 225 {"_id", "uuid:1"}, 226 {"foo", "bar"}, 227 }}, 228 {"$other", "op"}, 229 }, 230 }, 231 }, { 232 "bson.M $set", 233 txn.Op{ 234 C: machinesC, 235 Id: "1", 236 Update: bson.M{ 237 "$set": bson.M{"_id": "1"}, 238 "$foo": "bar", 239 }, 240 }, 241 txn.Op{ 242 C: machinesC, 243 Id: "uuid:1", 244 Update: bson.M{ 245 "$set": bson.D{{"_id", "uuid:1"}}, 246 "$foo": "bar", 247 }, 248 }, 249 }, 250 } 251 } 252 253 func (s *MultiModelRunnerSuite) TestRunTransaction(c *gc.C) { 254 for i, t := range getTestCases() { 255 c.Logf("TestRunTransaction %d: %s", i, t.label) 256 257 inOps := []txn.Op{t.input} 258 err := s.multiModelRunner.RunTransaction(inOps) 259 c.Assert(err, jc.ErrorIsNil) 260 261 expected := []txn.Op{t.expected} 262 263 // Check ops seen by underlying runner. 264 c.Check(s.testRunner.seenOps, gc.DeepEquals, expected) 265 } 266 } 267 268 func (s *MultiModelRunnerSuite) TestMultipleOps(c *gc.C) { 269 var inOps []txn.Op 270 var expectedOps []txn.Op 271 for _, t := range getTestCases() { 272 inOps = append(inOps, t.input) 273 expectedOps = append(expectedOps, t.expected) 274 } 275 276 err := s.multiModelRunner.RunTransaction(inOps) 277 c.Assert(err, jc.ErrorIsNil) 278 279 c.Assert(s.testRunner.seenOps, gc.DeepEquals, expectedOps) 280 } 281 282 type objIdDoc struct { 283 Id bson.ObjectId `bson:"_id"` 284 ModelUUID string `bson:"model-uuid"` 285 } 286 287 func (s *MultiModelRunnerSuite) TestWithObjectIds(c *gc.C) { 288 id := bson.NewObjectId() 289 inOps := []txn.Op{{ 290 C: logsC, 291 Id: id, 292 Insert: &objIdDoc{Id: id}, 293 }} 294 295 err := s.multiModelRunner.RunTransaction(inOps) 296 c.Assert(err, jc.ErrorIsNil) 297 298 expectedOps := []txn.Op{{ 299 C: logsC, 300 Id: id, 301 Insert: bson.D{ 302 {"_id", id}, 303 {"model-uuid", "uuid"}, 304 }, 305 }} 306 c.Assert(s.testRunner.seenOps, gc.DeepEquals, expectedOps) 307 } 308 309 func (s *MultiModelRunnerSuite) TestRejectsAttemptToInsertWrongModelUUID(c *gc.C) { 310 ops := []txn.Op{{ 311 C: machinesC, 312 Id: "1", 313 Insert: &machineDoc{}, 314 }} 315 err := s.multiModelRunner.RunTransaction(ops) 316 c.Assert(err, jc.ErrorIsNil) 317 318 ops = []txn.Op{{ 319 C: machinesC, 320 Id: "1", 321 Insert: &machineDoc{ 322 ModelUUID: "wrong", 323 }, 324 }} 325 err = s.multiModelRunner.RunTransaction(ops) 326 c.Assert(err, gc.ErrorMatches, `cannot insert into "machines": bad "model-uuid" value.+`) 327 } 328 329 func (s *MultiModelRunnerSuite) TestRejectsAttemptToChangeModelUUID(c *gc.C) { 330 // Setting to same env UUID is ok. 331 ops := []txn.Op{{ 332 C: machinesC, 333 Id: "1", 334 Update: bson.M{"$set": &machineDoc{ModelUUID: modelUUID}}, 335 }} 336 err := s.multiModelRunner.RunTransaction(ops) 337 c.Assert(err, jc.ErrorIsNil) 338 339 // Using the wrong env UUID isn't allowed. 340 ops = []txn.Op{{ 341 C: machinesC, 342 Id: "1", 343 Update: bson.M{"$set": &machineDoc{ModelUUID: "wrong"}}, 344 }} 345 err = s.multiModelRunner.RunTransaction(ops) 346 c.Assert(err, gc.ErrorMatches, `cannot update "machines": bad "model-uuid" value.+`) 347 } 348 349 func (s *MultiModelRunnerSuite) TestDoesNotAssertReferencedEnv(c *gc.C) { 350 err := s.multiModelRunner.RunTransaction([]txn.Op{{ 351 C: modelsC, 352 Id: modelUUID, 353 Insert: bson.M{}, 354 }}) 355 c.Check(err, jc.ErrorIsNil) 356 c.Check(s.testRunner.seenOps, jc.DeepEquals, []txn.Op{{ 357 C: modelsC, 358 Id: modelUUID, 359 Insert: bson.M{}, 360 }}) 361 } 362 363 func (s *MultiModelRunnerSuite) TestRejectRawAccessCollection(c *gc.C) { 364 err := s.multiModelRunner.RunTransaction([]txn.Op{{ 365 C: "raw", 366 Id: "whatever", 367 Assert: bson.D{{"any", "thing"}}, 368 }}) 369 c.Check(err, gc.ErrorMatches, `forbidden transaction: references raw-access collection "raw"`) 370 c.Check(s.testRunner.seenOps, gc.IsNil) 371 } 372 373 func (s *MultiModelRunnerSuite) TestRejectUnknownCollection(c *gc.C) { 374 err := s.multiModelRunner.RunTransaction([]txn.Op{{ 375 C: "unknown", 376 Id: "whatever", 377 Assert: bson.D{{"any", "thing"}}, 378 }}) 379 c.Check(err, gc.ErrorMatches, `forbidden transaction: references unknown collection "unknown"`) 380 c.Check(s.testRunner.seenOps, gc.IsNil) 381 } 382 383 func (s *MultiModelRunnerSuite) TestRejectStructModelUUIDMismatch(c *gc.C) { 384 err := s.multiModelRunner.RunTransaction([]txn.Op{{ 385 C: machinesC, 386 Id: "uuid:0", 387 Insert: &machineDoc{ 388 DocID: "uuid:0", 389 ModelUUID: "somethingelse", 390 }, 391 }}) 392 c.Check(err, gc.ErrorMatches, 393 `cannot insert into "machines": bad "model-uuid" value: expected uuid, got somethingelse`) 394 c.Check(s.testRunner.seenOps, gc.IsNil) 395 } 396 397 func (s *MultiModelRunnerSuite) TestRejectBsonDModelUUIDMismatch(c *gc.C) { 398 err := s.multiModelRunner.RunTransaction([]txn.Op{{ 399 C: machinesC, 400 Id: "uuid:0", 401 Insert: bson.D{{"model-uuid", "wtf"}}, 402 }}) 403 c.Check(err, gc.ErrorMatches, 404 `cannot insert into "machines": bad "model-uuid" value: expected uuid, got wtf`) 405 c.Check(s.testRunner.seenOps, gc.IsNil) 406 } 407 408 func (s *MultiModelRunnerSuite) TestRejectBsonMModelUUIDMismatch(c *gc.C) { 409 err := s.multiModelRunner.RunTransaction([]txn.Op{{ 410 C: machinesC, 411 Id: "uuid:0", 412 Insert: bson.M{"model-uuid": "wtf"}, 413 }}) 414 c.Check(err, gc.ErrorMatches, 415 `cannot insert into "machines": bad "model-uuid" value: expected uuid, got wtf`) 416 c.Check(s.testRunner.seenOps, gc.IsNil) 417 } 418 419 func (s *MultiModelRunnerSuite) TestRun(c *gc.C) { 420 for i, t := range getTestCases() { 421 c.Logf("TestRun %d: %s", i, t.label) 422 423 var seenAttempt int 424 err := s.multiModelRunner.Run(func(attempt int) ([]txn.Op, error) { 425 seenAttempt = attempt 426 return []txn.Op{t.input}, nil 427 }) 428 c.Assert(err, jc.ErrorIsNil) 429 430 c.Check(seenAttempt, gc.Equals, testTxnAttempt) 431 c.Check(s.testRunner.seenOps, gc.DeepEquals, []txn.Op{t.expected}) 432 } 433 } 434 435 func (s *MultiModelRunnerSuite) TestRunWithError(c *gc.C) { 436 err := s.multiModelRunner.Run(func(attempt int) ([]txn.Op, error) { 437 return nil, errors.New("boom") 438 }) 439 c.Check(err, gc.ErrorMatches, "boom") 440 c.Check(s.testRunner.seenOps, gc.IsNil) 441 } 442 443 func (s *MultiModelRunnerSuite) TestResumeTransactions(c *gc.C) { 444 err := s.multiModelRunner.ResumeTransactions() 445 c.Check(err, jc.ErrorIsNil) 446 c.Check(s.testRunner.resumeTransactionsCalled, jc.IsTrue) 447 } 448 449 func (s *MultiModelRunnerSuite) TestResumeTransactionsWithError(c *gc.C) { 450 s.testRunner.resumeTransactionsErr = errors.New("boom") 451 err := s.multiModelRunner.ResumeTransactions() 452 c.Check(err, gc.ErrorMatches, "boom") 453 } 454 455 func (s *MultiModelRunnerSuite) TestMaybePruneTransactions(c *gc.C) { 456 err := s.multiModelRunner.MaybePruneTransactions(2.0) 457 c.Check(err, jc.ErrorIsNil) 458 c.Check(s.testRunner.pruneTransactionsCalled, jc.IsTrue) 459 } 460 461 func (s *MultiModelRunnerSuite) TestMaybePruneTransactionsWithError(c *gc.C) { 462 s.testRunner.pruneTransactionsErr = errors.New("boom") 463 err := s.multiModelRunner.MaybePruneTransactions(2.0) 464 c.Check(err, gc.ErrorMatches, "boom") 465 } 466 467 // recordingRunner is fake transaction running that implements the 468 // jujutxn.Runner interface. Instead of doing anything with a database 469 // it simply records the transaction operations passed to it for later 470 // inspection. 471 // 472 // Note that a recordingRunner is only good for a single test because 473 // seenOps is overwritten for each call to RunTransaction and Run. A 474 // fresh instance should be created for each test. 475 type recordingRunner struct { 476 seenOps []txn.Op 477 resumeTransactionsCalled bool 478 resumeTransactionsErr error 479 pruneTransactionsCalled bool 480 pruneTransactionsErr error 481 } 482 483 func (r *recordingRunner) RunTransaction(ops []txn.Op) error { 484 r.seenOps = ops 485 return nil 486 } 487 488 func (r *recordingRunner) Run(transactions jujutxn.TransactionSource) (err error) { 489 r.seenOps, err = transactions(testTxnAttempt) 490 return 491 } 492 493 func (r *recordingRunner) ResumeTransactions() error { 494 r.resumeTransactionsCalled = true 495 return r.resumeTransactionsErr 496 } 497 498 func (r *recordingRunner) MaybePruneTransactions(float32) error { 499 r.pruneTransactionsCalled = true 500 return r.pruneTransactionsErr 501 }