feat: atomic goal, watchlist, profile writes

This commit is contained in:
2026-10-26 00:49:28 +01:00
parent c8a1e8bfe4
commit 62ad9f4017
48 changed files with 1296 additions and 264 deletions

View File

@@ -3,8 +3,9 @@ use domain::testing::test_movie_with_year;
use std::sync::Arc;
use domain::{
events::DomainEvent,
ports::MovieCommand,
testing::{InMemoryStore, NoopEventPublisher},
testing::{InMemoryStore, NoopEventPublisher, RefusingWatchlistRepository},
};
use application::watchlist::{add, commands::AddToWatchlistCommand, deps::WatchlistAddDeps};
@@ -108,3 +109,50 @@ async fn adding_the_same_movie_to_a_watchlist_twice_keeps_a_single_entry_and_suc
assert_eq!(movies.watchlist_count(), 1);
}
#[tokio::test]
async fn adding_an_entry_records_the_entry_and_its_event_together() {
let movies = InMemoryStore::new();
let movie = test_movie_with_year("Heat", 1995);
movies.upsert_movie(&movie).await.unwrap();
let deps = make_deps(Arc::clone(&movies), Arc::clone(&movies));
add::execute(
&deps,
add_to_watchlist_command(uuid::Uuid::new_v4(), movie.id().value()),
)
.await
.unwrap();
assert_eq!(movies.watchlist_count(), 1);
let recorded = movies.recorded_events();
assert_eq!(recorded.len(), 1);
assert!(matches!(
recorded[0],
DomainEvent::WatchlistEntryAdded { .. }
));
}
#[tokio::test]
async fn a_refused_add_propagates_and_records_nothing() {
let movies = InMemoryStore::new();
let movie = test_movie_with_year("Heat", 1995);
movies.upsert_movie(&movie).await.unwrap();
let deps = WatchlistAddDeps {
watchlist: Arc::new(RefusingWatchlistRepository),
..make_deps(Arc::clone(&movies), Arc::clone(&movies))
};
let result = add::execute(
&deps,
add_to_watchlist_command(uuid::Uuid::new_v4(), movie.id().value()),
)
.await;
assert!(matches!(
result,
Err(domain::errors::DomainError::InfrastructureError(m)) if m == "refused"
));
assert_eq!(movies.watchlist_count(), 0);
assert!(movies.recorded_events().is_empty());
}

View File

@@ -15,10 +15,10 @@ async fn returns_true_when_present() {
let uid = Uuid::new_v4();
let mid = Uuid::new_v4();
watchlist
.add(&WatchlistEntry::new(
UserId::from_uuid(uid),
MovieId::from_uuid(mid),
))
.add(
&WatchlistEntry::new(UserId::from_uuid(uid), MovieId::from_uuid(mid)),
&[],
)
.await
.unwrap();

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use domain::events::DomainEvent;
use domain::models::WatchlistEntry;
use domain::ports::WatchlistRepository;
use domain::testing::{InMemoryStore, NoopEventPublisher};
use domain::testing::{InMemoryStore, RefusingWatchlistRepository};
use domain::value_objects::{MovieId, UserId};
use uuid::Uuid;
@@ -13,22 +13,20 @@ use application::watchlist::{
};
#[tokio::test]
async fn removes_entry_and_emits_event() {
async fn removing_an_entry_records_the_removal_and_its_event_together() {
let watchlist = InMemoryStore::new();
let events = NoopEventPublisher::new();
let uid = Uuid::new_v4();
let mid = Uuid::new_v4();
watchlist
.add(&WatchlistEntry::new(
UserId::from_uuid(uid),
MovieId::from_uuid(mid),
))
.add(
&WatchlistEntry::new(UserId::from_uuid(uid), MovieId::from_uuid(mid)),
&[],
)
.await
.unwrap();
let deps = RemoveFromWatchlistDeps {
watchlist: Arc::clone(&watchlist) as _,
event_publisher: Arc::clone(&events) as _,
};
remove::execute(
&deps,
@@ -41,12 +39,12 @@ async fn removes_entry_and_emits_event() {
.unwrap();
assert_eq!(watchlist.watchlist_count(), 0);
let published = events.published();
assert!(
published
.iter()
.any(|e| matches!(e, DomainEvent::WatchlistEntryRemoved { .. }))
);
let recorded = watchlist.recorded_events();
assert_eq!(recorded.len(), 1);
assert!(matches!(
recorded[0],
DomainEvent::WatchlistEntryRemoved { .. }
));
}
#[tokio::test]
@@ -54,7 +52,6 @@ async fn fails_when_not_on_watchlist() {
let b = TestContextBuilder::new();
let deps = RemoveFromWatchlistDeps {
watchlist: b.watchlist_repo.clone(),
event_publisher: b.event_publisher.clone(),
};
let result = remove::execute(
&deps,
@@ -67,3 +64,44 @@ async fn fails_when_not_on_watchlist() {
assert!(result.is_err());
}
#[tokio::test]
async fn a_failed_removal_records_no_event() {
let store = InMemoryStore::new();
let deps = RemoveFromWatchlistDeps {
watchlist: Arc::clone(&store) as _,
};
let result = remove::execute(
&deps,
RemoveFromWatchlistCommand {
user_id: Uuid::new_v4(),
movie_id: Uuid::new_v4(),
},
)
.await;
assert!(result.is_err());
assert!(store.recorded_events().is_empty());
}
#[tokio::test]
async fn a_refused_remove_propagates_the_refusal() {
let deps = RemoveFromWatchlistDeps {
watchlist: Arc::new(RefusingWatchlistRepository),
};
let result = remove::execute(
&deps,
RemoveFromWatchlistCommand {
user_id: Uuid::new_v4(),
movie_id: Uuid::new_v4(),
},
)
.await;
assert!(matches!(
result,
Err(domain::errors::DomainError::InfrastructureError(m)) if m == "refused"
));
}