openzeppelin_relayer/repositories/
redis_base.rs

1//! Base Redis repository functionality shared across all Redis implementations.
2//!
3//! This module provides common utilities and patterns used by all Redis repository
4//! implementations to reduce code duplication and ensure consistency.
5
6use crate::models::RepositoryError;
7use deadpool_redis::{Connection, Pool, PoolError, TimeoutType};
8use redis::RedisError;
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use tracing::{error, warn};
12
13/// Base trait for Redis repositories providing common functionality
14pub trait RedisRepository {
15    fn serialize_entity<T, F>(
16        &self,
17        entity: &T,
18        id_extractor: F,
19        entity_type: &str,
20    ) -> Result<String, RepositoryError>
21    where
22        T: Serialize,
23        F: Fn(&T) -> &str,
24    {
25        serde_json::to_string(entity).map_err(|e| {
26            let id = id_extractor(entity);
27            error!(entity_type = %entity_type, id = %id, error = %e, "serialization failed");
28            RepositoryError::InvalidData(format!("Failed to serialize {entity_type} {id}: {e}"))
29        })
30    }
31
32    /// Deserialize entity with detailed error context
33    /// Default implementation that works for any Deserialize type
34    fn deserialize_entity<T>(
35        &self,
36        json: &str,
37        entity_id: &str,
38        entity_type: &str,
39    ) -> Result<T, RepositoryError>
40    where
41        T: for<'de> Deserialize<'de>,
42    {
43        serde_json::from_str(json).map_err(|e| {
44            error!(entity_type = %entity_type, entity_id = %entity_id, error = %e, "deserialization failed");
45            RepositoryError::InvalidData(format!(
46                "Failed to deserialize {} {}: {} (JSON length: {})",
47                entity_type,
48                entity_id,
49                e,
50                json.len()
51            ))
52        })
53    }
54
55    /// Convert Redis errors to appropriate RepositoryError types
56    fn map_redis_error(&self, error: RedisError, context: &str) -> RepositoryError {
57        warn!(context = %context, error = %error, "redis operation failed");
58
59        match error.kind() {
60            redis::ErrorKind::TypeError => RepositoryError::InvalidData(format!(
61                "Redis data type error in operation '{context}': {error}"
62            )),
63            redis::ErrorKind::AuthenticationFailed => {
64                RepositoryError::InvalidData("Redis authentication failed".to_string())
65            }
66            redis::ErrorKind::NoScriptError => RepositoryError::InvalidData(format!(
67                "Redis script error in operation '{context}': {error}"
68            )),
69            redis::ErrorKind::ReadOnly => RepositoryError::InvalidData(format!(
70                "Redis is read-only in operation '{context}': {error}"
71            )),
72            redis::ErrorKind::ExecAbortError => RepositoryError::InvalidData(format!(
73                "Redis transaction aborted in operation '{context}': {error}"
74            )),
75            redis::ErrorKind::BusyLoadingError => RepositoryError::InvalidData(format!(
76                "Redis is busy in operation '{context}': {error}"
77            )),
78            redis::ErrorKind::ExtensionError => RepositoryError::InvalidData(format!(
79                "Redis extension error in operation '{context}': {error}"
80            )),
81            // Default to Other for connection errors and other issues
82            _ => RepositoryError::Other(format!("Redis operation '{context}' failed: {error}")),
83        }
84    }
85
86    /// Convert deadpool Pool errors to appropriate RepositoryError types
87    fn map_pool_error(&self, error: PoolError, context: &str) -> RepositoryError {
88        error!(context = %context, error = %error, "redis pool operation failed");
89
90        match error {
91            PoolError::Timeout(timeout) => {
92                let detail = match timeout {
93                    TimeoutType::Wait => "waiting for an available connection",
94                    TimeoutType::Create => "creating a new connection",
95                    TimeoutType::Recycle => "recycling a connection",
96                };
97                RepositoryError::ConnectionError(format!(
98                    "Redis pool timeout while {detail} in operation '{context}'"
99                ))
100            }
101            PoolError::Backend(redis_err) => self.map_redis_error(redis_err, context),
102            PoolError::Closed => {
103                RepositoryError::ConnectionError("Redis pool is closed".to_string())
104            }
105            PoolError::NoRuntimeSpecified => {
106                RepositoryError::ConnectionError("Redis pool has no runtime specified".to_string())
107            }
108            other => RepositoryError::ConnectionError(format!(
109                "Redis pool error in operation '{context}': {other}"
110            )),
111        }
112    }
113
114    /// Get a connection from the Redis pool with error handling
115    ///
116    /// # Arguments
117    ///
118    /// * `pool` - Reference to the Redis connection pool
119    /// * `context` - Context string for error messages (e.g., "get_by_id", "create")
120    ///
121    /// # Returns
122    ///
123    /// A connection from the pool, or a RepositoryError if getting the connection fails
124    #[allow(async_fn_in_trait)]
125    async fn get_connection(
126        &self,
127        pool: &Arc<Pool>,
128        context: &str,
129    ) -> Result<Connection, RepositoryError> {
130        pool.get()
131            .await
132            .map_err(|e| self.map_pool_error(e, context))
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use serde::{Deserialize, Serialize};
140
141    // Test structs for serialization/deserialization
142    #[derive(Debug, Serialize, Deserialize, PartialEq)]
143    struct TestEntity {
144        id: String,
145        name: String,
146        value: i32,
147    }
148
149    #[derive(Debug, Serialize, Deserialize, PartialEq)]
150    struct SimpleEntity {
151        id: String,
152    }
153
154    // Test implementation of RedisRepository trait
155    struct TestRedisRepository;
156
157    impl RedisRepository for TestRedisRepository {}
158
159    impl TestRedisRepository {
160        fn new() -> Self {
161            TestRedisRepository
162        }
163    }
164
165    #[test]
166    fn test_serialize_entity_success() {
167        let repo = TestRedisRepository::new();
168        let entity = TestEntity {
169            id: "test-id".to_string(),
170            name: "test-name".to_string(),
171            value: 42,
172        };
173
174        let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
175
176        assert!(result.is_ok());
177        let json = result.unwrap();
178        assert!(json.contains("test-id"));
179        assert!(json.contains("test-name"));
180        assert!(json.contains("42"));
181    }
182
183    #[test]
184    fn test_serialize_entity_with_different_id_extractor() {
185        let repo = TestRedisRepository::new();
186        let entity = TestEntity {
187            id: "test-id".to_string(),
188            name: "test-name".to_string(),
189            value: 42,
190        };
191
192        // Use name as ID extractor
193        let result = repo.serialize_entity(&entity, |e| &e.name, "TestEntity");
194
195        assert!(result.is_ok());
196        let json = result.unwrap();
197
198        // Should still serialize the entire entity
199        assert!(json.contains("test-id"));
200        assert!(json.contains("test-name"));
201        assert!(json.contains("42"));
202    }
203
204    #[test]
205    fn test_serialize_entity_simple_struct() {
206        let repo = TestRedisRepository::new();
207        let entity = SimpleEntity {
208            id: "simple-id".to_string(),
209        };
210
211        let result = repo.serialize_entity(&entity, |e| &e.id, "SimpleEntity");
212
213        assert!(result.is_ok());
214        let json = result.unwrap();
215        assert!(json.contains("simple-id"));
216    }
217
218    #[test]
219    fn test_deserialize_entity_success() {
220        let repo = TestRedisRepository::new();
221        let json = r#"{"id":"test-id","name":"test-name","value":42}"#;
222
223        let result: Result<TestEntity, RepositoryError> =
224            repo.deserialize_entity(json, "test-id", "TestEntity");
225
226        assert!(result.is_ok());
227        let entity = result.unwrap();
228        assert_eq!(entity.id, "test-id");
229        assert_eq!(entity.name, "test-name");
230        assert_eq!(entity.value, 42);
231    }
232
233    #[test]
234    fn test_deserialize_entity_invalid_json() {
235        let repo = TestRedisRepository::new();
236        let invalid_json = r#"{"id":"test-id","name":"test-name","value":}"#; // Missing value
237
238        let result: Result<TestEntity, RepositoryError> =
239            repo.deserialize_entity(invalid_json, "test-id", "TestEntity");
240
241        assert!(result.is_err());
242        match result.unwrap_err() {
243            RepositoryError::InvalidData(msg) => {
244                assert!(msg.contains("Failed to deserialize TestEntity test-id"));
245                assert!(msg.contains("JSON length:"));
246            }
247            _ => panic!("Expected InvalidData error"),
248        }
249    }
250
251    #[test]
252    fn test_deserialize_entity_invalid_structure() {
253        let repo = TestRedisRepository::new();
254        let json = r#"{"wrongfield":"test-id"}"#;
255
256        let result: Result<TestEntity, RepositoryError> =
257            repo.deserialize_entity(json, "test-id", "TestEntity");
258
259        assert!(result.is_err());
260        match result.unwrap_err() {
261            RepositoryError::InvalidData(msg) => {
262                assert!(msg.contains("Failed to deserialize TestEntity test-id"));
263            }
264            _ => panic!("Expected InvalidData error"),
265        }
266    }
267
268    #[test]
269    fn test_map_redis_error_type_error() {
270        let repo = TestRedisRepository::new();
271        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
272
273        let result = repo.map_redis_error(redis_error, "test_operation");
274
275        match result {
276            RepositoryError::InvalidData(msg) => {
277                assert!(msg.contains("Redis data type error"));
278                assert!(msg.contains("test_operation"));
279            }
280            _ => panic!("Expected InvalidData error"),
281        }
282    }
283
284    #[test]
285    fn test_map_redis_error_authentication_failed() {
286        let repo = TestRedisRepository::new();
287        let redis_error = RedisError::from((redis::ErrorKind::AuthenticationFailed, "Auth failed"));
288
289        let result = repo.map_redis_error(redis_error, "auth_operation");
290
291        match result {
292            RepositoryError::InvalidData(msg) => {
293                assert!(msg.contains("Redis authentication failed"));
294            }
295            _ => panic!("Expected InvalidData error"),
296        }
297    }
298
299    #[test]
300    fn test_map_redis_error_connection_error() {
301        let repo = TestRedisRepository::new();
302        let redis_error = RedisError::from((redis::ErrorKind::IoError, "Connection failed"));
303
304        let result = repo.map_redis_error(redis_error, "connection_operation");
305
306        match result {
307            RepositoryError::Other(msg) => {
308                assert!(msg.contains("Redis operation"));
309                assert!(msg.contains("connection_operation"));
310            }
311            _ => panic!("Expected Other error"),
312        }
313    }
314
315    #[test]
316    fn test_map_redis_error_no_script_error() {
317        let repo = TestRedisRepository::new();
318        let redis_error = RedisError::from((redis::ErrorKind::NoScriptError, "Script not found"));
319
320        let result = repo.map_redis_error(redis_error, "script_operation");
321
322        match result {
323            RepositoryError::InvalidData(msg) => {
324                assert!(msg.contains("Redis script error"));
325                assert!(msg.contains("script_operation"));
326            }
327            _ => panic!("Expected InvalidData error"),
328        }
329    }
330
331    #[test]
332    fn test_map_redis_error_read_only() {
333        let repo = TestRedisRepository::new();
334        let redis_error = RedisError::from((redis::ErrorKind::ReadOnly, "Read only"));
335
336        let result = repo.map_redis_error(redis_error, "write_operation");
337
338        match result {
339            RepositoryError::InvalidData(msg) => {
340                assert!(msg.contains("Redis is read-only"));
341                assert!(msg.contains("write_operation"));
342            }
343            _ => panic!("Expected InvalidData error"),
344        }
345    }
346
347    #[test]
348    fn test_map_redis_error_exec_abort_error() {
349        let repo = TestRedisRepository::new();
350        let redis_error =
351            RedisError::from((redis::ErrorKind::ExecAbortError, "Transaction aborted"));
352
353        let result = repo.map_redis_error(redis_error, "transaction_operation");
354
355        match result {
356            RepositoryError::InvalidData(msg) => {
357                assert!(msg.contains("Redis transaction aborted"));
358                assert!(msg.contains("transaction_operation"));
359            }
360            _ => panic!("Expected InvalidData error"),
361        }
362    }
363
364    #[test]
365    fn test_map_redis_error_busy_error() {
366        let repo = TestRedisRepository::new();
367        let redis_error = RedisError::from((redis::ErrorKind::BusyLoadingError, "Server busy"));
368
369        let result = repo.map_redis_error(redis_error, "busy_operation");
370
371        match result {
372            RepositoryError::InvalidData(msg) => {
373                assert!(msg.contains("Redis is busy"));
374                assert!(msg.contains("busy_operation"));
375            }
376            _ => panic!("Expected InvalidData error"),
377        }
378    }
379
380    #[test]
381    fn test_map_redis_error_extension_error() {
382        let repo = TestRedisRepository::new();
383        let redis_error = RedisError::from((redis::ErrorKind::ExtensionError, "Extension error"));
384
385        let result = repo.map_redis_error(redis_error, "extension_operation");
386
387        match result {
388            RepositoryError::InvalidData(msg) => {
389                assert!(msg.contains("Redis extension error"));
390                assert!(msg.contains("extension_operation"));
391            }
392            _ => panic!("Expected InvalidData error"),
393        }
394    }
395
396    #[test]
397    fn test_map_redis_error_context_propagation() {
398        let repo = TestRedisRepository::new();
399        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
400        let context = "user_repository_get_operation";
401
402        let result = repo.map_redis_error(redis_error, context);
403
404        match result {
405            RepositoryError::InvalidData(msg) => {
406                assert!(msg.contains("Redis data type error"));
407                // Context should be used in logging but not necessarily in the error message
408            }
409            _ => panic!("Expected InvalidData error"),
410        }
411    }
412
413    #[test]
414    fn test_serialize_deserialize_roundtrip() {
415        let repo = TestRedisRepository::new();
416        let original = TestEntity {
417            id: "roundtrip-id".to_string(),
418            name: "roundtrip-name".to_string(),
419            value: 123,
420        };
421
422        // Serialize
423        let json = repo
424            .serialize_entity(&original, |e| &e.id, "TestEntity")
425            .unwrap();
426
427        // Deserialize
428        let deserialized: TestEntity = repo
429            .deserialize_entity(&json, "roundtrip-id", "TestEntity")
430            .unwrap();
431
432        // Should be identical
433        assert_eq!(original, deserialized);
434    }
435
436    #[test]
437    fn test_serialize_deserialize_unicode_content() {
438        let repo = TestRedisRepository::new();
439        let original = TestEntity {
440            id: "unicode-id".to_string(),
441            name: "测试名称 🚀".to_string(),
442            value: 456,
443        };
444
445        // Serialize
446        let json = repo
447            .serialize_entity(&original, |e| &e.id, "TestEntity")
448            .unwrap();
449
450        // Deserialize
451        let deserialized: TestEntity = repo
452            .deserialize_entity(&json, "unicode-id", "TestEntity")
453            .unwrap();
454
455        // Should handle unicode correctly
456        assert_eq!(original, deserialized);
457    }
458
459    #[test]
460    fn test_serialize_entity_with_complex_data() {
461        let repo = TestRedisRepository::new();
462
463        #[derive(Serialize)]
464        struct ComplexEntity {
465            id: String,
466            nested: NestedData,
467            list: Vec<i32>,
468        }
469
470        #[derive(Serialize)]
471        struct NestedData {
472            field1: String,
473            field2: bool,
474        }
475
476        let complex_entity = ComplexEntity {
477            id: "complex-id".to_string(),
478            nested: NestedData {
479                field1: "nested-value".to_string(),
480                field2: true,
481            },
482            list: vec![1, 2, 3],
483        };
484
485        let result = repo.serialize_entity(&complex_entity, |e| &e.id, "ComplexEntity");
486
487        assert!(result.is_ok());
488        let json = result.unwrap();
489        assert!(json.contains("complex-id"));
490        assert!(json.contains("nested-value"));
491        assert!(json.contains("true"));
492        assert!(json.contains("[1,2,3]"));
493    }
494
495    // Test specifically for u128 serialization/deserialization with large values
496    #[test]
497    fn test_serialize_deserialize_u128_large_values() {
498        use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
499
500        #[derive(Serialize, Deserialize, PartialEq, Debug)]
501        struct TestU128Entity {
502            id: String,
503            #[serde(
504                serialize_with = "serialize_optional_u128",
505                deserialize_with = "deserialize_optional_u128",
506                default
507            )]
508            gas_price: Option<u128>,
509            #[serde(
510                serialize_with = "serialize_optional_u128",
511                deserialize_with = "deserialize_optional_u128",
512                default
513            )]
514            max_fee_per_gas: Option<u128>,
515        }
516
517        let repo = TestRedisRepository::new();
518
519        // Test with very large u128 values that would overflow JSON numbers
520        let original = TestU128Entity {
521            id: "u128-test".to_string(),
522            gas_price: Some(u128::MAX), // 340282366920938463463374607431768211455
523            max_fee_per_gas: Some(999999999999999999999999999999999u128),
524        };
525
526        // Serialize
527        let json = repo
528            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
529            .unwrap();
530
531        // Verify it contains string representations, not numbers
532        assert!(json.contains("\"340282366920938463463374607431768211455\""));
533        assert!(json.contains("\"999999999999999999999999999999999\""));
534        // Make sure they're not stored as numbers (which would cause overflow)
535        assert!(!json.contains("3.4028236692093846e+38"));
536
537        // Deserialize
538        let deserialized: TestU128Entity = repo
539            .deserialize_entity(&json, "u128-test", "TestU128Entity")
540            .unwrap();
541
542        // Should be identical
543        assert_eq!(original, deserialized);
544        assert_eq!(deserialized.gas_price, Some(u128::MAX));
545        assert_eq!(
546            deserialized.max_fee_per_gas,
547            Some(999999999999999999999999999999999u128)
548        );
549    }
550
551    #[test]
552    fn test_serialize_deserialize_u128_none_values() {
553        use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
554
555        #[derive(Serialize, Deserialize, PartialEq, Debug)]
556        struct TestU128Entity {
557            id: String,
558            #[serde(
559                serialize_with = "serialize_optional_u128",
560                deserialize_with = "deserialize_optional_u128",
561                default
562            )]
563            gas_price: Option<u128>,
564        }
565
566        let repo = TestRedisRepository::new();
567
568        // Test with None values
569        let original = TestU128Entity {
570            id: "u128-none-test".to_string(),
571            gas_price: None,
572        };
573
574        // Serialize
575        let json = repo
576            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
577            .unwrap();
578
579        // Should contain null
580        assert!(json.contains("null"));
581
582        // Deserialize
583        let deserialized: TestU128Entity = repo
584            .deserialize_entity(&json, "u128-none-test", "TestU128Entity")
585            .unwrap();
586
587        // Should be identical
588        assert_eq!(original, deserialized);
589        assert_eq!(deserialized.gas_price, None);
590    }
591
592    #[test]
593    fn test_map_pool_error_timeout_wait() {
594        let repo = TestRedisRepository::new();
595        let timeout_error = PoolError::Timeout(TimeoutType::Wait);
596
597        let result = repo.map_pool_error(timeout_error, "test_operation");
598
599        match result {
600            RepositoryError::ConnectionError(msg) => {
601                assert!(msg.contains("Redis pool timeout"));
602                assert!(msg.contains("waiting for an available connection"));
603                assert!(msg.contains("test_operation"));
604            }
605            _ => panic!("Expected ConnectionError"),
606        }
607    }
608
609    #[test]
610    fn test_map_pool_error_timeout_create() {
611        let repo = TestRedisRepository::new();
612        let timeout_error = PoolError::Timeout(TimeoutType::Create);
613
614        let result = repo.map_pool_error(timeout_error, "create_operation");
615
616        match result {
617            RepositoryError::ConnectionError(msg) => {
618                assert!(msg.contains("Redis pool timeout"));
619                assert!(msg.contains("creating a new connection"));
620                assert!(msg.contains("create_operation"));
621            }
622            _ => panic!("Expected ConnectionError"),
623        }
624    }
625
626    #[test]
627    fn test_map_pool_error_timeout_recycle() {
628        let repo = TestRedisRepository::new();
629        let timeout_error = PoolError::Timeout(TimeoutType::Recycle);
630
631        let result = repo.map_pool_error(timeout_error, "recycle_operation");
632
633        match result {
634            RepositoryError::ConnectionError(msg) => {
635                assert!(msg.contains("Redis pool timeout"));
636                assert!(msg.contains("recycling a connection"));
637                assert!(msg.contains("recycle_operation"));
638            }
639            _ => panic!("Expected ConnectionError"),
640        }
641    }
642
643    #[test]
644    fn test_map_pool_error_backend() {
645        let repo = TestRedisRepository::new();
646        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Backend error"));
647        let pool_error = PoolError::Backend(redis_error);
648
649        let result = repo.map_pool_error(pool_error, "backend_operation");
650
651        // Should delegate to map_redis_error
652        match result {
653            RepositoryError::InvalidData(msg) => {
654                assert!(msg.contains("Redis data type error"));
655                assert!(msg.contains("backend_operation"));
656            }
657            _ => panic!("Expected InvalidData error from map_redis_error"),
658        }
659    }
660
661    #[test]
662    fn test_map_pool_error_closed() {
663        let repo = TestRedisRepository::new();
664        let pool_error = PoolError::Closed;
665
666        let result = repo.map_pool_error(pool_error, "closed_operation");
667
668        match result {
669            RepositoryError::ConnectionError(msg) => {
670                assert_eq!(msg, "Redis pool is closed");
671            }
672            _ => panic!("Expected ConnectionError"),
673        }
674    }
675
676    #[test]
677    fn test_map_pool_error_no_runtime() {
678        let repo = TestRedisRepository::new();
679        let pool_error = PoolError::NoRuntimeSpecified;
680
681        let result = repo.map_pool_error(pool_error, "runtime_operation");
682
683        match result {
684            RepositoryError::ConnectionError(msg) => {
685                assert_eq!(msg, "Redis pool has no runtime specified");
686            }
687            _ => panic!("Expected ConnectionError"),
688        }
689    }
690
691    #[test]
692    fn test_map_redis_error_empty_context() {
693        let repo = TestRedisRepository::new();
694        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
695
696        let result = repo.map_redis_error(redis_error, "");
697
698        match result {
699            RepositoryError::InvalidData(msg) => {
700                assert!(msg.contains("Redis data type error"));
701            }
702            _ => panic!("Expected InvalidData error"),
703        }
704    }
705
706    #[test]
707    fn test_map_pool_error_empty_context() {
708        let repo = TestRedisRepository::new();
709        let pool_error = PoolError::Closed;
710
711        let result = repo.map_pool_error(pool_error, "");
712
713        match result {
714            RepositoryError::ConnectionError(msg) => {
715                assert_eq!(msg, "Redis pool is closed");
716            }
717            _ => panic!("Expected ConnectionError"),
718        }
719    }
720
721    #[test]
722    fn test_serialize_entity_empty_id() {
723        let repo = TestRedisRepository::new();
724        let entity = TestEntity {
725            id: "".to_string(),
726            name: "test-name".to_string(),
727            value: 42,
728        };
729
730        let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
731
732        assert!(result.is_ok());
733        let json = result.unwrap();
734        assert!(json.contains("\"id\":\"\""));
735    }
736
737    #[test]
738    fn test_deserialize_entity_empty_json() {
739        let repo = TestRedisRepository::new();
740
741        let result: Result<TestEntity, RepositoryError> =
742            repo.deserialize_entity("", "test-id", "TestEntity");
743
744        assert!(result.is_err());
745        match result.unwrap_err() {
746            RepositoryError::InvalidData(msg) => {
747                assert!(msg.contains("Failed to deserialize"));
748                assert!(msg.contains("JSON length: 0"));
749            }
750            _ => panic!("Expected InvalidData error"),
751        }
752    }
753
754    #[test]
755    fn test_deserialize_entity_malformed_json() {
756        let repo = TestRedisRepository::new();
757        let malformed_json = r#"{"id":"test-id","name":"test-name","value":}"#;
758
759        let result: Result<TestEntity, RepositoryError> =
760            repo.deserialize_entity(malformed_json, "test-id", "TestEntity");
761
762        assert!(result.is_err());
763        match result.unwrap_err() {
764            RepositoryError::InvalidData(msg) => {
765                assert!(msg.contains("Failed to deserialize"));
766                assert!(msg.contains("test-id"));
767            }
768            _ => panic!("Expected InvalidData error"),
769        }
770    }
771
772    #[test]
773    fn test_serialize_entity_with_special_characters() {
774        let repo = TestRedisRepository::new();
775        let entity = TestEntity {
776            id: "test-id".to_string(),
777            name: "test\"name\nwith\tspecial\rchars".to_string(),
778            value: 42,
779        };
780
781        let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
782
783        assert!(result.is_ok());
784        let json = result.unwrap();
785        // JSON should properly escape special characters
786        assert!(json.contains("test-id"));
787        // Verify it's valid JSON by deserializing
788        let deserialized: TestEntity = repo
789            .deserialize_entity(&json, "test-id", "TestEntity")
790            .unwrap();
791        assert_eq!(deserialized.name, entity.name);
792    }
793
794    #[test]
795    fn test_serialize_entity_with_numeric_id() {
796        let repo = TestRedisRepository::new();
797        #[derive(Serialize)]
798        struct NumericIdEntity {
799            id: i32,
800            name: String,
801        }
802
803        let entity = NumericIdEntity {
804            id: 12345,
805            name: "test".to_string(),
806        };
807
808        // Use a static string for the ID extractor since we're testing numeric IDs
809        let result = repo.serialize_entity(&entity, |_| "numeric-id", "NumericIdEntity");
810
811        assert!(result.is_ok());
812        let json = result.unwrap();
813        assert!(json.contains("12345"));
814        assert!(json.contains("test")); // Verify the name field is serialized
815    }
816
817    #[test]
818    fn test_map_redis_error_all_error_kinds() {
819        let repo = TestRedisRepository::new();
820        let error_kinds = vec![
821            (redis::ErrorKind::TypeError, "TypeError"),
822            (
823                redis::ErrorKind::AuthenticationFailed,
824                "AuthenticationFailed",
825            ),
826            (redis::ErrorKind::NoScriptError, "NoScriptError"),
827            (redis::ErrorKind::ReadOnly, "ReadOnly"),
828            (redis::ErrorKind::ExecAbortError, "ExecAbortError"),
829            (redis::ErrorKind::BusyLoadingError, "BusyLoadingError"),
830            (redis::ErrorKind::ExtensionError, "ExtensionError"),
831            (redis::ErrorKind::IoError, "IoError"),
832            (redis::ErrorKind::ClientError, "ClientError"),
833        ];
834
835        for (kind, expected_type) in error_kinds {
836            let redis_error = RedisError::from((kind, "test error"));
837            let result = repo.map_redis_error(redis_error, "test_op");
838
839            match result {
840                RepositoryError::InvalidData(_)
841                    if expected_type != "IoError" && expected_type != "ClientError" =>
842                {
843                    // Expected for most error kinds
844                }
845                RepositoryError::Other(_)
846                    if expected_type == "IoError" || expected_type == "ClientError" =>
847                {
848                    // Expected for connection/client errors
849                }
850                _ => panic!("Unexpected error type for {kind:?}"),
851            }
852        }
853    }
854
855    #[test]
856    fn test_serialize_entity_error_message_includes_id() {
857        let repo = TestRedisRepository::new();
858        // Create an entity that will fail serialization by using a custom serializer
859        // Actually, let's test with a valid entity but verify the error message format
860        let entity = TestEntity {
861            id: "error-test-id".to_string(),
862            name: "test-name".to_string(),
863            value: 42,
864        };
865
866        // This should succeed, but let's verify the error message would include the ID
867        // by checking the error handling path
868        let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
869        assert!(result.is_ok());
870
871        // For a real error case, we'd need a type that fails to serialize
872        // But we can verify the error message format is correct from the code
873    }
874
875    #[test]
876    fn test_deserialize_entity_error_message_includes_length() {
877        let repo = TestRedisRepository::new();
878        let short_json = r#"{"id":"test"}"#;
879
880        // Test with short JSON
881        let result: Result<TestEntity, RepositoryError> =
882            repo.deserialize_entity(short_json, "test-id", "TestEntity");
883        assert!(result.is_err());
884        if let RepositoryError::InvalidData(msg) = result.unwrap_err() {
885            assert!(msg.contains("JSON length:"));
886        }
887
888        // Test with longer JSON to verify length is included
889        let long_json_str = format!(r#"{{"id":"test","name":"{}"}}"#, "a".repeat(1000));
890        let result: Result<TestEntity, RepositoryError> =
891            repo.deserialize_entity(&long_json_str, "test-id", "TestEntity");
892        assert!(result.is_err());
893        if let RepositoryError::InvalidData(msg) = result.unwrap_err() {
894            assert!(msg.contains("JSON length:"));
895            // Should include a length > 1000
896            assert!(msg.len() > 20); // Error message should be substantial
897        }
898    }
899
900    #[test]
901    fn test_map_pool_error_context_in_error_message() {
902        let repo = TestRedisRepository::new();
903        let contexts = vec!["get_by_id", "create", "update", "delete", "list_all"];
904
905        for context in contexts {
906            // Test Timeout which includes context in error message
907            let timeout_error = PoolError::Timeout(TimeoutType::Wait);
908            let timeout_result = repo.map_pool_error(timeout_error, context);
909
910            match timeout_result {
911                RepositoryError::ConnectionError(msg) => {
912                    assert!(
913                        msg.contains(context),
914                        "Context '{context}' should appear in error message"
915                    );
916                }
917                _ => panic!("Expected ConnectionError"),
918            }
919        }
920    }
921
922    #[test]
923    fn test_serialize_entity_with_null_values() {
924        #[derive(Serialize, Deserialize, PartialEq, Debug)]
925        struct NullableEntity {
926            id: String,
927            optional_field: Option<String>,
928        }
929
930        let repo = TestRedisRepository::new();
931        let entity = NullableEntity {
932            id: "null-test".to_string(),
933            optional_field: None,
934        };
935
936        let json = repo
937            .serialize_entity(&entity, |e| &e.id, "NullableEntity")
938            .unwrap();
939
940        assert!(json.contains("null"));
941        assert!(json.contains("null-test"));
942
943        // Verify roundtrip
944        let deserialized: NullableEntity = repo
945            .deserialize_entity(&json, "null-test", "NullableEntity")
946            .unwrap();
947        assert_eq!(entity, deserialized);
948    }
949
950    #[test]
951    fn test_serialize_entity_with_empty_strings() {
952        let repo = TestRedisRepository::new();
953        let entity = TestEntity {
954            id: "empty-test".to_string(),
955            name: "".to_string(),
956            value: 0,
957        };
958
959        let json = repo
960            .serialize_entity(&entity, |e| &e.id, "TestEntity")
961            .unwrap();
962
963        assert!(json.contains("\"name\":\"\""));
964        assert!(json.contains("\"value\":0"));
965
966        // Verify roundtrip
967        let deserialized: TestEntity = repo
968            .deserialize_entity(&json, "empty-test", "TestEntity")
969            .unwrap();
970        assert_eq!(entity, deserialized);
971    }
972
973    #[test]
974    fn test_map_redis_error_different_contexts() {
975        let repo = TestRedisRepository::new();
976        let contexts = vec![
977            "short",
978            "very_long_context_name_that_might_be_used_in_real_world_scenarios",
979            "context-with-dashes",
980            "context_with_underscores",
981            "context.with.dots",
982        ];
983
984        for context in contexts {
985            let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
986            let result = repo.map_redis_error(redis_error, context);
987            match result {
988                RepositoryError::InvalidData(msg) => {
989                    assert!(msg.contains("Redis data type error"));
990                }
991                _ => panic!("Expected InvalidData error"),
992            }
993        }
994    }
995}