Skip to main content

soaprs_memory/
auth.rs

1//! Reference in-memory authentication persistence.
2
3use std::{collections::BTreeMap, sync::RwLock};
4
5use soaprs_auth::{Session, SessionId, SessionStore};
6use soaprs_core::{BoxFuture, SoapError, SoapResult};
7
8/// Thread-safe reference session store with replacement and idempotent deletion.
9#[derive(Debug)]
10pub struct MemorySessionStore<P> {
11    sessions: RwLock<BTreeMap<SessionId, Session<P>>>,
12}
13
14impl<P> MemorySessionStore<P> {
15    /// Creates an empty session store.
16    pub const fn new() -> Self {
17        Self {
18            sessions: RwLock::new(BTreeMap::new()),
19        }
20    }
21}
22
23impl<P> Default for MemorySessionStore<P> {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl<P> SessionStore<P> for MemorySessionStore<P>
30where
31    P: Clone + Send + Sync + 'static,
32{
33    fn load<'a>(&'a self, id: &'a SessionId) -> BoxFuture<'a, SoapResult<Option<Session<P>>>> {
34        Box::pin(async move {
35            let sessions = self
36                .sessions
37                .read()
38                .map_err(|_| SoapError::infrastructure("in-memory session read lock poisoned"))?;
39            Ok(sessions.get(id).cloned())
40        })
41    }
42
43    fn save(&self, session: Session<P>) -> BoxFuture<'_, SoapResult<()>> {
44        Box::pin(async move {
45            let mut sessions = self
46                .sessions
47                .write()
48                .map_err(|_| SoapError::infrastructure("in-memory session write lock poisoned"))?;
49            sessions.insert(session.id().clone(), session);
50            Ok(())
51        })
52    }
53
54    fn delete<'a>(&'a self, id: &'a SessionId) -> BoxFuture<'a, SoapResult<()>> {
55        Box::pin(async move {
56            let mut sessions = self
57                .sessions
58                .write()
59                .map_err(|_| SoapError::infrastructure("in-memory session write lock poisoned"))?;
60            sessions.remove(id);
61            Ok(())
62        })
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use std::time::{Duration, UNIX_EPOCH};
69
70    use soaprs_auth::{Session, SessionId, StandardPrincipal};
71    use soaprs_contract_tests::{block_on, verify_session_store_contract};
72
73    use super::MemorySessionStore;
74
75    #[test]
76    fn session_store_passes_the_shared_contract() {
77        let Some(id) = SessionId::new("session-contract").ok() else {
78            panic!("valid session ID");
79        };
80        let first = StandardPrincipal::new("user-1").and_then(|principal| {
81            Session::new(
82                id.clone(),
83                principal,
84                UNIX_EPOCH,
85                UNIX_EPOCH + Duration::from_secs(60),
86            )
87        });
88        let replacement = StandardPrincipal::new("user-2").and_then(|principal| {
89            Session::new(
90                id,
91                principal,
92                UNIX_EPOCH,
93                UNIX_EPOCH + Duration::from_secs(120),
94            )
95        });
96        let (Some(first), Some(replacement)) = (first.ok(), replacement.ok()) else {
97            panic!("valid session fixtures");
98        };
99        let store = MemorySessionStore::new();
100
101        assert!(block_on(verify_session_store_contract(&store, first, replacement)).is_ok());
102    }
103}