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
167#[async_trait]
168impl Repository<RelayerRepoModel, String> for InMemoryRelayerRepository {
169    async fn create(&self, relayer: RelayerRepoModel) -> Result<RelayerRepoModel, RepositoryError> {
170        let mut store = Self::acquire_lock(&self.store).await?;
171        if store.contains_key(&relayer.id) {
172            return Err(RepositoryError::ConstraintViolation(format!(
173                "Relayer with ID {} already exists",
174                relayer.id
175            )));
176        }
177        store.insert(relayer.id.clone(), relayer.clone());
178        Ok(relayer)
179    }
180
181    async fn get_by_id(&self, id: String) -> Result<RelayerRepoModel, RepositoryError> {
182        let store = Self::acquire_lock(&self.store).await?;
183        match store.get(&id) {
184            Some(relayer) => Ok(relayer.clone()),
185            None => Err(RepositoryError::NotFound(format!(
186                "Relayer with ID {id} not found"
187            ))),
188        }
189    }
190    #[allow(clippy::map_entry)]
191    async fn update(
192        &self,
193        id: String,
194        relayer: RelayerRepoModel,
195    ) -> Result<RelayerRepoModel, RepositoryError> {
196        let mut store = Self::acquire_lock(&self.store).await?;
197        if store.contains_key(&id) {
198            // Ensure we update the existing entry
199            let mut updated_relayer = relayer;
200            updated_relayer.id = id.clone(); // Preserve original ID
201            store.insert(id, updated_relayer.clone());
202            Ok(updated_relayer)
203        } else {
204            Err(RepositoryError::NotFound(format!(
205                "Relayer with ID {id} not found"
206            )))
207        }
208    }
209
210    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
211        let mut store = Self::acquire_lock(&self.store).await?;
212        if store.remove(&id).is_some() {
213            Ok(())
214        } else {
215            Err(RepositoryError::NotFound(format!(
216                "Relayer with ID {id} not found"
217            )))
218        }
219    }
220
221    async fn list_all(&self) -> Result<Vec<RelayerRepoModel>, RepositoryError> {
222        let store = Self::acquire_lock(&self.store).await?;
223        Ok(store.values().cloned().collect())
224    }
225
226    async fn list_paginated(
227        &self,
228        query: PaginationQuery,
229    ) -> Result<PaginatedResult<RelayerRepoModel>, RepositoryError> {
230        let total = self.count().await?;
231        let start = ((query.page - 1) * query.per_page) as usize;
232        let items = self
233            .store
234            .lock()
235            .await
236            .values()
237            .skip(start)
238            .take(query.per_page as usize)
239            .cloned()
240            .collect();
241        Ok(PaginatedResult {
242            items,
243            total: total as u64,
244            page: query.page,
245            per_page: query.per_page,
246        })
247    }
248
249    async fn count(&self) -> Result<usize, RepositoryError> {
250        Ok(self.store.lock().await.len())
251    }
252
253    async fn has_entries(&self) -> Result<bool, RepositoryError> {
254        let store = Self::acquire_lock(&self.store).await?;
255        Ok(!store.is_empty())
256    }
257
258    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
259        let mut store = Self::acquire_lock(&self.store).await?;
260        store.clear();
261        Ok(())
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use crate::models::{NetworkType, RelayerEvmPolicy};
268
269    use super::*;
270
271    fn create_test_relayer(id: String) -> RelayerRepoModel {
272        RelayerRepoModel {
273            id: id.clone(),
274            name: format!("Relayer {}", id.clone()),
275            network: "TestNet".to_string(),
276            paused: false,
277            network_type: NetworkType::Evm,
278            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
279                gas_price_cap: None,
280                whitelist_receivers: None,
281                eip1559_pricing: Some(false),
282                private_transactions: Some(false),
283                min_balance: Some(0),
284                gas_limit_estimation: Some(true),
285            }),
286            signer_id: "test".to_string(),
287            address: "0x".to_string(),
288            notification_id: None,
289            system_disabled: false,
290            custom_rpc_urls: None,
291            ..Default::default()
292        }
293    }
294
295    #[actix_web::test]
296    async fn test_new_repository_is_empty() {
297        let repo = InMemoryRelayerRepository::new();
298        assert_eq!(repo.count().await.unwrap(), 0);
299    }
300
301    #[actix_web::test]
302    async fn test_add_relayer() {
303        let repo = InMemoryRelayerRepository::new();
304        let relayer = create_test_relayer("test".to_string());
305
306        repo.create(relayer.clone()).await.unwrap();
307        assert_eq!(repo.count().await.unwrap(), 1);
308
309        let stored = repo.get_by_id("test".to_string()).await.unwrap();
310        assert_eq!(stored.id, relayer.id);
311        assert_eq!(stored.name, relayer.name);
312    }
313
314    #[actix_web::test]
315    async fn test_update_relayer() {
316        let repo = InMemoryRelayerRepository::new();
317        let mut relayer = create_test_relayer("test".to_string());
318
319        repo.create(relayer.clone()).await.unwrap();
320
321        relayer.name = "Updated Name".to_string();
322        repo.update("test".to_string(), relayer.clone())
323            .await
324            .unwrap();
325
326        let updated = repo.get_by_id("test".to_string()).await.unwrap();
327        assert_eq!(updated.name, "Updated Name");
328    }
329
330    #[actix_web::test]
331    async fn test_list_relayers() {
332        let repo = InMemoryRelayerRepository::new();
333        let relayer1 = create_test_relayer("test".to_string());
334        let relayer2 = create_test_relayer("test2".to_string());
335
336        repo.create(relayer1.clone()).await.unwrap();
337        repo.create(relayer2).await.unwrap();
338
339        let relayers = repo.list_all().await.unwrap();
340        assert_eq!(relayers.len(), 2);
341    }
342
343    #[actix_web::test]
344    async fn test_list_active_relayers() {
345        let repo = InMemoryRelayerRepository::new();
346        let relayer1 = create_test_relayer("test".to_string());
347        let mut relayer2 = create_test_relayer("test2".to_string());
348
349        relayer2.paused = true;
350
351        repo.create(relayer1.clone()).await.unwrap();
352        repo.create(relayer2).await.unwrap();
353
354        let active_relayers = repo.list_active().await.unwrap();
355        assert_eq!(active_relayers.len(), 1);
356        assert_eq!(active_relayers[0].id, "test".to_string());
357    }
358
359    #[actix_web::test]
360    async fn test_update_nonexistent_relayer() {
361        let repo = InMemoryRelayerRepository::new();
362        let relayer = create_test_relayer("test".to_string());
363
364        let result = repo.update("test".to_string(), relayer).await;
365        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
366    }
367
368    #[actix_web::test]
369    async fn test_get_nonexistent_relayer() {
370        let repo = InMemoryRelayerRepository::new();
371
372        let result = repo.get_by_id("test".to_string()).await;
373        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
374    }
375
376    #[actix_web::test]
377    async fn test_partial_update_relayer() {
378        let repo = InMemoryRelayerRepository::new();
379
380        // Add a relayer to the repository
381        let relayer_id = "test_relayer".to_string();
382        let initial_relayer = create_test_relayer(relayer_id.clone());
383
384        repo.create(initial_relayer.clone()).await.unwrap();
385
386        // Perform a partial update on the relayer
387        let update_req = UpdateRelayerRequest {
388            name: None,
389            paused: Some(true),
390            policies: None,
391            notification_id: None,
392            custom_rpc_urls: None,
393        };
394
395        let updated_relayer = repo
396            .partial_update(relayer_id.clone(), update_req)
397            .await
398            .unwrap();
399
400        assert_eq!(updated_relayer.id, initial_relayer.id);
401        assert!(updated_relayer.paused);
402    }
403
404    #[actix_web::test]
405    async fn test_disable_relayer() {
406        let repo = InMemoryRelayerRepository::new();
407
408        // Add a relayer to the repository
409        let relayer_id = "test_relayer".to_string();
410        let initial_relayer = create_test_relayer(relayer_id.clone());
411
412        repo.create(initial_relayer.clone()).await.unwrap();
413
414        // Disable the relayer
415        let disabled_relayer = repo
416            .disable_relayer(
417                relayer_id.clone(),
418                DisabledReason::BalanceCheckFailed("test reason".to_string()),
419            )
420            .await
421            .unwrap();
422
423        assert_eq!(disabled_relayer.id, initial_relayer.id);
424        assert!(disabled_relayer.system_disabled);
425        assert_eq!(
426            disabled_relayer.disabled_reason,
427            Some(DisabledReason::BalanceCheckFailed(
428                "test reason".to_string()
429            ))
430        );
431    }
432
433    #[actix_web::test]
434    async fn test_enable_relayer() {
435        let repo = InMemoryRelayerRepository::new();
436
437        // Add a relayer to the repository
438        let relayer_id = "test_relayer".to_string();
439        let mut initial_relayer = create_test_relayer(relayer_id.clone());
440
441        initial_relayer.system_disabled = true;
442
443        repo.create(initial_relayer.clone()).await.unwrap();
444
445        // Enable the relayer
446        let enabled_relayer = repo.enable_relayer(relayer_id.clone()).await.unwrap();
447
448        assert_eq!(enabled_relayer.id, initial_relayer.id);
449        assert!(!enabled_relayer.system_disabled);
450    }
451
452    #[actix_web::test]
453    async fn test_update_policy() {
454        let repo = InMemoryRelayerRepository::new();
455        let relayer = create_test_relayer("test".to_string());
456
457        repo.create(relayer.clone()).await.unwrap();
458
459        // Create a new policy to update
460        let new_policy = RelayerNetworkPolicy::Evm(RelayerEvmPolicy {
461            gas_price_cap: Some(50000000000),
462            whitelist_receivers: Some(vec!["0x1234".to_string()]),
463            eip1559_pricing: Some(true),
464            private_transactions: Some(true),
465            min_balance: Some(1000000),
466            gas_limit_estimation: Some(true),
467        });
468
469        // Update the policy
470        let updated_relayer = repo
471            .update_policy("test".to_string(), new_policy.clone())
472            .await
473            .unwrap();
474
475        // Verify the policy was updated
476        match updated_relayer.policies {
477            RelayerNetworkPolicy::Evm(policy) => {
478                assert_eq!(policy.gas_price_cap, Some(50000000000));
479                assert_eq!(policy.whitelist_receivers, Some(vec!["0x1234".to_string()]));
480                assert_eq!(policy.eip1559_pricing, Some(true));
481                assert!(policy.private_transactions.unwrap_or(false));
482                assert_eq!(policy.min_balance, Some(1000000));
483            }
484            _ => panic!("Unexpected policy type"),
485        }
486    }
487
488    // test has_entries
489    #[actix_web::test]
490    async fn test_has_entries() {
491        let repo = InMemoryRelayerRepository::new();
492        assert!(!repo.has_entries().await.unwrap());
493
494        let relayer = create_test_relayer("test".to_string());
495
496        repo.create(relayer.clone()).await.unwrap();
497        assert!(repo.has_entries().await.unwrap());
498    }
499
500    #[actix_web::test]
501    async fn test_drop_all_entries() {
502        let repo = InMemoryRelayerRepository::new();
503        let relayer = create_test_relayer("test".to_string());
504
505        repo.create(relayer.clone()).await.unwrap();
506
507        assert!(repo.has_entries().await.unwrap());
508
509        repo.drop_all_entries().await.unwrap();
510        assert!(!repo.has_entries().await.unwrap());
511    }
512
513    #[actix_web::test]
514    async fn test_list_by_signer_id() {
515        let repo = InMemoryRelayerRepository::new();
516
517        // Create test relayers with different signers
518        let relayer1 = RelayerRepoModel {
519            id: "relayer-1".to_string(),
520            name: "Relayer 1".to_string(),
521            network: "ethereum".to_string(),
522            paused: false,
523            network_type: NetworkType::Evm,
524            signer_id: "signer-alpha".to_string(),
525            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
526            address: "0x1111".to_string(),
527            notification_id: None,
528            system_disabled: false,
529            custom_rpc_urls: None,
530            ..Default::default()
531        };
532
533        let relayer2 = RelayerRepoModel {
534            id: "relayer-2".to_string(),
535            name: "Relayer 2".to_string(),
536            network: "polygon".to_string(),
537            paused: true,
538            network_type: NetworkType::Evm,
539            signer_id: "signer-alpha".to_string(), // Same signer as relayer1
540            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
541            address: "0x2222".to_string(),
542            notification_id: None,
543            system_disabled: false,
544            custom_rpc_urls: None,
545            ..Default::default()
546        };
547
548        let relayer3 = RelayerRepoModel {
549            id: "relayer-3".to_string(),
550            name: "Relayer 3".to_string(),
551            network: "solana".to_string(),
552            paused: false,
553            network_type: NetworkType::Solana,
554            signer_id: "signer-beta".to_string(), // Different signer
555            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
556            address: "solana-addr".to_string(),
557            notification_id: None,
558            system_disabled: false,
559            custom_rpc_urls: None,
560            ..Default::default()
561        };
562
563        let relayer4 = RelayerRepoModel {
564            id: "relayer-4".to_string(),
565            name: "Relayer 4".to_string(),
566            network: "stellar".to_string(),
567            paused: false,
568            network_type: NetworkType::Stellar,
569            signer_id: "signer-alpha".to_string(), // Same signer as relayer1 and relayer2
570            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
571            address: "stellar-addr".to_string(),
572            notification_id: Some("notification-1".to_string()),
573            system_disabled: true,
574            custom_rpc_urls: None,
575            ..Default::default()
576        };
577
578        // Add all relayers to the repository
579        repo.create(relayer1).await.unwrap();
580        repo.create(relayer2).await.unwrap();
581        repo.create(relayer3).await.unwrap();
582        repo.create(relayer4).await.unwrap();
583
584        // Test: Find relayers with signer-alpha (should return 3: relayer-1, relayer-2, relayer-4)
585        let relayers_with_alpha = repo.list_by_signer_id("signer-alpha").await.unwrap();
586        assert_eq!(relayers_with_alpha.len(), 3);
587
588        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
589        assert!(alpha_ids.contains(&"relayer-1".to_string()));
590        assert!(alpha_ids.contains(&"relayer-2".to_string()));
591        assert!(alpha_ids.contains(&"relayer-4".to_string()));
592        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
593
594        // Verify the relayers have different states (paused, system_disabled)
595        let relayer2_found = relayers_with_alpha
596            .iter()
597            .find(|r| r.id == "relayer-2")
598            .unwrap();
599        let relayer4_found = relayers_with_alpha
600            .iter()
601            .find(|r| r.id == "relayer-4")
602            .unwrap();
603        assert!(relayer2_found.paused); // Should be paused
604        assert!(relayer4_found.system_disabled); // Should be disabled
605
606        // Test: Find relayers with signer-beta (should return 1: relayer-3)
607        let relayers_with_beta = repo.list_by_signer_id("signer-beta").await.unwrap();
608        assert_eq!(relayers_with_beta.len(), 1);
609        assert_eq!(relayers_with_beta[0].id, "relayer-3");
610        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
611
612        // Test: Find relayers with non-existent signer (should return empty)
613        let relayers_with_gamma = repo.list_by_signer_id("signer-gamma").await.unwrap();
614        assert_eq!(relayers_with_gamma.len(), 0);
615
616        // Test: Find relayers with empty signer ID (should return empty)
617        let relayers_with_empty = repo.list_by_signer_id("").await.unwrap();
618        assert_eq!(relayers_with_empty.len(), 0);
619
620        // Test: Verify total count hasn't changed
621        assert_eq!(repo.count().await.unwrap(), 4);
622
623        // Test: Remove one relayer and verify list_by_signer_id updates correctly
624        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
625
626        let relayers_with_alpha_after_delete =
627            repo.list_by_signer_id("signer-alpha").await.unwrap();
628        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
629
630        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
631            .iter()
632            .map(|r| r.id.clone())
633            .collect();
634        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
635        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
636        assert!(alpha_ids_after.contains(&"relayer-4".to_string()));
637    }
638
639    #[actix_web::test]
640    async fn test_list_by_notification_id() {
641        let repo = InMemoryRelayerRepository::new();
642
643        // Create test relayers with different notifications
644        let relayer1 = RelayerRepoModel {
645            id: "relayer-1".to_string(),
646            name: "Relayer 1".to_string(),
647            network: "ethereum".to_string(),
648            paused: false,
649            network_type: NetworkType::Evm,
650            signer_id: "test-signer".to_string(),
651            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
652            address: "0x1111".to_string(),
653            notification_id: Some("notification-alpha".to_string()),
654            system_disabled: false,
655            custom_rpc_urls: None,
656            ..Default::default()
657        };
658
659        let relayer2 = RelayerRepoModel {
660            id: "relayer-2".to_string(),
661            name: "Relayer 2".to_string(),
662            network: "polygon".to_string(),
663            paused: true,
664            network_type: NetworkType::Evm,
665            signer_id: "test-signer".to_string(),
666            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
667            address: "0x2222".to_string(),
668            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1
669            system_disabled: false,
670            custom_rpc_urls: None,
671            ..Default::default()
672        };
673
674        let relayer3 = RelayerRepoModel {
675            id: "relayer-3".to_string(),
676            name: "Relayer 3".to_string(),
677            network: "solana".to_string(),
678            paused: false,
679            network_type: NetworkType::Solana,
680            signer_id: "test-signer".to_string(),
681            policies: RelayerNetworkPolicy::Solana(crate::models::RelayerSolanaPolicy::default()),
682            address: "solana-addr".to_string(),
683            notification_id: Some("notification-beta".to_string()), // Different notification
684            system_disabled: false,
685            custom_rpc_urls: None,
686            ..Default::default()
687        };
688
689        let relayer4 = RelayerRepoModel {
690            id: "relayer-4".to_string(),
691            name: "Relayer 4".to_string(),
692            network: "stellar".to_string(),
693            paused: false,
694            network_type: NetworkType::Stellar,
695            signer_id: "test-signer".to_string(),
696            policies: RelayerNetworkPolicy::Stellar(crate::models::RelayerStellarPolicy::default()),
697            address: "stellar-addr".to_string(),
698            notification_id: None, // No notification
699            system_disabled: true,
700            custom_rpc_urls: None,
701            ..Default::default()
702        };
703
704        let relayer5 = RelayerRepoModel {
705            id: "relayer-5".to_string(),
706            name: "Relayer 5".to_string(),
707            network: "bsc".to_string(),
708            paused: false,
709            network_type: NetworkType::Evm,
710            signer_id: "test-signer".to_string(),
711            policies: RelayerNetworkPolicy::Evm(RelayerEvmPolicy::default()),
712            address: "0x5555".to_string(),
713            notification_id: Some("notification-alpha".to_string()), // Same notification as relayer1 and relayer2
714            system_disabled: false,
715            custom_rpc_urls: None,
716            ..Default::default()
717        };
718
719        // Add all relayers to the repository
720        repo.create(relayer1).await.unwrap();
721        repo.create(relayer2).await.unwrap();
722        repo.create(relayer3).await.unwrap();
723        repo.create(relayer4).await.unwrap();
724        repo.create(relayer5).await.unwrap();
725
726        // Test: Find relayers with notification-alpha (should return 3: relayer-1, relayer-2, relayer-5)
727        let relayers_with_alpha = repo
728            .list_by_notification_id("notification-alpha")
729            .await
730            .unwrap();
731        assert_eq!(relayers_with_alpha.len(), 3);
732
733        let alpha_ids: Vec<String> = relayers_with_alpha.iter().map(|r| r.id.clone()).collect();
734        assert!(alpha_ids.contains(&"relayer-1".to_string()));
735        assert!(alpha_ids.contains(&"relayer-2".to_string()));
736        assert!(alpha_ids.contains(&"relayer-5".to_string()));
737        assert!(!alpha_ids.contains(&"relayer-3".to_string()));
738        assert!(!alpha_ids.contains(&"relayer-4".to_string()));
739
740        // Verify the relayers have different states (paused, different networks)
741        let relayer2_found = relayers_with_alpha
742            .iter()
743            .find(|r| r.id == "relayer-2")
744            .unwrap();
745        let relayer5_found = relayers_with_alpha
746            .iter()
747            .find(|r| r.id == "relayer-5")
748            .unwrap();
749        assert!(relayer2_found.paused); // Should be paused
750        assert_eq!(relayer5_found.network, "bsc"); // Should be on BSC network
751
752        // Test: Find relayers with notification-beta (should return 1: relayer-3)
753        let relayers_with_beta = repo
754            .list_by_notification_id("notification-beta")
755            .await
756            .unwrap();
757        assert_eq!(relayers_with_beta.len(), 1);
758        assert_eq!(relayers_with_beta[0].id, "relayer-3");
759        assert_eq!(relayers_with_beta[0].network_type, NetworkType::Solana);
760
761        // Test: Find relayers with non-existent notification (should return empty)
762        let relayers_with_gamma = repo
763            .list_by_notification_id("notification-gamma")
764            .await
765            .unwrap();
766        assert_eq!(relayers_with_gamma.len(), 0);
767
768        // Test: Find relayers with empty string notification (should return empty)
769        let relayers_with_empty = repo.list_by_notification_id("").await.unwrap();
770        assert_eq!(relayers_with_empty.len(), 0);
771
772        // Test: Verify total count hasn't changed
773        assert_eq!(repo.count().await.unwrap(), 5);
774
775        // Test: Remove one relayer and verify list_by_notification_id updates correctly
776        repo.delete_by_id("relayer-2".to_string()).await.unwrap();
777
778        let relayers_with_alpha_after_delete = repo
779            .list_by_notification_id("notification-alpha")
780            .await
781            .unwrap();
782        assert_eq!(relayers_with_alpha_after_delete.len(), 2); // Should now be 2 instead of 3
783
784        let alpha_ids_after: Vec<String> = relayers_with_alpha_after_delete
785            .iter()
786            .map(|r| r.id.clone())
787            .collect();
788        assert!(alpha_ids_after.contains(&"relayer-1".to_string()));
789        assert!(!alpha_ids_after.contains(&"relayer-2".to_string())); // Deleted
790        assert!(alpha_ids_after.contains(&"relayer-5".to_string()));
791
792        // Test: Update a relayer's notification and verify the lists update correctly
793        let mut updated_relayer = repo.get_by_id("relayer-5".to_string()).await.unwrap();
794        updated_relayer.notification_id = Some("notification-beta".to_string());
795        repo.update("relayer-5".to_string(), updated_relayer)
796            .await
797            .unwrap();
798
799        // Check notification-alpha list again (should now have only relayer-1)
800        let relayers_with_alpha_final = repo
801            .list_by_notification_id("notification-alpha")
802            .await
803            .unwrap();
804        assert_eq!(relayers_with_alpha_final.len(), 1);
805        assert_eq!(relayers_with_alpha_final[0].id, "relayer-1");
806
807        // Check notification-beta list (should now have relayer-3 and relayer-5)
808        let relayers_with_beta_final = repo
809            .list_by_notification_id("notification-beta")
810            .await
811            .unwrap();
812        assert_eq!(relayers_with_beta_final.len(), 2);
813        let beta_ids_final: Vec<String> = relayers_with_beta_final
814            .iter()
815            .map(|r| r.id.clone())
816            .collect();
817        assert!(beta_ids_final.contains(&"relayer-3".to_string()));
818        assert!(beta_ids_final.contains(&"relayer-5".to_string()));
819    }
820}