openzeppelin_relayer/bootstrap/
initialize_app_state.rs

1//! Application state initialization
2//!
3//! This module contains functions for initializing the application state,
4//! including setting up repositories, job queues, and other necessary components.
5use crate::{
6    config::{RepositoryStorageType, ServerConfig},
7    jobs,
8    models::{AppState, DefaultAppState},
9    queues::create_queue_backend,
10    repositories::{
11        ApiKeyRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage,
12        PluginRepositoryStorage, RelayerRepositoryStorage, SignerRepositoryStorage,
13        TransactionCounterRepositoryStorage, TransactionRepositoryStorage,
14    },
15    utils::{initialize_redis_connections, RedisConnections},
16};
17use actix_web::web;
18use color_eyre::Result;
19use std::sync::Arc;
20use tracing::warn;
21
22pub struct RepositoryCollection {
23    pub relayer: Arc<RelayerRepositoryStorage>,
24    pub transaction: Arc<TransactionRepositoryStorage>,
25    pub signer: Arc<SignerRepositoryStorage>,
26    pub notification: Arc<NotificationRepositoryStorage>,
27    pub network: Arc<NetworkRepositoryStorage>,
28    pub transaction_counter: Arc<TransactionCounterRepositoryStorage>,
29    pub plugin: Arc<PluginRepositoryStorage>,
30    pub api_key: Arc<ApiKeyRepositoryStorage>,
31}
32
33/// Initializes repositories based on the server configuration
34///
35/// # Arguments
36///
37/// * `config` - Server configuration
38/// * `connections` - Redis connections (required for Redis storage type, None for in-memory)
39///
40/// # Returns
41///
42/// * `Result<RepositoryCollection>` - Initialized repositories
43///
44/// # Errors
45pub async fn initialize_repositories(
46    config: &ServerConfig,
47    connections: Option<Arc<RedisConnections>>,
48) -> eyre::Result<RepositoryCollection> {
49    let repositories = match config.repository_storage_type {
50        RepositoryStorageType::InMemory => RepositoryCollection {
51            relayer: Arc::new(RelayerRepositoryStorage::new_in_memory()),
52            transaction: Arc::new(TransactionRepositoryStorage::new_in_memory()),
53            signer: Arc::new(SignerRepositoryStorage::new_in_memory()),
54            notification: Arc::new(NotificationRepositoryStorage::new_in_memory()),
55            network: Arc::new(NetworkRepositoryStorage::new_in_memory()),
56            transaction_counter: Arc::new(TransactionCounterRepositoryStorage::new_in_memory()),
57            plugin: Arc::new(PluginRepositoryStorage::new_in_memory()),
58            api_key: Arc::new(ApiKeyRepositoryStorage::new_in_memory()),
59        },
60        RepositoryStorageType::Redis => {
61            if config.storage_encryption_key.is_none() {
62                warn!("⚠️ Storage encryption key is not set. Please set the STORAGE_ENCRYPTION_KEY environment variable.");
63                return Err(eyre::eyre!("Storage encryption key is not set. Please set the STORAGE_ENCRYPTION_KEY environment variable."));
64            }
65
66            let connections = connections
67                .ok_or_else(|| eyre::eyre!("Redis connections required for Redis storage type"))?;
68
69            // Use the shared connections for all repositories
70            RepositoryCollection {
71                relayer: Arc::new(RelayerRepositoryStorage::new_redis(
72                    connections.clone(),
73                    config.redis_key_prefix.clone(),
74                )?),
75                transaction: Arc::new(TransactionRepositoryStorage::new_redis(
76                    connections.clone(),
77                    config.redis_key_prefix.clone(),
78                )?),
79                signer: Arc::new(SignerRepositoryStorage::new_redis(
80                    connections.clone(),
81                    config.redis_key_prefix.clone(),
82                )?),
83                notification: Arc::new(NotificationRepositoryStorage::new_redis(
84                    connections.clone(),
85                    config.redis_key_prefix.clone(),
86                )?),
87                network: Arc::new(NetworkRepositoryStorage::new_redis(
88                    connections.clone(),
89                    config.redis_key_prefix.clone(),
90                )?),
91                transaction_counter: Arc::new(TransactionCounterRepositoryStorage::new_redis(
92                    connections.clone(),
93                    config.redis_key_prefix.clone(),
94                )?),
95                plugin: Arc::new(PluginRepositoryStorage::new_redis(
96                    connections.clone(),
97                    config.redis_key_prefix.clone(),
98                )?),
99                api_key: Arc::new(ApiKeyRepositoryStorage::new_redis(
100                    connections,
101                    config.redis_key_prefix.clone(),
102                )?),
103            }
104        }
105    };
106
107    Ok(repositories)
108}
109
110/// Initializes application state
111///
112/// # Returns
113///
114/// * `Result<web::Data<AppState>>` - Initialized application state
115///
116/// # Errors
117///
118/// Returns error if:
119/// - Repository initialization fails
120/// - Configuration loading fails
121pub async fn initialize_app_state(
122    server_config: Arc<ServerConfig>,
123) -> Result<web::ThinData<DefaultAppState>> {
124    // Always initialize Redis connections - required for the job queue.
125    // When REDIS_READER_URL is set, read operations use the reader endpoint.
126    // The queue uses deadpool for better connection lifecycle management.
127    let redis_connections = initialize_redis_connections(&server_config).await?;
128
129    // For repositories, pass connections based on storage type
130    let repo_connections = match server_config.repository_storage_type {
131        RepositoryStorageType::Redis => Some(redis_connections.clone()),
132        RepositoryStorageType::InMemory => None,
133    };
134
135    let repositories = initialize_repositories(&server_config, repo_connections).await?;
136
137    let queue_backend = create_queue_backend(redis_connections).await?;
138    let job_producer = Arc::new(jobs::JobProducer::new(queue_backend));
139
140    let app_state = web::ThinData(AppState {
141        relayer_repository: repositories.relayer,
142        transaction_repository: repositories.transaction,
143        signer_repository: repositories.signer,
144        network_repository: repositories.network,
145        notification_repository: repositories.notification,
146        transaction_counter_store: repositories.transaction_counter,
147        job_producer,
148        plugin_repository: repositories.plugin,
149        api_key_repository: repositories.api_key,
150    });
151
152    Ok(app_state)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::{
159        config::RepositoryStorageType,
160        models::SecretString,
161        repositories::{ApiKeyRepositoryTrait, Repository},
162        utils::mocks::mockutils::{
163            create_mock_api_key, create_mock_network, create_mock_relayer, create_mock_signer,
164            create_test_server_config,
165        },
166    };
167    use std::sync::Arc;
168
169    #[tokio::test]
170    async fn test_initialize_repositories_in_memory() {
171        let config = create_test_server_config(RepositoryStorageType::InMemory);
172        // For in-memory storage, pool is not required
173        let result = initialize_repositories(&config, None).await;
174
175        assert!(result.is_ok());
176        let repositories = result.unwrap();
177
178        // Verify all repositories are created
179        assert!(Arc::strong_count(&repositories.relayer) >= 1);
180        assert!(Arc::strong_count(&repositories.transaction) >= 1);
181        assert!(Arc::strong_count(&repositories.signer) >= 1);
182        assert!(Arc::strong_count(&repositories.notification) >= 1);
183        assert!(Arc::strong_count(&repositories.network) >= 1);
184        assert!(Arc::strong_count(&repositories.transaction_counter) >= 1);
185        assert!(Arc::strong_count(&repositories.plugin) >= 1);
186        assert!(Arc::strong_count(&repositories.api_key) >= 1);
187    }
188
189    #[tokio::test]
190    async fn test_repository_collection_functionality() {
191        let config = create_test_server_config(RepositoryStorageType::InMemory);
192        // For in-memory storage, pool is not required
193        let repositories = initialize_repositories(&config, None).await.unwrap();
194
195        // Test basic repository operations
196        let relayer = create_mock_relayer("test-relayer".to_string(), false);
197        let signer = create_mock_signer();
198        let network = create_mock_network();
199        let api_key = create_mock_api_key();
200
201        // Test creating and retrieving items
202        repositories.relayer.create(relayer.clone()).await.unwrap();
203        repositories.signer.create(signer.clone()).await.unwrap();
204        repositories.network.create(network.clone()).await.unwrap();
205        repositories.api_key.create(api_key.clone()).await.unwrap();
206
207        let retrieved_relayer = repositories
208            .relayer
209            .get_by_id("test-relayer".to_string())
210            .await
211            .unwrap();
212        let retrieved_signer = repositories
213            .signer
214            .get_by_id("test".to_string())
215            .await
216            .unwrap();
217        let retrieved_network = repositories
218            .network
219            .get_by_id("test".to_string())
220            .await
221            .unwrap();
222        let retrieved_api_key = repositories
223            .api_key
224            .get_by_id("test-api-key")
225            .await
226            .unwrap();
227
228        assert_eq!(retrieved_relayer.id, "test-relayer");
229        assert_eq!(retrieved_signer.id, "test");
230        assert_eq!(retrieved_network.id, "test");
231        assert_eq!(retrieved_api_key.unwrap().id, "test-api-key");
232    }
233
234    #[tokio::test]
235    async fn test_initialize_app_state_repository_error() {
236        let mut config = create_test_server_config(RepositoryStorageType::Redis);
237        config.redis_url = "redis://invalid_url".to_string();
238
239        let result = initialize_app_state(Arc::new(config)).await;
240
241        // Should fail during repository initialization
242        assert!(result.is_err());
243        let error = result.unwrap_err();
244        assert!(error.to_string().contains("Redis") || error.to_string().contains("connection"));
245    }
246
247    #[tokio::test]
248    async fn test_initialize_repositories_redis_without_encryption_key() {
249        let mut config = create_test_server_config(RepositoryStorageType::Redis);
250        // Explicitly set encryption key to None
251        config.storage_encryption_key = None;
252
253        // Even with a pool, should fail without encryption key
254        // We pass None for pool since it will fail before pool is used
255        let result = initialize_repositories(&config, None).await;
256
257        assert!(result.is_err());
258        let error = match result {
259            Err(e) => e,
260            Ok(_) => panic!("Expected error for missing encryption key"),
261        };
262        assert!(
263            error.to_string().contains("encryption key"),
264            "Expected error about encryption key, got: {error}"
265        );
266    }
267
268    #[tokio::test]
269    async fn test_initialize_repositories_redis_without_pool() {
270        let mut config = create_test_server_config(RepositoryStorageType::Redis);
271        // Set encryption key so we get past that check
272        config.storage_encryption_key = Some(SecretString::new("test-encryption-key-32-bytes!!!"));
273
274        // Pass None for pool - should fail
275        let result = initialize_repositories(&config, None).await;
276
277        assert!(result.is_err());
278        let error = match result {
279            Err(e) => e,
280            Ok(_) => panic!("Expected error for missing pool"),
281        };
282        assert!(
283            error
284                .to_string()
285                .contains("Redis connections required for Redis storage type"),
286            "Expected error about Redis pool being required, got: {error}"
287        );
288    }
289
290    #[tokio::test]
291    async fn test_initialize_repositories_in_memory_ignores_pool() {
292        // For in-memory storage, providing a pool should be fine (it's ignored)
293        // We can't easily create a real pool without Redis, but we can test with None
294        let config = create_test_server_config(RepositoryStorageType::InMemory);
295
296        // In-memory should work with None
297        let result = initialize_repositories(&config, None).await;
298        assert!(result.is_ok());
299
300        // Verify repositories are functional
301        let repositories = result.unwrap();
302        let relayer = create_mock_relayer("test-relayer".to_string(), false);
303        repositories.relayer.create(relayer).await.unwrap();
304        let retrieved = repositories
305            .relayer
306            .get_by_id("test-relayer".to_string())
307            .await
308            .unwrap();
309        assert_eq!(retrieved.id, "test-relayer");
310    }
311
312    #[tokio::test]
313    async fn test_repository_collection_all_eight_repositories() {
314        // Verify that RepositoryCollection contains exactly 8 repositories
315        let config = create_test_server_config(RepositoryStorageType::InMemory);
316        let repositories = initialize_repositories(&config, None).await.unwrap();
317
318        // Count the repositories by checking Arc strong counts
319        // All should have at least 1 reference
320        let repo_refs = [
321            Arc::strong_count(&repositories.relayer),
322            Arc::strong_count(&repositories.transaction),
323            Arc::strong_count(&repositories.signer),
324            Arc::strong_count(&repositories.notification),
325            Arc::strong_count(&repositories.network),
326            Arc::strong_count(&repositories.transaction_counter),
327            Arc::strong_count(&repositories.plugin),
328            Arc::strong_count(&repositories.api_key),
329        ];
330
331        assert_eq!(repo_refs.len(), 8, "Expected exactly 8 repositories");
332        for (i, count) in repo_refs.iter().enumerate() {
333            assert!(*count >= 1, "Repository {i} has invalid Arc count: {count}");
334        }
335    }
336
337    #[tokio::test]
338    async fn test_repository_delete_operations() {
339        let config = create_test_server_config(RepositoryStorageType::InMemory);
340        let repositories = initialize_repositories(&config, None).await.unwrap();
341
342        // Create and then delete items
343        let relayer = create_mock_relayer("delete-test".to_string(), false);
344        repositories.relayer.create(relayer).await.unwrap();
345
346        // Verify item exists
347        let exists = repositories
348            .relayer
349            .get_by_id("delete-test".to_string())
350            .await;
351        assert!(exists.is_ok());
352
353        // Delete the item
354        let delete_result = repositories
355            .relayer
356            .delete_by_id("delete-test".to_string())
357            .await;
358        assert!(delete_result.is_ok());
359
360        // Verify item is gone
361        let after_delete = repositories
362            .relayer
363            .get_by_id("delete-test".to_string())
364            .await;
365        assert!(after_delete.is_err() || after_delete.unwrap().id != "delete-test");
366    }
367
368    #[tokio::test]
369    async fn test_repository_update_operations() {
370        let config = create_test_server_config(RepositoryStorageType::InMemory);
371        let repositories = initialize_repositories(&config, None).await.unwrap();
372
373        // Create a relayer
374        let relayer = create_mock_relayer("update-test".to_string(), false);
375        repositories.relayer.create(relayer.clone()).await.unwrap();
376
377        // Update the relayer (enable it)
378        let mut updated_relayer = relayer.clone();
379        updated_relayer.system_disabled = true;
380
381        let update_result = repositories
382            .relayer
383            .update("update-test".to_string(), updated_relayer)
384            .await;
385        assert!(update_result.is_ok());
386
387        // Verify the update
388        let retrieved = repositories
389            .relayer
390            .get_by_id("update-test".to_string())
391            .await
392            .unwrap();
393        assert!(retrieved.system_disabled);
394    }
395
396    #[tokio::test]
397    async fn test_repository_list_operations() {
398        let config = create_test_server_config(RepositoryStorageType::InMemory);
399        let repositories = initialize_repositories(&config, None).await.unwrap();
400
401        // Create multiple relayers
402        for i in 0..5 {
403            let relayer = create_mock_relayer(format!("list-test-{i}"), false);
404            repositories.relayer.create(relayer).await.unwrap();
405        }
406
407        // List all relayers
408        let all_relayers = repositories.relayer.list_all().await.unwrap();
409        assert_eq!(all_relayers.len(), 5);
410
411        // Verify all items are present
412        for i in 0..5 {
413            let found = all_relayers
414                .iter()
415                .any(|r| r.id == format!("list-test-{i}"));
416            assert!(found, "Expected to find relayer list-test-{i}");
417        }
418    }
419
420    #[tokio::test]
421    async fn test_repository_collection_struct_fields() {
422        // Verify the RepositoryCollection struct has all expected fields
423        let config = create_test_server_config(RepositoryStorageType::InMemory);
424        let repos = initialize_repositories(&config, None).await.unwrap();
425
426        // Access all fields to ensure they exist and are properly typed
427        let _ = &repos.relayer;
428        let _ = &repos.transaction;
429        let _ = &repos.signer;
430        let _ = &repos.notification;
431        let _ = &repos.network;
432        let _ = &repos.transaction_counter;
433        let _ = &repos.plugin;
434        let _ = &repos.api_key;
435
436        // If we get here, all fields exist
437        assert!(true);
438    }
439}