application: channels bounded context
CQRS use cases (create/update/delete/get/list/list_by_owner), ownership checks, auto-snapshot on config change. Channel setters added to domain model.
This commit is contained in:
251
crates/application/src/channels/tests/update.rs
Normal file
251
crates/application/src/channels/tests/update.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
|
||||
use domain::value_objects::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<InMemoryChannelRepository>) {
|
||||
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.value(),
|
||||
name: "Original".into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = update::execute(
|
||||
&deps,
|
||||
UpdateChannelCommand {
|
||||
channel_id: channel.id().value(),
|
||||
owner_id: owner.value(),
|
||||
name: Some("Renamed".into()),
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
auto_schedule: 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.value(),
|
||||
name: "Protected".into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = update::execute(
|
||||
&deps,
|
||||
UpdateChannelCommand {
|
||||
channel_id: channel.id().value(),
|
||||
owner_id: stranger.value(),
|
||||
name: Some("Hacked".into()),
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
auto_schedule: 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: uuid::Uuid::new_v4(),
|
||||
owner_id: uuid::Uuid::new_v4(),
|
||||
name: Some("Ghost".into()),
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
auto_schedule: 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.value(),
|
||||
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().value(),
|
||||
owner_id: owner.value(),
|
||||
name: None,
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: Some(new_config),
|
||||
recycle_policy: None,
|
||||
auto_schedule: 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.value(),
|
||||
name: "NoSnapshot".into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Update name only — no config change
|
||||
update::execute(
|
||||
&deps,
|
||||
UpdateChannelCommand {
|
||||
channel_id: channel.id().value(),
|
||||
owner_id: owner.value(),
|
||||
name: Some("Renamed".into()),
|
||||
description: None,
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
auto_schedule: 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.value(),
|
||||
name: "Desc Test".into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set description
|
||||
let updated = update::execute(
|
||||
&deps,
|
||||
UpdateChannelCommand {
|
||||
channel_id: channel.id().value(),
|
||||
owner_id: owner.value(),
|
||||
name: None,
|
||||
description: Some(Some("A description".into())),
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
auto_schedule: 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().value(),
|
||||
owner_id: owner.value(),
|
||||
name: None,
|
||||
description: Some(None),
|
||||
timezone: None,
|
||||
schedule_config: None,
|
||||
recycle_policy: None,
|
||||
auto_schedule: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(cleared.description().is_none());
|
||||
}
|
||||
Reference in New Issue
Block a user