github.com/nokia/migrate/v4@v4.16.0/database/mongodb/mongodb_test.go (about)

     1  package mongodb
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"strconv"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/dhui/dktest"
    15  	"github.com/nokia/migrate/v4"
    16  	"go.mongodb.org/mongo-driver/bson"
    17  	"go.mongodb.org/mongo-driver/mongo"
    18  	"go.mongodb.org/mongo-driver/mongo/options"
    19  
    20  	dt "github.com/nokia/migrate/v4/database/testing"
    21  	"github.com/nokia/migrate/v4/dktesting"
    22  
    23  	_ "github.com/nokia/migrate/v4/source/file"
    24  )
    25  
    26  var (
    27  	opts = dktest.Options{PortRequired: true, ReadyFunc: isReady}
    28  	// Supported versions: https://www.mongodb.com/support-policy
    29  	specs = []dktesting.ContainerSpec{
    30  		{ImageName: "mongo:3.4", Options: opts},
    31  		{ImageName: "mongo:3.6", Options: opts},
    32  		{ImageName: "mongo:4.0", Options: opts},
    33  		{ImageName: "mongo:4.2", Options: opts},
    34  	}
    35  )
    36  
    37  func mongoConnectionString(host, port string) string {
    38  	// there is connect option for excluding serverConnection algorithm
    39  	// it's let avoid errors with mongo replica set connection in docker container
    40  	return fmt.Sprintf("mongodb://%s:%s/testMigration?connect=direct", host, port)
    41  }
    42  
    43  func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
    44  	ip, port, err := c.FirstPort()
    45  	if err != nil {
    46  		return false
    47  	}
    48  
    49  	client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoConnectionString(ip, port)))
    50  	if err != nil {
    51  		return false
    52  	}
    53  	defer func() {
    54  		if err := client.Disconnect(ctx); err != nil {
    55  			log.Println("close error:", err)
    56  		}
    57  	}()
    58  
    59  	if err = client.Ping(ctx, nil); err != nil {
    60  		switch err {
    61  		case io.EOF:
    62  			return false
    63  		default:
    64  			log.Println(err)
    65  		}
    66  		return false
    67  	}
    68  	return true
    69  }
    70  
    71  func Test(t *testing.T) {
    72  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
    73  		ip, port, err := c.FirstPort()
    74  		if err != nil {
    75  			t.Fatal(err)
    76  		}
    77  
    78  		addr := mongoConnectionString(ip, port)
    79  		p := &Mongo{}
    80  		d, err := p.Open(addr)
    81  		if err != nil {
    82  			t.Fatal(err)
    83  		}
    84  		defer func() {
    85  			if err := d.Close(); err != nil {
    86  				t.Error(err)
    87  			}
    88  		}()
    89  		dt.TestNilVersion(t, d)
    90  		dt.TestLockAndUnlock(t, d)
    91  		dt.TestRun(t, d, bytes.NewReader([]byte(`[{"insert":"hello","documents":[{"wild":"world"}]}]`)))
    92  		dt.TestSetVersion(t, d)
    93  		dt.TestDrop(t, d)
    94  	})
    95  }
    96  
    97  func TestMigrate(t *testing.T) {
    98  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
    99  		ip, port, err := c.FirstPort()
   100  		if err != nil {
   101  			t.Fatal(err)
   102  		}
   103  
   104  		addr := mongoConnectionString(ip, port)
   105  		p := &Mongo{}
   106  		d, err := p.Open(addr)
   107  		if err != nil {
   108  			t.Fatal(err)
   109  		}
   110  		defer func() {
   111  			if err := d.Close(); err != nil {
   112  				t.Error(err)
   113  			}
   114  		}()
   115  		m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "", d)
   116  		if err != nil {
   117  			t.Fatal(err)
   118  		}
   119  		dt.TestMigrate(t, m)
   120  	})
   121  }
   122  
   123  func TestWithAuth(t *testing.T) {
   124  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
   125  		ip, port, err := c.FirstPort()
   126  		if err != nil {
   127  			t.Fatal(err)
   128  		}
   129  
   130  		addr := mongoConnectionString(ip, port)
   131  		p := &Mongo{}
   132  		d, err := p.Open(addr)
   133  		if err != nil {
   134  			t.Fatal(err)
   135  		}
   136  		defer func() {
   137  			if err := d.Close(); err != nil {
   138  				t.Error(err)
   139  			}
   140  		}()
   141  		createUserCMD := []byte(`[{"createUser":"deminem","pwd":"gogo","roles":[{"role":"readWrite","db":"testMigration"}]}]`)
   142  		err = d.Run(bytes.NewReader(createUserCMD))
   143  		if err != nil {
   144  			t.Fatal(err)
   145  		}
   146  		testcases := []struct {
   147  			name            string
   148  			connectUri      string
   149  			isErrorExpected bool
   150  		}{
   151  			{"right auth data", "mongodb://deminem:gogo@%s:%v/testMigration", false},
   152  			{"wrong auth data", "mongodb://wrong:auth@%s:%v/testMigration", true},
   153  		}
   154  
   155  		for _, tcase := range testcases {
   156  			t.Run(tcase.name, func(t *testing.T) {
   157  				mc := &Mongo{}
   158  				d, err := mc.Open(fmt.Sprintf(tcase.connectUri, ip, port))
   159  				if err == nil {
   160  					defer func() {
   161  						if err := d.Close(); err != nil {
   162  							t.Error(err)
   163  						}
   164  					}()
   165  				}
   166  
   167  				switch {
   168  				case tcase.isErrorExpected && err == nil:
   169  					t.Fatalf("no error when expected")
   170  				case !tcase.isErrorExpected && err != nil:
   171  					t.Fatalf("unexpected error: %v", err)
   172  				}
   173  			})
   174  		}
   175  	})
   176  }
   177  
   178  func TestLockWorks(t *testing.T) {
   179  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
   180  		ip, port, err := c.FirstPort()
   181  		if err != nil {
   182  			t.Fatal(err)
   183  		}
   184  
   185  		addr := mongoConnectionString(ip, port)
   186  		p := &Mongo{}
   187  		d, err := p.Open(addr)
   188  		if err != nil {
   189  			t.Fatal(err)
   190  		}
   191  		defer func() {
   192  			if err := d.Close(); err != nil {
   193  				t.Error(err)
   194  			}
   195  		}()
   196  
   197  		dt.TestRun(t, d, bytes.NewReader([]byte(`[{"insert":"hello","documents":[{"wild":"world"}]}]`)))
   198  
   199  		mc := d.(*Mongo)
   200  
   201  		err = mc.Lock()
   202  		if err != nil {
   203  			t.Fatal(err)
   204  		}
   205  		err = mc.Unlock()
   206  		if err != nil {
   207  			t.Fatal(err)
   208  		}
   209  
   210  		err = mc.Lock()
   211  		if err != nil {
   212  			t.Fatal(err)
   213  		}
   214  		err = mc.Unlock()
   215  		if err != nil {
   216  			t.Fatal(err)
   217  		}
   218  
   219  		// enable locking,
   220  		// try to hit a lock conflict
   221  		mc.config.Locking.Enabled = true
   222  		mc.config.Locking.Timeout = 1
   223  		err = mc.Lock()
   224  		if err != nil {
   225  			t.Fatal(err)
   226  		}
   227  		err = mc.Lock()
   228  		if err == nil {
   229  			t.Fatal("should have failed, mongo should be locked already")
   230  		}
   231  	})
   232  }
   233  
   234  func TestTransaction(t *testing.T) {
   235  	transactionSpecs := []dktesting.ContainerSpec{
   236  		{ImageName: "mongo:4", Options: dktest.Options{
   237  			PortRequired: true, ReadyFunc: isReady,
   238  			Cmd: []string{"mongod", "--bind_ip_all", "--replSet", "rs0"},
   239  		}},
   240  	}
   241  	dktesting.ParallelTest(t, transactionSpecs, func(t *testing.T, c dktest.ContainerInfo) {
   242  		ip, port, err := c.FirstPort()
   243  		if err != nil {
   244  			t.Fatal(err)
   245  		}
   246  
   247  		client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(mongoConnectionString(ip, port)))
   248  		if err != nil {
   249  			t.Fatal(err)
   250  		}
   251  		err = client.Ping(context.TODO(), nil)
   252  		if err != nil {
   253  			t.Fatal(err)
   254  		}
   255  		// rs.initiate()
   256  		err = client.Database("admin").RunCommand(context.TODO(), bson.D{bson.E{Key: "replSetInitiate", Value: bson.D{}}}).Err()
   257  		if err != nil {
   258  			t.Fatal(err)
   259  		}
   260  		err = waitForReplicaInit(client)
   261  		if err != nil {
   262  			t.Fatal(err)
   263  		}
   264  		d, err := WithInstance(client, &Config{
   265  			DatabaseName: "testMigration",
   266  		})
   267  		if err != nil {
   268  			t.Fatal(err)
   269  		}
   270  		defer func() {
   271  			if err := d.Close(); err != nil {
   272  				t.Error(err)
   273  			}
   274  		}()
   275  		// We have to create collection
   276  		// transactions don't support operations with creating new dbs, collections
   277  		// Unique index need for checking transaction aborting
   278  		insertCMD := []byte(`[
   279  				{"create":"hello"},
   280  				{"createIndexes": "hello",
   281  					"indexes": [{
   282  						"key": {
   283  							"wild": 1
   284  						},
   285  						"name": "unique_wild",
   286  						"unique": true,
   287  						"background": true
   288  					}]
   289  			}]`)
   290  		err = d.Run(bytes.NewReader(insertCMD))
   291  		if err != nil {
   292  			t.Fatal(err)
   293  		}
   294  		testcases := []struct {
   295  			name            string
   296  			cmds            []byte
   297  			documentsCount  int64
   298  			isErrorExpected bool
   299  		}{
   300  			{
   301  				name: "success transaction",
   302  				cmds: []byte(`[{"insert":"hello","documents":[
   303  										{"wild":"world"},
   304  										{"wild":"west"},
   305  										{"wild":"natural"}
   306  									 ]
   307  								  }]`),
   308  				documentsCount:  3,
   309  				isErrorExpected: false,
   310  			},
   311  			{
   312  				name: "failure transaction",
   313  				// transaction have to be failure - duplicate unique key wild:west
   314  				// none of the documents should be added
   315  				cmds: []byte(`[{"insert":"hello","documents":[{"wild":"flower"}]},
   316  									{"insert":"hello","documents":[
   317  										{"wild":"cat"},
   318  										{"wild":"west"}
   319  									 ]
   320  								  }]`),
   321  				documentsCount:  3,
   322  				isErrorExpected: true,
   323  			},
   324  		}
   325  		for _, tcase := range testcases {
   326  			t.Run(tcase.name, func(t *testing.T) {
   327  				client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(mongoConnectionString(ip, port)))
   328  				if err != nil {
   329  					t.Fatal(err)
   330  				}
   331  				err = client.Ping(context.TODO(), nil)
   332  				if err != nil {
   333  					t.Fatal(err)
   334  				}
   335  				d, err := WithInstance(client, &Config{
   336  					DatabaseName:    "testMigration",
   337  					TransactionMode: true,
   338  				})
   339  				if err != nil {
   340  					t.Fatal(err)
   341  				}
   342  				defer func() {
   343  					if err := d.Close(); err != nil {
   344  						t.Error(err)
   345  					}
   346  				}()
   347  				runErr := d.Run(bytes.NewReader(tcase.cmds))
   348  				if runErr != nil {
   349  					if !tcase.isErrorExpected {
   350  						t.Fatal(runErr)
   351  					}
   352  				}
   353  				documentsCount, err := client.Database("testMigration").Collection("hello").CountDocuments(context.TODO(), bson.M{})
   354  				if err != nil {
   355  					t.Fatal(err)
   356  				}
   357  				if tcase.documentsCount != documentsCount {
   358  					t.Fatalf("expected %d and actual %d documents count not equal. run migration error:%s", tcase.documentsCount, documentsCount, runErr)
   359  				}
   360  			})
   361  		}
   362  	})
   363  }
   364  
   365  type isMaster struct {
   366  	IsMaster bool `bson:"ismaster"`
   367  }
   368  
   369  func waitForReplicaInit(client *mongo.Client) error {
   370  	ticker := time.NewTicker(time.Second * 1)
   371  	defer ticker.Stop()
   372  	timeout, err := strconv.Atoi(os.Getenv("MIGRATE_TEST_MONGO_REPLICA_SET_INIT_TIMEOUT"))
   373  	if err != nil {
   374  		timeout = 30
   375  	}
   376  	timeoutTimer := time.NewTimer(time.Duration(timeout) * time.Second)
   377  	defer timeoutTimer.Stop()
   378  	for {
   379  		select {
   380  		case <-ticker.C:
   381  			var status isMaster
   382  			// Check that node is primary because
   383  			// during replica set initialization, the first node first becomes a secondary and then becomes the primary
   384  			// should consider that initialization is completed only after the node has become the primary
   385  			result := client.Database("admin").RunCommand(context.TODO(), bson.D{bson.E{Key: "isMaster", Value: 1}})
   386  			r, err := result.DecodeBytes()
   387  			if err != nil {
   388  				return err
   389  			}
   390  			err = bson.Unmarshal(r, &status)
   391  			if err != nil {
   392  				return err
   393  			}
   394  			if status.IsMaster {
   395  				return nil
   396  			}
   397  		case <-timeoutTimer.C:
   398  			return fmt.Errorf("replica init timeout")
   399  		}
   400  	}
   401  }