use super::violated;
use crate::event_store::{EventRecord, EventStore};
const KIND: &str = "EventStore";
pub async fn event_store_contract<S, F>(make: F)
where
S: EventStore,
F: Fn() -> S,
{
replay_is_strictly_after_and_ordered(&make()).await;
payloads_are_preserved(&make()).await;
replay_does_not_consume(&make()).await;
unknown_sessions_replay_empty(&make()).await;
sessions_are_isolated(&make()).await;
purge_removes_the_session_log(&make()).await;
}
async fn append<S: EventStore>(store: &S, session: &str, id: u64, rule: &str) {
ok!(
rule,
store
.append(session, EventRecord::new(id, format!("event-{id}")))
.await
);
}
async fn replay<S: EventStore>(
store: &S,
session: &str,
after: u64,
rule: &str,
) -> Vec<EventRecord> {
ok!(rule, store.replay_after(session, after).await)
}
fn ids(events: &[EventRecord]) -> Vec<u64> {
events.iter().map(|event| event.id).collect()
}
async fn replay_is_strictly_after_and_ordered<S: EventStore>(store: &S) {
let rule = "replay_after returns events with ids strictly greater than after_id, in order";
for id in 1..=25 {
append(store, "s", id, rule).await;
}
let all = replay(store, "s", 0, rule).await;
check!(
ids(&all) == (1..=25).collect::<Vec<_>>(),
rule,
"replay_after(0) returned ids {:?}",
ids(&all)
);
for after in [1, 9, 10, 24] {
let tail = replay(store, "s", after, rule).await;
check!(
ids(&tail) == (after + 1..=25).collect::<Vec<_>>(),
rule,
"replay_after({after}) returned ids {:?}",
ids(&tail)
);
}
let none = replay(store, "s", 25, rule).await;
check!(
none.is_empty(),
rule,
"replay_after(25), the last id, returned {:?}",
ids(&none)
);
let none = replay(store, "s", u64::MAX, rule).await;
check!(
none.is_empty(),
rule,
"replay_after(u64::MAX) returned {:?}",
ids(&none)
);
}
async fn payloads_are_preserved<S: EventStore>(store: &S) {
let rule = "an appended event is replayed with its id and data unchanged";
let payloads = [
r#"{"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info"}}"#,
"two\nlines\r\nand a trailing newline\n",
"unicode: caf\u{e9} \u{1F980} \u{4e2d}\u{6587}",
"",
];
for (offset, data) in payloads.iter().enumerate() {
let id = offset as u64 + 1;
ok!(rule, store.append("s", EventRecord::new(id, *data)).await);
}
let replayed = replay(store, "s", 0, rule).await;
check!(
replayed.len() == payloads.len(),
rule,
"appended {} events, replayed {}",
payloads.len(),
replayed.len()
);
for (offset, (event, data)) in replayed.iter().zip(payloads).enumerate() {
let id = offset as u64 + 1;
check!(
event.id == id && event.data == data,
rule,
"event {id} replayed as id {} with data {:?}, expected {data:?}",
event.id,
event.data
);
}
}
async fn replay_does_not_consume<S: EventStore>(store: &S) {
let rule = "replaying does not consume events";
for id in 1..=3 {
append(store, "s", id, rule).await;
}
let first = replay(store, "s", 0, rule).await;
let second = replay(store, "s", 0, rule).await;
check!(
ids(&first) == vec![1, 2, 3] && ids(&second) == vec![1, 2, 3],
rule,
"two replays returned {:?} and {:?}",
ids(&first),
ids(&second)
);
append(store, "s", 4, rule).await;
let extended = replay(store, "s", 2, rule).await;
check!(
ids(&extended) == vec![3, 4],
rule,
"replay_after(2) after a later append returned {:?}",
ids(&extended)
);
}
async fn unknown_sessions_replay_empty<S: EventStore>(store: &S) {
let rule = "a session with no events replays as an empty list";
let events = replay(store, "no-such-session", 0, rule).await;
check!(
events.is_empty(),
rule,
"an unknown session replayed {:?}",
ids(&events)
);
}
async fn sessions_are_isolated<S: EventStore>(store: &S) {
let rule = "sessions are isolated from one another";
for id in 1..=4 {
append(store, "a", id, rule).await;
append(store, "b", id + 100, rule).await;
}
let a = replay(store, "a", 0, rule).await;
let b = replay(store, "b", 0, rule).await;
check!(
ids(&a) == vec![1, 2, 3, 4],
rule,
"session a replayed {:?}",
ids(&a)
);
check!(
ids(&b) == vec![101, 102, 103, 104],
rule,
"session b replayed {:?}",
ids(&b)
);
let rule = "purging a session leaves other sessions untouched";
ok!(rule, store.purge_session("a").await);
let b = replay(store, "b", 0, rule).await;
check!(
ids(&b) == vec![101, 102, 103, 104],
rule,
"session b replayed {:?} after session a was purged",
ids(&b)
);
}
async fn purge_removes_the_session_log<S: EventStore>(store: &S) {
let rule = "purge_session removes every event for the session";
for id in 1..=5 {
append(store, "s", id, rule).await;
}
ok!(rule, store.purge_session("s").await);
let after = replay(store, "s", 0, rule).await;
check!(
after.is_empty(),
rule,
"replay_after(0) returned {:?} after the purge",
ids(&after)
);
let rule = "purge_session is idempotent";
ok!(rule, store.purge_session("s").await);
ok!(rule, store.purge_session("never-existed").await);
let rule = "a purged session can record new events";
append(store, "s", 6, rule).await;
let fresh = replay(store, "s", 0, rule).await;
check!(
ids(&fresh) == vec![6],
rule,
"replay_after(0) returned {:?} after a purge and one append",
ids(&fresh)
);
}