1use serde::{Deserialize, Serialize};
7use std::process::Stdio;
8use tokio::process::Command;
9use utoipa::ToSchema;
10
11use super::PluginError;
12
13#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, ToSchema)]
14#[serde(rename_all = "lowercase")]
15pub enum LogLevel {
16 Log,
17 Info,
18 Error,
19 Warn,
20 Debug,
21 Result,
22}
23
24#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, ToSchema)]
25pub struct LogEntry {
26 pub level: LogLevel,
27 pub message: String,
28}
29
30#[derive(Serialize, Deserialize, Debug, ToSchema)]
31pub struct ScriptResult {
32 pub logs: Vec<LogEntry>,
33 pub error: String,
34 pub trace: Vec<serde_json::Value>,
35 pub return_value: String,
36}
37
38pub struct ScriptExecutor;
39
40impl ScriptExecutor {
41 #[allow(clippy::too_many_arguments)]
42 pub async fn execute_typescript(
43 plugin_id: String,
44 script_path: String,
45 socket_path: String,
46 script_params: String,
47 http_request_id: Option<String>,
48 headers_json: Option<String>,
49 route: Option<String>,
50 config_json: Option<String>,
51 method: Option<String>,
52 query_json: Option<String>,
53 ) -> Result<ScriptResult, PluginError> {
54 if Command::new("ts-node")
55 .arg("--version")
56 .output()
57 .await
58 .is_err()
59 {
60 return Err(PluginError::SocketError(
61 "ts-node is not installed or not in PATH. Please install it with: npm install -g ts-node".to_string()
62 ));
63 }
64
65 let executor_path = std::env::current_dir()
68 .map(|cwd| cwd.join("plugins/lib/executor.ts").display().to_string())
69 .unwrap_or_else(|_| "plugins/lib/executor.ts".to_string());
70
71 let output = Command::new("ts-node")
72 .arg(executor_path) .arg(socket_path) .arg(plugin_id) .arg(script_params) .arg(script_path) .arg(http_request_id.unwrap_or_default()) .arg(headers_json.unwrap_or_default()) .arg(route.unwrap_or_default()) .arg(config_json.unwrap_or_default()) .arg(method.unwrap_or_default()) .arg(query_json.unwrap_or_default()) .stdin(Stdio::null())
84 .stdout(Stdio::piped())
85 .stderr(Stdio::piped())
86 .output()
87 .await
88 .map_err(|e| PluginError::SocketError(format!("Failed to execute script: {e}")))?;
89
90 let stdout = String::from_utf8_lossy(&output.stdout);
91 let stderr = String::from_utf8_lossy(&output.stderr);
92
93 let (logs, return_value) =
94 Self::parse_logs(stdout.lines().map(|l| l.to_string()).collect())?;
95
96 if !output.status.success() {
98 if let Some(error_line) = stderr.lines().find(|l| !l.trim().is_empty()) {
100 if let Ok(error_info) = serde_json::from_str::<serde_json::Value>(error_line) {
101 let message = error_info["message"]
102 .as_str()
103 .unwrap_or(&stderr)
104 .to_string();
105 let status = error_info
106 .get("status")
107 .and_then(|v| v.as_u64())
108 .unwrap_or(500) as u16;
109 let code = error_info
110 .get("code")
111 .and_then(|v| v.as_str())
112 .map(|s| s.to_string());
113 let details = error_info
114 .get("details")
115 .cloned()
116 .or_else(|| error_info.get("data").cloned());
117 return Err(PluginError::HandlerError(Box::new(
118 super::PluginHandlerPayload {
119 message,
120 status,
121 code,
122 details,
123 logs: Some(logs),
124 traces: None,
125 },
126 )));
127 }
128 }
129 return Err(PluginError::HandlerError(Box::new(
131 super::PluginHandlerPayload {
132 message: stderr.to_string(),
133 status: 500,
134 code: None,
135 details: None,
136 logs: Some(logs),
137 traces: None,
138 },
139 )));
140 }
141
142 Ok(ScriptResult {
143 logs,
144 return_value,
145 error: stderr.to_string(),
146 trace: Vec::new(),
147 })
148 }
149
150 fn parse_logs(logs: Vec<String>) -> Result<(Vec<LogEntry>, String), PluginError> {
151 let mut result = Vec::new();
152 let mut return_value = String::new();
153
154 for log in logs {
155 let log: LogEntry = serde_json::from_str(&log).map_err(|e| {
156 PluginError::PluginExecutionError(format!("Failed to parse log: {e}"))
157 })?;
158
159 if log.level == LogLevel::Result {
160 return_value = log.message;
161 } else {
162 result.push(log);
163 }
164 }
165
166 Ok((result, return_value))
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::fs;
173
174 use tempfile::tempdir;
175
176 use super::*;
177
178 static TS_CONFIG: &str = r#"
179 {
180 "compilerOptions": {
181 "target": "es2016",
182 "module": "commonjs",
183 "esModuleInterop": true,
184 "forceConsistentCasingInFileNames": true,
185 "strict": true,
186 "skipLibCheck": true
187 }
188 }
189"#;
190
191 #[tokio::test]
192 async fn test_execute_typescript() {
193 let temp_dir = tempdir().unwrap();
194 let ts_config = temp_dir.path().join("tsconfig.json");
195 let script_path = temp_dir.path().join("test_execute_typescript.ts");
196 let socket_path = temp_dir.path().join("test_execute_typescript.sock");
197
198 let content = r#"
199 export async function handler(api: any, params: any) {
200 console.log('test');
201 console.info('test-info');
202 return 'test-result';
203 }
204 "#;
205 fs::write(script_path.clone(), content).unwrap();
206 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
207
208 let result = ScriptExecutor::execute_typescript(
209 "test-plugin-1".to_string(),
210 script_path.display().to_string(),
211 socket_path.display().to_string(),
212 "{}".to_string(),
213 None,
214 None,
215 None,
216 None,
217 None,
218 None,
219 )
220 .await;
221
222 assert!(result.is_ok());
223 let result = result.unwrap();
224 assert_eq!(result.logs[0].level, LogLevel::Log);
225 assert_eq!(result.logs[0].message, "test");
226 assert_eq!(result.logs[1].level, LogLevel::Info);
227 assert_eq!(result.logs[1].message, "test-info");
228 assert_eq!(result.return_value, "test-result");
229 }
230
231 #[tokio::test]
232 async fn test_execute_typescript_with_result() {
233 let temp_dir = tempdir().unwrap();
234 let ts_config = temp_dir.path().join("tsconfig.json");
235 let script_path = temp_dir
236 .path()
237 .join("test_execute_typescript_with_result.ts");
238 let socket_path = temp_dir
239 .path()
240 .join("test_execute_typescript_with_result.sock");
241
242 let content = r#"
243 export async function handler(api: any, params: any) {
244 console.log('test');
245 console.info('test-info');
246 return {
247 test: 'test-result',
248 test2: 'test-result2'
249 };
250 }
251 "#;
252 fs::write(script_path.clone(), content).unwrap();
253 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
254
255 let result = ScriptExecutor::execute_typescript(
256 "test-plugin-1".to_string(),
257 script_path.display().to_string(),
258 socket_path.display().to_string(),
259 "{}".to_string(),
260 None,
261 None,
262 None,
263 None,
264 None,
265 None,
266 )
267 .await;
268
269 assert!(result.is_ok());
270 let result = result.unwrap();
271 assert_eq!(result.logs[0].level, LogLevel::Log);
272 assert_eq!(result.logs[0].message, "test");
273 assert_eq!(result.logs[1].level, LogLevel::Info);
274 assert_eq!(result.logs[1].message, "test-info");
275 assert_eq!(
276 result.return_value,
277 "{\"test\":\"test-result\",\"test2\":\"test-result2\"}"
278 );
279 }
280
281 #[tokio::test]
282 async fn test_execute_typescript_error() {
283 let temp_dir = tempdir().unwrap();
284 let ts_config = temp_dir.path().join("tsconfig.json");
285 let script_path = temp_dir.path().join("test_execute_typescript_error.ts");
286 let socket_path = temp_dir.path().join("test_execute_typescript_error.sock");
287
288 let content = "console.logger('test');";
289 fs::write(script_path.clone(), content).unwrap();
290 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
291
292 let result = ScriptExecutor::execute_typescript(
293 "test-plugin-1".to_string(),
294 script_path.display().to_string(),
295 socket_path.display().to_string(),
296 "{}".to_string(),
297 None,
298 None,
299 None,
300 None,
301 None,
302 None,
303 )
304 .await;
305
306 assert!(result.is_err());
308
309 if let Err(PluginError::HandlerError(ctx)) = result {
310 assert_eq!(ctx.status, 500);
313 assert!(!ctx.message.is_empty());
315 } else {
316 panic!("Expected PluginError::HandlerError, got: {result:?}");
317 }
318 }
319
320 #[tokio::test]
321 async fn test_execute_typescript_handler_json_error() {
322 let temp_dir = tempdir().unwrap();
323 let ts_config = temp_dir.path().join("tsconfig.json");
324 let script_path = temp_dir
325 .path()
326 .join("test_execute_typescript_handler_json_error.ts");
327 let socket_path = temp_dir
328 .path()
329 .join("test_execute_typescript_handler_json_error.sock");
330
331 let content = r#"
334 export async function handler(_api: any, _params: any) {
335 const err: any = new Error('Validation failed');
336 err.code = 'VALIDATION_FAILED';
337 err.status = 422;
338 err.details = { field: 'email' };
339 throw err;
340 }
341 "#;
342 fs::write(&script_path, content).unwrap();
343 fs::write(&ts_config, TS_CONFIG.as_bytes()).unwrap();
344
345 let result = ScriptExecutor::execute_typescript(
346 "test-plugin-json-error".to_string(),
347 script_path.display().to_string(),
348 socket_path.display().to_string(),
349 "{}".to_string(),
350 None,
351 None,
352 None,
353 None,
354 None,
355 None,
356 )
357 .await;
358
359 match result {
360 Err(PluginError::HandlerError(ctx)) => {
361 assert_eq!(ctx.message, "Validation failed");
362 assert_eq!(ctx.status, 422);
363 assert_eq!(ctx.code.as_deref(), Some("VALIDATION_FAILED"));
364 let d = ctx.details.expect("details should be present");
365 assert_eq!(d["field"].as_str(), Some("email"));
366 }
367 other => panic!("Expected HandlerError, got: {other:?}"),
368 }
369 }
370 #[tokio::test]
371 async fn test_parse_logs_error() {
372 let temp_dir = tempdir().unwrap();
373 let ts_config = temp_dir.path().join("tsconfig.json");
374 let script_path = temp_dir.path().join("test_execute_typescript.ts");
375 let socket_path = temp_dir.path().join("test_execute_typescript.sock");
376
377 let invalid_content = r#"
378 export async function handler(api: any, params: any) {
379 // Output raw invalid JSON directly to stdout (bypasses LogInterceptor)
380 process.stdout.write('invalid json line\n');
381 process.stdout.write('{"level":"log","message":"valid"}\n');
382 process.stdout.write('another invalid line\n');
383 return 'test';
384 }
385 "#;
386 fs::write(script_path.clone(), invalid_content).unwrap();
387 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
388
389 let result = ScriptExecutor::execute_typescript(
390 "test-plugin-1".to_string(),
391 script_path.display().to_string(),
392 socket_path.display().to_string(),
393 "{}".to_string(),
394 None,
395 None,
396 None,
397 None,
398 None,
399 None,
400 )
401 .await;
402
403 assert!(result.is_err());
404 assert!(result
405 .err()
406 .unwrap()
407 .to_string()
408 .contains("Failed to parse log"));
409 }
410
411 #[tokio::test]
412 async fn test_execute_typescript_with_headers() {
413 let temp_dir = tempdir().unwrap();
414 let ts_config = temp_dir.path().join("tsconfig.json");
415 let script_path = temp_dir
416 .path()
417 .join("test_execute_typescript_with_headers.ts");
418 let socket_path = temp_dir
419 .path()
420 .join("test_execute_typescript_with_headers.sock");
421
422 let content = r#"
424 export async function handler(context: any) {
425 const { headers, params } = context;
426 console.log(`Received ${Object.keys(headers).length} headers`);
427 return {
428 headerCount: Object.keys(headers).length,
429 customHeader: headers['x-custom-header']?.[0],
430 authHeader: headers['authorization']?.[0],
431 multiValueHeader: headers['x-multi-value'],
432 params: params
433 };
434 }
435 "#;
436 fs::write(script_path.clone(), content).unwrap();
437 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
438
439 let headers_json = r#"{"x-custom-header":["custom-value"],"authorization":["Bearer token123"],"x-multi-value":["value1","value2"]}"#;
441
442 let result = ScriptExecutor::execute_typescript(
443 "test-plugin-headers".to_string(),
444 script_path.display().to_string(),
445 socket_path.display().to_string(),
446 r#"{"foo":"bar"}"#.to_string(),
447 None,
448 Some(headers_json.to_string()),
449 None,
450 None,
451 None,
452 None,
453 )
454 .await;
455
456 assert!(result.is_ok());
457 let result = result.unwrap();
458
459 assert_eq!(result.logs[0].level, LogLevel::Log);
461 assert!(result.logs[0].message.contains("Received 3 headers"));
462
463 let return_obj: serde_json::Value =
465 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
466
467 assert_eq!(return_obj["headerCount"], 3);
468 assert_eq!(return_obj["customHeader"], "custom-value");
469 assert_eq!(return_obj["authHeader"], "Bearer token123");
470 assert_eq!(
471 return_obj["multiValueHeader"],
472 serde_json::json!(["value1", "value2"])
473 );
474 assert_eq!(return_obj["params"], serde_json::json!({"foo": "bar"}));
475 }
476
477 #[tokio::test]
478 async fn test_execute_typescript_all_log_levels() {
479 let temp_dir = tempdir().unwrap();
480 let ts_config = temp_dir.path().join("tsconfig.json");
481 let script_path = temp_dir
482 .path()
483 .join("test_execute_typescript_all_log_levels.ts");
484 let socket_path = temp_dir
485 .path()
486 .join("test_execute_typescript_all_log_levels.sock");
487
488 let content = r#"
489 export async function handler(api: any, params: any) {
490 console.log('log message');
491 console.info('info message');
492 console.warn('warn message');
493 console.error('error message');
494 console.debug('debug message');
495 return 'success';
496 }
497 "#;
498 fs::write(script_path.clone(), content).unwrap();
499 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
500
501 let result = ScriptExecutor::execute_typescript(
502 "test-plugin-log-levels".to_string(),
503 script_path.display().to_string(),
504 socket_path.display().to_string(),
505 "{}".to_string(),
506 None,
507 None,
508 None,
509 None,
510 None,
511 None,
512 )
513 .await;
514
515 assert!(result.is_ok());
516 let result = result.unwrap();
517 assert_eq!(result.logs.len(), 5);
518 assert_eq!(result.logs[0].level, LogLevel::Log);
519 assert_eq!(result.logs[0].message, "log message");
520 assert_eq!(result.logs[1].level, LogLevel::Info);
521 assert_eq!(result.logs[1].message, "info message");
522 assert_eq!(result.logs[2].level, LogLevel::Warn);
523 assert_eq!(result.logs[2].message, "warn message");
524 assert_eq!(result.logs[3].level, LogLevel::Error);
525 assert_eq!(result.logs[3].message, "error message");
526 assert_eq!(result.logs[4].level, LogLevel::Debug);
527 assert_eq!(result.logs[4].message, "debug message");
528 assert_eq!(result.return_value, "success");
529 }
530
531 #[tokio::test]
532 async fn test_execute_typescript_with_request_id() {
533 let temp_dir = tempdir().unwrap();
534 let ts_config = temp_dir.path().join("tsconfig.json");
535 let script_path = temp_dir
536 .path()
537 .join("test_execute_typescript_with_request_id.ts");
538 let socket_path = temp_dir
539 .path()
540 .join("test_execute_typescript_with_request_id.sock");
541
542 let content = r#"
546 export async function handler(api: any, params: any) {
547 console.log('handler executed');
548 return { status: 'ok' };
549 }
550 "#;
551 fs::write(script_path.clone(), content).unwrap();
552 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
553
554 let result = ScriptExecutor::execute_typescript(
555 "test-plugin-request-id".to_string(),
556 script_path.display().to_string(),
557 socket_path.display().to_string(),
558 "{}".to_string(),
559 Some("req-12345-abcde".to_string()),
560 None,
561 None,
562 None,
563 None,
564 None,
565 )
566 .await;
567
568 assert!(result.is_ok());
569 let result = result.unwrap();
570 assert_eq!(result.logs[0].level, LogLevel::Log);
571 assert_eq!(result.logs[0].message, "handler executed");
572 assert_eq!(result.return_value, "{\"status\":\"ok\"}");
573 }
574
575 #[tokio::test]
576 async fn test_execute_typescript_empty_return() {
577 let temp_dir = tempdir().unwrap();
578 let ts_config = temp_dir.path().join("tsconfig.json");
579 let script_path = temp_dir
580 .path()
581 .join("test_execute_typescript_empty_return.ts");
582 let socket_path = temp_dir
583 .path()
584 .join("test_execute_typescript_empty_return.sock");
585
586 let content = r#"
587 export async function handler(api: any, params: any) {
588 console.log('handler called');
589 // Return undefined (no explicit return)
590 }
591 "#;
592 fs::write(script_path.clone(), content).unwrap();
593 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
594
595 let result = ScriptExecutor::execute_typescript(
596 "test-plugin-empty-return".to_string(),
597 script_path.display().to_string(),
598 socket_path.display().to_string(),
599 "{}".to_string(),
600 None,
601 None,
602 None,
603 None,
604 None,
605 None,
606 )
607 .await;
608
609 assert!(result.is_ok());
610 let result = result.unwrap();
611 assert_eq!(result.logs[0].level, LogLevel::Log);
612 assert_eq!(result.logs[0].message, "handler called");
613 assert!(result.return_value.is_empty() || result.return_value == "undefined");
615 }
616
617 #[tokio::test]
618 async fn test_execute_typescript_null_return() {
619 let temp_dir = tempdir().unwrap();
620 let ts_config = temp_dir.path().join("tsconfig.json");
621 let script_path = temp_dir
622 .path()
623 .join("test_execute_typescript_null_return.ts");
624 let socket_path = temp_dir
625 .path()
626 .join("test_execute_typescript_null_return.sock");
627
628 let content = r#"
629 export async function handler(api: any, params: any) {
630 console.log('returning null');
631 return null;
632 }
633 "#;
634 fs::write(script_path.clone(), content).unwrap();
635 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
636
637 let result = ScriptExecutor::execute_typescript(
638 "test-plugin-null-return".to_string(),
639 script_path.display().to_string(),
640 socket_path.display().to_string(),
641 "{}".to_string(),
642 None,
643 None,
644 None,
645 None,
646 None,
647 None,
648 )
649 .await;
650
651 assert!(result.is_ok());
652 let result = result.unwrap();
653 assert_eq!(result.return_value, "null");
654 }
655
656 #[tokio::test]
657 async fn test_execute_typescript_legacy_two_param_handler() {
658 let temp_dir = tempdir().unwrap();
659 let ts_config = temp_dir.path().join("tsconfig.json");
660 let script_path = temp_dir
661 .path()
662 .join("test_execute_typescript_legacy_handler.ts");
663 let socket_path = temp_dir
664 .path()
665 .join("test_execute_typescript_legacy_handler.sock");
666
667 let content = r#"
669 export async function handler(api: any, params: any) {
670 console.log('legacy handler');
671 return {
672 paramsReceived: params,
673 handlerType: 'legacy-2-param'
674 };
675 }
676 "#;
677 fs::write(script_path.clone(), content).unwrap();
678 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
679
680 let result = ScriptExecutor::execute_typescript(
681 "test-plugin-legacy".to_string(),
682 script_path.display().to_string(),
683 socket_path.display().to_string(),
684 r#"{"key":"value"}"#.to_string(),
685 None,
686 None,
687 None,
688 None,
689 None,
690 None,
691 )
692 .await;
693
694 assert!(result.is_ok());
695 let result = result.unwrap();
696 assert_eq!(result.logs[0].level, LogLevel::Log);
697 assert_eq!(result.logs[0].message, "legacy handler");
698
699 let return_obj: serde_json::Value =
700 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
701 assert_eq!(return_obj["handlerType"], "legacy-2-param");
702 assert_eq!(
703 return_obj["paramsReceived"],
704 serde_json::json!({"key": "value"})
705 );
706 }
707
708 #[tokio::test]
709 async fn test_execute_typescript_context_single_param_handler() {
710 let temp_dir = tempdir().unwrap();
711 let ts_config = temp_dir.path().join("tsconfig.json");
712 let script_path = temp_dir
713 .path()
714 .join("test_execute_typescript_context_handler.ts");
715 let socket_path = temp_dir
716 .path()
717 .join("test_execute_typescript_context_handler.sock");
718
719 let content = r#"
721 export async function handler(context: any) {
722 const { api, params, kv, headers } = context;
723 console.log('modern context handler');
724 return {
725 paramsReceived: params,
726 hasApi: !!api,
727 hasKv: !!kv,
728 hasHeaders: !!headers,
729 handlerType: 'modern-context'
730 };
731 }
732 "#;
733 fs::write(script_path.clone(), content).unwrap();
734 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
735
736 let result = ScriptExecutor::execute_typescript(
737 "test-plugin-context".to_string(),
738 script_path.display().to_string(),
739 socket_path.display().to_string(),
740 r#"{"foo":"bar"}"#.to_string(),
741 None,
742 None,
743 None,
744 None,
745 None,
746 None,
747 )
748 .await;
749
750 assert!(result.is_ok());
751 let result = result.unwrap();
752
753 let return_obj: serde_json::Value =
754 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
755 assert_eq!(return_obj["handlerType"], "modern-context");
756 assert_eq!(return_obj["hasApi"], true);
757 assert_eq!(return_obj["hasKv"], true);
758 assert_eq!(return_obj["hasHeaders"], true);
759 assert_eq!(
760 return_obj["paramsReceived"],
761 serde_json::json!({"foo": "bar"})
762 );
763 }
764
765 #[tokio::test]
766 async fn test_execute_typescript_complex_params() {
767 let temp_dir = tempdir().unwrap();
768 let ts_config = temp_dir.path().join("tsconfig.json");
769 let script_path = temp_dir
770 .path()
771 .join("test_execute_typescript_complex_params.ts");
772 let socket_path = temp_dir
773 .path()
774 .join("test_execute_typescript_complex_params.sock");
775
776 let content = r#"
777 export async function handler(api: any, params: any) {
778 console.log(`Received ${params.items.length} items`);
779 return {
780 processedItems: params.items.map((item: any) => ({
781 ...item,
782 processed: true
783 })),
784 metadata: params.metadata,
785 totalCount: params.items.length
786 };
787 }
788 "#;
789 fs::write(script_path.clone(), content).unwrap();
790 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
791
792 let complex_params = r#"{
793 "items": [
794 {"id": 1, "name": "item1", "tags": ["a", "b"]},
795 {"id": 2, "name": "item2", "tags": ["c", "d"]}
796 ],
797 "metadata": {
798 "source": "test",
799 "timestamp": 1234567890,
800 "nested": {
801 "deep": {
802 "value": true
803 }
804 }
805 }
806 }"#;
807
808 let result = ScriptExecutor::execute_typescript(
809 "test-plugin-complex-params".to_string(),
810 script_path.display().to_string(),
811 socket_path.display().to_string(),
812 complex_params.to_string(),
813 None,
814 None,
815 None,
816 None,
817 None,
818 None,
819 )
820 .await;
821
822 assert!(result.is_ok());
823 let result = result.unwrap();
824 assert_eq!(result.logs[0].level, LogLevel::Log);
825 assert!(result.logs[0].message.contains("Received 2 items"));
826
827 let return_obj: serde_json::Value =
828 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
829 assert_eq!(return_obj["totalCount"], 2);
830 assert_eq!(return_obj["processedItems"][0]["processed"], true);
831 assert_eq!(return_obj["processedItems"][1]["processed"], true);
832 assert_eq!(return_obj["processedItems"][0]["name"], "item1");
833 assert_eq!(return_obj["metadata"]["source"], "test");
834 assert_eq!(return_obj["metadata"]["nested"]["deep"]["value"], true);
835 }
836
837 #[tokio::test]
838 async fn test_execute_typescript_empty_params() {
839 let temp_dir = tempdir().unwrap();
840 let ts_config = temp_dir.path().join("tsconfig.json");
841 let script_path = temp_dir
842 .path()
843 .join("test_execute_typescript_empty_params.ts");
844 let socket_path = temp_dir
845 .path()
846 .join("test_execute_typescript_empty_params.sock");
847
848 let content = r#"
849 export async function handler(api: any, params: any) {
850 console.log(`Params is empty: ${Object.keys(params).length === 0}`);
851 return { receivedEmptyParams: Object.keys(params).length === 0 };
852 }
853 "#;
854 fs::write(script_path.clone(), content).unwrap();
855 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
856
857 let result = ScriptExecutor::execute_typescript(
858 "test-plugin-empty-params".to_string(),
859 script_path.display().to_string(),
860 socket_path.display().to_string(),
861 "{}".to_string(),
862 None,
863 None,
864 None,
865 None,
866 None,
867 None,
868 )
869 .await;
870
871 assert!(result.is_ok());
872 let result = result.unwrap();
873 assert!(result.logs[0].message.contains("Params is empty: true"));
874
875 let return_obj: serde_json::Value =
876 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
877 assert_eq!(return_obj["receivedEmptyParams"], true);
878 }
879
880 #[tokio::test]
881 async fn test_execute_typescript_array_return() {
882 let temp_dir = tempdir().unwrap();
883 let ts_config = temp_dir.path().join("tsconfig.json");
884 let script_path = temp_dir
885 .path()
886 .join("test_execute_typescript_array_return.ts");
887 let socket_path = temp_dir
888 .path()
889 .join("test_execute_typescript_array_return.sock");
890
891 let content = r#"
892 export async function handler(api: any, params: any) {
893 console.log('returning array');
894 return [1, 2, 3, { nested: 'value' }, 'string'];
895 }
896 "#;
897 fs::write(script_path.clone(), content).unwrap();
898 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
899
900 let result = ScriptExecutor::execute_typescript(
901 "test-plugin-array-return".to_string(),
902 script_path.display().to_string(),
903 socket_path.display().to_string(),
904 "{}".to_string(),
905 None,
906 None,
907 None,
908 None,
909 None,
910 None,
911 )
912 .await;
913
914 assert!(result.is_ok());
915 let result = result.unwrap();
916
917 let return_array: serde_json::Value =
918 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
919 assert!(return_array.is_array());
920 assert_eq!(return_array[0], 1);
921 assert_eq!(return_array[1], 2);
922 assert_eq!(return_array[2], 3);
923 assert_eq!(return_array[3]["nested"], "value");
924 assert_eq!(return_array[4], "string");
925 }
926
927 #[tokio::test]
928 async fn test_execute_typescript_multiple_errors() {
929 let temp_dir = tempdir().unwrap();
930 let ts_config = temp_dir.path().join("tsconfig.json");
931 let script_path = temp_dir
932 .path()
933 .join("test_execute_typescript_multiple_errors.ts");
934 let socket_path = temp_dir
935 .path()
936 .join("test_execute_typescript_multiple_errors.sock");
937
938 let content = r#"
939 export async function handler(api: any, params: any) {
940 console.error('Error message 1');
941 console.error('Error message 2');
942 console.warn('Warning message');
943 throw new Error('Handler failed');
944 }
945 "#;
946 fs::write(script_path.clone(), content).unwrap();
947 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
948
949 let result = ScriptExecutor::execute_typescript(
950 "test-plugin-multiple-errors".to_string(),
951 script_path.display().to_string(),
952 socket_path.display().to_string(),
953 "{}".to_string(),
954 None,
955 None,
956 None,
957 None,
958 None,
959 None,
960 )
961 .await;
962
963 assert!(result.is_err());
964 if let Err(PluginError::HandlerError(ctx)) = result {
965 let logs = ctx.logs.expect("logs should be present");
967 assert_eq!(logs.len(), 3);
968 assert_eq!(logs[0].level, LogLevel::Error);
969 assert_eq!(logs[0].message, "Error message 1");
970 assert_eq!(logs[1].level, LogLevel::Error);
971 assert_eq!(logs[1].message, "Error message 2");
972 assert_eq!(logs[2].level, LogLevel::Warn);
973 assert_eq!(logs[2].message, "Warning message");
974 assert!(ctx.message.contains("Handler failed"));
975 } else {
976 panic!("Expected HandlerError");
977 }
978 }
979
980 #[tokio::test]
981 async fn test_execute_typescript_with_route() {
982 let temp_dir = tempdir().unwrap();
983 let ts_config = temp_dir.path().join("tsconfig.json");
984 let script_path = temp_dir
985 .path()
986 .join("test_execute_typescript_with_route.ts");
987 let socket_path = temp_dir
988 .path()
989 .join("test_execute_typescript_with_route.sock");
990
991 let content = r#"
993 export async function handler(context: any) {
994 const { route, params } = context;
995 console.log(`Received route: ${route}`);
996 return {
997 route: route,
998 params: params
999 };
1000 }
1001 "#;
1002 fs::write(script_path.clone(), content).unwrap();
1003 fs::write(ts_config.clone(), TS_CONFIG.as_bytes()).unwrap();
1004
1005 let result = ScriptExecutor::execute_typescript(
1006 "test-plugin-route".to_string(),
1007 script_path.display().to_string(),
1008 socket_path.display().to_string(),
1009 r#"{"foo":"bar"}"#.to_string(),
1010 None,
1011 None,
1012 Some("/verify".to_string()),
1013 None,
1014 None,
1015 None,
1016 )
1017 .await;
1018
1019 assert!(result.is_ok());
1020 let result = result.unwrap();
1021
1022 assert_eq!(result.logs[0].level, LogLevel::Log);
1024 assert!(result.logs[0].message.contains("Received route: /verify"));
1025
1026 let return_obj: serde_json::Value =
1028 serde_json::from_str(&result.return_value).expect("Failed to parse return value");
1029
1030 assert_eq!(return_obj["route"], "/verify");
1031 assert_eq!(return_obj["params"], serde_json::json!({"foo": "bar"}));
1032 }
1033}