1use 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
25pub 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
63pub 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
93pub 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 let signer = Signer::try_from(request)?;
126
127 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
136pub 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
172pub 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 let _signer = state.signer_repository.get_by_id(signer_id.clone()).await?;
204
205 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 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 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 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(), }),
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 ), };
296
297 SignerCreateRequest {
298 id,
299 signer_type: signer_type_req,
300 config,
301 }
302 }
303
304 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 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 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 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 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 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(), }),
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(), 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, 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()); 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(), }),
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(), 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(), }),
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 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 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 let signer = create_test_signer_model("connected-signer", SignerType::Local);
860 app_state.signer_repository.create(signer).await.unwrap();
861
862 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(), 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 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 let signer = create_test_signer_model("cleanup-signer", SignerType::Local);
920 app_state.signer_repository.create(signer).await.unwrap();
921
922 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 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 let app_state2 = create_mock_app_state(None, None, None, None, None).await;
950
951 let signer2 = create_test_signer_model("cleanup-signer", SignerType::Local);
953 app_state2.signer_repository.create(signer2).await.unwrap();
954
955 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 let signer = create_test_signer_model("multi-relayer-signer", SignerType::AwsKms);
988 app_state.signer_repository.create(signer).await.unwrap();
989
990 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, 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, custom_rpc_urls: None,
1036 },
1037 ];
1038
1039 for relayer in relayers {
1041 app_state.relayer_repository.create(relayer).await.unwrap();
1042 }
1043
1044 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 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 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(), 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(), 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 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")); } else {
1125 panic!("Expected BadRequest error");
1126 }
1127
1128 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}