openzeppelin_relayer/repositories/plugin/
mod.rs

1//! Plugin Repository Module
2//!
3//! This module provides the plugin repository layer for the OpenZeppelin Relayer service.
4//! It implements a specialized repository pattern for managing plugin configurations,
5//! supporting both in-memory and Redis-backed storage implementations.
6//!
7//! ## Features
8//!
9//! - **Plugin Management**: Store and retrieve plugin configurations
10//! - **Path Resolution**: Manage plugin script paths for execution
11//! - **Duplicate Prevention**: Ensure unique plugin IDs
12//! - **Configuration Loading**: Convert from file configurations to repository models
13//!
14//! ## Repository Implementations
15//!
16//! - [`InMemoryPluginRepository`]: Fast in-memory storage for testing/development
17//! - [`RedisPluginRepository`]: Redis-backed storage for production environments
18//!
19//! ## Plugin System
20//!
21//! The plugin system allows extending the relayer functionality through external scripts.
22//! Each plugin is identified by a unique ID and contains a path to the executable script.
23//!
24
25pub mod plugin_in_memory;
26pub mod plugin_redis;
27
28pub use plugin_in_memory::*;
29pub use plugin_redis::*;
30
31use async_trait::async_trait;
32use redis::aio::ConnectionManager;
33use std::{sync::Arc, time::Duration};
34
35#[cfg(test)]
36use mockall::automock;
37
38use crate::{
39    config::PluginFileConfig,
40    constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS,
41    models::{PaginationQuery, PluginModel, RepositoryError},
42    repositories::{ConversionError, PaginatedResult},
43};
44
45#[async_trait]
46#[allow(dead_code)]
47#[cfg_attr(test, automock)]
48pub trait PluginRepositoryTrait {
49    async fn get_by_id(&self, id: &str) -> Result<Option<PluginModel>, RepositoryError>;
50    async fn add(&self, plugin: PluginModel) -> Result<(), RepositoryError>;
51    async fn list_paginated(
52        &self,
53        query: PaginationQuery,
54    ) -> Result<PaginatedResult<PluginModel>, RepositoryError>;
55    async fn count(&self) -> Result<usize, RepositoryError>;
56    async fn has_entries(&self) -> Result<bool, RepositoryError>;
57    async fn drop_all_entries(&self) -> Result<(), RepositoryError>;
58}
59
60/// Enum wrapper for different plugin repository implementations
61#[derive(Debug, Clone)]
62pub enum PluginRepositoryStorage {
63    InMemory(InMemoryPluginRepository),
64    Redis(RedisPluginRepository),
65}
66
67impl PluginRepositoryStorage {
68    pub fn new_in_memory() -> Self {
69        Self::InMemory(InMemoryPluginRepository::new())
70    }
71
72    pub fn new_redis(
73        connection_manager: Arc<ConnectionManager>,
74        key_prefix: String,
75    ) -> Result<Self, RepositoryError> {
76        let redis_repo = RedisPluginRepository::new(connection_manager, key_prefix)?;
77        Ok(Self::Redis(redis_repo))
78    }
79}
80
81#[async_trait]
82impl PluginRepositoryTrait for PluginRepositoryStorage {
83    async fn get_by_id(&self, id: &str) -> Result<Option<PluginModel>, RepositoryError> {
84        match self {
85            PluginRepositoryStorage::InMemory(repo) => repo.get_by_id(id).await,
86            PluginRepositoryStorage::Redis(repo) => repo.get_by_id(id).await,
87        }
88    }
89
90    async fn add(&self, plugin: PluginModel) -> Result<(), RepositoryError> {
91        match self {
92            PluginRepositoryStorage::InMemory(repo) => repo.add(plugin).await,
93            PluginRepositoryStorage::Redis(repo) => repo.add(plugin).await,
94        }
95    }
96
97    async fn list_paginated(
98        &self,
99        query: PaginationQuery,
100    ) -> Result<PaginatedResult<PluginModel>, RepositoryError> {
101        match self {
102            PluginRepositoryStorage::InMemory(repo) => repo.list_paginated(query).await,
103            PluginRepositoryStorage::Redis(repo) => repo.list_paginated(query).await,
104        }
105    }
106
107    async fn count(&self) -> Result<usize, RepositoryError> {
108        match self {
109            PluginRepositoryStorage::InMemory(repo) => repo.count().await,
110            PluginRepositoryStorage::Redis(repo) => repo.count().await,
111        }
112    }
113
114    async fn has_entries(&self) -> Result<bool, RepositoryError> {
115        match self {
116            PluginRepositoryStorage::InMemory(repo) => repo.has_entries().await,
117            PluginRepositoryStorage::Redis(repo) => repo.has_entries().await,
118        }
119    }
120
121    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
122        match self {
123            PluginRepositoryStorage::InMemory(repo) => repo.drop_all_entries().await,
124            PluginRepositoryStorage::Redis(repo) => repo.drop_all_entries().await,
125        }
126    }
127}
128
129impl TryFrom<PluginFileConfig> for PluginModel {
130    type Error = ConversionError;
131
132    fn try_from(config: PluginFileConfig) -> Result<Self, Self::Error> {
133        let timeout = Duration::from_secs(config.timeout.unwrap_or(DEFAULT_PLUGIN_TIMEOUT_SECONDS));
134
135        Ok(PluginModel {
136            id: config.id.clone(),
137            path: config.path.clone(),
138            timeout,
139        })
140    }
141}
142
143impl PartialEq for PluginModel {
144    fn eq(&self, other: &Self) -> bool {
145        self.id == other.id && self.path == other.path
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::{config::PluginFileConfig, constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS};
152    use std::time::Duration;
153
154    use super::*;
155
156    #[tokio::test]
157    async fn test_try_from() {
158        let plugin = PluginFileConfig {
159            id: "test-plugin".to_string(),
160            path: "test-path".to_string(),
161            timeout: None,
162        };
163        let result = PluginModel::try_from(plugin);
164        assert!(result.is_ok());
165        assert_eq!(
166            result.unwrap(),
167            PluginModel {
168                id: "test-plugin".to_string(),
169                path: "test-path".to_string(),
170                timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
171            }
172        );
173    }
174
175    // Helper function to create a test plugin
176    fn create_test_plugin(id: &str, path: &str) -> PluginModel {
177        PluginModel {
178            id: id.to_string(),
179            path: path.to_string(),
180            timeout: Duration::from_secs(30),
181        }
182    }
183
184    #[tokio::test]
185    async fn test_plugin_repository_storage_get_by_id_existing() {
186        let storage = PluginRepositoryStorage::new_in_memory();
187        let plugin = create_test_plugin("test-plugin", "/path/to/script.js");
188
189        // Add the plugin first
190        storage.add(plugin.clone()).await.unwrap();
191
192        // Get the plugin
193        let result = storage.get_by_id("test-plugin").await.unwrap();
194        assert_eq!(result, Some(plugin));
195    }
196
197    #[tokio::test]
198    async fn test_plugin_repository_storage_get_by_id_non_existing() {
199        let storage = PluginRepositoryStorage::new_in_memory();
200
201        let result = storage.get_by_id("non-existent").await.unwrap();
202        assert_eq!(result, None);
203    }
204
205    #[tokio::test]
206    async fn test_plugin_repository_storage_add_success() {
207        let storage = PluginRepositoryStorage::new_in_memory();
208        let plugin = create_test_plugin("test-plugin", "/path/to/script.js");
209
210        let result = storage.add(plugin).await;
211        assert!(result.is_ok());
212    }
213
214    #[tokio::test]
215    async fn test_plugin_repository_storage_add_duplicate() {
216        let storage = PluginRepositoryStorage::new_in_memory();
217        let plugin = create_test_plugin("test-plugin", "/path/to/script.js");
218
219        // Add the plugin first time
220        storage.add(plugin.clone()).await.unwrap();
221
222        // Try to add the same plugin again - should succeed (overwrite)
223        let result = storage.add(plugin).await;
224        assert!(result.is_ok());
225    }
226
227    #[tokio::test]
228    async fn test_plugin_repository_storage_count_empty() {
229        let storage = PluginRepositoryStorage::new_in_memory();
230
231        let count = storage.count().await.unwrap();
232        assert_eq!(count, 0);
233    }
234
235    #[tokio::test]
236    async fn test_plugin_repository_storage_count_with_plugins() {
237        let storage = PluginRepositoryStorage::new_in_memory();
238
239        // Add multiple plugins
240        storage
241            .add(create_test_plugin("plugin1", "/path/1.js"))
242            .await
243            .unwrap();
244        storage
245            .add(create_test_plugin("plugin2", "/path/2.js"))
246            .await
247            .unwrap();
248        storage
249            .add(create_test_plugin("plugin3", "/path/3.js"))
250            .await
251            .unwrap();
252
253        let count = storage.count().await.unwrap();
254        assert_eq!(count, 3);
255    }
256
257    #[tokio::test]
258    async fn test_plugin_repository_storage_has_entries_empty() {
259        let storage = PluginRepositoryStorage::new_in_memory();
260
261        let has_entries = storage.has_entries().await.unwrap();
262        assert!(!has_entries);
263    }
264
265    #[tokio::test]
266    async fn test_plugin_repository_storage_has_entries_with_plugins() {
267        let storage = PluginRepositoryStorage::new_in_memory();
268
269        storage
270            .add(create_test_plugin("plugin1", "/path/1.js"))
271            .await
272            .unwrap();
273
274        let has_entries = storage.has_entries().await.unwrap();
275        assert!(has_entries);
276    }
277
278    #[tokio::test]
279    async fn test_plugin_repository_storage_drop_all_entries_empty() {
280        let storage = PluginRepositoryStorage::new_in_memory();
281
282        let result = storage.drop_all_entries().await;
283        assert!(result.is_ok());
284
285        let count = storage.count().await.unwrap();
286        assert_eq!(count, 0);
287    }
288
289    #[tokio::test]
290    async fn test_plugin_repository_storage_drop_all_entries_with_plugins() {
291        let storage = PluginRepositoryStorage::new_in_memory();
292
293        // Add multiple plugins
294        storage
295            .add(create_test_plugin("plugin1", "/path/1.js"))
296            .await
297            .unwrap();
298        storage
299            .add(create_test_plugin("plugin2", "/path/2.js"))
300            .await
301            .unwrap();
302
303        let result = storage.drop_all_entries().await;
304        assert!(result.is_ok());
305
306        let count = storage.count().await.unwrap();
307        assert_eq!(count, 0);
308
309        let has_entries = storage.has_entries().await.unwrap();
310        assert!(!has_entries);
311    }
312
313    #[tokio::test]
314    async fn test_plugin_repository_storage_list_paginated_empty() {
315        let storage = PluginRepositoryStorage::new_in_memory();
316
317        let query = PaginationQuery {
318            page: 1,
319            per_page: 10,
320        };
321        let result = storage.list_paginated(query).await.unwrap();
322
323        assert_eq!(result.items.len(), 0);
324        assert_eq!(result.total, 0);
325        assert_eq!(result.page, 1);
326        assert_eq!(result.per_page, 10);
327    }
328
329    #[tokio::test]
330    async fn test_plugin_repository_storage_list_paginated_with_plugins() {
331        let storage = PluginRepositoryStorage::new_in_memory();
332
333        // Add multiple plugins
334        storage
335            .add(create_test_plugin("plugin1", "/path/1.js"))
336            .await
337            .unwrap();
338        storage
339            .add(create_test_plugin("plugin2", "/path/2.js"))
340            .await
341            .unwrap();
342        storage
343            .add(create_test_plugin("plugin3", "/path/3.js"))
344            .await
345            .unwrap();
346
347        let query = PaginationQuery {
348            page: 1,
349            per_page: 2,
350        };
351        let result = storage.list_paginated(query).await.unwrap();
352
353        assert_eq!(result.items.len(), 2);
354        assert_eq!(result.total, 3);
355        assert_eq!(result.page, 1);
356        assert_eq!(result.per_page, 2);
357    }
358
359    #[tokio::test]
360    async fn test_plugin_repository_storage_workflow() {
361        let storage = PluginRepositoryStorage::new_in_memory();
362
363        // Initially empty
364        assert!(!storage.has_entries().await.unwrap());
365        assert_eq!(storage.count().await.unwrap(), 0);
366
367        // Add plugins
368        let plugin1 = create_test_plugin("auth-plugin", "/scripts/auth.js");
369        let plugin2 = create_test_plugin("email-plugin", "/scripts/email.js");
370
371        storage.add(plugin1.clone()).await.unwrap();
372        storage.add(plugin2.clone()).await.unwrap();
373
374        // Check state
375        assert!(storage.has_entries().await.unwrap());
376        assert_eq!(storage.count().await.unwrap(), 2);
377
378        // Retrieve specific plugin
379        let retrieved = storage.get_by_id("auth-plugin").await.unwrap();
380        assert_eq!(retrieved, Some(plugin1));
381
382        // List all plugins
383        let query = PaginationQuery {
384            page: 1,
385            per_page: 10,
386        };
387        let result = storage.list_paginated(query).await.unwrap();
388        assert_eq!(result.items.len(), 2);
389        assert_eq!(result.total, 2);
390
391        // Clear all plugins
392        storage.drop_all_entries().await.unwrap();
393        assert!(!storage.has_entries().await.unwrap());
394        assert_eq!(storage.count().await.unwrap(), 0);
395    }
396}