1use std::{fmt, sync::Arc};
4
5use crate::observability::request_id::get_request_id;
6use crate::{
7 jobs::JobProducerTrait,
8 models::{
9 AppState, NetworkRepoModel, NotificationRepoModel, PluginCallRequest, PluginMetadata,
10 PluginModel, RelayerRepoModel, SignerRepoModel, ThinDataAppState, TransactionRepoModel,
11 },
12 repositories::{
13 ApiKeyRepositoryTrait, NetworkRepository, PluginRepositoryTrait, RelayerRepository,
14 Repository, TransactionCounterTrait, TransactionRepository,
15 },
16};
17use actix_web::web;
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21use uuid::Uuid;
22
23pub mod runner;
24pub use runner::*;
25
26pub mod relayer_api;
27pub use relayer_api::*;
28
29pub mod script_executor;
30pub use script_executor::*;
31
32pub mod socket;
33pub use socket::*;
34
35#[cfg(test)]
36use mockall::automock;
37
38#[derive(Error, Debug, Serialize)]
39pub enum PluginError {
40 #[error("Socket error: {0}")]
41 SocketError(String),
42 #[error("Plugin error: {0}")]
43 PluginError(String),
44 #[error("Relayer error: {0}")]
45 RelayerError(String),
46 #[error("Plugin execution error: {0}")]
47 PluginExecutionError(String),
48 #[error("Script execution timed out after {0} seconds")]
49 ScriptTimeout(u64),
50 #[error("Invalid method: {0}")]
51 InvalidMethod(String),
52 #[error("Invalid payload: {0}")]
53 InvalidPayload(String),
54 #[error("{0}")]
55 HandlerError(Box<PluginHandlerPayload>),
56}
57
58impl PluginError {
59 pub fn with_traces(self, traces: Vec<serde_json::Value>) -> Self {
62 match self {
63 PluginError::HandlerError(mut payload) => {
64 payload.append_traces(traces);
65 PluginError::HandlerError(payload)
66 }
67 other => other,
68 }
69 }
70}
71
72impl From<PluginError> for String {
73 fn from(error: PluginError) -> Self {
74 error.to_string()
75 }
76}
77
78#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
79pub struct PluginCallResponse {
80 pub result: serde_json::Value,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub metadata: Option<PluginMetadata>,
85}
86
87#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
88pub struct PluginHandlerError {
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub code: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub details: Option<serde_json::Value>,
93}
94
95#[derive(Debug)]
96pub struct PluginHandlerResponse {
97 pub status: u16,
98 pub message: String,
99 pub error: PluginHandlerError,
100 pub metadata: Option<PluginMetadata>,
101}
102
103#[derive(Debug, Serialize)]
104pub struct PluginHandlerPayload {
105 pub status: u16,
106 pub message: String,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub code: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub details: Option<serde_json::Value>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub logs: Option<Vec<LogEntry>>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub traces: Option<Vec<serde_json::Value>>,
115}
116
117impl PluginHandlerPayload {
118 fn append_traces(&mut self, traces: Vec<serde_json::Value>) {
119 match &mut self.traces {
120 Some(existing) => existing.extend(traces),
121 None => self.traces = Some(traces),
122 }
123 }
124
125 fn into_response(self, emit_logs: bool, emit_traces: bool) -> PluginHandlerResponse {
126 let logs = if emit_logs { self.logs } else { None };
127 let traces = if emit_traces { self.traces } else { None };
128 let message = derive_handler_message(&self.message, logs.as_deref());
129 let metadata = build_metadata(logs, traces);
130
131 PluginHandlerResponse {
132 status: self.status,
133 message,
134 error: PluginHandlerError {
135 code: self.code,
136 details: self.details,
137 },
138 metadata,
139 }
140 }
141}
142
143impl fmt::Display for PluginHandlerPayload {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.write_str(&self.message)
146 }
147}
148
149fn derive_handler_message(message: &str, logs: Option<&[LogEntry]>) -> String {
150 if !message.trim().is_empty() {
151 return message.to_string();
152 }
153
154 if let Some(logs) = logs {
155 if let Some(entry) = logs
156 .iter()
157 .rev()
158 .find(|entry| matches!(entry.level, LogLevel::Error | LogLevel::Warn))
159 {
160 return entry.message.clone();
161 }
162
163 if let Some(entry) = logs.last() {
164 return entry.message.clone();
165 }
166 }
167
168 "Plugin execution failed".to_string()
169}
170
171fn build_metadata(
172 logs: Option<Vec<LogEntry>>,
173 traces: Option<Vec<serde_json::Value>>,
174) -> Option<PluginMetadata> {
175 if logs.is_some() || traces.is_some() {
176 Some(PluginMetadata { logs, traces })
177 } else {
178 None
179 }
180}
181
182#[derive(Debug)]
183pub enum PluginCallResult {
184 Success(PluginCallResponse),
185 Handler(PluginHandlerResponse),
186 Fatal(PluginError),
187}
188
189#[derive(Default)]
190pub struct PluginService<R: PluginRunnerTrait> {
191 runner: R,
192}
193
194impl<R: PluginRunnerTrait> PluginService<R> {
195 pub fn new(runner: R) -> Self {
196 Self { runner }
197 }
198
199 fn resolve_plugin_path(plugin_path: &str) -> String {
200 if plugin_path.starts_with("plugins/") {
201 plugin_path.to_string()
202 } else {
203 format!("plugins/{plugin_path}")
204 }
205 }
206
207 #[allow(clippy::type_complexity)]
208 async fn call_plugin<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>(
209 &self,
210 plugin: PluginModel,
211 plugin_call_request: PluginCallRequest,
212 state: Arc<ThinDataAppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>,
213 ) -> PluginCallResult
214 where
215 J: JobProducerTrait + Send + Sync + 'static,
216 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
217 TR: TransactionRepository
218 + Repository<TransactionRepoModel, String>
219 + Send
220 + Sync
221 + 'static,
222 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
223 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
224 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
225 TCR: TransactionCounterTrait + Send + Sync + 'static,
226 PR: PluginRepositoryTrait + Send + Sync + 'static,
227 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
228 {
229 let socket_path = format!("/tmp/{}.sock", Uuid::new_v4());
230 let script_path = Self::resolve_plugin_path(&plugin.path);
231 let script_params = plugin_call_request.params.to_string();
232
233 let result = self
234 .runner
235 .run(
236 plugin.id.clone(),
237 &socket_path,
238 script_path,
239 plugin.timeout,
240 script_params,
241 get_request_id(),
242 state,
243 )
244 .await;
245
246 match result {
247 Ok(script_result) => {
248 let logs = if plugin.emit_logs {
250 Some(script_result.logs)
251 } else {
252 None
253 };
254 let traces = if plugin.emit_traces {
255 Some(script_result.trace)
256 } else {
257 None
258 };
259 let metadata = build_metadata(logs, traces);
260
261 let result = if script_result.return_value.trim() == "undefined" {
263 serde_json::Value::Null
264 } else {
265 serde_json::from_str::<serde_json::Value>(&script_result.return_value)
266 .unwrap_or(serde_json::Value::String(script_result.return_value))
267 };
268
269 PluginCallResult::Success(PluginCallResponse { result, metadata })
270 }
271 Err(e) => match e {
272 PluginError::HandlerError(payload) => {
273 let failure = payload.into_response(plugin.emit_logs, plugin.emit_traces);
274 let has_logs = failure
275 .metadata
276 .as_ref()
277 .and_then(|meta| meta.logs.as_ref())
278 .is_some();
279 let has_traces = failure
280 .metadata
281 .as_ref()
282 .and_then(|meta| meta.traces.as_ref())
283 .is_some();
284
285 tracing::debug!(
286 status = failure.status,
287 message = %failure.message,
288 code = ?failure.error.code.as_ref(),
289 details = ?failure.error.details.as_ref(),
290 has_logs,
291 has_traces,
292 "Plugin handler returned error"
293 );
294
295 PluginCallResult::Handler(failure)
296 }
297 other => {
298 tracing::error!("Plugin execution failed: {:?}", other);
300 PluginCallResult::Fatal(other)
301 }
302 },
303 }
304 }
305}
306
307#[async_trait]
308#[cfg_attr(test, automock)]
309pub trait PluginServiceTrait<J, TR, RR, NR, NFR, SR, TCR, PR, AKR>: Send + Sync
310where
311 J: JobProducerTrait + 'static,
312 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
313 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
314 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
315 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
316 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
317 TCR: TransactionCounterTrait + Send + Sync + 'static,
318 PR: PluginRepositoryTrait + Send + Sync + 'static,
319 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
320{
321 fn new(runner: PluginRunner) -> Self;
322 async fn call_plugin(
323 &self,
324 plugin: PluginModel,
325 plugin_call_request: PluginCallRequest,
326 state: Arc<web::ThinData<AppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>>,
327 ) -> PluginCallResult;
328}
329
330#[async_trait]
331impl<J, TR, RR, NR, NFR, SR, TCR, PR, AKR> PluginServiceTrait<J, TR, RR, NR, NFR, SR, TCR, PR, AKR>
332 for PluginService<PluginRunner>
333where
334 J: JobProducerTrait + 'static,
335 TR: TransactionRepository + Repository<TransactionRepoModel, String> + Send + Sync + 'static,
336 RR: RelayerRepository + Repository<RelayerRepoModel, String> + Send + Sync + 'static,
337 NR: NetworkRepository + Repository<NetworkRepoModel, String> + Send + Sync + 'static,
338 NFR: Repository<NotificationRepoModel, String> + Send + Sync + 'static,
339 SR: Repository<SignerRepoModel, String> + Send + Sync + 'static,
340 TCR: TransactionCounterTrait + Send + Sync + 'static,
341 PR: PluginRepositoryTrait + Send + Sync + 'static,
342 AKR: ApiKeyRepositoryTrait + Send + Sync + 'static,
343{
344 fn new(runner: PluginRunner) -> Self {
345 Self::new(runner)
346 }
347
348 async fn call_plugin(
349 &self,
350 plugin: PluginModel,
351 plugin_call_request: PluginCallRequest,
352 state: Arc<web::ThinData<AppState<J, RR, TR, NR, NFR, SR, TCR, PR, AKR>>>,
353 ) -> PluginCallResult {
354 self.call_plugin(plugin, plugin_call_request, state).await
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use std::time::Duration;
361
362 use crate::{
363 constants::DEFAULT_PLUGIN_TIMEOUT_SECONDS,
364 jobs::MockJobProducerTrait,
365 models::PluginModel,
366 repositories::{
367 ApiKeyRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage,
368 PluginRepositoryStorage, RelayerRepositoryStorage, SignerRepositoryStorage,
369 TransactionCounterRepositoryStorage, TransactionRepositoryStorage,
370 },
371 utils::mocks::mockutils::create_mock_app_state,
372 };
373
374 use super::*;
375
376 #[test]
377 fn test_resolve_plugin_path() {
378 assert_eq!(
379 PluginService::<MockPluginRunnerTrait>::resolve_plugin_path("plugins/examples/test.ts"),
380 "plugins/examples/test.ts"
381 );
382
383 assert_eq!(
384 PluginService::<MockPluginRunnerTrait>::resolve_plugin_path("examples/test.ts"),
385 "plugins/examples/test.ts"
386 );
387
388 assert_eq!(
389 PluginService::<MockPluginRunnerTrait>::resolve_plugin_path("test.ts"),
390 "plugins/test.ts"
391 );
392 }
393
394 #[tokio::test]
395 async fn test_call_plugin() {
396 let plugin = PluginModel {
397 id: "test-plugin".to_string(),
398 path: "test-path".to_string(),
399 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
400 emit_logs: true,
401 emit_traces: false,
402 };
403 let app_state =
404 create_mock_app_state(None, None, None, None, Some(vec![plugin.clone()]), None).await;
405
406 let mut plugin_runner = MockPluginRunnerTrait::default();
407
408 plugin_runner
409 .expect_run::<MockJobProducerTrait, RelayerRepositoryStorage, TransactionRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage, PluginRepositoryStorage, ApiKeyRepositoryStorage>()
410 .returning(|_, _, _, _, _, _, _| {
411 Ok(ScriptResult {
412 logs: vec![LogEntry {
413 level: LogLevel::Log,
414 message: "test-log".to_string(),
415 }],
416 error: "test-error".to_string(),
417 return_value: "test-result".to_string(),
418 trace: Vec::new(),
419 })
420 });
421
422 let plugin_service = PluginService::<MockPluginRunnerTrait>::new(plugin_runner);
423 let outcome = plugin_service
424 .call_plugin(
425 plugin,
426 PluginCallRequest {
427 params: serde_json::Value::Null,
428 },
429 Arc::new(web::ThinData(app_state)),
430 )
431 .await;
432 match outcome {
433 PluginCallResult::Success(result) => {
434 assert_eq!(
436 result.result,
437 serde_json::Value::String("test-result".to_string())
438 );
439 assert!(result.metadata.and_then(|meta| meta.logs).is_some());
441 }
442 PluginCallResult::Handler(_) | PluginCallResult::Fatal(_) => {
443 panic!("expected success outcome")
444 }
445 }
446 }
447
448 #[tokio::test]
449 async fn test_from_plugin_error_to_string() {
450 let error = PluginError::PluginExecutionError("test-error".to_string());
451 let result: String = error.into();
452 assert_eq!(result, "Plugin execution error: test-error");
453 }
454
455 #[test]
456 fn test_plugin_error_with_traces_handler_error() {
457 let payload = PluginHandlerPayload {
458 status: 400,
459 message: "test message".to_string(),
460 code: Some("TEST_CODE".to_string()),
461 details: None,
462 logs: None,
463 traces: Some(vec![serde_json::json!({"trace": "1"})]),
464 };
465 let error = PluginError::HandlerError(Box::new(payload));
466 let new_traces = vec![
467 serde_json::json!({"trace": "2"}),
468 serde_json::json!({"trace": "3"}),
469 ];
470
471 let enriched_error = error.with_traces(new_traces);
472
473 match enriched_error {
474 PluginError::HandlerError(payload) => {
475 let traces = payload.traces.unwrap();
476 assert_eq!(traces.len(), 3);
477 assert_eq!(traces[0], serde_json::json!({"trace": "1"}));
478 assert_eq!(traces[1], serde_json::json!({"trace": "2"}));
479 assert_eq!(traces[2], serde_json::json!({"trace": "3"}));
480 }
481 _ => panic!("Expected HandlerError variant"),
482 }
483 }
484
485 #[test]
486 fn test_plugin_error_with_traces_other_variants() {
487 let error = PluginError::PluginExecutionError("test".to_string());
488 let new_traces = vec![serde_json::json!({"trace": "1"})];
489
490 let result = error.with_traces(new_traces);
491
492 match result {
493 PluginError::PluginExecutionError(msg) => assert_eq!(msg, "test"),
494 _ => panic!("Expected PluginExecutionError variant"),
495 }
496 }
497
498 #[test]
499 fn test_derive_handler_message_with_message() {
500 let result = derive_handler_message("Custom error message", None);
501 assert_eq!(result, "Custom error message");
502 }
503
504 #[test]
505 fn test_derive_handler_message_with_error_log() {
506 let logs = vec![
507 LogEntry {
508 level: LogLevel::Log,
509 message: "info log".to_string(),
510 },
511 LogEntry {
512 level: LogLevel::Error,
513 message: "error log".to_string(),
514 },
515 ];
516 let result = derive_handler_message("", Some(&logs));
517 assert_eq!(result, "error log");
518 }
519
520 #[test]
521 fn test_derive_handler_message_with_warn_log() {
522 let logs = vec![
523 LogEntry {
524 level: LogLevel::Log,
525 message: "info log".to_string(),
526 },
527 LogEntry {
528 level: LogLevel::Warn,
529 message: "warn log".to_string(),
530 },
531 ];
532 let result = derive_handler_message("", Some(&logs));
533 assert_eq!(result, "warn log");
534 }
535
536 #[test]
537 fn test_derive_handler_message_with_only_info_logs() {
538 let logs = vec![
539 LogEntry {
540 level: LogLevel::Log,
541 message: "first log".to_string(),
542 },
543 LogEntry {
544 level: LogLevel::Info,
545 message: "last log".to_string(),
546 },
547 ];
548 let result = derive_handler_message("", Some(&logs));
549 assert_eq!(result, "last log");
550 }
551
552 #[test]
553 fn test_derive_handler_message_no_logs() {
554 let result = derive_handler_message("", None);
555 assert_eq!(result, "Plugin execution failed");
556 }
557
558 #[test]
559 fn test_build_metadata_with_logs_and_traces() {
560 let logs = vec![LogEntry {
561 level: LogLevel::Log,
562 message: "test".to_string(),
563 }];
564 let traces = vec![serde_json::json!({"trace": "1"})];
565
566 let result = build_metadata(Some(logs.clone()), Some(traces.clone()));
567
568 assert!(result.is_some());
569 let metadata = result.unwrap();
570 assert_eq!(metadata.logs.unwrap(), logs);
571 assert_eq!(metadata.traces.unwrap(), traces);
572 }
573
574 #[test]
575 fn test_build_metadata_with_only_logs() {
576 let logs = vec![LogEntry {
577 level: LogLevel::Log,
578 message: "test".to_string(),
579 }];
580
581 let result = build_metadata(Some(logs.clone()), None);
582
583 assert!(result.is_some());
584 let metadata = result.unwrap();
585 assert_eq!(metadata.logs.unwrap(), logs);
586 assert!(metadata.traces.is_none());
587 }
588
589 #[test]
590 fn test_build_metadata_with_only_traces() {
591 let traces = vec![serde_json::json!({"trace": "1"})];
592
593 let result = build_metadata(None, Some(traces.clone()));
594
595 assert!(result.is_some());
596 let metadata = result.unwrap();
597 assert!(metadata.logs.is_none());
598 assert_eq!(metadata.traces.unwrap(), traces);
599 }
600
601 #[test]
602 fn test_build_metadata_with_neither() {
603 let result = build_metadata(None, None);
604 assert!(result.is_none());
605 }
606
607 #[test]
608 fn test_plugin_handler_payload_append_traces_to_existing() {
609 let mut payload = PluginHandlerPayload {
610 status: 400,
611 message: "test".to_string(),
612 code: None,
613 details: None,
614 logs: None,
615 traces: Some(vec![serde_json::json!({"trace": "1"})]),
616 };
617
618 payload.append_traces(vec![serde_json::json!({"trace": "2"})]);
619
620 let traces = payload.traces.unwrap();
621 assert_eq!(traces.len(), 2);
622 assert_eq!(traces[0], serde_json::json!({"trace": "1"}));
623 assert_eq!(traces[1], serde_json::json!({"trace": "2"}));
624 }
625
626 #[test]
627 fn test_plugin_handler_payload_append_traces_to_none() {
628 let mut payload = PluginHandlerPayload {
629 status: 400,
630 message: "test".to_string(),
631 code: None,
632 details: None,
633 logs: None,
634 traces: None,
635 };
636
637 payload.append_traces(vec![serde_json::json!({"trace": "1"})]);
638
639 let traces = payload.traces.unwrap();
640 assert_eq!(traces.len(), 1);
641 assert_eq!(traces[0], serde_json::json!({"trace": "1"}));
642 }
643
644 #[test]
645 fn test_plugin_handler_payload_into_response_with_logs_and_traces() {
646 let logs = vec![LogEntry {
647 level: LogLevel::Error,
648 message: "error message".to_string(),
649 }];
650 let payload = PluginHandlerPayload {
651 status: 400,
652 message: "".to_string(),
653 code: Some("ERR_CODE".to_string()),
654 details: Some(serde_json::json!({"key": "value"})),
655 logs: Some(logs.clone()),
656 traces: Some(vec![serde_json::json!({"trace": "1"})]),
657 };
658
659 let response = payload.into_response(true, true);
660
661 assert_eq!(response.status, 400);
662 assert_eq!(response.message, "error message"); assert_eq!(response.error.code, Some("ERR_CODE".to_string()));
664 assert!(response.metadata.is_some());
665 let metadata = response.metadata.unwrap();
666 assert_eq!(metadata.logs.unwrap(), logs);
667 assert_eq!(metadata.traces.unwrap().len(), 1);
668 }
669
670 #[test]
671 fn test_plugin_handler_payload_into_response_without_logs() {
672 let logs = vec![LogEntry {
673 level: LogLevel::Log,
674 message: "test log".to_string(),
675 }];
676 let payload = PluginHandlerPayload {
677 status: 500,
678 message: "explicit message".to_string(),
679 code: None,
680 details: None,
681 logs: Some(logs),
682 traces: None,
683 };
684
685 let response = payload.into_response(false, false);
686
687 assert_eq!(response.status, 500);
688 assert_eq!(response.message, "explicit message");
689 assert!(response.metadata.is_none()); }
691
692 #[tokio::test]
693 async fn test_call_plugin_handler_error() {
694 let plugin = PluginModel {
695 id: "test-plugin".to_string(),
696 path: "test-path".to_string(),
697 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
698 emit_logs: true,
699 emit_traces: true,
700 };
701 let app_state =
702 create_mock_app_state(None, None, None, None, Some(vec![plugin.clone()]), None).await;
703
704 let mut plugin_runner = MockPluginRunnerTrait::default();
705
706 plugin_runner
707 .expect_run::<MockJobProducerTrait, RelayerRepositoryStorage, TransactionRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage, PluginRepositoryStorage, ApiKeyRepositoryStorage>()
708 .returning(move |_, _, _, _, _, _, _| {
709 Err(PluginError::HandlerError(Box::new(PluginHandlerPayload {
710 status: 400,
711 message: "Plugin handler error".to_string(),
712 code: Some("VALIDATION_ERROR".to_string()),
713 details: Some(serde_json::json!({"field": "email"})),
714 logs: Some(vec![LogEntry {
715 level: LogLevel::Error,
716 message: "Invalid email".to_string(),
717 }]),
718 traces: Some(vec![serde_json::json!({"step": "validation"})]),
719 })))
720 });
721
722 let plugin_service = PluginService::<MockPluginRunnerTrait>::new(plugin_runner);
723 let outcome = plugin_service
724 .call_plugin(
725 plugin,
726 PluginCallRequest {
727 params: serde_json::Value::Null,
728 },
729 Arc::new(web::ThinData(app_state)),
730 )
731 .await;
732
733 match outcome {
734 PluginCallResult::Handler(response) => {
735 assert_eq!(response.status, 400);
736 assert_eq!(response.error.code, Some("VALIDATION_ERROR".to_string()));
737 assert!(response.metadata.is_some());
738 let metadata = response.metadata.unwrap();
739 assert!(metadata.logs.is_some());
740 assert!(metadata.traces.is_some());
741 }
742 _ => panic!("Expected Handler result"),
743 }
744 }
745
746 #[tokio::test]
747 async fn test_call_plugin_fatal_error() {
748 let plugin = PluginModel {
749 id: "test-plugin".to_string(),
750 path: "test-path".to_string(),
751 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
752 emit_logs: false,
753 emit_traces: false,
754 };
755 let app_state =
756 create_mock_app_state(None, None, None, None, Some(vec![plugin.clone()]), None).await;
757
758 let mut plugin_runner = MockPluginRunnerTrait::default();
759
760 plugin_runner
761 .expect_run::<MockJobProducerTrait, RelayerRepositoryStorage, TransactionRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage, PluginRepositoryStorage, ApiKeyRepositoryStorage>()
762 .returning(|_, _, _, _, _, _, _| {
763 Err(PluginError::PluginExecutionError("Fatal error".to_string()))
764 });
765
766 let plugin_service = PluginService::<MockPluginRunnerTrait>::new(plugin_runner);
767 let outcome = plugin_service
768 .call_plugin(
769 plugin,
770 PluginCallRequest {
771 params: serde_json::Value::Null,
772 },
773 Arc::new(web::ThinData(app_state)),
774 )
775 .await;
776
777 match outcome {
778 PluginCallResult::Fatal(error) => {
779 assert!(matches!(error, PluginError::PluginExecutionError(_)));
780 }
781 _ => panic!("Expected Fatal result"),
782 }
783 }
784
785 #[tokio::test]
786 async fn test_call_plugin_success_with_json_result() {
787 let plugin = PluginModel {
788 id: "test-plugin".to_string(),
789 path: "test-path".to_string(),
790 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
791 emit_logs: true,
792 emit_traces: true,
793 };
794 let app_state =
795 create_mock_app_state(None, None, None, None, Some(vec![plugin.clone()]), None).await;
796
797 let mut plugin_runner = MockPluginRunnerTrait::default();
798
799 plugin_runner
800 .expect_run::<MockJobProducerTrait, RelayerRepositoryStorage, TransactionRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage, PluginRepositoryStorage, ApiKeyRepositoryStorage>()
801 .returning(|_, _, _, _, _, _, _| {
802 Ok(ScriptResult {
803 logs: vec![LogEntry {
804 level: LogLevel::Log,
805 message: "test-log".to_string(),
806 }],
807 error: "".to_string(),
808 return_value: r#"{"result": "success"}"#.to_string(),
809 trace: vec![serde_json::json!({"step": "1"})],
810 })
811 });
812
813 let plugin_service = PluginService::<MockPluginRunnerTrait>::new(plugin_runner);
814 let outcome = plugin_service
815 .call_plugin(
816 plugin,
817 PluginCallRequest {
818 params: serde_json::Value::Null,
819 },
820 Arc::new(web::ThinData(app_state)),
821 )
822 .await;
823
824 match outcome {
825 PluginCallResult::Success(result) => {
826 assert_eq!(result.result, serde_json::json!({"result": "success"}));
828 assert!(result.metadata.is_some());
829 let metadata = result.metadata.unwrap();
830 assert!(metadata.logs.is_some());
831 assert!(metadata.traces.is_some());
832 }
833 _ => panic!("Expected Success result"),
834 }
835 }
836
837 #[tokio::test]
838 async fn test_call_plugin_success_with_undefined_result() {
839 let plugin = PluginModel {
840 id: "test-plugin".to_string(),
841 path: "test-path".to_string(),
842 timeout: Duration::from_secs(DEFAULT_PLUGIN_TIMEOUT_SECONDS),
843 emit_logs: false,
844 emit_traces: false,
845 };
846 let app_state =
847 create_mock_app_state(None, None, None, None, Some(vec![plugin.clone()]), None).await;
848
849 let mut plugin_runner = MockPluginRunnerTrait::default();
850
851 plugin_runner
852 .expect_run::<MockJobProducerTrait, RelayerRepositoryStorage, TransactionRepositoryStorage, NetworkRepositoryStorage, NotificationRepositoryStorage, SignerRepositoryStorage, TransactionCounterRepositoryStorage, PluginRepositoryStorage, ApiKeyRepositoryStorage>()
853 .returning(|_, _, _, _, _, _, _| {
854 Ok(ScriptResult {
855 logs: vec![],
856 error: "".to_string(),
857 return_value: "undefined".to_string(),
858 trace: vec![],
859 })
860 });
861
862 let plugin_service = PluginService::<MockPluginRunnerTrait>::new(plugin_runner);
863 let outcome = plugin_service
864 .call_plugin(
865 plugin,
866 PluginCallRequest {
867 params: serde_json::Value::Null,
868 },
869 Arc::new(web::ThinData(app_state)),
870 )
871 .await;
872
873 match outcome {
874 PluginCallResult::Success(result) => {
875 assert_eq!(result.result, serde_json::Value::Null);
877 assert!(result.metadata.is_none());
879 }
880 _ => panic!("Expected Success result"),
881 }
882 }
883}