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