feat(infra): transactional outbox — OutboxWriter port, PgOutboxWriter, OutboxRelay, TestOutbox; update create_thought + delete_thought
This commit is contained in:
@@ -2,7 +2,7 @@ use domain::{
|
||||
errors::DomainError,
|
||||
events::DomainEvent,
|
||||
models::thought::{Thought, Visibility},
|
||||
ports::{EventPublisher, TagRepository, ThoughtRepository, UserReader},
|
||||
ports::{EventPublisher, OutboxWriter, TagRepository, ThoughtRepository, UserReader},
|
||||
value_objects::{Content, ThoughtId, UserId},
|
||||
};
|
||||
|
||||
@@ -53,7 +53,8 @@ pub async fn create_thought(
|
||||
thoughts: &dyn ThoughtRepository,
|
||||
_users: &dyn UserReader,
|
||||
tags: &dyn TagRepository,
|
||||
events: &dyn EventPublisher,
|
||||
_events: &dyn EventPublisher,
|
||||
outbox: &dyn OutboxWriter,
|
||||
input: CreateThoughtInput,
|
||||
) -> Result<CreateThoughtOutput, DomainError> {
|
||||
let content = Content::new_local(input.content)?;
|
||||
@@ -81,8 +82,8 @@ pub async fn create_thought(
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
.publish(&DomainEvent::ThoughtCreated {
|
||||
outbox
|
||||
.append(&DomainEvent::ThoughtCreated {
|
||||
thought_id: thought.id.clone(),
|
||||
user_id: thought.user_id.clone(),
|
||||
in_reply_to_id: input.in_reply_to_id,
|
||||
@@ -93,7 +94,8 @@ pub async fn create_thought(
|
||||
|
||||
pub async fn delete_thought(
|
||||
thoughts: &dyn ThoughtRepository,
|
||||
events: &dyn EventPublisher,
|
||||
_events: &dyn EventPublisher,
|
||||
outbox: &dyn OutboxWriter,
|
||||
id: &ThoughtId,
|
||||
user_id: &UserId,
|
||||
) -> Result<(), DomainError> {
|
||||
@@ -103,8 +105,8 @@ pub async fn delete_thought(
|
||||
.ok_or(DomainError::NotFound)?;
|
||||
require_owner(&thought, user_id)?;
|
||||
thoughts.delete(id, user_id).await?;
|
||||
events
|
||||
.publish(&DomainEvent::ThoughtDeleted {
|
||||
outbox
|
||||
.append(&DomainEvent::ThoughtDeleted {
|
||||
thought_id: id.clone(),
|
||||
user_id: user_id.clone(),
|
||||
})
|
||||
@@ -154,7 +156,7 @@ mod tests {
|
||||
use super::*;
|
||||
use domain::{
|
||||
models::user::User,
|
||||
testing::{NoOpEventPublisher, TestStore},
|
||||
testing::{NoOpEventPublisher, NoOpOutboxWriter, TestOutbox, TestStore},
|
||||
value_objects::*,
|
||||
};
|
||||
|
||||
@@ -179,15 +181,18 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_thought_saves_and_emits_event() {
|
||||
async fn create_thought_saves_and_stages_outbox_event() {
|
||||
let store = TestStore::default();
|
||||
let outbox = TestOutbox::default();
|
||||
let u = user();
|
||||
store.users.lock().unwrap().push(u.clone());
|
||||
let out = create_thought(&store, &store, &store, &store, input(u.id.clone()))
|
||||
let out = create_thought(&store, &store, &store, &NoOpEventPublisher, &outbox, input(u.id.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(out.thought.content.as_str(), "hello");
|
||||
assert_eq!(store.events.lock().unwrap().len(), 1);
|
||||
let staged = outbox.staged();
|
||||
assert_eq!(staged.len(), 1);
|
||||
assert!(matches!(staged[0], DomainEvent::ThoughtCreated { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -200,11 +205,12 @@ mod tests {
|
||||
&store,
|
||||
&store,
|
||||
&NoOpEventPublisher,
|
||||
&NoOpOutboxWriter,
|
||||
input(u.id.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &u.id)
|
||||
delete_thought(&store, &NoOpEventPublisher, &NoOpOutboxWriter, &out.thought.id, &u.id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(store.thoughts.lock().unwrap().is_empty());
|
||||
@@ -230,11 +236,12 @@ mod tests {
|
||||
&store,
|
||||
&store,
|
||||
&NoOpEventPublisher,
|
||||
&NoOpOutboxWriter,
|
||||
input(alice.id.clone()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let err = delete_thought(&store, &NoOpEventPublisher, &out.thought.id, &bob.id)
|
||||
let err = delete_thought(&store, &NoOpEventPublisher, &NoOpOutboxWriter, &out.thought.id, &bob.id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, DomainError::NotFound));
|
||||
@@ -245,7 +252,7 @@ mod tests {
|
||||
let store = TestStore::default();
|
||||
let alice = user();
|
||||
store.users.lock().unwrap().push(alice.clone());
|
||||
let out = create_thought(&store, &store, &store, &store, input(alice.id.clone()))
|
||||
let out = create_thought(&store, &store, &store, &NoOpEventPublisher, &NoOpOutboxWriter, input(alice.id.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let tid = out.thought.id.clone();
|
||||
@@ -280,6 +287,7 @@ mod tests {
|
||||
&store,
|
||||
&store,
|
||||
&NoOpEventPublisher,
|
||||
&NoOpOutboxWriter,
|
||||
input(alice.id.clone()),
|
||||
)
|
||||
.await
|
||||
@@ -291,6 +299,7 @@ mod tests {
|
||||
&store,
|
||||
&store,
|
||||
&NoOpEventPublisher,
|
||||
&NoOpOutboxWriter,
|
||||
CreateThoughtInput {
|
||||
user_id: alice.id.clone(),
|
||||
content: "reply".into(),
|
||||
|
||||
Reference in New Issue
Block a user