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
153
154
155
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
165 cwd string
166 existingFiles []string
167 expectedFiles []string
168 expectedErr error
169 dir string
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
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 }
294
View as plain text