use std::collections::HashMap;
use std::sync::Mutex;
use chrono::Utc;
use crate::domain::ports::RevocationStore;
#[derive(Debug)]
struct Entry {
exp: i64,
}
#[derive(Debug, Default)]
pub struct InMemoryRevocationStore {
inner: Mutex<HashMap<String, Entry>>,
}
impl InMemoryRevocationStore {
pub fn new() -> Self {
Self::default()
}
fn evict_expired(map: &mut HashMap<String, Entry>) {
let now = Utc::now().timestamp();
map.retain(|_, entry| entry.exp > now);
}
}
impl RevocationStore for InMemoryRevocationStore {
fn revoke(&self, jti: &str, exp: i64) {
let mut map = self.inner.lock().expect("revocation store lock poisoned");
Self::evict_expired(&mut map);
map.insert(jti.to_owned(), Entry { exp });
}
fn is_revoked(&self, jti: &str) -> bool {
let map = self.inner.lock().expect("revocation store lock poisoned");
map.contains_key(jti)
}
}
#[cfg(test)]
mod tests {
use chrono::{Duration, Utc};
use super::*;
fn future_exp(secs: i64) -> i64 {
(Utc::now() + Duration::seconds(secs)).timestamp()
}
fn past_exp(secs: i64) -> i64 {
(Utc::now() - Duration::seconds(secs)).timestamp()
}
#[test]
fn revoke_then_is_revoked_returns_true() {
let store = InMemoryRevocationStore::new();
let jti = "test-jti-001";
store.revoke(jti, future_exp(3600));
assert!(store.is_revoked(jti));
}
#[test]
fn unrevoked_jti_is_not_revoked() {
let store = InMemoryRevocationStore::new();
store.revoke("other-jti", future_exp(3600));
assert!(!store.is_revoked("unrelated-jti"));
}
#[test]
fn expired_entries_are_evicted_on_next_revoke() {
let store = InMemoryRevocationStore::new();
let expired_jti = "expired-jti";
store.revoke(expired_jti, past_exp(10));
store.revoke("fresh-jti", future_exp(3600));
assert!(!store.is_revoked(expired_jti));
}
#[test]
fn fresh_revoked_entry_survives_eviction_pass() {
let store = InMemoryRevocationStore::new();
let live_jti = "live-jti";
store.revoke(live_jti, future_exp(3600));
store.revoke("another-jti", future_exp(7200));
assert!(store.is_revoked(live_jti));
}
#[test]
fn multiple_distinct_jtis_tracked_independently() {
let store = InMemoryRevocationStore::new();
store.revoke("jti-a", future_exp(3600));
store.revoke("jti-b", future_exp(7200));
assert!(store.is_revoked("jti-a"));
assert!(store.is_revoked("jti-b"));
assert!(!store.is_revoked("jti-c"));
}
}