use std::sync::Arc; use domain::testing::{InMemoryChannelRepository, NoopEventPublisher}; use domain::value_objects::{ChannelId, UserId}; use domain::DomainError; use crate::channels::commands::{CreateChannelCommand, UpdateChannelCommand}; use crate::channels::deps::ChannelCommandDeps; use crate::channels::{create, update}; fn make_deps() -> (ChannelCommandDeps, Arc) { let repo = Arc::new(InMemoryChannelRepository::new()); let deps = ChannelCommandDeps { channel_command: repo.clone(), channel_query: repo.clone(), event_publisher: Arc::new(NoopEventPublisher::new()), }; (deps, repo) } #[tokio::test] async fn updates_channel_name() { let (deps, _) = make_deps(); let owner = UserId::generate(); let channel = create::execute( &deps, CreateChannelCommand { owner_id: owner, name: "Original".into(), timezone: "UTC".into(), }, ) .await .unwrap(); let updated = update::execute( &deps, UpdateChannelCommand { channel_id: channel.id(), owner_id: owner, name: Some("Renamed".into()), description: None, timezone: None, schedule_config: None, rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await .unwrap(); assert_eq!(updated.name(), "Renamed"); assert_eq!(updated.timezone(), "UTC"); // unchanged } #[tokio::test] async fn update_fails_if_not_owner() { let (deps, _) = make_deps(); let owner = UserId::generate(); let stranger = UserId::generate(); let channel = create::execute( &deps, CreateChannelCommand { owner_id: owner, name: "Protected".into(), timezone: "UTC".into(), }, ) .await .unwrap(); let result = update::execute( &deps, UpdateChannelCommand { channel_id: channel.id(), owner_id: stranger, name: Some("Hacked".into()), description: None, timezone: None, schedule_config: None, rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await; assert!(result.is_err()); match result.unwrap_err() { DomainError::Forbidden(_) => {} other => panic!("expected Forbidden, got: {:?}", other), } } #[tokio::test] async fn update_nonexistent_channel_returns_not_found() { let (deps, _) = make_deps(); let result = update::execute( &deps, UpdateChannelCommand { channel_id: ChannelId::generate(), owner_id: UserId::generate(), name: Some("Ghost".into()), description: None, timezone: None, schedule_config: None, rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await; assert!(result.is_err()); assert!(matches!( result.unwrap_err(), DomainError::ChannelNotFound(_) )); } #[tokio::test] async fn update_config_creates_snapshot() { let (deps, repo) = make_deps(); let owner = UserId::generate(); let channel = create::execute( &deps, CreateChannelCommand { owner_id: owner, name: "Snapshotted".into(), timezone: "UTC".into(), }, ) .await .unwrap(); // Update with new schedule_config let new_config = domain::models::ScheduleConfig::default(); update::execute( &deps, UpdateChannelCommand { channel_id: channel.id(), owner_id: owner, name: None, description: None, timezone: None, schedule_config: Some(new_config), rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await .unwrap(); // Verify a config snapshot was created let snapshots = repo.snapshots.lock().unwrap(); assert_eq!(snapshots.len(), 1); assert_eq!(snapshots[0].channel_id(), channel.id()); } #[tokio::test] async fn update_without_config_skips_snapshot() { let (deps, repo) = make_deps(); let owner = UserId::generate(); let channel = create::execute( &deps, CreateChannelCommand { owner_id: owner, name: "NoSnapshot".into(), timezone: "UTC".into(), }, ) .await .unwrap(); // Update name only — no config change update::execute( &deps, UpdateChannelCommand { channel_id: channel.id(), owner_id: owner, name: Some("Renamed".into()), description: None, timezone: None, schedule_config: None, rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await .unwrap(); // No snapshot should exist let snapshots = repo.snapshots.lock().unwrap(); assert!(snapshots.is_empty()); } #[tokio::test] async fn update_description_clear() { let (deps, _) = make_deps(); let owner = UserId::generate(); let channel = create::execute( &deps, CreateChannelCommand { owner_id: owner, name: "Desc Test".into(), timezone: "UTC".into(), }, ) .await .unwrap(); // Set description let updated = update::execute( &deps, UpdateChannelCommand { channel_id: channel.id(), owner_id: owner, name: None, description: Some(Some("A description".into())), timezone: None, schedule_config: None, rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await .unwrap(); assert_eq!(updated.description(), Some("A description")); // Clear description with Some(None) let cleared = update::execute( &deps, UpdateChannelCommand { channel_id: channel.id(), owner_id: owner, name: None, description: Some(None), timezone: None, schedule_config: None, rotation_policy: None, auto_schedule: None, gap_filler: None, }, ) .await .unwrap(); assert!(cleared.description().is_none()); }