From 11ace448ff31ca0c5a3931aecaf79f2a11bc8960 Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Sat, 10 Oct 2026 17:15:41 +0200 Subject: [PATCH] Fixed opening hours validation accepting input the server cannot parse. Fixes #3 Co-Authored-By: Claude Opus 5.5 --- src/backend/data.rs | 59 +++++++++++++++++++++++++++++++++++---- src/locales/catalogues.rs | 1 + 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/backend/data.rs b/src/backend/data.rs index 0285880..eb3d96a 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -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> { - 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(()) } if let Ok(h) = DayHours::try_new(value.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(()) } else { - Ok(()) + Err(hours_error("HOURS_FORMAT", "Hours must be in HH:MM - HH:MM format")) } } 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)] -#[validate(schema(function = "validate_hours", message = "Time 'to' must be after time 'from'"))] +#[validate(schema(function = "validate_hours"))] pub struct WeekHours { day: Weekday, #[validate(regex(path = "RE_HOURS", message = "Hours must be in HH:MM - HH:MM format"))] @@ -948,12 +958,49 @@ mod tests { #[test] 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() }; 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::>(); + 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] 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"] { diff --git a/src/locales/catalogues.rs b/src/locales/catalogues.rs index 5eb1aea..d8bc9a3 100644 --- a/src/locales/catalogues.rs +++ b/src/locales/catalogues.rs @@ -103,6 +103,7 @@ lazy_static! { ("Date can't be in past", "Datum nesmí být v minulosti"), ("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"), + ("Time 'to' must be after time 'from'", "Čas 'do' musí být po čase 'od'"), ("Edit profile", "Upravit profil"), ("Get emails", "Dostávat e-maily"), ("Enter username", "Zadejte uživatelské jméno"),