openzeppelin_relayer/repositories/relayer/
relayer_in_memory.rs

1//! This module defines the `RelayerRepository` trait and its in-memory implementation,
2//! `InMemoryRelayerRepository`. It provides functionality for managing relayers, including
3//! creating, updating, enabling, disabling, and listing relayers. The module also includes
4//! conversion logic for transforming configuration file data into repository models and
5//! implements pagination for listing relayers.
6//!
7//! The `RelayerRepository` trait is designed to be implemented by any storage backend,
8//! allowing for flexibility in how relayers are stored and managed. The in-memory
9//! implementation is useful for testing and development purposes.
10use crate::models::PaginationQuery;
11use crate::{
12    models::UpdateRelayerRequest,
13    models::{DisabledReason, RelayerNetworkPolicy, RelayerRepoModel, RepositoryError},
14};
15use async_trait::async_trait;
16use eyre::Result;
17use std::collections::HashMap;
18use tokio::sync::{Mutex, MutexGuard};
19
20use crate::repositories::{PaginatedResult, RelayerRepository, Repository};
21
22#[derive(Debug)]
23pub struct InMemoryRelayerRepository {
24    store: Mutex<HashMap<String, RelayerRepoModel>>,
25}
26
27impl InMemoryRelayerRepository {
28    pub fn new() -> Self {
29        Self {
30            store: Mutex::new(HashMap::new()),
31        }
32    }
33    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
34        Ok(lock.lock().await)
35    }
36}
37
38impl Default for InMemoryRelayerRepository {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl Clone for InMemoryRelayerRepository {
45    fn clone(&self) -> Self {
46        // Try to get the current data, or use empty HashMap if lock fails
47        let data = self
48            .store
49            .try_lock()
50            .map(|guard| guard.clone())
51            .unwrap_or_else(|_| HashMap::new());
52
53        Self {
54            store: Mutex::new(data),
55        }
56    }
57}
58
59#[async_trait]
60impl RelayerRepository for InMemoryRelayerRepository {
61    async fn list_active(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
62        let store = Self::acquire_lock(&self.store).await?;
63        let active_relayers: Vec<RelayerRepoModel> = store
64            .values()
65            .filter(|&relayer| !relayer.paused)
66            .cloned()
67            .collect();
68        Ok(active_relayers)
69    }
70
71    async fn list_by_signer_id(
72        &self,
73        signer_id: &str,
74    ) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
75        let store = Self::acquire_lock(&self.store).await?;
76        let relayers_with_signer: Vec<RelayerRepoModel> = store
77            .values()
78            .filter(|&relayer| relayer.signer_id == signer_id)
79            .cloned()
80            .collect();
81        Ok(relayers_with_signer)
82    }
83
84    async fn list_by_notification_id(
85        &self,
86        notification_id: &str,
87    ) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
88        let store = Self::acquire_lock(&self.store).await?;
89        let relayers_with_notification: Vec<RelayerRepoModel> = store
90            .values()
91            .filter(|&relayer| {
92                relayer
93                    .notification_id
94                    .as_ref()
95                    .is_some_and(|id| id == notification_id)
96            })
97            .cloned()
98            .collect();
99        Ok(relayers_with_notification)
100    }
101
102    async fn partial_update(
103        &self,
104        id: String,
105        update: UpdateRelayerRequest,
106    ) -> Result<RelayerRepoModel, RepositoryError> {
107        let mut store = Self::acquire_lock(&self.store).await?;
108        if let Some(relayer) = store.get_mut(&id) {
109            if let Some(paused) = update.paused {
110                relayer.paused = paused;
111            }
112            Ok(relayer.clone())
113        } else {
114            Err(RepositoryError::NotFound(format!(
115                "Relayer with ID {id} not found"
116            )))
117        }
118    }
119
120    async fn update_policy(
121        &self,
122        id: String,
123        policy: RelayerNetworkPolicy,
124    ) -> Result<RelayerRepoModel, RepositoryError> {
125        let mut store = Self::acquire_lock(&self.store).await?;
126        let relayer = store
127            .get_mut(&id)
128            .ok_or_else(|| RepositoryError::NotFound(format!("Relayer with ID {id} not found")))?;
129        relayer.policies = policy;
130        Ok(relayer.clone())
131    }
132
133    async fn disable_relayer(
134        &self,
135        relayer_id: String,
136        reason: DisabledReason,
137    ) -> Result<RelayerRepoModel, RepositoryError> {
138        let mut store = self.store.lock().await;
139        if let Some(relayer) = store.get_mut(&relayer_id) {
140            relayer.system_disabled = true;
141            relayer.disabled_reason = Some(reason);
142            Ok(relayer.clone())
143        } else {
144            Err(RepositoryError::NotFound(format!(
145                "Relayer with ID {relayer_id} not found"
146            )))
147        }
148    }
149
150    async fn enable_relayer(
151        &self,
152        relayer_id: String,
153    ) -> Result<RelayerRepoModel, RepositoryError> {
154        let mut store = self.store.lock().await;
155        if let Some(relayer) = store.get_mut(&relayer_id) {
156            relayer.system_disabled = false;
157            relayer.disabled_reason = None;
158            Ok(relayer.clone())
159        } else {
160            Err(RepositoryError::NotFound(format!(
161                "Relayer with ID {relayer_id} not found"
162            )))
163        }
164    }
165
166    fn is_persistent_storage(&self) -> bool {
167        false
168    }
169}
170
171#[async_trait]
172impl Repository<RelayerRepoModel, String> for InMemoryRelayerRepository {
173    async fn create(&self, relayer: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError> {
174        let mut store = Self::acquire_lock(&self.store).await?;
175        if store.contains_key(&relayer.id) {
176            return Err(RepositoryError::ConstraintViolation(format!(
177                "Relayer with ID {} already exists",
178                relayer.id
179            )));
180        }
181        store.insert(relayer.id.clone(), relayer.clone());
182        Ok(relayer)
183    }
184
185    async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError> {
186        let store = Self::acquire_lock(&self.store).await?;
187        match store.get(&id) {
188            Some(relayer) => Ok(relayer.clone()),
189            None => Err(RepositoryError::NotFound(format!(
190                "Relayer with ID {id} not found"
191            ))),
192        }
193    }
194    #[allow(clippy::map_entry)]
195    async fn update(
196        &self,
197        id: String,
198        relayer: RelayerRepoModel,
199    ) -> Result<RelayerRepoModel, RepositoryError> {
200        let mut store = Self::acquire_lock(&self.store).await?;
201        if store.contains_key(&id) {
202            // Ensure we update the existing entry
203            let mut updated_relayer = relayer;
204            updated_relayer.id = id.clone(); // Preserve original ID
205            store.insert(id, updated_relayer.clone());
206            Ok(updated_relayer)
207        } else {
208            Err(RepositoryError::NotFound(format!(
209                "Relayer with ID {id} not found"
210            )))
211        }
212    }
213
214    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
215        let mut store = Self::acquire_lock(&self.store).await?;
216        if store.remove(&id).is_some() {
217            Ok(())
218        } else {
219            Err(RepositoryError::NotFound(format!(
220                "Relayer with ID {id} not found"
221            )))
222        }
223    }
224
225    async fn list_all(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
226        let store = Self::acquire_lock(&self.store).await?;
227        Ok(store.values().cloned().collect())
228    }
229
230    async fn list_paginated(
231        &self,
232        query: PaginationQuery,
233    ) -> Result<PaginatedResult<RelayerRepoModel>, RepositoryError> {
234        let total = self.count().await?;
235        let start = ((query.page - 1) * query.per_page) as usize;
236        let items = self
237            .store
238            .lock()
239            .await
240            .values()
241            .skip(start)
242            .take(query.per_page as usize)
243            .cloned()
244            .collect();
245        Ok(PaginatedResult {
246            items,
247            total: total as u64,
248            page: query.page,
249            per_page: query.per_page,
250        })
251    }
252
253    async fn count(&self) -> Result<usize, RepositoryError> {
254        Ok(self.store.lock().await.len())
255    }
256
257    async fn has_entries(&self) -> Result<bool, RepositoryError> {
258        let store = Self::acquire_lock(&self.store).await?;
259        Ok(!store.is_empty())
260    }
261
262    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
263        let mut store = Self::acquire_lock(&self.store).await?;
264        store.clear();
265        Ok(())
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use crate::models::{NetworkType, RelayerEvmPolicy};
272
273    use super::*;
274
275    fn create_test_relayer(id: String) -> RelayerRepoModel {
276        RelayerRepoModel {
277            id: id.clone(),
278            name: format!("Relayer {}", id.clone()),
279            network: "TestNet".to_string(),
280            paused: false,
281            network_type: NetworkType::Evm,
282            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
283                gas_price_cap: None,
284                whitelist_receivers: None,
285                eip1559_pricing: Some(false),
286                private_transactions: Some(false),
287                min_balance: Some(0),
288                gas_limit_estimation: Some(true),
289            }),
290            signer_id: "test".to_string(),
291            address: "0x".to_string(),
292            notification_id: None,
293            system_disabled: false,
294            custom_rpc_urls: None,
295            ..Default::default()
296        }
297    }
298
299    #[actix_web::test]
300    async fn test_new_repository_is_empty() {
301        let repo = InMemoryRelayerRepository::new();
302        assert_eq!(repo.count().await.unwrap(), 0);
303    }
304
305    #[actix_web::test]
306    async fn test_add_relayer() {
307        let repo = InMemoryRelayerRepository::new();
308        let relayer = create_test_relayer("test".to_string());
309
310        repo.create(relayer.clone()).await.unwrap();
311        assert_eq!(repo.count().await.unwrap(), 1);
312
313        let stored = repo.get_by_id("test".to_string()).await.unwrap();
314        assert_eq!(stored.id, relayer.id);
315        assert_eq!(stored.name, relayer.name);
316    }
317
318    #[actix_web::test]
319    async fn test_update_relayer() {
320        let repo = InMemoryRelayerRepository::new();
321        let mut relayer = create_test_relayer("test".to_string());
322
323        repo.create(relayer.clone()).await.unwrap();
324
325        relayer.name = "Updated Name".to_string();
326        repo.update("test".to_string(), relayer.clone())
327            .await
328            .unwrap();
329
330        let updated = repo.get_by_id("test".to_string()).await.unwrap();
331        assert_eq!(updated.name, "Updated Name");
332    }
333
334    #[actix_web::test]
335    async fn test_list_relayers() {
336        let repo = InMemoryRelayerRepository::new();
337        let relayer1 = create_test_relayer("test".to_string());
338        let relayer2 = create_test_relayer("test2".to_string());
339
340        repo.create(relayer1.clone()).await.unwrap();
341        repo.create(relayer2).await.unwrap();
342
343        let relayers = repo.list_all().await.unwrap();
344        assert_eq!(relayers.len(), 2);
345    }
346
347    #[actix_web::test]
348    async fn test_list_active_relayers() {
349        let repo = InMemoryRelayerRepository::new();
350        let relayer1 = create_test_relayer("test".to_string());
351        let mut relayer2 = create_test_relayer("test2".to_string());
352
353        relayer2.paused = true;
354
355        repo.create(relayer1.clone()).await.unwrap();
356        repo.create(relayer2).await.unwrap();
357
358        let active_relayers = repo.list_active().await.unwrap();
359        assert_eq!(active_relayers.len(), 1);
360        assert_eq!(active_relayers[0].id, "test".to_string());
361    }
362
363    #[actix_web::test]
364    async fn test_update_nonexistent_relayer() {
365        let repo = InMemoryRelayerRepository::new();
366        let relayer = create_test_relayer("test".to_string());
367
368        let result = repo.update("test".to_string(), relayer).await;
369        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
370    }
371
372    #[actix_web::test]
373    async fn test_get_nonexistent_relayer() {
374        let repo = InMemoryRelayerRepository::new();
375
376        let result = repo.get_by_id("test".to_string()).await;
377        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
378    }
379
380    #[actix_web::test]
381    async fn test_partial_update_relayer() {
382        let repo = InMemoryRelayerRepository::new();
383
384        // Add a relayer to the repository
385        let relayer_id = "test_relayer".to_string();
386        let initial_relayer = create_test_relayer(relayer_id.clone());
387
388        repo.create(initial_relayer.clone()).await.unwrap();
389
390        // Perform a partial update on the relayer
391        let update_req = UpdateRelayerRequest {
392            name: None,
393            paused: Some(true),
394            policies: None,
395            notification_id: None,
396            custom_rpc_urls: None,
397        };
398
399        let updated_relayer = repo
400            .partial_update(relayer_id.clone(), update_req)
401            .await
402            .unwrap();
403
404        assert_eq!(updated_relayer.id, initial_relayer.id);
405        assert!(updated_relayer.paused);
406    }
407
408    #[actix_web::test]
409    async fn test_disable_relayer() {
410        let repo = InMemoryRelayerRepository::new();
411
412        // Add a relayer to the repository
413        let relayer_id = "test_relayer".to_string();
414        let initial_relayer = create_test_relayer(relayer_id.clone());
415
416        repo.create(initial_relayer.clone()).await.unwrap();
417
418        // Disable the relayer
419        let disabled_relayer = repo
420            .disable_relayer(
421                relayer_id.clone(),
422                DisabledReason::BalanceCheckFailed("test reason".to_string()),
423            )
424            .await
425            .unwrap();
426
427        assert_eq!(disabled_relayer.id, initial_relayer.id);
428        assert!(disabled_relayer.system_disabled);
429        assert_eq!(
430            disabled_relayer.disabled_reason,
431            Some(DisabledReason::BalanceCheckFailed(
432                "test reason".to_string()
433            ))
434        );
435    }
436
437    #[actix_web::test]
438    async fn test_enable_relayer() {
439        let repo = InMemoryRelayerRepository::new();
440
441        // Add a relayer to the repository
442        let relayer_id = "test_relayer".to_string();
443        let mut initial_relayer = create_test_relayer(relayer_id.clone());
444
445        initial_relayer.system_disabled = true;
446
447        repo.create(initial_relayer.clone()).await.unwrap();
448
449        // Enable the relayer
450        let enabled_relayer = repo.enable_relayer(relayer_id.clone()).await.unwrap();
451
452        assert_eq!(enabled_relayer.id, initial_relayer.id);
453        assert!(!enabled_relayer.system_disabled);
454    }
455
456    #[actix_web::test]
457    async fn test_update_policy() {
458        let repo = InMemoryRelayerRepository::new();
459        let relayer = create_test_relayer("test".to_string());
460
461        repo.create(relayer.clone()).await.unwrap();
462
463        // Create a new policy to update
464        let new_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
465            gas_price_cap: Some(50000000000),
466            whitelist_receivers: Some(vec!["0x1234".to_string()]),
467            eip1559_pricing: Some(true),
468            private_transactions: Some(true),
469            min_balance: Some(1000000),
470            gas_limit_estimation: Some(true),
471        });
472
473        // Update the policy
474        let updated_relayer = repo
475            .update_policy("test".to_string(), new_policy.clone())
476            .await
477            .unwrap();
478
479        // Verify the policy was updated
480        match updated_relayer.policies {
481            RelayerNetworkPolicy::Evm(policy) => {
482                assert_eq!(policy.gas_price_cap, Some(50000000000));
483                assert_eq!(policy.whitelist_receivers, Some(vec!["0x1234".to_string()]));
484                assert_eq!(policy.eip1559_pricing, Some(true));
485                assert!(policy.private_transactions.unwrap_or(false));
486                assert_eq!(policy.min_balance, Some(1000000));
487            }
488            _ => panic!("Unexpected policy type"),
489        }
490    }
491
492    // test has_entries
493    #[actix_web::test]
494    async fn test_has_entries() {
495        let repo = InMemoryRelayerRepository::new();
496        assert!(!repo.has_entries().await.unwrap());
497
498        let relayer = create_test_relayer("test".to_string());
499
500        repo.create(relayer.clone()).await.unwrap();
501        assert!(repo.has_entries().await.unwrap());
502    }
503
504    #[actix_web::test]
505    async fn test_drop_all_entries() {
506        let repo = InMemoryRelayerRepository::new();
507        let relayer = create_test_relayer("test".to_string());
508
509        repo.create(relayer.clone()).await.unwrap();
510
511        assert!(repo.has_entries().await.unwrap());
512
513        repo.drop_all_entries().await.unwrap();
514        assert!(!repo.has_entries().await.unwrap());
515    }
516
517    #[actix_web::test]
518    async fn test_list_by_signer_id() {
519        let repo = InMemoryRelayerRepository::new();
520
521        // Create test relayers with different signers
522        let relayer1 = RelayerRepoModel {
523            id: "relayer-1".to_string(),
524            name: "Relayer 1".to_string(),
525            network: "ethereum".to_string(),
526            paused: false,
527            network_type: NetworkType::Evm,
528            signer_id: "signer-alpha".to_string(),
529            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
530            address: "0x1111".to_string(),
531            notification_id: None,
532            system_disabled: false,
533            custom_rpc_urls: None,
534            ..Default::default()
535        };
536
537        let relayer2 = RelayerRepoModel {
538            id: "relayer-2".to_string(),
539            name: "Relayer 2".to_string(),
540            network: "polygon".to_string(),
541            paused: true,
542            network_type: NetworkType::Evm,
543            signer_id: "signer-alpha".to_string(), // Same signer as relayer1
544            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
545            address: "0x2222".to_string(),
546            notification_id: None,
547            system_disabled: false,
548            custom_rpc_urls: None,
549            ..Default::default()
550        };
551
552        let relayer3 = RelayerRepoModel {
553            id: "relayer-3".to_string(),
554            name: "Relayer 3".to_string(),
555            network: "solana".to_string(),
556            paused: false,
557            network_type: NetworkType::Solana,
558            signer_id: "signer-beta".to_string(), // Different signer
559            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
560            address: "solana-addr".to_string(),
561            notification_id: None,
562            system_disabled: false,
563            custom_rpc_urls: None,
564            ..Default::default()
565        };
566
567        let relayer4 = RelayerRepoModel {
568            id: "relayer-4".to_string(),
569            name: "Relayer 4".to_string(),
570            network: "stellar".to_string(),
571            paused: false,
572            network_type: NetworkType::Stellar,
573            signer_id: "signer-alpha".to_string(), // Same signer as relayer1 and relayer2
574            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
575            address: "stellar-addr".to_string(),
576            notification_id: Some("notification-1".to_string()),
577            system_disabled: true,
578            custom_rpc_urls: None,
579            ..Default::default()
580        };
581
582        // Add all relayers to the repository
583        repo.create(relayer1).await.unwrap();
584        repo.create(relayer2).await.unwrap();
585        repo.create(relayer3).await.unwrap();
586        repo.create(relayer4).await.unwrap();
587
588        // Test: Find relayers with signer-alpha (should return 3: relayer-1, relayer-2, relayer-4)
589        let relayers_with_alpha = repo.list_by_signer_id("signer-alpha").await.unwrap();
590        assert_eq!(relayers_with_alpha.len(), 3);
591
592        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
593        assert!(alpha_ids.contains(&"relayer-1".to_string()));
594        assert!(alpha_ids.contains(&"relayer-2".to_string()));
595        assert!(alpha_ids.contains(&"relayer-4".to_string()));
596        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
597
598        // Verify the relayers have different states (paused, system_disabled)
599        let relayer2_found = relayers_with_alpha
600            .iter()
601            .find(|r| r.id == "relayer-2")
602            .unwrap();
603        let relayer4_found = relayers_with_alpha
604            .iter()
605            .find(|r| r.id == "relayer-4")
606            .unwrap();
607        assert!(relayer2_found.paused); // Should be paused
608        assert!(relayer4_found.system_disabled); // Should be disabled
609
610        // Test: Find relayers with signer-beta (should return 1: relayer-3)
611        let relayers_with_beta = repo.list_by_signer_id("signer-beta").await.unwrap();
612        assert_eq!(relayers_with_beta.len(), 1);
613        assert_eq!(relayers_with_beta[0].id, "relayer-3");
614        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
615
616        // Test: Find relayers with non-existent signer (should return empty)
617        let relayers_with_gamma = repo.list_by_signer_id("signer-gamma").await.unwrap();
618        assert_eq!(relayers_with_gamma.len(), 0);
619
620        // Test: Find relayers with empty signer ID (should return empty)
621        let relayers_with_empty = repo.list_by_signer_id("").await.unwrap();
622        assert_eq!(relayers_with_empty.len(), 0);
623
624        // Test: Verify total count hasn't changed
625        assert_eq!(repo.count().await.unwrap(), 4);
626
627        // Test: Remove one relayer and verify list_by_signer_id updates correctly
628        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
629
630        let relayers_with_alpha_after_delete =
631            repo.list_by_signer_id("signer-alpha").await.unwrap();
632        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
633
634        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
635            .iter()
636            .map(|r| r.id.clone())
637            .collect();
638        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
639        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
640        assert!(alpha_ids_after.contains(&"relayer-4".to_string()));
641    }
642
643    #[actix_web::test]
644    async fn test_list_by_notification_id() {
645        let repo = InMemoryRelayerRepository::new();
646
647        // Create test relayers with different notifications
648        let relayer1 = RelayerRepoModel {
649            id: "relayer-1".to_string(),
650            name: "Relayer 1".to_string(),
651            network: "ethereum".to_string(),
652            paused: false,
653            network_type: NetworkType::Evm,
654            signer_id: "test-signer".to_string(),
655            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
656            address: "0x1111".to_string(),
657            notification_id: Some("notification-alpha".to_string()),
658            system_disabled: false,
659            custom_rpc_urls: None,
660            ..Default::default()
661        };
662
663        let relayer2 = RelayerRepoModel {
664            id: "relayer-2".to_string(),
665            name: "Relayer 2".to_string(),
666            network: "polygon".to_string(),
667            paused: true,
668            network_type: NetworkType::Evm,
669            signer_id: "test-signer".to_string(),
670            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
671            address: "0x2222".to_string(),
672            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1
673            system_disabled: false,
674            custom_rpc_urls: None,
675            ..Default::default()
676        };
677
678        let relayer3 = RelayerRepoModel {
679            id: "relayer-3".to_string(),
680            name: "Relayer 3".to_string(),
681            network: "solana".to_string(),
682            paused: false,
683            network_type: NetworkType::Solana,
684            signer_id: "test-signer".to_string(),
685            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
686            address: "solana-addr".to_string(),
687            notification_id: Some("notification-beta".to_string()), // Different notification
688            system_disabled: false,
689            custom_rpc_urls: None,
690            ..Default::default()
691        };
692
693        let relayer4 = RelayerRepoModel {
694            id: "relayer-4".to_string(),
695            name: "Relayer 4".to_string(),
696            network: "stellar".to_string(),
697            paused: false,
698            network_type: NetworkType::Stellar,
699            signer_id: "test-signer".to_string(),
700            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
701            address: "stellar-addr".to_string(),
702            notification_id: None, // No notification
703            system_disabled: true,
704            custom_rpc_urls: None,
705            ..Default::default()
706        };
707
708        let relayer5 = RelayerRepoModel {
709            id: "relayer-5".to_string(),
710            name: "Relayer 5".to_string(),
711            network: "bsc".to_string(),
712            paused: false,
713            network_type: NetworkType::Evm,
714            signer_id: "test-signer".to_string(),
715            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
716            address: "0x5555".to_string(),
717            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1 and relayer2
718            system_disabled: false,
719            custom_rpc_urls: None,
720            ..Default::default()
721        };
722
723        // Add all relayers to the repository
724        repo.create(relayer1).await.unwrap();
725        repo.create(relayer2).await.unwrap();
726        repo.create(relayer3).await.unwrap();
727        repo.create(relayer4).await.unwrap();
728        repo.create(relayer5).await.unwrap();
729
730        // Test: Find relayers with notification-alpha (should return 3: relayer-1, relayer-2, relayer-5)
731        let relayers_with_alpha = repo
732            .list_by_notification_id("notification-alpha")
733            .await
734            .unwrap();
735        assert_eq!(relayers_with_alpha.len(), 3);
736
737        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
738        assert!(alpha_ids.contains(&"relayer-1".to_string()));
739        assert!(alpha_ids.contains(&"relayer-2".to_string()));
740        assert!(alpha_ids.contains(&"relayer-5".to_string()));
741        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
742        assert!(!alpha_ids.contains(&"relayer-4".to_string()));
743
744        // Verify the relayers have different states (paused, different networks)
745        let relayer2_found = relayers_with_alpha
746            .iter()
747            .find(|r| r.id == "relayer-2")
748            .unwrap();
749        let relayer5_found = relayers_with_alpha
750            .iter()
751            .find(|r| r.id == "relayer-5")
752            .unwrap();
753        assert!(relayer2_found.paused); // Should be paused
754        assert_eq!(relayer5_found.network, "bsc"); // Should be on BSC network
755
756        // Test: Find relayers with notification-beta (should return 1: relayer-3)
757        let relayers_with_beta = repo
758            .list_by_notification_id("notification-beta")
759            .await
760            .unwrap();
761        assert_eq!(relayers_with_beta.len(), 1);
762        assert_eq!(relayers_with_beta[0].id, "relayer-3");
763        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
764
765        // Test: Find relayers with non-existent notification (should return empty)
766        let relayers_with_gamma = repo
767            .list_by_notification_id("notification-gamma")
768            .await
769            .unwrap();
770        assert_eq!(relayers_with_gamma.len(), 0);
771
772        // Test: Find relayers with empty string notification (should return empty)
773        let relayers_with_empty = repo.list_by_notification_id("").await.unwrap();
774        assert_eq!(relayers_with_empty.len(), 0);
775
776        // Test: Verify total count hasn't changed
777        assert_eq!(repo.count().await.unwrap(), 5);
778
779        // Test: Remove one relayer and verify list_by_notification_id updates correctly
780        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
781
782        let relayers_with_alpha_after_delete = repo
783            .list_by_notification_id("notification-alpha")
784            .await
785            .unwrap();
786        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
787
788        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
789            .iter()
790            .map(|r| r.id.clone())
791            .collect();
792        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
793        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
794        assert!(alpha_ids_after.contains(&"relayer-5".to_string()));
795
796        // Test: Update a relayer's notification and verify the lists update correctly
797        let mut updated_relayer = repo.get_by_id("relayer-5".to_string()).await.unwrap();
798        updated_relayer.notification_id = Some("notification-beta".to_string());
799        repo.update("relayer-5".to_string(), updated_relayer)
800            .await
801            .unwrap();
802
803        // Check notification-alpha list again (should now have only relayer-1)
804        let relayers_with_alpha_final = repo
805            .list_by_notification_id("notification-alpha")
806            .await
807            .unwrap();
808        assert_eq!(relayers_with_alpha_final.len(), 1);
809        assert_eq!(relayers_with_alpha_final[0].id, "relayer-1");
810
811        // Check notification-beta list (should now have relayer-3 and relayer-5)
812        let relayers_with_beta_final = repo
813            .list_by_notification_id("notification-beta")
814            .await
815            .unwrap();
816        assert_eq!(relayers_with_beta_final.len(), 2);
817        let beta_ids_final: Vec<String> = relayers_with_beta_final
818            .iter()
819            .map(|r| r.id.clone())
820            .collect();
821        assert!(beta_ids_final.contains(&"relayer-3".to_string()));
822        assert!(beta_ids_final.contains(&"relayer-5".to_string()));
823    }
824}