package testing import ( "context" "errors" "reflect" "testing" "time" influxdb "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/kv" "go.uber.org/zap" ) // NowFunc is a function which returns a time type NowFunc func() time.Time // Migrator tests a migrator against a provided store. // The migrator is constructed via a provided constructor function which takes // a logger and a now function used to derive time. func Migrator(t *testing.T, store kv.Store, newMigrator func(*zap.Logger, NowFunc) *kv.Migrator) { var ( ctx = context.TODO() logger = zap.NewNop() migrationOne = newMigration("migration one") migrationTwo = newMigration("migration two") migrationThree = newMigration("migration three") migrationFour = newMigration("migration four") // mocking now time timestamp = int64(0) now = func() time.Time { timestamp++ return time.Unix(timestamp, 0).In(time.UTC) } // ts returns a point to a time at N unix seconds. ts = func(n int64) *time.Time { t := time.Unix(n, 0).In(time.UTC) return &t } migrator = newMigrator(logger, now) ) migrator.AddMigrations( // all migrations excluding number four (for now) migrationOne, migrationTwo, migrationThree, ) if err := migrator.Initialize(ctx, store); err != nil { t.Fatal(err) } t.Run("List() shows all migrations in down state", func(t *testing.T) { migrations, err := migrator.List(ctx, store) if err != nil { t.Fatal(err) } if expected := []kv.Migration{ { ID: influxdb.ID(1), Name: "migration one", State: kv.DownMigrationState, }, { ID: influxdb.ID(2), Name: "migration two", State: kv.DownMigrationState, }, { ID: influxdb.ID(3), Name: "migration three", State: kv.DownMigrationState, }, }; !reflect.DeepEqual(expected, migrations) { t.Errorf("expected %#v, found %#v", expected, migrations) } }) t.Run("Up() runs each migration in turn", func(t *testing.T) { // apply all migrations if err := migrator.Up(ctx, store); err != nil { t.Fatal(err) } // list migration again migrations, err := migrator.List(ctx, store) if err != nil { t.Fatal(err) } if expected := []kv.Migration{ { ID: influxdb.ID(1), Name: "migration one", State: kv.UpMigrationState, StartedAt: ts(1), FinishedAt: ts(2), }, { ID: influxdb.ID(2), Name: "migration two", State: kv.UpMigrationState, StartedAt: ts(3), FinishedAt: ts(4), }, { ID: influxdb.ID(3), Name: "migration three", State: kv.UpMigrationState, StartedAt: ts(5), FinishedAt: ts(6), }, }; !reflect.DeepEqual(expected, migrations) { t.Errorf("expected %#v, found %#v", expected, migrations) } // assert each migration was called migrationOne.assertUpCalled(t, 1) migrationTwo.assertUpCalled(t, 1) migrationThree.assertUpCalled(t, 1) }) t.Run("List() after adding new migration it reports as expected", func(t *testing.T) { migrator.AddMigrations(migrationFour) // list migration again migrations, err := migrator.List(ctx, store) if err != nil { t.Fatal(err) } if expected := []kv.Migration{ { ID: influxdb.ID(1), Name: "migration one", State: kv.UpMigrationState, StartedAt: ts(1), FinishedAt: ts(2), }, { ID: influxdb.ID(2), Name: "migration two", State: kv.UpMigrationState, StartedAt: ts(3), FinishedAt: ts(4), }, { ID: influxdb.ID(3), Name: "migration three", State: kv.UpMigrationState, StartedAt: ts(5), FinishedAt: ts(6), }, { ID: influxdb.ID(4), Name: "migration four", State: kv.DownMigrationState, }, }; !reflect.DeepEqual(expected, migrations) { t.Errorf("expected %#v, found %#v", expected, migrations) } }) t.Run("Up() only applies the single down migration", func(t *testing.T) { // apply all migrations if err := migrator.Up(ctx, store); err != nil { t.Fatal(err) } // list migration again migrations, err := migrator.List(ctx, store) if err != nil { t.Fatal(err) } if expected := []kv.Migration{ { ID: influxdb.ID(1), Name: "migration one", State: kv.UpMigrationState, StartedAt: ts(1), FinishedAt: ts(2), }, { ID: influxdb.ID(2), Name: "migration two", State: kv.UpMigrationState, StartedAt: ts(3), FinishedAt: ts(4), }, { ID: influxdb.ID(3), Name: "migration three", State: kv.UpMigrationState, StartedAt: ts(5), FinishedAt: ts(6), }, { ID: influxdb.ID(4), Name: "migration four", State: kv.UpMigrationState, StartedAt: ts(7), FinishedAt: ts(8), }, }; !reflect.DeepEqual(expected, migrations) { t.Errorf("expected %#v, found %#v", expected, migrations) } // assert each migration was called only once migrationOne.assertUpCalled(t, 1) migrationTwo.assertUpCalled(t, 1) migrationThree.assertUpCalled(t, 1) migrationFour.assertUpCalled(t, 1) }) t.Run("Down() calls down for each migration", func(t *testing.T) { // apply all migrations if err := migrator.Down(ctx, store); err != nil { t.Fatal(err) } // list migration again migrations, err := migrator.List(ctx, store) if err != nil { t.Fatal(err) } if expected := []kv.Migration{ { ID: influxdb.ID(1), Name: "migration one", State: kv.DownMigrationState, }, { ID: influxdb.ID(2), Name: "migration two", State: kv.DownMigrationState, }, { ID: influxdb.ID(3), Name: "migration three", State: kv.DownMigrationState, }, { ID: influxdb.ID(4), Name: "migration four", State: kv.DownMigrationState, }, }; !reflect.DeepEqual(expected, migrations) { t.Errorf("expected %#v, found %#v", expected, migrations) } // assert each migration was called only once migrationOne.assertDownCalled(t, 1) migrationTwo.assertDownCalled(t, 1) migrationThree.assertDownCalled(t, 1) migrationFour.assertDownCalled(t, 1) }) t.Run("Up() re-applies all migrations", func(t *testing.T) { // apply all migrations if err := migrator.Up(ctx, store); err != nil { t.Fatal(err) } // list migration again migrations, err := migrator.List(ctx, store) if err != nil { t.Fatal(err) } if expected := []kv.Migration{ { ID: influxdb.ID(1), Name: "migration one", State: kv.UpMigrationState, StartedAt: ts(9), FinishedAt: ts(10), }, { ID: influxdb.ID(2), Name: "migration two", State: kv.UpMigrationState, StartedAt: ts(11), FinishedAt: ts(12), }, { ID: influxdb.ID(3), Name: "migration three", State: kv.UpMigrationState, StartedAt: ts(13), FinishedAt: ts(14), }, { ID: influxdb.ID(4), Name: "migration four", State: kv.UpMigrationState, StartedAt: ts(15), FinishedAt: ts(16), }, }; !reflect.DeepEqual(expected, migrations) { t.Errorf("expected %#v, found %#v", expected, migrations) } // assert each migration up was called for a second time migrationOne.assertUpCalled(t, 2) migrationTwo.assertUpCalled(t, 2) migrationThree.assertUpCalled(t, 2) migrationFour.assertUpCalled(t, 2) }) t.Run("List() missing migration spec errors as expected", func(t *testing.T) { // remove last specification from migration list migrator.MigrationSpecs = migrator.MigrationSpecs[:len(migrator.MigrationSpecs)-1] // list migration again _, err := migrator.List(ctx, store) if !errors.Is(err, kv.ErrMigrationSpecNotFound) { t.Errorf("expected migration spec error, found %v", err) } }) } func newMigration(name string) *spyMigrationSpec { return &spyMigrationSpec{name: name} } type spyMigrationSpec struct { name string upCalled int downCalled int } func (s *spyMigrationSpec) MigrationName() string { return s.name } func (s *spyMigrationSpec) assertUpCalled(t *testing.T, times int) { t.Helper() if s.upCalled != times { t.Errorf("expected Up() to be called %d times, instead found %d times", times, s.upCalled) } } func (s *spyMigrationSpec) Up(ctx context.Context, store kv.Store) error { s.upCalled++ return nil } func (s *spyMigrationSpec) assertDownCalled(t *testing.T, times int) { t.Helper() if s.downCalled != times { t.Errorf("expected Down() to be called %d times, instead found %d times", times, s.downCalled) } } func (s *spyMigrationSpec) Down(ctx context.Context, store kv.Store) error { s.downCalled++ return nil }