use std::collections::HashMap;
use std::sync::RwLock;
use parse_rust_core::object_id::random_string;
const REVOCABLE_PREFIX: &str = "r:";
#[derive(Default)]
pub struct SessionStore {
by_token: RwLock<HashMap<String, String>>,
}
impl SessionStore {
pub fn create(&self, user_object_id: &str) -> String {
let token = format!("{REVOCABLE_PREFIX}{}", random_string(32));
if let Ok(mut map) = self.by_token.write() {
map.insert(token.clone(), user_object_id.to_string());
}
token
}
pub fn user_for(&self, token: &str) -> Option<String> {
self.by_token.read().ok()?.get(token).cloned()
}
pub fn revoke(&self, token: &str) -> bool {
self.by_token
.write()
.map(|mut m| m.remove(token).is_some())
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokens_carry_the_revocable_prefix() {
let s = SessionStore::default();
let t = s.create("u1");
assert!(
t.starts_with("r:"),
"clients treat the r: prefix as meaningful: {t}"
);
assert_eq!(t.len(), 2 + 32);
}
#[test]
fn a_token_resolves_to_its_user_and_only_its_user() {
let s = SessionStore::default();
let a = s.create("alice");
let b = s.create("bob");
assert_eq!(s.user_for(&a).as_deref(), Some("alice"));
assert_eq!(s.user_for(&b).as_deref(), Some("bob"));
assert_ne!(a, b);
}
#[test]
fn an_unknown_token_resolves_to_nobody() {
let s = SessionStore::default();
assert_eq!(s.user_for("r:nonsense"), None);
assert_eq!(s.user_for(""), None);
}
#[test]
fn revoking_removes_the_token() {
let s = SessionStore::default();
let t = s.create("u1");
assert!(s.revoke(&t));
assert_eq!(
s.user_for(&t),
None,
"a revoked token must resolve to nobody"
);
assert!(!s.revoke(&t), "revoking twice reports the second as a miss");
}
}