Skip to main content

mx_cache/
mock.rs

1//! Mock implementations for testing cache operations without external dependencies.
2//!
3//! This module provides mock implementations that can be used in tests to simulate
4//! Redis behavior without requiring a live Redis server.
5
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use crate::Cache;
11
12/// Helper macro to check error mode and return an error if enabled.
13/// Reduces boilerplate in mock implementations.
14macro_rules! check_error_mode {
15    ($error_mode:expr) => {{
16        let guard = $error_mode.lock().unwrap();
17        if let Some(ref msg) = *guard {
18            return Err(anyhow::anyhow!("{}", msg));
19        }
20    }};
21}
22
23/// Entry in the mock cache with value and expiration time.
24#[derive(Clone, Debug)]
25struct MockEntry {
26    value: Vec<u8>,
27    expires_at: Option<Instant>,
28}
29
30impl MockEntry {
31    fn is_expired(&self) -> bool {
32        self.expires_at
33            .map(|t| Instant::now() >= t)
34            .unwrap_or(false)
35    }
36}
37
38/// Mock cache implementation for testing purposes.
39///
40/// Provides an in-memory cache that mimics Redis behavior including:
41/// - TTL expiration
42/// - NX (not exists) semantics
43/// - Error injection for testing error handling
44///
45/// # Example
46///
47/// ```
48/// use mx_cache::mock::MockCache;
49/// use mx_cache::Cache;
50/// use std::time::Duration;
51///
52/// # tokio_test::block_on(async {
53/// let cache = MockCache::new();
54///
55/// cache.set(b"key", b"value", Duration::from_secs(60)).await.unwrap();
56/// let result = cache.get(b"key").await.unwrap();
57/// assert_eq!(result, Some(b"value".to_vec()));
58/// # });
59/// ```
60#[derive(Clone, Debug)]
61pub struct MockCache {
62    data: Arc<Mutex<HashMap<Vec<u8>, MockEntry>>>,
63    /// When set, all operations will return this error message
64    error_mode: Arc<Mutex<Option<String>>>,
65    /// Count of operations performed (for verification in tests)
66    operation_count: Arc<Mutex<OperationCounts>>,
67}
68
69/// Tracks operation counts for test verification.
70#[derive(Clone, Debug, Default)]
71pub struct OperationCounts {
72    pub gets: usize,
73    pub sets: usize,
74    pub set_nx_px: usize,
75    pub deletes: usize,
76}
77
78impl MockCache {
79    /// Creates a new empty mock cache.
80    pub fn new() -> Self {
81        Self {
82            data: Arc::new(Mutex::new(HashMap::new())),
83            error_mode: Arc::new(Mutex::new(None)),
84            operation_count: Arc::new(Mutex::new(OperationCounts::default())),
85        }
86    }
87
88    /// Creates a mock cache with pre-populated data.
89    ///
90    /// # Arguments
91    ///
92    /// * `entries` - Iterator of (key, value) pairs to pre-populate
93    pub fn with_data<I, K, V>(entries: I) -> Self
94    where
95        I: IntoIterator<Item = (K, V)>,
96        K: AsRef<[u8]>,
97        V: AsRef<[u8]>,
98    {
99        let cache = Self::new();
100        {
101            let mut data = cache.data.lock().unwrap();
102            for (key, value) in entries {
103                data.insert(
104                    key.as_ref().to_vec(),
105                    MockEntry {
106                        value: value.as_ref().to_vec(),
107                        expires_at: None,
108                    },
109                );
110            }
111        }
112        cache
113    }
114
115    /// Enables error mode - all subsequent operations will fail with the given message.
116    ///
117    /// This is useful for testing error handling paths.
118    pub fn enable_error_mode(&self, message: &str) {
119        let mut error_mode = self.error_mode.lock().unwrap();
120        *error_mode = Some(message.to_owned());
121    }
122
123    /// Disables error mode - operations will proceed normally.
124    pub fn disable_error_mode(&self) {
125        let mut error_mode = self.error_mode.lock().unwrap();
126        *error_mode = None;
127    }
128
129    /// Returns the current operation counts.
130    pub fn operation_counts(&self) -> OperationCounts {
131        self.operation_count.lock().unwrap().clone()
132    }
133
134    /// Resets operation counts to zero.
135    pub fn reset_counts(&self) {
136        let mut counts = self.operation_count.lock().unwrap();
137        *counts = OperationCounts::default();
138    }
139
140    /// Returns the number of non-expired entries in the cache.
141    pub fn len(&self) -> usize {
142        let data = self.data.lock().unwrap();
143        data.values().filter(|e| !e.is_expired()).count()
144    }
145
146    /// Returns true if the cache has no non-expired entries.
147    pub fn is_empty(&self) -> bool {
148        self.len() == 0
149    }
150
151    /// Clears all entries from the cache.
152    pub fn clear(&self) {
153        let mut data = self.data.lock().unwrap();
154        data.clear();
155    }
156
157    /// Manually expires an entry (for testing TTL behavior).
158    pub fn force_expire(&self, key: &[u8]) {
159        let mut data = self.data.lock().unwrap();
160        if let Some(entry) = data.get_mut(&key.to_vec()) {
161            entry.expires_at = Some(Instant::now() - Duration::from_secs(1));
162        }
163    }
164}
165
166impl Default for MockCache {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172impl Cache for MockCache {
173    fn set_nx_px(
174        &self,
175        key: &[u8],
176        value: &[u8],
177        ttl: Duration,
178    ) -> impl Future<Output = anyhow::Result<bool>> + Send {
179        let key_vec = key.to_vec();
180        let value_vec = value.to_vec();
181        let data = Arc::clone(&self.data);
182        let error_mode = Arc::clone(&self.error_mode);
183        let operation_count = Arc::clone(&self.operation_count);
184
185        async move {
186            check_error_mode!(error_mode);
187            operation_count.lock().unwrap().set_nx_px += 1;
188
189            let mut data = data.lock().unwrap();
190
191            // Check if key exists and is not expired
192            if let Some(entry) = data.get(&key_vec)
193                && !entry.is_expired()
194            {
195                return Ok(false);
196            }
197
198            // Insert new entry
199            let expires_at = if ttl.is_zero() {
200                Some(Instant::now())
201            } else {
202                Some(Instant::now() + ttl)
203            };
204
205            data.insert(
206                key_vec,
207                MockEntry {
208                    value: value_vec,
209                    expires_at,
210                },
211            );
212
213            Ok(true)
214        }
215    }
216
217    fn set(
218        &self,
219        key: &[u8],
220        value: &[u8],
221        ttl: Duration,
222    ) -> impl Future<Output = anyhow::Result<()>> + Send {
223        let key_vec = key.to_vec();
224        let value_vec = value.to_vec();
225        let data = Arc::clone(&self.data);
226        let error_mode = Arc::clone(&self.error_mode);
227        let operation_count = Arc::clone(&self.operation_count);
228
229        async move {
230            check_error_mode!(error_mode);
231            operation_count.lock().unwrap().sets += 1;
232
233            let expires_at = if ttl.is_zero() {
234                Some(Instant::now())
235            } else {
236                Some(Instant::now() + ttl)
237            };
238
239            let mut data = data.lock().unwrap();
240            data.insert(
241                key_vec,
242                MockEntry {
243                    value: value_vec,
244                    expires_at,
245                },
246            );
247
248            Ok(())
249        }
250    }
251
252    fn get(&self, key: &[u8]) -> impl Future<Output = anyhow::Result<Option<Vec<u8>>>> + Send {
253        let key_vec = key.to_vec();
254        let data = Arc::clone(&self.data);
255        let error_mode = Arc::clone(&self.error_mode);
256        let operation_count = Arc::clone(&self.operation_count);
257
258        async move {
259            check_error_mode!(error_mode);
260            operation_count.lock().unwrap().gets += 1;
261
262            let data = data.lock().unwrap();
263
264            match data.get(&key_vec) {
265                Some(entry) if !entry.is_expired() => Ok(Some(entry.value.clone())),
266                _ => Ok(None),
267            }
268        }
269    }
270
271    fn del(&self, key: &[u8]) -> impl Future<Output = anyhow::Result<()>> + Send {
272        let key_vec = key.to_vec();
273        let data = Arc::clone(&self.data);
274        let error_mode = Arc::clone(&self.error_mode);
275        let operation_count = Arc::clone(&self.operation_count);
276
277        async move {
278            check_error_mode!(error_mode);
279            operation_count.lock().unwrap().deletes += 1;
280
281            let mut data = data.lock().unwrap();
282            data.remove(&key_vec);
283
284            Ok(())
285        }
286    }
287}
288
289/// Mock set implementation for testing `RedisSet` operations.
290#[derive(Clone, Debug)]
291pub struct MockSet {
292    data: Arc<Mutex<std::collections::HashSet<String>>>,
293    error_mode: Arc<Mutex<Option<String>>>,
294}
295
296impl MockSet {
297    /// Creates a new empty mock set.
298    pub fn new() -> Self {
299        Self {
300            data: Arc::new(Mutex::new(std::collections::HashSet::new())),
301            error_mode: Arc::new(Mutex::new(None)),
302        }
303    }
304
305    /// Enables error mode for testing error handling.
306    pub fn enable_error_mode(&self, message: &str) {
307        let mut error_mode = self.error_mode.lock().unwrap();
308        *error_mode = Some(message.to_owned());
309    }
310
311    /// Disables error mode.
312    pub fn disable_error_mode(&self) {
313        let mut error_mode = self.error_mode.lock().unwrap();
314        *error_mode = None;
315    }
316
317    /// Adds items to the set.
318    pub async fn add_items(&self, items: &[String]) -> anyhow::Result<()> {
319        check_error_mode!(self.error_mode);
320
321        if items.is_empty() {
322            return Ok(());
323        }
324
325        let mut data = self.data.lock().unwrap();
326        for item in items {
327            data.insert(item.clone());
328        }
329        Ok(())
330    }
331
332    /// Removes items from the set.
333    pub async fn remove_items(&self, items: &[String]) -> anyhow::Result<()> {
334        check_error_mode!(self.error_mode);
335
336        if items.is_empty() {
337            return Ok(());
338        }
339
340        let mut data = self.data.lock().unwrap();
341        for item in items {
342            data.remove(item);
343        }
344        Ok(())
345    }
346
347    /// Adds a single item to the set.
348    pub async fn add_item(&self, item: &str) -> anyhow::Result<()> {
349        self.add_items(&[item.to_owned()]).await
350    }
351
352    /// Removes a single item from the set.
353    pub async fn remove_item(&self, item: &str) -> anyhow::Result<()> {
354        self.remove_items(&[item.to_owned()]).await
355    }
356
357    /// Returns all items in the set.
358    pub async fn load_items(&self) -> anyhow::Result<Vec<String>> {
359        check_error_mode!(self.error_mode);
360
361        let data = self.data.lock().unwrap();
362        Ok(data.iter().cloned().collect())
363    }
364
365    /// Trims the set to `max_entries` by removing arbitrary entries.
366    pub async fn trim_to(&self, max_entries: usize) -> anyhow::Result<()> {
367        check_error_mode!(self.error_mode);
368
369        if max_entries == 0 {
370            return Ok(());
371        }
372
373        let mut data = self.data.lock().unwrap();
374        while data.len() > max_entries {
375            // Remove an arbitrary element
376            if let Some(item) = data.iter().next().cloned() {
377                data.remove(&item);
378            }
379        }
380        Ok(())
381    }
382
383    /// Returns the number of items in the set.
384    pub fn len(&self) -> usize {
385        self.data.lock().unwrap().len()
386    }
387
388    /// Returns true if the set is empty.
389    pub fn is_empty(&self) -> bool {
390        self.data.lock().unwrap().is_empty()
391    }
392
393    /// Clears all items from the set.
394    pub fn clear(&self) {
395        self.data.lock().unwrap().clear();
396    }
397
398    /// Checks if the set contains an item.
399    pub fn contains(&self, item: &str) -> bool {
400        self.data.lock().unwrap().contains(item)
401    }
402}
403
404impl Default for MockSet {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[tokio::test]
415    async fn test_mock_cache_basic_operations() {
416        let cache = MockCache::new();
417
418        // Test set and get
419        cache
420            .set(b"key1", b"value1", Duration::from_secs(60))
421            .await
422            .unwrap();
423        let result = cache.get(b"key1").await.unwrap();
424        assert_eq!(result, Some(b"value1".to_vec()));
425
426        // Test get non-existent
427        let result = cache.get(b"nonexistent").await.unwrap();
428        assert_eq!(result, None);
429
430        // Test delete
431        cache.del(b"key1").await.unwrap();
432        let result = cache.get(b"key1").await.unwrap();
433        assert_eq!(result, None);
434    }
435
436    #[tokio::test]
437    async fn test_mock_cache_set_nx_px() {
438        let cache = MockCache::new();
439
440        // First set should succeed
441        let was_set = cache
442            .set_nx_px(b"key1", b"value1", Duration::from_secs(60))
443            .await
444            .unwrap();
445        assert!(was_set);
446
447        // Second set should fail
448        let was_set = cache
449            .set_nx_px(b"key1", b"value2", Duration::from_secs(60))
450            .await
451            .unwrap();
452        assert!(!was_set);
453
454        // Value should be original
455        let result = cache.get(b"key1").await.unwrap();
456        assert_eq!(result, Some(b"value1".to_vec()));
457    }
458
459    #[tokio::test]
460    async fn test_mock_cache_error_mode() {
461        let cache = MockCache::new();
462
463        // Normal operation should succeed
464        cache
465            .set(b"key1", b"value1", Duration::from_secs(60))
466            .await
467            .unwrap();
468
469        // Enable error mode
470        cache.enable_error_mode("Redis connection failed");
471
472        // All operations should fail
473        let result = cache.get(b"key1").await;
474        assert!(result.is_err());
475        assert!(
476            result
477                .unwrap_err()
478                .to_string()
479                .contains("connection failed")
480        );
481
482        let result = cache.set(b"key2", b"value2", Duration::from_secs(60)).await;
483        assert!(result.is_err());
484
485        let result = cache
486            .set_nx_px(b"key3", b"value3", Duration::from_secs(60))
487            .await;
488        assert!(result.is_err());
489
490        let result = cache.del(b"key1").await;
491        assert!(result.is_err());
492
493        // Disable error mode
494        cache.disable_error_mode();
495
496        // Operations should succeed again
497        let result = cache.get(b"key1").await.unwrap();
498        assert_eq!(result, Some(b"value1".to_vec()));
499    }
500
501    #[tokio::test]
502    async fn test_mock_cache_operation_counts() {
503        let cache = MockCache::new();
504
505        cache
506            .set(b"k1", b"v1", Duration::from_secs(60))
507            .await
508            .unwrap();
509        cache
510            .set(b"k2", b"v2", Duration::from_secs(60))
511            .await
512            .unwrap();
513        cache.get(b"k1").await.unwrap();
514        cache.get(b"k2").await.unwrap();
515        cache.get(b"k3").await.unwrap();
516        cache
517            .set_nx_px(b"k4", b"v4", Duration::from_secs(60))
518            .await
519            .unwrap();
520        cache.del(b"k1").await.unwrap();
521
522        let counts = cache.operation_counts();
523        assert_eq!(counts.sets, 2);
524        assert_eq!(counts.gets, 3);
525        assert_eq!(counts.set_nx_px, 1);
526        assert_eq!(counts.deletes, 1);
527
528        cache.reset_counts();
529        let counts = cache.operation_counts();
530        assert_eq!(counts.sets, 0);
531        assert_eq!(counts.gets, 0);
532    }
533
534    #[tokio::test]
535    async fn test_mock_cache_ttl_expiration() {
536        let cache = MockCache::new();
537
538        cache
539            .set(b"key1", b"value1", Duration::from_millis(50))
540            .await
541            .unwrap();
542
543        // Should exist immediately
544        let result = cache.get(b"key1").await.unwrap();
545        assert_eq!(result, Some(b"value1".to_vec()));
546
547        // Wait for expiration
548        tokio::time::sleep(Duration::from_millis(100)).await;
549
550        // Should be expired
551        let result = cache.get(b"key1").await.unwrap();
552        assert_eq!(result, None);
553    }
554
555    #[tokio::test]
556    async fn test_mock_cache_force_expire() {
557        let cache = MockCache::new();
558
559        cache
560            .set(b"key1", b"value1", Duration::from_secs(60))
561            .await
562            .unwrap();
563
564        // Force expire
565        cache.force_expire(b"key1");
566
567        // Should be expired
568        let result = cache.get(b"key1").await.unwrap();
569        assert_eq!(result, None);
570    }
571
572    #[tokio::test]
573    async fn test_mock_cache_with_data() {
574        let cache = MockCache::with_data([
575            (b"key1".as_slice(), b"value1".as_slice()),
576            (b"key2", b"value2"),
577        ]);
578
579        let result1 = cache.get(b"key1").await.unwrap();
580        assert_eq!(result1, Some(b"value1".to_vec()));
581
582        let result2 = cache.get(b"key2").await.unwrap();
583        assert_eq!(result2, Some(b"value2".to_vec()));
584    }
585
586    #[tokio::test]
587    async fn test_mock_cache_len_and_clear() {
588        let cache = MockCache::new();
589
590        assert!(cache.is_empty());
591        assert_eq!(cache.len(), 0);
592
593        cache
594            .set(b"k1", b"v1", Duration::from_secs(60))
595            .await
596            .unwrap();
597        cache
598            .set(b"k2", b"v2", Duration::from_secs(60))
599            .await
600            .unwrap();
601
602        assert!(!cache.is_empty());
603        assert_eq!(cache.len(), 2);
604
605        cache.clear();
606
607        assert!(cache.is_empty());
608        assert_eq!(cache.len(), 0);
609    }
610
611    #[tokio::test]
612    async fn test_mock_set_basic_operations() {
613        let set = MockSet::new();
614
615        assert!(set.is_empty());
616
617        // Add items
618        set.add_item("item1").await.unwrap();
619        set.add_item("item2").await.unwrap();
620
621        assert_eq!(set.len(), 2);
622        assert!(set.contains("item1"));
623        assert!(set.contains("item2"));
624        assert!(!set.contains("item3"));
625
626        // Load items
627        let items = set.load_items().await.unwrap();
628        assert_eq!(items.len(), 2);
629
630        // Remove item
631        set.remove_item("item1").await.unwrap();
632        assert!(!set.contains("item1"));
633        assert_eq!(set.len(), 1);
634
635        // Clear
636        set.clear();
637        assert!(set.is_empty());
638    }
639
640    #[tokio::test]
641    async fn test_mock_set_batch_operations() {
642        let set = MockSet::new();
643
644        // Add multiple items
645        set.add_items(&["a".to_owned(), "b".to_owned(), "c".to_owned()])
646            .await
647            .unwrap();
648        assert_eq!(set.len(), 3);
649
650        // Remove multiple items
651        set.remove_items(&["a".to_owned(), "c".to_owned()])
652            .await
653            .unwrap();
654        assert_eq!(set.len(), 1);
655        assert!(set.contains("b"));
656    }
657
658    #[tokio::test]
659    async fn test_mock_set_trim_to() {
660        let set = MockSet::new();
661
662        for i in 0..10 {
663            set.add_item(&format!("item{i}")).await.unwrap();
664        }
665        assert_eq!(set.len(), 10);
666
667        set.trim_to(5).await.unwrap();
668        assert_eq!(set.len(), 5);
669
670        // trim_to(0) should do nothing (matches RedisSet behavior)
671        set.trim_to(0).await.unwrap();
672        assert_eq!(set.len(), 5);
673    }
674
675    #[tokio::test]
676    async fn test_mock_set_error_mode() {
677        let set = MockSet::new();
678
679        set.add_item("item1").await.unwrap();
680
681        set.enable_error_mode("Redis error");
682
683        assert!(set.add_item("item2").await.is_err());
684        assert!(set.remove_item("item1").await.is_err());
685        assert!(set.load_items().await.is_err());
686        assert!(set.trim_to(1).await.is_err());
687
688        set.disable_error_mode();
689
690        assert!(set.load_items().await.is_ok());
691    }
692
693    #[tokio::test]
694    async fn test_mock_set_empty_operations() {
695        let set = MockSet::new();
696
697        // Empty operations should succeed
698        set.add_items(&[]).await.unwrap();
699        set.remove_items(&[]).await.unwrap();
700
701        assert!(set.is_empty());
702    }
703}