Added unit tests for pure logic. Refs #2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -16,11 +16,14 @@ cargo leptos watch # dev server with reload (needs config.toml +
|
||||
cargo leptos build --release # production build -> target/release/rezervator + target/site
|
||||
cargo check --features ssr # type-check the server side
|
||||
cargo check --lib --features hydrate --target wasm32-unknown-unknown # type-check the WASM side
|
||||
cargo test --features ssr # unit tests
|
||||
```
|
||||
|
||||
Run both `cargo check` commands after any change; a plain `cargo check` (no features) checks neither side properly.
|
||||
Run both `cargo check` commands and the tests after any change; a plain `cargo check` / `cargo test` (no features) checks neither side properly.
|
||||
|
||||
There are no unit tests, and `end2end/` is still the untouched Leptos template (it asserts "Welcome to Leptos") — it does not test this app. Don't claim something is tested because these pass.
|
||||
Unit tests are `#[cfg(test)] mod tests` at the bottom of the file they test (`#[cfg(all(test, feature = "ssr"))]` for ssr-only code). They cover pure logic only — parsing, validation, paging, slot merging, mail templates, config. Nothing touches the database, so server functions, permissions and SQL are untested. Add tests for new pure logic; keep them free of DB and network.
|
||||
|
||||
`end2end/` is still the untouched Leptos template (it asserts "Welcome to Leptos") — it does not test this app.
|
||||
|
||||
## Running locally
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ You will need cargo-leptos tool:
|
||||
|
||||
`cargo leptos watch`
|
||||
|
||||
## Running tests
|
||||
|
||||
`cargo test --features ssr`
|
||||
|
||||
## Executing a Server on a Remote Machine Without the Toolchain
|
||||
After running a `cargo leptos build --release` the minimum files needed are:
|
||||
|
||||
|
||||
@@ -752,3 +752,449 @@ impl ClosingTime {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Duration;
|
||||
|
||||
fn time(h: u32, m: u32) -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(h, m, 0).unwrap()
|
||||
}
|
||||
|
||||
fn date(y: i32, m: u32, d: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(y, m, d).unwrap()
|
||||
}
|
||||
|
||||
fn has_field_error<T: Validate>(entity: &T, field: &str) -> bool {
|
||||
match entity.validate() {
|
||||
Ok(_) => false,
|
||||
Err(e) => e.field_errors().contains_key(field)
|
||||
}
|
||||
}
|
||||
|
||||
// Page
|
||||
|
||||
#[test]
|
||||
fn page_starts_on_first_page() {
|
||||
let page = Page::new(10);
|
||||
|
||||
assert_eq!(page.page_num(), 1);
|
||||
assert_eq!(page.limit(), 10);
|
||||
assert_eq!(page.offset(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_offset_follows_page_number() {
|
||||
let page = Page::new(10).with_page(3);
|
||||
|
||||
assert_eq!(page.page_num(), 3);
|
||||
assert_eq!(page.offset(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_with_zero_size_is_unlimited() {
|
||||
let page = Page::new(0).with_page(5);
|
||||
|
||||
assert_eq!(page.limit(), i32::MAX);
|
||||
assert_eq!(page.offset(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_navigation() {
|
||||
let mut page = Page::new(10);
|
||||
|
||||
page.prev();
|
||||
assert_eq!(page.page_num(), 1);
|
||||
|
||||
page.next();
|
||||
page.next();
|
||||
assert_eq!(page.page_num(), 3);
|
||||
|
||||
page.prev();
|
||||
assert_eq!(page.page_num(), 2);
|
||||
|
||||
page.go_to(7);
|
||||
assert_eq!(page.page_num(), 7);
|
||||
|
||||
page.first();
|
||||
assert_eq!(page.page_num(), 1);
|
||||
}
|
||||
|
||||
// PagedResponse
|
||||
|
||||
#[test]
|
||||
fn paged_response_first_and_last() {
|
||||
let first = PagedResponse::new(vec![1, 2], Page::new(2), 5);
|
||||
assert!(first.is_first());
|
||||
assert!(!first.is_last());
|
||||
|
||||
let middle = PagedResponse::new(vec![3, 4], Page::new(2).with_page(2), 5);
|
||||
assert!(!middle.is_first());
|
||||
assert!(!middle.is_last());
|
||||
|
||||
let last = PagedResponse::new(vec![5], Page::new(2).with_page(3), 5);
|
||||
assert!(!last.is_first());
|
||||
assert!(last.is_last());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paged_response_exactly_full_last_page() {
|
||||
let last = PagedResponse::new(vec![3, 4], Page::new(2).with_page(2), 4);
|
||||
|
||||
assert!(last.is_last());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paged_response_total_pages() {
|
||||
assert_eq!(PagedResponse::new(Vec::<i32>::new(), Page::new(10), 0).total_pages(), 0);
|
||||
assert_eq!(PagedResponse::new(vec![1], Page::new(10), 1).total_pages(), 1);
|
||||
assert_eq!(PagedResponse::new(vec![1], Page::new(10), 10).total_pages(), 1);
|
||||
assert_eq!(PagedResponse::new(vec![1], Page::new(10), 11).total_pages(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paged_response_unlimited_is_single_page() {
|
||||
let resp = PagedResponse::new(vec![1, 2, 3], Page::new(0), 3);
|
||||
|
||||
assert!(resp.is_first());
|
||||
assert!(resp.is_last());
|
||||
assert_eq!(resp.total_pages(), 1);
|
||||
assert_eq!(resp.data(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
// DayHour, DayHours
|
||||
|
||||
#[test]
|
||||
fn day_hour_parses_with_and_without_spaces() {
|
||||
let expected = DayHour::new(time(8, 0), time(12, 30), None);
|
||||
|
||||
assert_eq!(DayHour::try_from("08:00 - 12:30"), Ok(expected.clone()));
|
||||
assert_eq!(DayHour::try_from("08:00-12:30"), Ok(expected.clone()));
|
||||
assert_eq!(DayHour::try_from("08:00 - 12:30".to_string()), Ok(expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_hour_rejects_malformed_input() {
|
||||
for input in ["", "08:00", "08:00 - ", "08:00 - 12:00 - 14:00", "aa:bb - cc:dd", "25:00 - 26:00", "08:00 12:00"] {
|
||||
assert_eq!(DayHour::try_from(input), Err(AppError::HourParseError), "input: {:?}", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_hours_empty_string_is_closed_day() {
|
||||
let hours = DayHours::try_new("").unwrap();
|
||||
|
||||
assert!(hours.hours().is_empty());
|
||||
assert_eq!(hours.to_string(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_hours_parses_list() {
|
||||
let hours = DayHours::try_new("08:00 - 12:00, 13:00 - 17:00").unwrap();
|
||||
|
||||
assert_eq!(hours.hours(), &vec![
|
||||
DayHour::new(time(8, 0), time(12, 0), None),
|
||||
DayHour::new(time(13, 0), time(17, 0), None)
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_hours_rejects_list_with_invalid_item() {
|
||||
assert!(DayHours::try_new("08:00 - 12:00, nonsense").is_err());
|
||||
assert!(DayHours::try_new("08:00 - 12:00,").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_hours_display() {
|
||||
let hours = DayHours(vec![
|
||||
DayHour::new(time(8, 0), time(12, 0), None),
|
||||
DayHour::new(time(13, 0), time(17, 0), Some(10))
|
||||
]);
|
||||
|
||||
assert_eq!(hours.to_string(), "08:00 - 12:00, 13:00 - 17:00 (10)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_hours_display_round_trip() {
|
||||
let hours = DayHours(vec![
|
||||
DayHour::new(time(8, 0), time(12, 0), None),
|
||||
DayHour::new(time(13, 0), time(17, 0), None)
|
||||
]);
|
||||
|
||||
let parsed = DayHours::try_new(&hours.to_string()).unwrap();
|
||||
|
||||
assert_eq!(parsed.hours(), hours.hours());
|
||||
}
|
||||
|
||||
// WeekHours
|
||||
|
||||
#[test]
|
||||
fn week_hours_new_formats_hours() {
|
||||
let hours = WeekHours::new(Weekday::Tue, vec![DayHour::new(time(9, 0), time(18, 0), None)]);
|
||||
|
||||
assert_eq!(hours.day(), Weekday::Tue);
|
||||
assert_eq!(hours.hours(), "09:00 - 18:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn week_hours_accepts_valid_hours() {
|
||||
for input in ["", "08:00 - 12:00", "08:00-12:00", "08:00 - 12:00, 13:00 - 17:00"] {
|
||||
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
|
||||
assert!(hours.validate().is_ok(), "input: {:?}", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn week_hours_rejects_wrong_format() {
|
||||
for input in ["8-12", "8:00 - 12:00", "morning", "08.00 - 12.00"] {
|
||||
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
|
||||
assert!(has_field_error(&hours, "hours"), "input: {:?}", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[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"] {
|
||||
let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() };
|
||||
assert!(has_field_error(&hours, "__all__"), "input: {:?}", input);
|
||||
}
|
||||
}
|
||||
|
||||
// TmCheck
|
||||
|
||||
#[test]
|
||||
fn tm_check_parses_slot() {
|
||||
let check = TmCheck::from_str("08:00:00-08:30:00").unwrap();
|
||||
|
||||
assert_eq!(check, TmCheck { from: time(8, 0), to: time(8, 30) });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tm_check_rejects_malformed_slot() {
|
||||
for input in ["", "08:00:00", "08:00:00-", "08:00:00-09:00:00-10:00:00", "a-b"] {
|
||||
assert_eq!(TmCheck::from_str(input), Err(AppError::HourParseError), "input: {:?}", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tm_check_sorts_by_time() {
|
||||
let mut checks = vec![
|
||||
TmCheck::from_str("10:00:00-11:00:00").unwrap(),
|
||||
TmCheck::from_str("08:00:00-09:00:00").unwrap(),
|
||||
TmCheck::from_str("09:00:00-10:00:00").unwrap()
|
||||
];
|
||||
checks.sort();
|
||||
|
||||
assert_eq!(checks.iter().map(|c| c.from).collect::<Vec<_>>(), vec![time(8, 0), time(9, 0), time(10, 0)]);
|
||||
}
|
||||
|
||||
// Reservations
|
||||
|
||||
fn slot(from: u32, to: u32) -> TmCheck {
|
||||
TmCheck { from: time(from, 0), to: time(to, 0) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reservations_starts_empty() {
|
||||
let reservations = Reservations::new();
|
||||
|
||||
assert!(reservations.is_empty());
|
||||
assert!(reservations.reservations().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reservations_merges_adjacent_slots() {
|
||||
let mut reservations = Reservations::new();
|
||||
reservations.add_slot(&slot(8, 9), 1);
|
||||
reservations.add_slot(&slot(9, 10), 1);
|
||||
reservations.add_slot(&slot(10, 11), 1);
|
||||
|
||||
assert_eq!(reservations.reservations(), &vec![Reservation::new(time(8, 0), time(11, 0), 1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reservations_keeps_gap_between_slots() {
|
||||
let mut reservations = Reservations::new();
|
||||
reservations.add_slot(&slot(8, 9), 1);
|
||||
reservations.add_slot(&slot(10, 11), 1);
|
||||
|
||||
assert_eq!(reservations.reservations(), &vec![
|
||||
Reservation::new(time(8, 0), time(9, 0), 1),
|
||||
Reservation::new(time(10, 0), time(11, 0), 1)
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reservations_does_not_merge_across_properties() {
|
||||
let mut reservations = Reservations::new();
|
||||
reservations.add_slot(&slot(8, 9), 1);
|
||||
reservations.add_slot(&slot(9, 10), 2);
|
||||
|
||||
assert_eq!(reservations.reservations(), &vec![
|
||||
Reservation::new(time(8, 0), time(9, 0), 1),
|
||||
Reservation::new(time(9, 0), time(10, 0), 2)
|
||||
]);
|
||||
}
|
||||
|
||||
// Entity validation
|
||||
|
||||
#[test]
|
||||
fn company_requires_name_street_and_house_number() {
|
||||
let company = Company::default();
|
||||
|
||||
assert!(has_field_error(&company, "name"));
|
||||
assert!(has_field_error(&company, "street"));
|
||||
assert!(has_field_error(&company, "house_number"));
|
||||
assert!(!has_field_error(&company, "zip_code"));
|
||||
assert!(!has_field_error(&company, "city"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn company_valid() {
|
||||
let company = Company {
|
||||
name: "Sport club".to_string(),
|
||||
street: "Main".to_string(),
|
||||
house_number: "1".to_string(),
|
||||
..Company::default()
|
||||
};
|
||||
|
||||
assert!(company.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_property_requires_name() {
|
||||
let mut property = ResProperty::default();
|
||||
assert!(has_field_error(&property, "name"));
|
||||
|
||||
property.name = "Court 1".to_string();
|
||||
assert!(property.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_profile_validation() {
|
||||
let valid = UserProfile {
|
||||
login: "joe".to_string(),
|
||||
password: Some("secret".to_string()),
|
||||
password_ver: Some("secret".to_string()),
|
||||
full_name: "Joe Doe".to_string(),
|
||||
email: "[email protected]".to_string(),
|
||||
get_emails: None,
|
||||
admin: None
|
||||
};
|
||||
assert!(valid.validate().is_ok());
|
||||
|
||||
assert!(has_field_error(&UserProfile { login: "".to_string(), ..valid.clone() }, "login"));
|
||||
assert!(has_field_error(&UserProfile { email: "not an email".to_string(), ..valid.clone() }, "email"));
|
||||
assert!(has_field_error(&UserProfile { password_ver: Some("other".to_string()), ..valid.clone() }, "password"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_profile_checkboxes() {
|
||||
let unchecked = UserProfile::default();
|
||||
assert!(!unchecked.admin());
|
||||
assert!(!unchecked.get_emails());
|
||||
|
||||
let checked = UserProfile { admin: Some("on".to_string()), get_emails: Some("on".to_string()), ..UserProfile::default() };
|
||||
assert!(checked.admin());
|
||||
assert!(checked.get_emails());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pwd_change_validation() {
|
||||
let valid = PwdChange {
|
||||
login: "joe".to_string(),
|
||||
old_password: "old".to_string(),
|
||||
password: "new".to_string(),
|
||||
password_ver: "new".to_string()
|
||||
};
|
||||
assert!(valid.validate().is_ok());
|
||||
|
||||
assert!(has_field_error(&PwdChange { password_ver: "different".to_string(), ..valid.clone() }, "password"));
|
||||
assert!(has_field_error(&PwdChange { password: "".to_string(), password_ver: "".to_string(), ..valid.clone() }, "password"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_requires_subject_and_text() {
|
||||
let mut message = Message::default();
|
||||
assert!(has_field_error(&message, "subject"));
|
||||
assert!(has_field_error(&message, "text"));
|
||||
|
||||
message.subject = "Booking".to_string();
|
||||
message.text = "Hello".to_string();
|
||||
assert!(message.validate().is_ok());
|
||||
}
|
||||
|
||||
fn valid_reservation() -> CrReservation {
|
||||
CrReservation {
|
||||
date: Local::now().date_naive(),
|
||||
slots: vec!["08:00:00-09:00:00|1".to_string()],
|
||||
full_name: "Joe Doe".to_string(),
|
||||
email: "[email protected]".to_string(),
|
||||
phone: "123456789".to_string(),
|
||||
note: "".to_string(),
|
||||
remember: false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cr_reservation_valid_for_today_and_future() {
|
||||
assert!(valid_reservation().validate().is_ok());
|
||||
|
||||
let future = CrReservation { date: Local::now().date_naive() + Duration::days(30), ..valid_reservation() };
|
||||
assert!(future.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cr_reservation_rejects_past_date() {
|
||||
let past = CrReservation { date: Local::now().date_naive() - Duration::days(1), ..valid_reservation() };
|
||||
|
||||
assert!(has_field_error(&past, "date"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cr_reservation_requires_slot_and_contact() {
|
||||
assert!(has_field_error(&CrReservation { slots: vec![], ..valid_reservation() }, "slots"));
|
||||
assert!(has_field_error(&CrReservation { full_name: "".to_string(), ..valid_reservation() }, "full_name"));
|
||||
assert!(has_field_error(&CrReservation { email: "joe".to_string(), ..valid_reservation() }, "email"));
|
||||
assert!(has_field_error(&CrReservation { phone: "".to_string(), ..valid_reservation() }, "phone"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_time_validation() {
|
||||
let one_day = ClosingTime { id: 0, from_date: date(2030, 12, 24), to_date: date(2030, 12, 24) };
|
||||
assert!(one_day.validate().is_ok());
|
||||
|
||||
let range = ClosingTime { id: 0, from_date: date(2030, 12, 24), to_date: date(2031, 1, 1) };
|
||||
assert!(range.validate().is_ok());
|
||||
|
||||
let reversed = ClosingTime { id: 0, from_date: date(2031, 1, 1), to_date: date(2030, 12, 24) };
|
||||
assert!(has_field_error(&reversed, "__all__"));
|
||||
}
|
||||
|
||||
// Enums stored in the database must keep these names
|
||||
|
||||
#[test]
|
||||
fn enum_display() {
|
||||
assert_eq!(ReservationState::New.to_string(), "New");
|
||||
assert_eq!(ReservationState::Approved.to_string(), "Approved");
|
||||
assert_eq!(ReservationState::Canceled.to_string(), "Canceled");
|
||||
|
||||
assert_eq!(MessageType::NewReservation.to_string(), "NewReservation");
|
||||
assert_eq!(MessageType::NewReservationCust.to_string(), "NewReservationCust");
|
||||
assert_eq!(MessageType::ReservationApp.to_string(), "ReservationApp");
|
||||
assert_eq!(MessageType::ReservationCanceled.to_string(), "ReservationCanceled");
|
||||
|
||||
assert_eq!(SlotType::Quarter.to_string(), "Quarter an hour");
|
||||
assert_eq!(SlotType::Half.to_string(), "Half an hour");
|
||||
assert_eq!(SlotType::Hour.to_string(), "Hour");
|
||||
assert_eq!(SlotType::Day.to_string(), "Day");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_defaults() {
|
||||
assert_eq!(ReservationState::default(), ReservationState::New);
|
||||
assert_eq!(SlotType::default(), SlotType::Hour);
|
||||
}
|
||||
}
|
||||
|
||||
+83
-1
@@ -259,4 +259,86 @@ impl ForValidation for UpdateMessage {
|
||||
fn entity(&self) -> &dyn Validate {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "ssr"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
use crate::backend::data::{Reservation, ResProperty, ResWithProperty};
|
||||
|
||||
fn reservation() -> ResSumWithItems {
|
||||
let mut property = ResProperty::default();
|
||||
property.name = "Court 1".to_string();
|
||||
|
||||
let mut reservation = ResSumWithItems::default();
|
||||
reservation.summary.date = NaiveDate::from_ymd_opt(2030, 3, 5).unwrap();
|
||||
reservation.reservations.push(ResWithProperty {
|
||||
reservation: Reservation::new(
|
||||
NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
|
||||
NaiveTime::from_hms_opt(9, 30, 0).unwrap(),
|
||||
1),
|
||||
property
|
||||
});
|
||||
|
||||
reservation
|
||||
}
|
||||
|
||||
fn message(subject: &str, text: &str) -> Message {
|
||||
let mut message = Message::default();
|
||||
message.subject = subject.to_string();
|
||||
message.text = text.to_string();
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mail_message_replaces_variables() {
|
||||
let mail = MailMessage::new(
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string(),
|
||||
message("Booking", "Date: #date#, items: #summary#."),
|
||||
&reservation());
|
||||
|
||||
assert_eq!(mail.subject, "Booking");
|
||||
assert_eq!(mail.text, "Date: 05. 03. 2030, items: Court 1: 08:00 - 09:30.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mail_message_without_variables_is_unchanged() {
|
||||
let mail = MailMessage::new(
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string(),
|
||||
message("Booking", "Plain text"),
|
||||
&reservation());
|
||||
|
||||
assert_eq!(mail.text, "Plain text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_mail_sets_headers() {
|
||||
let mail = MailMessage::new(
|
||||
"[email protected]".to_string(),
|
||||
"[email protected]".to_string(),
|
||||
message("Booking", "Hello"),
|
||||
&reservation());
|
||||
|
||||
let built = String::from_utf8(mail.build_mail("[email protected]".to_string()).unwrap().formatted()).unwrap();
|
||||
|
||||
assert!(built.contains("From: [email protected]"));
|
||||
assert!(built.contains("Reply-To: [email protected]"));
|
||||
assert!(built.contains("To: [email protected]"));
|
||||
assert!(built.contains("Subject: Booking"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_mail_rejects_invalid_address() {
|
||||
let mail = MailMessage::new(
|
||||
"[email protected]".to_string(),
|
||||
"not an address".to_string(),
|
||||
message("Booking", "Hello"),
|
||||
&reservation());
|
||||
|
||||
assert!(matches!(mail.build_mail("[email protected]".to_string()), Err(AppError::MailAddrParseErr(_))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,4 +535,50 @@ pub async fn reservations_in_month(year: i32, month: u32) -> Result<ApiResponse<
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::Data(ret.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn time(h: u32, m: u32) -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(h, m, 0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_reserved_inside_reservation() {
|
||||
let reservations = vec![Reservation::new(time(8, 0), time(10, 0), 1)];
|
||||
|
||||
assert!(is_reserved(&reservations, &time(8, 0), 1));
|
||||
assert!(is_reserved(&reservations, &time(9, 30), 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_reserved_end_is_exclusive() {
|
||||
let reservations = vec![Reservation::new(time(8, 0), time(10, 0), 1)];
|
||||
|
||||
assert!(!is_reserved(&reservations, &time(10, 0), 1));
|
||||
assert!(!is_reserved(&reservations, &time(7, 59), 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_reserved_checks_property() {
|
||||
let reservations = vec![Reservation::new(time(8, 0), time(10, 0), 1)];
|
||||
|
||||
assert!(!is_reserved(&reservations, &time(9, 0), 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_reserved_with_more_reservations() {
|
||||
let reservations = vec![
|
||||
Reservation::new(time(8, 0), time(9, 0), 1),
|
||||
Reservation::new(time(12, 0), time(13, 0), 2),
|
||||
Reservation::new(time(15, 0), time(16, 0), 1)
|
||||
];
|
||||
|
||||
assert!(is_reserved(&reservations, &time(15, 30), 1));
|
||||
assert!(is_reserved(&reservations, &time(12, 0), 2));
|
||||
assert!(!is_reserved(&reservations, &time(12, 0), 1));
|
||||
assert!(!is_reserved(&vec![], &time(12, 0), 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,3 +154,60 @@ pub fn load_config(path: &str) -> Configuration {
|
||||
toml::from_str(read_to_string(path).expect("Can not open config file").as_str()).expect("Can not parse config file")
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(all(test, feature = "ssr"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample() -> Configuration {
|
||||
load_config(concat!(env!("CARGO_MANIFEST_DIR"), "/config_sample.toml"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_config_is_valid() {
|
||||
let config = sample();
|
||||
|
||||
assert_eq!(config.network().bind_address(), SocketAddr::from_str("127.0.0.1:3000").unwrap());
|
||||
assert_eq!(config.database().con_string(), "postgres://user:password@localhost:5432/rezervator");
|
||||
assert_eq!(config.logging().severity(), "debug");
|
||||
assert!(matches!(config.mailing().transport(), MailTransport::File));
|
||||
assert_eq!(config.mailing().from(), "[email protected]");
|
||||
assert_eq!(config.mailing().path(), &Some("Maildir".to_string()));
|
||||
assert_eq!(config.mailing().server(), &None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_session_key_is_long_enough() {
|
||||
// actix_web::cookie::Key::from panics on keys shorter than 64 bytes
|
||||
assert!(sample().session().key().len() >= 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_port_defaults_to_5432() {
|
||||
let database: Database = toml::from_str(
|
||||
"user = 'u'\npassword = 'p'\ndb_name = 'db'\nhost = 'db.example.com'").unwrap();
|
||||
|
||||
assert_eq!(database.con_string(), "postgres://u:[email protected]:5432/db");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_custom_port() {
|
||||
let database: Database = toml::from_str(
|
||||
"user = 'u'\npassword = 'p'\ndb_name = 'db'\nhost = 'db.example.com'\nport = 6543").unwrap();
|
||||
|
||||
assert_eq!(database.con_string(), "postgres://u:[email protected]:6543/db");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smtp_mailing_config() {
|
||||
let mailing: Mailing = toml::from_str(
|
||||
"transport = 'Smtp'\nfrom = '[email protected]'\nserver = 'smtp.b.cz'\nport = 587\ntls = true").unwrap();
|
||||
|
||||
assert!(matches!(mailing.transport(), MailTransport::Smtp));
|
||||
assert_eq!(mailing.server(), &Some("smtp.b.cz".to_string()));
|
||||
assert_eq!(mailing.port(), Some(587));
|
||||
assert_eq!(mailing.tls(), Some(true));
|
||||
assert_eq!(mailing.accept_all_certs(), None);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-1
@@ -101,4 +101,50 @@ impl From<std::io::Error> for AppError {
|
||||
log_error(&value);
|
||||
AppError::FileIOError(value.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveTime;
|
||||
|
||||
#[test]
|
||||
fn error_messages() {
|
||||
assert_eq!(AppError::HourParseError.to_string(), "Hour parse error");
|
||||
assert_eq!(AppError::SlotParseError.to_string(), "Book slot parse error");
|
||||
assert_eq!(AppError::ServerError("boom".to_string()).to_string(), "Server error: boom");
|
||||
assert_eq!(AppError::FatalError("boom".to_string()).to_string(), "Fatal error: boom");
|
||||
assert_eq!(AppError::MailAddrParseErr("boom".to_string()).to_string(), "Cannot parse email address: boom");
|
||||
assert_eq!(AppError::MailSendError("boom".to_string()).to_string(), "Cannot send email: boom");
|
||||
assert_eq!(AppError::FileIOError("boom".to_string()).to_string(), "IO error: boom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_parse_error_becomes_hour_parse_error() {
|
||||
let err = NaiveTime::parse_from_str("nonsense", "%H:%M").unwrap_err();
|
||||
|
||||
assert_eq!(AppError::from(err), AppError::HourParseError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_fn_error_keeps_message() {
|
||||
let err = AppError::from(ServerFnError::new("boom"));
|
||||
|
||||
assert!(matches!(err, AppError::ServerError(ref m) if m.contains("boom")));
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[test]
|
||||
fn io_error_becomes_file_io_error() {
|
||||
let err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
|
||||
|
||||
assert!(matches!(AppError::from(err), AppError::FileIOError(ref m) if m.contains("missing")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_error_converts_to_server_fn_error() {
|
||||
let err: ServerFnError = AppError::HourParseError.into();
|
||||
|
||||
assert!(err.to_string().contains("Hour parse error"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user