github.com/seashell-org/golang-migrate/v4@v4.15.3-0.20220722221203-6ab6c6c062d1/database/mongodb/mongodb_test.go (about)

     1  package mongodb
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  
     8  	"log"
     9  
    10  	"io"
    11  	"os"
    12  	"strconv"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/dhui/dktest"
    17  	migrate "github.com/seashell-org/golang-migrate/v4"
    18  	"go.mongodb.org/mongo-driver/bson"
    19  	"go.mongodb.org/mongo-driver/mongo"
    20  	"go.mongodb.org/mongo-driver/mongo/options"
    21  
    22  	dt "github.com/seashell-org/golang-migrate/v4/database/testing"
    23  	"github.com/seashell-org/golang-migrate/v4/dktesting"
    24  
    25  	_ "github.com/seashell-org/golang-migrate/v4/source/file"
    26  )
    27  
    28  var (
    29  	opts = dktest.Options{PortRequired: true, ReadyFunc: isReady}
    30  	// Supported versions: https://www.mongodb.com/support-policy
    31  	specs = []dktesting.ContainerSpec{
    32  		{ImageName: "mongo:3.4", Options: opts},
    33  		{ImageName: "mongo:3.6", Options: opts},
    34  		{ImageName: "mongo:4.0", Options: opts},
    35  		{ImageName: "mongo:4.2", Options: opts},
    36  	}
    37  )
    38  
    39  func mongoConnectionString(host, port string) string {
    40  	// there is connect option for excluding serverConnection algorithm
    41  	// it's let avoid errors with mongo replica set connection in docker container
    42  	return fmt.Sprintf("mongodb://%s:%s/testMigration?connect=direct", host, port)
    43  }
    44  
    45  func isReady(ctx context.Context, c dktest.ContainerInfo) bool {
    46  	ip, port, err := c.FirstPort()
    47  	if err != nil {
    48  		return false
    49  	}
    50  
    51  	client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoConnectionString(ip, port)))
    52  	if err != nil {
    53  		return false
    54  	}
    55  	defer func() {
    56  		if err := client.Disconnect(ctx); err != nil {
    57  			log.Println("close error:", err)
    58  		}
    59  	}()
    60  
    61  	if err = client.Ping(ctx, nil); err != nil {
    62  		switch err {
    63  		case io.EOF:
    64  			return false
    65  		default:
    66  			log.Println(err)
    67  		}
    68  		return false
    69  	}
    70  	return true
    71  }
    72  
    73  func Test(t *testing.T) {
    74  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
    75  		ip, port, err := c.FirstPort()
    76  		if err != nil {
    77  			t.Fatal(err)
    78  		}
    79  
    80  		addr := mongoConnectionString(ip, port)
    81  		p := &Mongo{}
    82  		d, err := p.Open(addr)
    83  		if err != nil {
    84  			t.Fatal(err)
    85  		}
    86  		defer func() {
    87  			if err := d.Close(); err != nil {
    88  				t.Error(err)
    89  			}
    90  		}()
    91  		dt.TestNilVersion(t, d)
    92  		dt.TestLockAndUnlock(t, d)
    93  		dt.TestRun(t, d, bytes.NewReader([]byte(`[{"insert":"hello","documents":[{"wild":"world"}]}]`)))
    94  		dt.TestSetVersion(t, d)
    95  		dt.TestDrop(t, d)
    96  	})
    97  }
    98  
    99  func TestMigrate(t *testing.T) {
   100  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
   101  		ip, port, err := c.FirstPort()
   102  		if err != nil {
   103  			t.Fatal(err)
   104  		}
   105  
   106  		addr := mongoConnectionString(ip, port)
   107  		p := &Mongo{}
   108  		d, err := p.Open(addr)
   109  		if err != nil {
   110  			t.Fatal(err)
   111  		}
   112  		defer func() {
   113  			if err := d.Close(); err != nil {
   114  				t.Error(err)
   115  			}
   116  		}()
   117  		m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "", d)
   118  		if err != nil {
   119  			t.Fatal(err)
   120  		}
   121  		dt.TestMigrate(t, m)
   122  	})
   123  }
   124  
   125  func TestWithAuth(t *testing.T) {
   126  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
   127  		ip, port, err := c.FirstPort()
   128  		if err != nil {
   129  			t.Fatal(err)
   130  		}
   131  
   132  		addr := mongoConnectionString(ip, port)
   133  		p := &Mongo{}
   134  		d, err := p.Open(addr)
   135  		if err != nil {
   136  			t.Fatal(err)
   137  		}
   138  		defer func() {
   139  			if err := d.Close(); err != nil {
   140  				t.Error(err)
   141  			}
   142  		}()
   143  		createUserCMD := []byte(`[{"createUser":"deminem","pwd":"gogo","roles":[{"role":"readWrite","db":"testMigration"}]}]`)
   144  		err = d.Run(bytes.NewReader(createUserCMD))
   145  		if err != nil {
   146  			t.Fatal(err)
   147  		}
   148  		testcases := []struct {
   149  			name            string
   150  			connectUri      string
   151  			isErrorExpected bool
   152  		}{
   153  			{"right auth data", "mongodb://deminem:gogo@%s:%v/testMigration", false},
   154  			{"wrong auth data", "mongodb://wrong:auth@%s:%v/testMigration", true},
   155  		}
   156  
   157  		for _, tcase := range testcases {
   158  			t.Run(tcase.name, func(t *testing.T) {
   159  				mc := &Mongo{}
   160  				d, err := mc.Open(fmt.Sprintf(tcase.connectUri, ip, port))
   161  				if err == nil {
   162  					defer func() {
   163  						if err := d.Close(); err != nil {
   164  							t.Error(err)
   165  						}
   166  					}()
   167  				}
   168  
   169  				switch {
   170  				case tcase.isErrorExpected && err == nil:
   171  					t.Fatalf("no error when expected")
   172  				case !tcase.isErrorExpected && err != nil:
   173  					t.Fatalf("unexpected error: %v", err)
   174  				}
   175  			})
   176  		}
   177  	})
   178  }
   179  
   180  func TestLockWorks(t *testing.T) {
   181  	dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
   182  		ip, port, err := c.FirstPort()
   183  		if err != nil {
   184  			t.Fatal(err)
   185  		}
   186  
   187  		addr := mongoConnectionString(ip, port)
   188  		p := &Mongo{}
   189  		d, err := p.Open(addr)
   190  		if err != nil {
   191  			t.Fatal(err)
   192  		}
   193  		defer func() {
   194  			if err := d.Close(); err != nil {
   195  				t.Error(err)
   196  			}
   197  		}()
   198  
   199  		dt.TestRun(t, d, bytes.NewReader([]byte(`[{"insert":"hello","documents":[{"wild":"world"}]}]`)))
   200  
   201  		mc := d.(*Mongo)
   202  
   203  		err = mc.Lock()
   204  		if err != nil {
   205  			t.Fatal(err)
   206  		}
   207  		err = mc.Unlock()
   208  		if err != nil {
   209  			t.Fatal(err)
   210  		}
   211  
   212  		err = mc.Lock()
   213  		if err != nil {
   214  			t.Fatal(err)
   215  		}
   216  		err = mc.Unlock()
   217  		if err != nil {
   218  			t.Fatal(err)
   219  		}
   220  
   221  		// enable locking,
   222  		//try to hit a lock conflict
   223  		mc.config.Locking.Enabled = true
   224  		mc.config.Locking.Timeout = 1
   225  		err = mc.Lock()
   226  		if err != nil {
   227  			t.Fatal(err)
   228  		}
   229  		err = mc.Lock()
   230  		if err == nil {
   231  			t.Fatal("should have failed, mongo should be locked already")
   232  		}
   233  	})
   234  }
   235  
   236  func TestTransaction(t *testing.T) {
   237  	transactionSpecs := []dktesting.ContainerSpec{
   238  		{ImageName: "mongo:4", Options: dktest.Options{PortRequired: true, ReadyFunc: isReady,
   239  			Cmd: []string{"mongod", "--bind_ip_all", "--replSet", "rs0"}}},
   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  
   402  }