1use crate::models::{NotificationRepoModel, PaginationQuery, RepositoryError};
4use crate::repositories::redis_base::RedisRepository;
5use crate::repositories::{BatchRetrievalResult, PaginatedResult, Repository};
6use crate::utils::{EncryptionContext, RedisConnections};
7use async_trait::async_trait;
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 connections: Arc<RedisConnections>,
19 pub key_prefix: String,
20}
21
22impl RedisRepository for RedisNotificationRepository {}
23
24impl RedisNotificationRepository {
25 pub fn new(
26 connections: Arc<RedisConnections>,
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 connections,
37 key_prefix,
38 })
39 }
40
41 fn notification_key(&self, notification_id: &str) -> String {
43 format!(
44 "{}:{}:{}",
45 self.key_prefix, NOTIFICATION_PREFIX, notification_id
46 )
47 }
48
49 fn notification_list_key(&self) -> String {
51 format!("{}:{}", self.key_prefix, NOTIFICATION_LIST_KEY)
52 }
53
54 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
68 .get_connection(self.connections.reader(), "get_by_ids")
69 .await?;
70 let keys: Vec<String> = ids.iter().map(|id| self.notification_key(id)).collect();
71
72 debug!(count = %keys.len(), "batch fetching notification data");
73
74 let values: Vec<Option<String>> = conn
75 .mget(&keys)
76 .await
77 .map_err(|e| self.map_redis_error(e, "batch_fetch_notifications"))?;
78
79 let mut notifications = Vec::new();
80 let mut failed_count = 0;
81 let mut failed_ids = Vec::new();
82 for (i, value) in values.into_iter().enumerate() {
83 match value {
84 Some(json) => {
85 let key = keys[i].clone();
87 match EncryptionContext::with_aad_sync(key, || {
88 self.deserialize_entity::<NotificationRepoModel>(
89 &json,
90 &ids[i],
91 "notification",
92 )
93 }) {
94 Ok(notification) => notifications.push(notification),
95 Err(e) => {
96 failed_count += 1;
97 error!(error = %e, "failed to deserialize notification");
98 failed_ids.push(ids[i].clone());
99 }
101 }
102 }
103 None => {
104 warn!("notification not found in batch fetch");
105 }
106 }
107 }
108
109 if failed_count > 0 {
110 warn!(failed_count = %failed_count, total_count = %ids.len(), failed_ids = ?failed_ids, "failed to deserialize notifications in batch");
111 }
112
113 debug!(count = %notifications.len(), "successfully fetched notifications");
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("pool", &"<Pool>")
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
151 .get_connection(self.connections.primary(), "create")
152 .await?;
153
154 debug!("creating notification");
155
156 let value = EncryptionContext::with_aad_sync(key.clone(), || {
158 self.serialize_entity(&entity, |n| &n.id, "notification")
159 })?;
160
161 let existing: Option<String> = conn
163 .get(&key)
164 .await
165 .map_err(|e| self.map_redis_error(e, "create_notification_check"))?;
166
167 if existing.is_some() {
168 return Err(RepositoryError::ConstraintViolation(format!(
169 "Notification with ID '{}' already exists",
170 entity.id
171 )));
172 }
173
174 let mut pipe = redis::pipe();
176 pipe.atomic();
177 pipe.set(&key, &value);
178 pipe.sadd(¬ification_list_key, &entity.id);
179
180 pipe.exec_async(&mut conn)
181 .await
182 .map_err(|e| self.map_redis_error(e, "create_notification"))?;
183
184 debug!("successfully created notification");
185 Ok(entity)
186 }
187
188 async fn get_by_id(&self, id: String) -> Result<NotificationRepoModel, RepositoryError> {
189 if id.is_empty() {
190 return Err(RepositoryError::InvalidData(
191 "Notification ID cannot be empty".to_string(),
192 ));
193 }
194
195 let mut conn = self
196 .get_connection(self.connections.reader(), "get_by_id")
197 .await?;
198 let key = self.notification_key(&id);
199
200 debug!("fetching notification");
201
202 let value: Option<String> = conn
203 .get(&key)
204 .await
205 .map_err(|e| self.map_redis_error(e, "get_notification_by_id"))?;
206
207 match value {
208 Some(json) => {
209 let notification = EncryptionContext::with_aad_sync(key, || {
211 self.deserialize_entity::<NotificationRepoModel>(&json, &id, "notification")
212 })?;
213 debug!("successfully fetched notification");
214 Ok(notification)
215 }
216 None => {
217 debug!("notification not found");
218 Err(RepositoryError::NotFound(format!(
219 "Notification with ID '{id}' not found"
220 )))
221 }
222 }
223 }
224
225 async fn list_all(&self) -> Result<Vec<NotificationRepoModel>, RepositoryError> {
226 let notification_ids = {
227 let mut conn = self
228 .get_connection(self.connections.reader(), "list_all")
229 .await?;
230 let notification_list_key = self.notification_list_key();
231
232 debug!("fetching all notification IDs");
233
234 let ids: Vec<String> = conn
235 .smembers(¬ification_list_key)
236 .await
237 .map_err(|e| self.map_redis_error(e, "list_all_notification_ids"))?;
238
239 debug!(count = %ids.len(), "found notification IDs");
240 ids
241 };
243
244 let notifications = self.get_notifications_by_ids(¬ification_ids).await?;
245 Ok(notifications.results)
246 }
247
248 async fn list_paginated(
249 &self,
250 query: PaginationQuery,
251 ) -> Result<PaginatedResult<NotificationRepoModel>, RepositoryError> {
252 if query.per_page == 0 {
253 return Err(RepositoryError::InvalidData(
254 "per_page must be greater than 0".to_string(),
255 ));
256 }
257
258 let (total, page_ids) = {
259 let mut conn = self
260 .get_connection(self.connections.reader(), "list_paginated")
261 .await?;
262 let notification_list_key = self.notification_list_key();
263
264 debug!(page = %query.page, per_page = %query.per_page, "fetching paginated notifications");
265
266 let all_notification_ids: Vec<String> = conn
267 .smembers(¬ification_list_key)
268 .await
269 .map_err(|e| self.map_redis_error(e, "list_paginated_notification_ids"))?;
270
271 let total = all_notification_ids.len() as u64;
272 let start = ((query.page - 1) * query.per_page) as usize;
273 let end = (start + query.per_page as usize).min(all_notification_ids.len());
274
275 if start >= all_notification_ids.len() {
276 debug!(page = %query.page, total = %total, "page is beyond available data");
277 return Ok(PaginatedResult {
278 items: vec![],
279 total,
280 page: query.page,
281 per_page: query.per_page,
282 });
283 }
284
285 (total, all_notification_ids[start..end].to_vec())
286 };
288
289 let items = self.get_notifications_by_ids(&page_ids).await?;
290
291 debug!(count = %items.results.len(), page = %query.page, "successfully fetched notifications for page");
292
293 Ok(PaginatedResult {
294 items: items.results.clone(),
295 total,
296 page: query.page,
297 per_page: query.per_page,
298 })
299 }
300
301 async fn update(
302 &self,
303 id: String,
304 entity: NotificationRepoModel,
305 ) -> Result<NotificationRepoModel, RepositoryError> {
306 if id.is_empty() {
307 return Err(RepositoryError::InvalidData(
308 "Notification ID cannot be empty".to_string(),
309 ));
310 }
311
312 if id != entity.id {
313 return Err(RepositoryError::InvalidData(
314 "Notification ID in URL does not match entity ID".to_string(),
315 ));
316 }
317
318 let key = self.notification_key(&id);
319 let mut conn = self
320 .get_connection(self.connections.primary(), "update")
321 .await?;
322
323 debug!("updating notification");
324
325 let existing: Option<String> = conn
327 .get(&key)
328 .await
329 .map_err(|e| self.map_redis_error(e, "update_notification_check"))?;
330
331 if existing.is_none() {
332 return Err(RepositoryError::NotFound(format!(
333 "Notification with ID '{id}' not found"
334 )));
335 }
336
337 let value = EncryptionContext::with_aad_sync(key.clone(), || {
339 self.serialize_entity(&entity, |n| &n.id, "notification")
340 })?;
341
342 let _: () = conn
344 .set(&key, value)
345 .await
346 .map_err(|e| self.map_redis_error(e, "update_notification"))?;
347
348 debug!("successfully updated notification");
349 Ok(entity)
350 }
351
352 async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
353 if id.is_empty() {
354 return Err(RepositoryError::InvalidData(
355 "Notification ID cannot be empty".to_string(),
356 ));
357 }
358
359 let key = self.notification_key(&id);
360 let notification_list_key = self.notification_list_key();
361 let mut conn = self
362 .get_connection(self.connections.primary(), "delete_by_id")
363 .await?;
364
365 debug!("deleting notification");
366
367 let existing: Option<String> = conn
369 .get(&key)
370 .await
371 .map_err(|e| self.map_redis_error(e, "delete_notification_check"))?;
372
373 if existing.is_none() {
374 return Err(RepositoryError::NotFound(format!(
375 "Notification with ID '{id}' not found"
376 )));
377 }
378
379 let mut pipe = redis::pipe();
381 pipe.atomic();
382 pipe.del(&key);
383 pipe.srem(¬ification_list_key, &id);
384
385 pipe.exec_async(&mut conn)
386 .await
387 .map_err(|e| self.map_redis_error(e, "delete_notification"))?;
388
389 debug!("successfully deleted notification");
390 Ok(())
391 }
392
393 async fn count(&self) -> Result<usize, RepositoryError> {
394 let mut conn = self
395 .get_connection(self.connections.reader(), "count")
396 .await?;
397 let notification_list_key = self.notification_list_key();
398
399 debug!("counting notifications");
400
401 let count: u64 = conn
402 .scard(¬ification_list_key)
403 .await
404 .map_err(|e| self.map_redis_error(e, "count_notifications"))?;
405
406 debug!(count = %count, "notification count");
407 Ok(count as usize)
408 }
409
410 async fn has_entries(&self) -> Result<bool, RepositoryError> {
411 let mut conn = self
412 .get_connection(self.connections.reader(), "has_entries")
413 .await?;
414 let notification_list_key = self.notification_list_key();
415
416 debug!("checking if notification entries exist");
417
418 let exists: bool = conn
419 .exists(¬ification_list_key)
420 .await
421 .map_err(|e| self.map_redis_error(e, "has_entries_check"))?;
422
423 debug!(exists = %exists, "notification entries exist");
424 Ok(exists)
425 }
426
427 async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
428 let mut conn = self
429 .get_connection(self.connections.primary(), "drop_all_entries")
430 .await?;
431 let notification_list_key = self.notification_list_key();
432
433 debug!("dropping all notification entries");
434
435 let notification_ids: Vec<String> = conn
437 .smembers(¬ification_list_key)
438 .await
439 .map_err(|e| self.map_redis_error(e, "drop_all_entries_get_ids"))?;
440
441 if notification_ids.is_empty() {
442 debug!("no notification entries to drop");
443 return Ok(());
444 }
445
446 let mut pipe = redis::pipe();
448 pipe.atomic();
449
450 for notification_id in ¬ification_ids {
452 let notification_key = self.notification_key(notification_id);
453 pipe.del(¬ification_key);
454 }
455
456 pipe.del(¬ification_list_key);
458
459 pipe.exec_async(&mut conn)
460 .await
461 .map_err(|e| self.map_redis_error(e, "drop_all_entries_pipeline"))?;
462
463 debug!(count = %notification_ids.len(), "dropped notification entries");
464 Ok(())
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use crate::models::NotificationType;
472 use tokio;
473 use uuid::Uuid;
474
475 fn create_test_notification(id: &str) -> NotificationRepoModel {
477 NotificationRepoModel {
478 id: id.to_string(),
479 notification_type: NotificationType::Webhook,
480 url: "http://localhost:8080/webhook".to_string(),
481 signing_key: None,
482 }
483 }
484
485 fn create_test_notification_with_url(id: &str, url: &str) -> NotificationRepoModel {
486 NotificationRepoModel {
487 id: id.to_string(),
488 notification_type: NotificationType::Webhook,
489 url: url.to_string(),
490 signing_key: None,
491 }
492 }
493
494 async fn setup_test_repo() -> RedisNotificationRepository {
495 let redis_url = std::env::var("REDIS_TEST_URL")
497 .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
498
499 let cfg = deadpool_redis::Config::from_url(&redis_url);
500 let pool = Arc::new(
501 cfg.builder()
502 .expect("Failed to create pool builder")
503 .max_size(16)
504 .runtime(deadpool_redis::Runtime::Tokio1)
505 .build()
506 .expect("Failed to build Redis pool"),
507 );
508 let connections = Arc::new(RedisConnections::new_single_pool(pool));
509
510 let random_id = uuid::Uuid::new_v4().to_string();
511 let key_prefix = format!("test_prefix:{random_id}");
512
513 RedisNotificationRepository::new(connections, key_prefix)
514 .expect("Failed to create RedisNotificationRepository")
515 }
516
517 #[tokio::test]
518 #[ignore = "Requires active Redis instance"]
519 async fn test_new_repository_creation() {
520 let repo = setup_test_repo().await;
521 assert!(repo.key_prefix.contains("test_prefix"));
522 }
523
524 #[tokio::test]
525 #[ignore = "Requires active Redis instance"]
526 async fn test_new_repository_empty_prefix_fails() {
527 let redis_url = std::env::var("REDIS_TEST_URL")
528 .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
529 let cfg = deadpool_redis::Config::from_url(&redis_url);
530 let pool = Arc::new(
531 cfg.builder()
532 .expect("Failed to create pool builder")
533 .max_size(16)
534 .runtime(deadpool_redis::Runtime::Tokio1)
535 .build()
536 .expect("Failed to build Redis pool"),
537 );
538 let connections = Arc::new(RedisConnections::new_single_pool(pool));
539
540 let result = RedisNotificationRepository::new(connections, "".to_string());
541 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
542 }
543
544 #[tokio::test]
545 #[ignore = "Requires active Redis instance"]
546 async fn test_key_generation() {
547 let repo = setup_test_repo().await;
548
549 let notification_key = repo.notification_key("test-id");
550 assert!(notification_key.contains(":notification:test-id"));
551
552 let list_key = repo.notification_list_key();
553 assert!(list_key.contains(":notification_list"));
554 }
555
556 #[tokio::test]
557 #[ignore = "Requires active Redis instance"]
558 async fn test_serialize_deserialize_notification() {
559 let repo = setup_test_repo().await;
560 let random_id = Uuid::new_v4().to_string();
561 let notification = create_test_notification(&random_id);
562 let test_key = format!("test_prefix:notification:{random_id}");
563
564 let serialized = EncryptionContext::with_aad_sync(test_key.clone(), || {
565 repo.serialize_entity(¬ification, |n| &n.id, "notification")
566 })
567 .expect("Serialization should succeed");
568
569 let deserialized: NotificationRepoModel =
570 EncryptionContext::with_aad_sync(test_key, || {
571 repo.deserialize_entity(&serialized, &random_id, "notification")
572 })
573 .expect("Deserialization should succeed");
574
575 assert_eq!(notification.id, deserialized.id);
576 assert_eq!(
577 notification.notification_type,
578 deserialized.notification_type
579 );
580 assert_eq!(notification.url, deserialized.url);
581 }
582
583 #[tokio::test]
584 #[ignore = "Requires active Redis instance"]
585 async fn test_create_notification() {
586 let repo = setup_test_repo().await;
587 let random_id = Uuid::new_v4().to_string();
588 let notification = create_test_notification(&random_id);
589
590 let result = repo.create(notification.clone()).await.unwrap();
591 assert_eq!(result.id, notification.id);
592 assert_eq!(result.url, notification.url);
593 }
594
595 #[tokio::test]
596 #[ignore = "Requires active Redis instance"]
597 async fn test_get_notification() {
598 let repo = setup_test_repo().await;
599 let random_id = Uuid::new_v4().to_string();
600 let notification = create_test_notification(&random_id);
601
602 repo.create(notification.clone()).await.unwrap();
603 let stored = repo.get_by_id(random_id.to_string()).await.unwrap();
604 assert_eq!(stored.id, notification.id);
605 assert_eq!(stored.url, notification.url);
606 }
607
608 #[tokio::test]
609 #[ignore = "Requires active Redis instance"]
610 async fn test_list_all_notifications() {
611 let repo = setup_test_repo().await;
612 let random_id = Uuid::new_v4().to_string();
613 let random_id2 = Uuid::new_v4().to_string();
614
615 let notification1 = create_test_notification(&random_id);
616 let notification2 = create_test_notification(&random_id2);
617
618 repo.create(notification1).await.unwrap();
619 repo.create(notification2).await.unwrap();
620
621 let notifications = repo.list_all().await.unwrap();
622 assert!(notifications.len() >= 2);
623 }
624
625 #[tokio::test]
626 #[ignore = "Requires active Redis instance"]
627 async fn test_count_notifications() {
628 let repo = setup_test_repo().await;
629 let random_id = Uuid::new_v4().to_string();
630 let notification = create_test_notification(&random_id);
631
632 let count = repo.count().await.unwrap();
633 repo.create(notification).await.unwrap();
634 assert!(repo.count().await.unwrap() > count);
635 }
636
637 #[tokio::test]
638 #[ignore = "Requires active Redis instance"]
639 async fn test_get_nonexistent_notification() {
640 let repo = setup_test_repo().await;
641 let result = repo.get_by_id("nonexistent".to_string()).await;
642 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
643 }
644
645 #[tokio::test]
646 #[ignore = "Requires active Redis instance"]
647 async fn test_duplicate_notification_creation() {
648 let repo = setup_test_repo().await;
649 let random_id = Uuid::new_v4().to_string();
650
651 let notification = create_test_notification(&random_id);
652
653 repo.create(notification.clone()).await.unwrap();
654 let result = repo.create(notification).await;
655
656 assert!(matches!(
657 result,
658 Err(RepositoryError::ConstraintViolation(_))
659 ));
660 }
661
662 #[tokio::test]
663 #[ignore = "Requires active Redis instance"]
664 async fn test_update_notification() {
665 let repo = setup_test_repo().await;
666 let random_id = Uuid::new_v4().to_string();
667 let mut notification = create_test_notification(&random_id);
668
669 repo.create(notification.clone()).await.unwrap();
671
672 notification.url = "http://updated.example.com/webhook".to_string();
674 let result = repo
675 .update(random_id.to_string(), notification.clone())
676 .await
677 .unwrap();
678 assert_eq!(result.url, "http://updated.example.com/webhook");
679
680 let stored = repo.get_by_id(random_id.to_string()).await.unwrap();
682 assert_eq!(stored.url, "http://updated.example.com/webhook");
683 }
684
685 #[tokio::test]
686 #[ignore = "Requires active Redis instance"]
687 async fn test_delete_notification() {
688 let repo = setup_test_repo().await;
689 let random_id = Uuid::new_v4().to_string();
690 let notification = create_test_notification(&random_id);
691
692 repo.create(notification).await.unwrap();
694
695 let stored = repo.get_by_id(random_id.to_string()).await.unwrap();
697 assert_eq!(stored.id, random_id);
698
699 repo.delete_by_id(random_id.to_string()).await.unwrap();
701
702 let result = repo.get_by_id(random_id.to_string()).await;
704 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
705 }
706
707 #[tokio::test]
708 #[ignore = "Requires active Redis instance"]
709 async fn test_list_paginated() {
710 let repo = setup_test_repo().await;
711
712 for i in 1..=10 {
714 let random_id = Uuid::new_v4().to_string();
715 let notification =
716 create_test_notification_with_url(&random_id, &format!("http://test{i}.com"));
717 repo.create(notification).await.unwrap();
718 }
719
720 let query = PaginationQuery {
722 page: 1,
723 per_page: 3,
724 };
725 let result = repo.list_paginated(query).await.unwrap();
726 assert_eq!(result.items.len(), 3);
727 assert!(result.total >= 10);
728 assert_eq!(result.page, 1);
729 assert_eq!(result.per_page, 3);
730
731 let query = PaginationQuery {
733 page: 1000,
734 per_page: 3,
735 };
736 let result = repo.list_paginated(query).await.unwrap();
737 assert_eq!(result.items.len(), 0);
738 }
739
740 #[tokio::test]
741 #[ignore = "Requires active Redis instance"]
742 async fn test_debug_implementation() {
743 let repo = setup_test_repo().await;
744 let debug_str = format!("{repo:?}");
745 assert!(debug_str.contains("RedisNotificationRepository"));
746 assert!(debug_str.contains("test_prefix"));
747 }
748
749 #[tokio::test]
750 #[ignore = "Requires active Redis instance"]
751 async fn test_error_handling_empty_id() {
752 let repo = setup_test_repo().await;
753
754 let result = repo.get_by_id("".to_string()).await;
755 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
756 }
757
758 #[tokio::test]
759 #[ignore = "Requires active Redis instance"]
760 async fn test_pagination_validation() {
761 let repo = setup_test_repo().await;
762
763 let query = PaginationQuery {
764 page: 1,
765 per_page: 0,
766 };
767 let result = repo.list_paginated(query).await;
768 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
769 }
770
771 #[tokio::test]
772 #[ignore = "Requires active Redis instance"]
773 async fn test_update_nonexistent_notification() {
774 let repo = setup_test_repo().await;
775 let random_id = Uuid::new_v4().to_string();
776 let notification = create_test_notification(&random_id);
777
778 let result = repo.update(random_id.to_string(), notification).await;
779 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
780 }
781
782 #[tokio::test]
783 #[ignore = "Requires active Redis instance"]
784 async fn test_delete_nonexistent_notification() {
785 let repo = setup_test_repo().await;
786 let random_id = Uuid::new_v4().to_string();
787
788 let result = repo.delete_by_id(random_id.to_string()).await;
789 assert!(matches!(result, Err(RepositoryError::NotFound(_))));
790 }
791
792 #[tokio::test]
793 #[ignore = "Requires active Redis instance"]
794 async fn test_update_with_empty_id() {
795 let repo = setup_test_repo().await;
796 let notification = create_test_notification("test-id");
797
798 let result = repo.update("".to_string(), notification).await;
799 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
800 }
801
802 #[tokio::test]
803 #[ignore = "Requires active Redis instance"]
804 async fn test_delete_with_empty_id() {
805 let repo = setup_test_repo().await;
806
807 let result = repo.delete_by_id("".to_string()).await;
808 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
809 }
810
811 #[tokio::test]
812 #[ignore = "Requires active Redis instance"]
813 async fn test_update_with_mismatched_id() {
814 let repo = setup_test_repo().await;
815 let random_id = Uuid::new_v4().to_string();
816 let notification = create_test_notification(&random_id);
817
818 repo.create(notification.clone()).await.unwrap();
820
821 let result = repo.update("different-id".to_string(), notification).await;
823 assert!(matches!(result, Err(RepositoryError::InvalidData(_))));
824 }
825
826 #[tokio::test]
827 #[ignore = "Requires active Redis instance"]
828 async fn test_delete_maintains_list_consistency() {
829 let repo = setup_test_repo().await;
830 let random_id = Uuid::new_v4().to_string();
831 let notification = create_test_notification(&random_id);
832
833 repo.create(notification).await.unwrap();
835
836 let all_notifications = repo.list_all().await.unwrap();
838 assert!(all_notifications.iter().any(|n| n.id == random_id));
839
840 repo.delete_by_id(random_id.to_string()).await.unwrap();
842
843 let all_notifications = repo.list_all().await.unwrap();
845 assert!(!all_notifications.iter().any(|n| n.id == random_id));
846 }
847
848 #[tokio::test]
850 #[ignore = "Requires active Redis instance"]
851 async fn test_has_entries() {
852 let repo = setup_test_repo().await;
853 assert!(!repo.has_entries().await.unwrap());
854
855 let notification = create_test_notification("test");
856 repo.create(notification.clone()).await.unwrap();
857 assert!(repo.has_entries().await.unwrap());
858 }
859
860 #[tokio::test]
861 #[ignore = "Requires active Redis instance"]
862 async fn test_drop_all_entries() {
863 let repo = setup_test_repo().await;
864 let notification = create_test_notification("test");
865
866 repo.create(notification.clone()).await.unwrap();
867 assert!(repo.has_entries().await.unwrap());
868
869 repo.drop_all_entries().await.unwrap();
870 assert!(!repo.has_entries().await.unwrap());
871 }
872}