reddit-magnet/src/services/action.rs

312 lines
9.6 KiB
Rust

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<Box<dyn Action>>,
db_service: Rc<RefCell<DatabaseService>>,
}
impl ActionService {
/// Create a new ActionService
pub fn new(db_service: Rc<RefCell<DatabaseService>>) -> 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<HashMap<String, ProcessedMagnets>> {
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<ProcessedMagnets> {
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<RefCell<DatabaseService>> {
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);
}
}