337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
use super::*;
|
|
|
|
use crate::models::{BlockContent, ProgrammingBlock, ScheduleConfig};
|
|
use crate::value_objects::{FillStrategy, InterstitialRule, MediaFilter, MidRollRule, Weekday};
|
|
use chrono::NaiveTime;
|
|
use std::collections::HashMap;
|
|
|
|
fn make_config_with_blocks(
|
|
entries: Vec<(Weekday, Vec<ProgrammingBlock>)>,
|
|
) -> ScheduleConfig {
|
|
let day_blocks: HashMap<Weekday, Vec<ProgrammingBlock>> = entries.into_iter().collect();
|
|
ScheduleConfig::from_day_blocks(day_blocks)
|
|
}
|
|
|
|
#[test]
|
|
fn vcalendar_header() {
|
|
let config = ScheduleConfig::new();
|
|
let ical = generate_ical("Test Channel", "Europe/Warsaw", &config);
|
|
assert!(ical.contains("BEGIN:VCALENDAR\r\n"));
|
|
assert!(ical.contains("VERSION:2.0\r\n"));
|
|
assert!(ical.contains("PRODID:-//K-TV//Schedule Export//EN\r\n"));
|
|
assert!(ical.contains("CALSCALE:GREGORIAN\r\n"));
|
|
assert!(ical.contains("X-WR-CALNAME:Test Channel\r\n"));
|
|
assert!(ical.contains("X-WR-TIMEZONE:Europe/Warsaw\r\n"));
|
|
assert!(ical.contains("END:VCALENDAR\r\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_config_produces_no_events() {
|
|
let config = ScheduleConfig::new();
|
|
let ical = generate_ical("Empty", "UTC", &config);
|
|
assert!(!ical.contains("BEGIN:VEVENT"));
|
|
}
|
|
|
|
#[test]
|
|
fn algorithmic_block_produces_vevent() {
|
|
let block = ProgrammingBlock::new_algorithmic(
|
|
"Morning Cartoons",
|
|
NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
|
|
120,
|
|
MediaFilter::default(),
|
|
FillStrategy::Random,
|
|
);
|
|
let config = make_config_with_blocks(vec![(Weekday::Monday, vec![block.clone()])]);
|
|
let ical = generate_ical("Kids TV", "UTC", &config);
|
|
|
|
assert!(ical.contains("BEGIN:VEVENT\r\n"));
|
|
assert!(ical.contains("END:VEVENT\r\n"));
|
|
assert!(ical.contains("DTSTART:080000\r\n"));
|
|
assert!(ical.contains("DURATION:PT2H\r\n"));
|
|
assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=MO\r\n"));
|
|
assert!(ical.contains("SUMMARY:Morning Cartoons\r\n"));
|
|
assert!(ical.contains(&format!("UID:{}\r\n", block.id())));
|
|
assert!(ical.contains("X-KTV-STRATEGY:random\r\n"));
|
|
assert!(ical.contains("X-KTV-CONTENT:"));
|
|
}
|
|
|
|
#[test]
|
|
fn different_days_produce_different_byday() {
|
|
let mon_block = ProgrammingBlock::new_algorithmic(
|
|
"Monday Block",
|
|
NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
|
|
60,
|
|
MediaFilter::default(),
|
|
FillStrategy::Sequential,
|
|
);
|
|
let fri_block = ProgrammingBlock::new_algorithmic(
|
|
"Friday Block",
|
|
NaiveTime::from_hms_opt(20, 0, 0).unwrap(),
|
|
90,
|
|
MediaFilter::default(),
|
|
FillStrategy::BestFit,
|
|
);
|
|
let config = make_config_with_blocks(vec![
|
|
(Weekday::Monday, vec![mon_block]),
|
|
(Weekday::Friday, vec![fri_block]),
|
|
]);
|
|
let ical = generate_ical("Multi-Day", "America/New_York", &config);
|
|
|
|
assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=MO\r\n"));
|
|
assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=FR\r\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn manual_block_has_no_strategy() {
|
|
let block = ProgrammingBlock::new_manual(
|
|
"Manual Show",
|
|
NaiveTime::from_hms_opt(14, 30, 0).unwrap(),
|
|
45,
|
|
vec![],
|
|
);
|
|
let config = make_config_with_blocks(vec![(Weekday::Wednesday, vec![block])]);
|
|
let ical = generate_ical("Manual Ch", "UTC", &config);
|
|
|
|
assert!(ical.contains("SUMMARY:Manual Show\r\n"));
|
|
assert!(ical.contains("DTSTART:143000\r\n"));
|
|
assert!(ical.contains("DURATION:PT45M\r\n"));
|
|
assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=WE\r\n"));
|
|
assert!(!ical.contains("X-KTV-STRATEGY:"));
|
|
}
|
|
|
|
#[test]
|
|
fn duration_with_hours_and_minutes() {
|
|
let block = ProgrammingBlock::new_manual(
|
|
"Mixed Duration",
|
|
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
|
150,
|
|
vec![],
|
|
);
|
|
let config = make_config_with_blocks(vec![(Weekday::Sunday, vec![block])]);
|
|
let ical = generate_ical("Test", "UTC", &config);
|
|
|
|
assert!(ical.contains("DURATION:PT2H30M\r\n"));
|
|
assert!(ical.contains("RRULE:FREQ=WEEKLY;BYDAY=SU\r\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn all_weekdays_mapped() {
|
|
let days_and_byday = [
|
|
(Weekday::Monday, "MO"),
|
|
(Weekday::Tuesday, "TU"),
|
|
(Weekday::Wednesday, "WE"),
|
|
(Weekday::Thursday, "TH"),
|
|
(Weekday::Friday, "FR"),
|
|
(Weekday::Saturday, "SA"),
|
|
(Weekday::Sunday, "SU"),
|
|
];
|
|
|
|
for (day, byday) in days_and_byday {
|
|
let block = ProgrammingBlock::new_manual(
|
|
"Test",
|
|
NaiveTime::from_hms_opt(12, 0, 0).unwrap(),
|
|
60,
|
|
vec![],
|
|
);
|
|
let config = make_config_with_blocks(vec![(day, vec![block])]);
|
|
let ical = generate_ical("Test", "UTC", &config);
|
|
assert!(
|
|
ical.contains(&format!("BYDAY={}\r\n", byday)),
|
|
"Expected BYDAY={} for {:?}",
|
|
byday,
|
|
day
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_algorithmic_block() {
|
|
let block = ProgrammingBlock::new_algorithmic(
|
|
"Morning Cartoons",
|
|
NaiveTime::from_hms_opt(8, 0, 0).unwrap(),
|
|
120,
|
|
MediaFilter::default(),
|
|
FillStrategy::Random,
|
|
);
|
|
let original = make_config_with_blocks(vec![(Weekday::Monday, vec![block.clone()])]);
|
|
let ical = generate_ical("Test", "UTC", &original);
|
|
let parsed = parse_ical(&ical).unwrap();
|
|
|
|
let blocks = parsed.blocks_for(Weekday::Monday);
|
|
assert_eq!(blocks.len(), 1);
|
|
let b = &blocks[0];
|
|
assert_eq!(b.name(), "Morning Cartoons");
|
|
assert_eq!(b.start_time(), NaiveTime::from_hms_opt(8, 0, 0).unwrap());
|
|
assert_eq!(b.duration_mins(), 120);
|
|
assert_eq!(b.id(), block.id());
|
|
assert!(matches!(b.content(), BlockContent::Algorithmic { strategy: FillStrategy::Random, .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_multi_day() {
|
|
let mon = ProgrammingBlock::new_algorithmic(
|
|
"Mon Block",
|
|
NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
|
|
60,
|
|
MediaFilter::default(),
|
|
FillStrategy::Sequential,
|
|
);
|
|
let fri = ProgrammingBlock::new_algorithmic(
|
|
"Fri Block",
|
|
NaiveTime::from_hms_opt(20, 0, 0).unwrap(),
|
|
90,
|
|
MediaFilter::default(),
|
|
FillStrategy::BestFit,
|
|
);
|
|
let original = make_config_with_blocks(vec![
|
|
(Weekday::Monday, vec![mon]),
|
|
(Weekday::Friday, vec![fri]),
|
|
]);
|
|
let ical = generate_ical("Multi", "UTC", &original);
|
|
let parsed = parse_ical(&ical).unwrap();
|
|
|
|
assert_eq!(parsed.blocks_for(Weekday::Monday).len(), 1);
|
|
assert_eq!(parsed.blocks_for(Weekday::Friday).len(), 1);
|
|
assert_eq!(parsed.blocks_for(Weekday::Monday)[0].name(), "Mon Block");
|
|
assert_eq!(parsed.blocks_for(Weekday::Friday)[0].name(), "Fri Block");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_manual_block() {
|
|
let block = ProgrammingBlock::new_manual(
|
|
"Manual Show",
|
|
NaiveTime::from_hms_opt(14, 30, 0).unwrap(),
|
|
45,
|
|
vec![],
|
|
);
|
|
let original = make_config_with_blocks(vec![(Weekday::Wednesday, vec![block])]);
|
|
let ical = generate_ical("Manual", "UTC", &original);
|
|
let parsed = parse_ical(&ical).unwrap();
|
|
|
|
let blocks = parsed.blocks_for(Weekday::Wednesday);
|
|
assert_eq!(blocks.len(), 1);
|
|
assert_eq!(blocks[0].name(), "Manual Show");
|
|
assert_eq!(blocks[0].duration_mins(), 45);
|
|
assert!(matches!(blocks[0].content(), BlockContent::Manual { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_with_interstitial_and_midroll() {
|
|
let mut block = ProgrammingBlock::new_algorithmic(
|
|
"Full Block",
|
|
NaiveTime::from_hms_opt(20, 0, 0).unwrap(),
|
|
180,
|
|
MediaFilter::default(),
|
|
FillStrategy::Random,
|
|
);
|
|
let interstitial = InterstitialRule::new(MediaFilter::default(), FillStrategy::Random, 30);
|
|
let midroll = MidRollRule::new(true, 15, 60, MediaFilter::default());
|
|
block = ProgrammingBlock::from_parts(
|
|
block.id(),
|
|
block.name().to_string(),
|
|
block.start_time(),
|
|
block.duration_mins(),
|
|
block.content().clone(),
|
|
Some(interstitial),
|
|
Some(midroll),
|
|
);
|
|
|
|
let original = make_config_with_blocks(vec![(Weekday::Saturday, vec![block])]);
|
|
let ical = generate_ical("Full", "UTC", &original);
|
|
let parsed = parse_ical(&ical).unwrap();
|
|
|
|
let blocks = parsed.blocks_for(Weekday::Saturday);
|
|
assert_eq!(blocks.len(), 1);
|
|
assert!(blocks[0].interstitial_rule().is_some());
|
|
assert!(blocks[0].mid_roll_rule().is_some());
|
|
assert_eq!(blocks[0].interstitial_rule().unwrap().min_gap_secs(), 30);
|
|
assert!(blocks[0].mid_roll_rule().unwrap().prefer_chapters());
|
|
}
|
|
|
|
#[test]
|
|
fn import_without_ktv_properties_uses_defaults() {
|
|
let ical = "BEGIN:VCALENDAR\r\n\
|
|
VERSION:2.0\r\n\
|
|
BEGIN:VEVENT\r\n\
|
|
SUMMARY:Generic Event\r\n\
|
|
DTSTART:090000\r\n\
|
|
DURATION:PT1H\r\n\
|
|
RRULE:FREQ=WEEKLY;BYDAY=TU\r\n\
|
|
END:VEVENT\r\n\
|
|
END:VCALENDAR\r\n";
|
|
|
|
let parsed = parse_ical(ical).unwrap();
|
|
let blocks = parsed.blocks_for(Weekday::Tuesday);
|
|
assert_eq!(blocks.len(), 1);
|
|
let b = &blocks[0];
|
|
assert_eq!(b.name(), "Generic Event");
|
|
assert_eq!(b.start_time(), NaiveTime::from_hms_opt(9, 0, 0).unwrap());
|
|
assert_eq!(b.duration_mins(), 60);
|
|
assert!(matches!(
|
|
b.content(),
|
|
BlockContent::Algorithmic { strategy: FillStrategy::Random, .. }
|
|
));
|
|
assert!(b.interstitial_rule().is_none());
|
|
assert!(b.mid_roll_rule().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn import_invalid_ical_missing_vcalendar() {
|
|
let result = parse_ical("not an ical file");
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(err.contains("BEGIN:VCALENDAR"), "error: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn import_invalid_ical_bad_duration() {
|
|
let ical = "BEGIN:VCALENDAR\r\n\
|
|
BEGIN:VEVENT\r\n\
|
|
SUMMARY:Bad\r\n\
|
|
DTSTART:090000\r\n\
|
|
DURATION:INVALID\r\n\
|
|
RRULE:FREQ=WEEKLY;BYDAY=MO\r\n\
|
|
END:VEVENT\r\n\
|
|
END:VCALENDAR\r\n";
|
|
|
|
let result = parse_ical(ical);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn import_handles_line_folding() {
|
|
let long_summary = "A".repeat(100);
|
|
let block = ProgrammingBlock::new_algorithmic(
|
|
&long_summary,
|
|
NaiveTime::from_hms_opt(10, 0, 0).unwrap(),
|
|
60,
|
|
MediaFilter::default(),
|
|
FillStrategy::Random,
|
|
);
|
|
let original = make_config_with_blocks(vec![(Weekday::Thursday, vec![block])]);
|
|
let ical = generate_ical("Fold Test", "UTC", &original);
|
|
let parsed = parse_ical(&ical).unwrap();
|
|
|
|
let blocks = parsed.blocks_for(Weekday::Thursday);
|
|
assert_eq!(blocks.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn import_multiple_byday() {
|
|
let ical = "BEGIN:VCALENDAR\r\n\
|
|
VERSION:2.0\r\n\
|
|
BEGIN:VEVENT\r\n\
|
|
SUMMARY:Weekday Show\r\n\
|
|
DTSTART:180000\r\n\
|
|
DURATION:PT2H\r\n\
|
|
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\r\n\
|
|
END:VEVENT\r\n\
|
|
END:VCALENDAR\r\n";
|
|
|
|
let parsed = parse_ical(ical).unwrap();
|
|
assert_eq!(parsed.blocks_for(Weekday::Monday).len(), 1);
|
|
assert_eq!(parsed.blocks_for(Weekday::Wednesday).len(), 1);
|
|
assert_eq!(parsed.blocks_for(Weekday::Friday).len(), 1);
|
|
assert_eq!(parsed.blocks_for(Weekday::Tuesday).len(), 0);
|
|
}
|