github.com/johnatannvmd/migrate/v4@v4.15.3-0.20220513150750-0bf01be30da8/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 }