123 lines
4.7 KiB
Rust
123 lines
4.7 KiB
Rust
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<ThoughtId>,
|
|
pub visibility: Option<String>,
|
|
pub content_warning: Option<String>,
|
|
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<CreateThoughtOutput, DomainError> {
|
|
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<Thought, DomainError> {
|
|
thoughts.find_by_id(id).await?.ok_or(DomainError::NotFound)
|
|
}
|
|
|
|
pub async fn get_thread(thoughts: &dyn ThoughtRepository, id: &ThoughtId) -> Result<Vec<Thought>, 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));
|
|
}
|
|
}
|