3ea90ed933
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
288 lines
11 KiB
Rust
288 lines
11 KiB
Rust
//! Integration tests for date math, formatting, expansion, daily paths and the month grid.
|
|
|
|
use notescraft_templates::{Context, Date, DateTime, Weekday, daily_note_path, expand, format, month_grid, parse_daily_name};
|
|
|
|
fn dt(y: i32, mo: u8, d: u8, h: u8, mi: u8, s: u8) -> DateTime {
|
|
DateTime::new(Date::new(y, mo, d).expect("valid date"), h, mi, s).expect("valid time")
|
|
}
|
|
|
|
#[test]
|
|
fn package_name_is_correct() {
|
|
assert_eq!(env!("CARGO_PKG_NAME"), "notescraft-templates");
|
|
}
|
|
|
|
#[test]
|
|
fn epoch_day_round_trip_1900_to_2200() {
|
|
for year in 1900..=2200 {
|
|
for month in 1..=12u8 {
|
|
let last = Date::days_in_month(year, month).expect("month");
|
|
for day in 1..=last {
|
|
let date = Date::new(year, month, day).expect("valid date");
|
|
let days = date.to_days_since_epoch();
|
|
assert_eq!(Date::from_days_since_epoch(days).expect("round trip"), date, "{date:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn epoch_anchors() {
|
|
assert_eq!(Date::new(1970, 1, 1).unwrap().to_days_since_epoch(), 0);
|
|
assert_eq!(Date::new(1970, 1, 2).unwrap().to_days_since_epoch(), 1);
|
|
assert_eq!(Date::new(1969, 12, 31).unwrap().to_days_since_epoch(), -1);
|
|
assert_eq!(Date::new(2000, 3, 1).unwrap().to_days_since_epoch(), 11017);
|
|
}
|
|
|
|
#[test]
|
|
fn known_weekdays_and_leap_years() {
|
|
let d = Date::new(2026, 10, 13).expect("valid");
|
|
assert_eq!(d.weekday(), Weekday::Tuesday);
|
|
assert_eq!(Date::new(2026, 10, 12).expect("valid").weekday(), Weekday::Monday);
|
|
assert_eq!(Date::new(2026, 10, 17).expect("valid").weekday(), Weekday::Saturday);
|
|
assert_eq!(Date::new(2026, 10, 18).expect("valid").weekday(), Weekday::Sunday);
|
|
assert_eq!(Date::new(1970, 1, 1).expect("valid").weekday(), Weekday::Thursday);
|
|
|
|
assert!(Date::new(2000, 2, 29).is_ok(), "2000 is a leap year");
|
|
assert!(Date::new(1900, 2, 29).is_err(), "1900 is not a leap year");
|
|
assert!(Date::new(2024, 2, 29).is_ok(), "2024 is a leap year");
|
|
assert!(Date::new(2023, 2, 29).is_err());
|
|
assert!(Date::new(2026, 13, 1).is_err());
|
|
assert!(Date::new(2026, 0, 1).is_err());
|
|
assert!(Date::new(2026, 4, 31).is_err());
|
|
assert!(Date::new(2026, 1, 0).is_err());
|
|
|
|
assert_eq!(Date::days_in_month(2024, 2), Some(29));
|
|
assert_eq!(Date::days_in_month(1900, 2), Some(28));
|
|
assert_eq!(Date::days_in_month(2026, 4), Some(30));
|
|
assert_eq!(Date::days_in_month(2026, 13), None);
|
|
}
|
|
|
|
#[test]
|
|
fn add_days_crosses_month_and_year() {
|
|
let d = Date::new(2026, 12, 31).expect("valid");
|
|
assert_eq!(d.add_days(1).expect("in range"), Date::new(2027, 1, 1).expect("valid"));
|
|
assert_eq!(d.add_days(-364).expect("in range"), Date::new(2026, 1, 1).expect("valid"));
|
|
let leap = Date::new(2024, 2, 28).expect("valid");
|
|
assert_eq!(leap.add_days(1).expect("in range"), Date::new(2024, 2, 29).expect("valid"));
|
|
assert!(Date::new(9999, 12, 31).expect("valid").add_days(1).is_err(), "out of the supported year range");
|
|
}
|
|
|
|
#[test]
|
|
fn now_with_offset_is_valid() {
|
|
for offset in [-720, -180, 0, 60, 330, 840] {
|
|
let now = DateTime::now_with_offset(offset).expect("current time is in range");
|
|
assert!(now.hour() <= 23 && now.minute() <= 59 && now.second() <= 59);
|
|
}
|
|
assert!(DateTime::now_with_offset(i32::MAX).is_err());
|
|
assert!(DateTime::now_with_offset(i32::MIN).is_err());
|
|
assert!(Date::today_utc().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn every_token() {
|
|
let d = dt(2026, 10, 13, 15, 4, 5);
|
|
let cases: [(&str, &str); 23] = [
|
|
("YYYY", "2026"),
|
|
("YY", "26"),
|
|
("MMMM", "October"),
|
|
("MMM", "Oct"),
|
|
("MM", "10"),
|
|
("M", "10"),
|
|
("DD", "13"),
|
|
("D", "13"),
|
|
("Do", "13th"),
|
|
("dddd", "Tuesday"),
|
|
("ddd", "Tue"),
|
|
("d", "2"),
|
|
("HH", "15"),
|
|
("H", "15"),
|
|
("hh", "03"),
|
|
("h", "3"),
|
|
("mm", "04"),
|
|
("m", "4"),
|
|
("ss", "05"),
|
|
("s", "5"),
|
|
("A", "PM"),
|
|
("a", "pm"),
|
|
("[YYYY] YYYY", "YYYY 2026"),
|
|
];
|
|
for (token, want) in cases {
|
|
assert_eq!(format(&d, token), want, "token {token}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn tokens_pad_and_meridiem_edges() {
|
|
let midnight = dt(2026, 1, 5, 0, 0, 0);
|
|
assert_eq!(format(&midnight, "HH:mm:ss hh:mm A"), "00:00:00 12:00 AM");
|
|
assert_eq!(format(&midnight, "h a"), "12 am");
|
|
let noon = dt(2026, 1, 5, 12, 30, 0);
|
|
assert_eq!(format(&noon, "HH:mm hh:mm a"), "12:30 12:30 pm");
|
|
let late = dt(2026, 11, 30, 23, 59, 59);
|
|
assert_eq!(format(&late, "YYYY-MM-DD HH:mm:ss"), "2026-11-30 23:59:59");
|
|
assert_eq!(format(&late, "YY-M-D h:m:s"), "26-11-30 11:59:59");
|
|
}
|
|
|
|
#[test]
|
|
fn ordinals_follow_english_rules() {
|
|
for (day, want) in
|
|
[(1u8, "1st"), (2, "2nd"), (3, "3rd"), (4, "4th"), (11, "11th"), (12, "12th"), (13, "13th"), (21, "21st"), (22, "22nd"), (23, "23rd"), (31, "31st")]
|
|
{
|
|
let d = dt(2026, 1, day, 0, 0, 0);
|
|
assert_eq!(format(&d, "Do"), want, "day {day}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn bracket_escapes_and_unknown_characters() {
|
|
let d = dt(2026, 10, 13, 15, 4, 5);
|
|
assert_eq!(format(&d, "[today is] YYYY"), "today is 2026");
|
|
assert_eq!(format(&d, "[Do] Do"), "Do 13th");
|
|
assert_eq!(format(&d, "[[nested]"), "[nested");
|
|
assert_eq!(format(&d, "![YYYY"), "![2026");
|
|
assert_eq!(format(&d, "Q q # 100% ZZ"), "Q q # 100% ZZ");
|
|
assert_eq!(format(&d, ""), "");
|
|
}
|
|
|
|
#[test]
|
|
fn expand_variables() {
|
|
let ctx = Context { title: "Meeting notes", now: dt(2026, 10, 13, 15, 4, 5) };
|
|
assert_eq!(expand("# {{title}}\n", &ctx), "# Meeting notes\n");
|
|
assert_eq!(expand("{{date}}", &ctx), "2026-10-13");
|
|
assert_eq!(expand("{{time}}", &ctx), "15:04");
|
|
assert_eq!(expand("{{date:dddd D MMMM YYYY}}", &ctx), "Tuesday 13 October 2026");
|
|
assert_eq!(expand("{{time:h:mm a}}", &ctx), "3:04 pm");
|
|
assert_eq!(expand("{{title}}{{title}}", &ctx), "Meeting notesMeeting notes");
|
|
}
|
|
|
|
#[test]
|
|
fn expand_leaves_unknown_and_broken_variables_alone() {
|
|
let ctx = Context { title: "T", now: dt(2026, 10, 13, 15, 4, 5) };
|
|
assert_eq!(expand("{{unknown}}", &ctx), "{{unknown}}");
|
|
assert_eq!(expand("{{date:BOGUS}}", &ctx), "BOGUS");
|
|
assert_eq!(expand("{{ title }}", &ctx), "{{ title }}");
|
|
assert_eq!(expand("{{date", &ctx), "{{date");
|
|
assert_eq!(expand("{date}}", &ctx), "{date}}");
|
|
assert_eq!(expand("plain text", &ctx), "plain text");
|
|
}
|
|
|
|
#[test]
|
|
fn expand_does_not_recurse() {
|
|
let ctx = Context { title: "{{date}} {{title}}", now: dt(2026, 10, 13, 15, 4, 5) };
|
|
assert_eq!(expand("{{title}}", &ctx), "{{date}} {{title}}");
|
|
assert_eq!(expand("{{date:YYYY}}-{{date:YYYY}}", &ctx), "2026-2026");
|
|
}
|
|
|
|
#[test]
|
|
fn expand_handles_huge_and_empty_input() {
|
|
let ctx = Context { title: "", now: dt(2026, 10, 13, 0, 0, 0) };
|
|
assert_eq!(expand("", &ctx), "");
|
|
let big = "{{date:YYYY-MM-DD}}".repeat(200_000);
|
|
let out = expand(&big, &ctx);
|
|
assert_eq!(out.len(), 200_000 * 10);
|
|
let unterminated = "{{".repeat(500_000);
|
|
assert_eq!(expand(&unterminated, &ctx), unterminated);
|
|
}
|
|
|
|
#[test]
|
|
fn daily_note_paths() {
|
|
let date = Date::new(2026, 10, 13).expect("valid");
|
|
assert_eq!(daily_note_path("Daily", "YYYY-MM-DD", &date).expect("ok"), "Daily/2026-10-13.md");
|
|
assert_eq!(daily_note_path("", "YYYY-MM-DD", &date).expect("ok"), "2026-10-13.md");
|
|
assert_eq!(daily_note_path("Daily/", "YYYY-MM-DD", &date).expect("ok"), "Daily/2026-10-13.md");
|
|
assert_eq!(daily_note_path("Daily", "YYYY/MM/YYYY-MM-DD", &date).expect("ok"), "Daily/2026/10/2026-10-13.md");
|
|
assert_eq!(daily_note_path("Daily\\Notes", "YYYY-MM-DD", &date).expect("ok"), "Daily/Notes/2026-10-13.md");
|
|
assert_eq!(daily_note_path("Daily", "DD-MM-YYYY", &date).expect("ok"), "Daily/13-10-2026.md");
|
|
}
|
|
|
|
#[test]
|
|
fn daily_note_path_rejects_traversal() {
|
|
let date = Date::new(2026, 10, 13).expect("valid");
|
|
for (folder, fmt) in [
|
|
("..", "YYYY-MM-DD"),
|
|
("../evil", "YYYY-MM-DD"),
|
|
("Daily/..", "YYYY-MM-DD"),
|
|
("a/../b", "YYYY-MM-DD"),
|
|
("Daily", ".."),
|
|
("Daily", "../YYYY-MM-DD"),
|
|
("Daily", "YYYY/../YYYY-MM-DD"),
|
|
] {
|
|
assert!(daily_note_path(folder, fmt, &date).is_err(), "{folder} {fmt} must be rejected");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parse_daily_name_common_formats() {
|
|
let want = Date::new(2026, 10, 13).expect("valid");
|
|
for fmt in ["YYYY-MM-DD", "YYYY/MM/YYYY-MM-DD", "DD-MM-YYYY", "YYYYMMDD", "YYYY_MM_DD", "ddd, DD MMM YYYY", "M-D-YYYY", "YYYY-MM-DD HH-mm"] {
|
|
let stem = match fmt {
|
|
"DD-MM-YYYY" => "13-10-2026",
|
|
"YYYYMMDD" => "20261013",
|
|
"YYYY_MM_DD" => "2026_10_13",
|
|
"ddd, DD MMM YYYY" => "Tue, 13 Oct 2026",
|
|
"M-D-YYYY" => "10-13-2026",
|
|
"YYYY-MM-DD HH-mm" => "2026-10-13 15-04",
|
|
_ => "2026-10-13",
|
|
};
|
|
assert_eq!(parse_daily_name(fmt, stem), Some(want), "format {fmt}");
|
|
}
|
|
assert_eq!(parse_daily_name("YYYY-MM-DD", "2026-10-13"), Some(want));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_daily_name_rejects_bad_input() {
|
|
for (fmt, stem) in [
|
|
("YYYY-MM-DD", "2026-10"),
|
|
("YYYY-MM-DD", "not-a-date"),
|
|
("YYYY-MM-DD", "2026-10-13 extra"),
|
|
("YYYY-MM-DD", "2026-13-13"),
|
|
("YYYY-MM-DD", "2023-02-29"),
|
|
("DD-MM-YYYY", "31-02-2026"),
|
|
("YYYY-MM-DD", ""),
|
|
("", "2026-10-13"),
|
|
("YYYY-MM-DD", "2026-10-33"),
|
|
("GGGG-MM-DD", "abcd-10-13"),
|
|
("dd-MM-yyyy", "13-10-2026"),
|
|
("YYYY-MM-DD", "2026-10-13-13"),
|
|
] {
|
|
assert_eq!(parse_daily_name(fmt, stem), None, "{fmt} / {stem} must not parse");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn month_grid_october_2026_sunday_start() {
|
|
let grid = month_grid(2026, 10, Weekday::Sunday).expect("ok");
|
|
assert_eq!(grid[0], [None, None, None, None, Some(1), Some(2), Some(3)]);
|
|
assert_eq!(grid[1], [Some(4), Some(5), Some(6), Some(7), Some(8), Some(9), Some(10)]);
|
|
assert_eq!(grid[4], [Some(25), Some(26), Some(27), Some(28), Some(29), Some(30), Some(31)]);
|
|
assert_eq!(grid[5], [None; 7]);
|
|
let filled: Vec<u8> = grid.iter().flatten().filter_map(|c| *c).collect();
|
|
assert_eq!(filled.len(), 31);
|
|
assert_eq!(filled.first().copied(), Some(1));
|
|
assert_eq!(filled.last().copied(), Some(31));
|
|
}
|
|
|
|
#[test]
|
|
fn month_grid_other_starts_and_months() {
|
|
let monday = month_grid(2026, 10, Weekday::Monday).expect("ok");
|
|
assert_eq!(monday[0], [None, None, None, Some(1), Some(2), Some(3), Some(4)]);
|
|
|
|
let feb_2026 = month_grid(2026, 2, Weekday::Sunday).expect("ok");
|
|
let feb_days: Vec<u8> = feb_2026.iter().flatten().filter_map(|c| *c).collect();
|
|
assert_eq!(feb_days.len(), 28);
|
|
assert_eq!(feb_2026[0][0], Some(1));
|
|
assert_eq!(feb_2026[3][6], Some(28));
|
|
assert_eq!(feb_2026[4], [None; 7]);
|
|
assert_eq!(feb_2026[5], [None; 7]);
|
|
|
|
let feb_2024 = month_grid(2024, 2, Weekday::Sunday).expect("ok");
|
|
assert_eq!(feb_2024.iter().flatten().filter_map(|c| *c).count(), 29);
|
|
assert_eq!(feb_2024[4][4], Some(29));
|
|
|
|
let june_2026 = month_grid(2026, 6, Weekday::Monday).expect("ok");
|
|
assert_eq!(june_2026[0][0], Some(1));
|
|
assert!(month_grid(2026, 13, Weekday::Sunday).is_err());
|
|
assert!(month_grid(2026, 0, Weekday::Sunday).is_err());
|
|
assert!(month_grid(99_999, 1, Weekday::Sunday).is_err());
|
|
}
|