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 redis::RedisError;
8use serde::{Deserialize, Serialize};
9use tracing::{error, warn};
10
11/// Base trait for Redis repositories providing common functionality
12pub trait RedisRepository {
13    fn serialize_entity<T, F>(
14        &self,
15        entity: &T,
16        id_extractor: F,
17        entity_type: &str,
18    ) -> Result<String, RepositoryError>
19    where
20        T: Serialize,
21        F: Fn(&T) -> &str,
22    {
23        serde_json::to_string(entity).map_err(|e| {
24            let id = id_extractor(entity);
25            error!(entity_type = %entity_type, id = %id, error = %e, "serialization failed");
26            RepositoryError::InvalidData(format!(
27                "Failed to serialize {} {}: {}",
28                entity_type, id, e
29            ))
30        })
31    }
32
33    /// Deserialize entity with detailed error context
34    /// Default implementation that works for any Deserialize type
35    fn deserialize_entity<T>(
36        &self,
37        json: &str,
38        entity_id: &str,
39        entity_type: &str,
40    ) -> Result<T, RepositoryError>
41    where
42        T: for<'de> Deserialize<'de>,
43    {
44        serde_json::from_str(json).map_err(|e| {
45            error!(entity_type = %entity_type, entity_id = %entity_id, error = %e, "deserialization failed");
46            RepositoryError::InvalidData(format!(
47                "Failed to deserialize {} {}: {} (JSON length: {})",
48                entity_type,
49                entity_id,
50                e,
51                json.len()
52            ))
53        })
54    }
55
56    /// Convert Redis errors to appropriate RepositoryError types
57    fn map_redis_error(&self, error: RedisError, context: &str) -> RepositoryError {
58        warn!(context = %context, error = %error, "redis operation failed");
59
60        match error.kind() {
61            redis::ErrorKind::TypeError => RepositoryError::InvalidData(format!(
62                "Redis data type error in operation '{}': {}",
63                context, error
64            )),
65            redis::ErrorKind::AuthenticationFailed => {
66                RepositoryError::InvalidData("Redis authentication failed".to_string())
67            }
68            redis::ErrorKind::NoScriptError => RepositoryError::InvalidData(format!(
69                "Redis script error in operation '{}': {}",
70                context, error
71            )),
72            redis::ErrorKind::ReadOnly => RepositoryError::InvalidData(format!(
73                "Redis is read-only in operation '{}': {}",
74                context, error
75            )),
76            redis::ErrorKind::ExecAbortError => RepositoryError::InvalidData(format!(
77                "Redis transaction aborted in operation '{}': {}",
78                context, error
79            )),
80            redis::ErrorKind::BusyLoadingError => RepositoryError::InvalidData(format!(
81                "Redis is busy in operation '{}': {}",
82                context, error
83            )),
84            redis::ErrorKind::ExtensionError => RepositoryError::InvalidData(format!(
85                "Redis extension error in operation '{}': {}",
86                context, error
87            )),
88            // Default to Other for connection errors and other issues
89            _ => RepositoryError::Other(format!("Redis operation '{}' failed: {}", context, error)),
90        }
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use serde::{Deserialize, Serialize};
98
99    // Test structs for serialization/deserialization
100    #[derive(Debug, Serialize, Deserialize, PartialEq)]
101    struct TestEntity {
102        id: String,
103        name: String,
104        value: i32,
105    }
106
107    #[derive(Debug, Serialize, Deserialize, PartialEq)]
108    struct SimpleEntity {
109        id: String,
110    }
111
112    // Test implementation of RedisRepository trait
113    struct TestRedisRepository;
114
115    impl RedisRepository for TestRedisRepository {}
116
117    impl TestRedisRepository {
118        fn new() -> Self {
119            TestRedisRepository
120        }
121    }
122
123    #[test]
124    fn test_serialize_entity_success() {
125        let repo = TestRedisRepository::new();
126        let entity = TestEntity {
127            id: "test-id".to_string(),
128            name: "test-name".to_string(),
129            value: 42,
130        };
131
132        let result = repo.serialize_entity(&entity, |e| &e.id, "TestEntity");
133
134        assert!(result.is_ok());
135        let json = result.unwrap();
136        assert!(json.contains("test-id"));
137        assert!(json.contains("test-name"));
138        assert!(json.contains("42"));
139    }
140
141    #[test]
142    fn test_serialize_entity_with_different_id_extractor() {
143        let repo = TestRedisRepository::new();
144        let entity = TestEntity {
145            id: "test-id".to_string(),
146            name: "test-name".to_string(),
147            value: 42,
148        };
149
150        // Use name as ID extractor
151        let result = repo.serialize_entity(&entity, |e| &e.name, "TestEntity");
152
153        assert!(result.is_ok());
154        let json = result.unwrap();
155
156        // Should still serialize the entire entity
157        assert!(json.contains("test-id"));
158        assert!(json.contains("test-name"));
159        assert!(json.contains("42"));
160    }
161
162    #[test]
163    fn test_serialize_entity_simple_struct() {
164        let repo = TestRedisRepository::new();
165        let entity = SimpleEntity {
166            id: "simple-id".to_string(),
167        };
168
169        let result = repo.serialize_entity(&entity, |e| &e.id, "SimpleEntity");
170
171        assert!(result.is_ok());
172        let json = result.unwrap();
173        assert!(json.contains("simple-id"));
174    }
175
176    #[test]
177    fn test_deserialize_entity_success() {
178        let repo = TestRedisRepository::new();
179        let json = r#"{"id":"test-id","name":"test-name","value":42}"#;
180
181        let result: Result<TestEntity, RepositoryError> =
182            repo.deserialize_entity(json, "test-id", "TestEntity");
183
184        assert!(result.is_ok());
185        let entity = result.unwrap();
186        assert_eq!(entity.id, "test-id");
187        assert_eq!(entity.name, "test-name");
188        assert_eq!(entity.value, 42);
189    }
190
191    #[test]
192    fn test_deserialize_entity_invalid_json() {
193        let repo = TestRedisRepository::new();
194        let invalid_json = r#"{"id":"test-id","name":"test-name","value":}"#; // Missing value
195
196        let result: Result<TestEntity, RepositoryError> =
197            repo.deserialize_entity(invalid_json, "test-id", "TestEntity");
198
199        assert!(result.is_err());
200        match result.unwrap_err() {
201            RepositoryError::InvalidData(msg) => {
202                assert!(msg.contains("Failed to deserialize TestEntity test-id"));
203                assert!(msg.contains("JSON length:"));
204            }
205            _ => panic!("Expected InvalidData error"),
206        }
207    }
208
209    #[test]
210    fn test_deserialize_entity_invalid_structure() {
211        let repo = TestRedisRepository::new();
212        let json = r#"{"wrongfield":"test-id"}"#;
213
214        let result: Result<TestEntity, RepositoryError> =
215            repo.deserialize_entity(json, "test-id", "TestEntity");
216
217        assert!(result.is_err());
218        match result.unwrap_err() {
219            RepositoryError::InvalidData(msg) => {
220                assert!(msg.contains("Failed to deserialize TestEntity test-id"));
221            }
222            _ => panic!("Expected InvalidData error"),
223        }
224    }
225
226    #[test]
227    fn test_map_redis_error_type_error() {
228        let repo = TestRedisRepository::new();
229        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
230
231        let result = repo.map_redis_error(redis_error, "test_operation");
232
233        match result {
234            RepositoryError::InvalidData(msg) => {
235                assert!(msg.contains("Redis data type error"));
236                assert!(msg.contains("test_operation"));
237            }
238            _ => panic!("Expected InvalidData error"),
239        }
240    }
241
242    #[test]
243    fn test_map_redis_error_authentication_failed() {
244        let repo = TestRedisRepository::new();
245        let redis_error = RedisError::from((redis::ErrorKind::AuthenticationFailed, "Auth failed"));
246
247        let result = repo.map_redis_error(redis_error, "auth_operation");
248
249        match result {
250            RepositoryError::InvalidData(msg) => {
251                assert!(msg.contains("Redis authentication failed"));
252            }
253            _ => panic!("Expected InvalidData error"),
254        }
255    }
256
257    #[test]
258    fn test_map_redis_error_connection_error() {
259        let repo = TestRedisRepository::new();
260        let redis_error = RedisError::from((redis::ErrorKind::IoError, "Connection failed"));
261
262        let result = repo.map_redis_error(redis_error, "connection_operation");
263
264        match result {
265            RepositoryError::Other(msg) => {
266                assert!(msg.contains("Redis operation"));
267                assert!(msg.contains("connection_operation"));
268            }
269            _ => panic!("Expected Other error"),
270        }
271    }
272
273    #[test]
274    fn test_map_redis_error_no_script_error() {
275        let repo = TestRedisRepository::new();
276        let redis_error = RedisError::from((redis::ErrorKind::NoScriptError, "Script not found"));
277
278        let result = repo.map_redis_error(redis_error, "script_operation");
279
280        match result {
281            RepositoryError::InvalidData(msg) => {
282                assert!(msg.contains("Redis script error"));
283                assert!(msg.contains("script_operation"));
284            }
285            _ => panic!("Expected InvalidData error"),
286        }
287    }
288
289    #[test]
290    fn test_map_redis_error_read_only() {
291        let repo = TestRedisRepository::new();
292        let redis_error = RedisError::from((redis::ErrorKind::ReadOnly, "Read only"));
293
294        let result = repo.map_redis_error(redis_error, "write_operation");
295
296        match result {
297            RepositoryError::InvalidData(msg) => {
298                assert!(msg.contains("Redis is read-only"));
299                assert!(msg.contains("write_operation"));
300            }
301            _ => panic!("Expected InvalidData error"),
302        }
303    }
304
305    #[test]
306    fn test_map_redis_error_exec_abort_error() {
307        let repo = TestRedisRepository::new();
308        let redis_error =
309            RedisError::from((redis::ErrorKind::ExecAbortError, "Transaction aborted"));
310
311        let result = repo.map_redis_error(redis_error, "transaction_operation");
312
313        match result {
314            RepositoryError::InvalidData(msg) => {
315                assert!(msg.contains("Redis transaction aborted"));
316                assert!(msg.contains("transaction_operation"));
317            }
318            _ => panic!("Expected InvalidData error"),
319        }
320    }
321
322    #[test]
323    fn test_map_redis_error_busy_error() {
324        let repo = TestRedisRepository::new();
325        let redis_error = RedisError::from((redis::ErrorKind::BusyLoadingError, "Server busy"));
326
327        let result = repo.map_redis_error(redis_error, "busy_operation");
328
329        match result {
330            RepositoryError::InvalidData(msg) => {
331                assert!(msg.contains("Redis is busy"));
332                assert!(msg.contains("busy_operation"));
333            }
334            _ => panic!("Expected InvalidData error"),
335        }
336    }
337
338    #[test]
339    fn test_map_redis_error_extension_error() {
340        let repo = TestRedisRepository::new();
341        let redis_error = RedisError::from((redis::ErrorKind::ExtensionError, "Extension error"));
342
343        let result = repo.map_redis_error(redis_error, "extension_operation");
344
345        match result {
346            RepositoryError::InvalidData(msg) => {
347                assert!(msg.contains("Redis extension error"));
348                assert!(msg.contains("extension_operation"));
349            }
350            _ => panic!("Expected InvalidData error"),
351        }
352    }
353
354    #[test]
355    fn test_map_redis_error_context_propagation() {
356        let repo = TestRedisRepository::new();
357        let redis_error = RedisError::from((redis::ErrorKind::TypeError, "Type error"));
358        let context = "user_repository_get_operation";
359
360        let result = repo.map_redis_error(redis_error, context);
361
362        match result {
363            RepositoryError::InvalidData(msg) => {
364                assert!(msg.contains("Redis data type error"));
365                // Context should be used in logging but not necessarily in the error message
366            }
367            _ => panic!("Expected InvalidData error"),
368        }
369    }
370
371    #[test]
372    fn test_serialize_deserialize_roundtrip() {
373        let repo = TestRedisRepository::new();
374        let original = TestEntity {
375            id: "roundtrip-id".to_string(),
376            name: "roundtrip-name".to_string(),
377            value: 123,
378        };
379
380        // Serialize
381        let json = repo
382            .serialize_entity(&original, |e| &e.id, "TestEntity")
383            .unwrap();
384
385        // Deserialize
386        let deserialized: TestEntity = repo
387            .deserialize_entity(&json, "roundtrip-id", "TestEntity")
388            .unwrap();
389
390        // Should be identical
391        assert_eq!(original, deserialized);
392    }
393
394    #[test]
395    fn test_serialize_deserialize_unicode_content() {
396        let repo = TestRedisRepository::new();
397        let original = TestEntity {
398            id: "unicode-id".to_string(),
399            name: "测试名称 🚀".to_string(),
400            value: 456,
401        };
402
403        // Serialize
404        let json = repo
405            .serialize_entity(&original, |e| &e.id, "TestEntity")
406            .unwrap();
407
408        // Deserialize
409        let deserialized: TestEntity = repo
410            .deserialize_entity(&json, "unicode-id", "TestEntity")
411            .unwrap();
412
413        // Should handle unicode correctly
414        assert_eq!(original, deserialized);
415    }
416
417    #[test]
418    fn test_serialize_entity_with_complex_data() {
419        let repo = TestRedisRepository::new();
420
421        #[derive(Serialize)]
422        struct ComplexEntity {
423            id: String,
424            nested: NestedData,
425            list: Vec<i32>,
426        }
427
428        #[derive(Serialize)]
429        struct NestedData {
430            field1: String,
431            field2: bool,
432        }
433
434        let complex_entity = ComplexEntity {
435            id: "complex-id".to_string(),
436            nested: NestedData {
437                field1: "nested-value".to_string(),
438                field2: true,
439            },
440            list: vec![1, 2, 3],
441        };
442
443        let result = repo.serialize_entity(&complex_entity, |e| &e.id, "ComplexEntity");
444
445        assert!(result.is_ok());
446        let json = result.unwrap();
447        assert!(json.contains("complex-id"));
448        assert!(json.contains("nested-value"));
449        assert!(json.contains("true"));
450        assert!(json.contains("[1,2,3]"));
451    }
452
453    // Test specifically for u128 serialization/deserialization with large values
454    #[test]
455    fn test_serialize_deserialize_u128_large_values() {
456        use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
457
458        #[derive(Serialize, Deserialize, PartialEq, Debug)]
459        struct TestU128Entity {
460            id: String,
461            #[serde(
462                serialize_with = "serialize_optional_u128",
463                deserialize_with = "deserialize_optional_u128",
464                default
465            )]
466            gas_price: Option<u128>,
467            #[serde(
468                serialize_with = "serialize_optional_u128",
469                deserialize_with = "deserialize_optional_u128",
470                default
471            )]
472            max_fee_per_gas: Option<u128>,
473        }
474
475        let repo = TestRedisRepository::new();
476
477        // Test with very large u128 values that would overflow JSON numbers
478        let original = TestU128Entity {
479            id: "u128-test".to_string(),
480            gas_price: Some(u128::MAX), // 340282366920938463463374607431768211455
481            max_fee_per_gas: Some(999999999999999999999999999999999u128),
482        };
483
484        // Serialize
485        let json = repo
486            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
487            .unwrap();
488
489        // Verify it contains string representations, not numbers
490        assert!(json.contains("\"340282366920938463463374607431768211455\""));
491        assert!(json.contains("\"999999999999999999999999999999999\""));
492        // Make sure they're not stored as numbers (which would cause overflow)
493        assert!(!json.contains("3.4028236692093846e+38"));
494
495        // Deserialize
496        let deserialized: TestU128Entity = repo
497            .deserialize_entity(&json, "u128-test", "TestU128Entity")
498            .unwrap();
499
500        // Should be identical
501        assert_eq!(original, deserialized);
502        assert_eq!(deserialized.gas_price, Some(u128::MAX));
503        assert_eq!(
504            deserialized.max_fee_per_gas,
505            Some(999999999999999999999999999999999u128)
506        );
507    }
508
509    #[test]
510    fn test_serialize_deserialize_u128_none_values() {
511        use crate::utils::{deserialize_optional_u128, serialize_optional_u128};
512
513        #[derive(Serialize, Deserialize, PartialEq, Debug)]
514        struct TestU128Entity {
515            id: String,
516            #[serde(
517                serialize_with = "serialize_optional_u128",
518                deserialize_with = "deserialize_optional_u128",
519                default
520            )]
521            gas_price: Option<u128>,
522        }
523
524        let repo = TestRedisRepository::new();
525
526        // Test with None values
527        let original = TestU128Entity {
528            id: "u128-none-test".to_string(),
529            gas_price: None,
530        };
531
532        // Serialize
533        let json = repo
534            .serialize_entity(&original, |e| &e.id, "TestU128Entity")
535            .unwrap();
536
537        // Should contain null
538        assert!(json.contains("null"));
539
540        // Deserialize
541        let deserialized: TestU128Entity = repo
542            .deserialize_entity(&json, "u128-none-test", "TestU128Entity")
543            .unwrap();
544
545        // Should be identical
546        assert_eq!(original, deserialized);
547        assert_eq!(deserialized.gas_price, None);
548    }
549}