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 {id} not found"
74            ))),
75        }
76    }
77
78    #[allow(clippy::map_entry)]
79    async fn update(
80        &self,
81        id: String,
82        signer: SignerRepoModel,
83    ) -> Result<SignerRepoModel, RepositoryError> {
84        let mut store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
85            Self::acquire_lock(&self.store).await?;
86        if !store.contains_key(&id) {
87            return Err(RepositoryError::NotFound(format!(
88                "Signer with ID {id} not found"
89            )));
90        }
91        store.insert(id, signer.clone());
92        Ok(signer)
93    }
94
95    async fn delete_by_id(&self, id: String) -> Result<(), RepositoryError> {
96        let mut store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
97            Self::acquire_lock(&self.store).await?;
98        if !store.contains_key(&id) {
99            return Err(RepositoryError::NotFound(format!(
100                "Signer with ID {id} not found"
101            )));
102        }
103        store.remove(&id);
104        Ok(())
105    }
106
107    async fn list_all(&self) -> Result<Vec<SignerRepoModel>, RepositoryError> {
108        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
109            Self::acquire_lock(&self.store).await?;
110        let signers: Vec<SignerRepoModel> = store.values().cloned().collect();
111        Ok(signers)
112    }
113
114    async fn list_paginated(
115        &self,
116        query: PaginationQuery,
117    ) -> Result<PaginatedResult<SignerRepoModel>, RepositoryError> {
118        let total = self.count().await?;
119        let start = ((query.page - 1) * query.per_page) as usize;
120        let items: Vec<SignerRepoModel> = self
121            .store
122            .lock()
123            .await
124            .values()
125            .skip(start)
126            .take(query.per_page as usize)
127            .cloned()
128            .collect();
129
130        Ok(PaginatedResult {
131            items,
132            total: total as u64,
133            page: query.page,
134            per_page: query.per_page,
135        })
136    }
137
138    async fn count(&self) -> Result<usize, RepositoryError> {
139        let store: MutexGuard<'_, HashMap<String, SignerRepoModel>> =
140            Self::acquire_lock(&self.store).await?;
141        let length = store.len();
142        Ok(length)
143    }
144
145    async fn has_entries(&self) -> Result<bool, RepositoryError> {
146        let store = Self::acquire_lock(&self.store).await?;
147        Ok(!store.is_empty())
148    }
149
150    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
151        let mut store = Self::acquire_lock(&self.store).await?;
152        store.clear();
153        Ok(())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use secrets::SecretVec;
160
161    use crate::models::{LocalSignerConfigStorage, SignerConfigStorage};
162
163    use super::*;
164
165    fn create_test_signer(id: String) -> SignerRepoModel {
166        SignerRepoModel {
167            id: id.clone(),
168            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
169                raw_key: SecretVec::zero(0),
170            }),
171        }
172    }
173
174    #[actix_web::test]
175    async fn test_new_repository_is_empty() {
176        let repo = InMemorySignerRepository::new();
177        assert_eq!(repo.count().await.unwrap(), 0);
178    }
179
180    #[actix_web::test]
181    async fn test_add_signer() {
182        let repo = InMemorySignerRepository::new();
183        let signer = create_test_signer("test".to_string());
184
185        repo.create(signer.clone()).await.unwrap();
186        assert_eq!(repo.count().await.unwrap(), 1);
187
188        let stored = repo.get_by_id("test".to_string()).await.unwrap();
189        assert_eq!(stored.id, signer.id);
190    }
191
192    #[actix_web::test]
193    async fn test_update_signer() {
194        let repo = InMemorySignerRepository::new();
195        let signer = create_test_signer("test".to_string());
196
197        // Create the signer first
198        repo.create(signer.clone()).await.unwrap();
199
200        // Update the signer
201        let updated_signer = SignerRepoModel {
202            id: "test".to_string(),
203            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
204                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[2; 32])),
205            }),
206        };
207
208        let result = repo.update("test".to_string(), updated_signer).await;
209        assert!(result.is_ok());
210    }
211
212    #[actix_web::test]
213    async fn test_list_signers() {
214        let repo = InMemorySignerRepository::new();
215        let signer1 = create_test_signer("test".to_string());
216        let signer2 = create_test_signer("test2".to_string());
217
218        repo.create(signer1.clone()).await.unwrap();
219        repo.create(signer2).await.unwrap();
220
221        let signers = repo.list_all().await.unwrap();
222        assert_eq!(signers.len(), 2);
223    }
224
225    #[actix_web::test]
226    async fn test_update_nonexistent_signer() {
227        let repo = InMemorySignerRepository::new();
228        let signer = create_test_signer("test".to_string());
229
230        let result = repo.update("test2".to_string(), signer).await;
231        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
232    }
233
234    #[actix_web::test]
235    async fn test_get_nonexistent_relayer() {
236        let repo = InMemorySignerRepository::new();
237
238        let result = repo.get_by_id("test".to_string()).await;
239        assert!(matches!(result, Err(RepositoryError::NotFound(_))));
240    }
241
242    #[actix_web::test]
243    async fn test_has_entries() {
244        let repo = InMemorySignerRepository::new();
245        assert!(!repo.has_entries().await.unwrap());
246
247        let signer = create_test_signer("test".to_string());
248
249        repo.create(signer.clone()).await.unwrap();
250        assert!(repo.has_entries().await.unwrap());
251    }
252
253    #[actix_web::test]
254    async fn test_drop_all_entries() {
255        let repo = InMemorySignerRepository::new();
256        let signer = create_test_signer("test".to_string());
257        repo.create(signer.clone()).await.unwrap();
258        assert!(repo.has_entries().await.unwrap());
259
260        repo.drop_all_entries().await.unwrap();
261        assert!(!repo.has_entries().await.unwrap());
262    }
263}