github.com/nokia/migrate/v4@v4.16.0/internal/cli/commands_test.go (about)

     1  package cli
     2  
     3  import (
     4  	"errors"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/suite"
    14  )
    15  
    16  type CreateCmdSuite struct {
    17  	suite.Suite
    18  }
    19  
    20  func TestCreateCmdSuite(t *testing.T) {
    21  	suite.Run(t, &CreateCmdSuite{})
    22  }
    23  
    24  func (s *CreateCmdSuite) mustCreateTempDir() string {
    25  	tmpDir, err := ioutil.TempDir("", "migrate_")
    26  
    27  	if err != nil {
    28  		s.FailNow(err.Error())
    29  	}
    30  
    31  	return tmpDir
    32  }
    33  
    34  func (s *CreateCmdSuite) mustCreateDir(dir string) {
    35  	if err := os.MkdirAll(dir, 0755); err != nil {
    36  		s.FailNow(err.Error())
    37  	}
    38  }
    39  
    40  func (s *CreateCmdSuite) mustRemoveDir(dir string) {
    41  	if err := os.RemoveAll(dir); err != nil {
    42  		s.FailNow(err.Error())
    43  	}
    44  }
    45  
    46  func (s *CreateCmdSuite) mustWriteFile(dir, file, body string) {
    47  	if err := ioutil.WriteFile(filepath.Join(dir, file), []byte(body), 0644); err != nil {
    48  		s.FailNow(err.Error())
    49  	}
    50  }
    51  
    52  func (s *CreateCmdSuite) mustGetwd() string {
    53  	cwd, err := os.Getwd()
    54  
    55  	if err != nil {
    56  		s.FailNow(err.Error())
    57  	}
    58  
    59  	return cwd
    60  }
    61  
    62  func (s *CreateCmdSuite) mustChdir(dir string) {
    63  	if err := os.Chdir(dir); err != nil {
    64  		s.FailNow(err.Error())
    65  	}
    66  }
    67  
    68  func (s *CreateCmdSuite) assertEmptyDir(dir string) bool {
    69  	fis, err := ioutil.ReadDir(dir)
    70  
    71  	if err != nil {
    72  		return s.Fail(err.Error())
    73  	}
    74  
    75  	return s.Empty(fis)
    76  }
    77  
    78  func (s *CreateCmdSuite) TestNextSeqVersion() {
    79  	cases := []struct {
    80  		tid         string
    81  		matches     []string
    82  		seqDigits   int
    83  		expected    string
    84  		expectedErr error
    85  	}{
    86  		{"Bad digits", []string{}, 0, "", errInvalidSequenceWidth},
    87  		{"Single digit initialize", []string{}, 1, "1", nil},
    88  		{"Single digit malformed", []string{"bad"}, 1, "", errors.New("Malformed migration filename: bad")},
    89  		{"Single digit no int", []string{"bad_bad"}, 1, "", errors.New(`strconv.ParseUint: parsing "bad": invalid syntax`)},
    90  		{"Single digit negative seq", []string{"-5_test"}, 1, "", errors.New(`strconv.ParseUint: parsing "-5": invalid syntax`)},
    91  		{"Single digit increment", []string{"3_test", "4_test"}, 1, "5", nil},
    92  		{"Single digit overflow", []string{"9_test"}, 1, "", errors.New("Next sequence number 10 too large. At most 1 digits are allowed")},
    93  		{"Zero-pad initialize", []string{}, 6, "000001", nil},
    94  		{"Zero-pad malformed", []string{"bad"}, 6, "", errors.New("Malformed migration filename: bad")},
    95  		{"Zero-pad no int", []string{"bad_bad"}, 6, "", errors.New(`strconv.ParseUint: parsing "bad": invalid syntax`)},
    96  		{"Zero-pad negative seq", []string{"-000005_test"}, 6, "", errors.New(`strconv.ParseUint: parsing "-000005": invalid syntax`)},
    97  		{"Zero-pad increment", []string{"000003_test", "000004_test"}, 6, "000005", nil},
    98  		{"Zero-pad overflow", []string{"999999_test"}, 6, "", errors.New("Next sequence number 1000000 too large. At most 6 digits are allowed")},
    99  		{"dir absolute path", []string{"/migrationDir/000001_test"}, 6, "000002", nil},
   100  		{"dir relative path", []string{"migrationDir/000001_test"}, 6, "000002", nil},
   101  		{"dir dot prefix", []string{"./migrationDir/000001_test"}, 6, "000002", nil},
   102  		{"dir parent prefix", []string{"../migrationDir/000001_test"}, 6, "000002", nil},
   103  		{"dir no prefix", []string{"000001_test"}, 6, "000002", nil},
   104  	}
   105  
   106  	for _, c := range cases {
   107  		s.Run(c.tid, func() {
   108  			v, err := nextSeqVersion(c.matches, c.seqDigits)
   109  
   110  			if c.expectedErr != nil {
   111  				s.EqualError(err, c.expectedErr.Error())
   112  			} else {
   113  				s.NoError(err)
   114  				s.Equal(c.expected, v)
   115  			}
   116  		})
   117  	}
   118  }
   119  
   120  func (s *CreateCmdSuite) TestTimeVersion() {
   121  	ts := time.Date(2000, 12, 25, 00, 01, 02, 3456789, time.UTC)
   122  	tsUnixStr := strconv.FormatInt(ts.Unix(), 10)
   123  	tsUnixNanoStr := strconv.FormatInt(ts.UnixNano(), 10)
   124  
   125  	cases := []struct {
   126  		tid         string
   127  		time        time.Time
   128  		format      string
   129  		expected    string
   130  		expectedErr error
   131  	}{
   132  		{"Bad format", ts, "", "", errInvalidTimeFormat},
   133  		{"unix", ts, "unix", tsUnixStr, nil},
   134  		{"unixNano", ts, "unixNano", tsUnixNanoStr, nil},
   135  		{"custom ymthms", ts, "20060102150405", "20001225000102", nil},
   136  	}
   137  
   138  	for _, c := range cases {
   139  		s.Run(c.tid, func() {
   140  			v, err := timeVersion(c.time, c.format)
   141  
   142  			if c.expectedErr != nil {
   143  				s.EqualError(err, c.expectedErr.Error())
   144  			} else {
   145  				s.NoError(err)
   146  				s.Equal(c.expected, v)
   147  			}
   148  		})
   149  	}
   150  }
   151  
   152  // TestCreateCmd tests function createCmd.
   153  //
   154  // For each test case, it creates a temp dir as "sandbox" (called `baseDir`) and
   155  // all path manipulations are relative to `baseDir`.
   156  func (s *CreateCmdSuite) TestCreateCmd() {
   157  	ts := time.Date(2000, 12, 25, 00, 01, 02, 3456789, time.UTC)
   158  	tsUnixStr := strconv.FormatInt(ts.Unix(), 10)
   159  	tsUnixNanoStr := strconv.FormatInt(ts.UnixNano(), 10)
   160  	testCwd := s.mustGetwd()
   161  
   162  	cases := []struct {
   163  		tid           string
   164  		existingDirs  []string // directory paths to create before test. relative to baseDir.
   165  		cwd           string   // path to chdir to before test. relative to baseDir.
   166  		existingFiles []string // file paths created before test. relative to baseDir.
   167  		expectedFiles []string // file paths expected to exist after test. paths relative to baseDir.
   168  		expectedErr   error
   169  		dir           string // `dir` parameter. if absolute path, will be converted to baseDir/dir.
   170  		startTime     time.Time
   171  		format        string
   172  		seq           bool
   173  		seqDigits     int
   174  		ext           string
   175  		name          string
   176  	}{
   177  		{"seq and format", nil, "", nil, nil, errIncompatibleSeqAndFormat, ".", ts, "unix", true, 4, "sql", "name"},
   178  		{"seq init dir dot", nil, "", nil, []string{"0001_name.up.sql", "0001_name.down.sql"}, nil, ".", ts, defaultTimeFormat, true, 4, "sql", "name"},
   179  		{"seq init dir dot trailing slash", nil, "", nil, []string{"0001_name.up.sql", "0001_name.down.sql"}, nil, "./", ts, defaultTimeFormat, true, 4, "sql", "name"},
   180  		{"seq init dir double dot", []string{"subdir"}, "subdir", nil, []string{"0001_name.up.sql", "0001_name.down.sql"}, nil, "..", ts, defaultTimeFormat, true, 4, "sql", "name"},
   181  		{"seq init dir double dot trailing slash", []string{"subdir"}, "subdir", nil, []string{"0001_name.up.sql", "0001_name.down.sql"}, nil, "../", ts, defaultTimeFormat, true, 4, "sql", "name"},
   182  		{"seq init dir absolute", []string{"subdir"}, "", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "/subdir", ts, defaultTimeFormat, true, 4, "sql", "name"},
   183  		{"seq init dir absolute trailing slash", []string{"subdir"}, "", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "/subdir/", ts, defaultTimeFormat, true, 4, "sql", "name"},
   184  		{"seq init dir relative", []string{"subdir"}, "", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "subdir", ts, defaultTimeFormat, true, 4, "sql", "name"},
   185  		{"seq init dir relative trailing slash", []string{"subdir"}, "", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "subdir/", ts, defaultTimeFormat, true, 4, "sql", "name"},
   186  		{"seq init dir dot relative", []string{"subdir"}, "", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "./subdir", ts, defaultTimeFormat, true, 4, "sql", "name"},
   187  		{"seq init dir dot relative trailing slash", []string{"subdir"}, "", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "./subdir/", ts, defaultTimeFormat, true, 4, "sql", "name"},
   188  		{"seq init dir double dot relative", []string{"subdir"}, "subdir", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "../subdir", ts, defaultTimeFormat, true, 4, "sql", "name"},
   189  		{"seq init dir double dot relative trailing slash", []string{"subdir"}, "subdir", nil, []string{"subdir/0001_name.up.sql", "subdir/0001_name.down.sql"}, nil, "../subdir/", ts, defaultTimeFormat, true, 4, "sql", "name"},
   190  		{"seq init dir maze", []string{"subdir"}, "subdir", nil, []string{"0001_name.up.sql", "0001_name.down.sql"}, nil, "..//subdir/./.././/subdir/..", ts, defaultTimeFormat, true, 4, "sql", "name"},
   191  		{"seq width invalid", nil, "", nil, nil, errInvalidSequenceWidth, ".", ts, defaultTimeFormat, true, 0, "sql", "name"},
   192  		{"seq malformed", nil, "", []string{"bad.sql"}, []string{"bad.sql"}, errors.New("Malformed migration filename: bad.sql"), ".", ts, defaultTimeFormat, true, 4, "sql", "name"},
   193  		{"seq not int", nil, "", []string{"bad_bad.sql"}, []string{"bad_bad.sql"}, errors.New(`strconv.ParseUint: parsing "bad": invalid syntax`), ".", ts, defaultTimeFormat, true, 4, "sql", "name"},
   194  		{"seq negative", nil, "", []string{"-5_negative.sql"}, []string{"-5_negative.sql"}, errors.New(`strconv.ParseUint: parsing "-5": invalid syntax`), ".", ts, defaultTimeFormat, true, 4, "sql", "name"},
   195  		{"seq increment", nil, "", []string{"3_three.sql", "4_four.sql"}, []string{"3_three.sql", "4_four.sql", "0005_five.up.sql", "0005_five.down.sql"}, nil, ".", ts, defaultTimeFormat, true, 4, "sql", "five"},
   196  		{"seq overflow", nil, "", []string{"9_nine.sql"}, []string{"9_nine.sql"}, errors.New(`Next sequence number 10 too large. At most 1 digits are allowed`), ".", ts, defaultTimeFormat, true, 1, "sql", "ten"},
   197  		{"time empty format", nil, "", nil, nil, errInvalidTimeFormat, ".", ts, "", false, 0, "sql", "name"},
   198  		{"time unix", nil, "", nil, []string{tsUnixStr + "_name.up.sql", tsUnixStr + "_name.down.sql"}, nil, ".", ts, "unix", false, 0, "sql", "name"},
   199  		{"time unixNano", nil, "", nil, []string{tsUnixNanoStr + "_name.up.sql", tsUnixNanoStr + "_name.down.sql"}, nil, ".", ts, "unixNano", false, 0, "sql", "name"},
   200  		{"time custom format", nil, "", nil, []string{"20001225000102_name.up.sql", "20001225000102_name.down.sql"}, nil, ".", ts, "20060102150405", false, 0, "sql", "name"},
   201  		{"time version collision", nil, "", []string{"20001225_name.up.sql", "20001225_name.down.sql"}, []string{"20001225_name.up.sql", "20001225_name.down.sql"}, errors.New("duplicate migration version: 20001225"), ".", ts, "20060102", false, 0, "sql", "name"},
   202  		{"dir invalid", nil, "", []string{"file"}, []string{"file"}, errors.New("mkdir 'test: this is invalid dir name'\x00: invalid argument"), "'test: this is invalid dir name'\000", ts, "unix", false, 0, "sql", "name"},
   203  	}
   204  
   205  	for _, c := range cases {
   206  		s.Run(c.tid, func() {
   207  			baseDir := s.mustCreateTempDir()
   208  
   209  			for _, d := range c.existingDirs {
   210  				s.mustCreateDir(filepath.Join(baseDir, d))
   211  			}
   212  
   213  			cwd := baseDir
   214  
   215  			if c.cwd != "" {
   216  				cwd = filepath.Join(baseDir, c.cwd)
   217  			}
   218  
   219  			s.mustChdir(cwd)
   220  
   221  			for _, f := range c.existingFiles {
   222  				s.mustWriteFile(baseDir, f, "")
   223  			}
   224  
   225  			dir := c.dir
   226  			dir = filepath.ToSlash(dir)
   227  			volName := filepath.VolumeName(baseDir)
   228  			// Windows specific, can not recognize \subdir as abs path
   229  			isWindowsAbsPathNoLetter := strings.HasPrefix(dir, "/") && volName != ""
   230  			isRealAbsPath := filepath.IsAbs(dir)
   231  			if isWindowsAbsPathNoLetter || isRealAbsPath {
   232  				dir = filepath.Join(baseDir, dir)
   233  			}
   234  
   235  			err := createCmd(dir, c.startTime, c.format, c.name, c.ext, c.seq, c.seqDigits, false)
   236  
   237  			if c.expectedErr != nil {
   238  				s.EqualError(err, c.expectedErr.Error())
   239  			} else {
   240  				s.NoError(err)
   241  			}
   242  
   243  			if len(c.expectedFiles) == 0 {
   244  				s.assertEmptyDir(baseDir)
   245  			} else {
   246  				for _, f := range c.expectedFiles {
   247  					s.FileExists(filepath.Join(baseDir, f))
   248  				}
   249  			}
   250  
   251  			s.mustChdir(testCwd)
   252  			s.mustRemoveDir(baseDir)
   253  		})
   254  	}
   255  }
   256  
   257  func TestNumDownFromArgs(t *testing.T) {
   258  	cases := []struct {
   259  		name                string
   260  		args                []string
   261  		applyAll            bool
   262  		expectedNeedConfirm bool
   263  		expectedNum         int
   264  		expectedErrStr      string
   265  	}{
   266  		{"no args", []string{}, false, true, -1, ""},
   267  		{"down all", []string{}, true, false, -1, ""},
   268  		{"down 5", []string{"5"}, false, false, 5, ""},
   269  		{"down N", []string{"N"}, false, false, 0, "can't read limit argument N"},
   270  		{"extra arg after -all", []string{"5"}, true, false, 0, "-all cannot be used with other arguments"},
   271  		{"extra arg before -all", []string{"5", "-all"}, false, false, 0, "too many arguments"},
   272  	}
   273  	for _, c := range cases {
   274  		t.Run(c.name, func(t *testing.T) {
   275  			num, needsConfirm, err := numDownMigrationsFromArgs(c.applyAll, c.args)
   276  			if needsConfirm != c.expectedNeedConfirm {
   277  				t.Errorf("Incorrect needsConfirm was: %v wanted %v", needsConfirm, c.expectedNeedConfirm)
   278  			}
   279  
   280  			if num != c.expectedNum {
   281  				t.Errorf("Incorrect num was: %v wanted %v", num, c.expectedNum)
   282  			}
   283  
   284  			if err != nil {
   285  				if err.Error() != c.expectedErrStr {
   286  					t.Error("Incorrect error: " + err.Error() + " != " + c.expectedErrStr)
   287  				}
   288  			} else if c.expectedErrStr != "" {
   289  				t.Error("Expected error: " + c.expectedErrStr + " but got nil instead")
   290  			}
   291  		})
   292  	}
   293  }