use async_trait::async_trait;
use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::error::{Error, Result, StorageError, TokenError};
use crate::random::{generate_random_base64_url, generate_random_hex};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefreshToken {
pub token: String,
pub token_id: String,
pub user_id: String,
pub family_id: String,
pub created_at: i64,
pub expires_at: i64,
pub used: bool,
pub used_at: Option<i64>,
pub replaced_by: Option<String>,
pub device_info: Option<String>,
pub ip_address: Option<String>,
pub metadata: serde_json::Value,
}
impl RefreshToken {
fn new(user_id: impl Into<String>, family_id: String, expires_in: Duration) -> Result<Self> {
let now = Utc::now().timestamp();
let token = generate_random_base64_url(48)?;
let token_id = generate_random_hex(16)?;
Ok(Self {
token,
token_id,
user_id: user_id.into(),
family_id,
created_at: now,
expires_at: now + expires_in.num_seconds(),
used: false,
used_at: None,
replaced_by: None,
device_info: None,
ip_address: None,
metadata: serde_json::Value::Object(serde_json::Map::new()),
})
}
#[inline]
pub fn is_expired(&self) -> bool {
Utc::now().timestamp() > self.expires_at
}
#[inline]
pub fn is_valid(&self) -> bool {
!self.is_expired() && !self.used
}
pub fn time_to_live(&self) -> i64 {
(self.expires_at - Utc::now().timestamp()).max(0)
}
fn mark_as_used(&mut self, replaced_by: Option<String>) {
self.used = true;
self.used_at = Some(Utc::now().timestamp());
self.replaced_by = replaced_by;
}
pub fn set_metadata<T: Serialize>(&mut self, key: impl Into<String>, value: T) {
if let Ok(json_value) = serde_json::to_value(value)
&& let Some(obj) = self.metadata.as_object_mut()
{
obj.insert(key.into(), json_value);
}
}
pub fn get_metadata<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
self.metadata
.get(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn get_metadata_raw(&self, key: &str) -> Option<&serde_json::Value> {
self.metadata.get(key)
}
pub fn has_metadata(&self, key: &str) -> bool {
self.metadata.get(key).is_some()
}
pub fn remove_metadata(&mut self, key: &str) -> Option<serde_json::Value> {
self.metadata
.as_object_mut()
.and_then(|obj| obj.remove(key))
}
pub fn clear_metadata(&mut self) {
self.metadata = serde_json::Value::Object(serde_json::Map::new());
}
pub fn metadata_keys(&self) -> Vec<&str> {
self.metadata
.as_object()
.map(|obj| obj.keys().map(|k| k.as_str()).collect())
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct RefreshConfig {
pub expiration: Duration,
pub rotation_enabled: bool,
pub reuse_detection: bool,
pub max_tokens_per_family: usize,
pub max_families_per_user: usize,
pub grace_period: Duration,
}
impl Default for RefreshConfig {
fn default() -> Self {
Self {
expiration: Duration::days(30),
rotation_enabled: true,
reuse_detection: true,
max_tokens_per_family: 5,
max_families_per_user: 5,
grace_period: Duration::seconds(0),
}
}
}
impl RefreshConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_expiration(mut self, duration: Duration) -> Self {
self.expiration = duration;
self
}
pub fn with_rotation(mut self, enabled: bool) -> Self {
self.rotation_enabled = enabled;
self
}
pub fn with_reuse_detection(mut self, enabled: bool) -> Self {
self.reuse_detection = enabled;
self
}
pub fn with_max_families_per_user(mut self, max: usize) -> Self {
self.max_families_per_user = max;
self
}
pub fn with_grace_period(mut self, duration: Duration) -> Self {
self.grace_period = duration;
self
}
pub fn high_security() -> Self {
Self {
expiration: Duration::days(7),
rotation_enabled: true,
reuse_detection: true,
max_tokens_per_family: 3,
max_families_per_user: 3,
grace_period: Duration::seconds(0),
}
}
pub fn relaxed() -> Self {
Self {
expiration: Duration::days(90),
rotation_enabled: false,
reuse_detection: false,
max_tokens_per_family: 10,
max_families_per_user: 10,
grace_period: Duration::seconds(60),
}
}
}
#[async_trait]
pub trait RefreshTokenStore: Send + Sync {
async fn save(&self, token: &RefreshToken) -> Result<()>;
async fn get_by_token(&self, token: &str) -> Result<Option<RefreshToken>>;
async fn get_by_id(&self, token_id: &str) -> Result<Option<RefreshToken>>;
async fn update(&self, token: &RefreshToken) -> Result<()>;
async fn delete(&self, token_id: &str) -> Result<()>;
async fn get_by_user(&self, user_id: &str) -> Result<Vec<RefreshToken>>;
async fn get_by_family(&self, family_id: &str) -> Result<Vec<RefreshToken>>;
async fn delete_family(&self, family_id: &str) -> Result<usize>;
async fn delete_by_user(&self, user_id: &str) -> Result<usize>;
async fn cleanup_expired(&self) -> Result<usize>;
async fn count(&self) -> Result<usize>;
}
#[derive(Debug, Default)]
pub struct InMemoryRefreshTokenStore {
tokens: RwLock<HashMap<String, RefreshToken>>,
token_index: RwLock<HashMap<String, String>>, }
impl InMemoryRefreshTokenStore {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl RefreshTokenStore for InMemoryRefreshTokenStore {
async fn save(&self, token: &RefreshToken) -> Result<()> {
let mut tokens = self
.tokens
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let mut index = self
.token_index
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
index.insert(token.token.clone(), token.token_id.clone());
tokens.insert(token.token_id.clone(), token.clone());
Ok(())
}
async fn get_by_token(&self, token: &str) -> Result<Option<RefreshToken>> {
let index = self
.token_index
.read()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let tokens = self
.tokens
.read()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
if let Some(token_id) = index.get(token) {
Ok(tokens.get(token_id).cloned())
} else {
Ok(None)
}
}
async fn get_by_id(&self, token_id: &str) -> Result<Option<RefreshToken>> {
let tokens = self
.tokens
.read()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
Ok(tokens.get(token_id).cloned())
}
async fn update(&self, token: &RefreshToken) -> Result<()> {
let mut tokens = self
.tokens
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
if tokens.contains_key(&token.token_id) {
tokens.insert(token.token_id.clone(), token.clone());
Ok(())
} else {
Err(Error::Storage(StorageError::NotFound(
"token not found".into(),
)))
}
}
async fn delete(&self, token_id: &str) -> Result<()> {
let mut tokens = self
.tokens
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let mut index = self
.token_index
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
if let Some(token) = tokens.remove(token_id) {
index.remove(&token.token);
}
Ok(())
}
async fn get_by_user(&self, user_id: &str) -> Result<Vec<RefreshToken>> {
let tokens = self
.tokens
.read()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
Ok(tokens
.values()
.filter(|t| t.user_id == user_id)
.cloned()
.collect())
}
async fn get_by_family(&self, family_id: &str) -> Result<Vec<RefreshToken>> {
let tokens = self
.tokens
.read()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
Ok(tokens
.values()
.filter(|t| t.family_id == family_id)
.cloned()
.collect())
}
async fn delete_family(&self, family_id: &str) -> Result<usize> {
let mut tokens = self
.tokens
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let mut index = self
.token_index
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let to_delete: Vec<_> = tokens
.iter()
.filter(|(_, t)| t.family_id == family_id)
.map(|(id, t)| (id.clone(), t.token.clone()))
.collect();
let count = to_delete.len();
for (token_id, token) in to_delete {
tokens.remove(&token_id);
index.remove(&token);
}
Ok(count)
}
async fn delete_by_user(&self, user_id: &str) -> Result<usize> {
let mut tokens = self
.tokens
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let mut index = self
.token_index
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let to_delete: Vec<_> = tokens
.iter()
.filter(|(_, t)| t.user_id == user_id)
.map(|(id, t)| (id.clone(), t.token.clone()))
.collect();
let count = to_delete.len();
for (token_id, token) in to_delete {
tokens.remove(&token_id);
index.remove(&token);
}
Ok(count)
}
async fn cleanup_expired(&self) -> Result<usize> {
let mut tokens = self
.tokens
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let mut index = self
.token_index
.write()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
let now = Utc::now().timestamp();
let to_delete: Vec<_> = tokens
.iter()
.filter(|(_, t)| t.expires_at < now)
.map(|(id, t)| (id.clone(), t.token.clone()))
.collect();
let count = to_delete.len();
for (token_id, token) in to_delete {
tokens.remove(&token_id);
index.remove(&token);
}
Ok(count)
}
async fn count(&self) -> Result<usize> {
let tokens = self
.tokens
.read()
.map_err(|_| Error::Storage(StorageError::OperationFailed("lock poisoned".into())))?;
Ok(tokens.len())
}
}
#[derive(Debug, Clone)]
pub struct TokenUseResult {
pub new_token: Option<RefreshToken>,
pub user_id: String,
pub family_id: String,
pub was_in_grace_period: bool,
}
pub struct RefreshTokenManager {
store: Arc<dyn RefreshTokenStore>,
config: RefreshConfig,
}
impl RefreshTokenManager {
pub fn new(config: RefreshConfig) -> Self {
Self {
store: Arc::new(InMemoryRefreshTokenStore::new()),
config,
}
}
pub fn with_store(config: RefreshConfig, store: Arc<dyn RefreshTokenStore>) -> Self {
Self { store, config }
}
pub async fn generate(&self, user_id: impl Into<String>) -> Result<RefreshToken> {
let user_id = user_id.into();
self.enforce_max_families(&user_id).await?;
let family_id = generate_random_hex(16)?;
let token = RefreshToken::new(&user_id, family_id, self.config.expiration)?;
self.store.save(&token).await?;
Ok(token)
}
pub async fn generate_with_options(
&self,
user_id: impl Into<String>,
options: GenerateOptions,
) -> Result<RefreshToken> {
let user_id = user_id.into();
self.enforce_max_families(&user_id).await?;
let family_id = options
.family_id
.unwrap_or_else(|| generate_random_hex(16).unwrap_or_default());
let mut token = RefreshToken::new(&user_id, family_id, self.config.expiration)?;
token.device_info = options.device_info;
token.ip_address = options.ip_address;
if let Some(metadata) = options.metadata {
token.metadata = metadata;
}
self.store.save(&token).await?;
Ok(token)
}
pub async fn use_token(&self, token: &str) -> Result<TokenUseResult> {
let mut stored_token = self
.store
.get_by_token(token)
.await?
.ok_or_else(|| Error::Token(TokenError::InvalidFormat("token not found".into())))?;
if stored_token.is_expired() {
self.store.delete(&stored_token.token_id).await?;
return Err(Error::Token(TokenError::Expired));
}
if stored_token.used {
if self.config.reuse_detection {
let grace_period_secs = self.config.grace_period.num_seconds();
let used_at = stored_token.used_at.unwrap_or(0);
let now = Utc::now().timestamp();
if grace_period_secs == 0 || now > used_at + grace_period_secs {
self.store.delete_family(&stored_token.family_id).await?;
return Err(Error::Token(TokenError::InvalidFormat(
"token reuse detected, family revoked".into(),
)));
}
return Ok(TokenUseResult {
new_token: None,
user_id: stored_token.user_id.clone(),
family_id: stored_token.family_id.clone(),
was_in_grace_period: true,
});
} else {
return Err(Error::Token(TokenError::InvalidFormat(
"token already used".into(),
)));
}
}
let new_token = if self.config.rotation_enabled {
let mut new = RefreshToken::new(
&stored_token.user_id,
stored_token.family_id.clone(),
self.config.expiration,
)?;
new.device_info = stored_token.device_info.clone();
new.ip_address = stored_token.ip_address.clone();
new.metadata = stored_token.metadata.clone();
stored_token.mark_as_used(Some(new.token_id.clone()));
self.store.update(&stored_token).await?;
self.store.save(&new).await?;
self.enforce_max_tokens_per_family(&stored_token.family_id)
.await?;
Some(new)
} else {
stored_token.mark_as_used(None);
self.store.update(&stored_token).await?;
None
};
Ok(TokenUseResult {
new_token,
user_id: stored_token.user_id,
family_id: stored_token.family_id,
was_in_grace_period: false,
})
}
pub async fn validate(&self, token: &str) -> Result<RefreshToken> {
let stored_token = self
.store
.get_by_token(token)
.await?
.ok_or_else(|| Error::Token(TokenError::InvalidFormat("token not found".into())))?;
if stored_token.is_expired() {
return Err(Error::Token(TokenError::Expired));
}
if stored_token.used && self.config.reuse_detection {
return Err(Error::Token(TokenError::InvalidFormat(
"token already used".into(),
)));
}
Ok(stored_token)
}
pub async fn revoke(&self, token: &str) -> Result<()> {
if let Some(stored) = self.store.get_by_token(token).await? {
self.store.delete(&stored.token_id).await?;
}
Ok(())
}
pub async fn revoke_family(&self, family_id: &str) -> Result<usize> {
self.store.delete_family(family_id).await
}
pub async fn revoke_all_for_user(&self, user_id: &str) -> Result<usize> {
self.store.delete_by_user(user_id).await
}
pub async fn get_user_tokens(&self, user_id: &str) -> Result<Vec<RefreshToken>> {
self.store.get_by_user(user_id).await
}
pub async fn get_family_tokens(&self, family_id: &str) -> Result<Vec<RefreshToken>> {
self.store.get_by_family(family_id).await
}
pub async fn cleanup(&self) -> Result<usize> {
self.store.cleanup_expired().await
}
pub async fn count(&self) -> Result<usize> {
self.store.count().await
}
pub fn config(&self) -> &RefreshConfig {
&self.config
}
async fn enforce_max_families(&self, user_id: &str) -> Result<()> {
if self.config.max_families_per_user == 0 {
return Ok(());
}
let tokens = self.store.get_by_user(user_id).await?;
let mut families: HashMap<String, i64> = HashMap::new();
for token in tokens {
families
.entry(token.family_id.clone())
.or_insert(token.created_at);
}
if families.len() >= self.config.max_families_per_user
&& let Some((oldest_family, _)) =
families
.iter()
.min_by(|(id_a, created_a), (id_b, created_b)| {
created_a.cmp(created_b).then_with(|| id_a.cmp(id_b))
})
{
self.store.delete_family(oldest_family).await?;
}
Ok(())
}
async fn enforce_max_tokens_per_family(&self, family_id: &str) -> Result<()> {
if self.config.max_tokens_per_family == 0 {
return Ok(());
}
let mut tokens = self.store.get_by_family(family_id).await?;
tokens.sort_by_key(|t| t.created_at);
while tokens.len() > self.config.max_tokens_per_family {
if let Some(oldest) = tokens.first() {
self.store.delete(&oldest.token_id).await?;
tokens.remove(0);
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct GenerateOptions {
pub device_info: Option<String>,
pub ip_address: Option<String>,
pub metadata: Option<serde_json::Value>,
pub family_id: Option<String>,
}
impl GenerateOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_device_info(mut self, info: impl Into<String>) -> Self {
self.device_info = Some(info.into());
self
}
pub fn with_ip_address(mut self, ip: impl Into<String>) -> Self {
self.ip_address = Some(ip.into());
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_metadata_from<T: Serialize>(mut self, data: T) -> Self {
self.metadata = serde_json::to_value(data).ok();
self
}
pub fn with_family_id(mut self, family_id: impl Into<String>) -> Self {
self.family_id = Some(family_id.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_refresh_token_creation() {
let token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
assert!(!token.token.is_empty());
assert!(!token.token_id.is_empty());
assert_eq!(token.user_id, "user123");
assert_eq!(token.family_id, "family1");
assert!(!token.used);
assert!(!token.is_expired());
assert!(token.is_valid());
}
#[test]
fn test_refresh_token_expiration() {
let mut token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
assert!(!token.is_expired());
token.expires_at = Utc::now().timestamp() - 1;
assert!(token.is_expired());
assert!(!token.is_valid());
}
#[test]
fn test_refresh_token_mark_as_used() {
let mut token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
assert!(token.is_valid());
token.mark_as_used(Some("new_token_id".to_string()));
assert!(token.used);
assert!(token.used_at.is_some());
assert_eq!(token.replaced_by, Some("new_token_id".to_string()));
assert!(!token.is_valid());
}
#[test]
fn test_refresh_token_metadata() {
let mut token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
token.set_metadata("device", "iPhone");
token.set_metadata("count", 42);
token.set_metadata("tags", vec!["a", "b"]);
let device: Option<String> = token.get_metadata("device");
assert_eq!(device, Some("iPhone".to_string()));
let count: Option<i32> = token.get_metadata("count");
assert_eq!(count, Some(42));
assert!(token.has_metadata("device"));
assert!(!token.has_metadata("nonexistent"));
token.remove_metadata("device");
assert!(!token.has_metadata("device"));
}
#[tokio::test]
async fn test_manager_generate() {
let manager = RefreshTokenManager::new(RefreshConfig::default());
let token = manager.generate("user123").await.unwrap();
assert_eq!(token.user_id, "user123");
assert!(!token.token.is_empty());
}
#[tokio::test]
async fn test_manager_use_token_with_rotation() {
let config = RefreshConfig::default().with_rotation(true);
let manager = RefreshTokenManager::new(config);
let token = manager.generate("user123").await.unwrap();
let result = manager.use_token(&token.token).await.unwrap();
assert!(result.new_token.is_some());
assert_eq!(result.user_id, "user123");
assert!(manager.validate(&token.token).await.is_err());
}
#[tokio::test]
async fn test_manager_use_token_without_rotation() {
let config = RefreshConfig::default().with_rotation(false);
let manager = RefreshTokenManager::new(config);
let token = manager.generate("user123").await.unwrap();
let result = manager.use_token(&token.token).await.unwrap();
assert!(result.new_token.is_none());
}
#[tokio::test]
async fn test_manager_reuse_detection() {
let config = RefreshConfig::default()
.with_rotation(true)
.with_reuse_detection(true)
.with_grace_period(Duration::seconds(0));
let manager = RefreshTokenManager::new(config);
let token = manager.generate("user123").await.unwrap();
let token_str = token.token.clone();
let family_id = token.family_id.clone();
let result = manager.use_token(&token_str).await.unwrap();
assert!(result.new_token.is_some());
let new_token = result.new_token.unwrap();
let reuse_result = manager.use_token(&token_str).await;
assert!(reuse_result.is_err());
assert!(manager.validate(&new_token.token).await.is_err());
assert_eq!(
manager.get_family_tokens(&family_id).await.unwrap().len(),
0
);
}
#[tokio::test]
async fn test_manager_revoke() {
let manager = RefreshTokenManager::new(RefreshConfig::default());
let token = manager.generate("user123").await.unwrap();
manager.revoke(&token.token).await.unwrap();
assert!(manager.validate(&token.token).await.is_err());
}
#[tokio::test]
async fn test_manager_revoke_all_for_user() {
let config = RefreshConfig::default().with_max_families_per_user(0);
let manager = RefreshTokenManager::new(config);
manager.generate("user123").await.unwrap();
manager.generate("user123").await.unwrap();
manager.generate("user456").await.unwrap();
let count = manager.revoke_all_for_user("user123").await.unwrap();
assert_eq!(count, 2);
assert_eq!(manager.get_user_tokens("user123").await.unwrap().len(), 0);
assert_eq!(manager.get_user_tokens("user456").await.unwrap().len(), 1);
}
#[tokio::test]
async fn test_manager_max_families() {
let config = RefreshConfig::default().with_max_families_per_user(2);
let manager = RefreshTokenManager::new(config);
let t1 = manager.generate("user123").await.unwrap();
let t2 = manager.generate("user123").await.unwrap();
let t3 = manager.generate("user123").await.unwrap();
let tokens = manager.get_user_tokens("user123").await.unwrap();
let families: std::collections::HashSet<_> =
tokens.iter().map(|t| t.family_id.clone()).collect();
assert_eq!(families.len(), 2);
let mut valid_count = 0;
for t in [&t1.token, &t2.token, &t3.token] {
if manager.validate(t).await.is_ok() {
valid_count += 1;
}
}
assert_eq!(valid_count, 2);
assert!(manager.validate(&t3.token).await.is_ok());
}
#[tokio::test]
async fn test_manager_validate() {
let manager = RefreshTokenManager::new(RefreshConfig::default());
let token = manager.generate("user123").await.unwrap();
let validated = manager.validate(&token.token).await.unwrap();
assert_eq!(validated.user_id, "user123");
}
#[tokio::test]
async fn test_manager_cleanup() {
let manager = RefreshTokenManager::new(RefreshConfig::default());
let mut token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
token.expires_at = Utc::now().timestamp() - 100;
manager.store.save(&token).await.unwrap();
manager.generate("user456").await.unwrap();
let cleaned = manager.cleanup().await.unwrap();
assert_eq!(cleaned, 1);
assert_eq!(manager.count().await.unwrap(), 1);
}
#[test]
fn test_config_presets() {
let high_security = RefreshConfig::high_security();
assert!(high_security.rotation_enabled);
assert!(high_security.reuse_detection);
assert_eq!(high_security.expiration.num_days(), 7);
let relaxed = RefreshConfig::relaxed();
assert!(!relaxed.rotation_enabled);
assert!(!relaxed.reuse_detection);
}
#[tokio::test]
async fn test_generate_with_options() {
let manager = RefreshTokenManager::new(RefreshConfig::default());
let options = GenerateOptions::new()
.with_device_info("iPhone 15")
.with_ip_address("192.168.1.1");
let token = manager
.generate_with_options("user123", options)
.await
.unwrap();
assert_eq!(token.device_info, Some("iPhone 15".to_string()));
assert_eq!(token.ip_address, Some("192.168.1.1".to_string()));
}
#[test]
fn test_token_time_to_live() {
let token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
let ttl = token.time_to_live();
assert!(ttl > 3500 && ttl <= 3600);
}
#[tokio::test]
async fn test_in_memory_store() {
let store = InMemoryRefreshTokenStore::new();
let token =
RefreshToken::new("user123", "family1".to_string(), Duration::hours(1)).unwrap();
store.save(&token).await.unwrap();
let retrieved = store.get_by_token(&token.token).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().user_id, "user123");
let by_id = store.get_by_id(&token.token_id).await.unwrap();
assert!(by_id.is_some());
store.delete(&token.token_id).await.unwrap();
assert!(store.get_by_token(&token.token).await.unwrap().is_none());
}
#[tokio::test]
async fn test_token_rotation_inherits_metadata() {
let manager = RefreshTokenManager::new(RefreshConfig::default().with_rotation(true));
let mut token = manager.generate("user123").await.unwrap();
token.set_metadata("custom_key", "custom_value");
token.device_info = Some("TestDevice".to_string());
manager.store.update(&token).await.unwrap();
let result = manager.use_token(&token.token).await.unwrap();
let new_token = result.new_token.unwrap();
assert_eq!(new_token.device_info, Some("TestDevice".to_string()));
let value: Option<String> = new_token.get_metadata("custom_key");
assert_eq!(value, Some("custom_value".to_string()));
}
}