Files
thoughts/crates/application/src/use_cases/thoughts.rs
Gabriel Kaszewski 550865bad4
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m25s
test / unit (pull_request) Successful in 16m57s
test / integration (pull_request) Failing after 17m29s
fix: resolve all clippy warnings — redundant closures, dead code, collapsible_if, needless borrow
2026-05-14 16:33:34 +02:00

261 lines
7.4 KiB
Rust

use domain::{
errors::DomainError,
events::DomainEvent,
models::thought::{Thought, Visibility},
ports::{EventPublisher, ThoughtRepository, UserRepository},
value_objects::{Content, ThoughtId, UserId},
};
fn require_owner(thought: &Thought, user_id: &UserId) -> Result<(), DomainError> {
if thought.user_id != *user_id {
return Err(DomainError::NotFound);
}
Ok(())
}
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_db_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)?;
require_owner(&thought, user_id)?;
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)?;
require_owner(&thought, user_id)?;
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));
}
#[tokio::test]
async fn edit_thought_changes_content_and_emits_event() {
let store = TestStore::default();
let alice = user();
store.users.lock().unwrap().push(alice.clone());
let out = create_thought(&store, &store, &store, input(alice.id.clone()))
.await
.unwrap();
let tid = out.thought.id.clone();
edit_thought(&store, &store, &tid, &alice.id, "updated".to_string())
.await
.unwrap();
let saved = store
.thoughts
.lock()
.unwrap()
.iter()
.find(|t| t.id == tid)
.unwrap()
.clone();
assert_eq!(saved.content.as_str(), "updated");
let events = store.events.lock().unwrap();
assert!(events.iter().any(
|e| matches!(e, DomainEvent::ThoughtUpdated { thought_id, .. } if thought_id == &tid)
));
}
#[tokio::test]
async fn create_reply_sets_in_reply_to_id() {
let store = TestStore::default();
let alice = user();
store.users.lock().unwrap().push(alice.clone());
let original = create_thought(&store, &store, &NoOpEventPublisher, input(alice.id.clone()))
.await
.unwrap()
.thought;
create_thought(
&store,
&store,
&NoOpEventPublisher,
CreateThoughtInput {
user_id: alice.id.clone(),
content: "reply".into(),
in_reply_to_id: Some(original.id.clone()),
visibility: None,
content_warning: None,
sensitive: false,
},
)
.await
.unwrap();
let thoughts = store.thoughts.lock().unwrap();
let reply = thoughts
.iter()
.find(|t| t.content.as_str() == "reply")
.unwrap();
assert_eq!(reply.in_reply_to_id, Some(original.id.clone()));
}
}