Files
thoughts/crates/application/src/services/notification_event.rs
Gabriel Kaszewski 10c4a66de5
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 6m49s
test / unit (pull_request) Successful in 16m24s
test / integration (pull_request) Failing after 17m7s
Refactor handlers and OpenAPI documentation for improved readability and consistency
- Reorganized imports in health, notifications, social, thoughts, and users handlers for clarity.
- Updated function signatures in handlers to improve readability by aligning parameters.
- Enhanced JSON response formatting in notifications and thoughts handlers.
- Improved error handling in user-related functions.
- Refactored OpenAPI documentation to maintain consistent formatting and structure.
- Cleaned up unnecessary code and comments across various files.
- Ensured consistent use of `Arc` for shared state in AppState and WorkerHandlers.
2026-05-14 16:28:57 +02:00

317 lines
10 KiB
Rust

use chrono::Utc;
use domain::{
errors::DomainError,
events::DomainEvent,
models::notification::{Notification, NotificationType},
ports::{NotificationRepository, ThoughtRepository},
value_objects::{NotificationId, UserId},
};
use std::sync::Arc;
pub struct NotificationEventService {
pub thoughts: Arc<dyn ThoughtRepository>,
pub notifications: Arc<dyn NotificationRepository>,
}
fn is_self_action(thought_author: &UserId, actor: &UserId) -> bool {
thought_author == actor
}
impl NotificationEventService {
pub async fn process(&self, event: &DomainEvent) -> Result<(), DomainError> {
match event {
DomainEvent::LikeAdded {
like_id: _,
user_id,
thought_id,
} => {
let thought = match self.thoughts.find_by_id(thought_id).await? {
Some(t) => t,
None => return Ok(()),
};
if is_self_action(&thought.user_id, user_id) {
return Ok(());
}
self.notifications
.save(&Notification {
id: NotificationId::new(),
user_id: thought.user_id,
notification_type: NotificationType::Like,
from_user_id: Some(user_id.clone()),
thought_id: Some(thought_id.clone()),
read: false,
created_at: Utc::now(),
})
.await
}
DomainEvent::BoostAdded {
boost_id: _,
user_id,
thought_id,
} => {
let thought = match self.thoughts.find_by_id(thought_id).await? {
Some(t) => t,
None => return Ok(()),
};
if is_self_action(&thought.user_id, user_id) {
return Ok(());
}
self.notifications
.save(&Notification {
id: NotificationId::new(),
user_id: thought.user_id,
notification_type: NotificationType::Boost,
from_user_id: Some(user_id.clone()),
thought_id: Some(thought_id.clone()),
read: false,
created_at: Utc::now(),
})
.await
}
DomainEvent::FollowAccepted {
follower_id,
following_id,
} => {
self.notifications
.save(&Notification {
id: NotificationId::new(),
user_id: following_id.clone(),
notification_type: NotificationType::Follow,
from_user_id: Some(follower_id.clone()),
thought_id: None,
read: false,
created_at: Utc::now(),
})
.await
}
DomainEvent::ThoughtCreated {
thought_id,
user_id,
in_reply_to_id,
} => {
let reply_to_id = match in_reply_to_id {
Some(id) => id,
None => return Ok(()),
};
let original = match self.thoughts.find_by_id(reply_to_id).await? {
Some(t) => t,
None => return Ok(()),
};
if is_self_action(&original.user_id, user_id) {
return Ok(());
}
self.notifications
.save(&Notification {
id: NotificationId::new(),
user_id: original.user_id,
notification_type: NotificationType::Reply,
from_user_id: Some(user_id.clone()),
thought_id: Some(thought_id.clone()),
read: false,
created_at: Utc::now(),
})
.await
}
_ => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use domain::{
models::{
thought::{Thought, Visibility},
user::User,
},
testing::TestStore,
value_objects::*,
};
use std::sync::Arc;
fn alice() -> User {
User::new_local(
UserId::new(),
Username::new("alice").unwrap(),
Email::new("alice@ex.com").unwrap(),
PasswordHash("h".into()),
)
}
#[tokio::test]
async fn like_creates_notification_for_thought_author() {
let store = TestStore::default();
let alice = alice();
let bob_id = UserId::new();
let thought = Thought::new_local(
ThoughtId::new(),
alice.id.clone(),
Content::new_local("hello").unwrap(),
None,
Visibility::Public,
None,
false,
);
store.thoughts.lock().unwrap().push(thought.clone());
let svc = NotificationEventService {
thoughts: Arc::new(store.clone()),
notifications: Arc::new(store.clone()),
};
svc.process(&DomainEvent::LikeAdded {
like_id: LikeId::new(),
user_id: bob_id,
thought_id: thought.id.clone(),
})
.await
.unwrap();
let notifs = store.notifications.lock().unwrap();
assert_eq!(notifs.len(), 1);
assert!(matches!(
notifs[0].notification_type,
NotificationType::Like
));
}
#[tokio::test]
async fn self_like_creates_no_notification() {
let store = TestStore::default();
let alice = alice();
let thought = Thought::new_local(
ThoughtId::new(),
alice.id.clone(),
Content::new_local("hello").unwrap(),
None,
Visibility::Public,
None,
false,
);
store.thoughts.lock().unwrap().push(thought.clone());
let svc = NotificationEventService {
thoughts: Arc::new(store.clone()),
notifications: Arc::new(store.clone()),
};
svc.process(&DomainEvent::LikeAdded {
like_id: LikeId::new(),
user_id: alice.id.clone(),
thought_id: thought.id.clone(),
})
.await
.unwrap();
assert!(store.notifications.lock().unwrap().is_empty());
}
#[tokio::test]
async fn follow_accepted_creates_notification() {
let store = TestStore::default();
let alice = alice();
let bob_id = UserId::new();
let svc = NotificationEventService {
thoughts: Arc::new(store.clone()),
notifications: Arc::new(store.clone()),
};
svc.process(&DomainEvent::FollowAccepted {
follower_id: bob_id,
following_id: alice.id.clone(),
})
.await
.unwrap();
let notifs = store.notifications.lock().unwrap();
assert_eq!(notifs.len(), 1);
assert!(matches!(
notifs[0].notification_type,
NotificationType::Follow
));
}
#[tokio::test]
async fn reply_creates_notification_for_original_author() {
let store = TestStore::default();
let alice = alice();
let bob_id = UserId::new();
let original = Thought::new_local(
ThoughtId::new(),
alice.id.clone(),
Content::new_local("original").unwrap(),
None,
Visibility::Public,
None,
false,
);
store.thoughts.lock().unwrap().push(original.clone());
let svc = NotificationEventService {
thoughts: Arc::new(store.clone()),
notifications: Arc::new(store.clone()),
};
svc.process(&DomainEvent::ThoughtCreated {
thought_id: ThoughtId::new(),
user_id: bob_id,
in_reply_to_id: Some(original.id.clone()),
})
.await
.unwrap();
let notifs = store.notifications.lock().unwrap();
assert_eq!(notifs.len(), 1);
assert!(matches!(
notifs[0].notification_type,
NotificationType::Reply
));
}
#[tokio::test]
async fn self_reply_creates_no_notification() {
let store = TestStore::default();
let alice = alice();
let original = Thought::new_local(
ThoughtId::new(),
alice.id.clone(),
Content::new_local("original").unwrap(),
None,
Visibility::Public,
None,
false,
);
store.thoughts.lock().unwrap().push(original.clone());
let svc = NotificationEventService {
thoughts: Arc::new(store.clone()),
notifications: Arc::new(store.clone()),
};
svc.process(&DomainEvent::ThoughtCreated {
thought_id: ThoughtId::new(),
user_id: alice.id.clone(),
in_reply_to_id: Some(original.id.clone()),
})
.await
.unwrap();
assert!(store.notifications.lock().unwrap().is_empty());
}
#[tokio::test]
async fn self_boost_creates_no_notification() {
let store = TestStore::default();
let alice = alice();
let thought = Thought::new_local(
ThoughtId::new(),
alice.id.clone(),
Content::new_local("hello").unwrap(),
None,
Visibility::Public,
None,
false,
);
store.thoughts.lock().unwrap().push(thought.clone());
let svc = NotificationEventService {
thoughts: Arc::new(store.clone()),
notifications: Arc::new(store.clone()),
};
svc.process(&DomainEvent::BoostAdded {
boost_id: BoostId::new(),
user_id: alice.id.clone(),
thought_id: thought.id.clone(),
})
.await
.unwrap();
assert!(store.notifications.lock().unwrap().is_empty());
}
}