use domain::{ errors::DomainError, events::DomainEvent, models::thought::{Thought, Visibility}, ports::{EventPublisher, ThoughtRepository, UserRepository}, value_objects::{Content, ThoughtId, UserId}, }; pub struct CreateThoughtInput { pub user_id: UserId, pub content: String, pub in_reply_to_id: Option, pub visibility: Option, pub content_warning: Option, pub sensitive: bool, } pub struct CreateThoughtOutput { pub thought: Thought } pub async fn create_thought( thoughts: &dyn ThoughtRepository, _users: &dyn UserRepository, events: &dyn EventPublisher, input: CreateThoughtInput, ) -> Result { let content = Content::new_local(input.content)?; let visibility = input.visibility.as_deref().map(Visibility::from_str).unwrap_or(Visibility::Public); let thought = Thought::new_local( ThoughtId::new(), input.user_id, content, input.in_reply_to_id.clone(), visibility, input.content_warning, input.sensitive, ); thoughts.save(&thought).await?; events.publish(&DomainEvent::ThoughtCreated { thought_id: thought.id.clone(), user_id: thought.user_id.clone(), in_reply_to_id: input.in_reply_to_id, }).await?; Ok(CreateThoughtOutput { thought }) } pub async fn delete_thought( thoughts: &dyn ThoughtRepository, events: &dyn EventPublisher, id: &ThoughtId, user_id: &UserId, ) -> Result<(), DomainError> { let thought = thoughts.find_by_id(id).await?.ok_or(DomainError::NotFound)?; if thought.user_id != *user_id { return Err(DomainError::NotFound); } thoughts.delete(id, user_id).await?; events.publish(&DomainEvent::ThoughtDeleted { thought_id: id.clone(), user_id: user_id.clone() }).await?; Ok(()) } pub async fn edit_thought( thoughts: &dyn ThoughtRepository, events: &dyn EventPublisher, id: &ThoughtId, user_id: &UserId, new_content: String, ) -> Result<(), DomainError> { let thought = thoughts.find_by_id(id).await?.ok_or(DomainError::NotFound)?; if thought.user_id != *user_id { return Err(DomainError::NotFound); } let content = Content::new_local(new_content)?; thoughts.update_content(id, &content).await?; events.publish(&DomainEvent::ThoughtUpdated { thought_id: id.clone(), user_id: user_id.clone() }).await?; Ok(()) } pub async fn get_thought(thoughts: &dyn ThoughtRepository, id: &ThoughtId) -> Result { thoughts.find_by_id(id).await?.ok_or(DomainError::NotFound) } pub async fn get_thread(thoughts: &dyn ThoughtRepository, id: &ThoughtId) -> Result, DomainError> { thoughts.get_thread(id).await } #[cfg(test)] mod tests { use super::*; use domain::{ models::user::User, testing::{NoOpEventPublisher, TestStore}, value_objects::*, }; fn user() -> User { User::new_local(UserId::new(), Username::new("alice").unwrap(), Email::new("alice@ex.com").unwrap(), PasswordHash("h".into())) } fn input(uid: UserId) -> CreateThoughtInput { CreateThoughtInput { user_id: uid, content: "hello".into(), in_reply_to_id: None, visibility: None, content_warning: None, sensitive: false } } #[tokio::test] async fn create_thought_saves_and_emits_event() { let store = TestStore::default(); let u = user(); store.users.lock().unwrap().push(u.clone()); let out = create_thought(&store, &store, &store, input(u.id.clone())).await.unwrap(); assert_eq!(out.thought.content.as_str(), "hello"); assert_eq!(store.events.lock().unwrap().len(), 1); } #[tokio::test] async fn delete_own_thought_succeeds() { let store = TestStore::default(); let u = user(); store.users.lock().unwrap().push(u.clone()); let out = create_thought(&store, &store, &NoOpEventPublisher, input(u.id.clone())).await.unwrap(); delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &u.id).await.unwrap(); assert!(store.thoughts.lock().unwrap().is_empty()); } #[tokio::test] async fn delete_other_thought_returns_not_found() { let store = TestStore::default(); let alice = user(); let bob = User::new_local(UserId::new(), Username::new("bob").unwrap(), Email::new("bob@ex.com").unwrap(), PasswordHash("h".into())); store.users.lock().unwrap().extend([alice.clone(), bob.clone()]); let out = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone())).await.unwrap(); let err = delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &bob.id).await.unwrap_err(); assert!(matches!(err, DomainError::NotFound)); } }