use async_trait::async_trait;
use uuid::Uuid;
use crate::session::model::Session;
use crate::utils::error::ChainError;
#[async_trait]
pub trait SessionStore: Send + Sync {
async fn get(&self, id: Uuid) -> Result<Session, ChainError>;
async fn create(&self, session: Session) -> Result<(), ChainError>;
async fn save(&self, session: Session) -> Result<(), ChainError>;
async fn save_cas(&self, session: Session, expected_version: u64) -> Result<(), ChainError>;
async fn delete(&self, id: Uuid) -> Result<bool, ChainError>;
async fn cleanup(&self) -> Result<usize, ChainError>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::model::Session;
use crate::utils::error::ChainError;
use mockall::mock;
use optionstratlib::simulation::WalkType;
use optionstratlib::utils::TimeFrame;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use uuid::Uuid;
mock! {
pub SessionStore {}
#[async_trait]
impl SessionStore for SessionStore {
async fn get(&self, id: Uuid) -> Result<Session, ChainError>;
async fn create(&self, session: Session) -> Result<(), ChainError>;
async fn save(&self, session: Session) -> Result<(), ChainError>;
async fn save_cas(&self, session: Session, expected_version: u64) -> Result<(), ChainError>;
async fn delete(&self, id: Uuid) -> Result<bool, ChainError>;
async fn cleanup(&self) -> Result<usize, ChainError>;
}
}
struct TestSessionStore {
sessions: Arc<Mutex<HashMap<Uuid, Session>>>,
}
impl TestSessionStore {
fn new() -> Self {
TestSessionStore {
sessions: Arc::new(Mutex::new(HashMap::new())),
}
}
}
#[async_trait]
impl SessionStore for TestSessionStore {
async fn get(&self, id: Uuid) -> Result<Session, ChainError> {
let sessions = self.sessions.lock().unwrap();
match sessions.get(&id) {
Some(session) => Ok(session.clone()),
None => Err(ChainError::NotFound(format!(
"Session with id {} not found",
id
))),
}
}
async fn create(&self, session: Session) -> Result<(), ChainError> {
let mut sessions = self.sessions.lock().unwrap();
if sessions.contains_key(&session.id) {
return Err(ChainError::AlreadyExists(format!(
"Session with id {} already exists",
session.id
)));
}
sessions.insert(session.id, session);
Ok(())
}
async fn save(&self, session: Session) -> Result<(), ChainError> {
let mut sessions = self.sessions.lock().unwrap();
sessions.insert(session.id, session);
Ok(())
}
async fn save_cas(
&self,
session: Session,
expected_version: u64,
) -> Result<(), ChainError> {
let mut sessions = self.sessions.lock().unwrap();
match sessions.get(&session.id) {
None => Err(ChainError::NotFound(format!(
"Session with id {} not found",
session.id
))),
Some(existing) if existing.version != expected_version => {
Err(ChainError::Conflict(format!(
"Session {} was modified concurrently (expected version {}, found {})",
session.id, expected_version, existing.version
)))
}
Some(_) => {
sessions.insert(session.id, session);
Ok(())
}
}
}
async fn delete(&self, id: Uuid) -> Result<bool, ChainError> {
let mut sessions = self.sessions.lock().unwrap();
Ok(sessions.remove(&id).is_some())
}
async fn cleanup(&self) -> Result<usize, ChainError> {
let now = SystemTime::now();
let mut sessions = self.sessions.lock().unwrap();
let old_count = sessions.len();
sessions.retain(|_, session| {
match now.duration_since(session.updated_at) {
Ok(duration) => duration.as_secs() < 3600, Err(_) => true, }
});
Ok(old_count - sessions.len())
}
}
fn create_test_session(id: Option<Uuid>) -> Session {
use crate::session::model::{SessionState, SimulationParameters};
Session {
id: id.unwrap_or_else(Uuid::new_v4),
created_at: SystemTime::now(),
updated_at: SystemTime::now(),
parameters: SimulationParameters {
symbol: "".to_string(),
steps: 0,
initial_price: Default::default(),
days_to_expiration: Default::default(),
volatility: Default::default(),
risk_free_rate: Default::default(),
dividend_yield: Default::default(),
method: WalkType::Brownian {
dt: Default::default(),
drift: Default::default(),
volatility: Default::default(),
},
time_frame: TimeFrame::Microsecond,
chain_size: None,
strike_interval: None,
skew_slope: None,
smile_curve: None,
spread: None,
seed: None,
},
current_step: 0,
total_steps: 100,
state: SessionState::Initialized,
version: 0,
}
}
#[tokio::test]
async fn test_get_existing_session() {
let store = TestSessionStore::new();
let session = create_test_session(None);
let session_id = session.id;
store.save(session.clone()).await.unwrap();
let result = store.get(session_id).await;
assert!(result.is_ok());
let retrieved_session = result.unwrap();
assert_eq!(retrieved_session.id, session_id);
assert_eq!(retrieved_session.current_step, session.current_step);
assert_eq!(retrieved_session.total_steps, session.total_steps);
}
#[tokio::test]
async fn test_get_non_existing_session() {
let store = TestSessionStore::new();
let non_existent_id = Uuid::new_v4();
let result = store.get(non_existent_id).await;
assert!(result.is_err());
match result {
Err(ChainError::NotFound(_)) => {} _ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async fn test_save_new_session() {
let store = TestSessionStore::new();
let session = create_test_session(None);
let session_id = session.id;
let save_result = store.save(session).await;
assert!(save_result.is_ok());
let get_result = store.get(session_id).await;
assert!(get_result.is_ok());
}
#[tokio::test]
async fn test_create_new_session() {
let store = TestSessionStore::new();
let session = create_test_session(None);
let session_id = session.id;
let create_result = store.create(session).await;
assert!(create_result.is_ok());
assert!(store.get(session_id).await.is_ok());
}
#[tokio::test]
async fn test_create_duplicate_session_returns_already_exists() {
let store = TestSessionStore::new();
let session = create_test_session(None);
assert!(store.create(session.clone()).await.is_ok());
let dup_result = store.create(session).await;
match dup_result {
Err(ChainError::AlreadyExists(_)) => {}
other => panic!("Expected AlreadyExists error, got {:?}", other),
}
}
#[tokio::test]
async fn test_save_existing_session() {
let store = TestSessionStore::new();
let mut session = create_test_session(None);
let session_id = session.id;
store.save(session.clone()).await.unwrap();
session.current_step = 50;
let save_result = store.save(session.clone()).await;
assert!(save_result.is_ok());
let get_result = store.get(session_id).await.unwrap();
assert_eq!(get_result.current_step, 50);
}
#[tokio::test]
async fn test_delete_existing_session() {
let store = TestSessionStore::new();
let session = create_test_session(None);
let session_id = session.id;
store.save(session).await.unwrap();
let delete_result = store.delete(session_id).await;
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
let get_result = store.get(session_id).await;
assert!(get_result.is_err());
}
#[tokio::test]
async fn test_delete_non_existing_session() {
let store = TestSessionStore::new();
let non_existent_id = Uuid::new_v4();
let delete_result = store.delete(non_existent_id).await;
assert!(delete_result.is_ok());
assert!(!delete_result.unwrap()); }
#[tokio::test]
async fn test_cleanup_with_no_expired_sessions() {
let store = TestSessionStore::new();
for _ in 0..5 {
store.save(create_test_session(None)).await.unwrap();
}
let cleanup_result = store.cleanup().await;
assert!(cleanup_result.is_ok());
assert_eq!(cleanup_result.unwrap(), 0); }
}