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::{self, Queue},
8    models::{AppState, DefaultAppState},
9    repositories::{
10        ApiKeyRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage,
11        PluginRepositoryStorage, RelayerRepositoryStorage, SignerRepositoryStorage,
12        TransactionCounterRepositoryStorage, TransactionRepositoryStorage,
13    },
14    utils::{initialize_redis_connections, RedisConnections},
15};
16use actix_web::web;
17use color_eyre::Result;
18use std::sync::Arc;
19use tracing::warn;
20
21pub struct RepositoryCollection {
22    pub relayer: Arc<RelayerRepositoryStorage>,
23    pub transaction: Arc<TransactionRepositoryStorage>,
24    pub signer: Arc<SignerRepositoryStorage>,
25    pub notification: Arc<NotificationRepositoryStorage>,
26    pub network: Arc<NetworkRepositoryStorage>,
27    pub transaction_counter: Arc<TransactionCounterRepositoryStorage>,
28    pub plugin: Arc<PluginRepositoryStorage>,
29    pub api_key: Arc<ApiKeyRepositoryStorage>,
30}
31
32/// Initializes repositories based on the server configuration
33///
34/// # Arguments
35///
36/// * `config` - Server configuration
37/// * `connections` - Redis connections (required for Redis storage type, None for in-memory)
38///
39/// # Returns
40///
41/// * `Result<RepositoryCollection>` - Initialized repositories
42///
43/// # Errors
44pub async fn initialize_repositories(
45    config: &ServerConfig,
46    connections: Option<Arc<RedisConnections>>,
47) -> eyre::Result<RepositoryCollection> {
48    let repositories = match config.repository_storage_type {
49        RepositoryStorageType::InMemory => RepositoryCollection {
50            relayer: Arc::new(RelayerRepositoryStorage::new_in_memory()),
51            transaction: Arc::new(TransactionRepositoryStorage::new_in_memory()),
52            signer: Arc::new(SignerRepositoryStorage::new_in_memory()),
53            notification: Arc::new(NotificationRepositoryStorage::new_in_memory()),
54            network: Arc::new(NetworkRepositoryStorage::new_in_memory()),
55            transaction_counter: Arc::new(TransactionCounterRepositoryStorage::new_in_memory()),
56            plugin: Arc::new(PluginRepositoryStorage::new_in_memory()),
57            api_key: Arc::new(ApiKeyRepositoryStorage::new_in_memory()),
58        },
59        RepositoryStorageType::Redis => {
60            if config.storage_encryption_key.is_none() {
61                warn!("⚠️ Storage encryption key is not set. Please set the STORAGE_ENCRYPTION_KEY environment variable.");
62                return Err(eyre::eyre!("Storage encryption key is not set. Please set the STORAGE_ENCRYPTION_KEY environment variable."));
63            }
64
65            let connections = connections
66                .ok_or_else(|| eyre::eyre!("Redis connections required for Redis storage type"))?;
67
68            // Use the shared connections for all repositories
69            RepositoryCollection {
70                relayer: Arc::new(RelayerRepositoryStorage::new_redis(
71                    connections.clone(),
72                    config.redis_key_prefix.clone(),
73                )?),
74                transaction: Arc::new(TransactionRepositoryStorage::new_redis(
75                    connections.clone(),
76                    config.redis_key_prefix.clone(),
77                )?),
78                signer: Arc::new(SignerRepositoryStorage::new_redis(
79                    connections.clone(),
80                    config.redis_key_prefix.clone(),
81                )?),
82                notification: Arc::new(NotificationRepositoryStorage::new_redis(
83                    connections.clone(),
84                    config.redis_key_prefix.clone(),
85                )?),
86                network: Arc::new(NetworkRepositoryStorage::new_redis(
87                    connections.clone(),
88                    config.redis_key_prefix.clone(),
89                )?),
90                transaction_counter: Arc::new(TransactionCounterRepositoryStorage::new_redis(
91                    connections.clone(),
92                    config.redis_key_prefix.clone(),
93                )?),
94                plugin: Arc::new(PluginRepositoryStorage::new_redis(
95                    connections.clone(),
96                    config.redis_key_prefix.clone(),
97                )?),
98                api_key: Arc::new(ApiKeyRepositoryStorage::new_redis(
99                    connections,
100                    config.redis_key_prefix.clone(),
101                )?),
102            }
103        }
104    };
105
106    Ok(repositories)
107}
108
109/// Initializes application state
110///
111/// # Returns
112///
113/// * `Result<web::Data<AppState>>` - Initialized application state
114///
115/// # Errors
116///
117/// Returns error if:
118/// - Repository initialization fails
119/// - Configuration loading fails
120pub async fn initialize_app_state(
121    server_config: Arc<ServerConfig>,
122) -> Result<web::ThinData<DefaultAppState>> {
123    // Always initialize Redis connections - required for the job queue.
124    // When REDIS_READER_URL is set, read operations use the reader endpoint.
125    // The queue uses deadpool for better connection lifecycle management.
126    let redis_connections = initialize_redis_connections(&server_config).await?;
127
128    // For repositories, pass connections based on storage type
129    let repo_connections = match server_config.repository_storage_type {
130        RepositoryStorageType::Redis => Some(redis_connections.clone()),
131        RepositoryStorageType::InMemory => None,
132    };
133
134    let repositories = initialize_repositories(&server_config, repo_connections).await?;
135
136    // Queue always uses Redis with deadpool connections
137    let queue = Queue::setup(redis_connections).await?;
138    let job_producer = Arc::new(jobs::JobProducer::new(queue.clone()));
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: {}",
265            error
266        );
267    }
268
269    #[tokio::test]
270    async fn test_initialize_repositories_redis_without_pool() {
271        let mut config = create_test_server_config(RepositoryStorageType::Redis);
272        // Set encryption key so we get past that check
273        config.storage_encryption_key = Some(SecretString::new("test-encryption-key-32-bytes!!!"));
274
275        // Pass None for pool - should fail
276        let result = initialize_repositories(&config, None).await;
277
278        assert!(result.is_err());
279        let error = match result {
280            Err(e) => e,
281            Ok(_) => panic!("Expected error for missing pool"),
282        };
283        assert!(
284            error
285                .to_string()
286                .contains("Redis connections required for Redis storage type"),
287            "Expected error about Redis pool being required, got: {}",
288            error
289        );
290    }
291
292    #[tokio::test]
293    async fn test_initialize_repositories_in_memory_ignores_pool() {
294        // For in-memory storage, providing a pool should be fine (it's ignored)
295        // We can't easily create a real pool without Redis, but we can test with None
296        let config = create_test_server_config(RepositoryStorageType::InMemory);
297
298        // In-memory should work with None
299        let result = initialize_repositories(&config, None).await;
300        assert!(result.is_ok());
301
302        // Verify repositories are functional
303        let repositories = result.unwrap();
304        let relayer = create_mock_relayer("test-relayer".to_string(), false);
305        repositories.relayer.create(relayer).await.unwrap();
306        let retrieved = repositories
307            .relayer
308            .get_by_id("test-relayer".to_string())
309            .await
310            .unwrap();
311        assert_eq!(retrieved.id, "test-relayer");
312    }
313
314    #[tokio::test]
315    async fn test_repository_collection_all_eight_repositories() {
316        // Verify that RepositoryCollection contains exactly 8 repositories
317        let config = create_test_server_config(RepositoryStorageType::InMemory);
318        let repositories = initialize_repositories(&config, None).await.unwrap();
319
320        // Count the repositories by checking Arc strong counts
321        // All should have at least 1 reference
322        let repo_refs = vec![
323            Arc::strong_count(&repositories.relayer),
324            Arc::strong_count(&repositories.transaction),
325            Arc::strong_count(&repositories.signer),
326            Arc::strong_count(&repositories.notification),
327            Arc::strong_count(&repositories.network),
328            Arc::strong_count(&repositories.transaction_counter),
329            Arc::strong_count(&repositories.plugin),
330            Arc::strong_count(&repositories.api_key),
331        ];
332
333        assert_eq!(repo_refs.len(), 8, "Expected exactly 8 repositories");
334        for (i, count) in repo_refs.iter().enumerate() {
335            assert!(
336                *count >= 1,
337                "Repository {} has invalid Arc count: {}",
338                i,
339                count
340            );
341        }
342    }
343
344    #[tokio::test]
345    async fn test_repository_delete_operations() {
346        let config = create_test_server_config(RepositoryStorageType::InMemory);
347        let repositories = initialize_repositories(&config, None).await.unwrap();
348
349        // Create and then delete items
350        let relayer = create_mock_relayer("delete-test".to_string(), false);
351        repositories.relayer.create(relayer).await.unwrap();
352
353        // Verify item exists
354        let exists = repositories
355            .relayer
356            .get_by_id("delete-test".to_string())
357            .await;
358        assert!(exists.is_ok());
359
360        // Delete the item
361        let delete_result = repositories
362            .relayer
363            .delete_by_id("delete-test".to_string())
364            .await;
365        assert!(delete_result.is_ok());
366
367        // Verify item is gone
368        let after_delete = repositories
369            .relayer
370            .get_by_id("delete-test".to_string())
371            .await;
372        assert!(after_delete.is_err() || after_delete.unwrap().id != "delete-test");
373    }
374
375    #[tokio::test]
376    async fn test_repository_update_operations() {
377        let config = create_test_server_config(RepositoryStorageType::InMemory);
378        let repositories = initialize_repositories(&config, None).await.unwrap();
379
380        // Create a relayer
381        let relayer = create_mock_relayer("update-test".to_string(), false);
382        repositories.relayer.create(relayer.clone()).await.unwrap();
383
384        // Update the relayer (enable it)
385        let mut updated_relayer = relayer.clone();
386        updated_relayer.system_disabled = true;
387
388        let update_result = repositories
389            .relayer
390            .update("update-test".to_string(), updated_relayer)
391            .await;
392        assert!(update_result.is_ok());
393
394        // Verify the update
395        let retrieved = repositories
396            .relayer
397            .get_by_id("update-test".to_string())
398            .await
399            .unwrap();
400        assert!(retrieved.system_disabled);
401    }
402
403    #[tokio::test]
404    async fn test_repository_list_operations() {
405        let config = create_test_server_config(RepositoryStorageType::InMemory);
406        let repositories = initialize_repositories(&config, None).await.unwrap();
407
408        // Create multiple relayers
409        for i in 0..5 {
410            let relayer = create_mock_relayer(format!("list-test-{}", i), false);
411            repositories.relayer.create(relayer).await.unwrap();
412        }
413
414        // List all relayers
415        let all_relayers = repositories.relayer.list_all().await.unwrap();
416        assert_eq!(all_relayers.len(), 5);
417
418        // Verify all items are present
419        for i in 0..5 {
420            let found = all_relayers
421                .iter()
422                .any(|r| r.id == format!("list-test-{}", i));
423            assert!(found, "Expected to find relayer list-test-{}", i);
424        }
425    }
426
427    #[tokio::test]
428    async fn test_repository_collection_struct_fields() {
429        // Verify the RepositoryCollection struct has all expected fields
430        let config = create_test_server_config(RepositoryStorageType::InMemory);
431        let repos = initialize_repositories(&config, None).await.unwrap();
432
433        // Access all fields to ensure they exist and are properly typed
434        let _ = &repos.relayer;
435        let _ = &repos.transaction;
436        let _ = &repos.signer;
437        let _ = &repos.notification;
438        let _ = &repos.network;
439        let _ = &repos.transaction_counter;
440        let _ = &repos.plugin;
441        let _ = &repos.api_key;
442
443        // If we get here, all fields exist
444        assert!(true);
445    }
446}