use crate::actions::action::{Action, ProcessedMagnets}; use crate::actions::bitmagnet::BitmagnetAction; use crate::actions::factory::init_action; use crate::actions::transmission::TransmissionAction; use crate::config::Config; use crate::services::database::DatabaseService; use color_eyre::eyre::Result; use log::{debug, warn}; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; /// Service for managing actions pub struct ActionService { actions: Vec>, db_service: Rc>, } impl ActionService { /// Create a new ActionService pub fn new(db_service: Rc>) -> Self { Self { actions: Vec::new(), db_service, } } /// Initialize actions based on configuration pub fn init_actions(&mut self, config: &Config) -> Result<()> { if let Some(action) = init_action(&config.bitmagnet, BitmagnetAction::new)? { self.actions.push(action); } if let Some(action) = init_action(&config.transmission, TransmissionAction::new)? { self.actions.push(action); } Ok(()) } /// Process magnet links with actions #[allow(clippy::await_holding_refcell_ref)] // Not sure how to address this pub async fn process_actions(&mut self) -> Result> { let mut action_results = HashMap::new(); for action in &mut self.actions { debug!("Processing magnet links with {}", action.get_name()); match action .process_unprocessed_magnets(&mut self.db_service.borrow_mut()) .await { Ok(processed_magnets) => { let count = processed_magnets.success.len(); debug!( "Successfully processed {} magnet links with {}", count, action.get_name() ); action_results.insert(action.get_name().to_string(), processed_magnets); } Err(e) => { warn!( "Failed to process magnet links with {}: {}", action.get_name(), e ); } } } Ok(action_results) } } #[cfg(test)] mod tests { use super::*; use crate::actions::action::ProcessedMagnets; use crate::db::Database; use crate::models::Magnet; use async_trait::async_trait; use chrono::NaiveDateTime; use color_eyre::eyre::{eyre, Result}; use std::collections::BTreeMap; use std::sync::atomic::{AtomicBool, Ordering}; use tempfile::tempdir; struct MockAction { name: &'static str, process_called: AtomicBool, should_fail: bool, success_count: usize, failed_count: usize, } impl MockAction { fn new(name: &'static str, should_fail: bool) -> Self { Self { name, process_called: AtomicBool::new(false), should_fail, success_count: 0, failed_count: 0, } } fn with_success_count(mut self, count: usize) -> Self { self.success_count = count; self } fn with_failed_count(mut self, count: usize) -> Self { self.failed_count = count; self } } #[async_trait] impl Action for MockAction { fn name() -> &'static str where Self: Sized, { "MockAction" } fn get_name(&self) -> &'static str { self.name } async fn process_unprocessed_magnets( &mut self, _db_service: &mut DatabaseService, ) -> Result { self.process_called.store(true, Ordering::SeqCst); if self.should_fail { return Err(eyre!("Mock action failed")); } let mut success = Vec::new(); for i in 0..self.success_count { success.push(create_test_magnet( i as i32, &format!("Success Magnet {}", i), )); } let mut failed = Vec::new(); for i in 0..self.failed_count { failed.push(create_test_magnet( i as i32 + 100, &format!("Failed Magnet {}", i), )); } Ok(ProcessedMagnets { success, failed }) } } fn create_test_magnet(id: i32, title: &str) -> Magnet { Magnet { id, title: title.to_string(), submitter: "test_user".to_string(), subreddit: "test_subreddit".to_string(), link: format!("magnet:?xt=urn:btih:{}", id), published_at: NaiveDateTime::default(), imdb_id: None, } } fn setup_test_db() -> Rc> { let temp_dir = tempdir().unwrap(); let db_path = temp_dir.path().join("test.db"); let db = Database::new(&db_path).unwrap(); let db_service = DatabaseService::new(db); Rc::new(RefCell::new(db_service)) } #[test] fn test_new_action_service() { let db_service = setup_test_db(); let action_service = ActionService::new(db_service); assert_eq!(action_service.actions.len(), 0); } #[test] fn test_init_actions_with_empty_config() { let db_service = setup_test_db(); let mut action_service = ActionService::new(db_service); // Create an empty config let config = Config { bitmagnet: None, transmission: None, ntfy: None, sources: BTreeMap::new(), }; let result = action_service.init_actions(&config); assert!(result.is_ok()); assert_eq!(action_service.actions.len(), 0); } #[tokio::test] async fn test_process_actions_with_no_actions() { let db_service = setup_test_db(); let mut action_service = ActionService::new(db_service); let result = action_service.process_actions().await; assert!(result.is_ok()); let action_results = result.unwrap(); assert_eq!(action_results.len(), 0); } #[tokio::test] async fn test_process_actions_with_successful_action() { let db_service = setup_test_db(); let mut action_service = ActionService::new(db_service); // Create a mock action that succeeds let mock_action = MockAction::new("SuccessAction", false).with_success_count(2); action_service.actions.push(Box::new(mock_action)); let result = action_service.process_actions().await; assert!(result.is_ok()); let action_results = result.unwrap(); assert_eq!(action_results.len(), 1); assert!(action_results.contains_key("SuccessAction")); let processed_magnets = &action_results["SuccessAction"]; assert_eq!(processed_magnets.success.len(), 2); assert_eq!(processed_magnets.failed.len(), 0); } #[tokio::test] async fn test_process_actions_with_failing_action() { let db_service = setup_test_db(); let mut action_service = ActionService::new(db_service); // Create a mock action that fails let mock_action = MockAction::new("FailingAction", true); action_service.actions.push(Box::new(mock_action)); let result = action_service.process_actions().await; assert!(result.is_ok()); let action_results = result.unwrap(); assert_eq!(action_results.len(), 0); // No results for failing action } #[tokio::test] async fn test_process_actions_with_mixed_results() { let db_service = setup_test_db(); let mut action_service = ActionService::new(db_service); // Create a mock action with both successful and failed magnets let mock_action = MockAction::new("MixedAction", false) .with_success_count(1) .with_failed_count(1); action_service.actions.push(Box::new(mock_action)); let result = action_service.process_actions().await; assert!(result.is_ok()); let action_results = result.unwrap(); assert_eq!(action_results.len(), 1); assert!(action_results.contains_key("MixedAction")); let processed_magnets = &action_results["MixedAction"]; assert_eq!(processed_magnets.success.len(), 1); assert_eq!(processed_magnets.failed.len(), 1); } #[tokio::test] async fn test_process_actions_with_multiple_actions() { let db_service = setup_test_db(); let mut action_service = ActionService::new(db_service); // Create two mock actions let action1 = MockAction::new("Action1", false).with_success_count(1); let action2 = MockAction::new("Action2", false).with_success_count(1); action_service.actions.push(Box::new(action1)); action_service.actions.push(Box::new(action2)); let result = action_service.process_actions().await; assert!(result.is_ok()); let action_results = result.unwrap(); assert_eq!(action_results.len(), 2); assert!(action_results.contains_key("Action1")); assert!(action_results.contains_key("Action2")); let processed_magnets1 = &action_results["Action1"]; assert_eq!(processed_magnets1.success.len(), 1); let processed_magnets2 = &action_results["Action2"]; assert_eq!(processed_magnets2.success.len(), 1); } }