use std::path::{Path, PathBuf};
use event_store_adapter_test_utils_rs::id_generator::id_generate;
use crate::event_store_for_sqlite::EventStoreForSqlite;
use crate::event_store_test_support::{
exercise_user_account_flow, find_by_id, init_tracing, UserAccount, UserAccountEvent, UserAccountId,
};
use crate::types::{Aggregate, EventStore, EventStoreWriteError};
struct TempDb {
path: PathBuf,
}
impl TempDb {
fn new() -> Self {
Self {
path: std::env::temp_dir().join(format!("event-store-adapter-rs-sqlite-test-{}.db", id_generate())),
}
}
}
impl Drop for TempDb {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn new_file_event_store(path: &Path) -> EventStoreForSqlite<UserAccountId, UserAccount, UserAccountEvent> {
EventStoreForSqlite::new(path).expect("failed to open sqlite event store")
}
#[tokio::test]
async fn test_event_store_on_sqlite() {
init_tracing();
let db = TempDb::new();
std::fs::File::create(&db.path).expect("failed to create empty db file");
let mut event_store = new_file_event_store(&db.path);
let id = UserAccountId::new(id_generate().to_string());
exercise_user_account_flow(&mut event_store, &id)
.await
.expect("scenario failed");
}
#[tokio::test]
async fn test_event_store_on_sqlite_in_memory() {
let mut event_store: EventStoreForSqlite<UserAccountId, UserAccount, UserAccountEvent> =
EventStoreForSqlite::new_in_memory().expect("failed to open in-memory sqlite event store");
let id = UserAccountId::new(id_generate().to_string());
exercise_user_account_flow(&mut event_store, &id)
.await
.expect("scenario failed");
}
#[tokio::test]
async fn test_event_store_on_sqlite_in_memory_clone_shares_state() {
let mut original: EventStoreForSqlite<UserAccountId, UserAccount, UserAccountEvent> =
EventStoreForSqlite::new_in_memory().expect("failed to open in-memory sqlite event store");
let mut cloned = original.clone();
let id = UserAccountId::new(id_generate().to_string());
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
original
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
let mut from_clone = find_by_id(&mut cloned, &id)
.await
.unwrap()
.expect("clone must see the original's write");
assert_eq!(from_clone.version(), 1);
let event = from_clone.rename("renamed-via-clone").unwrap();
cloned.persist_event(&event, from_clone.version()).await.unwrap();
let from_original = find_by_id(&mut original, &id).await.unwrap().unwrap();
assert_eq!(from_original.version(), 2);
assert_eq!(from_original.seq_nr(), 2);
}
#[tokio::test]
async fn test_event_store_on_sqlite_file_reopen_restores_state() {
let db = TempDb::new();
let id = UserAccountId::new(id_generate().to_string());
{
let mut event_store = new_file_event_store(&db.path);
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
event_store
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
let mut account = find_by_id(&mut event_store, &id).await.unwrap().unwrap();
let event = account.rename("renamed-before-reopen").unwrap();
event_store.persist_event(&event, account.version()).await.unwrap();
}
let mut reopened = new_file_event_store(&db.path);
let restored = find_by_id(&mut reopened, &id)
.await
.unwrap()
.expect("state must survive store reconstruction");
assert_eq!(restored.version(), 2);
assert_eq!(restored.seq_nr(), 2);
}
#[tokio::test]
async fn test_event_store_on_sqlite_optimistic_lock_conflict() {
let db = TempDb::new();
let store = new_file_event_store(&db.path);
let id = UserAccountId::new(id_generate().to_string());
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
{
let mut writer = store.clone();
writer
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
}
let mut store_a = store.clone();
let mut store_b = store.clone();
let mut account_a = find_by_id(&mut store_a, &id).await.unwrap().unwrap();
let mut account_b = find_by_id(&mut store_b, &id).await.unwrap().unwrap();
let event_a = account_a.rename("first").unwrap();
store_a.persist_event(&event_a, account_a.version()).await.unwrap();
let event_b = account_b.rename("second").unwrap();
let result = store_b.persist_event(&event_b, account_b.version()).await;
assert!(
matches!(result, Err(EventStoreWriteError::OptimisticLockError(_))),
"expected OptimisticLockError, got {:?}",
result
);
let events = store_a.get_events_by_id_since_seq_nr(&id, 0).await.unwrap();
assert_eq!(events.len(), 2);
let replayed = find_by_id(&mut store_a, &id).await.unwrap().unwrap();
assert_eq!(replayed.version(), 2);
assert_eq!(replayed.seq_nr(), 2);
}
#[tokio::test]
async fn test_event_store_on_sqlite_error_contract() {
let db = TempDb::new();
let mut store_a = new_file_event_store(&db.path);
let mut store_b = store_a.clone();
let id = UserAccountId::new(id_generate().to_string());
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
store_a
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
let mut account_a = find_by_id(&mut store_a, &id).await.unwrap().unwrap();
let mut account_b = find_by_id(&mut store_b, &id).await.unwrap().unwrap();
let event_a = account_a.rename("first").unwrap();
store_a.persist_event(&event_a, account_a.version()).await.unwrap();
let event_b = account_b.rename("second").unwrap();
let result = store_b.persist_event(&event_b, account_b.version()).await;
match result {
Err(EventStoreWriteError::OptimisticLockError(message)) => {
assert_eq!(
message,
format!(
"optimistic lock failed, aid={}, expected_version=1, actual_version=2",
id
)
);
}
other => panic!("expected OptimisticLockError, got {:?}", other),
}
let (duplicate_account, duplicate_created) = UserAccount::new(id.clone(), "duplicate".to_string());
let result = store_b
.persist_event_and_snapshot(&duplicate_created, &duplicate_account)
.await;
match result {
Err(EventStoreWriteError::OptimisticLockError(message)) => {
assert_eq!(
message,
format!(
"optimistic lock failed, aid={}, expected_version=1, actual_version=2",
id
)
);
}
other => panic!("expected OptimisticLockError, got {:?}", other),
}
}
fn count_snapshot_rows(path: &Path, id: &UserAccountId) -> (i64, Vec<i64>) {
let connection = rusqlite::Connection::open(path).expect("failed to open db for inspection");
let slot_count: i64 = connection
.query_row(
"SELECT COUNT(*) FROM snapshot WHERE aid = ?1 AND seq_nr = 0",
rusqlite::params![id.to_string()],
|row| row.get(0),
)
.unwrap();
let mut statement = connection
.prepare("SELECT seq_nr FROM snapshot WHERE aid = ?1 AND seq_nr > 0 ORDER BY seq_nr ASC")
.unwrap();
let history_seq_nrs = statement
.query_map(rusqlite::params![id.to_string()], |row| row.get::<_, i64>(0))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
(slot_count, history_seq_nrs)
}
#[tokio::test]
async fn test_event_store_on_sqlite_snapshot_retention() {
let db = TempDb::new();
let id = UserAccountId::new(id_generate().to_string());
{
let mut event_store = new_file_event_store(&db.path).with_keep_snapshot_count(Some(1));
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
event_store
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
let mut account = find_by_id(&mut event_store, &id).await.unwrap().unwrap();
let event = account.rename("renamed-once").unwrap();
event_store.persist_event_and_snapshot(&event, &account).await.unwrap();
}
let (slot_count, history_seq_nrs) = count_snapshot_rows(&db.path, &id);
assert_eq!(slot_count, 1);
assert_eq!(history_seq_nrs, vec![2]);
}
#[tokio::test]
async fn test_event_store_on_sqlite_snapshot_ttl_expiration() {
let db = TempDb::new();
let id = UserAccountId::new(id_generate().to_string());
{
let mut event_store = new_file_event_store(&db.path)
.with_keep_snapshot_count(Some(10))
.with_delete_ttl(Some(chrono::Duration::milliseconds(500)));
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
event_store
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
let mut account = find_by_id(&mut event_store, &id).await.unwrap().unwrap();
let event = account.rename("renamed-after-ttl").unwrap();
event_store.persist_event_and_snapshot(&event, &account).await.unwrap();
}
let (slot_count, history_seq_nrs) = count_snapshot_rows(&db.path, &id);
assert_eq!(slot_count, 1);
assert_eq!(history_seq_nrs, vec![2]);
}
#[tokio::test]
async fn test_event_store_on_sqlite_write_failure_returns_neutral_error() {
let missing_dir = std::env::temp_dir().join(format!("event-store-adapter-rs-missing-{}", id_generate()));
let result: Result<EventStoreForSqlite<UserAccountId, UserAccount, UserAccountEvent>, _> =
EventStoreForSqlite::new(missing_dir.join("db.sqlite"));
match result {
Err(EventStoreWriteError::IOError(_)) => {}
other => panic!("expected IOError, got {:?}", other.map(|_| "Ok(store)")),
}
}
#[tokio::test]
async fn test_event_store_on_sqlite_snapshot_retention_zero() {
let db = TempDb::new();
let id = UserAccountId::new(id_generate().to_string());
{
let mut event_store = new_file_event_store(&db.path).with_keep_snapshot_count(Some(0));
let (user_account, created) = UserAccount::new(id.clone(), "test".to_string());
event_store
.persist_event_and_snapshot(&created, &user_account)
.await
.unwrap();
let mut account = find_by_id(&mut event_store, &id).await.unwrap().unwrap();
let event = account.rename("renamed-once").unwrap();
event_store.persist_event_and_snapshot(&event, &account).await.unwrap();
}
let (slot_count, history_seq_nrs) = count_snapshot_rows(&db.path, &id);
assert_eq!(slot_count, 1);
assert!(history_seq_nrs.is_empty());
}
#[tokio::test]
async fn test_event_store_on_sqlite_zero_shard_count_returns_neutral_error() {
let mut event_store: EventStoreForSqlite<UserAccountId, UserAccount, UserAccountEvent> =
EventStoreForSqlite::new_in_memory()
.expect("failed to open in-memory sqlite event store")
.with_shard_count(0);
let (user_account, created) = UserAccount::new(UserAccountId::new(id_generate().to_string()), "test".to_string());
let result = event_store.persist_event_and_snapshot(&created, &user_account).await;
match result {
Err(EventStoreWriteError::OtherError(message)) => {
assert!(message.contains("shard_count"));
}
other => panic!("expected OtherError, got {:?}", other),
}
}