use std::sync::Arc;
use tower_sessions::cookie::{Cookie, CookieJar, Key};
use tower_sessions::{Session, SessionStore};
use tower_sessions_memory_store::MemoryStore;
#[derive(Clone)]
pub struct TestSessions {
store: MemoryStore,
cookie_name: String,
key: Arc<Key>,
}
#[derive(Debug)]
pub enum TestSessionError {
InvalidSigningKey {
length: usize,
},
Serialize(String),
Store(String),
}
impl std::fmt::Display for TestSessionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidSigningKey { length } => write!(
formatter,
"session signing key must be 64 bytes, got {length}"
),
Self::Serialize(message) => {
write!(formatter, "session value did not serialize: {message}")
}
Self::Store(message) => write!(formatter, "session store write failed: {message}"),
}
}
}
impl std::error::Error for TestSessionError {}
impl TestSessions {
pub fn new(
cookie_name: impl Into<String>,
signing_key: &[u8],
) -> Result<Self, TestSessionError> {
if signing_key.len() != 64 {
return Err(TestSessionError::InvalidSigningKey {
length: signing_key.len(),
});
}
Ok(Self {
store: MemoryStore::default(),
cookie_name: cookie_name.into(),
key: Arc::new(Key::from(signing_key)),
})
}
#[must_use]
pub fn store(&self) -> MemoryStore {
self.store.clone()
}
#[must_use]
pub fn cookie_name(&self) -> &str {
&self.cookie_name
}
}
impl TestSessions {
pub async fn cookie_for(
&self,
entries: &[(String, serde_json::Value)],
) -> Result<String, TestSessionError> {
let session = Session::new(None, Arc::new(self.store.clone()), None);
for (key, value) in entries {
session
.insert(key, value)
.await
.map_err(|error| TestSessionError::Serialize(error.to_string()))?;
}
session
.save()
.await
.map_err(|error| TestSessionError::Store(error.to_string()))?;
let id = session
.id()
.ok_or_else(|| TestSessionError::Store("session was saved without an id".into()))?;
let mut jar = CookieJar::new();
jar.signed_mut(&self.key)
.add(Cookie::new(self.cookie_name.clone(), id.to_string()));
let signed = jar
.get(&self.cookie_name)
.ok_or_else(|| TestSessionError::Store("signed cookie was not produced".into()))?;
Ok(format!("{}={}", signed.name(), signed.value()))
}
pub async fn get<T>(&self, id: &str, key: &str) -> Result<Option<T>, TestSessionError>
where
T: serde::de::DeserializeOwned,
{
let id: tower_sessions::session::Id = id
.parse()
.map_err(|_| TestSessionError::Store(format!("`{id}` is not a session id")))?;
let record = self
.store
.load(&id)
.await
.map_err(|error| TestSessionError::Store(error.to_string()))?;
let Some(record) = record else {
return Ok(None);
};
record
.data
.get(key)
.map(|value| serde_json::from_value(value.clone()))
.transpose()
.map_err(|error| TestSessionError::Serialize(error.to_string()))
}
}
impl std::fmt::Debug for TestSessions {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TestSessions")
.field("cookie_name", &self.cookie_name)
.field("signing_key", &"<redacted 64-byte secret>")
.finish_non_exhaustive()
}
}