kellnr_common/
token_cache.rs1use std::sync::atomic::{AtomicU64, Ordering};
2use std::time::Duration;
3
4use moka::future::Cache;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct CachedTokenData {
8 pub user: String,
9 pub is_admin: bool,
10 pub is_read_only: bool,
11}
12
13pub struct TokenCacheManager {
14 cache: Option<Cache<String, CachedTokenData>>,
15 generation: AtomicU64,
19}
20
21impl TokenCacheManager {
22 pub fn new(enabled: bool, ttl_seconds: u64, max_capacity: u64) -> Self {
30 let cache = if enabled {
31 Some(
32 Cache::builder()
33 .max_capacity(max_capacity)
34 .time_to_live(Duration::from_secs(ttl_seconds))
35 .build(),
36 )
37 } else {
38 None
39 };
40
41 Self {
42 cache,
43 generation: AtomicU64::new(0),
44 }
45 }
46
47 pub async fn get(&self, token: &str) -> Option<CachedTokenData> {
48 match &self.cache {
49 Some(cache) => cache.get(token).await,
50 None => None,
51 }
52 }
53
54 pub fn generation(&self) -> u64 {
57 self.generation.load(Ordering::SeqCst)
58 }
59
60 pub async fn insert(&self, token: String, data: CachedTokenData, generation: u64) {
65 let Some(cache) = &self.cache else {
66 return;
67 };
68 if self.generation() != generation {
69 return;
70 }
71 cache.insert(token.clone(), data).await;
72 if self.generation() != generation {
73 cache.invalidate(&token).await;
74 }
75 }
76
77 pub fn invalidate_all(&self) {
78 self.generation.fetch_add(1, Ordering::SeqCst);
81 if let Some(cache) = &self.cache {
82 cache.invalidate_all();
83 }
84 }
85
86 pub async fn invalidate(&self, token: &str) {
88 if let Some(cache) = &self.cache {
89 cache.invalidate(token).await;
90 }
91 }
92
93 pub fn is_enabled(&self) -> bool {
94 self.cache.is_some()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn data(is_admin: bool, is_read_only: bool) -> CachedTokenData {
103 CachedTokenData {
104 user: "test_user".to_string(),
105 is_admin,
106 is_read_only,
107 }
108 }
109
110 #[tokio::test]
111 async fn test_cache_disabled() {
112 let cache = TokenCacheManager::new(false, 60, 100);
113 assert!(!cache.is_enabled());
114
115 cache
116 .insert(
117 "token123".to_string(),
118 data(false, false),
119 cache.generation(),
120 )
121 .await;
122 assert!(cache.get("token123").await.is_none());
123 }
124
125 #[tokio::test]
126 async fn test_cache_enabled() {
127 let cache = TokenCacheManager::new(true, 60, 100);
128 assert!(cache.is_enabled());
129
130 cache
131 .insert(
132 "token123".to_string(),
133 data(true, false),
134 cache.generation(),
135 )
136 .await;
137
138 let retrieved = cache.get("token123").await;
139 assert!(retrieved.is_some());
140 let retrieved = retrieved.unwrap();
141 assert_eq!(retrieved.user, "test_user");
142 assert!(retrieved.is_admin);
143 assert!(!retrieved.is_read_only);
144 }
145
146 #[tokio::test]
147 async fn test_cache_miss() {
148 let cache = TokenCacheManager::new(true, 60, 100);
149 assert!(cache.get("nonexistent").await.is_none());
150 }
151
152 #[tokio::test]
153 async fn test_invalidate_all() {
154 let cache = TokenCacheManager::new(true, 60, 100);
155
156 let generation = cache.generation();
157 cache
158 .insert("token1".to_string(), data(false, true), generation)
159 .await;
160 cache
161 .insert("token2".to_string(), data(false, true), generation)
162 .await;
163
164 assert!(cache.get("token1").await.is_some());
165 assert!(cache.get("token2").await.is_some());
166
167 cache.invalidate_all();
168
169 assert!(cache.get("token1").await.is_none());
170 assert!(cache.get("token2").await.is_none());
171 }
172
173 #[tokio::test]
176 async fn insert_after_invalidation_is_dropped() {
177 let cache = TokenCacheManager::new(true, 60, 100);
178
179 let generation = cache.generation();
180 cache.invalidate_all();
181 cache
182 .insert("token".to_string(), data(true, false), generation)
183 .await;
184
185 assert!(cache.get("token").await.is_none());
186 }
187
188 #[tokio::test]
189 async fn invalidate_all_bumps_generation() {
190 let cache = TokenCacheManager::new(false, 60, 100);
191 let before = cache.generation();
192
193 cache.invalidate_all();
194
195 assert_ne!(before, cache.generation());
196 }
197}