openzeppelin_relayer/repositories/
signer.rs

1//! This module defines an in-memory repository for managing signer models.
2//! It provides asynchronous CRUD operations and supports pagination.
3//! The repository is thread-safe, using a `Mutex` to protect access to the underlying data store.
4use crate::{
5    models::{RepositoryError, SignerRepoModel},
6    repositories::*,
7};
8use async_trait::async_trait;
9use eyre::Result;
10use std::collections::HashMap;
11use tokio::sync::{Mutex, MutexGuard};
12
13#[derive(Debug)]
14pub struct InMemorySignerRepository {
15    store: Mutex<HashMap<String, SignerRepoModel>>,
16}
17
18#[allow(dead_code)]
19impl InMemorySignerRepository {
20    pub fn new() -> Self {
21        Self {
22            store: Mutex::new(HashMap::new()),
23        }
24    }
25
26    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
27        Ok(lock.lock().await)
28    }
29}
30
31impl Default for InMemorySignerRepository {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37#[async_trait]
38impl Repository<SignerRepoModel, String> for InMemorySignerRepository {
39    async fn create(&self, signer: SignerRepoModel) -> Result<SignerRepoModel, RepositoryError> {
40        let mut store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
41            Self::acquire_lock(&self.store).await?;
42        if store.contains_key(&signer.id) {
43            return Err(RepositoryError::ConstraintViolation(format!(
44                "Signer with ID {} already exists",
45                signer.id
46            )));
47        }
48        store.insert(signer.id.clone(), signer.clone());
49        Ok(signer)
50    }
51
52    async fn get_by_id(&self, id: String) -> Result<SignerRepoModel, RepositoryError> {
53        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
54            Self::acquire_lock(&self.store).await?;
55        match store.get(&id) {
56            Some(signer) => Ok(signer.clone()),
57            None => Err(RepositoryError::NotFound(format!(
58                "Signer with ID {} not found",
59                id
60            ))),
61        }
62    }
63
64    #[allow(clippy::map_entry)]
65    async fn update(
66        &self,
67        _id: String,
68        _relayer: SignerRepoModel,
69    ) -> Result<SignerRepoModel, RepositoryError> {
70        Err(RepositoryError::NotSupported("Not supported".to_string()))
71    }
72
73    async fn delete_by_id(&self, _id: String) -> Result<(), RepositoryError> {
74        Err(RepositoryError::NotSupported("Not supported".to_string()))
75    }
76
77    async fn list_all(&self) -> Result<Vec<SignerRepoModel>, RepositoryError> {
78        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
79            Self::acquire_lock(&self.store).await?;
80        let signers: Vec<SignerRepoModel> = store.values().cloned().collect();
81        Ok(signers)
82    }
83
84    async fn list_paginated(
85        &self,
86        query: PaginationQuery,
87    ) -> Result<PaginatedResult<SignerRepoModel>, RepositoryError> {
88        let total = self.count().await?;
89        let start = ((query.page - 1) * query.per_page) as usize;
90        let items: Vec<SignerRepoModel> = self
91            .store
92            .lock()
93            .await
94            .values()
95            .skip(start)
96            .take(query.per_page as usize)
97            .cloned()
98            .collect();
99
100        Ok(PaginatedResult {
101            items,
102            total: total as u64,
103            page: query.page,
104            per_page: query.per_page,
105        })
106    }
107
108    async fn count(&self) -> Result<usize, RepositoryError> {
109        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
110            Self::acquire_lock(&self.store).await?;
111        let length = store.len();
112        Ok(length)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use secrets::SecretVec;
119
120    use crate::models::{LocalSignerConfig, SignerConfig};
121
122    use super::*;
123
124    fn create_test_signer(id: String) -> SignerRepoModel {
125        SignerRepoModel {
126            id: id.clone(),
127            config: SignerConfig::Local(LocalSignerConfig {
128                raw_key: SecretVec::zero(0),
129            }),
130        }
131    }
132
133    #[actix_web::test]
134    async fn test_new_repository_is_empty() {
135        let repo = InMemorySignerRepository::new();
136        assert_eq!(repo.count().await.unwrap(), 0);
137    }
138
139    #[actix_web::test]
140    async fn test_add_signer() {
141        let repo = InMemorySignerRepository::new();
142        let signer = create_test_signer("test".to_string());
143
144        repo.create(signer.clone()).await.unwrap();
145        assert_eq!(repo.count().await.unwrap(), 1);
146
147        let stored = repo.get_by_id("test".to_string()).await.unwrap();
148        assert_eq!(stored.id, signer.id);
149    }
150
151    #[actix_web::test]
152    async fn test_update_signer() {
153        let repo = InMemorySignerRepository::new();
154        let signer = create_test_signer("test".to_string());
155
156        let result = repo.update("test".to_string(), signer).await;
157        assert!(matches!(result, Err(RepositoryError::NotSupported(_))));
158    }
159
160    #[actix_web::test]
161    async fn test_list_signers() {
162        let repo = InMemorySignerRepository::new();
163        let signer1 = create_test_signer("test".to_string());
164        let signer2 = create_test_signer("test2".to_string());
165
166        repo.create(signer1.clone()).await.unwrap();
167        repo.create(signer2).await.unwrap();
168
169        let signers = repo.list_all().await.unwrap();
170        assert_eq!(signers.len(), 2);
171    }
172
173    #[actix_web::test]
174    async fn test_update_nonexistent_signer() {
175        let repo = InMemorySignerRepository::new();
176        let signer = create_test_signer("test".to_string());
177
178        let result = repo.update("test2".to_string(), signer).await;
179        assert!(matches!(result, Err(RepositoryError::NotSupported(_))));
180    }
181
182    #[actix_web::test]
183    async fn test_get_nonexistent_relayer() {
184        let repo = InMemorySignerRepository::new();
185
186        let result = repo.get_by_id("test".to_string()).await;
187        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
188    }
189}