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