feat(tests): add unit tests for auto-scheduler functionality
This commit is contained in:
@@ -74,3 +74,211 @@ async fn tick(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use domain::{
|
||||
Channel, ChannelRepository, Collection, DomainResult, GeneratedSchedule, IProviderRegistry,
|
||||
MediaFilter, MediaItem, MediaItemId, PlaybackRecord, ProviderCapabilities, ScheduleEngineService,
|
||||
ScheduleRepository, SeriesSummary, StreamQuality, StreamingProtocol,
|
||||
};
|
||||
use domain::value_objects::{ChannelId, ContentType, UserId};
|
||||
use uuid::Uuid;
|
||||
|
||||
// ── Mocks ─────────────────────────────────────────────────────────────────
|
||||
|
||||
struct MockChannelRepo {
|
||||
channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ChannelRepository for MockChannelRepo {
|
||||
async fn find_by_id(&self, id: ChannelId) -> DomainResult<Option<Channel>> {
|
||||
Ok(self.channels.iter().find(|c| c.id == id).cloned())
|
||||
}
|
||||
async fn find_by_owner(&self, _owner_id: UserId) -> DomainResult<Vec<Channel>> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn find_all(&self) -> DomainResult<Vec<Channel>> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn find_auto_schedule_enabled(&self) -> DomainResult<Vec<Channel>> {
|
||||
Ok(self.channels.clone())
|
||||
}
|
||||
async fn save(&self, _channel: &Channel) -> DomainResult<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn delete(&self, _id: ChannelId) -> DomainResult<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
struct MockScheduleRepo {
|
||||
latest: Option<GeneratedSchedule>,
|
||||
saved: Arc<Mutex<Vec<GeneratedSchedule>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ScheduleRepository for MockScheduleRepo {
|
||||
async fn find_active(
|
||||
&self,
|
||||
_channel_id: ChannelId,
|
||||
_at: DateTime<Utc>,
|
||||
) -> DomainResult<Option<GeneratedSchedule>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn find_latest(&self, _channel_id: ChannelId) -> DomainResult<Option<GeneratedSchedule>> {
|
||||
Ok(self.latest.clone())
|
||||
}
|
||||
async fn save(&self, schedule: &GeneratedSchedule) -> DomainResult<()> {
|
||||
self.saved.lock().unwrap().push(schedule.clone());
|
||||
Ok(())
|
||||
}
|
||||
async fn find_playback_history(&self, _channel_id: ChannelId) -> DomainResult<Vec<PlaybackRecord>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn save_playback_record(&self, _record: &PlaybackRecord) -> DomainResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockRegistry;
|
||||
|
||||
#[async_trait]
|
||||
impl IProviderRegistry for MockRegistry {
|
||||
async fn fetch_items(&self, _provider_id: &str, _filter: &MediaFilter) -> DomainResult<Vec<MediaItem>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
async fn fetch_by_id(&self, _item_id: &MediaItemId) -> DomainResult<Option<MediaItem>> {
|
||||
Ok(None)
|
||||
}
|
||||
async fn get_stream_url(&self, _item_id: &MediaItemId, _quality: &StreamQuality) -> DomainResult<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn provider_ids(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
fn primary_id(&self) -> &str {
|
||||
""
|
||||
}
|
||||
fn capabilities(&self, _provider_id: &str) -> Option<ProviderCapabilities> {
|
||||
None
|
||||
}
|
||||
async fn list_collections(&self, _provider_id: &str) -> DomainResult<Vec<Collection>> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn list_series(&self, _provider_id: &str, _collection_id: Option<&str>) -> DomainResult<Vec<SeriesSummary>> {
|
||||
unimplemented!()
|
||||
}
|
||||
async fn list_genres(&self, _provider_id: &str, _content_type: Option<&ContentType>) -> DomainResult<Vec<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_channel() -> Channel {
|
||||
let mut ch = Channel::new(Uuid::new_v4(), "Test", "UTC");
|
||||
ch.auto_schedule = true;
|
||||
ch
|
||||
}
|
||||
|
||||
fn make_schedule(channel_id: ChannelId, valid_until: DateTime<Utc>) -> GeneratedSchedule {
|
||||
GeneratedSchedule {
|
||||
id: Uuid::new_v4(),
|
||||
channel_id,
|
||||
valid_from: valid_until - Duration::hours(48),
|
||||
valid_until,
|
||||
generation: 1,
|
||||
slots: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn make_engine(
|
||||
channel_repo: Arc<dyn ChannelRepository>,
|
||||
schedule_repo: Arc<dyn ScheduleRepository>,
|
||||
) -> Arc<ScheduleEngineService> {
|
||||
Arc::new(ScheduleEngineService::new(
|
||||
Arc::new(MockRegistry),
|
||||
channel_repo,
|
||||
schedule_repo,
|
||||
))
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_schedule_generates_from_now() {
|
||||
let ch = make_channel();
|
||||
let saved = Arc::new(Mutex::new(vec![]));
|
||||
let channel_repo: Arc<dyn ChannelRepository> = Arc::new(MockChannelRepo { channels: vec![ch] });
|
||||
let schedule_repo: Arc<dyn ScheduleRepository> =
|
||||
Arc::new(MockScheduleRepo { latest: None, saved: saved.clone() });
|
||||
let engine = make_engine(channel_repo.clone(), schedule_repo);
|
||||
|
||||
tick(&engine, &channel_repo).await;
|
||||
|
||||
let saved = saved.lock().unwrap();
|
||||
assert_eq!(saved.len(), 1);
|
||||
let diff = (saved[0].valid_from - Utc::now()).num_seconds().abs();
|
||||
assert!(diff < 5, "valid_from should be ~now, diff={diff}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fresh_schedule_skips() {
|
||||
let ch = make_channel();
|
||||
let valid_until = Utc::now() + Duration::hours(25);
|
||||
let schedule = make_schedule(ch.id, valid_until);
|
||||
let saved = Arc::new(Mutex::new(vec![]));
|
||||
let channel_repo: Arc<dyn ChannelRepository> = Arc::new(MockChannelRepo { channels: vec![ch] });
|
||||
let schedule_repo: Arc<dyn ScheduleRepository> =
|
||||
Arc::new(MockScheduleRepo { latest: Some(schedule), saved: saved.clone() });
|
||||
let engine = make_engine(channel_repo.clone(), schedule_repo);
|
||||
|
||||
tick(&engine, &channel_repo).await;
|
||||
|
||||
assert_eq!(saved.lock().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_expiring_schedule_seamless_handoff() {
|
||||
let ch = make_channel();
|
||||
let valid_until = Utc::now() + Duration::hours(20);
|
||||
let schedule = make_schedule(ch.id, valid_until);
|
||||
let saved = Arc::new(Mutex::new(vec![]));
|
||||
let channel_repo: Arc<dyn ChannelRepository> = Arc::new(MockChannelRepo { channels: vec![ch] });
|
||||
let schedule_repo: Arc<dyn ScheduleRepository> =
|
||||
Arc::new(MockScheduleRepo { latest: Some(schedule), saved: saved.clone() });
|
||||
let engine = make_engine(channel_repo.clone(), schedule_repo);
|
||||
|
||||
tick(&engine, &channel_repo).await;
|
||||
|
||||
let saved = saved.lock().unwrap();
|
||||
assert_eq!(saved.len(), 1);
|
||||
assert_eq!(saved[0].valid_from, valid_until);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_expired_schedule_generates_from_now() {
|
||||
let ch = make_channel();
|
||||
let valid_until = Utc::now() - Duration::hours(1);
|
||||
let schedule = make_schedule(ch.id, valid_until);
|
||||
let saved = Arc::new(Mutex::new(vec![]));
|
||||
let channel_repo: Arc<dyn ChannelRepository> = Arc::new(MockChannelRepo { channels: vec![ch] });
|
||||
let schedule_repo: Arc<dyn ScheduleRepository> =
|
||||
Arc::new(MockScheduleRepo { latest: Some(schedule), saved: saved.clone() });
|
||||
let engine = make_engine(channel_repo.clone(), schedule_repo);
|
||||
|
||||
tick(&engine, &channel_repo).await;
|
||||
|
||||
let saved = saved.lock().unwrap();
|
||||
assert_eq!(saved.len(), 1);
|
||||
let diff = (saved[0].valid_from - Utc::now()).num_seconds().abs();
|
||||
assert!(diff < 5, "valid_from should be ~now, diff={diff}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user