feat(infra): transactional outbox — OutboxWriter port, PgOutboxWriter, OutboxRelay, TestOutbox; update create_thought + delete_thought

This commit is contained in:
2026-05-15 18:31:57 +02:00
parent 15b1d0fdb7
commit 6024a65060
16 changed files with 245 additions and 20 deletions

View File

@@ -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(),