openzeppelin_relayer/repositories/signer/
signer_in_memory.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
18impl Clone for InMemorySignerRepository {
19    fn clone(&self) -> Self {
20        // Try to get the current data, or use empty HashMap if lock fails
21        let data = self
22            .store
23            .try_lock()
24            .map(|guard| guard.clone())
25            .unwrap_or_else(|_| HashMap::new());
26
27        Self {
28            store: Mutex::new(data),
29        }
30    }
31}
32
33#[allow(dead_code)]
34impl InMemorySignerRepository {
35    pub fn new() -> Self {
36        Self {
37            store: Mutex::new(HashMap::new()),
38        }
39    }
40
41    async fn acquire_lock<T>(lock: &Mutex<T>) -> Result<MutexGuard<T>, RepositoryError> {
42        Ok(lock.lock().await)
43    }
44}
45
46impl Default for InMemorySignerRepository {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52#[async_trait]
53impl Repository<SignerRepoModel, String> for InMemorySignerRepository {
54    async fn create(&self, signer: SignerRepoModel) -> Result<SignerRepoModel, RepositoryError> {
55        let mut store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
56            Self::acquire_lock(&self.store).await?;
57        if store.contains_key(&signer.id) {
58            return Err(RepositoryError::ConstraintViolation(format!(
59                "Signer with ID {} already exists",
60                signer.id
61            )));
62        }
63        store.insert(signer.id.clone(), signer.clone());
64        Ok(signer)
65    }
66
67    async fn get_by_id(&self, id: String) -> Result<SignerRepoModel, RepositoryError> {
68        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
69            Self::acquire_lock(&self.store).await?;
70        match store.get(&id) {
71            Some(signer) => Ok(signer.clone()),
72            None => Err(RepositoryError::NotFound(format!(
73                "Signer with ID {} not found",
74                id
75            ))),
76        }
77    }
78
79    #[allow(clippy::map_entry)]
80    async fn update(
81        &self,
82        id: String,
83        signer: SignerRepoModel,
84    ) -> Result<SignerRepoModel, RepositoryError> {
85        let mut store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
86            Self::acquire_lock(&self.store).await?;
87        if !store.contains_key(&id) {
88            return Err(RepositoryError::NotFound(format!(
89                "Signer with ID {} not found",
90                id
91            )));
92        }
93        store.insert(id, signer.clone());
94        Ok(signer)
95    }
96
97    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
98        let mut store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
99            Self::acquire_lock(&self.store).await?;
100        if !store.contains_key(&id) {
101            return Err(RepositoryError::NotFound(format!(
102                "Signer with ID {} not found",
103                id
104            )));
105        }
106        store.remove(&id);
107        Ok(())
108    }
109
110    async fn list_all(&self) -> Result<Vec<SignerRepoModel>, RepositoryError> {
111        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
112            Self::acquire_lock(&self.store).await?;
113        let signers: Vec<SignerRepoModel> = store.values().cloned().collect();
114        Ok(signers)
115    }
116
117    async fn list_paginated(
118        &self,
119        query: PaginationQuery,
120    ) -> Result<PaginatedResult<SignerRepoModel>, RepositoryError> {
121        let total = self.count().await?;
122        let start = ((query.page - 1) * query.per_page) as usize;
123        let items: Vec<SignerRepoModel> = self
124            .store
125            .lock()
126            .await
127            .values()
128            .skip(start)
129            .take(query.per_page as usize)
130            .cloned()
131            .collect();
132
133        Ok(PaginatedResult {
134            items,
135            total: total as u64,
136            page: query.page,
137            per_page: query.per_page,
138        })
139    }
140
141    async fn count(&self) -> Result<usize, RepositoryError> {
142        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
143            Self::acquire_lock(&self.store).await?;
144        let length = store.len();
145        Ok(length)
146    }
147
148    async fn has_entries(&self) -> Result<bool, RepositoryError> {
149        let store = Self::acquire_lock(&self.store).await?;
150        Ok(!store.is_empty())
151    }
152
153    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
154        let mut store = Self::acquire_lock(&self.store).await?;
155        store.clear();
156        Ok(())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use secrets::SecretVec;
163
164    use crate::models::{LocalSignerConfigStorage, SignerConfigStorage};
165
166    use super::*;
167
168    fn create_test_signer(id: String) -> SignerRepoModel {
169        SignerRepoModel {
170            id: id.clone(),
171            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
172                raw_key: SecretVec::zero(0),
173            }),
174        }
175    }
176
177    #[actix_web::test]
178    async fn test_new_repository_is_empty() {
179        let repo = InMemorySignerRepository::new();
180        assert_eq!(repo.count().await.unwrap(), 0);
181    }
182
183    #[actix_web::test]
184    async fn test_add_signer() {
185        let repo = InMemorySignerRepository::new();
186        let signer = create_test_signer("test".to_string());
187
188        repo.create(signer.clone()).await.unwrap();
189        assert_eq!(repo.count().await.unwrap(), 1);
190
191        let stored = repo.get_by_id("test".to_string()).await.unwrap();
192        assert_eq!(stored.id, signer.id);
193    }
194
195    #[actix_web::test]
196    async fn test_update_signer() {
197        let repo = InMemorySignerRepository::new();
198        let signer = create_test_signer("test".to_string());
199
200        // Create the signer first
201        repo.create(signer.clone()).await.unwrap();
202
203        // Update the signer
204        let updated_signer = SignerRepoModel {
205            id: "test".to_string(),
206            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
207                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[2; 32])),
208            }),
209        };
210
211        let result = repo.update("test".to_string(), updated_signer).await;
212        assert!(result.is_ok());
213    }
214
215    #[actix_web::test]
216    async fn test_list_signers() {
217        let repo = InMemorySignerRepository::new();
218        let signer1 = create_test_signer("test".to_string());
219        let signer2 = create_test_signer("test2".to_string());
220
221        repo.create(signer1.clone()).await.unwrap();
222        repo.create(signer2).await.unwrap();
223
224        let signers = repo.list_all().await.unwrap();
225        assert_eq!(signers.len(), 2);
226    }
227
228    #[actix_web::test]
229    async fn test_update_nonexistent_signer() {
230        let repo = InMemorySignerRepository::new();
231        let signer = create_test_signer("test".to_string());
232
233        let result = repo.update("test2".to_string(), signer).await;
234        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
235    }
236
237    #[actix_web::test]
238    async fn test_get_nonexistent_relayer() {
239        let repo = InMemorySignerRepository::new();
240
241        let result = repo.get_by_id("test".to_string()).await;
242        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
243    }
244
245    #[actix_web::test]
246    async fn test_has_entries() {
247        let repo = InMemorySignerRepository::new();
248        assert!(!repo.has_entries().await.unwrap());
249
250        let signer = create_test_signer("test".to_string());
251
252        repo.create(signer.clone()).await.unwrap();
253        assert!(repo.has_entries().await.unwrap());
254    }
255
256    #[actix_web::test]
257    async fn test_drop_all_entries() {
258        let repo = InMemorySignerRepository::new();
259        let signer = create_test_signer("test".to_string());
260        repo.create(signer.clone()).await.unwrap();
261        assert!(repo.has_entries().await.unwrap());
262
263        repo.drop_all_entries().await.unwrap();
264        assert!(!repo.has_entries().await.unwrap());
265    }
266}