pub mod test;
use std::future::Future;
use async_trait::async_trait;
use crate::message;
pub type Envelope<T> = message::Envelope<T>;
#[async_trait]
pub trait Handler<T>: Send + Sync
where
T: message::Message,
{
type Error: Send + Sync;
async fn handle(&self, command: Envelope<T>) -> Result<(), Self::Error>;
}
#[async_trait]
impl<T, Err, F, Fut> Handler<T> for F
where
T: message::Message + Send + Sync + 'static,
Err: Send + Sync,
F: Send + Sync + Fn(Envelope<T>) -> Fut,
Fut: Send + Sync + Future<Output = Result<(), Err>>,
{
type Error = Err;
async fn handle(&self, command: Envelope<T>) -> Result<(), Self::Error> {
self(command).await
}
}
#[cfg(test)]
mod test_user_domain {
use std::sync::Arc;
use async_trait::async_trait;
use crate::aggregate::test_user_domain::{User, UserEvent};
use crate::{aggregate, command, event, message};
struct UserService(Arc<dyn aggregate::Repository<User>>);
impl<R> From<R> for UserService
where
R: aggregate::Repository<User> + 'static,
{
fn from(repository: R) -> Self {
Self(Arc::new(repository))
}
}
struct CreateUser {
email: String,
password: String,
}
impl message::Message for CreateUser {
fn name(&self) -> &'static str {
"CreateUser"
}
}
#[async_trait]
impl command::Handler<CreateUser> for UserService {
type Error = anyhow::Error;
async fn handle(&self, command: command::Envelope<CreateUser>) -> Result<(), Self::Error> {
let command = command.message;
let mut user = aggregate::Root::<User>::create(command.email, command.password)?;
self.0.save(&mut user).await?;
Ok(())
}
}
struct ChangeUserPassword {
email: String,
password: String,
}
impl message::Message for ChangeUserPassword {
fn name(&self) -> &'static str {
"ChangeUserPassword"
}
}
#[async_trait]
impl command::Handler<ChangeUserPassword> for UserService {
type Error = anyhow::Error;
async fn handle(
&self,
command: command::Envelope<ChangeUserPassword>,
) -> Result<(), Self::Error> {
let command = command.message;
let mut user = self.0.get(&command.email).await?;
user.change_password(command.password)?;
self.0.save(&mut user).await?;
Ok(())
}
}
#[tokio::test]
async fn it_creates_a_new_user_successfully() {
command::test::Scenario
.when(command::Envelope::from(CreateUser {
email: "test@test.com".to_owned(),
password: "not-a-secret".to_owned(),
}))
.then(vec![event::Persisted {
stream_id: "test@test.com".to_owned(),
version: 1,
event: event::Envelope::from(UserEvent::WasCreated {
email: "test@test.com".to_owned(),
password: "not-a-secret".to_owned(),
}),
}])
.assert_on(|event_store| {
UserService::from(aggregate::EventSourcedRepository::from(event_store))
})
.await;
}
#[tokio::test]
async fn it_fails_to_create_an_user_if_it_still_exists() {
command::test::Scenario
.given(vec![event::Persisted {
stream_id: "test@test.com".to_owned(),
version: 1,
event: event::Envelope::from(UserEvent::WasCreated {
email: "test@test.com".to_owned(),
password: "not-a-secret".to_owned(),
}),
}])
.when(command::Envelope::from(CreateUser {
email: "test@test.com".to_owned(),
password: "not-a-secret".to_owned(),
}))
.then_fails()
.assert_on(|event_store| {
UserService::from(aggregate::EventSourcedRepository::from(event_store))
})
.await;
}
#[tokio::test]
async fn it_updates_the_password_of_an_existing_user() {
command::test::Scenario
.given(vec![event::Persisted {
stream_id: "test@test.com".to_owned(),
version: 1,
event: event::Envelope::from(UserEvent::WasCreated {
email: "test@test.com".to_owned(),
password: "not-a-secret".to_owned(),
}),
}])
.when(command::Envelope::from(ChangeUserPassword {
email: "test@test.com".to_owned(),
password: "new-password".to_owned(),
}))
.then(vec![event::Persisted {
stream_id: "test@test.com".to_owned(),
version: 2,
event: event::Envelope::from(UserEvent::PasswordWasChanged {
password: "new-password".to_owned(),
}),
}])
.assert_on(|event_store| {
UserService::from(aggregate::EventSourcedRepository::from(event_store))
})
.await;
}
#[tokio::test]
async fn it_fails_to_update_the_password_if_the_user_does_not_exist() {
command::test::Scenario
.when(command::Envelope::from(ChangeUserPassword {
email: "test@test.com".to_owned(),
password: "new-password".to_owned(),
}))
.then_fails()
.assert_on(|event_store| {
UserService::from(aggregate::EventSourcedRepository::from(event_store))
})
.await;
}
}