github.com/bdollma-te/migrate/v4@v4.17.0-clickv2/internal/cli/commands_test.go (about)

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