Fixed opening hours validation accepting input the server cannot parse. Fixes #3

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-10 17:15:41 +02:00
co-authored by Claude Opus 5.5
parent 2babe19299
commit 11ace448ff
2 changed files with 54 additions and 6 deletions
+53 -6
View File
@@ -331,26 +331,36 @@ impl TryFrom<&str> for DayHour {
} }
} }
fn hours_error(code: &'static str, message: &'static str) -> ValidationError {
let mut err = ValidationError::new(code);
err.message = Some(message.into());
err
}
fn validate_hours(value: &WeekHours) -> Result<(), ValidationError> { fn validate_hours(value: &WeekHours) -> Result<(), ValidationError> {
if value.hours().is_empty() { // Wrong format is already reported by the regex on the field
if value.hours().is_empty() || !RE_HOURS.is_match(value.hours()) {
return Ok(()) return Ok(())
} }
if let Ok(h) = DayHours::try_new(value.hours()) { if let Ok(h) = DayHours::try_new(value.hours()) {
for hr in h.hours() { for hr in h.hours() {
if hr.from() >= hr.to() { return Err(ValidationError::new("TO_BEFORE_FROM")) } if hr.from() >= hr.to() {
return Err(hours_error("TO_BEFORE_FROM", "Time 'to' must be after time 'from'"))
}
} }
Ok(()) Ok(())
} else { } else {
Ok(()) Err(hours_error("HOURS_FORMAT", "Hours must be in HH:MM - HH:MM format"))
} }
} }
lazy_static! { lazy_static! {
static ref RE_HOURS: Regex = Regex::new(r"^$|(^\d{2}:\d{2} ?- ?\d{2}:\d{2} ?(\(\d+\))?,? ?)+").unwrap(); static ref RE_HOURS: Regex = Regex::new(
r"^$|^\d{2}:\d{2} ?- ?\d{2}:\d{2}( ?\(\d+\))?( ?, ?\d{2}:\d{2} ?- ?\d{2}:\d{2}( ?\(\d+\))?)*$").unwrap();
} }
#[derive(Clone, Serialize, Deserialize, Debug, Validate)] #[derive(Clone, Serialize, Deserialize, Debug, Validate)]
#[validate(schema(function = "validate_hours", message = "Time 'to' must be after time 'from'"))] #[validate(schema(function = "validate_hours"))]
pub struct WeekHours { pub struct WeekHours {
day: Weekday, day: Weekday,
#[validate(regex(path = "RE_HOURS", message = "Hours must be in HH:MM - HH:MM format"))] #[validate(regex(path = "RE_HOURS", message = "Hours must be in HH:MM - HH:MM format"))]
@@ -948,12 +958,49 @@ mod tests {
#[test] #[test]
fn week_hours_rejects_wrong_format() { fn week_hours_rejects_wrong_format() {
for input in ["8-12", "8:00 - 12:00", "morning", "08.00 - 12.00"] { for input in ["8-12", "8:00 - 12:00", "morning", "08.00 - 12.00",
"08:00 - 12:00, nonsense", "08:00 - 12:00 garbage", "08:00 - 12:00,", ", 08:00 - 12:00"] {
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() }; let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
assert!(has_field_error(&hours, "hours"), "input: {:?}", input); assert!(has_field_error(&hours, "hours"), "input: {:?}", input);
assert!(!has_field_error(&hours, "__all__"), "input: {:?}", input);
} }
} }
#[test]
fn week_hours_rejects_hours_that_cannot_be_parsed() {
// Matches the format, but is not a valid time
for input in ["25:00 - 26:00", "08:00 - 12:60", "08:00 - 12:00, 13:00 - 24:00"] {
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
assert!(has_field_error(&hours, "__all__"), "input: {:?}", input);
}
}
#[test]
fn week_hours_validation_agrees_with_parser() {
for input in ["", "08:00 - 12:00", "08:00-12:00", "08:00 - 12:00,13:00 - 17:00", "08:00 - 12:00 , 13:00 - 17:00",
"08:00 - 12:00, nonsense", "08:00 - 12:00,", "25:00 - 26:00", "08:00 - 12:00 garbage"] {
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
if hours.validate().is_ok() {
assert!(DayHours::try_new(input).is_ok(), "input: {:?}", input);
}
}
}
#[test]
fn week_hours_error_messages() {
let message = |input: &str| {
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
let errors = hours.validate().unwrap_err();
let all = errors.field_errors().values().flat_map(|e| e.iter()).map(|e| e.message.clone().unwrap().to_string()).collect::<Vec<_>>();
assert_eq!(all.len(), 1, "input: {:?}", input);
all[0].clone()
};
assert_eq!(message("nonsense"), "Hours must be in HH:MM - HH:MM format");
assert_eq!(message("25:00 - 26:00"), "Hours must be in HH:MM - HH:MM format");
assert_eq!(message("12:00 - 08:00"), "Time 'to' must be after time 'from'");
}
#[test] #[test]
fn week_hours_rejects_to_before_from() { fn week_hours_rejects_to_before_from() {
for input in ["12:00 - 08:00", "08:00 - 08:00", "08:00 - 12:00, 17:00 - 13:00"] { for input in ["12:00 - 08:00", "08:00 - 08:00", "08:00 - 12:00, 17:00 - 13:00"] {
+1
View File
@@ -103,6 +103,7 @@ lazy_static! {
("Date can't be in past", "Datum nesmí být v minulosti"), ("Date can't be in past", "Datum nesmí být v minulosti"),
("Enter your username", "Zadejte své uživatelské jméno"), ("Enter your username", "Zadejte své uživatelské jméno"),
("Hours must be in HH:MM - HH:MM format", "Hodiny musí být zadány ve tvaru HH:MM - HH:MM"), ("Hours must be in HH:MM - HH:MM format", "Hodiny musí být zadány ve tvaru HH:MM - HH:MM"),
("Time 'to' must be after time 'from'", "Čas 'do' musí být po čase 'od'"),
("Edit profile", "Upravit profil"), ("Edit profile", "Upravit profil"),
("Get emails", "Dostávat e-maily"), ("Get emails", "Dostávat e-maily"),
("Enter username", "Zadejte uživatelské jméno"), ("Enter username", "Zadejte uživatelské jméno"),