1use crate::{
11 jobs::JobProducerTrait,
12 models::{
13 ApiError, ApiResponse, NetworkRepoModel, Notification, NotificationCreateRequest,
14 NotificationRepoModel, NotificationResponse, NotificationUpdateRequest, PaginationMeta,
15 PaginationQuery, RelayerRepoModel, SignerRepoModel, ThinDataAppState, TransactionRepoModel,
16 },
17 repositories::{
18 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
19 Repository, TransactionCounterTrait, TransactionRepository,
20 },
21};
22
23use actix_web::HttpResponse;
24use eyre::Result;
25
26pub async fn list_notifications<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
37 query: PaginationQuery,
38 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
39) -> Result<HttpResponse, ApiError>
40where
41 J: JobProducerTrait + Send + Sync + 'static,
42 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
43 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
44 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
45 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
46 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
47 TCR: TransactionCounterTrait + Send + Sync + 'static,
48 PR: PluginRepositoryTrait + Send + Sync + 'static,
49 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
50{
51 let notifications = state.notification_repository.list_paginated(query).await?;
52
53 let mapped_notifications: Vec<NotificationResponse> =
54 notifications.items.into_iter().map(|n| n.into()).collect();
55
56 Ok(HttpResponse::Ok().json(ApiResponse::paginated(
57 mapped_notifications,
58 PaginationMeta {
59 total_items: notifications.total,
60 current_page: notifications.page,
61 per_page: notifications.per_page,
62 },
63 )))
64}
65
66pub async fn get_notification<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
77 notification_id: String,
78 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
79) -> Result<HttpResponse, ApiError>
80where
81 J: JobProducerTrait + Send + Sync + 'static,
82 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
83 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
84 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
85 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
86 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
87 TCR: TransactionCounterTrait + Send + Sync + 'static,
88 PR: PluginRepositoryTrait + Send + Sync + 'static,
89 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
90{
91 let notification = state
92 .notification_repository
93 .get_by_id(notification_id)
94 .await?;
95
96 let response = NotificationResponse::from(notification);
97 Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
98}
99
100pub async fn create_notification<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
111 request: NotificationCreateRequest,
112 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
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 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
124{
125 let notification = Notification::try_from(request)?;
127
128 let notification_model = NotificationRepoModel::from(notification);
130 let created_notification = state
131 .notification_repository
132 .create(notification_model)
133 .await?;
134
135 let response = NotificationResponse::from(created_notification);
136 Ok(HttpResponse::Created().json(ApiResponse::success(response)))
137}
138
139pub async fn update_notification<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
151 notification_id: String,
152 request: NotificationUpdateRequest,
153 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
154) -> Result<HttpResponse, ApiError>
155where
156 J: JobProducerTrait + Send + Sync + 'static,
157 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
158 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
159 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
160 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
161 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
162 TCR: TransactionCounterTrait + Send + Sync + 'static,
163 PR: PluginRepositoryTrait + Send + Sync + 'static,
164 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
165{
166 let existing_repo_model = state
168 .notification_repository
169 .get_by_id(notification_id.clone())
170 .await?;
171
172 let updated = Notification::from(existing_repo_model).apply_update(&request)?;
174
175 let saved_notification = state
176 .notification_repository
177 .update(notification_id, NotificationRepoModel::from(updated))
178 .await?;
179
180 let response = NotificationResponse::from(saved_notification);
181 Ok(HttpResponse::Ok().json(ApiResponse::success(response)))
182}
183
184pub async fn delete_notification<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
201 notification_id: String,
202 state: ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>,
203) -> Result<HttpResponse, ApiError>
204where
205 J: JobProducerTrait + Send + Sync + 'static,
206 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
207 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
208 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
209 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
210 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
211 TCR: TransactionCounterTrait + Send + Sync + 'static,
212 PR: PluginRepositoryTrait + Send + Sync + 'static,
213 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
214{
215 let _notification = state
217 .notification_repository
218 .get_by_id(notification_id.clone())
219 .await?;
220
221 let connected_relayers = state
223 .relayer_repository
224 .list_by_notification_id(¬ification_id)
225 .await?;
226
227 if !connected_relayers.is_empty() {
228 let relayer_names: Vec<String> =
229 connected_relayers.iter().map(|r| r.name.clone()).collect();
230 return Err(ApiError::BadRequest(format!(
231 "Cannot delete notification '{}' because it is being used by {} relayer(s): {}. Please remove or reconfigure these relayers before deleting the notification.",
232 notification_id,
233 connected_relayers.len(),
234 relayer_names.join(", ")
235 )));
236 }
237
238 state
240 .notification_repository
241 .delete_by_id(notification_id)
242 .await?;
243
244 Ok(HttpResponse::Ok().json(ApiResponse::success("Notification deleted successfully")))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::{
251 models::{ApiError, NotificationType, SecretString},
252 utils::mocks::mockutils::create_mock_app_state,
253 };
254 use actix_web::web::ThinData;
255
256 fn create_test_notification_model(id: &str) -> NotificationRepoModel {
258 NotificationRepoModel {
259 id: id.to_string(),
260 notification_type: NotificationType::Webhook,
261 url: "https://example.com/webhook".to_string(),
262 signing_key: Some(SecretString::new("a".repeat(32).as_str())), }
264 }
265
266 fn create_test_notification_create_request(id: &str) -> NotificationCreateRequest {
268 NotificationCreateRequest {
269 id: Some(id.to_string()),
270 r#type: Some(NotificationType::Webhook),
271 url: "https://example.com/webhook".to_string(),
272 signing_key: Some("a".repeat(32)), }
274 }
275
276 fn create_test_notification_update_request() -> NotificationUpdateRequest {
278 NotificationUpdateRequest {
279 r#type: Some(NotificationType::Webhook),
280 url: Some("https://updated.example.com/webhook".to_string()),
281 signing_key: Some("b".repeat(32)), }
283 }
284
285 #[actix_web::test]
286 async fn test_list_notifications_empty() {
287 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
288 let query = PaginationQuery {
289 page: 1,
290 per_page: 10,
291 };
292
293 let result = list_notifications(query, ThinData(app_state)).await;
294
295 assert!(result.is_ok());
296 let response = result.unwrap();
297 assert_eq!(response.status(), 200);
298
299 let body = actix_web::body::to_bytes(response.into_body())
300 .await
301 .unwrap();
302 let api_response: ApiResponse<Vec<NotificationResponse>> =
303 serde_json::from_slice(&body).unwrap();
304
305 assert!(api_response.success);
306 let data = api_response.data.unwrap();
307 assert_eq!(data.len(), 0);
308 }
309
310 #[actix_web::test]
311 async fn test_list_notifications_with_data() {
312 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
313
314 let notification1 = create_test_notification_model("test-1");
316 let notification2 = create_test_notification_model("test-2");
317
318 app_state
319 .notification_repository
320 .create(notification1)
321 .await
322 .unwrap();
323 app_state
324 .notification_repository
325 .create(notification2)
326 .await
327 .unwrap();
328
329 let query = PaginationQuery {
330 page: 1,
331 per_page: 10,
332 };
333
334 let result = list_notifications(query, ThinData(app_state)).await;
335
336 assert!(result.is_ok());
337 let response = result.unwrap();
338 assert_eq!(response.status(), 200);
339
340 let body = actix_web::body::to_bytes(response.into_body())
341 .await
342 .unwrap();
343 let api_response: ApiResponse<Vec<NotificationResponse>> =
344 serde_json::from_slice(&body).unwrap();
345
346 assert!(api_response.success);
347 let data = api_response.data.unwrap();
348 assert_eq!(data.len(), 2);
349
350 let ids: Vec<&String> = data.iter().map(|n| &n.id).collect();
352 assert!(ids.contains(&&"test-1".to_string()));
353 assert!(ids.contains(&&"test-2".to_string()));
354 }
355
356 #[actix_web::test]
357 async fn test_list_notifications_pagination() {
358 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
359
360 for i in 1..=5 {
362 let notification = create_test_notification_model(&format!("test-{}", i));
363 app_state
364 .notification_repository
365 .create(notification)
366 .await
367 .unwrap();
368 }
369
370 let query = PaginationQuery {
371 page: 2,
372 per_page: 2,
373 };
374
375 let result = list_notifications(query, ThinData(app_state)).await;
376
377 assert!(result.is_ok());
378 let response = result.unwrap();
379 assert_eq!(response.status(), 200);
380
381 let body = actix_web::body::to_bytes(response.into_body())
382 .await
383 .unwrap();
384 let api_response: ApiResponse<Vec<NotificationResponse>> =
385 serde_json::from_slice(&body).unwrap();
386
387 assert!(api_response.success);
388 let data = api_response.data.unwrap();
389 assert_eq!(data.len(), 2);
390 }
391
392 #[actix_web::test]
393 async fn test_get_notification_success() {
394 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
395
396 let notification = create_test_notification_model("test-notification");
398 app_state
399 .notification_repository
400 .create(notification.clone())
401 .await
402 .unwrap();
403
404 let result = get_notification("test-notification".to_string(), ThinData(app_state)).await;
405
406 assert!(result.is_ok());
407 let response = result.unwrap();
408 assert_eq!(response.status(), 200);
409
410 let body = actix_web::body::to_bytes(response.into_body())
411 .await
412 .unwrap();
413 let api_response: ApiResponse<NotificationResponse> =
414 serde_json::from_slice(&body).unwrap();
415
416 assert!(api_response.success);
417 let data = api_response.data.unwrap();
418 assert_eq!(data.id, "test-notification");
419 assert_eq!(data.r#type, NotificationType::Webhook);
420 assert_eq!(data.url, "https://example.com/webhook");
421 assert!(data.has_signing_key); }
423
424 #[actix_web::test]
425 async fn test_get_notification_not_found() {
426 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
427
428 let result = get_notification("non-existent".to_string(), ThinData(app_state)).await;
429
430 assert!(result.is_err());
431 let error = result.unwrap_err();
432 assert!(matches!(error, ApiError::NotFound(_)));
433 }
434
435 #[actix_web::test]
436 async fn test_create_notification_success() {
437 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
438
439 let request = create_test_notification_create_request("new-notification");
440
441 let result = create_notification(request, ThinData(app_state)).await;
442
443 assert!(result.is_ok());
444 let response = result.unwrap();
445 assert_eq!(response.status(), 201);
446
447 let body = actix_web::body::to_bytes(response.into_body())
448 .await
449 .unwrap();
450 let api_response: ApiResponse<NotificationResponse> =
451 serde_json::from_slice(&body).unwrap();
452
453 assert!(api_response.success);
454 let data = api_response.data.unwrap();
455 assert_eq!(data.id, "new-notification");
456 assert_eq!(data.r#type, NotificationType::Webhook);
457 assert_eq!(data.url, "https://example.com/webhook");
458 assert!(data.has_signing_key); }
460
461 #[actix_web::test]
462 async fn test_create_notification_without_signing_key() {
463 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
464
465 let request = NotificationCreateRequest {
466 id: Some("new-notification".to_string()),
467 r#type: Some(NotificationType::Webhook),
468 url: "https://example.com/webhook".to_string(),
469 signing_key: None,
470 };
471
472 let result = create_notification(request, ThinData(app_state)).await;
473
474 assert!(result.is_ok());
475 let response = result.unwrap();
476 assert_eq!(response.status(), 201);
477
478 let body = actix_web::body::to_bytes(response.into_body())
479 .await
480 .unwrap();
481 let api_response: ApiResponse<NotificationResponse> =
482 serde_json::from_slice(&body).unwrap();
483
484 assert!(api_response.success);
485 let data = api_response.data.unwrap();
486 assert_eq!(data.id, "new-notification");
487 assert_eq!(data.r#type, NotificationType::Webhook);
488 assert_eq!(data.url, "https://example.com/webhook");
489 assert!(!data.has_signing_key); }
491
492 #[actix_web::test]
493 async fn test_update_notification_success() {
494 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
495
496 let notification = create_test_notification_model("test-notification");
498 app_state
499 .notification_repository
500 .create(notification)
501 .await
502 .unwrap();
503
504 let update_request = create_test_notification_update_request();
505
506 let result = update_notification(
507 "test-notification".to_string(),
508 update_request,
509 ThinData(app_state),
510 )
511 .await;
512
513 assert!(result.is_ok());
514 let response = result.unwrap();
515 assert_eq!(response.status(), 200);
516
517 let body = actix_web::body::to_bytes(response.into_body())
518 .await
519 .unwrap();
520 let api_response: ApiResponse<NotificationResponse> =
521 serde_json::from_slice(&body).unwrap();
522
523 assert!(api_response.success);
524 let data = api_response.data.unwrap();
525 assert_eq!(data.id, "test-notification");
526 assert_eq!(data.url, "https://updated.example.com/webhook");
527 assert!(data.has_signing_key); }
529
530 #[actix_web::test]
531 async fn test_update_notification_not_found() {
532 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
533
534 let update_request = create_test_notification_update_request();
535
536 let result = update_notification(
537 "non-existent".to_string(),
538 update_request,
539 ThinData(app_state),
540 )
541 .await;
542
543 assert!(result.is_err());
544 let error = result.unwrap_err();
545 assert!(matches!(error, ApiError::NotFound(_)));
546 }
547
548 #[actix_web::test]
549 async fn test_delete_notification_success() {
550 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
551
552 let notification = create_test_notification_model("test-notification");
554 app_state
555 .notification_repository
556 .create(notification)
557 .await
558 .unwrap();
559
560 let result =
561 delete_notification("test-notification".to_string(), ThinData(app_state)).await;
562
563 assert!(result.is_ok());
564 let response = result.unwrap();
565 assert_eq!(response.status(), 200);
566
567 let body = actix_web::body::to_bytes(response.into_body())
568 .await
569 .unwrap();
570 let api_response: ApiResponse<&str> = serde_json::from_slice(&body).unwrap();
571
572 assert!(api_response.success);
573 assert_eq!(
574 api_response.data.unwrap(),
575 "Notification deleted successfully"
576 );
577 }
578
579 #[actix_web::test]
580 async fn test_delete_notification_not_found() {
581 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
582
583 let result = delete_notification("non-existent".to_string(), ThinData(app_state)).await;
584
585 assert!(result.is_err());
586 let error = result.unwrap_err();
587 assert!(matches!(error, ApiError::NotFound(_)));
588 }
589
590 #[actix_web::test]
591 async fn test_notification_response_conversion() {
592 let notification_model = NotificationRepoModel {
593 id: "test-id".to_string(),
594 notification_type: NotificationType::Webhook,
595 url: "https://example.com/webhook".to_string(),
596 signing_key: Some(SecretString::new("secret-key")),
597 };
598
599 let response = NotificationResponse::from(notification_model);
600
601 assert_eq!(response.id, "test-id");
602 assert_eq!(response.r#type, NotificationType::Webhook);
603 assert_eq!(response.url, "https://example.com/webhook");
604 assert!(response.has_signing_key);
605 }
606
607 #[actix_web::test]
608 async fn test_notification_response_conversion_without_signing_key() {
609 let notification_model = NotificationRepoModel {
610 id: "test-id".to_string(),
611 notification_type: NotificationType::Webhook,
612 url: "https://example.com/webhook".to_string(),
613 signing_key: None,
614 };
615
616 let response = NotificationResponse::from(notification_model);
617
618 assert_eq!(response.id, "test-id");
619 assert_eq!(response.r#type, NotificationType::Webhook);
620 assert_eq!(response.url, "https://example.com/webhook");
621 assert!(!response.has_signing_key);
622 }
623
624 #[actix_web::test]
625 async fn test_create_notification_validates_repository_creation() {
626 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
627 let app_state_2 = create_mock_app_state(None, None, None, None, None, None).await;
628
629 let request = create_test_notification_create_request("new-notification");
630 let result = create_notification(request, ThinData(app_state)).await;
631
632 assert!(result.is_ok());
633 let response = result.unwrap();
634 assert_eq!(response.status(), 201);
635
636 let body = actix_web::body::to_bytes(response.into_body())
637 .await
638 .unwrap();
639 let api_response: ApiResponse<NotificationResponse> =
640 serde_json::from_slice(&body).unwrap();
641
642 assert!(api_response.success);
643 let data = api_response.data.unwrap();
644 assert_eq!(data.id, "new-notification");
645 assert_eq!(data.r#type, NotificationType::Webhook);
646 assert_eq!(data.url, "https://example.com/webhook");
647 assert!(data.has_signing_key);
648
649 let request_2 = create_test_notification_create_request("new-notification");
650 let result_2 = create_notification(request_2, ThinData(app_state_2)).await;
651
652 assert!(result_2.is_ok());
653 let response_2 = result_2.unwrap();
654 assert_eq!(response_2.status(), 201);
655 }
656
657 #[actix_web::test]
658 async fn test_create_notification_validation_error() {
659 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
660
661 let request = NotificationCreateRequest {
663 id: Some("invalid@id".to_string()), r#type: Some(NotificationType::Webhook),
665 url: "https://valid.example.com/webhook".to_string(), signing_key: Some("a".repeat(32)), };
668
669 let result = create_notification(request, ThinData(app_state)).await;
670
671 assert!(result.is_err());
673 if let Err(ApiError::BadRequest(msg)) = result {
674 assert!(msg.contains("ID must contain only letters, numbers, dashes and underscores"));
677 } else {
678 panic!("Expected BadRequest error with validation messages");
679 }
680 }
681
682 #[actix_web::test]
683 async fn test_update_notification_validation_error() {
684 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
685
686 let notification = create_test_notification_model("test-notification");
688 app_state
689 .notification_repository
690 .create(notification)
691 .await
692 .unwrap();
693
694 let update_request = NotificationUpdateRequest {
696 r#type: Some(NotificationType::Webhook),
697 url: Some("https://valid.example.com/webhook".to_string()), signing_key: Some("short".to_string()), };
700
701 let result = update_notification(
702 "test-notification".to_string(),
703 update_request,
704 ThinData(app_state),
705 )
706 .await;
707
708 assert!(result.is_err());
710 if let Err(ApiError::BadRequest(msg)) = result {
711 assert!(
714 msg.contains("Signing key must be at least") && msg.contains("characters long")
715 );
716 } else {
717 panic!("Expected BadRequest error with validation messages");
718 }
719 }
720
721 #[actix_web::test]
722 async fn test_delete_notification_blocked_by_connected_relayers() {
723 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
724
725 let notification = create_test_notification_model("connected-notification");
727 app_state
728 .notification_repository
729 .create(notification)
730 .await
731 .unwrap();
732
733 let relayer = crate::models::RelayerRepoModel {
735 id: "test-relayer".to_string(),
736 name: "Test Relayer".to_string(),
737 network: "ethereum".to_string(),
738 paused: false,
739 network_type: crate::models::NetworkType::Evm,
740 signer_id: "test-signer".to_string(),
741 policies: crate::models::RelayerNetworkPolicy::Evm(
742 crate::models::RelayerEvmPolicy::default(),
743 ),
744 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
745 notification_id: Some("connected-notification".to_string()), system_disabled: false,
747 custom_rpc_urls: None,
748 ..Default::default()
749 };
750 app_state.relayer_repository.create(relayer).await.unwrap();
751
752 let result =
754 delete_notification("connected-notification".to_string(), ThinData(app_state)).await;
755
756 assert!(result.is_err());
757 let error = result.unwrap_err();
758 if let ApiError::BadRequest(msg) = error {
759 assert!(msg.contains("Cannot delete notification"));
760 assert!(msg.contains("being used by"));
761 assert!(msg.contains("Test Relayer"));
762 assert!(msg.contains("remove or reconfigure"));
763 } else {
764 panic!("Expected BadRequest error");
765 }
766 }
767
768 #[actix_web::test]
769 async fn test_delete_notification_after_relayer_removed() {
770 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
771
772 let notification = create_test_notification_model("cleanup-notification");
774 app_state
775 .notification_repository
776 .create(notification)
777 .await
778 .unwrap();
779
780 let relayer = crate::models::RelayerRepoModel {
782 id: "temp-relayer".to_string(),
783 name: "Temporary Relayer".to_string(),
784 network: "ethereum".to_string(),
785 paused: false,
786 network_type: crate::models::NetworkType::Evm,
787 signer_id: "test-signer".to_string(),
788 policies: crate::models::RelayerNetworkPolicy::Evm(
789 crate::models::RelayerEvmPolicy::default(),
790 ),
791 address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
792 notification_id: Some("cleanup-notification".to_string()),
793 system_disabled: false,
794 custom_rpc_urls: None,
795 ..Default::default()
796 };
797 app_state.relayer_repository.create(relayer).await.unwrap();
798
799 let result =
801 delete_notification("cleanup-notification".to_string(), ThinData(app_state)).await;
802 assert!(result.is_err());
803
804 let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
806
807 let notification2 = create_test_notification_model("cleanup-notification");
809 app_state2
810 .notification_repository
811 .create(notification2)
812 .await
813 .unwrap();
814
815 let result =
817 delete_notification("cleanup-notification".to_string(), ThinData(app_state2)).await;
818
819 assert!(result.is_ok());
820 let response = result.unwrap();
821 assert_eq!(response.status(), 200);
822 }
823
824 #[actix_web::test]
825 async fn test_delete_notification_with_multiple_relayers() {
826 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
827
828 let notification = create_test_notification_model("multi-relayer-notification");
830 app_state
831 .notification_repository
832 .create(notification)
833 .await
834 .unwrap();
835
836 let relayers = vec![
838 crate::models::RelayerRepoModel {
839 id: "relayer-1".to_string(),
840 name: "EVM Relayer".to_string(),
841 network: "ethereum".to_string(),
842 paused: false,
843 network_type: crate::models::NetworkType::Evm,
844 signer_id: "test-signer".to_string(),
845 policies: crate::models::RelayerNetworkPolicy::Evm(
846 crate::models::RelayerEvmPolicy::default(),
847 ),
848 address: "0x1111111111111111111111111111111111111111".to_string(),
849 notification_id: Some("multi-relayer-notification".to_string()),
850 system_disabled: false,
851 custom_rpc_urls: None,
852 ..Default::default()
853 },
854 crate::models::RelayerRepoModel {
855 id: "relayer-2".to_string(),
856 name: "Solana Relayer".to_string(),
857 network: "solana".to_string(),
858 paused: true, network_type: crate::models::NetworkType::Solana,
860 signer_id: "test-signer".to_string(),
861 policies: crate::models::RelayerNetworkPolicy::Solana(
862 crate::models::RelayerSolanaPolicy::default(),
863 ),
864 address: "solana-address".to_string(),
865 notification_id: Some("multi-relayer-notification".to_string()),
866 system_disabled: false,
867 custom_rpc_urls: None,
868 ..Default::default()
869 },
870 crate::models::RelayerRepoModel {
871 id: "relayer-3".to_string(),
872 name: "Stellar Relayer".to_string(),
873 network: "stellar".to_string(),
874 paused: false,
875 network_type: crate::models::NetworkType::Stellar,
876 signer_id: "test-signer".to_string(),
877 policies: crate::models::RelayerNetworkPolicy::Stellar(
878 crate::models::RelayerStellarPolicy::default(),
879 ),
880 address: "stellar-address".to_string(),
881 notification_id: Some("multi-relayer-notification".to_string()),
882 system_disabled: true, custom_rpc_urls: None,
884 ..Default::default()
885 },
886 ];
887
888 for relayer in relayers {
890 app_state.relayer_repository.create(relayer).await.unwrap();
891 }
892
893 let result = delete_notification(
895 "multi-relayer-notification".to_string(),
896 ThinData(app_state),
897 )
898 .await;
899
900 assert!(result.is_err());
901 let error = result.unwrap_err();
902 if let ApiError::BadRequest(msg) = error {
903 assert!(msg.contains("Cannot delete notification 'multi-relayer-notification'"));
904 assert!(msg.contains("being used by 3 relayer(s)"));
905 assert!(msg.contains("EVM Relayer"));
906 assert!(msg.contains("Solana Relayer"));
907 assert!(msg.contains("Stellar Relayer"));
908 assert!(msg.contains("remove or reconfigure"));
909 } else {
910 panic!("Expected BadRequest error, got: {:?}", error);
911 }
912 }
913
914 #[actix_web::test]
915 async fn test_delete_notification_with_some_relayers_using_different_notification() {
916 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
917
918 let notification1 = create_test_notification_model("notification-to-delete");
920 let notification2 = create_test_notification_model("other-notification");
921 app_state
922 .notification_repository
923 .create(notification1)
924 .await
925 .unwrap();
926 app_state
927 .notification_repository
928 .create(notification2)
929 .await
930 .unwrap();
931
932 let relayer1 = crate::models::RelayerRepoModel {
934 id: "blocking-relayer".to_string(),
935 name: "Blocking Relayer".to_string(),
936 network: "ethereum".to_string(),
937 paused: false,
938 network_type: crate::models::NetworkType::Evm,
939 signer_id: "test-signer".to_string(),
940 policies: crate::models::RelayerNetworkPolicy::Evm(
941 crate::models::RelayerEvmPolicy::default(),
942 ),
943 address: "0x1111111111111111111111111111111111111111".to_string(),
944 notification_id: Some("notification-to-delete".to_string()), system_disabled: false,
946 custom_rpc_urls: None,
947 ..Default::default()
948 };
949
950 let relayer2 = crate::models::RelayerRepoModel {
951 id: "non-blocking-relayer".to_string(),
952 name: "Non-blocking Relayer".to_string(),
953 network: "polygon".to_string(),
954 paused: false,
955 network_type: crate::models::NetworkType::Evm,
956 signer_id: "test-signer".to_string(),
957 policies: crate::models::RelayerNetworkPolicy::Evm(
958 crate::models::RelayerEvmPolicy::default(),
959 ),
960 address: "0x2222222222222222222222222222222222222222".to_string(),
961 notification_id: Some("other-notification".to_string()), system_disabled: false,
963 custom_rpc_urls: None,
964 ..Default::default()
965 };
966
967 let relayer3 = crate::models::RelayerRepoModel {
968 id: "no-notification-relayer".to_string(),
969 name: "No Notification Relayer".to_string(),
970 network: "bsc".to_string(),
971 paused: false,
972 network_type: crate::models::NetworkType::Evm,
973 signer_id: "test-signer".to_string(),
974 policies: crate::models::RelayerNetworkPolicy::Evm(
975 crate::models::RelayerEvmPolicy::default(),
976 ),
977 address: "0x3333333333333333333333333333333333333333".to_string(),
978 notification_id: None, system_disabled: false,
980 custom_rpc_urls: None,
981 ..Default::default()
982 };
983
984 app_state.relayer_repository.create(relayer1).await.unwrap();
985 app_state.relayer_repository.create(relayer2).await.unwrap();
986 app_state.relayer_repository.create(relayer3).await.unwrap();
987
988 let result =
990 delete_notification("notification-to-delete".to_string(), ThinData(app_state)).await;
991
992 assert!(result.is_err());
993 let error = result.unwrap_err();
994 if let ApiError::BadRequest(msg) = error {
995 assert!(msg.contains("being used by 1 relayer(s)"));
996 assert!(msg.contains("Blocking Relayer"));
997 assert!(!msg.contains("Non-blocking Relayer")); assert!(!msg.contains("No Notification Relayer")); } else {
1000 panic!("Expected BadRequest error");
1001 }
1002
1003 let app_state2 = create_mock_app_state(None, None, None, None, None, None).await;
1005 let notification2_recreated = create_test_notification_model("other-notification");
1006 app_state2
1007 .notification_repository
1008 .create(notification2_recreated)
1009 .await
1010 .unwrap();
1011
1012 let result =
1013 delete_notification("other-notification".to_string(), ThinData(app_state2)).await;
1014
1015 assert!(result.is_ok());
1016 }
1017}