Skip to content

Commit 682777b

Browse files
authored
Merge pull request #1206 from Scalingo/fix/1205/schedule_at_parse
fix(databases): `parseScheduleAtFlag` returns a validation error
2 parents 9f7ac0a + 40bdb7d commit 682777b

File tree

3 files changed

+111
-15
lines changed

3 files changed

+111
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* feat(run): add a `bash` alias for us out there often forgetting the `run` in front
1010
* feat(sshkeys): add support for ed25519 keys
1111
* feat(apps/create): detect Git main branch name
12+
* fix(databases): `parseScheduleAtFlag` returns a validation error
1213

1314
## 1.43.3
1415

cmd/databases.go

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package cmd
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
6+
"regexp"
77
"strconv"
88
"strings"
99
"time"
@@ -15,6 +15,7 @@ import (
1515
"github.com/Scalingo/cli/detect"
1616
"github.com/Scalingo/cli/utils"
1717
"github.com/Scalingo/go-scalingo/v10"
18+
"github.com/Scalingo/go-utils/errors/v3"
1819
)
1920

2021
var (
@@ -42,7 +43,7 @@ var (
4243

4344
addonName := addonUUIDFromFlags(ctx, c, currentResource, true)
4445
if c.NArg() != 1 {
45-
errorQuit(ctx, errors.New("feature argument should be specified"))
46+
errorQuit(ctx, errors.New(ctx, "feature argument should be specified"))
4647
}
4748
feature := c.Args().First()
4849
err := db.EnableFeature(ctx, c, currentResource, addonName, feature)
@@ -73,7 +74,7 @@ var (
7374

7475
addonName := addonUUIDFromFlags(ctx, c, currentResource, true)
7576
if c.NArg() != 1 {
76-
errorQuit(ctx, errors.New("feature argument should be specified"))
77+
errorQuit(ctx, errors.New(ctx, "feature argument should be specified"))
7778
}
7879
feature := c.Args().First()
7980
err := db.DisableFeature(ctx, currentResource, addonName, feature)
@@ -115,25 +116,25 @@ var (
115116
scheduleAtFlag := c.String("schedule-at")
116117
disable := c.Bool("unschedule")
117118
if scheduleAtFlag != "" && disable {
118-
errorQuit(ctx, errors.New("you cannot use both --schedule-at and --unschedule at the same time"))
119+
errorQuit(ctx, errors.New(ctx, "you cannot use both --schedule-at and --unschedule at the same time"))
119120
}
120121

121122
if disable {
122123
continueB := askContinue("Disabling periodic backups will prevent Scalingo from restoring your database in case of data loss or corruption. Backups are a critical safeguard, and we strongly recommend keeping them enabled. Do you want to continue? (yes/no)")
123124
if !continueB {
124-
errorQuit(ctx, errors.New("periodic backups are still enabled"))
125+
errorQuit(ctx, errors.New(ctx, "periodic backups are still enabled"))
125126
return nil
126127
}
127128

128129
params.Enabled = utils.BoolPtr(false)
129130
}
130131
if scheduleAtFlag != "" {
131132
params.Enabled = utils.BoolPtr(true)
132-
scheduleAt, loc, err := parseScheduleAtFlag(scheduleAtFlag)
133+
scheduleAtHour, loc, err := parseScheduleAtFlag(scheduleAtFlag)
133134
if err != nil {
134135
errorQuit(ctx, err)
135136
}
136-
localTime := time.Date(1986, 7, 22, scheduleAt, 0, 0, 0, loc)
137+
localTime := time.Date(1986, 7, 22, scheduleAtHour, 0, 0, 0, loc)
137138
hour := localTime.UTC().Hour()
138139
params.ScheduledAt = &hour
139140
}
@@ -296,26 +297,39 @@ Only available on ` + fmt.Sprintf("%s", dbUsers.SupportedAddons),
296297
)
297298

298299
func parseScheduleAtFlag(flag string) (int, *time.Location, error) {
299-
scheduleAt, err := strconv.Atoi(flag)
300+
scheduleAtHour, err := strconv.Atoi(flag)
300301
if err == nil {
301302
// In this case, the schedule-at flag equals a single number
302-
return scheduleAt, time.Local, nil
303+
return scheduleAtHour, time.Local, nil
303304
}
304305

306+
validationError := errors.NewValidationErrorsBuilder()
307+
305308
// From now on the schedule-at flag is a number and a timezone such as
306309
// "3 Europe/Paris"
307310
s := strings.Split(flag, " ")
308311
if len(s) < 2 {
309-
return -1, nil, errors.New("parse the schedule-at value")
312+
validationError = validationError.Set("schedule-at", "only contains two space-separated fields")
313+
return -1, nil, validationError.Build()
310314
}
311-
scheduleAt, err = strconv.Atoi(s[0])
315+
scheduleAtTime := s[0]
316+
scheduleAtTimezone := s[1]
317+
318+
// If client writes "4:00" or "4h00", we only want to keep "4" in scheduleAtTime
319+
re := regexp.MustCompile(`([:hH].*)|[0]+`)
320+
scheduleAtTime = re.ReplaceAllString(scheduleAtTime, "")
321+
322+
scheduleAtHour, err = strconv.Atoi(scheduleAtTime)
312323
if err != nil {
313-
return -1, nil, errors.New("parse the schedule-at value")
324+
validationError = validationError.Set("schedule-at", "first field must be a digit representing the hour")
325+
return -1, nil, validationError.Build()
314326
}
315-
loc, err := time.LoadLocation(s[1])
327+
328+
loc, err := time.LoadLocation(scheduleAtTimezone)
316329
if err != nil {
317-
return -1, nil, fmt.Errorf("unknown timezone '%s'", s[1])
330+
validationError = validationError.Set("schedule-at", "unknown timezone"+s[1])
331+
return -1, nil, validationError.Build()
318332
}
319333

320-
return scheduleAt, loc, nil
334+
return scheduleAtHour, loc, nil
321335
}

cmd/databases_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestParseScheduleAtFlag(t *testing.T) {
12+
tests := map[string]struct {
13+
flag string
14+
expectedHour int
15+
expectedLoc string
16+
expectedError string
17+
}{
18+
"single hour uses local timezone": {
19+
flag: "3",
20+
expectedHour: 3,
21+
expectedLoc: time.Local.String(),
22+
},
23+
"hour and timezone": {
24+
flag: "3 Europe/Paris",
25+
expectedHour: 3,
26+
expectedLoc: "Europe/Paris",
27+
},
28+
"hour with minutes and timezone (1)": {
29+
flag: "4:00 Europe/Paris",
30+
expectedHour: 4,
31+
expectedLoc: "Europe/Paris",
32+
},
33+
"hour with minutes and timezone (2)": {
34+
flag: "4h00 Europe/Paris",
35+
expectedHour: 4,
36+
expectedLoc: "Europe/Paris",
37+
},
38+
"hour with minutes and timezone (3)": {
39+
flag: "4H00 Europe/Paris",
40+
expectedHour: 4,
41+
expectedLoc: "Europe/Paris",
42+
},
43+
"invalid flag (1)": {
44+
flag: "invalid",
45+
expectedError: "schedule-at=only contains two space-separated fields",
46+
},
47+
"invalid flag (2)": {
48+
flag: "invalid invalid2",
49+
expectedError: "schedule-at=first field must be a digit representing the hour",
50+
},
51+
"invalid flag (3)": {
52+
flag: "invalid invalid2:invalid3",
53+
expectedError: "schedule-at=first field must be a digit representing the hour",
54+
},
55+
"invalid hour": {
56+
flag: "aa Europe/Paris",
57+
expectedError: "schedule-at=first field must be a digit representing the hour",
58+
},
59+
"unknown timezone": {
60+
flag: "3 Europe/Unknown",
61+
expectedError: "schedule-at=unknown timezoneEurope/Unknown",
62+
},
63+
}
64+
65+
for name, test := range tests {
66+
t.Run(name, func(t *testing.T) {
67+
hour, loc, err := parseScheduleAtFlag(test.flag)
68+
if test.expectedError != "" {
69+
require.ErrorContains(t, err, test.expectedError)
70+
assert.Equal(t, -1, hour)
71+
assert.Nil(t, loc)
72+
return
73+
}
74+
75+
require.NoError(t, err)
76+
require.NotNil(t, loc)
77+
assert.Equal(t, test.expectedHour, hour)
78+
assert.Equal(t, test.expectedLoc, loc.String())
79+
})
80+
}
81+
}

0 commit comments

Comments
 (0)