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