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