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  }