feat: UserUnblocked + UserRegistered events, fix unblock_user and register signatures

This commit is contained in:
2026-05-14 11:06:36 +02:00
parent f0b87311e3
commit 1127a5946f
5 changed files with 61 additions and 3 deletions

View File

@@ -61,6 +61,13 @@ pub enum EventPayload {
blocker_id: String, blocker_id: String,
blocked_id: String, blocked_id: String,
}, },
UserUnblocked {
blocker_id: String,
blocked_id: String,
},
UserRegistered {
user_id: String,
},
} }
impl EventPayload { impl EventPayload {
@@ -79,6 +86,8 @@ impl EventPayload {
Self::FollowRejected { .. } => "follows.rejected", Self::FollowRejected { .. } => "follows.rejected",
Self::Unfollowed { .. } => "follows.removed", Self::Unfollowed { .. } => "follows.removed",
Self::UserBlocked { .. } => "users.blocked", Self::UserBlocked { .. } => "users.blocked",
Self::UserUnblocked { .. } => "users.unblocked",
Self::UserRegistered { .. } => "users.registered",
} }
} }
} }
@@ -126,6 +135,12 @@ impl From<&DomainEvent> for EventPayload {
DomainEvent::UserBlocked { blocker_id, blocked_id } => Self::UserBlocked { DomainEvent::UserBlocked { blocker_id, blocked_id } => Self::UserBlocked {
blocker_id: blocker_id.to_string(), blocked_id: blocked_id.to_string(), blocker_id: blocker_id.to_string(), blocked_id: blocked_id.to_string(),
}, },
DomainEvent::UserUnblocked { blocker_id, blocked_id } => Self::UserUnblocked {
blocker_id: blocker_id.to_string(), blocked_id: blocked_id.to_string(),
},
DomainEvent::UserRegistered { user_id } => Self::UserRegistered {
user_id: user_id.to_string(),
},
} }
} }
} }
@@ -195,6 +210,13 @@ impl TryFrom<EventPayload> for DomainEvent {
blocker_id: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?), blocker_id: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?),
blocked_id: UserId::from_uuid(parse_uuid(&blocked_id, "blocked_id")?), blocked_id: UserId::from_uuid(parse_uuid(&blocked_id, "blocked_id")?),
}, },
EventPayload::UserUnblocked { blocker_id, blocked_id } => DomainEvent::UserUnblocked {
blocker_id: UserId::from_uuid(parse_uuid(&blocker_id, "blocker_id")?),
blocked_id: UserId::from_uuid(parse_uuid(&blocked_id, "blocked_id")?),
},
EventPayload::UserRegistered { user_id } => DomainEvent::UserRegistered {
user_id: UserId::from_uuid(parse_uuid(&user_id, "user_id")?),
},
}) })
} }
} }

View File

@@ -1,5 +1,6 @@
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
events::DomainEvent,
models::user::User, models::user::User,
ports::{AuthService, EventPublisher, PasswordHasher, UserRepository}, ports::{AuthService, EventPublisher, PasswordHasher, UserRepository},
value_objects::{Email, UserId, Username}, value_objects::{Email, UserId, Username},
@@ -13,7 +14,7 @@ pub async fn register(
users: &dyn UserRepository, users: &dyn UserRepository,
hasher: &dyn PasswordHasher, hasher: &dyn PasswordHasher,
auth: &dyn AuthService, auth: &dyn AuthService,
_events: &dyn EventPublisher, events: &dyn EventPublisher,
input: RegisterInput, input: RegisterInput,
) -> Result<RegisterOutput, DomainError> { ) -> Result<RegisterOutput, DomainError> {
let username = Username::new(input.username)?; let username = Username::new(input.username)?;
@@ -27,6 +28,7 @@ pub async fn register(
let hash = hasher.hash(&input.password).await?; let hash = hasher.hash(&input.password).await?;
let user = User::new_local(UserId::new(), username, email, hash); let user = User::new_local(UserId::new(), username, email, hash);
users.save(&user).await?; users.save(&user).await?;
events.publish(&DomainEvent::UserRegistered { user_id: user.id.clone() }).await?;
let token = auth.generate_token(&user.id)?; let token = auth.generate_token(&user.id)?;
Ok(RegisterOutput { user, token: token.token }) Ok(RegisterOutput { user, token: token.token })
} }
@@ -56,6 +58,7 @@ mod tests {
use async_trait::async_trait; use async_trait::async_trait;
use domain::{ use domain::{
errors::DomainError, errors::DomainError,
events::DomainEvent,
ports::{AuthService, GeneratedToken, PasswordHasher}, ports::{AuthService, GeneratedToken, PasswordHasher},
testing::{NoOpEventPublisher, TestStore}, testing::{NoOpEventPublisher, TestStore},
value_objects::{PasswordHash, UserId}, value_objects::{PasswordHash, UserId},
@@ -112,4 +115,13 @@ mod tests {
let err = login(&store, &FakeHasher, &FakeAuth, LoginInput { email: "alice@ex.com".into(), password: "wrong".into() }).await.unwrap_err(); let err = login(&store, &FakeHasher, &FakeAuth, LoginInput { email: "alice@ex.com".into(), password: "wrong".into() }).await.unwrap_err();
assert!(matches!(err, DomainError::Unauthorized)); assert!(matches!(err, DomainError::Unauthorized));
} }
#[tokio::test]
async fn register_publishes_user_registered_event() {
let store = TestStore::default();
register(&store, &FakeHasher, &FakeAuth, &store, input()).await.unwrap();
let events = store.events.lock().unwrap();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], DomainEvent::UserRegistered { .. }));
}
} }

View File

@@ -67,8 +67,17 @@ pub async fn block_user(blocks: &dyn BlockRepository, events: &dyn EventPublishe
Ok(()) Ok(())
} }
pub async fn unblock_user(blocks: &dyn BlockRepository, blocker_id: &UserId, blocked_id: &UserId) -> Result<(), DomainError> { pub async fn unblock_user(
blocks: &dyn BlockRepository,
events: &dyn EventPublisher,
blocker_id: &UserId,
blocked_id: &UserId,
) -> Result<(), DomainError> {
blocks.delete(blocker_id, blocked_id).await?; blocks.delete(blocker_id, blocked_id).await?;
events.publish(&DomainEvent::UserUnblocked {
blocker_id: blocker_id.clone(),
blocked_id: blocked_id.clone(),
}).await?;
Ok(()) Ok(())
} }
@@ -114,4 +123,17 @@ mod tests {
let err = follow_user(&store, &store, &alice.id, &alice.id).await.unwrap_err(); let err = follow_user(&store, &store, &alice.id, &alice.id).await.unwrap_err();
assert!(matches!(err, DomainError::InvalidInput(_))); assert!(matches!(err, DomainError::InvalidInput(_)));
} }
#[tokio::test]
async fn unblock_user_publishes_event() {
let store = TestStore::default();
let alice = user("alice");
let bob = user("bob");
block_user(&store, &store, &alice.id, &bob.id).await.unwrap();
store.events.lock().unwrap().clear();
unblock_user(&store, &store, &alice.id, &bob.id).await.unwrap();
let events = store.events.lock().unwrap();
assert_eq!(events.len(), 1);
assert!(matches!(events[0], DomainEvent::UserUnblocked { .. }));
}
} }

View File

@@ -14,6 +14,8 @@ pub enum DomainEvent {
FollowRejected { follower_id: UserId, following_id: UserId }, FollowRejected { follower_id: UserId, following_id: UserId },
Unfollowed { follower_id: UserId, following_id: UserId }, Unfollowed { follower_id: UserId, following_id: UserId },
UserBlocked { blocker_id: UserId, blocked_id: UserId }, UserBlocked { blocker_id: UserId, blocked_id: UserId },
UserUnblocked { blocker_id: UserId, blocked_id: UserId },
UserRegistered { user_id: UserId },
} }
pub struct EventEnvelope { pub struct EventEnvelope {

View File

@@ -35,7 +35,7 @@ pub async fn post_block(State(s): State<AppState>, AuthUser(uid): AuthUser, Path
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
pub async fn delete_block(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(target): Path<Uuid>) -> Result<StatusCode, ApiError> { pub async fn delete_block(State(s): State<AppState>, AuthUser(uid): AuthUser, Path(target): Path<Uuid>) -> Result<StatusCode, ApiError> {
unblock_user(&*s.blocks, &uid, &UserId::from_uuid(target)).await?; unblock_user(&*s.blocks, &*s.events, &uid, &UserId::from_uuid(target)).await?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
pub async fn put_top_friends(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<SetTopFriendsRequest>) -> Result<StatusCode, ApiError> { pub async fn put_top_friends(State(s): State<AppState>, AuthUser(uid): AuthUser, Json(body): Json<SetTopFriendsRequest>) -> Result<StatusCode, ApiError> {