openzeppelin_relayer/api/controllers/
signer.rs

1//! # Signers Controller
2//!
3//! Handles HTTP endpoints for signer operations including:
4//! - Listing signers
5//! - Getting signer details
6//! - Creating signers
7//! - Updating signers
8//! - Deleting signers
9
10use crate::{
11    jobs::JobProducerTrait,
12    models::{
13        ApiError, ApiResponse, NetworkRepoModel, NotificationRepoModel, PaginationMeta,
14        PaginationQuery, RelayerRepoModel, Signer, SignerCreateRequest, SignerRepoModel,
15        SignerResponse, SignerUpdateRequest, ThinDataAppState, TransactionRepoModel,
16    },
17    repositories::{
18        ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
19        Repository, TransactionCounterTrait, TransactionRepository,
20    },
21};
22use actix_web::HttpResponse;
23use eyre::Result;
24
25/// Lists all signers with pagination support.
26///
27/// # Arguments
28///
29/// * `query` - The pagination query parameters.
30/// * `state` - The application state containing the signer repository.
31///
32/// # Returns
33///
34/// A paginated list of signers.
35pub async fn list_signers<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
36    query: PaginationQuery,
37    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
38) -> Result<HttpResponse, ApiError>
39where
40    J: JobProducerTrait + Send + Sync + 'static,
41    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
42    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
43    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
44    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
45    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
46    TCR: TransactionCounterTrait + Send + Sync + 'static,
47    PR: PluginRepositoryTrait + Send + Sync + 'static,
48    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
49{
50    let signers = state.signer_repository.list_paginated(query).await?;
51
52    let mapped_signers: Vec<SignerResponse> = signers.items.into_iter().map(|s| s.into()).collect();
53
54    Ok(HttpResponse::Ok().json(ApiResponse::paginated(
55        mapped_signers,
56        PaginationMeta {
57            total_items: signers.total,
58            current_page: signers.page,
59            per_page: signers.per_page,
60        },
61    )))
62}
63
64/// Retrieves details of a specific signer by ID.
65///
66/// # Arguments
67///
68/// * `signer_id` - The ID of the signer to retrieve.
69/// * `state` - The application state containing the signer repository.
70///
71/// # Returns
72///
73/// The signer details or an error if not found.
74pub async fn get_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
75    signer_id: String,
76    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
77) -> Result<HttpResponse, ApiError>
78where
79    J: JobProducerTrait + Send + Sync + 'static,
80    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
81    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
82    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
83    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
84    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
85    TCR: TransactionCounterTrait + Send + Sync + 'static,
86    PR: PluginRepositoryTrait + Send + Sync + 'static,
87    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
88{
89    let signer = state.signer_repository.get_by_id(signer_id).await?;
90
91    let response = SignerResponse::from(signer);
92    Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
93}
94
95/// Creates a new signer.
96///
97/// # Arguments
98///
99/// * `request` - The signer creation request.
100/// * `state` - The application state containing the signer repository.
101///
102/// # Returns
103///
104/// The created signer or an error if creation fails.
105///
106/// # Note
107///
108/// This endpoint only creates the signer metadata. The actual signer configuration
109/// (keys, credentials, etc.) should be provided through configuration files or
110/// other secure channels. This is a security measure to prevent sensitive data
111/// from being transmitted through API requests.
112pub async fn create_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
113    request: SignerCreateRequest,
114    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
115) -> Result<HttpResponse, ApiError>
116where
117    J: JobProducerTrait + Send + Sync + 'static,
118    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
119    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
120    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
121    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
122    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
123    TCR: TransactionCounterTrait + Send + Sync + 'static,
124    PR: PluginRepositoryTrait + Send + Sync + 'static,
125    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
126{
127    // Convert request to domain model (validates automatically and includes placeholder config)
128    let signer = Signer::try_from(request)?;
129
130    // Convert domain model to repository model
131    let signer_model = SignerRepoModel::from(signer);
132
133    let created_signer = state.signer_repository.create(signer_model).await?;
134
135    let response = SignerResponse::from(created_signer);
136    Ok(HttpResponse::Created().json(ApiResponse::success(response)))
137}
138
139/// Updates an existing signer.
140///
141/// # Arguments
142///
143/// * `signer_id` - The ID of the signer to update.
144/// * `request` - The signer update request.
145/// * `state` - The application state containing the signer repository.
146///
147/// # Returns
148///
149/// An error indicating that updates are not allowed.
150///
151/// # Note
152///
153/// Signer updates are not supported for security reasons. To modify a signer,
154/// delete the existing one and create a new signer with the desired configuration.
155pub async fn update_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
156    _signer_id: String,
157    _request: SignerUpdateRequest,
158    _state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
159) -> Result<HttpResponse, ApiError>
160where
161    J: JobProducerTrait + Send + Sync + 'static,
162    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
163    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
164    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
165    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
166    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
167    TCR: TransactionCounterTrait + Send + Sync + 'static,
168    PR: PluginRepositoryTrait + Send + Sync + 'static,
169    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
170{
171    Err(ApiError::BadRequest(
172        "Signer updates are not allowed for security reasons. Please delete the existing signer and create a new one with the desired configuration.".to_string()
173    ))
174}
175
176/// Deletes a signer by ID.
177///
178/// # Arguments
179///
180/// * `signer_id` - The ID of the signer to delete.
181/// * `state` - The application state containing the signer repository.
182///
183/// # Returns
184///
185/// A success response or an error if deletion fails.
186///
187/// # Security
188///
189/// This endpoint ensures that signers cannot be deleted if they are still being
190/// used by any relayers. This prevents breaking existing relayer configurations
191/// and maintains system integrity.
192pub async fn delete_signer<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
193    signer_id: String,
194    state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
195) -> Result<HttpResponse, ApiError>
196where
197    J: JobProducerTrait + Send + Sync + 'static,
198    RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
199    TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
200    NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
201    NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
202    SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
203    TCR: TransactionCounterTrait + Send + Sync + 'static,
204    PR: PluginRepositoryTrait + Send + Sync + 'static,
205    AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
206{
207    // First check if the signer exists
208    let _signer = state.signer_repository.get_by_id(signer_id.clone()).await?;
209
210    // Check if any relayers are using this signer
211    let connected_relayers = state
212        .relayer_repository
213        .list_by_signer_id(&signer_id)
214        .await?;
215
216    if !connected_relayers.is_empty() {
217        let relayer_names: Vec<String> =
218            connected_relayers.iter().map(|r| r.name.clone()).collect();
219        return Err(ApiError::BadRequest(format!(
220            "Cannot delete signer '{}' because it is being used by {} relayer(s): {}. Please remove or reconfigure these relayers before deleting the signer.",
221            signer_id,
222            connected_relayers.len(),
223            relayer_names.join(", ")
224        )));
225    }
226
227    // Safe to delete - no relayers are using this signer
228    state.signer_repository.delete_by_id(signer_id).await?;
229
230    Ok(HttpResponse::Ok().json(ApiResponse::success("Signer deleted successfully")))
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use crate::{
237        models::{
238            AwsKmsSignerConfigStorage, AwsKmsSignerRequestConfig,
239            GoogleCloudKmsSignerKeyRequestConfig, GoogleCloudKmsSignerRequestConfig,
240            GoogleCloudKmsSignerServiceAccountRequestConfig, LocalSignerConfigStorage,
241            LocalSignerRequestConfig, SignerConfigRequest, SignerConfigStorage, SignerType,
242            SignerTypeRequest, TurnkeySignerRequestConfig, VaultSignerRequestConfig,
243        },
244        utils::mocks::mockutils::create_mock_app_state,
245    };
246    use secrets::SecretVec;
247
248    /// Helper function to create a test signer model
249    fn create_test_signer_model(id: &str, signer_type: SignerType) -> SignerRepoModel {
250        let config = match signer_type {
251            SignerType::Local => SignerConfigStorage::Local(LocalSignerConfigStorage {
252                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
253            }),
254            SignerType::AwsKms => SignerConfigStorage::AwsKms(AwsKmsSignerConfigStorage {
255                region: Some("us-east-1".to_string()),
256                key_id: "test-key-id".to_string(),
257            }),
258            _ => SignerConfigStorage::Local(LocalSignerConfigStorage {
259                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
260            }),
261        };
262
263        SignerRepoModel {
264            id: id.to_string(),
265            config,
266        }
267    }
268
269    /// Helper function to create a test signer create request
270    fn create_test_signer_create_request(
271        id: Option<String>,
272        signer_type: SignerType,
273    ) -> SignerCreateRequest {
274        use crate::models::{
275            AwsKmsSignerRequestConfig, LocalSignerRequestConfig, SignerConfigRequest,
276            SignerTypeRequest,
277        };
278
279        let (signer_type_req, config) = match signer_type {
280            SignerType::Local => (
281                SignerTypeRequest::Local,
282                SignerConfigRequest::Local(LocalSignerRequestConfig {
283                    key: "1111111111111111111111111111111111111111111111111111111111111111"
284                        .to_string(), // Valid 32-byte hex key
285                }),
286            ),
287            SignerType::AwsKms => (
288                SignerTypeRequest::AwsKms,
289                SignerConfigRequest::AwsKms(AwsKmsSignerRequestConfig {
290                    region: "us-east-1".to_string(),
291                    key_id: "test-key-id".to_string(),
292                }),
293            ),
294            _ => (
295                SignerTypeRequest::Local,
296                SignerConfigRequest::Local(LocalSignerRequestConfig {
297                    key: "placeholder-key".to_string(),
298                }),
299            ), // Use Local for other types in helper
300        };
301
302        SignerCreateRequest {
303            id,
304            signer_type: signer_type_req,
305            config,
306        }
307    }
308
309    /// Helper function to create a test signer update request
310    fn create_test_signer_update_request() -> SignerUpdateRequest {
311        SignerUpdateRequest {}
312    }
313
314    #[actix_web::test]
315    async fn test_list_signers_empty() {
316        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
317        let query = PaginationQuery {
318            page: 1,
319            per_page: 10,
320        };
321
322        let result = list_signers(query, actix_web::web::ThinData(app_state)).await;
323
324        assert!(result.is_ok());
325        let response = result.unwrap();
326        assert_eq!(response.status(), 200);
327
328        let body = actix_web::body::to_bytes(response.into_body())
329            .await
330            .unwrap();
331        let api_response: ApiResponse<Vec<SignerResponse>> = serde_json::from_slice(&body).unwrap();
332
333        assert!(api_response.success);
334        let data = api_response.data.unwrap();
335        assert_eq!(data.len(), 0);
336    }
337
338    #[actix_web::test]
339    async fn test_list_signers_with_data() {
340        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
341
342        // Create test signers
343        let signer1 = create_test_signer_model("test-1", SignerType::Local);
344        let signer2 = create_test_signer_model("test-2", SignerType::AwsKms);
345
346        app_state.signer_repository.create(signer1).await.unwrap();
347        app_state.signer_repository.create(signer2).await.unwrap();
348
349        let query = PaginationQuery {
350            page: 1,
351            per_page: 10,
352        };
353
354        let result = list_signers(query, actix_web::web::ThinData(app_state)).await;
355
356        assert!(result.is_ok());
357        let response = result.unwrap();
358        assert_eq!(response.status(), 200);
359
360        let body = actix_web::body::to_bytes(response.into_body())
361            .await
362            .unwrap();
363        let api_response: ApiResponse<Vec<SignerResponse>> = serde_json::from_slice(&body).unwrap();
364
365        assert!(api_response.success);
366        let data = api_response.data.unwrap();
367        assert_eq!(data.len(), 2);
368
369        // Check that both signers are present
370        let ids: Vec<&String> = data.iter().map(|s| &s.id).collect();
371        assert!(ids.contains(&&"test-1".to_string()));
372        assert!(ids.contains(&&"test-2".to_string()));
373    }
374
375    #[actix_web::test]
376    async fn test_get_signer_success() {
377        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
378
379        // Create a test signer
380        let signer = create_test_signer_model("test-signer", SignerType::Local);
381        app_state
382            .signer_repository
383            .create(signer.clone())
384            .await
385            .unwrap();
386
387        let result = get_signer(
388            "test-signer".to_string(),
389            actix_web::web::ThinData(app_state),
390        )
391        .await;
392
393        assert!(result.is_ok());
394        let response = result.unwrap();
395        assert_eq!(response.status(), 200);
396
397        let body = actix_web::body::to_bytes(response.into_body())
398            .await
399            .unwrap();
400        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
401
402        assert!(api_response.success);
403        let data = api_response.data.unwrap();
404        assert_eq!(data.id, "test-signer");
405        assert_eq!(data.r#type, SignerType::Local);
406    }
407
408    #[actix_web::test]
409    async fn test_get_signer_not_found() {
410        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
411
412        let result = get_signer(
413            "non-existent".to_string(),
414            actix_web::web::ThinData(app_state),
415        )
416        .await;
417
418        assert!(result.is_err());
419        let error = result.unwrap_err();
420        assert!(matches!(error, ApiError::NotFound(_)));
421    }
422
423    #[actix_web::test]
424    async fn test_create_signer_test_type_success() {
425        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
426
427        let request = create_test_signer_create_request(
428            Some("new-test-signer".to_string()),
429            SignerType::Local,
430        );
431
432        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
433
434        assert!(result.is_ok());
435        let response = result.unwrap();
436        assert_eq!(response.status(), 201);
437
438        let body = actix_web::body::to_bytes(response.into_body())
439            .await
440            .unwrap();
441        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
442
443        assert!(api_response.success);
444        let data = api_response.data.unwrap();
445        assert_eq!(data.id, "new-test-signer");
446        assert_eq!(data.r#type, SignerType::Local);
447    }
448
449    #[actix_web::test]
450    async fn test_create_signer_with_valid_configs() {
451        // Test Local signer with valid key
452        let app_state1 = create_mock_app_state(None, None, None, None, None, None).await;
453        let request =
454            create_test_signer_create_request(Some("local-test".to_string()), SignerType::Local);
455        let result = create_signer(request, actix_web::web::ThinData(app_state1)).await;
456        assert!(result.is_ok(), "Local signer with valid key should succeed");
457
458        // Test AWS KMS signer with valid config
459        let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
460        let request =
461            create_test_signer_create_request(Some("aws-test".to_string()), SignerType::AwsKms);
462        let result = create_signer(request, actix_web::web::ThinData(app_state2)).await;
463        assert!(
464            result.is_ok(),
465            "AWS KMS signer with valid config should succeed"
466        );
467    }
468
469    #[actix_web::test]
470    async fn test_create_signer_local_with_valid_key() {
471        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
472
473        let request = SignerCreateRequest {
474            id: Some("local-signer-test".to_string()),
475            signer_type: SignerTypeRequest::Local,
476            config: SignerConfigRequest::Local(LocalSignerRequestConfig {
477                key: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(), // 32 bytes as hex
478            }),
479        };
480
481        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
482
483        assert!(result.is_ok());
484        let response = result.unwrap();
485        assert_eq!(response.status(), 201);
486
487        let body = actix_web::body::to_bytes(response.into_body())
488            .await
489            .unwrap();
490        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
491
492        assert!(api_response.success);
493        let data = api_response.data.unwrap();
494        assert_eq!(data.id, "local-signer-test");
495        assert_eq!(data.r#type, SignerType::Local);
496    }
497
498    #[actix_web::test]
499    async fn test_create_signer_aws_kms_comprehensive() {
500        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
501
502        let request = SignerCreateRequest {
503            id: Some("aws-kms-signer".to_string()),
504            signer_type: SignerTypeRequest::AwsKms,
505            config: SignerConfigRequest::AwsKms(AwsKmsSignerRequestConfig {
506                region: "us-west-2".to_string(),
507                key_id:
508                    "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
509                        .to_string(),
510            }),
511        };
512
513        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
514
515        assert!(result.is_ok());
516        let response = result.unwrap();
517        assert_eq!(response.status(), 201);
518
519        let body = actix_web::body::to_bytes(response.into_body())
520            .await
521            .unwrap();
522        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
523
524        assert!(api_response.success);
525        let data = api_response.data.unwrap();
526        assert_eq!(data.id, "aws-kms-signer");
527        assert_eq!(data.r#type, SignerType::AwsKms);
528    }
529
530    #[actix_web::test]
531    async fn test_create_signer_vault() {
532        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
533
534        let request = SignerCreateRequest {
535            id: Some("vault-signer".to_string()),
536            signer_type: SignerTypeRequest::Vault,
537            config: SignerConfigRequest::Vault(VaultSignerRequestConfig {
538                address: "https://vault.example.com:8200".to_string(),
539                namespace: Some("development".to_string()),
540                role_id: "test-role-id-12345".to_string(),
541                secret_id: "test-secret-id-67890".to_string(),
542                key_name: "ethereum-key".to_string(),
543                mount_point: Some("secret".to_string()),
544            }),
545        };
546
547        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
548
549        assert!(result.is_ok());
550        let response = result.unwrap();
551        assert_eq!(response.status(), 201);
552
553        let body = actix_web::body::to_bytes(response.into_body())
554            .await
555            .unwrap();
556        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
557
558        assert!(api_response.success);
559        let data = api_response.data.unwrap();
560        assert_eq!(data.id, "vault-signer");
561        assert_eq!(data.r#type, SignerType::Vault);
562    }
563
564    #[actix_web::test]
565    async fn test_create_signer_vault_transit() {
566        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
567
568        use crate::models::{
569            SignerConfigRequest, SignerTypeRequest, VaultTransitSignerRequestConfig,
570        };
571        let request = SignerCreateRequest {
572            id: Some("vault-transit-signer".to_string()),
573            signer_type: SignerTypeRequest::VaultTransit,
574            config: SignerConfigRequest::VaultTransit(VaultTransitSignerRequestConfig {
575                key_name: "ethereum-transit-key".to_string(),
576                address: "https://vault.example.com:8200".to_string(),
577                namespace: None,
578                role_id: "transit-role-id-12345".to_string(),
579                secret_id: "transit-secret-id-67890".to_string(),
580                pubkey: "0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235".to_string(),
581                mount_point: Some("transit".to_string()),
582            }),
583        };
584
585        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
586
587        assert!(result.is_ok());
588        let response = result.unwrap();
589        assert_eq!(response.status(), 201);
590
591        let body = actix_web::body::to_bytes(response.into_body())
592            .await
593            .unwrap();
594        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
595
596        assert!(api_response.success);
597        let data = api_response.data.unwrap();
598        assert_eq!(data.id, "vault-transit-signer");
599        assert_eq!(data.r#type, SignerType::VaultTransit);
600    }
601
602    #[actix_web::test]
603    async fn test_create_signer_turnkey() {
604        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
605
606        let request = SignerCreateRequest {
607            id: Some("turnkey-signer".to_string()),
608            signer_type: SignerTypeRequest::Turnkey,
609            config: SignerConfigRequest::Turnkey(TurnkeySignerRequestConfig {
610                api_public_key: "turnkey-api-public-key-example".to_string(),
611                api_private_key: "turnkey-api-private-key-example".to_string(),
612                organization_id: "turnkey-org-12345".to_string(),
613                private_key_id: "turnkey-private-key-67890".to_string(),
614                public_key: "0x04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235".to_string(),
615            }),
616        };
617
618        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
619
620        assert!(result.is_ok());
621        let response = result.unwrap();
622        assert_eq!(response.status(), 201);
623
624        let body = actix_web::body::to_bytes(response.into_body())
625            .await
626            .unwrap();
627        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
628
629        assert!(api_response.success);
630        let data = api_response.data.unwrap();
631        assert_eq!(data.id, "turnkey-signer");
632        assert_eq!(data.r#type, SignerType::Turnkey);
633    }
634
635    #[actix_web::test]
636    async fn test_create_signer_google_cloud_kms() {
637        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
638
639        let request = SignerCreateRequest {
640            id: Some("gcp-kms-signer".to_string()),
641            signer_type: SignerTypeRequest::GoogleCloudKms,
642            config: SignerConfigRequest::GoogleCloudKms(GoogleCloudKmsSignerRequestConfig {
643                service_account: GoogleCloudKmsSignerServiceAccountRequestConfig {
644                    private_key: "-----BEGIN EXAMPLE PRIVATE KEY-----\nSDFGSDFGSDGSDFGSDFGSDFGSDFGSDFGSAFAS...\n-----END EXAMPLE PRIVATE KEY-----\n".to_string(), // noboost
645                    private_key_id: "gcp-private-key-id-12345".to_string(),
646                    project_id: "my-gcp-project".to_string(),
647                    client_email: "service-account@my-gcp-project.iam.gserviceaccount.com".to_string(),
648                    client_id: "123456789012345678901".to_string(),
649                    auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(),
650                    token_uri: "https://oauth2.googleapis.com/token".to_string(),
651                    auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(),
652                    client_x509_cert_url: "https://www.googleapis.com/robot/v1/metadata/x509/service-account%40my-gcp-project.iam.gserviceaccount.com".to_string(),
653                    universe_domain: "googleapis.com".to_string(),
654                },
655                key: GoogleCloudKmsSignerKeyRequestConfig {
656                    location: "global".to_string(),
657                    key_ring_id: "ethereum-keyring".to_string(),
658                    key_id: "ethereum-signing-key".to_string(),
659                    key_version: 1,
660                },
661            }),
662        };
663
664        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
665
666        assert!(result.is_ok());
667        let response = result.unwrap();
668        assert_eq!(response.status(), 201);
669
670        let body = actix_web::body::to_bytes(response.into_body())
671            .await
672            .unwrap();
673        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
674
675        assert!(api_response.success);
676        let data = api_response.data.unwrap();
677        assert_eq!(data.id, "gcp-kms-signer");
678        assert_eq!(data.r#type, SignerType::GoogleCloudKms);
679    }
680
681    #[actix_web::test]
682    async fn test_create_signer_auto_generated_id() {
683        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
684
685        let request = SignerCreateRequest {
686            id: None, // Let the system generate an ID
687            signer_type: SignerTypeRequest::Local,
688            config: SignerConfigRequest::Local(LocalSignerRequestConfig {
689                key: "fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string(),
690            }),
691        };
692
693        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
694
695        assert!(result.is_ok());
696        let response = result.unwrap();
697        assert_eq!(response.status(), 201);
698
699        let body = actix_web::body::to_bytes(response.into_body())
700            .await
701            .unwrap();
702        let api_response: ApiResponse<SignerResponse> = serde_json::from_slice(&body).unwrap();
703
704        assert!(api_response.success);
705        let data = api_response.data.unwrap();
706        assert!(!data.id.is_empty());
707        assert!(uuid::Uuid::parse_str(&data.id).is_ok()); // Should be a valid UUID
708        assert_eq!(data.r#type, SignerType::Local);
709    }
710
711    #[actix_web::test]
712    async fn test_create_signer_invalid_local_key() {
713        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
714
715        let request = SignerCreateRequest {
716            id: Some("invalid-key-signer".to_string()),
717            signer_type: SignerTypeRequest::Local,
718            config: SignerConfigRequest::Local(LocalSignerRequestConfig {
719                key: "invalid-hex-key".to_string(), // Invalid hex
720            }),
721        };
722
723        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
724
725        assert!(result.is_err());
726        if let Err(ApiError::BadRequest(msg)) = result {
727            assert!(msg.contains("Invalid hex key format"));
728        } else {
729            panic!("Expected BadRequest error for invalid hex key");
730        }
731    }
732
733    #[actix_web::test]
734    async fn test_create_signer_invalid_vault_address() {
735        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
736
737        let request = SignerCreateRequest {
738            id: Some("invalid-vault-signer".to_string()),
739            signer_type: SignerTypeRequest::Vault,
740            config: SignerConfigRequest::Vault(VaultSignerRequestConfig {
741                address: "not-a-valid-url".to_string(), // Invalid URL
742                namespace: None,
743                role_id: "test-role".to_string(),
744                secret_id: "test-secret".to_string(),
745                key_name: "test-key".to_string(),
746                mount_point: None,
747            }),
748        };
749
750        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
751
752        assert!(result.is_err());
753        if let Err(ApiError::BadRequest(msg)) = result {
754            assert!(msg.contains("Address must be a valid URL"));
755        } else {
756            panic!("Expected BadRequest error for invalid Vault address");
757        }
758    }
759
760    #[actix_web::test]
761    async fn test_create_signer_empty_aws_kms_key_id() {
762        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
763
764        let request = SignerCreateRequest {
765            id: Some("empty-key-id-signer".to_string()),
766            signer_type: SignerTypeRequest::AwsKms,
767            config: SignerConfigRequest::AwsKms(AwsKmsSignerRequestConfig {
768                region: "us-east-1".to_string(),
769                key_id: "".to_string(), // Empty key ID
770            }),
771        };
772
773        let result = create_signer(request, actix_web::web::ThinData(app_state)).await;
774
775        assert!(result.is_err());
776        if let Err(ApiError::BadRequest(msg)) = result {
777            assert!(msg.contains("Key ID cannot be empty"));
778        } else {
779            panic!("Expected BadRequest error for empty AWS KMS key ID");
780        }
781    }
782
783    #[actix_web::test]
784    async fn test_update_signer_not_allowed() {
785        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
786
787        // Create a test signer
788        let signer = create_test_signer_model("test-signer", SignerType::Local);
789        app_state.signer_repository.create(signer).await.unwrap();
790
791        let update_request = create_test_signer_update_request();
792
793        let result = update_signer(
794            "test-signer".to_string(),
795            update_request,
796            actix_web::web::ThinData(app_state),
797        )
798        .await;
799
800        assert!(result.is_err());
801        let error = result.unwrap_err();
802        if let ApiError::BadRequest(msg) = error {
803            assert!(msg.contains("Signer updates are not allowed"));
804            assert!(msg.contains("delete the existing signer and create a new one"));
805        } else {
806            panic!("Expected BadRequest error");
807        }
808    }
809
810    #[actix_web::test]
811    async fn test_update_signer_always_fails() {
812        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
813
814        let update_request = create_test_signer_update_request();
815
816        let result = update_signer(
817            "non-existent".to_string(),
818            update_request,
819            actix_web::web::ThinData(app_state),
820        )
821        .await;
822
823        assert!(result.is_err());
824        let error = result.unwrap_err();
825        if let ApiError::BadRequest(msg) = error {
826            assert!(msg.contains("Signer updates are not allowed"));
827        } else {
828            panic!("Expected BadRequest error");
829        }
830    }
831
832    #[actix_web::test]
833    async fn test_delete_signer_success() {
834        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
835
836        // Create a test signer
837        let signer = create_test_signer_model("test-signer", SignerType::Local);
838        app_state.signer_repository.create(signer).await.unwrap();
839
840        let result = delete_signer(
841            "test-signer".to_string(),
842            actix_web::web::ThinData(app_state),
843        )
844        .await;
845
846        assert!(result.is_ok());
847        let response = result.unwrap();
848        assert_eq!(response.status(), 200);
849
850        let body = actix_web::body::to_bytes(response.into_body())
851            .await
852            .unwrap();
853        let api_response: ApiResponse<&str> = serde_json::from_slice(&body).unwrap();
854
855        assert!(api_response.success);
856        assert_eq!(api_response.data.unwrap(), "Signer deleted successfully");
857    }
858
859    #[actix_web::test]
860    async fn test_delete_signer_blocked_by_connected_relayers() {
861        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
862
863        // Create a test signer
864        let signer = create_test_signer_model("connected-signer", SignerType::Local);
865        app_state.signer_repository.create(signer).await.unwrap();
866
867        // Create a relayer that uses this signer
868        let relayer = crate::models::RelayerRepoModel {
869            id: "test-relayer".to_string(),
870            name: "Test Relayer".to_string(),
871            network: "ethereum".to_string(),
872            paused: false,
873            network_type: crate::models::NetworkType::Evm,
874            signer_id: "connected-signer".to_string(), // References our signer
875            policies: crate::models::RelayerNetworkPolicy::Evm(
876                crate::models::RelayerEvmPolicy::default(),
877            ),
878            address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
879            notification_id: None,
880            system_disabled: false,
881            custom_rpc_urls: None,
882            ..Default::default()
883        };
884        app_state.relayer_repository.create(relayer).await.unwrap();
885
886        // Try to delete the signer - should fail
887        let result = delete_signer(
888            "connected-signer".to_string(),
889            actix_web::web::ThinData(app_state),
890        )
891        .await;
892
893        assert!(result.is_err());
894        let error = result.unwrap_err();
895        if let ApiError::BadRequest(msg) = error {
896            assert!(msg.contains("Cannot delete signer"));
897            assert!(msg.contains("being used by"));
898            assert!(msg.contains("Test Relayer"));
899            assert!(msg.contains("remove or reconfigure"));
900        } else {
901            panic!("Expected BadRequest error");
902        }
903    }
904
905    #[actix_web::test]
906    async fn test_delete_signer_not_found() {
907        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
908
909        let result = delete_signer(
910            "non-existent".to_string(),
911            actix_web::web::ThinData(app_state),
912        )
913        .await;
914
915        assert!(result.is_err());
916        let error = result.unwrap_err();
917        assert!(matches!(error, ApiError::NotFound(_)));
918    }
919
920    #[actix_web::test]
921    async fn test_delete_signer_after_relayer_removed() {
922        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
923
924        // Create a test signer
925        let signer = create_test_signer_model("cleanup-signer", SignerType::Local);
926        app_state.signer_repository.create(signer).await.unwrap();
927
928        // Create a relayer that uses this signer
929        let relayer = crate::models::RelayerRepoModel {
930            id: "temp-relayer".to_string(),
931            name: "Temporary Relayer".to_string(),
932            network: "ethereum".to_string(),
933            paused: false,
934            network_type: crate::models::NetworkType::Evm,
935            signer_id: "cleanup-signer".to_string(),
936            policies: crate::models::RelayerNetworkPolicy::Evm(
937                crate::models::RelayerEvmPolicy::default(),
938            ),
939            address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
940            notification_id: None,
941            system_disabled: false,
942            custom_rpc_urls: None,
943            ..Default::default()
944        };
945        app_state.relayer_repository.create(relayer).await.unwrap();
946
947        // First deletion attempt should fail
948        let result = delete_signer(
949            "cleanup-signer".to_string(),
950            actix_web::web::ThinData(app_state),
951        )
952        .await;
953        assert!(result.is_err());
954
955        // Create new app state for second test (since app_state was consumed)
956        let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
957
958        // Re-create the signer in the new state
959        let signer2 = create_test_signer_model("cleanup-signer", SignerType::Local);
960        app_state2.signer_repository.create(signer2).await.unwrap();
961
962        // Now signer deletion should succeed (no relayers in new state)
963        let result = delete_signer(
964            "cleanup-signer".to_string(),
965            actix_web::web::ThinData(app_state2),
966        )
967        .await;
968
969        assert!(result.is_ok());
970        let response = result.unwrap();
971        assert_eq!(response.status(), 200);
972    }
973
974    #[actix_web::test]
975    async fn test_signer_response_conversion() {
976        let signer_model = SignerRepoModel {
977            id: "test-id".to_string(),
978            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
979                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
980            }),
981        };
982
983        let response = SignerResponse::from(signer_model);
984
985        assert_eq!(response.id, "test-id");
986        assert_eq!(response.r#type, SignerType::Local);
987    }
988
989    #[actix_web::test]
990    async fn test_delete_signer_with_multiple_relayers() {
991        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
992
993        // Create a test signer
994        let signer = create_test_signer_model("multi-relayer-signer", SignerType::AwsKms);
995        app_state.signer_repository.create(signer).await.unwrap();
996
997        // Create multiple relayers that use this signer
998        let relayers = vec![
999            crate::models::RelayerRepoModel {
1000                id: "relayer-1".to_string(),
1001                name: "EVM Relayer".to_string(),
1002                network: "ethereum".to_string(),
1003                paused: false,
1004                network_type: crate::models::NetworkType::Evm,
1005                signer_id: "multi-relayer-signer".to_string(),
1006                policies: crate::models::RelayerNetworkPolicy::Evm(
1007                    crate::models::RelayerEvmPolicy::default(),
1008                ),
1009                address: "0x1111111111111111111111111111111111111111".to_string(),
1010                notification_id: None,
1011                system_disabled: false,
1012                custom_rpc_urls: None,
1013                ..Default::default()
1014            },
1015            crate::models::RelayerRepoModel {
1016                id: "relayer-2".to_string(),
1017                name: "Solana Relayer".to_string(),
1018                network: "solana".to_string(),
1019                paused: true, // Even paused relayers should block deletion
1020                network_type: crate::models::NetworkType::Solana,
1021                signer_id: "multi-relayer-signer".to_string(),
1022                policies: crate::models::RelayerNetworkPolicy::Solana(
1023                    crate::models::RelayerSolanaPolicy::default(),
1024                ),
1025                address: "solana-address".to_string(),
1026                notification_id: None,
1027                system_disabled: false,
1028                custom_rpc_urls: None,
1029                ..Default::default()
1030            },
1031            crate::models::RelayerRepoModel {
1032                id: "relayer-3".to_string(),
1033                name: "Stellar Relayer".to_string(),
1034                network: "stellar".to_string(),
1035                paused: false,
1036                network_type: crate::models::NetworkType::Stellar,
1037                signer_id: "multi-relayer-signer".to_string(),
1038                policies: crate::models::RelayerNetworkPolicy::Stellar(
1039                    crate::models::RelayerStellarPolicy::default(),
1040                ),
1041                address: "stellar-address".to_string(),
1042                notification_id: None,
1043                system_disabled: true, // Even disabled relayers should block deletion
1044                custom_rpc_urls: None,
1045                ..Default::default()
1046            },
1047        ];
1048
1049        // Create all relayers
1050        for relayer in relayers {
1051            app_state.relayer_repository.create(relayer).await.unwrap();
1052        }
1053
1054        // Try to delete the signer - should fail with detailed error
1055        let result = delete_signer(
1056            "multi-relayer-signer".to_string(),
1057            actix_web::web::ThinData(app_state),
1058        )
1059        .await;
1060
1061        assert!(result.is_err());
1062        let error = result.unwrap_err();
1063        if let ApiError::BadRequest(msg) = error {
1064            assert!(msg.contains("Cannot delete signer 'multi-relayer-signer'"));
1065            assert!(msg.contains("being used by 3 relayer(s)"));
1066            assert!(msg.contains("EVM Relayer"));
1067            assert!(msg.contains("Solana Relayer"));
1068            assert!(msg.contains("Stellar Relayer"));
1069            assert!(msg.contains("remove or reconfigure"));
1070        } else {
1071            panic!("Expected BadRequest error, got: {:?}", error);
1072        }
1073    }
1074
1075    #[actix_web::test]
1076    async fn test_delete_signer_with_some_relayers_using_different_signer() {
1077        let app_state = create_mock_app_state(None, None, None, None, None, None).await;
1078
1079        // Create two test signers
1080        let signer1 = create_test_signer_model("signer-to-delete", SignerType::Local);
1081        let signer2 = create_test_signer_model("other-signer", SignerType::AwsKms);
1082        app_state.signer_repository.create(signer1).await.unwrap();
1083        app_state.signer_repository.create(signer2).await.unwrap();
1084
1085        // Create relayers - only one uses the signer we want to delete
1086        let relayer1 = crate::models::RelayerRepoModel {
1087            id: "blocking-relayer".to_string(),
1088            name: "Blocking Relayer".to_string(),
1089            network: "ethereum".to_string(),
1090            paused: false,
1091            network_type: crate::models::NetworkType::Evm,
1092            signer_id: "signer-to-delete".to_string(), // This one blocks deletion
1093            policies: crate::models::RelayerNetworkPolicy::Evm(
1094                crate::models::RelayerEvmPolicy::default(),
1095            ),
1096            address: "0x1111111111111111111111111111111111111111".to_string(),
1097            notification_id: None,
1098            system_disabled: false,
1099            custom_rpc_urls: None,
1100            ..Default::default()
1101        };
1102
1103        let relayer2 = crate::models::RelayerRepoModel {
1104            id: "non-blocking-relayer".to_string(),
1105            name: "Non-blocking Relayer".to_string(),
1106            network: "polygon".to_string(),
1107            paused: false,
1108            network_type: crate::models::NetworkType::Evm,
1109            signer_id: "other-signer".to_string(), // This one uses different signer
1110            policies: crate::models::RelayerNetworkPolicy::Evm(
1111                crate::models::RelayerEvmPolicy::default(),
1112            ),
1113            address: "0x2222222222222222222222222222222222222222".to_string(),
1114            notification_id: None,
1115            system_disabled: false,
1116            custom_rpc_urls: None,
1117            ..Default::default()
1118        };
1119
1120        app_state.relayer_repository.create(relayer1).await.unwrap();
1121        app_state.relayer_repository.create(relayer2).await.unwrap();
1122
1123        // Try to delete the first signer - should fail because of one relayer
1124        let result = delete_signer(
1125            "signer-to-delete".to_string(),
1126            actix_web::web::ThinData(app_state),
1127        )
1128        .await;
1129
1130        assert!(result.is_err());
1131        let error = result.unwrap_err();
1132        if let ApiError::BadRequest(msg) = error {
1133            assert!(msg.contains("being used by 1 relayer(s)"));
1134            assert!(msg.contains("Blocking Relayer"));
1135            assert!(!msg.contains("Non-blocking Relayer")); // Should not mention the other relayer
1136        } else {
1137            panic!("Expected BadRequest error");
1138        }
1139
1140        // Try to delete the second signer - should succeed (no relayers using it in our test)
1141        let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
1142        let signer2_recreated = create_test_signer_model("other-signer", SignerType::AwsKms);
1143        app_state2
1144            .signer_repository
1145            .create(signer2_recreated)
1146            .await
1147            .unwrap();
1148
1149        let result = delete_signer(
1150            "other-signer".to_string(),
1151            actix_web::web::ThinData(app_state2),
1152        )
1153        .await;
1154
1155        assert!(result.is_ok());
1156    }
1157}