openzeppelin_relayer/repositories/notification/
notification_redis.rs

1//! Redis-backed implementation of the NotificationRepository.
2
3use crate::models::{NotificationRepoModel, PaginationQuery, RepositoryError};
4use crate::repositories::redis_base::RedisRepository;
5use crate::repositories::{BatchRetrievalResult, PaginatedResult, Repository};
6use async_trait::async_trait;
7use log::{debug, error, warn};
8use redis::aio::ConnectionManager;
9use redis::AsyncCommands;
10use std::fmt;
11use std::sync::Arc;
12
13const NOTIFICATION_PREFIX: &str = "notification";
14const NOTIFICATION_LIST_KEY: &str = "notification_list";
15
16#[derive(Clone)]
17pub struct RedisNotificationRepository {
18    pub client: Arc<ConnectionManager>,
19    pub key_prefix: String,
20}
21
22impl RedisRepository for RedisNotificationRepository {}
23
24impl RedisNotificationRepository {
25    pub fn new(
26        connection_manager: Arc<ConnectionManager>,
27        key_prefix: String,
28    ) -> Result<Self, RepositoryError> {
29        if key_prefix.is_empty() {
30            return Err(RepositoryError::InvalidData(
31                "Redis key prefix cannot be empty".to_string(),
32            ));
33        }
34
35        Ok(Self {
36            client: connection_manager,
37            key_prefix,
38        })
39    }
40
41    /// Generate key for notification data: notification:{notification_id}
42    fn notification_key(&self, notification_id: &str) -> String {
43        format!(
44            "{}:{}:{}",
45            self.key_prefix, NOTIFICATION_PREFIX, notification_id
46        )
47    }
48
49    /// Generate key for notification list: notification_list (set of all notification IDs)
50    fn notification_list_key(&self) -> String {
51        format!("{}:{}", self.key_prefix, NOTIFICATION_LIST_KEY)
52    }
53
54    /// Batch fetch notifications by IDs
55    async fn get_notifications_by_ids(
56        &self,
57        ids: &[String],
58    ) -> Result<BatchRetrievalResult<NotificationRepoModel>, RepositoryError> {
59        if ids.is_empty() {
60            debug!("No notification IDs provided for batch fetch");
61            return Ok(BatchRetrievalResult {
62                results: vec![],
63                failed_ids: vec![],
64            });
65        }
66
67        let mut conn = self.client.as_ref().clone();
68        let keys: Vec<String> = ids.iter().map(|id| self.notification_key(id)).collect();
69
70        debug!("Batch fetching {} notification data", keys.len());
71
72        let values: Vec<Option<String>> = conn
73            .mget(&keys)
74            .await
75            .map_err(|e| self.map_redis_error(e, "batch_fetch_notifications"))?;
76
77        let mut notifications = Vec::new();
78        let mut failed_count = 0;
79        let mut failed_ids = Vec::new();
80        for (i, value) in values.into_iter().enumerate() {
81            match value {
82                Some(json) => {
83                    match self.deserialize_entity::<NotificationRepoModel>(
84                        &json,
85                        &ids[i],
86                        "notification",
87                    ) {
88                        Ok(notification) => notifications.push(notification),
89                        Err(e) => {
90                            failed_count += 1;
91                            error!("Failed to deserialize notification {}: {}", ids[i], e);
92                            failed_ids.push(ids[i].clone());
93                            // Continue processing other notifications
94                        }
95                    }
96                }
97                None => {
98                    warn!("Notification {} not found in batch fetch", ids[i]);
99                }
100            }
101        }
102
103        if failed_count > 0 {
104            warn!(
105                "Failed to deserialize {} out of {} notifications in batch",
106                failed_count,
107                ids.len()
108            );
109        }
110
111        warn!("Failed to deserialize notifications: {:?}", failed_ids);
112
113        debug!("Successfully fetched {} notifications", notifications.len());
114        Ok(BatchRetrievalResult {
115            results: notifications,
116            failed_ids,
117        })
118    }
119}
120
121impl fmt::Debug for RedisNotificationRepository {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.debug_struct("RedisNotificationRepository")
124            .field("client", &"<ConnectionManager>")
125            .field("key_prefix", &self.key_prefix)
126            .finish()
127    }
128}
129
130#[async_trait]
131impl Repository<NotificationRepoModel, String> for RedisNotificationRepository {
132    async fn create(
133        &self,
134        entity: NotificationRepoModel,
135    ) -> Result<NotificationRepoModel, RepositoryError> {
136        if entity.id.is_empty() {
137            return Err(RepositoryError::InvalidData(
138                "Notification ID cannot be empty".to_string(),
139            ));
140        }
141
142        if entity.url.is_empty() {
143            return Err(RepositoryError::InvalidData(
144                "Notification URL cannot be empty".to_string(),
145            ));
146        }
147
148        let key = self.notification_key(&entity.id);
149        let notification_list_key = self.notification_list_key();
150        let mut conn = self.client.as_ref().clone();
151
152        debug!("Creating notification with ID: {}", entity.id);
153
154        let value = self.serialize_entity(&entity, |n| &n.id, "notification")?;
155
156        // Check if notification already exists
157        let existing: Option<String> = conn
158            .get(&key)
159            .await
160            .map_err(|e| self.map_redis_error(e, "create_notification_check"))?;
161
162        if existing.is_some() {
163            return Err(RepositoryError::ConstraintViolation(format!(
164                "Notification with ID '{}' already exists",
165                entity.id
166            )));
167        }
168
169        // Use atomic pipeline for consistency
170        let mut pipe = redis::pipe();
171        pipe.atomic();
172        pipe.set(&key, &value);
173        pipe.sadd(&notification_list_key, &entity.id);
174
175        pipe.exec_async(&mut conn)
176            .await
177            .map_err(|e| self.map_redis_error(e, "create_notification"))?;
178
179        debug!("Successfully created notification {}", entity.id);
180        Ok(entity)
181    }
182
183    async fn get_by_id(&self, id: String) -> Result<NotificationRepoModel, RepositoryError> {
184        if id.is_empty() {
185            return Err(RepositoryError::InvalidData(
186                "Notification ID cannot be empty".to_string(),
187            ));
188        }
189
190        let mut conn = self.client.as_ref().clone();
191        let key = self.notification_key(&id);
192
193        debug!("Fetching notification with ID: {}", id);
194
195        let value: Option<String> = conn
196            .get(&key)
197            .await
198            .map_err(|e| self.map_redis_error(e, "get_notification_by_id"))?;
199
200        match value {
201            Some(json) => {
202                let notification =
203                    self.deserialize_entity::<NotificationRepoModel>(&json, &id, "notification")?;
204                debug!("Successfully fetched notification {}", id);
205                Ok(notification)
206            }
207            None => {
208                debug!("Notification {} not found", id);
209                Err(RepositoryError::NotFound(format!(
210                    "Notification with ID '{}' not found",
211                    id
212                )))
213            }
214        }
215    }
216
217    async fn list_all(&self) -> Result<Vec<NotificationRepoModel>, RepositoryError> {
218        let mut conn = self.client.as_ref().clone();
219        let notification_list_key = self.notification_list_key();
220
221        debug!("Fetching all notification IDs");
222
223        let notification_ids: Vec<String> = conn
224            .smembers(&notification_list_key)
225            .await
226            .map_err(|e| self.map_redis_error(e, "list_all_notification_ids"))?;
227
228        debug!("Found {} notification IDs", notification_ids.len());
229
230        let notifications = self.get_notifications_by_ids(&notification_ids).await?;
231        Ok(notifications.results)
232    }
233
234    async fn list_paginated(
235        &self,
236        query: PaginationQuery,
237    ) -> Result<PaginatedResult<NotificationRepoModel>, RepositoryError> {
238        if query.per_page == 0 {
239            return Err(RepositoryError::InvalidData(
240                "per_page must be greater than 0".to_string(),
241            ));
242        }
243
244        let mut conn = self.client.as_ref().clone();
245        let notification_list_key = self.notification_list_key();
246
247        debug!(
248            "Fetching paginated notifications (page: {}, per_page: {})",
249            query.page, query.per_page
250        );
251
252        let all_notification_ids: Vec<String> = conn
253            .smembers(&notification_list_key)
254            .await
255            .map_err(|e| self.map_redis_error(e, "list_paginated_notification_ids"))?;
256
257        let total = all_notification_ids.len() as u64;
258        let start = ((query.page - 1) * query.per_page) as usize;
259        let end = (start + query.per_page as usize).min(all_notification_ids.len());
260
261        if start >= all_notification_ids.len() {
262            debug!(
263                "Page {} is beyond available data (total: {})",
264                query.page, total
265            );
266            return Ok(PaginatedResult {
267                items: vec![],
268                total,
269                page: query.page,
270                per_page: query.per_page,
271            });
272        }
273
274        let page_ids = &all_notification_ids[start..end];
275        let items = self.get_notifications_by_ids(page_ids).await?;
276
277        debug!(
278            "Successfully fetched {} notifications for page {}",
279            items.results.len(),
280            query.page
281        );
282
283        Ok(PaginatedResult {
284            items: items.results.clone(),
285            total,
286            page: query.page,
287            per_page: query.per_page,
288        })
289    }
290
291    async fn update(
292        &self,
293        id: String,
294        entity: NotificationRepoModel,
295    ) -> Result<NotificationRepoModel, RepositoryError> {
296        if id.is_empty() {
297            return Err(RepositoryError::InvalidData(
298                "Notification ID cannot be empty".to_string(),
299            ));
300        }
301
302        if id != entity.id {
303            return Err(RepositoryError::InvalidData(
304                "Notification ID in URL does not match entity ID".to_string(),
305            ));
306        }
307
308        let key = self.notification_key(&id);
309        let mut conn = self.client.as_ref().clone();
310
311        debug!("Updating notification with ID: {}", id);
312
313        // Check if notification exists
314        let existing: Option<String> = conn
315            .get(&key)
316            .await
317            .map_err(|e| self.map_redis_error(e, "update_notification_check"))?;
318
319        if existing.is_none() {
320            return Err(RepositoryError::NotFound(format!(
321                "Notification with ID '{}' not found",
322                id
323            )));
324        }
325
326        let value = self.serialize_entity(&entity, |n| &n.id, "notification")?;
327
328        // Update notification data
329        let _: () = conn
330            .set(&key, value)
331            .await
332            .map_err(|e| self.map_redis_error(e, "update_notification"))?;
333
334        debug!("Successfully updated notification {}", id);
335        Ok(entity)
336    }
337
338    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
339        if id.is_empty() {
340            return Err(RepositoryError::InvalidData(
341                "Notification ID cannot be empty".to_string(),
342            ));
343        }
344
345        let key = self.notification_key(&id);
346        let notification_list_key = self.notification_list_key();
347        let mut conn = self.client.as_ref().clone();
348
349        debug!("Deleting notification with ID: {}", id);
350
351        // Check if notification exists
352        let existing: Option<String> = conn
353            .get(&key)
354            .await
355            .map_err(|e| self.map_redis_error(e, "delete_notification_check"))?;
356
357        if existing.is_none() {
358            return Err(RepositoryError::NotFound(format!(
359                "Notification with ID '{}' not found",
360                id
361            )));
362        }
363
364        // Use atomic pipeline to ensure consistency
365        let mut pipe = redis::pipe();
366        pipe.atomic();
367        pipe.del(&key);
368        pipe.srem(&notification_list_key, &id);
369
370        pipe.exec_async(&mut conn)
371            .await
372            .map_err(|e| self.map_redis_error(e, "delete_notification"))?;
373
374        debug!("Successfully deleted notification {}", id);
375        Ok(())
376    }
377
378    async fn count(&self) -> Result<usize, RepositoryError> {
379        let mut conn = self.client.as_ref().clone();
380        let notification_list_key = self.notification_list_key();
381
382        debug!("Counting notifications");
383
384        let count: u64 = conn
385            .scard(&notification_list_key)
386            .await
387            .map_err(|e| self.map_redis_error(e, "count_notifications"))?;
388
389        debug!("Notification count: {}", count);
390        Ok(count as usize)
391    }
392
393    async fn has_entries(&self) -> Result<bool, RepositoryError> {
394        let mut conn = self.client.as_ref().clone();
395        let notification_list_key = self.notification_list_key();
396
397        debug!("Checking if notification entries exist");
398
399        let exists: bool = conn
400            .exists(&notification_list_key)
401            .await
402            .map_err(|e| self.map_redis_error(e, "has_entries_check"))?;
403
404        debug!("Notification entries exist: {}", exists);
405        Ok(exists)
406    }
407
408    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
409        let mut conn = self.client.as_ref().clone();
410        let notification_list_key = self.notification_list_key();
411
412        debug!("Dropping all notification entries");
413
414        // Get all notification IDs first
415        let notification_ids: Vec<String> = conn
416            .smembers(&notification_list_key)
417            .await
418            .map_err(|e| self.map_redis_error(e, "drop_all_entries_get_ids"))?;
419
420        if notification_ids.is_empty() {
421            debug!("No notification entries to drop");
422            return Ok(());
423        }
424
425        // Use pipeline for atomic operations
426        let mut pipe = redis::pipe();
427        pipe.atomic();
428
429        // Delete all individual notification entries
430        for notification_id in &notification_ids {
431            let notification_key = self.notification_key(notification_id);
432            pipe.del(&notification_key);
433        }
434
435        // Delete the notification list key
436        pipe.del(&notification_list_key);
437
438        pipe.exec_async(&mut conn)
439            .await
440            .map_err(|e| self.map_redis_error(e, "drop_all_entries_pipeline"))?;
441
442        debug!("Dropped {} notification entries", notification_ids.len());
443        Ok(())
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use crate::models::NotificationType;
451    use redis::Client;
452    use tokio;
453    use uuid::Uuid;
454
455    // Helper function to create test notifications
456    fn create_test_notification(id: &str) -> NotificationRepoModel {
457        NotificationRepoModel {
458            id: id.to_string(),
459            notification_type: NotificationType::Webhook,
460            url: "http://localhost:8080/webhook".to_string(),
461            signing_key: None,
462        }
463    }
464
465    fn create_test_notification_with_url(id: &str, url: &str) -> NotificationRepoModel {
466        NotificationRepoModel {
467            id: id.to_string(),
468            notification_type: NotificationType::Webhook,
469            url: url.to_string(),
470            signing_key: None,
471        }
472    }
473
474    async fn setup_test_repo() -> RedisNotificationRepository {
475        // Use a mock Redis URL - in real integration tests, this would connect to a test Redis instance
476        let redis_url = std::env::var("REDIS_TEST_URL")
477            .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
478
479        let client = Client::open(redis_url).expect("Failed to create Redis client");
480        let connection_manager = ConnectionManager::new(client)
481            .await
482            .expect("Failed to create connection manager");
483
484        RedisNotificationRepository::new(Arc::new(connection_manager), "test_prefix".to_string())
485            .expect("Failed to create RedisNotificationRepository")
486    }
487
488    #[tokio::test]
489    #[ignore = "Requires active Redis instance"]
490    async fn test_new_repository_creation() {
491        let repo = setup_test_repo().await;
492        assert_eq!(repo.key_prefix, "test_prefix");
493    }
494
495    #[tokio::test]
496    #[ignore = "Requires active Redis instance"]
497    async fn test_new_repository_empty_prefix_fails() {
498        let redis_url = std::env::var("REDIS_TEST_URL")
499            .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
500        let client = Client::open(redis_url).expect("Failed to create Redis client");
501        let connection_manager = ConnectionManager::new(client)
502            .await
503            .expect("Failed to create connection manager");
504
505        let result = RedisNotificationRepository::new(Arc::new(connection_manager), "".to_string());
506        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
507    }
508
509    #[tokio::test]
510    #[ignore = "Requires active Redis instance"]
511    async fn test_key_generation() {
512        let repo = setup_test_repo().await;
513
514        assert_eq!(
515            repo.notification_key("test-id"),
516            "test_prefix:notification:test-id"
517        );
518        assert_eq!(
519            repo.notification_list_key(),
520            "test_prefix:notification_list"
521        );
522    }
523
524    #[tokio::test]
525    #[ignore = "Requires active Redis instance"]
526
527    async fn test_serialize_deserialize_notification() {
528        let repo = setup_test_repo().await;
529        let random_id = Uuid::new_v4().to_string();
530        let notification = create_test_notification(&random_id);
531
532        let serialized = repo
533            .serialize_entity(&notification, |n| &n.id, "notification")
534            .expect("Serialization should succeed");
535        let deserialized: NotificationRepoModel = repo
536            .deserialize_entity(&serialized, &random_id, "notification")
537            .expect("Deserialization should succeed");
538
539        assert_eq!(notification.id, deserialized.id);
540        assert_eq!(
541            notification.notification_type,
542            deserialized.notification_type
543        );
544        assert_eq!(notification.url, deserialized.url);
545    }
546
547    #[tokio::test]
548    #[ignore = "Requires active Redis instance"]
549    async fn test_create_notification() {
550        let repo = setup_test_repo().await;
551        let random_id = Uuid::new_v4().to_string();
552        let notification = create_test_notification(&random_id);
553
554        let result = repo.create(notification.clone()).await.unwrap();
555        assert_eq!(result.id, notification.id);
556        assert_eq!(result.url, notification.url);
557    }
558
559    #[tokio::test]
560    #[ignore = "Requires active Redis instance"]
561    async fn test_get_notification() {
562        let repo = setup_test_repo().await;
563        let random_id = Uuid::new_v4().to_string();
564        let notification = create_test_notification(&random_id);
565
566        repo.create(notification.clone()).await.unwrap();
567        let stored = repo.get_by_id(random_id.to_string()).await.unwrap();
568        assert_eq!(stored.id, notification.id);
569        assert_eq!(stored.url, notification.url);
570    }
571
572    #[tokio::test]
573    #[ignore = "Requires active Redis instance"]
574    async fn test_list_all_notifications() {
575        let repo = setup_test_repo().await;
576        let random_id = Uuid::new_v4().to_string();
577        let random_id2 = Uuid::new_v4().to_string();
578
579        let notification1 = create_test_notification(&random_id);
580        let notification2 = create_test_notification(&random_id2);
581
582        repo.create(notification1).await.unwrap();
583        repo.create(notification2).await.unwrap();
584
585        let notifications = repo.list_all().await.unwrap();
586        assert!(notifications.len() >= 2);
587    }
588
589    #[tokio::test]
590    #[ignore = "Requires active Redis instance"]
591    async fn test_count_notifications() {
592        let repo = setup_test_repo().await;
593        let random_id = Uuid::new_v4().to_string();
594        let notification = create_test_notification(&random_id);
595
596        let count = repo.count().await.unwrap();
597        repo.create(notification).await.unwrap();
598        assert!(repo.count().await.unwrap() > count);
599    }
600
601    #[tokio::test]
602    #[ignore = "Requires active Redis instance"]
603    async fn test_get_nonexistent_notification() {
604        let repo = setup_test_repo().await;
605        let result = repo.get_by_id("nonexistent".to_string()).await;
606        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
607    }
608
609    #[tokio::test]
610    #[ignore = "Requires active Redis instance"]
611    async fn test_duplicate_notification_creation() {
612        let repo = setup_test_repo().await;
613        let random_id = Uuid::new_v4().to_string();
614
615        let notification = create_test_notification(&random_id);
616
617        repo.create(notification.clone()).await.unwrap();
618        let result = repo.create(notification).await;
619
620        assert!(matches!(
621            result,
622            Err(RepositoryError::ConstraintViolation(_))
623        ));
624    }
625
626    #[tokio::test]
627    #[ignore = "Requires active Redis instance"]
628    async fn test_update_notification() {
629        let repo = setup_test_repo().await;
630        let random_id = Uuid::new_v4().to_string();
631        let mut notification = create_test_notification(&random_id);
632
633        // Create the notification first
634        repo.create(notification.clone()).await.unwrap();
635
636        // Update the notification
637        notification.url = "http://updated.example.com/webhook".to_string();
638        let result = repo
639            .update(random_id.to_string(), notification.clone())
640            .await
641            .unwrap();
642        assert_eq!(result.url, "http://updated.example.com/webhook");
643
644        // Verify the update by fetching the notification
645        let stored = repo.get_by_id(random_id.to_string()).await.unwrap();
646        assert_eq!(stored.url, "http://updated.example.com/webhook");
647    }
648
649    #[tokio::test]
650    #[ignore = "Requires active Redis instance"]
651    async fn test_delete_notification() {
652        let repo = setup_test_repo().await;
653        let random_id = Uuid::new_v4().to_string();
654        let notification = create_test_notification(&random_id);
655
656        // Create the notification first
657        repo.create(notification).await.unwrap();
658
659        // Verify it exists
660        let stored = repo.get_by_id(random_id.to_string()).await.unwrap();
661        assert_eq!(stored.id, random_id);
662
663        // Delete the notification
664        repo.delete_by_id(random_id.to_string()).await.unwrap();
665
666        // Verify it's gone
667        let result = repo.get_by_id(random_id.to_string()).await;
668        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
669    }
670
671    #[tokio::test]
672    #[ignore = "Requires active Redis instance"]
673    async fn test_list_paginated() {
674        let repo = setup_test_repo().await;
675
676        // Create multiple notifications
677        for i in 1..=10 {
678            let random_id = Uuid::new_v4().to_string();
679            let notification =
680                create_test_notification_with_url(&random_id, &format!("http://test{}.com", i));
681            repo.create(notification).await.unwrap();
682        }
683
684        // Test first page with 3 items per page
685        let query = PaginationQuery {
686            page: 1,
687            per_page: 3,
688        };
689        let result = repo.list_paginated(query).await.unwrap();
690        assert_eq!(result.items.len(), 3);
691        assert!(result.total >= 10);
692        assert_eq!(result.page, 1);
693        assert_eq!(result.per_page, 3);
694
695        // Test empty page (beyond total items)
696        let query = PaginationQuery {
697            page: 1000,
698            per_page: 3,
699        };
700        let result = repo.list_paginated(query).await.unwrap();
701        assert_eq!(result.items.len(), 0);
702    }
703
704    #[tokio::test]
705    #[ignore = "Requires active Redis instance"]
706    async fn test_debug_implementation() {
707        let repo = setup_test_repo().await;
708        let debug_str = format!("{:?}", repo);
709        assert!(debug_str.contains("RedisNotificationRepository"));
710        assert!(debug_str.contains("test_prefix"));
711    }
712
713    #[tokio::test]
714    #[ignore = "Requires active Redis instance"]
715    async fn test_error_handling_empty_id() {
716        let repo = setup_test_repo().await;
717
718        let result = repo.get_by_id("".to_string()).await;
719        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
720    }
721
722    #[tokio::test]
723    #[ignore = "Requires active Redis instance"]
724    async fn test_pagination_validation() {
725        let repo = setup_test_repo().await;
726
727        let query = PaginationQuery {
728            page: 1,
729            per_page: 0,
730        };
731        let result = repo.list_paginated(query).await;
732        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
733    }
734
735    #[tokio::test]
736    #[ignore = "Requires active Redis instance"]
737    async fn test_update_nonexistent_notification() {
738        let repo = setup_test_repo().await;
739        let random_id = Uuid::new_v4().to_string();
740        let notification = create_test_notification(&random_id);
741
742        let result = repo.update(random_id.to_string(), notification).await;
743        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
744    }
745
746    #[tokio::test]
747    #[ignore = "Requires active Redis instance"]
748    async fn test_delete_nonexistent_notification() {
749        let repo = setup_test_repo().await;
750        let random_id = Uuid::new_v4().to_string();
751
752        let result = repo.delete_by_id(random_id.to_string()).await;
753        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
754    }
755
756    #[tokio::test]
757    #[ignore = "Requires active Redis instance"]
758    async fn test_update_with_empty_id() {
759        let repo = setup_test_repo().await;
760        let notification = create_test_notification("test-id");
761
762        let result = repo.update("".to_string(), notification).await;
763        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
764    }
765
766    #[tokio::test]
767    #[ignore = "Requires active Redis instance"]
768    async fn test_delete_with_empty_id() {
769        let repo = setup_test_repo().await;
770
771        let result = repo.delete_by_id("".to_string()).await;
772        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
773    }
774
775    #[tokio::test]
776    #[ignore = "Requires active Redis instance"]
777    async fn test_update_with_mismatched_id() {
778        let repo = setup_test_repo().await;
779        let random_id = Uuid::new_v4().to_string();
780        let notification = create_test_notification(&random_id);
781
782        // Create the notification first
783        repo.create(notification.clone()).await.unwrap();
784
785        // Try to update with mismatched ID
786        let result = repo.update("different-id".to_string(), notification).await;
787        assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
788    }
789
790    #[tokio::test]
791    #[ignore = "Requires active Redis instance"]
792    async fn test_delete_maintains_list_consistency() {
793        let repo = setup_test_repo().await;
794        let random_id = Uuid::new_v4().to_string();
795        let notification = create_test_notification(&random_id);
796
797        // Create the notification
798        repo.create(notification).await.unwrap();
799
800        // Verify it's in the list
801        let all_notifications = repo.list_all().await.unwrap();
802        assert!(all_notifications.iter().any(|n| n.id == random_id));
803
804        // Delete the notification
805        repo.delete_by_id(random_id.to_string()).await.unwrap();
806
807        // Verify it's no longer in the list
808        let all_notifications = repo.list_all().await.unwrap();
809        assert!(!all_notifications.iter().any(|n| n.id == random_id));
810    }
811
812    // test has_entries
813    #[tokio::test]
814    #[ignore = "Requires active Redis instance"]
815    async fn test_has_entries() {
816        let repo = setup_test_repo().await;
817        assert!(!repo.has_entries().await.unwrap());
818
819        let notification = create_test_notification("test");
820        repo.create(notification.clone()).await.unwrap();
821        assert!(repo.has_entries().await.unwrap());
822    }
823
824    #[tokio::test]
825    #[ignore = "Requires active Redis instance"]
826    async fn test_drop_all_entries() {
827        let repo = setup_test_repo().await;
828        let notification = create_test_notification("test");
829
830        repo.create(notification.clone()).await.unwrap();
831        assert!(repo.has_entries().await.unwrap());
832
833        repo.drop_all_entries().await.unwrap();
834        assert!(!repo.has_entries().await.unwrap());
835    }
836}