feat: atomic review writes with events
This commit is contained in:
@@ -4,20 +4,14 @@ use std::sync::Arc;
|
||||
use domain::{
|
||||
models::ReviewEdit,
|
||||
ports::ReviewRepository,
|
||||
testing::{InMemoryStore, NoopEventPublisher, test_review},
|
||||
testing::{InMemoryStore, test_review},
|
||||
value_objects::{Comment, MovieId, ReviewId, UserId, WatchMedium},
|
||||
};
|
||||
|
||||
use application::diary::{commands::EditReviewCommand, deps::EditReviewDeps, edit_review};
|
||||
|
||||
async fn setup() -> (
|
||||
Arc<InMemoryStore>,
|
||||
Arc<NoopEventPublisher>,
|
||||
ReviewId,
|
||||
UserId,
|
||||
) {
|
||||
async fn setup() -> (Arc<InMemoryStore>, ReviewId, UserId) {
|
||||
let reviews = InMemoryStore::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
let user_id = UserId::generate();
|
||||
let review = test_review(&MovieId::generate(), &user_id, 3).apply_edit(ReviewEdit {
|
||||
comment: Some(Some(Comment::new("original comment".into()).unwrap())),
|
||||
@@ -25,23 +19,22 @@ async fn setup() -> (
|
||||
..Default::default()
|
||||
});
|
||||
let review_id = review.id().clone();
|
||||
reviews.save_review(&review).await.unwrap();
|
||||
(reviews, events, review_id, user_id)
|
||||
reviews.save_review(&review, &[]).await.unwrap();
|
||||
(reviews, review_id, user_id)
|
||||
}
|
||||
|
||||
fn deps(reviews: &Arc<InMemoryStore>, events: &Arc<NoopEventPublisher>) -> EditReviewDeps {
|
||||
fn deps(reviews: &Arc<InMemoryStore>) -> EditReviewDeps {
|
||||
EditReviewDeps {
|
||||
review: Arc::clone(reviews) as _,
|
||||
event_publisher: Arc::clone(events) as _,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_own_review_updates_rating() {
|
||||
let (reviews, events, review_id, user_id) = setup().await;
|
||||
let (reviews, review_id, user_id) = setup().await;
|
||||
|
||||
edit_review::execute(
|
||||
&deps(&reviews, &events),
|
||||
&deps(&reviews),
|
||||
EditReviewCommand {
|
||||
review_id: review_id.value(),
|
||||
requesting_user_id: user_id.value(),
|
||||
@@ -60,9 +53,8 @@ async fn edit_own_review_updates_rating() {
|
||||
#[tokio::test]
|
||||
async fn edit_nonexistent_review_returns_not_found() {
|
||||
let reviews = InMemoryStore::new();
|
||||
let events = NoopEventPublisher::new();
|
||||
|
||||
let err = edit_review::execute(&deps(&reviews, &events), edit_review_command(5))
|
||||
let err = edit_review::execute(&deps(&reviews), edit_review_command(5))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
@@ -71,10 +63,10 @@ async fn edit_nonexistent_review_returns_not_found() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_other_users_review_returns_forbidden() {
|
||||
let (reviews, events, review_id, _user_id) = setup().await;
|
||||
let (reviews, review_id, _user_id) = setup().await;
|
||||
|
||||
let err = edit_review::execute(
|
||||
&deps(&reviews, &events),
|
||||
&deps(&reviews),
|
||||
EditReviewCommand {
|
||||
review_id: review_id.value(),
|
||||
..edit_review_command(5)
|
||||
@@ -86,12 +78,59 @@ async fn edit_other_users_review_returns_forbidden() {
|
||||
assert!(matches!(err, domain::errors::DomainError::Forbidden(_)));
|
||||
}
|
||||
|
||||
/// Loads from the store but refuses every write.
|
||||
pub(crate) struct WriteRefusingReviews(pub(crate) Arc<InMemoryStore>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ReviewRepository for WriteRefusingReviews {
|
||||
async fn save_review(
|
||||
&self,
|
||||
_: &domain::models::Review,
|
||||
_: &[domain::events::DomainEvent],
|
||||
) -> Result<(), domain::errors::DomainError> {
|
||||
Err(domain::errors::DomainError::InfrastructureError(
|
||||
"refused".into(),
|
||||
))
|
||||
}
|
||||
async fn get_review_by_id(
|
||||
&self,
|
||||
id: &ReviewId,
|
||||
) -> Result<Option<domain::models::Review>, domain::errors::DomainError> {
|
||||
self.0.get_review_by_id(id).await
|
||||
}
|
||||
async fn update_review(
|
||||
&self,
|
||||
_: &domain::models::Review,
|
||||
_: &[domain::events::DomainEvent],
|
||||
) -> Result<(), domain::errors::DomainError> {
|
||||
Err(domain::errors::DomainError::InfrastructureError(
|
||||
"refused".into(),
|
||||
))
|
||||
}
|
||||
async fn delete_review(
|
||||
&self,
|
||||
_: &ReviewId,
|
||||
_: &[domain::events::DomainEvent],
|
||||
) -> Result<(), domain::errors::DomainError> {
|
||||
Err(domain::errors::DomainError::InfrastructureError(
|
||||
"refused".into(),
|
||||
))
|
||||
}
|
||||
async fn get_all_reviews_for_user(
|
||||
&self,
|
||||
id: &UserId,
|
||||
) -> Result<Vec<domain::models::Review>, domain::errors::DomainError> {
|
||||
self.0.get_all_reviews_for_user(id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_publishes_review_updated_event() {
|
||||
let (reviews, events, review_id, user_id) = setup().await;
|
||||
async fn editing_a_review_saves_the_change_and_its_event_together() {
|
||||
let (reviews, review_id, user_id) = setup().await;
|
||||
assert!(reviews.recorded_events().is_empty());
|
||||
|
||||
edit_review::execute(
|
||||
&deps(&reviews, &events),
|
||||
&deps(&reviews),
|
||||
EditReviewCommand {
|
||||
review_id: review_id.value(),
|
||||
requesting_user_id: user_id.value(),
|
||||
@@ -101,20 +140,45 @@ async fn edit_publishes_review_updated_event() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let published = events.published();
|
||||
assert_eq!(published.len(), 1);
|
||||
let updated = reviews.get_review_by_id(&review_id).await.unwrap().unwrap();
|
||||
assert_eq!(updated.rating().value(), 1);
|
||||
let recorded = reviews.recorded_events();
|
||||
assert_eq!(recorded.len(), 1);
|
||||
assert!(matches!(
|
||||
published[0],
|
||||
recorded[0],
|
||||
domain::events::DomainEvent::ReviewUpdated { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_refused_edit_records_neither_the_change_nor_its_event() {
|
||||
let (reviews, review_id, user_id) = setup().await;
|
||||
let deps = EditReviewDeps {
|
||||
review: Arc::new(WriteRefusingReviews(Arc::clone(&reviews))),
|
||||
};
|
||||
|
||||
let result = edit_review::execute(
|
||||
&deps,
|
||||
EditReviewCommand {
|
||||
review_id: review_id.value(),
|
||||
requesting_user_id: user_id.value(),
|
||||
..edit_review_command(1)
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
let unchanged = reviews.get_review_by_id(&review_id).await.unwrap().unwrap();
|
||||
assert_eq!(unchanged.rating().value(), 3);
|
||||
assert!(reviews.recorded_events().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_sets_watch_medium() {
|
||||
let (reviews, events, review_id, user_id) = setup().await;
|
||||
let (reviews, review_id, user_id) = setup().await;
|
||||
|
||||
edit_review::execute(
|
||||
&deps(&reviews, &events),
|
||||
&deps(&reviews),
|
||||
EditReviewCommand {
|
||||
rating: None,
|
||||
review_id: review_id.value(),
|
||||
@@ -132,10 +196,9 @@ async fn edit_sets_watch_medium() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn editing_a_review_fails_when_the_store_refuses_to_load_it() {
|
||||
let (_, events, review_id, user_id) = setup().await;
|
||||
let (_, review_id, user_id) = setup().await;
|
||||
let deps = EditReviewDeps {
|
||||
review: Arc::new(domain::testing::RefusingReviewRepository),
|
||||
event_publisher: Arc::clone(&events) as _,
|
||||
};
|
||||
|
||||
let result = edit_review::execute(
|
||||
|
||||
Reference in New Issue
Block a user