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 }