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