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//! - **Compiled Code Caching**: Cache pre-compiled JavaScript code for performance
14//!
15//! ## Repository Implementations
16//!
17//! - [`InMemoryPluginRepository`]: Fast in-memory storage for testing/development
18//! - [`RedisPluginRepository`]: Redis-backed storage for production environments
19//!
20//! ## Plugin System
21//!
22//! The plugin system allows extending the relayer functionality through external scripts.
23//! Each plugin is identified by a unique ID and contains a path to the executable script.
24//!
25
26pub mod plugin_in_memory;
27pub mod plugin_redis;
28
29pub use plugin_in_memory::*;
30pub use plugin_redis::*;
31
32use crate::utils::RedisConnections;
33use async_trait::async_trait;
34use std::{sync::Arc, time::Duration};
35
36#[cfg(test)]
37use mockall::automock;
38
39use crate::{
40    config::PluginFileConfig,
41    constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS,
42    models::{PaginationQuery, PluginModel, RepositoryError},
43    repositories::{ConversionError, PaginatedResult},
44};
45
46#[async_trait]
47#[allow(dead_code)]
48#[cfg_attr(test, automock)]
49pub trait PluginRepositoryTrait {
50    // Plugin CRUD operations
51    async fn get_by_id(&self, id: &str) -> Result<Option<PluginModel>, RepositoryError>;
52    async fn add(&self, plugin: PluginModel) -> Result<(), RepositoryError>;
53    /// Update an existing plugin. Returns the updated plugin if found.
54    async fn update(&self, plugin: PluginModel) -> Result<PluginModel, RepositoryError>;
55    async fn list_paginated(
56        &self,
57        query: PaginationQuery,
58    ) -> Result<PaginatedResult<PluginModel>, RepositoryError>;
59    async fn count(&self) -> Result<usize, RepositoryError>;
60    async fn has_entries(&self) -> Result<bool, RepositoryError>;
61    async fn drop_all_entries(&self) -> Result<(), RepositoryError>;
62
63    // Compiled code cache operations
64    /// Get compiled JavaScript code for a plugin
65    async fn get_compiled_code(&self, plugin_id: &str) -> Result<Option<String>, RepositoryError>;
66    /// Store compiled JavaScript code for a plugin
67    async fn store_compiled_code(
68        &self,
69        plugin_id: &str,
70        compiled_code: &str,
71        source_hash: Option<&str>,
72    ) -> Result<(), RepositoryError>;
73    /// Invalidate cached code for a plugin
74    async fn invalidate_compiled_code(&self, plugin_id: &str) -> Result<(), RepositoryError>;
75    /// Invalidate all cached plugin code
76    async fn invalidate_all_compiled_code(&self) -> Result<(), RepositoryError>;
77    /// Check if a plugin has cached compiled code
78    async fn has_compiled_code(&self, plugin_id: &str) -> Result<bool, RepositoryError>;
79    /// Get the source hash for cache validation
80    async fn get_source_hash(&self, plugin_id: &str) -> Result<Option<String>, RepositoryError>;
81}
82
83/// Enum wrapper for different plugin repository implementations
84#[derive(Debug, Clone)]
85pub enum PluginRepositoryStorage {
86    InMemory(InMemoryPluginRepository),
87    Redis(RedisPluginRepository),
88}
89
90impl PluginRepositoryStorage {
91    pub fn new_in_memory() -> Self {
92        Self::InMemory(InMemoryPluginRepository::new())
93    }
94
95    pub fn new_redis(
96        connections: Arc<RedisConnections>,
97        key_prefix: String,
98    ) -> Result<Self, RepositoryError> {
99        let redis_repo = RedisPluginRepository::new(connections, key_prefix)?;
100        Ok(Self::Redis(redis_repo))
101    }
102}
103
104#[async_trait]
105impl PluginRepositoryTrait for PluginRepositoryStorage {
106    async fn get_by_id(&self, id: &str) -> Result<Option<PluginModel>, RepositoryError> {
107        match self {
108            PluginRepositoryStorage::InMemory(repo) => repo.get_by_id(id).await,
109            PluginRepositoryStorage::Redis(repo) => repo.get_by_id(id).await,
110        }
111    }
112
113    async fn add(&self, plugin: PluginModel) -> Result<(), RepositoryError> {
114        match self {
115            PluginRepositoryStorage::InMemory(repo) => repo.add(plugin).await,
116            PluginRepositoryStorage::Redis(repo) => repo.add(plugin).await,
117        }
118    }
119
120    async fn update(&self, plugin: PluginModel) -> Result<PluginModel, RepositoryError> {
121        match self {
122            PluginRepositoryStorage::InMemory(repo) => repo.update(plugin).await,
123            PluginRepositoryStorage::Redis(repo) => repo.update(plugin).await,
124        }
125    }
126
127    async fn list_paginated(
128        &self,
129        query: PaginationQuery,
130    ) -> Result<PaginatedResult<PluginModel>, RepositoryError> {
131        match self {
132            PluginRepositoryStorage::InMemory(repo) => repo.list_paginated(query).await,
133            PluginRepositoryStorage::Redis(repo) => repo.list_paginated(query).await,
134        }
135    }
136
137    async fn count(&self) -> Result<usize, RepositoryError> {
138        match self {
139            PluginRepositoryStorage::InMemory(repo) => repo.count().await,
140            PluginRepositoryStorage::Redis(repo) => repo.count().await,
141        }
142    }
143
144    async fn has_entries(&self) -> Result<bool, RepositoryError> {
145        match self {
146            PluginRepositoryStorage::InMemory(repo) => repo.has_entries().await,
147            PluginRepositoryStorage::Redis(repo) => repo.has_entries().await,
148        }
149    }
150
151    async fn drop_all_entries(&self) -> Result<(), RepositoryError> {
152        match self {
153            PluginRepositoryStorage::InMemory(repo) => repo.drop_all_entries().await,
154            PluginRepositoryStorage::Redis(repo) => repo.drop_all_entries().await,
155        }
156    }
157
158    async fn get_compiled_code(&self, plugin_id: &str) -> Result<Option<String>, RepositoryError> {
159        match self {
160            PluginRepositoryStorage::InMemory(repo) => repo.get_compiled_code(plugin_id).await,
161            PluginRepositoryStorage::Redis(repo) => repo.get_compiled_code(plugin_id).await,
162        }
163    }
164
165    async fn store_compiled_code(
166        &self,
167        plugin_id: &str,
168        compiled_code: &str,
169        source_hash: Option<&str>,
170    ) -> Result<(), RepositoryError> {
171        match self {
172            PluginRepositoryStorage::InMemory(repo) => {
173                repo.store_compiled_code(plugin_id, compiled_code, source_hash)
174                    .await
175            }
176            PluginRepositoryStorage::Redis(repo) => {
177                repo.store_compiled_code(plugin_id, compiled_code, source_hash)
178                    .await
179            }
180        }
181    }
182
183    async fn invalidate_compiled_code(&self, plugin_id: &str) -> Result<(), RepositoryError> {
184        match self {
185            PluginRepositoryStorage::InMemory(repo) => {
186                repo.invalidate_compiled_code(plugin_id).await
187            }
188            PluginRepositoryStorage::Redis(repo) => repo.invalidate_compiled_code(plugin_id).await,
189        }
190    }
191
192    async fn invalidate_all_compiled_code(&self) -> Result<(), RepositoryError> {
193        match self {
194            PluginRepositoryStorage::InMemory(repo) => repo.invalidate_all_compiled_code().await,
195            PluginRepositoryStorage::Redis(repo) => repo.invalidate_all_compiled_code().await,
196        }
197    }
198
199    async fn has_compiled_code(&self, plugin_id: &str) -> Result<bool, RepositoryError> {
200        match self {
201            PluginRepositoryStorage::InMemory(repo) => repo.has_compiled_code(plugin_id).await,
202            PluginRepositoryStorage::Redis(repo) => repo.has_compiled_code(plugin_id).await,
203        }
204    }
205
206    async fn get_source_hash(&self, plugin_id: &str) -> Result<Option<String>, RepositoryError> {
207        match self {
208            PluginRepositoryStorage::InMemory(repo) => repo.get_source_hash(plugin_id).await,
209            PluginRepositoryStorage::Redis(repo) => repo.get_source_hash(plugin_id).await,
210        }
211    }
212}
213
214impl TryFrom<PluginFileConfig> for PluginModel {
215    type Error = ConversionError;
216
217    fn try_from(config: PluginFileConfig) -> Result<Self, Self::Error> {
218        let timeout = Duration::from_secs(config.timeout.unwrap_or(DEFAULT_PLUGIN_TIMEOUT_SECONDS));
219
220        Ok(PluginModel {
221            id: config.id.clone(),
222            path: config.path.clone(),
223            timeout,
224            emit_logs: config.emit_logs,
225            emit_traces: config.emit_traces,
226            raw_response: config.raw_response,
227            allow_get_invocation: config.allow_get_invocation,
228            config: config.config,
229            forward_logs: config.forward_logs,
230        })
231    }
232}
233
234impl PartialEq for PluginModel {
235    fn eq(&self, other: &Self) -> bool {
236        self.id == other.id && self.path == other.path
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use crate::{config::PluginFileConfig, constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS};
243    use std::time::Duration;
244
245    use super::*;
246
247    // ============================================
248    // Helper functions
249    // ============================================
250
251    fn create_test_plugin(id: &str, path: &str) -> PluginModel {
252        PluginModel {
253            id: id.to_string(),
254            path: path.to_string(),
255            timeout: Duration::from_secs(30),
256            emit_logs: false,
257            emit_traces: false,
258            raw_response: false,
259            allow_get_invocation: false,
260            config: None,
261            forward_logs: false,
262        }
263    }
264
265    fn create_test_plugin_with_options(
266        id: &str,
267        path: &str,
268        emit_logs: bool,
269        emit_traces: bool,
270        raw_response: bool,
271    ) -> PluginModel {
272        PluginModel {
273            id: id.to_string(),
274            path: path.to_string(),
275            timeout: Duration::from_secs(30),
276            emit_logs,
277            emit_traces,
278            raw_response,
279            allow_get_invocation: false,
280            config: None,
281            forward_logs: false,
282        }
283    }
284
285    // ============================================
286    // PluginModel TryFrom tests
287    // ============================================
288
289    #[tokio::test]
290    async fn test_try_from_default_timeout() {
291        let config = PluginFileConfig {
292            id: "test-plugin".to_string(),
293            path: "test-path".to_string(),
294            timeout: None,
295            emit_logs: false,
296            emit_traces: false,
297            raw_response: false,
298            allow_get_invocation: false,
299            config: None,
300            forward_logs: false,
301        };
302
303        let result = PluginModel::try_from(config);
304        assert!(result.is_ok());
305
306        let plugin = result.unwrap();
307        assert_eq!(plugin.id, "test-plugin");
308        assert_eq!(plugin.path, "test-path");
309        assert_eq!(
310            plugin.timeout,
311            Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS)
312        );
313    }
314
315    #[tokio::test]
316    async fn test_try_from_custom_timeout() {
317        let config = PluginFileConfig {
318            id: "test-plugin".to_string(),
319            path: "test-path".to_string(),
320            timeout: Some(120),
321            emit_logs: false,
322            emit_traces: false,
323            raw_response: false,
324            allow_get_invocation: false,
325            config: None,
326            forward_logs: false,
327        };
328
329        let result = PluginModel::try_from(config);
330        assert!(result.is_ok());
331
332        let plugin = result.unwrap();
333        assert_eq!(plugin.timeout, Duration::from_secs(120));
334    }
335
336    #[tokio::test]
337    async fn test_try_from_all_options_enabled() {
338        let mut config_map = serde_json::Map::new();
339        config_map.insert("key".to_string(), serde_json::json!("value"));
340
341        let config = PluginFileConfig {
342            id: "full-plugin".to_string(),
343            path: "/scripts/full.js".to_string(),
344            timeout: Some(60),
345            emit_logs: true,
346            emit_traces: true,
347            raw_response: true,
348            allow_get_invocation: true,
349            config: Some(config_map),
350            forward_logs: true,
351        };
352
353        let result = PluginModel::try_from(config);
354        assert!(result.is_ok());
355
356        let plugin = result.unwrap();
357        assert_eq!(plugin.id, "full-plugin");
358        assert!(plugin.emit_logs);
359        assert!(plugin.emit_traces);
360        assert!(plugin.raw_response);
361        assert!(plugin.allow_get_invocation);
362        assert!(plugin.config.is_some());
363        assert!(plugin.forward_logs);
364    }
365
366    #[tokio::test]
367    async fn test_try_from_zero_timeout() {
368        let config = PluginFileConfig {
369            id: "test".to_string(),
370            path: "path".to_string(),
371            timeout: Some(0),
372            emit_logs: false,
373            emit_traces: false,
374            raw_response: false,
375            allow_get_invocation: false,
376            config: None,
377            forward_logs: false,
378        };
379
380        let result = PluginModel::try_from(config);
381        assert!(result.is_ok());
382        assert_eq!(result.unwrap().timeout, Duration::from_secs(0));
383    }
384
385    // ============================================
386    // PluginModel PartialEq tests
387    // ============================================
388
389    #[test]
390    fn test_plugin_model_equality_same_id_and_path() {
391        let plugin1 = create_test_plugin("plugin-1", "/path/script.js");
392        let plugin2 = create_test_plugin("plugin-1", "/path/script.js");
393
394        assert_eq!(plugin1, plugin2);
395    }
396
397    #[test]
398    fn test_plugin_model_equality_different_id() {
399        let plugin1 = create_test_plugin("plugin-1", "/path/script.js");
400        let plugin2 = create_test_plugin("plugin-2", "/path/script.js");
401
402        assert_ne!(plugin1, plugin2);
403    }
404
405    #[test]
406    fn test_plugin_model_equality_different_path() {
407        let plugin1 = create_test_plugin("plugin-1", "/path/script1.js");
408        let plugin2 = create_test_plugin("plugin-1", "/path/script2.js");
409
410        assert_ne!(plugin1, plugin2);
411    }
412
413    #[test]
414    fn test_plugin_model_equality_ignores_other_fields() {
415        // Same id and path, different other fields
416        let plugin1 =
417            create_test_plugin_with_options("plugin-1", "/path/script.js", false, false, false);
418        let plugin2 =
419            create_test_plugin_with_options("plugin-1", "/path/script.js", true, true, true);
420
421        // Should be equal because only id and path matter
422        assert_eq!(plugin1, plugin2);
423    }
424
425    #[test]
426    fn test_plugin_model_equality_different_timeout() {
427        let mut plugin1 = create_test_plugin("plugin-1", "/path/script.js");
428        plugin1.timeout = Duration::from_secs(30);
429
430        let mut plugin2 = create_test_plugin("plugin-1", "/path/script.js");
431        plugin2.timeout = Duration::from_secs(60);
432
433        // Should be equal because timeout is not part of equality
434        assert_eq!(plugin1, plugin2);
435    }
436
437    // ============================================
438    // PluginRepositoryStorage constructor tests
439    // ============================================
440
441    #[tokio::test]
442    async fn test_new_in_memory_creates_empty_storage() {
443        let storage = PluginRepositoryStorage::new_in_memory();
444
445        assert_eq!(storage.count().await.unwrap(), 0);
446        assert!(!storage.has_entries().await.unwrap());
447    }
448
449    #[test]
450    fn test_storage_enum_debug() {
451        let storage = PluginRepositoryStorage::new_in_memory();
452        let debug_str = format!("{:?}", storage);
453        assert!(debug_str.contains("InMemory"));
454    }
455
456    // ============================================
457    // Basic CRUD tests
458    // ============================================
459
460    #[tokio::test]
461    async fn test_plugin_repository_storage_get_by_id_existing() {
462        let storage = PluginRepositoryStorage::new_in_memory();
463        let plugin = create_test_plugin("test-plugin", "/path/to/script.js");
464
465        storage.add(plugin.clone()).await.unwrap();
466
467        let result = storage.get_by_id("test-plugin").await.unwrap();
468        assert_eq!(result, Some(plugin));
469    }
470
471    #[tokio::test]
472    async fn test_plugin_repository_storage_get_by_id_non_existing() {
473        let storage = PluginRepositoryStorage::new_in_memory();
474
475        let result = storage.get_by_id("non-existent").await.unwrap();
476        assert_eq!(result, None);
477    }
478
479    #[tokio::test]
480    async fn test_plugin_repository_storage_add_success() {
481        let storage = PluginRepositoryStorage::new_in_memory();
482        let plugin = create_test_plugin("test-plugin", "/path/to/script.js");
483
484        let result = storage.add(plugin).await;
485        assert!(result.is_ok());
486    }
487
488    #[tokio::test]
489    async fn test_plugin_repository_storage_add_duplicate() {
490        let storage = PluginRepositoryStorage::new_in_memory();
491        let plugin = create_test_plugin("test-plugin", "/path/to/script.js");
492
493        storage.add(plugin.clone()).await.unwrap();
494
495        // Try to add the same plugin again - should succeed (overwrite)
496        let result = storage.add(plugin).await;
497        assert!(result.is_ok());
498    }
499
500    #[tokio::test]
501    async fn test_plugin_repository_storage_add_multiple() {
502        let storage = PluginRepositoryStorage::new_in_memory();
503
504        for i in 1..=10 {
505            let plugin = create_test_plugin(&format!("plugin-{}", i), &format!("/path/{}.js", i));
506            storage.add(plugin).await.unwrap();
507        }
508
509        assert_eq!(storage.count().await.unwrap(), 10);
510    }
511
512    // ============================================
513    // Update tests
514    // ============================================
515
516    #[tokio::test]
517    async fn test_plugin_repository_storage_update_existing() {
518        let storage = PluginRepositoryStorage::new_in_memory();
519
520        let plugin =
521            create_test_plugin_with_options("test-plugin", "/path/script.js", false, false, false);
522        storage.add(plugin).await.unwrap();
523
524        let updated =
525            create_test_plugin_with_options("test-plugin", "/path/script.js", true, true, true);
526        let result = storage.update(updated.clone()).await;
527
528        assert!(result.is_ok());
529        let returned = result.unwrap();
530        assert!(returned.emit_logs);
531        assert!(returned.emit_traces);
532        assert!(returned.raw_response);
533    }
534
535    #[tokio::test]
536    async fn test_plugin_repository_storage_update_nonexistent() {
537        let storage = PluginRepositoryStorage::new_in_memory();
538
539        let plugin = create_test_plugin("nonexistent", "/path/script.js");
540        let result = storage.update(plugin).await;
541
542        assert!(result.is_err());
543        match result {
544            Err(RepositoryError::NotFound(msg)) => {
545                assert!(msg.contains("nonexistent"));
546            }
547            _ => panic!("Expected NotFound error"),
548        }
549    }
550
551    #[tokio::test]
552    async fn test_plugin_repository_storage_update_persists_changes() {
553        let storage = PluginRepositoryStorage::new_in_memory();
554
555        let plugin = create_test_plugin("test-plugin", "/path/script.js");
556        storage.add(plugin).await.unwrap();
557
558        let mut updated = create_test_plugin("test-plugin", "/path/updated.js");
559        updated.emit_logs = true;
560        storage.update(updated).await.unwrap();
561
562        // Verify persisted changes
563        let retrieved = storage.get_by_id("test-plugin").await.unwrap().unwrap();
564        assert!(retrieved.emit_logs);
565        assert_eq!(retrieved.path, "/path/updated.js");
566    }
567
568    #[tokio::test]
569    async fn test_plugin_repository_storage_update_does_not_affect_others() {
570        let storage = PluginRepositoryStorage::new_in_memory();
571
572        storage
573            .add(create_test_plugin("plugin-1", "/path/1.js"))
574            .await
575            .unwrap();
576        storage
577            .add(create_test_plugin("plugin-2", "/path/2.js"))
578            .await
579            .unwrap();
580        storage
581            .add(create_test_plugin("plugin-3", "/path/3.js"))
582            .await
583            .unwrap();
584
585        let mut updated = create_test_plugin("plugin-2", "/path/updated.js");
586        updated.emit_logs = true;
587        storage.update(updated).await.unwrap();
588
589        // Others unchanged
590        let p1 = storage.get_by_id("plugin-1").await.unwrap().unwrap();
591        assert_eq!(p1.path, "/path/1.js");
592        assert!(!p1.emit_logs);
593
594        let p3 = storage.get_by_id("plugin-3").await.unwrap().unwrap();
595        assert_eq!(p3.path, "/path/3.js");
596        assert!(!p3.emit_logs);
597    }
598
599    // ============================================
600    // Count tests
601    // ============================================
602
603    #[tokio::test]
604    async fn test_plugin_repository_storage_count_empty() {
605        let storage = PluginRepositoryStorage::new_in_memory();
606
607        let count = storage.count().await.unwrap();
608        assert_eq!(count, 0);
609    }
610
611    #[tokio::test]
612    async fn test_plugin_repository_storage_count_with_plugins() {
613        let storage = PluginRepositoryStorage::new_in_memory();
614
615        storage
616            .add(create_test_plugin("plugin1", "/path/1.js"))
617            .await
618            .unwrap();
619        storage
620            .add(create_test_plugin("plugin2", "/path/2.js"))
621            .await
622            .unwrap();
623        storage
624            .add(create_test_plugin("plugin3", "/path/3.js"))
625            .await
626            .unwrap();
627
628        let count = storage.count().await.unwrap();
629        assert_eq!(count, 3);
630    }
631
632    #[tokio::test]
633    async fn test_plugin_repository_storage_count_after_drop() {
634        let storage = PluginRepositoryStorage::new_in_memory();
635
636        for i in 1..=5 {
637            storage
638                .add(create_test_plugin(
639                    &format!("p{}", i),
640                    &format!("/{}.js", i),
641                ))
642                .await
643                .unwrap();
644        }
645
646        assert_eq!(storage.count().await.unwrap(), 5);
647
648        storage.drop_all_entries().await.unwrap();
649
650        assert_eq!(storage.count().await.unwrap(), 0);
651    }
652
653    // ============================================
654    // has_entries tests
655    // ============================================
656
657    #[tokio::test]
658    async fn test_plugin_repository_storage_has_entries_empty() {
659        let storage = PluginRepositoryStorage::new_in_memory();
660
661        let has_entries = storage.has_entries().await.unwrap();
662        assert!(!has_entries);
663    }
664
665    #[tokio::test]
666    async fn test_plugin_repository_storage_has_entries_with_plugins() {
667        let storage = PluginRepositoryStorage::new_in_memory();
668
669        storage
670            .add(create_test_plugin("plugin1", "/path/1.js"))
671            .await
672            .unwrap();
673
674        let has_entries = storage.has_entries().await.unwrap();
675        assert!(has_entries);
676    }
677
678    // ============================================
679    // drop_all_entries tests
680    // ============================================
681
682    #[tokio::test]
683    async fn test_plugin_repository_storage_drop_all_entries_empty() {
684        let storage = PluginRepositoryStorage::new_in_memory();
685
686        let result = storage.drop_all_entries().await;
687        assert!(result.is_ok());
688
689        let count = storage.count().await.unwrap();
690        assert_eq!(count, 0);
691    }
692
693    #[tokio::test]
694    async fn test_plugin_repository_storage_drop_all_entries_with_plugins() {
695        let storage = PluginRepositoryStorage::new_in_memory();
696
697        storage
698            .add(create_test_plugin("plugin1", "/path/1.js"))
699            .await
700            .unwrap();
701        storage
702            .add(create_test_plugin("plugin2", "/path/2.js"))
703            .await
704            .unwrap();
705
706        let result = storage.drop_all_entries().await;
707        assert!(result.is_ok());
708
709        let count = storage.count().await.unwrap();
710        assert_eq!(count, 0);
711
712        let has_entries = storage.has_entries().await.unwrap();
713        assert!(!has_entries);
714    }
715
716    // ============================================
717    // Pagination tests
718    // ============================================
719
720    #[tokio::test]
721    async fn test_plugin_repository_storage_list_paginated_empty() {
722        let storage = PluginRepositoryStorage::new_in_memory();
723
724        let query = PaginationQuery {
725            page: 1,
726            per_page: 10,
727        };
728        let result = storage.list_paginated(query).await.unwrap();
729
730        assert_eq!(result.items.len(), 0);
731        assert_eq!(result.total, 0);
732        assert_eq!(result.page, 1);
733        assert_eq!(result.per_page, 10);
734    }
735
736    #[tokio::test]
737    async fn test_plugin_repository_storage_list_paginated_first_page() {
738        let storage = PluginRepositoryStorage::new_in_memory();
739
740        for i in 1..=10 {
741            storage
742                .add(create_test_plugin(
743                    &format!("plugin{}", i),
744                    &format!("/{}.js", i),
745                ))
746                .await
747                .unwrap();
748        }
749
750        let query = PaginationQuery {
751            page: 1,
752            per_page: 3,
753        };
754        let result = storage.list_paginated(query).await.unwrap();
755
756        assert_eq!(result.items.len(), 3);
757        assert_eq!(result.total, 10);
758        assert_eq!(result.page, 1);
759        assert_eq!(result.per_page, 3);
760    }
761
762    #[tokio::test]
763    async fn test_plugin_repository_storage_list_paginated_middle_page() {
764        let storage = PluginRepositoryStorage::new_in_memory();
765
766        for i in 1..=10 {
767            storage
768                .add(create_test_plugin(
769                    &format!("plugin{}", i),
770                    &format!("/{}.js", i),
771                ))
772                .await
773                .unwrap();
774        }
775
776        let query = PaginationQuery {
777            page: 2,
778            per_page: 3,
779        };
780        let result = storage.list_paginated(query).await.unwrap();
781
782        assert_eq!(result.items.len(), 3);
783        assert_eq!(result.total, 10);
784        assert_eq!(result.page, 2);
785    }
786
787    #[tokio::test]
788    async fn test_plugin_repository_storage_list_paginated_last_partial_page() {
789        let storage = PluginRepositoryStorage::new_in_memory();
790
791        for i in 1..=10 {
792            storage
793                .add(create_test_plugin(
794                    &format!("plugin{}", i),
795                    &format!("/{}.js", i),
796                ))
797                .await
798                .unwrap();
799        }
800
801        // 10 items, 3 per page, page 4 should have 1 item
802        let query = PaginationQuery {
803            page: 4,
804            per_page: 3,
805        };
806        let result = storage.list_paginated(query).await.unwrap();
807
808        assert_eq!(result.items.len(), 1);
809        assert_eq!(result.total, 10);
810    }
811
812    #[tokio::test]
813    async fn test_plugin_repository_storage_list_paginated_beyond_data() {
814        let storage = PluginRepositoryStorage::new_in_memory();
815
816        for i in 1..=5 {
817            storage
818                .add(create_test_plugin(
819                    &format!("plugin{}", i),
820                    &format!("/{}.js", i),
821                ))
822                .await
823                .unwrap();
824        }
825
826        let query = PaginationQuery {
827            page: 100,
828            per_page: 10,
829        };
830        let result = storage.list_paginated(query).await.unwrap();
831
832        assert_eq!(result.items.len(), 0);
833        assert_eq!(result.total, 5);
834    }
835
836    #[tokio::test]
837    async fn test_plugin_repository_storage_list_paginated_large_per_page() {
838        let storage = PluginRepositoryStorage::new_in_memory();
839
840        for i in 1..=5 {
841            storage
842                .add(create_test_plugin(
843                    &format!("plugin{}", i),
844                    &format!("/{}.js", i),
845                ))
846                .await
847                .unwrap();
848        }
849
850        let query = PaginationQuery {
851            page: 1,
852            per_page: 100,
853        };
854        let result = storage.list_paginated(query).await.unwrap();
855
856        assert_eq!(result.items.len(), 5);
857        assert_eq!(result.total, 5);
858    }
859
860    // ============================================
861    // Compiled code cache tests
862    // ============================================
863
864    #[tokio::test]
865    async fn test_store_and_get_compiled_code() {
866        let storage = PluginRepositoryStorage::new_in_memory();
867
868        storage
869            .store_compiled_code("plugin-1", "compiled code", None)
870            .await
871            .unwrap();
872
873        let code = storage.get_compiled_code("plugin-1").await.unwrap();
874        assert_eq!(code, Some("compiled code".to_string()));
875    }
876
877    #[tokio::test]
878    async fn test_get_compiled_code_nonexistent() {
879        let storage = PluginRepositoryStorage::new_in_memory();
880
881        let code = storage.get_compiled_code("nonexistent").await.unwrap();
882        assert_eq!(code, None);
883    }
884
885    #[tokio::test]
886    async fn test_store_compiled_code_with_source_hash() {
887        let storage = PluginRepositoryStorage::new_in_memory();
888
889        storage
890            .store_compiled_code("plugin-1", "code", Some("sha256:abc123"))
891            .await
892            .unwrap();
893
894        let code = storage.get_compiled_code("plugin-1").await.unwrap();
895        assert_eq!(code, Some("code".to_string()));
896
897        let hash = storage.get_source_hash("plugin-1").await.unwrap();
898        assert_eq!(hash, Some("sha256:abc123".to_string()));
899    }
900
901    #[tokio::test]
902    async fn test_store_compiled_code_overwrites() {
903        let storage = PluginRepositoryStorage::new_in_memory();
904
905        storage
906            .store_compiled_code("plugin-1", "old code", Some("old-hash"))
907            .await
908            .unwrap();
909        storage
910            .store_compiled_code("plugin-1", "new code", Some("new-hash"))
911            .await
912            .unwrap();
913
914        let code = storage.get_compiled_code("plugin-1").await.unwrap();
915        assert_eq!(code, Some("new code".to_string()));
916
917        let hash = storage.get_source_hash("plugin-1").await.unwrap();
918        assert_eq!(hash, Some("new-hash".to_string()));
919    }
920
921    #[tokio::test]
922    async fn test_has_compiled_code() {
923        let storage = PluginRepositoryStorage::new_in_memory();
924
925        assert!(!storage.has_compiled_code("plugin-1").await.unwrap());
926
927        storage
928            .store_compiled_code("plugin-1", "code", None)
929            .await
930            .unwrap();
931
932        assert!(storage.has_compiled_code("plugin-1").await.unwrap());
933        assert!(!storage.has_compiled_code("plugin-2").await.unwrap());
934    }
935
936    #[tokio::test]
937    async fn test_invalidate_compiled_code() {
938        let storage = PluginRepositoryStorage::new_in_memory();
939
940        storage
941            .store_compiled_code("plugin-1", "code1", None)
942            .await
943            .unwrap();
944        storage
945            .store_compiled_code("plugin-2", "code2", None)
946            .await
947            .unwrap();
948
949        storage.invalidate_compiled_code("plugin-1").await.unwrap();
950
951        assert!(!storage.has_compiled_code("plugin-1").await.unwrap());
952        assert!(storage.has_compiled_code("plugin-2").await.unwrap());
953    }
954
955    #[tokio::test]
956    async fn test_invalidate_compiled_code_nonexistent() {
957        let storage = PluginRepositoryStorage::new_in_memory();
958
959        // Should not fail
960        let result = storage.invalidate_compiled_code("nonexistent").await;
961        assert!(result.is_ok());
962    }
963
964    #[tokio::test]
965    async fn test_invalidate_all_compiled_code() {
966        let storage = PluginRepositoryStorage::new_in_memory();
967
968        for i in 1..=5 {
969            storage
970                .store_compiled_code(&format!("plugin-{}", i), &format!("code-{}", i), None)
971                .await
972                .unwrap();
973        }
974
975        storage.invalidate_all_compiled_code().await.unwrap();
976
977        for i in 1..=5 {
978            assert!(!storage
979                .has_compiled_code(&format!("plugin-{}", i))
980                .await
981                .unwrap());
982        }
983    }
984
985    #[tokio::test]
986    async fn test_invalidate_all_compiled_code_empty() {
987        let storage = PluginRepositoryStorage::new_in_memory();
988
989        // Should not fail
990        let result = storage.invalidate_all_compiled_code().await;
991        assert!(result.is_ok());
992    }
993
994    #[tokio::test]
995    async fn test_get_source_hash() {
996        let storage = PluginRepositoryStorage::new_in_memory();
997
998        // No hash
999        storage
1000            .store_compiled_code("plugin-1", "code", None)
1001            .await
1002            .unwrap();
1003        let hash = storage.get_source_hash("plugin-1").await.unwrap();
1004        assert_eq!(hash, None);
1005
1006        // With hash
1007        storage
1008            .store_compiled_code("plugin-2", "code", Some("hash123"))
1009            .await
1010            .unwrap();
1011        let hash = storage.get_source_hash("plugin-2").await.unwrap();
1012        assert_eq!(hash, Some("hash123".to_string()));
1013    }
1014
1015    #[tokio::test]
1016    async fn test_get_source_hash_nonexistent() {
1017        let storage = PluginRepositoryStorage::new_in_memory();
1018
1019        let hash = storage.get_source_hash("nonexistent").await.unwrap();
1020        assert_eq!(hash, None);
1021    }
1022
1023    // ============================================
1024    // Cache independence tests
1025    // ============================================
1026
1027    #[tokio::test]
1028    async fn test_compiled_cache_independent_of_plugin_store() {
1029        let storage = PluginRepositoryStorage::new_in_memory();
1030
1031        // Store compiled code without adding plugin
1032        storage
1033            .store_compiled_code("plugin-1", "code", None)
1034            .await
1035            .unwrap();
1036
1037        // Plugin doesn't exist
1038        assert!(storage.get_by_id("plugin-1").await.unwrap().is_none());
1039
1040        // But compiled code does
1041        assert!(storage.has_compiled_code("plugin-1").await.unwrap());
1042    }
1043
1044    #[tokio::test]
1045    async fn test_drop_all_does_not_clear_compiled_cache() {
1046        let storage = PluginRepositoryStorage::new_in_memory();
1047
1048        storage
1049            .add(create_test_plugin("plugin-1", "/path.js"))
1050            .await
1051            .unwrap();
1052        storage
1053            .store_compiled_code("plugin-1", "code", None)
1054            .await
1055            .unwrap();
1056
1057        storage.drop_all_entries().await.unwrap();
1058
1059        // Plugin gone
1060        assert!(storage.get_by_id("plugin-1").await.unwrap().is_none());
1061
1062        // Compiled cache still has entry
1063        assert!(storage.has_compiled_code("plugin-1").await.unwrap());
1064    }
1065
1066    #[tokio::test]
1067    async fn test_invalidate_all_compiled_does_not_clear_store() {
1068        let storage = PluginRepositoryStorage::new_in_memory();
1069
1070        storage
1071            .add(create_test_plugin("plugin-1", "/path.js"))
1072            .await
1073            .unwrap();
1074        storage
1075            .store_compiled_code("plugin-1", "code", None)
1076            .await
1077            .unwrap();
1078
1079        storage.invalidate_all_compiled_code().await.unwrap();
1080
1081        // Compiled cache cleared
1082        assert!(!storage.has_compiled_code("plugin-1").await.unwrap());
1083
1084        // Plugin still exists
1085        assert!(storage.get_by_id("plugin-1").await.unwrap().is_some());
1086    }
1087
1088    // ============================================
1089    // Workflow/integration tests
1090    // ============================================
1091
1092    #[tokio::test]
1093    async fn test_plugin_repository_storage_workflow() {
1094        let storage = PluginRepositoryStorage::new_in_memory();
1095
1096        // Initially empty
1097        assert!(!storage.has_entries().await.unwrap());
1098        assert_eq!(storage.count().await.unwrap(), 0);
1099
1100        // Add plugins
1101        let plugin1 = create_test_plugin("auth-plugin", "/scripts/auth.js");
1102        let plugin2 = create_test_plugin("email-plugin", "/scripts/email.js");
1103
1104        storage.add(plugin1.clone()).await.unwrap();
1105        storage.add(plugin2.clone()).await.unwrap();
1106
1107        // Check state
1108        assert!(storage.has_entries().await.unwrap());
1109        assert_eq!(storage.count().await.unwrap(), 2);
1110
1111        // Retrieve specific plugin
1112        let retrieved = storage.get_by_id("auth-plugin").await.unwrap();
1113        assert_eq!(retrieved, Some(plugin1));
1114
1115        // Update plugin
1116        let mut updated = create_test_plugin("auth-plugin", "/scripts/auth_v2.js");
1117        updated.emit_logs = true;
1118        storage.update(updated).await.unwrap();
1119
1120        let after_update = storage.get_by_id("auth-plugin").await.unwrap().unwrap();
1121        assert_eq!(after_update.path, "/scripts/auth_v2.js");
1122        assert!(after_update.emit_logs);
1123
1124        // List all plugins
1125        let query = PaginationQuery {
1126            page: 1,
1127            per_page: 10,
1128        };
1129        let result = storage.list_paginated(query).await.unwrap();
1130        assert_eq!(result.items.len(), 2);
1131        assert_eq!(result.total, 2);
1132
1133        // Clear all plugins
1134        storage.drop_all_entries().await.unwrap();
1135        assert!(!storage.has_entries().await.unwrap());
1136        assert_eq!(storage.count().await.unwrap(), 0);
1137    }
1138
1139    #[tokio::test]
1140    async fn test_compiled_code_workflow() {
1141        let storage = PluginRepositoryStorage::new_in_memory();
1142
1143        // Add plugin
1144        storage
1145            .add(create_test_plugin("my-plugin", "/scripts/plugin.js"))
1146            .await
1147            .unwrap();
1148
1149        // Initially no compiled code
1150        assert!(!storage.has_compiled_code("my-plugin").await.unwrap());
1151
1152        // Store compiled code
1153        storage
1154            .store_compiled_code("my-plugin", "compiled JS", Some("hash-v1"))
1155            .await
1156            .unwrap();
1157
1158        // Verify
1159        assert!(storage.has_compiled_code("my-plugin").await.unwrap());
1160        assert_eq!(
1161            storage.get_compiled_code("my-plugin").await.unwrap(),
1162            Some("compiled JS".to_string())
1163        );
1164        assert_eq!(
1165            storage.get_source_hash("my-plugin").await.unwrap(),
1166            Some("hash-v1".to_string())
1167        );
1168
1169        // Update compiled code
1170        storage
1171            .store_compiled_code("my-plugin", "updated JS", Some("hash-v2"))
1172            .await
1173            .unwrap();
1174
1175        assert_eq!(
1176            storage.get_compiled_code("my-plugin").await.unwrap(),
1177            Some("updated JS".to_string())
1178        );
1179        assert_eq!(
1180            storage.get_source_hash("my-plugin").await.unwrap(),
1181            Some("hash-v2".to_string())
1182        );
1183
1184        // Invalidate
1185        storage.invalidate_compiled_code("my-plugin").await.unwrap();
1186
1187        assert!(!storage.has_compiled_code("my-plugin").await.unwrap());
1188        assert_eq!(storage.get_compiled_code("my-plugin").await.unwrap(), None);
1189    }
1190
1191    #[tokio::test]
1192    async fn test_multiple_plugins_compiled_code() {
1193        let storage = PluginRepositoryStorage::new_in_memory();
1194
1195        // Store compiled code for multiple plugins
1196        for i in 1..=5 {
1197            storage
1198                .store_compiled_code(
1199                    &format!("plugin-{}", i),
1200                    &format!("code for plugin {}", i),
1201                    Some(&format!("hash-{}", i)),
1202                )
1203                .await
1204                .unwrap();
1205        }
1206
1207        // Verify all
1208        for i in 1..=5 {
1209            assert!(storage
1210                .has_compiled_code(&format!("plugin-{}", i))
1211                .await
1212                .unwrap());
1213            assert_eq!(
1214                storage
1215                    .get_compiled_code(&format!("plugin-{}", i))
1216                    .await
1217                    .unwrap(),
1218                Some(format!("code for plugin {}", i))
1219            );
1220            assert_eq!(
1221                storage
1222                    .get_source_hash(&format!("plugin-{}", i))
1223                    .await
1224                    .unwrap(),
1225                Some(format!("hash-{}", i))
1226            );
1227        }
1228
1229        // Invalidate one
1230        storage.invalidate_compiled_code("plugin-3").await.unwrap();
1231
1232        // Verify selective invalidation
1233        assert!(storage.has_compiled_code("plugin-1").await.unwrap());
1234        assert!(storage.has_compiled_code("plugin-2").await.unwrap());
1235        assert!(!storage.has_compiled_code("plugin-3").await.unwrap());
1236        assert!(storage.has_compiled_code("plugin-4").await.unwrap());
1237        assert!(storage.has_compiled_code("plugin-5").await.unwrap());
1238    }
1239}