#![cfg(feature = "json")]
#![allow(clippy::unwrap_used, reason = "tests")]
#![allow(clippy::expect_used, reason = "tests")]
#![allow(clippy::panic, reason = "tests")]
#![allow(clippy::doc_markdown, reason = "test code: prose doc comments")]
#![allow(
clippy::type_complexity,
reason = "test code: explicit synthetic-stream item types"
)]
use futures::{StreamExt, stream};
use mnesis::{DomainEvent, Message, Version};
use mnesis_inmemory::InMemoryStore;
use mnesis_store::PendingBatch;
use mnesis_store::store::RawEventStore;
use mnesis_store::{
DecodeStreamError, Decoded, Encode, JsonCodec, PersistedEnvelope, Step, StepStreamExt, Store,
StreamKey, pending_envelope,
};
use proptest::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
enum Money {
Deposited { amount: u64 },
}
impl Message for Money {}
impl DomainEvent for Money {
fn name(&self) -> &'static str {
match self {
Self::Deposited { .. } => "Deposited",
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error("synthetic read fault")]
struct SynthErr;
async fn persisted(version: u64, amount: u64) -> PersistedEnvelope {
let store = Store::new(InMemoryStore::new());
let id = StreamKey::from_slice(b"s");
let mut expected = None;
for v in 1..version {
store
.append(
&id,
expected,
PendingBatch::new(&[money_env(v, 0)]).expect("non-empty batch"),
)
.await
.unwrap();
expected = Version::new(v);
}
store
.append(
&id,
expected,
PendingBatch::new(&[money_env(version, amount)]).expect("non-empty batch"),
)
.await
.unwrap();
let raw = store
.read_stream(&id, Version::new(version).unwrap())
.await
.unwrap();
let mut cursor = std::pin::pin!(raw);
cursor.next().await.expect("one event").expect("ok")
}
async fn corrupt_persisted() -> PersistedEnvelope {
let store = Store::new(InMemoryStore::new());
let id = StreamKey::from_slice(b"s");
let bad = pending_envelope(Version::INITIAL)
.event_type("Deposited")
.payload(b"not json".to_vec())
.build()
.unwrap();
store
.append(&id, None, PendingBatch::of(&bad))
.await
.unwrap();
let raw = store.read_stream(&id, Version::INITIAL).await.unwrap();
let mut cursor = std::pin::pin!(raw);
cursor.next().await.expect("one event").expect("ok")
}
fn money_env(version: u64, amount: u64) -> mnesis_store::PendingEnvelope {
let bytes = JsonCodec::default()
.encode(&Money::Deposited { amount })
.unwrap();
pending_envelope(Version::new(version).unwrap())
.event_type("Deposited")
.payload(bytes)
.build()
.unwrap()
}
fn synth<I>(items: Vec<Result<I, SynthErr>>) -> impl futures::Stream<Item = Result<I, SynthErr>> {
stream::iter(items)
}
#[tokio::test]
async fn events_drops_caughtup_and_unwraps_events_in_order() {
let (p1, p2, p3) = (
persisted(1, 1).await,
persisted(2, 2).await,
persisted(3, 3).await,
);
let items = vec![
Ok(Step::Event(p1)),
Ok(Step::Event(p2)),
Ok(Step::CaughtUp),
Ok(Step::Event(p3)),
];
let out: Vec<_> = synth(items)
.events()
.map(|r| r.unwrap().version().as_u64())
.collect()
.await;
assert_eq!(
out,
vec![1, 2, 3],
"CaughtUp removed, events unwrapped in order"
);
}
#[tokio::test]
async fn events_on_a_caughtup_only_stream_is_empty() {
let items: Vec<Result<Step<PersistedEnvelope>, SynthErr>> =
vec![Ok(Step::CaughtUp), Ok(Step::CaughtUp)];
let out: Vec<_> = synth(items).events().collect().await;
assert!(out.is_empty(), "a stream of only markers yields no events");
}
#[tokio::test]
async fn events_drops_multiple_interleaved_caughtup_markers() {
let (p1, p2) = (persisted(1, 1).await, persisted(2, 2).await);
let items = vec![
Ok(Step::CaughtUp),
Ok(Step::Event(p1)),
Ok(Step::CaughtUp),
Ok(Step::Event(p2)),
Ok(Step::CaughtUp),
];
let out: Vec<_> = synth(items)
.events()
.map(|r| r.unwrap().version().as_u64())
.collect()
.await;
assert_eq!(
out,
vec![1, 2],
"every marker dropped regardless of position/count"
);
}
#[tokio::test]
async fn events_passes_error_items_through_in_place() {
let (p1, p2) = (persisted(1, 1).await, persisted(2, 2).await);
let items = vec![Ok(Step::Event(p1)), Err(SynthErr), Ok(Step::Event(p2))];
let out: Vec<_> = synth(items).events().collect().await;
assert_eq!(out.len(), 3);
assert!(out[0].is_ok());
assert_eq!(
out[1].as_ref().unwrap_err(),
&SynthErr,
"error preserved, not swallowed"
);
assert!(out[2].is_ok());
}
#[tokio::test]
async fn events_preserves_the_position_and_stream_key_on_all_style_items() {
let p1 = persisted(1, 1).await;
let items: Vec<Result<Step<(u64, StreamKey, PersistedEnvelope)>, SynthErr>> = vec![
Ok(Step::CaughtUp),
Ok(Step::Event((77u64, StreamKey::from_slice(b"s"), p1))),
];
let out: Vec<_> = synth(items).events().collect().await;
assert_eq!(out.len(), 1);
let (pos, key, env) = out[0].as_ref().unwrap();
assert_eq!(*pos, 77, "position tag rides through .events()");
assert_eq!(key.as_bytes(), b"s", "stream key rides through .events()");
assert_eq!(env.version().as_u64(), 1);
}
#[tokio::test]
async fn decoded_preserves_caughtup_and_decodes_each_event() {
let (p1, p2) = (persisted(1, 10).await, persisted(2, 20).await);
let items = vec![Ok(Step::Event(p1)), Ok(Step::CaughtUp), Ok(Step::Event(p2))];
let out: Vec<Result<Step<Decoded<Money>>, DecodeStreamError<SynthErr, _>>> = synth(items)
.decoded::<Money, _>(JsonCodec::default())
.collect()
.await;
assert_eq!(out.len(), 3);
match out[0].as_ref().unwrap() {
Step::Event(d) => {
assert_eq!(d.event, Money::Deposited { amount: 10 });
assert_eq!(d.version, Version::new(1).unwrap());
}
Step::CaughtUp => panic!("first item must be the decoded event"),
}
assert!(
out[1].as_ref().unwrap().is_caught_up(),
"the marker is preserved in place"
);
match out[2].as_ref().unwrap() {
Step::Event(d) => assert_eq!(d.event, Money::Deposited { amount: 20 }),
Step::CaughtUp => panic!("third item must be the decoded event"),
}
}
#[tokio::test]
async fn decoded_surfaces_a_read_error_as_the_read_variant() {
let items: Vec<Result<Step<PersistedEnvelope>, SynthErr>> = vec![Err(SynthErr)];
let out: Vec<_> = synth(items)
.decoded::<Money, _>(JsonCodec::default())
.collect()
.await;
assert_eq!(out.len(), 1);
assert!(
matches!(out[0], Err(DecodeStreamError::Read(SynthErr))),
"an upstream read fault is the Read domain, got {:?}",
out[0]
);
}
#[tokio::test]
async fn decoded_surfaces_a_bad_payload_as_the_decode_variant() {
let bad = corrupt_persisted().await;
let items: Vec<Result<Step<PersistedEnvelope>, SynthErr>> = vec![Ok(Step::Event(bad))];
let out: Vec<_> = synth(items)
.decoded::<Money, _>(JsonCodec::default())
.collect()
.await;
assert_eq!(out.len(), 1);
assert!(
matches!(out[0], Err(DecodeStreamError::Decode(_))),
"an un-decodable payload is the Decode domain, got {:?}",
out[0]
);
}
#[tokio::test]
async fn decoded_on_all_style_items_keeps_tag_and_phase() {
let p1 = persisted(1, 42).await;
let items: Vec<Result<Step<(u64, StreamKey, PersistedEnvelope)>, SynthErr>> = vec![
Ok(Step::Event((5u64, StreamKey::from_slice(b"s"), p1))),
Ok(Step::CaughtUp),
];
let out: Vec<Result<Step<(u64, StreamKey, Decoded<Money>)>, DecodeStreamError<SynthErr, _>>> =
synth(items)
.decoded::<Money, _>(JsonCodec::default())
.collect()
.await;
match out[0].as_ref().unwrap() {
Step::Event((pos, key, d)) => {
assert_eq!(*pos, 5, "tag preserved beside the decoded box");
assert_eq!(
key.as_bytes(),
b"s",
"stream key preserved beside the decoded box"
);
assert_eq!(d.event, Money::Deposited { amount: 42 });
}
Step::CaughtUp => panic!("first item is the tagged decoded event"),
}
assert!(out[1].as_ref().unwrap().is_caught_up());
}
proptest! {
#[test]
fn prop_events_yields_exactly_the_events_in_order(
flags in prop::collection::vec(any::<bool>(), 0..40)
) {
let rt = tokio::runtime::Runtime::new().unwrap();
let env = rt.block_on(persisted(1, 1));
let expected_events = flags.iter().filter(|&&b| b).count();
let items: Vec<Result<Step<PersistedEnvelope>, SynthErr>> = flags
.iter()
.map(|&is_event| Ok(if is_event { Step::Event(env.clone()) } else { Step::CaughtUp }))
.collect();
let out = rt.block_on(synth(items).events().collect::<Vec<_>>());
prop_assert_eq!(out.len(), expected_events, "one output per Event, markers dropped");
prop_assert!(out.iter().all(std::result::Result::is_ok), "no errors introduced");
}
}