1use std::collections::HashMap;
5
6use crate::{
7 api::controllers::plugin,
8 metrics::PLUGIN_CALLS,
9 models::{
10 ApiError, ApiResponse, DefaultAppState, PaginationQuery, PluginCallRequest,
11 UpdatePluginRequest,
12 },
13 repositories::PluginRepositoryTrait,
14};
15use actix_web::{get, patch, post, web, HttpRequest, HttpResponse, Responder, ResponseError};
16use url::form_urlencoded;
17
18#[get("/plugins")]
20async fn list_plugins(
21 query: web::Query<PaginationQuery>,
22 data: web::ThinData<DefaultAppState>,
23) -> impl Responder {
24 plugin::list_plugins(query.into_inner(), data).await
25}
26
27fn extract_headers(http_req: &HttpRequest) -> HashMap<String, Vec<String>> {
29 let mut headers: HashMap<String, Vec<String>> = HashMap::new();
30 for (name, value) in http_req.headers().iter() {
31 if let Ok(value_str) = value.to_str() {
32 headers
33 .entry(name.as_str().to_string())
34 .or_default()
35 .push(value_str.to_string());
36 }
37 }
38 headers
39}
40
41fn extract_query_params(http_req: &HttpRequest) -> HashMap<String, Vec<String>> {
44 let mut query_params: HashMap<String, Vec<String>> = HashMap::new();
45 let query_string = http_req.query_string();
46
47 if query_string.is_empty() {
48 return query_params;
49 }
50
51 for (key, value) in form_urlencoded::parse(query_string.as_bytes()) {
55 query_params
56 .entry(key.into_owned())
57 .or_default()
58 .push(value.into_owned());
59 }
60
61 query_params
62}
63
64fn resolve_route(path_route: &str, http_req: &HttpRequest) -> String {
67 if !path_route.is_empty() {
69 return path_route.to_string();
70 }
71
72 let query_params = extract_query_params(http_req);
74 query_params
75 .get("route")
76 .and_then(|values| values.first())
77 .cloned()
78 .unwrap_or_default()
79}
80
81fn build_plugin_call_request_from_post_body(
82 route: &str,
83 http_req: &HttpRequest,
84 body: &[u8],
85) -> Result<PluginCallRequest, HttpResponse> {
86 let body_json: serde_json::Value = match serde_json::from_slice(body) {
88 Ok(json) => json,
89 Err(e) => {
90 tracing::error!("Failed to parse request body as JSON: {}", e);
91 return Err(HttpResponse::BadRequest()
92 .json(ApiResponse::<()>::error(format!("Invalid JSON: {e}"))));
93 }
94 };
95
96 if body_json.get("params").is_some() {
98 match serde_json::from_value::<PluginCallRequest>(body_json) {
100 Ok(mut req) => {
101 req.headers = Some(extract_headers(http_req));
102 req.route = Some(route.to_string());
103 Ok(req)
104 }
105 Err(e) => {
106 tracing::error!("Failed to deserialize PluginCallRequest: {}", e);
107 Err(
108 HttpResponse::BadRequest().json(ApiResponse::<()>::error(format!(
109 "Invalid request format: {e}"
110 ))),
111 )
112 }
113 }
114 } else {
115 Ok(PluginCallRequest {
117 params: body_json,
118 headers: Some(extract_headers(http_req)),
119 route: Some(route.to_string()),
120 method: None,
121 query: None,
122 })
123 }
124}
125
126#[post("/plugins/{plugin_id}/call{route:.*}")]
128async fn plugin_call(
129 params: web::Path<(String, String)>,
130 http_req: HttpRequest,
131 body: web::Bytes,
132 data: web::ThinData<DefaultAppState>,
133) -> Result<HttpResponse, ApiError> {
134 let (plugin_id, path_route) = params.into_inner();
135 let route = resolve_route(&path_route, &http_req);
136
137 let mut plugin_call_request =
138 match build_plugin_call_request_from_post_body(&route, &http_req, body.as_ref()) {
139 Ok(req) => req,
140 Err(resp) => {
141 PLUGIN_CALLS
143 .with_label_values(&[plugin_id.as_str(), "POST", "400"])
144 .inc();
145 return Ok(resp);
146 }
147 };
148 plugin_call_request.method = Some("POST".to_string());
149 plugin_call_request.query = Some(extract_query_params(&http_req));
150
151 let result = plugin::call_plugin(plugin_id.clone(), plugin_call_request, data).await;
152
153 let status_code = match &result {
155 Ok(response) => response.status(),
156 Err(e) => e.error_response().status(),
157 };
158 let status = status_code.as_str();
159 PLUGIN_CALLS
160 .with_label_values(&[plugin_id.as_str(), "POST", status])
161 .inc();
162
163 result
164}
165
166#[get("/plugins/{plugin_id}/call{route:.*}")]
168async fn plugin_call_get(
169 params: web::Path<(String, String)>,
170 http_req: HttpRequest,
171 data: web::ThinData<DefaultAppState>,
172) -> Result<HttpResponse, ApiError> {
173 let (plugin_id, path_route) = params.into_inner();
174 let route = resolve_route(&path_route, &http_req);
175
176 let plugin = match data.plugin_repository.get_by_id(&plugin_id).await? {
178 Some(p) => p,
179 None => {
180 PLUGIN_CALLS
182 .with_label_values(&[plugin_id.as_str(), "GET", "404"])
183 .inc();
184 return Err(ApiError::NotFound(format!(
185 "Plugin with id {plugin_id} not found"
186 )));
187 }
188 };
189
190 if !plugin.allow_get_invocation {
191 PLUGIN_CALLS
193 .with_label_values(&[plugin_id.as_str(), "GET", "405"])
194 .inc();
195 return Ok(HttpResponse::MethodNotAllowed().json(ApiResponse::<()>::error(
196 "GET requests are not enabled for this plugin. Set 'allow_get_invocation: true' in plugin configuration to enable.",
197 )));
198 }
199
200 let plugin_call_request = PluginCallRequest {
202 params: serde_json::json!({}),
203 headers: Some(extract_headers(&http_req)),
204 route: Some(route),
205 method: Some("GET".to_string()),
206 query: Some(extract_query_params(&http_req)),
207 };
208
209 let result = plugin::call_plugin(plugin_id.clone(), plugin_call_request, data).await;
210
211 let status_code = match &result {
213 Ok(response) => response.status(),
214 Err(e) => e.error_response().status(),
215 };
216 let status = status_code.as_str();
217 PLUGIN_CALLS
218 .with_label_values(&[plugin_id.as_str(), "GET", status])
219 .inc();
220
221 result
222}
223
224#[get("/plugins/{plugin_id}")]
226async fn get_plugin(
227 path: web::Path<String>,
228 data: web::ThinData<DefaultAppState>,
229) -> impl Responder {
230 let plugin_id = path.into_inner();
231 plugin::get_plugin(plugin_id, data).await
232}
233
234#[patch("/plugins/{plugin_id}")]
236async fn update_plugin(
237 path: web::Path<String>,
238 body: web::Json<UpdatePluginRequest>,
239 data: web::ThinData<DefaultAppState>,
240) -> impl Responder {
241 let plugin_id = path.into_inner();
242 plugin::update_plugin(plugin_id, body.into_inner(), data).await
243}
244
245pub fn init(cfg: &mut web::ServiceConfig) {
247 cfg.service(plugin_call); cfg.service(plugin_call_get); cfg.service(get_plugin); cfg.service(update_plugin); cfg.service(list_plugins); }
254
255#[cfg(test)]
256mod tests {
257 use std::time::Duration;
258
259 use super::*;
260 use crate::{models::PluginModel, services::plugins::PluginCallResponse};
261 use actix_web::{test, App, HttpResponse};
262
263 #[derive(Clone, Default)]
269 struct CapturedRequest {
270 inner: std::sync::Arc<std::sync::Mutex<Option<PluginCallRequest>>>,
271 }
272
273 impl CapturedRequest {
274 fn capture(&self, req: PluginCallRequest) {
275 *self.inner.lock().unwrap() = Some(req);
276 }
277
278 fn get(&self) -> Option<PluginCallRequest> {
279 self.inner.lock().unwrap().clone()
280 }
281
282 fn clear(&self) {
283 *self.inner.lock().unwrap() = None;
284 }
285 }
286
287 async fn capturing_plugin_call_handler(
289 params: web::Path<(String, String)>,
290 http_req: HttpRequest,
291 body: web::Bytes,
292 captured: web::Data<CapturedRequest>,
293 ) -> impl Responder {
294 let (_plugin_id, path_route) = params.into_inner();
295 let route = resolve_route(&path_route, &http_req);
296 match build_plugin_call_request_from_post_body(&route, &http_req, body.as_ref()) {
297 Ok(mut req) => {
298 req.method = Some("POST".to_string());
299 req.query = Some(extract_query_params(&http_req));
300 captured.capture(req);
301 HttpResponse::Ok().json(PluginCallResponse {
302 result: serde_json::Value::Null,
303 metadata: None,
304 })
305 }
306 Err(resp) => resp,
307 }
308 }
309
310 async fn capturing_plugin_call_get_handler(
313 params: web::Path<(String, String)>,
314 http_req: HttpRequest,
315 captured: web::Data<CapturedRequest>,
316 ) -> impl Responder {
317 let (_plugin_id, path_route) = params.into_inner();
318 let route = resolve_route(&path_route, &http_req);
319 let plugin_call_request = PluginCallRequest {
321 params: serde_json::json!({}),
322 headers: Some(extract_headers(&http_req)),
323 route: Some(route),
324 method: Some("GET".to_string()),
325 query: Some(extract_query_params(&http_req)),
326 };
327 captured.capture(plugin_call_request);
328 HttpResponse::Ok().json(PluginCallResponse {
329 result: serde_json::Value::Null,
330 metadata: None,
331 })
332 }
333
334 async fn mock_list_plugins() -> impl Responder {
338 HttpResponse::Ok().json(vec![
339 PluginModel {
340 id: "test-plugin".to_string(),
341 path: "test-path".to_string(),
342 timeout: Duration::from_secs(69),
343 emit_logs: false,
344 emit_traces: false,
345 forward_logs: false,
346 allow_get_invocation: false,
347 config: None,
348 raw_response: false,
349 },
350 PluginModel {
351 id: "test-plugin2".to_string(),
352 path: "test-path2".to_string(),
353 timeout: Duration::from_secs(69),
354 emit_logs: false,
355 emit_traces: false,
356 forward_logs: false,
357 allow_get_invocation: false,
358 config: None,
359 raw_response: false,
360 },
361 ])
362 }
363
364 async fn mock_plugin_call() -> impl Responder {
365 HttpResponse::Ok().json(PluginCallResponse {
366 result: serde_json::Value::Null,
367 metadata: None,
368 })
369 }
370
371 #[actix_web::test]
372 async fn test_plugin_call() {
373 let app = test::init_service(
374 App::new()
375 .service(
376 web::resource("/plugins/{plugin_id}/call")
377 .route(web::post().to(mock_plugin_call)),
378 )
379 .configure(init),
380 )
381 .await;
382
383 let req = test::TestRequest::post()
384 .uri("/plugins/test-plugin/call")
385 .insert_header(("Content-Type", "application/json"))
386 .set_json(serde_json::json!({
387 "params": serde_json::Value::Null,
388 }))
389 .to_request();
390 let resp = test::call_service(&app, req).await;
391
392 assert!(resp.status().is_success());
393
394 let body = test::read_body(resp).await;
395 let plugin_call_response: PluginCallResponse = serde_json::from_slice(&body).unwrap();
396 assert!(plugin_call_response.result.is_null());
397 }
398
399 #[actix_web::test]
400 async fn test_list_plugins() {
401 let app = test::init_service(
402 App::new()
403 .service(web::resource("/plugins").route(web::get().to(mock_list_plugins)))
404 .configure(init),
405 )
406 .await;
407
408 let req = test::TestRequest::get().uri("/plugins").to_request();
409 let resp = test::call_service(&app, req).await;
410
411 assert!(resp.status().is_success());
412
413 let body = test::read_body(resp).await;
414 let plugin_call_response: Vec<PluginModel> = serde_json::from_slice(&body).unwrap();
415
416 assert_eq!(plugin_call_response.len(), 2);
417 assert_eq!(plugin_call_response[0].id, "test-plugin");
418 assert_eq!(plugin_call_response[0].path, "test-path");
419 assert_eq!(plugin_call_response[1].id, "test-plugin2");
420 assert_eq!(plugin_call_response[1].path, "test-path2");
421 }
422
423 #[actix_web::test]
424 async fn test_plugin_call_extracts_headers() {
425 let app = test::init_service(
427 App::new()
428 .service(
429 web::resource("/plugins/{plugin_id}/call")
430 .route(web::post().to(mock_plugin_call)),
431 )
432 .configure(init),
433 )
434 .await;
435
436 let req = test::TestRequest::post()
437 .uri("/plugins/test-plugin/call")
438 .insert_header(("Content-Type", "application/json"))
439 .insert_header(("X-Custom-Header", "custom-value"))
440 .insert_header(("Authorization", "Bearer test-token"))
441 .insert_header(("X-Request-Id", "req-12345"))
442 .insert_header(("Accept", "application/json"))
444 .set_json(serde_json::json!({
445 "params": {"test": "data"},
446 }))
447 .to_request();
448
449 let resp = test::call_service(&app, req).await;
450 assert!(resp.status().is_success());
451 }
452
453 #[actix_web::test]
454 async fn test_extract_headers_unit() {
455 use actix_web::test::TestRequest;
457
458 let req = TestRequest::default()
459 .insert_header(("X-Custom-Header", "value1"))
460 .insert_header(("Authorization", "Bearer token"))
461 .insert_header(("Content-Type", "application/json"))
462 .to_http_request();
463
464 let headers = extract_headers(&req);
465
466 assert_eq!(
467 headers.get("x-custom-header"),
468 Some(&vec!["value1".to_string()])
469 );
470 assert_eq!(
471 headers.get("authorization"),
472 Some(&vec!["Bearer token".to_string()])
473 );
474 assert_eq!(
475 headers.get("content-type"),
476 Some(&vec!["application/json".to_string()])
477 );
478 }
479
480 #[actix_web::test]
481 async fn test_extract_headers_multi_value() {
482 use actix_web::test::TestRequest;
483
484 let req = TestRequest::default()
486 .insert_header(("X-Values", "value1"))
487 .to_http_request();
488
489 let headers = extract_headers(&req);
490
491 let values = headers.get("x-values").unwrap();
493 assert_eq!(values.len(), 1);
494 assert_eq!(values[0], "value1");
495 }
496
497 #[actix_web::test]
498 async fn test_extract_headers_empty() {
499 use actix_web::test::TestRequest;
500
501 let req = TestRequest::default().to_http_request();
502 let headers = extract_headers(&req);
503
504 let _ = headers.len();
505 }
506
507 #[actix_web::test]
508 async fn test_extract_headers_skips_non_utf8_value() {
509 use actix_web::http::header::{HeaderName, HeaderValue};
510 use actix_web::test::TestRequest;
511
512 let non_utf8 = HeaderValue::from_bytes(&[0x80]).unwrap();
513 let req = TestRequest::default()
514 .insert_header((HeaderName::from_static("x-non-utf8"), non_utf8))
515 .insert_header(("X-Ok", "ok"))
516 .to_http_request();
517
518 let headers = extract_headers(&req);
519
520 assert_eq!(headers.get("x-ok"), Some(&vec!["ok".to_string()]));
521 assert!(headers.get("x-non-utf8").is_none());
522 }
523
524 #[actix_web::test]
525 async fn test_extract_query_params() {
526 use actix_web::test::TestRequest;
527
528 let req = TestRequest::default()
530 .uri("/test?foo=bar&baz=qux")
531 .to_http_request();
532
533 let query_params = extract_query_params(&req);
534
535 assert_eq!(query_params.get("foo"), Some(&vec!["bar".to_string()]));
536 assert_eq!(query_params.get("baz"), Some(&vec!["qux".to_string()]));
537 }
538
539 #[actix_web::test]
540 async fn test_extract_query_params_multiple_values() {
541 use actix_web::test::TestRequest;
542
543 let req = TestRequest::default()
545 .uri("/test?tag=a&tag=b&tag=c")
546 .to_http_request();
547
548 let query_params = extract_query_params(&req);
549
550 assert_eq!(
551 query_params.get("tag"),
552 Some(&vec!["a".to_string(), "b".to_string(), "c".to_string()])
553 );
554 }
555
556 #[actix_web::test]
557 async fn test_extract_query_params_empty() {
558 use actix_web::test::TestRequest;
559
560 let req = TestRequest::default().uri("/test").to_http_request();
562
563 let query_params = extract_query_params(&req);
564
565 assert!(query_params.is_empty());
566 }
567
568 #[actix_web::test]
569 async fn test_extract_query_params_decoding_and_flags() {
570 use actix_web::test::TestRequest;
571
572 let req = TestRequest::default()
574 .uri("/test?foo=hello%20world&bar=a+b&tag=a%2Bb&flag&tag=c")
575 .to_http_request();
576
577 let query_params = extract_query_params(&req);
578
579 assert_eq!(
580 query_params.get("foo"),
581 Some(&vec!["hello world".to_string()])
582 );
583 assert_eq!(query_params.get("bar"), Some(&vec!["a b".to_string()]));
584 assert_eq!(
585 query_params.get("tag"),
586 Some(&vec!["a+b".to_string(), "c".to_string()])
587 );
588 assert_eq!(query_params.get("flag"), Some(&vec!["".to_string()]));
589 }
590
591 #[actix_web::test]
592 async fn test_extract_query_params_decodes_keys_and_handles_empty_values() {
593 use actix_web::test::TestRequest;
594
595 let req = TestRequest::default()
596 .uri("/test?na%6De=al%69ce&empty=&=noval&tag=a&tag=")
597 .to_http_request();
598
599 let query_params = extract_query_params(&req);
600
601 assert_eq!(query_params.get("name"), Some(&vec!["alice".to_string()]));
602 assert_eq!(query_params.get("empty"), Some(&vec!["".to_string()]));
603 assert_eq!(query_params.get(""), Some(&vec!["noval".to_string()]));
604 assert_eq!(
605 query_params.get("tag"),
606 Some(&vec!["a".to_string(), "".to_string()])
607 );
608 }
609
610 #[actix_web::test]
611 async fn test_build_plugin_call_request_invalid_json() {
612 use actix_web::test::TestRequest;
613
614 let http_req = TestRequest::default().to_http_request();
615
616 let result = build_plugin_call_request_from_post_body("/verify", &http_req, b"{bad");
617 assert!(result.is_err());
618 assert_eq!(
619 result.err().unwrap().status(),
620 actix_web::http::StatusCode::BAD_REQUEST
621 );
622 }
623
624 #[actix_web::test]
625 async fn test_build_plugin_call_request_wraps_body_without_params_field() {
626 use actix_web::test::TestRequest;
627
628 let http_req = TestRequest::default()
629 .insert_header(("X-Custom", "v1"))
630 .to_http_request();
631
632 let body = serde_json::to_vec(&serde_json::json!({"user": "alice"})).unwrap();
633 let req = build_plugin_call_request_from_post_body("/route", &http_req, &body).unwrap();
634
635 assert_eq!(req.params, serde_json::json!({"user": "alice"}));
636 assert_eq!(req.route, Some("/route".to_string()));
637 assert!(req.headers.as_ref().unwrap().contains_key("x-custom"));
638 }
639
640 #[actix_web::test]
641 async fn test_build_plugin_call_request_uses_params_field_when_present() {
642 use actix_web::test::TestRequest;
643
644 let http_req = TestRequest::default()
645 .insert_header(("X-Custom", "v1"))
646 .to_http_request();
647
648 let body = serde_json::to_vec(&serde_json::json!({"params": {"k": "v"}})).unwrap();
649 let req = build_plugin_call_request_from_post_body("/route", &http_req, &body).unwrap();
650
651 assert_eq!(req.params, serde_json::json!({"k": "v"}));
652 assert_eq!(req.route, Some("/route".to_string()));
653 assert!(req.headers.as_ref().unwrap().contains_key("x-custom"));
654 }
655
656 #[actix_web::test]
657 async fn test_build_plugin_call_request_with_empty_body() {
658 use actix_web::test::TestRequest;
659
660 let http_req = TestRequest::default().to_http_request();
661 let body = b"{}";
662 let req = build_plugin_call_request_from_post_body("/test", &http_req, body).unwrap();
663
664 assert_eq!(req.params, serde_json::json!({}));
665 assert_eq!(req.route, Some("/test".to_string()));
666 }
667
668 #[actix_web::test]
669 async fn test_build_plugin_call_request_with_null_params() {
670 use actix_web::test::TestRequest;
671
672 let http_req = TestRequest::default().to_http_request();
673 let body = serde_json::to_vec(&serde_json::json!({"params": null})).unwrap();
674 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
675
676 assert_eq!(req.params, serde_json::Value::Null);
677 assert_eq!(req.route, Some("/test".to_string()));
678 }
679
680 #[actix_web::test]
681 async fn test_build_plugin_call_request_with_array_params() {
682 use actix_web::test::TestRequest;
683
684 let http_req = TestRequest::default().to_http_request();
685 let body = serde_json::to_vec(&serde_json::json!({"params": [1, 2, 3]})).unwrap();
686 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
687
688 assert_eq!(req.params, serde_json::json!([1, 2, 3]));
689 assert_eq!(req.route, Some("/test".to_string()));
690 }
691
692 #[actix_web::test]
693 async fn test_build_plugin_call_request_with_string_params() {
694 use actix_web::test::TestRequest;
695
696 let http_req = TestRequest::default().to_http_request();
697 let body = serde_json::to_vec(&serde_json::json!({"params": "test-string"})).unwrap();
698 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
699
700 assert_eq!(req.params, serde_json::json!("test-string"));
701 assert_eq!(req.route, Some("/test".to_string()));
702 }
703
704 #[actix_web::test]
705 async fn test_build_plugin_call_request_with_additional_fields() {
706 use actix_web::test::TestRequest;
707
708 let http_req = TestRequest::default().to_http_request();
709 let body = serde_json::to_vec(&serde_json::json!({
712 "params": {"k": "v"},
713 "extra_field": "ignored"
714 }))
715 .unwrap();
716 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
717
718 assert_eq!(req.params, serde_json::json!({"k": "v"}));
719 assert_eq!(req.route, Some("/test".to_string()));
720 }
721
722 #[actix_web::test]
723 async fn test_plugin_call_get_not_found() {
724 use crate::api::controllers::plugin;
725 use crate::utils::mocks::mockutils::create_mock_app_state;
726
727 let app_state = create_mock_app_state(None, None, None, None, None, None).await;
729 let _plugin_call_request = PluginCallRequest {
730 params: serde_json::json!({}),
731 headers: None,
732 route: None,
733 method: Some("GET".to_string()),
734 query: None,
735 };
736
737 let result = plugin::call_plugin(
739 "non-existent-plugin".to_string(),
740 _plugin_call_request,
741 web::ThinData(app_state),
742 )
743 .await;
744
745 assert!(result.is_err());
746 if let Err(crate::models::ApiError::NotFound(_)) = result {
748 } else {
750 panic!("Expected NotFound error, got different error");
751 }
752 }
753
754 #[actix_web::test]
755 async fn test_plugin_call_get_allowed() {
756 use crate::utils::mocks::mockutils::create_mock_app_state;
757 use std::time::Duration;
758
759 let plugin = PluginModel {
760 id: "test-plugin".to_string(),
761 path: "test-path".to_string(),
762 timeout: Duration::from_secs(60),
763 emit_logs: false,
764 emit_traces: false,
765 raw_response: false,
766 allow_get_invocation: true,
767 config: None,
768 forward_logs: false,
769 };
770
771 let app_state =
772 create_mock_app_state(None, None, None, None, Some(vec![plugin]), None).await;
773
774 let plugin_repo = app_state.plugin_repository.clone();
776 let found_plugin = plugin_repo.get_by_id("test-plugin").await.unwrap();
777 assert!(found_plugin.is_some());
778 assert!(found_plugin.unwrap().allow_get_invocation);
779 }
780
781 #[actix_web::test]
782 async fn test_extract_query_params_with_only_question_mark() {
783 use actix_web::test::TestRequest;
784
785 let req = TestRequest::default().uri("/test?").to_http_request();
786 let query_params = extract_query_params(&req);
787
788 assert!(query_params.is_empty());
789 }
790
791 #[actix_web::test]
792 async fn test_extract_query_params_with_ampersand_only() {
793 use actix_web::test::TestRequest;
794
795 let req = TestRequest::default().uri("/test?&").to_http_request();
796 let query_params = extract_query_params(&req);
797
798 let _ = query_params.len();
802 }
803
804 #[actix_web::test]
805 async fn test_extract_query_params_with_special_characters() {
806 use actix_web::test::TestRequest;
807
808 let req = TestRequest::default()
809 .uri("/test?key=value%20with%20spaces&symbol=%26%3D%3F")
810 .to_http_request();
811 let query_params = extract_query_params(&req);
812
813 assert_eq!(
814 query_params.get("key"),
815 Some(&vec!["value with spaces".to_string()])
816 );
817 assert_eq!(query_params.get("symbol"), Some(&vec!["&=?".to_string()]));
818 }
819
820 #[actix_web::test]
821 async fn test_extract_headers_case_insensitive() {
822 use actix_web::test::TestRequest;
823
824 let req = TestRequest::default()
825 .insert_header(("X-Custom-Header", "value1"))
826 .insert_header(("x-custom-header", "value2"))
827 .to_http_request();
828
829 let headers = extract_headers(&req);
830
831 let values = headers.get("x-custom-header");
833 assert!(values.is_some());
834 assert!(!values.unwrap().is_empty());
836 }
837
838 #[actix_web::test]
839 async fn test_extract_headers_with_empty_value() {
840 use actix_web::test::TestRequest;
841
842 let req = TestRequest::default()
843 .insert_header(("X-Empty", ""))
844 .insert_header(("X-Normal", "normal-value"))
845 .to_http_request();
846
847 let headers = extract_headers(&req);
848
849 assert_eq!(headers.get("x-empty"), Some(&vec!["".to_string()]));
850 assert_eq!(
851 headers.get("x-normal"),
852 Some(&vec!["normal-value".to_string()])
853 );
854 }
855
856 #[actix_web::test]
857 async fn test_build_plugin_call_request_with_empty_route() {
858 use actix_web::test::TestRequest;
859
860 let http_req = TestRequest::default().to_http_request();
861 let body = serde_json::to_vec(&serde_json::json!({"user": "alice"})).unwrap();
862 let req = build_plugin_call_request_from_post_body("", &http_req, &body).unwrap();
863
864 assert_eq!(req.route, Some("".to_string()));
865 assert_eq!(req.params, serde_json::json!({"user": "alice"}));
866 }
867
868 #[actix_web::test]
869 async fn test_build_plugin_call_request_with_root_route() {
870 use actix_web::test::TestRequest;
871
872 let http_req = TestRequest::default().to_http_request();
873 let body = serde_json::to_vec(&serde_json::json!({"user": "alice"})).unwrap();
874 let req = build_plugin_call_request_from_post_body("/", &http_req, &body).unwrap();
875
876 assert_eq!(req.route, Some("/".to_string()));
877 }
878
879 #[actix_web::test]
880 async fn test_extract_query_params_with_unicode() {
881 use actix_web::test::TestRequest;
882
883 let req = TestRequest::default()
884 .uri("/test?name=%E4%B8%AD%E6%96%87&value=test")
885 .to_http_request();
886
887 let query_params = extract_query_params(&req);
888
889 assert_eq!(query_params.get("name"), Some(&vec!["䏿–‡".to_string()]));
891 assert_eq!(query_params.get("value"), Some(&vec!["test".to_string()]));
892 }
893
894 #[actix_web::test]
900 async fn test_headers_actually_extracted_and_passed() {
901 let captured = web::Data::new(CapturedRequest::default());
902 let app = test::init_service(
903 App::new()
904 .app_data(captured.clone())
905 .service(
906 web::resource("/plugins/{plugin_id}/call{route:.*}")
907 .route(web::post().to(capturing_plugin_call_handler)),
908 )
909 .configure(init),
910 )
911 .await;
912
913 let req = test::TestRequest::post()
914 .uri("/plugins/test-plugin/call/verify")
915 .insert_header(("Content-Type", "application/json"))
916 .insert_header(("X-Custom-Header", "custom-value"))
917 .insert_header(("Authorization", "Bearer token123"))
918 .insert_header(("X-Request-Id", "req-12345"))
919 .set_json(serde_json::json!({"test": "data"}))
920 .to_request();
921
922 let resp = test::call_service(&app, req).await;
923 assert!(resp.status().is_success());
924
925 let captured_req = captured.get().expect("Request should have been captured");
927 let headers = captured_req.headers.expect("Headers should be present");
928
929 assert!(
930 headers.contains_key("x-custom-header"),
931 "X-Custom-Header should be extracted (lowercased)"
932 );
933 assert!(
934 headers.contains_key("authorization"),
935 "Authorization header should be extracted"
936 );
937 assert_eq!(
938 headers.get("x-custom-header").unwrap()[0],
939 "custom-value",
940 "Header value should match"
941 );
942 assert_eq!(
943 headers.get("authorization").unwrap()[0],
944 "Bearer token123",
945 "Authorization header value should match"
946 );
947 assert_eq!(
948 headers.get("x-request-id").unwrap()[0],
949 "req-12345",
950 "X-Request-Id header value should match"
951 );
952 }
953
954 #[actix_web::test]
956 async fn test_route_field_correctly_set() {
957 let captured = web::Data::new(CapturedRequest::default());
958 let app = test::init_service(
959 App::new()
960 .app_data(captured.clone())
961 .service(
962 web::resource("/plugins/{plugin_id}/call{route:.*}")
963 .route(web::post().to(capturing_plugin_call_handler)),
964 )
965 .configure(init),
966 )
967 .await;
968
969 let test_cases = vec![
970 ("/plugins/test/call", ""),
971 ("/plugins/test/call/verify", "/verify"),
972 ("/plugins/test/call/api/v1/verify", "/api/v1/verify"),
973 (
974 "/plugins/test/call/settle/transaction",
975 "/settle/transaction",
976 ),
977 ];
978
979 for (uri, expected_route) in test_cases {
980 captured.clear();
981 let req = test::TestRequest::post()
982 .uri(uri)
983 .insert_header(("Content-Type", "application/json"))
984 .set_json(serde_json::json!({}))
985 .to_request();
986
987 test::call_service(&app, req).await;
988
989 let captured_req = captured.get().expect("Request should have been captured");
990 assert_eq!(
991 captured_req.route,
992 Some(expected_route.to_string()),
993 "Route should be '{}' for URI '{}'",
994 expected_route,
995 uri
996 );
997 }
998 }
999
1000 #[actix_web::test]
1002 async fn test_route_from_query_parameter() {
1003 let captured = web::Data::new(CapturedRequest::default());
1004 let app = test::init_service(
1005 App::new()
1006 .app_data(captured.clone())
1007 .service(
1008 web::resource("/plugins/{plugin_id}/call{route:.*}")
1009 .route(web::post().to(capturing_plugin_call_handler)),
1010 )
1011 .configure(init),
1012 )
1013 .await;
1014
1015 let req = test::TestRequest::post()
1017 .uri("/plugins/test/call?route=/verify")
1018 .insert_header(("Content-Type", "application/json"))
1019 .set_json(serde_json::json!({}))
1020 .to_request();
1021
1022 test::call_service(&app, req).await;
1023
1024 let captured_req = captured.get().expect("Request should have been captured");
1025 assert_eq!(
1026 captured_req.route,
1027 Some("/verify".to_string()),
1028 "Route should be extracted from query parameter"
1029 );
1030 }
1031
1032 #[actix_web::test]
1034 async fn test_path_route_takes_precedence_over_query() {
1035 let captured = web::Data::new(CapturedRequest::default());
1036 let app = test::init_service(
1037 App::new()
1038 .app_data(captured.clone())
1039 .service(
1040 web::resource("/plugins/{plugin_id}/call{route:.*}")
1041 .route(web::post().to(capturing_plugin_call_handler)),
1042 )
1043 .configure(init),
1044 )
1045 .await;
1046
1047 let req = test::TestRequest::post()
1049 .uri("/plugins/test/call/settle?route=/verify")
1050 .insert_header(("Content-Type", "application/json"))
1051 .set_json(serde_json::json!({}))
1052 .to_request();
1053
1054 test::call_service(&app, req).await;
1055
1056 let captured_req = captured.get().expect("Request should have been captured");
1057 assert_eq!(
1058 captured_req.route,
1059 Some("/settle".to_string()),
1060 "Path route should take precedence over query parameter"
1061 );
1062 }
1063
1064 #[actix_web::test]
1066 async fn test_query_params_extracted_and_passed() {
1067 let captured = web::Data::new(CapturedRequest::default());
1068 let app = test::init_service(
1069 App::new()
1070 .app_data(captured.clone())
1071 .service(
1072 web::resource("/plugins/{plugin_id}/call{route:.*}")
1073 .route(web::post().to(capturing_plugin_call_handler)),
1074 )
1075 .configure(init),
1076 )
1077 .await;
1078
1079 let req = test::TestRequest::post()
1080 .uri("/plugins/test/call?token=abc123&action=verify&tag=a&tag=b")
1081 .insert_header(("Content-Type", "application/json"))
1082 .set_json(serde_json::json!({}))
1083 .to_request();
1084
1085 test::call_service(&app, req).await;
1086
1087 let captured_req = captured.get().expect("Request should have been captured");
1088 let query = captured_req.query.expect("Query params should be present");
1089
1090 assert_eq!(
1091 query.get("token"),
1092 Some(&vec!["abc123".to_string()]),
1093 "Token query param should be extracted"
1094 );
1095 assert_eq!(
1096 query.get("action"),
1097 Some(&vec!["verify".to_string()]),
1098 "Action query param should be extracted"
1099 );
1100 assert_eq!(
1101 query.get("tag"),
1102 Some(&vec!["a".to_string(), "b".to_string()]),
1103 "Multiple tag query params should be extracted as vector"
1104 );
1105 }
1106
1107 #[actix_web::test]
1109 async fn test_method_field_set_correctly_for_post() {
1110 let captured = web::Data::new(CapturedRequest::default());
1111 let app = test::init_service(
1112 App::new()
1113 .app_data(captured.clone())
1114 .service(
1115 web::resource("/plugins/{plugin_id}/call{route:.*}")
1116 .route(web::post().to(capturing_plugin_call_handler)),
1117 )
1118 .configure(init),
1119 )
1120 .await;
1121
1122 let req = test::TestRequest::post()
1123 .uri("/plugins/test/call")
1124 .insert_header(("Content-Type", "application/json"))
1125 .set_json(serde_json::json!({}))
1126 .to_request();
1127
1128 test::call_service(&app, req).await;
1129
1130 let captured_req = captured.get().expect("Request should have been captured");
1131 assert_eq!(
1132 captured_req.method,
1133 Some("POST".to_string()),
1134 "Method should be set to POST for POST requests"
1135 );
1136 }
1137
1138 #[actix_web::test]
1140 async fn test_invalid_json_returns_proper_error() {
1141 use actix_web::test::TestRequest;
1142
1143 let http_req = TestRequest::default().to_http_request();
1144
1145 let result =
1147 build_plugin_call_request_from_post_body("/test", &http_req, b"{invalid json here}");
1148 assert!(result.is_err(), "Invalid JSON should return error");
1149 let err_response = result.unwrap_err();
1150 assert_eq!(
1151 err_response.status(),
1152 actix_web::http::StatusCode::BAD_REQUEST,
1153 "Invalid JSON should return 400 Bad Request"
1154 );
1155 let body_bytes = actix_web::body::to_bytes(err_response.into_body())
1156 .await
1157 .unwrap();
1158 let body_str = std::str::from_utf8(&body_bytes).unwrap();
1159 assert!(
1160 body_str.contains("Invalid JSON"),
1161 "Error message should contain 'Invalid JSON', got: {}",
1162 body_str
1163 );
1164
1165 let result =
1167 build_plugin_call_request_from_post_body("/test", &http_req, b"{'key': 'value'}");
1168 assert!(
1169 result.is_err(),
1170 "Invalid JSON with single quotes should return error"
1171 );
1172
1173 let result = build_plugin_call_request_from_post_body("/test", &http_req, b"{key: value}");
1175 assert!(
1176 result.is_err(),
1177 "Invalid JSON with unquoted keys should return error"
1178 );
1179 }
1180
1181 #[actix_web::test]
1183 async fn test_query_params_with_edge_cases() {
1184 use actix_web::test::TestRequest;
1185
1186 let req = TestRequest::default()
1188 .uri("/test?flag&key=value")
1189 .to_http_request();
1190 let params = extract_query_params(&req);
1191
1192 assert_eq!(
1193 params.get("flag"),
1194 Some(&vec!["".to_string()]),
1195 "Flag parameter without value should have empty string value"
1196 );
1197 assert_eq!(
1198 params.get("key"),
1199 Some(&vec!["value".to_string()]),
1200 "Key with value should be extracted correctly"
1201 );
1202
1203 let req = TestRequest::default()
1205 .uri("/test?empty=&flag")
1206 .to_http_request();
1207 let params = extract_query_params(&req);
1208
1209 assert_eq!(
1210 params.get("empty"),
1211 Some(&vec!["".to_string()]),
1212 "Empty value should be preserved"
1213 );
1214 assert_eq!(
1215 params.get("flag"),
1216 Some(&vec!["".to_string()]),
1217 "Flag without value should also have empty string"
1218 );
1219 }
1220
1221 #[actix_web::test]
1223 async fn test_extract_headers_preserves_original_case_in_values() {
1224 use actix_web::test::TestRequest;
1225
1226 let req = TestRequest::default()
1227 .insert_header(("X-Mixed-Case", "Value-With-CAPS"))
1228 .insert_header(("X-Lower", "lowercase-value"))
1229 .insert_header(("X-Upper", "UPPERCASE-VALUE"))
1230 .to_http_request();
1231
1232 let headers = extract_headers(&req);
1233
1234 let mixed_case_values = headers.get("x-mixed-case").unwrap();
1236 assert_eq!(
1237 mixed_case_values[0], "Value-With-CAPS",
1238 "Header values should preserve original case"
1239 );
1240
1241 let lower_values = headers.get("x-lower").unwrap();
1242 assert_eq!(lower_values[0], "lowercase-value");
1243
1244 let upper_values = headers.get("x-upper").unwrap();
1245 assert_eq!(upper_values[0], "UPPERCASE-VALUE");
1246 }
1247
1248 #[actix_web::test]
1250 async fn test_very_long_query_string() {
1251 use actix_web::test::TestRequest;
1252
1253 let long_value = "a".repeat(1000);
1255 let uri = format!("/test?data={}", long_value);
1256
1257 let req = TestRequest::default().uri(&uri).to_http_request();
1258
1259 let params = extract_query_params(&req);
1260 assert_eq!(
1261 params.get("data").unwrap()[0].len(),
1262 1000,
1263 "Long query parameter value should be handled correctly"
1264 );
1265 assert_eq!(
1266 params.get("data").unwrap()[0],
1267 long_value,
1268 "Long query parameter value should match"
1269 );
1270 }
1271
1272 #[actix_web::test]
1274 async fn test_params_field_with_complex_nested_structure() {
1275 use actix_web::test::TestRequest;
1276
1277 let http_req = TestRequest::default().to_http_request();
1278 let complex_json = serde_json::json!({
1279 "params": {
1280 "user": {
1281 "name": "alice",
1282 "metadata": {
1283 "tags": ["a", "b", "c"],
1284 "score": 100
1285 }
1286 },
1287 "action": "verify"
1288 }
1289 });
1290
1291 let body = serde_json::to_vec(&complex_json).unwrap();
1292 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
1293
1294 assert_eq!(
1296 req.params.get("user").unwrap().get("name").unwrap(),
1297 "alice",
1298 "Nested user name should be preserved"
1299 );
1300 assert!(
1301 req.params.get("user").unwrap().get("metadata").is_some(),
1302 "Nested metadata should be preserved"
1303 );
1304 let metadata = req.params.get("user").unwrap().get("metadata").unwrap();
1305 assert_eq!(
1306 metadata.get("score").unwrap(),
1307 100,
1308 "Nested score should be preserved"
1309 );
1310 assert!(
1311 metadata.get("tags").is_some(),
1312 "Nested tags array should be preserved"
1313 );
1314 }
1315
1316 #[actix_web::test]
1321 async fn test_get_restriction_logic_through_route_handler() {
1322 use crate::utils::mocks::mockutils::create_mock_app_state;
1323 use std::time::Duration;
1324
1325 let plugin_disabled = PluginModel {
1327 id: "plugin-no-get".to_string(),
1328 path: "test-path".to_string(),
1329 timeout: Duration::from_secs(60),
1330 emit_logs: false,
1331 emit_traces: false,
1332 raw_response: false,
1333 allow_get_invocation: false,
1334 config: None,
1335 forward_logs: false,
1336 };
1337
1338 let plugin_enabled = PluginModel {
1340 id: "plugin-with-get".to_string(),
1341 path: "test-path".to_string(),
1342 timeout: Duration::from_secs(60),
1343 emit_logs: false,
1344 emit_traces: false,
1345 raw_response: false,
1346 allow_get_invocation: true,
1347 config: None,
1348 forward_logs: false,
1349 };
1350
1351 let app_state = create_mock_app_state(
1352 None,
1353 None,
1354 None,
1355 None,
1356 Some(vec![plugin_disabled.clone(), plugin_enabled.clone()]),
1357 None,
1358 )
1359 .await;
1360
1361 let plugin_repo = app_state.plugin_repository.clone();
1364 let found_plugin = plugin_repo
1365 .get_by_id("non-existent")
1366 .await
1367 .expect("Repository call should succeed");
1368 assert!(
1369 found_plugin.is_none(),
1370 "Non-existent plugin should return None (would trigger 404 in route handler)"
1371 );
1372
1373 let found_plugin = plugin_repo
1375 .get_by_id("plugin-no-get")
1376 .await
1377 .expect("Repository call should succeed");
1378 assert!(found_plugin.is_some(), "Plugin should exist in repository");
1379 let plugin = found_plugin.unwrap();
1380 assert!(
1381 !plugin.allow_get_invocation,
1382 "Plugin should have allow_get_invocation=false (would trigger 405 in route handler)"
1383 );
1384
1385 let found_plugin = plugin_repo
1387 .get_by_id("plugin-with-get")
1388 .await
1389 .expect("Repository call should succeed");
1390 assert!(found_plugin.is_some(), "Plugin should exist in repository");
1391 assert!(
1392 found_plugin.unwrap().allow_get_invocation,
1393 "Plugin should have allow_get_invocation=true (would proceed in route handler)"
1394 );
1395 }
1396
1397 #[actix_web::test]
1399 async fn test_error_responses_contain_appropriate_messages() {
1400 use crate::models::ApiResponse;
1401 use actix_web::test::TestRequest;
1402
1403 let http_req = TestRequest::default().to_http_request();
1404
1405 let result = build_plugin_call_request_from_post_body("/test", &http_req, b"{invalid}");
1407
1408 assert!(result.is_err());
1409 let err_response = result.unwrap_err();
1410 assert_eq!(
1411 err_response.status(),
1412 actix_web::http::StatusCode::BAD_REQUEST,
1413 "Should return 400 Bad Request"
1414 );
1415
1416 let body_bytes = actix_web::body::to_bytes(err_response.into_body())
1418 .await
1419 .unwrap();
1420 let api_response: ApiResponse<()> =
1421 serde_json::from_slice(&body_bytes).expect("Response should be valid JSON");
1422 assert!(
1423 !api_response.success,
1424 "Error response should have success=false"
1425 );
1426 assert!(
1427 api_response.error.is_some(),
1428 "Error response should contain error message"
1429 );
1430 assert!(
1431 api_response.error.unwrap().contains("Invalid JSON"),
1432 "Error message should mention 'Invalid JSON'"
1433 );
1434 }
1435
1436 #[actix_web::test]
1438 async fn test_query_params_with_special_characters_and_encoding() {
1439 use actix_web::test::TestRequest;
1440
1441 let req = TestRequest::default()
1443 .uri("/test?key=value%20with%20spaces&symbol=%26%3D%3F&unicode=%E4%B8%AD%E6%96%87")
1444 .to_http_request();
1445
1446 let params = extract_query_params(&req);
1447
1448 assert_eq!(
1449 params.get("key"),
1450 Some(&vec!["value with spaces".to_string()]),
1451 "URL-encoded spaces should be decoded"
1452 );
1453 assert_eq!(
1454 params.get("symbol"),
1455 Some(&vec!["&=?".to_string()]),
1456 "URL-encoded special characters should be decoded"
1457 );
1458 assert_eq!(
1459 params.get("unicode"),
1460 Some(&vec!["䏿–‡".to_string()]),
1461 "URL-encoded Unicode should be decoded"
1462 );
1463 }
1464
1465 #[actix_web::test]
1467 async fn test_params_without_wrapper_correctly_wrapped() {
1468 use actix_web::test::TestRequest;
1469
1470 let http_req = TestRequest::default().to_http_request();
1471
1472 let body_without_params = serde_json::json!({
1474 "user": "alice",
1475 "action": "transfer",
1476 "amount": 100
1477 });
1478
1479 let body = serde_json::to_vec(&body_without_params).unwrap();
1480 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
1481
1482 assert_eq!(
1484 req.params.get("user").unwrap(),
1485 "alice",
1486 "User field should be in params"
1487 );
1488 assert_eq!(
1489 req.params.get("action").unwrap(),
1490 "transfer",
1491 "Action field should be in params"
1492 );
1493 assert_eq!(
1494 req.params.get("amount").unwrap(),
1495 100,
1496 "Amount field should be in params"
1497 );
1498 }
1499
1500 #[actix_web::test]
1502 async fn test_method_field_set_correctly_for_get() {
1503 let captured = web::Data::new(CapturedRequest::default());
1504 let app = test::init_service(
1505 App::new()
1506 .app_data(captured.clone())
1507 .service(
1508 web::resource("/plugins/{plugin_id}/call{route:.*}")
1509 .route(web::get().to(capturing_plugin_call_get_handler)),
1510 )
1511 .configure(init),
1512 )
1513 .await;
1514
1515 let req = test::TestRequest::get()
1516 .uri("/plugins/test/call/verify?token=abc123")
1517 .to_request();
1518
1519 test::call_service(&app, req).await;
1520
1521 let captured_req = captured.get().expect("Request should have been captured");
1522 assert_eq!(
1523 captured_req.method,
1524 Some("GET".to_string()),
1525 "Method should be set to GET for GET requests"
1526 );
1527 assert_eq!(
1528 captured_req.params,
1529 serde_json::json!({}),
1530 "GET requests should have empty params object"
1531 );
1532 }
1533
1534 #[actix_web::test]
1537 async fn test_invalid_json_through_route_handler() {
1538 let captured = web::Data::new(CapturedRequest::default());
1539 let app = test::init_service(
1540 App::new()
1541 .app_data(captured.clone())
1542 .service(
1543 web::resource("/plugins/{plugin_id}/call{route:.*}")
1544 .route(web::post().to(capturing_plugin_call_handler)),
1545 )
1546 .configure(init),
1547 )
1548 .await;
1549
1550 let req = test::TestRequest::post()
1551 .uri("/plugins/test-plugin/call")
1552 .insert_header(("Content-Type", "application/json"))
1553 .set_payload("{invalid json syntax}")
1554 .to_request();
1555
1556 let resp = test::call_service(&app, req).await;
1557
1558 assert_eq!(
1559 resp.status(),
1560 actix_web::http::StatusCode::BAD_REQUEST,
1561 "Invalid JSON should return 400 Bad Request through route handler"
1562 );
1563
1564 let body = test::read_body(resp).await;
1566 let body_str = std::str::from_utf8(&body).unwrap();
1567 assert!(
1568 body_str.contains("Invalid JSON"),
1569 "Error message should contain 'Invalid JSON', got: {}",
1570 body_str
1571 );
1572
1573 assert!(
1575 captured.get().is_none(),
1576 "Invalid JSON request should not be captured"
1577 );
1578 }
1579
1580 #[actix_web::test]
1583 async fn test_plugin_call_request_with_various_param_types() {
1584 use actix_web::test::TestRequest;
1585
1586 let http_req = TestRequest::default().to_http_request();
1587
1588 let body_with_string_params = serde_json::json!({
1590 "params": "this is a string, not an object"
1591 });
1592 let body = serde_json::to_vec(&body_with_string_params).unwrap();
1593 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1594 assert!(result.is_ok(), "Params as string should be valid");
1595 let req = result.unwrap();
1596 assert_eq!(
1597 req.params,
1598 serde_json::json!("this is a string, not an object"),
1599 "String params should be preserved"
1600 );
1601
1602 let body_with_array_params = serde_json::json!({
1604 "params": [1, 2, 3, "four"]
1605 });
1606 let body = serde_json::to_vec(&body_with_array_params).unwrap();
1607 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1608 assert!(result.is_ok(), "Params as array should be valid");
1609 let req = result.unwrap();
1610 assert_eq!(
1611 req.params,
1612 serde_json::json!([1, 2, 3, "four"]),
1613 "Array params should be preserved"
1614 );
1615
1616 let body_with_number_params = serde_json::json!({
1618 "params": 42
1619 });
1620 let body = serde_json::to_vec(&body_with_number_params).unwrap();
1621 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1622 assert!(result.is_ok(), "Params as number should be valid");
1623 let req = result.unwrap();
1624 assert_eq!(
1625 req.params,
1626 serde_json::json!(42),
1627 "Number params should be preserved"
1628 );
1629
1630 let body_with_bool_params = serde_json::json!({
1632 "params": true
1633 });
1634 let body = serde_json::to_vec(&body_with_bool_params).unwrap();
1635 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1636 assert!(result.is_ok(), "Params as boolean should be valid");
1637 let req = result.unwrap();
1638 assert_eq!(
1639 req.params,
1640 serde_json::json!(true),
1641 "Boolean params should be preserved"
1642 );
1643 }
1644
1645 #[actix_web::test]
1647 async fn test_request_without_content_type_header() {
1648 let captured = web::Data::new(CapturedRequest::default());
1649 let app = test::init_service(
1650 App::new()
1651 .app_data(captured.clone())
1652 .service(
1653 web::resource("/plugins/{plugin_id}/call{route:.*}")
1654 .route(web::post().to(capturing_plugin_call_handler)),
1655 )
1656 .configure(init),
1657 )
1658 .await;
1659
1660 let req = test::TestRequest::post()
1662 .uri("/plugins/test-plugin/call")
1663 .set_payload("{\"test\": \"data\"}")
1664 .to_request();
1665
1666 let resp = test::call_service(&app, req).await;
1667
1668 assert!(
1671 resp.status().is_success(),
1672 "Request without Content-Type should be handled gracefully, got status: {}",
1673 resp.status()
1674 );
1675
1676 let captured_req = captured.get();
1678 assert!(
1679 captured_req.is_some(),
1680 "Request without Content-Type should still be processed"
1681 );
1682 }
1683
1684 #[actix_web::test]
1686 async fn test_very_large_request_body() {
1687 use actix_web::test::TestRequest;
1688
1689 let http_req = TestRequest::default().to_http_request();
1690
1691 let large_data = "x".repeat(10000);
1693 let large_json = serde_json::json!({
1694 "params": {
1695 "large_data": large_data,
1696 "count": 10000
1697 }
1698 });
1699
1700 let body = serde_json::to_vec(&large_json).unwrap();
1701 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1702
1703 assert!(result.is_ok(), "Large request body should be handled");
1704 let req = result.unwrap();
1705 assert_eq!(
1706 req.params
1707 .get("large_data")
1708 .unwrap()
1709 .as_str()
1710 .unwrap()
1711 .len(),
1712 10000,
1713 "Large data should be preserved correctly"
1714 );
1715 }
1716
1717 #[actix_web::test]
1719 async fn test_nested_routes_with_special_characters() {
1720 use actix_web::test::TestRequest;
1721
1722 let http_req = TestRequest::default().to_http_request();
1723 let body = serde_json::to_vec(&serde_json::json!({"test": "data"})).unwrap();
1724
1725 let test_cases = vec![
1726 ("/api/v1/verify", "/api/v1/verify"),
1727 ("/settle/transaction", "/settle/transaction"),
1728 ("/path-with-dashes", "/path-with-dashes"),
1729 ("/path_with_underscores", "/path_with_underscores"),
1730 ("/path.with.dots", "/path.with.dots"),
1731 ("/path%20with%20spaces", "/path%20with%20spaces"), ];
1733
1734 for (route, expected_route) in test_cases {
1735 let req = build_plugin_call_request_from_post_body(route, &http_req, &body).unwrap();
1736 assert_eq!(
1737 req.route,
1738 Some(expected_route.to_string()),
1739 "Route '{}' should be preserved as '{}'",
1740 route,
1741 expected_route
1742 );
1743 }
1744 }
1745
1746 #[actix_web::test]
1747 async fn test_plugin_call_route_handler_post() {
1748 use crate::utils::mocks::mockutils::create_mock_app_state;
1749 use std::time::Duration;
1750
1751 let plugin = PluginModel {
1752 id: "test-plugin".to_string(),
1753 path: "test-path".to_string(),
1754 timeout: Duration::from_secs(60),
1755 emit_logs: false,
1756 emit_traces: false,
1757 raw_response: false,
1758 allow_get_invocation: false,
1759 config: None,
1760 forward_logs: false,
1761 };
1762
1763 let app_state =
1764 create_mock_app_state(None, None, None, None, Some(vec![plugin]), None).await;
1765
1766 let app = test::init_service(
1767 App::new()
1768 .app_data(web::Data::new(web::ThinData(app_state)))
1769 .configure(init),
1770 )
1771 .await;
1772
1773 let req = test::TestRequest::post()
1774 .uri("/plugins/test-plugin/call")
1775 .insert_header(("Content-Type", "application/json"))
1776 .set_json(serde_json::json!({"params": {"test": "data"}}))
1777 .to_request();
1778
1779 let resp = test::call_service(&app, req).await;
1780
1781 assert!(
1784 resp.status().is_server_error() || resp.status().is_client_error(),
1785 "Route handler should be executed, got status: {}",
1786 resp.status()
1787 );
1788 }
1789
1790 #[actix_web::test]
1793 async fn test_plugin_call_get_route_handler_allowed() {
1794 use crate::utils::mocks::mockutils::create_mock_app_state;
1795 use std::time::Duration;
1796
1797 let plugin = PluginModel {
1798 id: "test-plugin-with-get".to_string(),
1799 path: "test-path".to_string(),
1800 timeout: Duration::from_secs(60),
1801 emit_logs: false,
1802 emit_traces: false,
1803 raw_response: false,
1804 allow_get_invocation: true, config: None,
1806 forward_logs: false,
1807 };
1808
1809 let app_state =
1810 create_mock_app_state(None, None, None, None, Some(vec![plugin]), None).await;
1811
1812 let app = test::init_service(
1813 App::new()
1814 .app_data(web::Data::new(web::ThinData(app_state)))
1815 .configure(init),
1816 )
1817 .await;
1818
1819 let req = test::TestRequest::get()
1820 .uri("/plugins/test-plugin-with-get/call?token=abc123")
1821 .to_request();
1822
1823 let resp = test::call_service(&app, req).await;
1824
1825 assert!(
1828 resp.status().is_server_error() || resp.status().is_client_error(),
1829 "Route handler should be executed, got status: {}",
1830 resp.status()
1831 );
1832 }
1833
1834 async fn mock_get_plugin(path: web::Path<String>) -> impl Responder {
1840 let plugin_id = path.into_inner();
1841
1842 if plugin_id == "not-found" {
1843 return HttpResponse::NotFound().json(ApiResponse::<()>::error(format!(
1844 "Plugin with id {} not found",
1845 plugin_id
1846 )));
1847 }
1848
1849 let plugin = PluginModel {
1850 id: plugin_id,
1851 path: "test-path.ts".to_string(),
1852 timeout: Duration::from_secs(30),
1853 emit_logs: true,
1854 emit_traces: false,
1855 raw_response: false,
1856 allow_get_invocation: true,
1857 config: None,
1858 forward_logs: true,
1859 };
1860
1861 HttpResponse::Ok().json(ApiResponse::success(plugin))
1862 }
1863
1864 #[actix_web::test]
1865 async fn test_get_plugin_route_success() {
1866 let app = test::init_service(
1867 App::new()
1868 .service(
1869 web::resource("/plugins/{plugin_id}").route(web::get().to(mock_get_plugin)),
1870 )
1871 .configure(init),
1872 )
1873 .await;
1874
1875 let req = test::TestRequest::get()
1876 .uri("/plugins/my-plugin")
1877 .to_request();
1878
1879 let resp = test::call_service(&app, req).await;
1880 assert!(resp.status().is_success());
1881
1882 let body = test::read_body(resp).await;
1883 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
1884 assert!(response.success);
1885 assert_eq!(response.data.as_ref().unwrap().id, "my-plugin");
1886 assert!(response.data.as_ref().unwrap().emit_logs);
1887 assert!(response.data.as_ref().unwrap().forward_logs);
1888 }
1889
1890 #[actix_web::test]
1891 async fn test_get_plugin_route_not_found() {
1892 let app = test::init_service(
1893 App::new()
1894 .service(
1895 web::resource("/plugins/{plugin_id}").route(web::get().to(mock_get_plugin)),
1896 )
1897 .configure(init),
1898 )
1899 .await;
1900
1901 let req = test::TestRequest::get()
1902 .uri("/plugins/not-found")
1903 .to_request();
1904
1905 let resp = test::call_service(&app, req).await;
1906 assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND);
1907 }
1908
1909 async fn mock_update_plugin(
1915 path: web::Path<String>,
1916 body: web::Json<UpdatePluginRequest>,
1917 ) -> impl Responder {
1918 let plugin_id = path.into_inner();
1919 let update = body.into_inner();
1920
1921 let updated_plugin = PluginModel {
1923 id: plugin_id,
1924 path: "test-path".to_string(),
1925 timeout: Duration::from_secs(update.timeout.unwrap_or(30)),
1926 emit_logs: update.emit_logs.unwrap_or(false),
1927 emit_traces: update.emit_traces.unwrap_or(false),
1928 raw_response: update.raw_response.unwrap_or(false),
1929 allow_get_invocation: update.allow_get_invocation.unwrap_or(false),
1930 config: update.config.flatten(),
1931 forward_logs: update.forward_logs.unwrap_or(false),
1932 };
1933
1934 HttpResponse::Ok().json(ApiResponse::success(updated_plugin))
1935 }
1936
1937 #[actix_web::test]
1938 async fn test_update_plugin_route_success() {
1939 let app = test::init_service(
1940 App::new()
1941 .service(
1942 web::resource("/plugins/{plugin_id}")
1943 .route(web::patch().to(mock_update_plugin)),
1944 )
1945 .configure(init),
1946 )
1947 .await;
1948
1949 let req = test::TestRequest::patch()
1950 .uri("/plugins/test-plugin")
1951 .insert_header(("Content-Type", "application/json"))
1952 .set_json(serde_json::json!({
1953 "timeout": 60,
1954 "emit_logs": true,
1955 "forward_logs": true
1956 }))
1957 .to_request();
1958
1959 let resp = test::call_service(&app, req).await;
1960 assert!(resp.status().is_success());
1961
1962 let body = test::read_body(resp).await;
1963 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
1964 assert!(response.success);
1965 assert_eq!(response.data.as_ref().unwrap().id, "test-plugin");
1966 assert_eq!(
1967 response.data.as_ref().unwrap().timeout,
1968 Duration::from_secs(60)
1969 );
1970 assert!(response.data.as_ref().unwrap().emit_logs);
1971 assert!(response.data.as_ref().unwrap().forward_logs);
1972 }
1973
1974 #[actix_web::test]
1975 async fn test_update_plugin_route_with_config() {
1976 let app = test::init_service(
1977 App::new()
1978 .service(
1979 web::resource("/plugins/{plugin_id}")
1980 .route(web::patch().to(mock_update_plugin)),
1981 )
1982 .configure(init),
1983 )
1984 .await;
1985
1986 let req = test::TestRequest::patch()
1987 .uri("/plugins/my-plugin")
1988 .insert_header(("Content-Type", "application/json"))
1989 .set_json(serde_json::json!({
1990 "config": {
1991 "feature_enabled": true,
1992 "api_key": "secret123"
1993 }
1994 }))
1995 .to_request();
1996
1997 let resp = test::call_service(&app, req).await;
1998 assert!(resp.status().is_success());
1999
2000 let body = test::read_body(resp).await;
2001 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
2002 assert!(response.success);
2003 assert!(response.data.as_ref().unwrap().config.is_some());
2004 let config = response.data.as_ref().unwrap().config.as_ref().unwrap();
2005 assert_eq!(
2006 config.get("feature_enabled"),
2007 Some(&serde_json::json!(true))
2008 );
2009 assert_eq!(config.get("api_key"), Some(&serde_json::json!("secret123")));
2010 }
2011
2012 #[actix_web::test]
2013 async fn test_update_plugin_route_clear_config() {
2014 let app = test::init_service(
2015 App::new()
2016 .service(
2017 web::resource("/plugins/{plugin_id}")
2018 .route(web::patch().to(mock_update_plugin)),
2019 )
2020 .configure(init),
2021 )
2022 .await;
2023
2024 let req = test::TestRequest::patch()
2025 .uri("/plugins/test-plugin")
2026 .insert_header(("Content-Type", "application/json"))
2027 .set_json(serde_json::json!({
2028 "config": null
2029 }))
2030 .to_request();
2031
2032 let resp = test::call_service(&app, req).await;
2033 assert!(resp.status().is_success());
2034
2035 let body = test::read_body(resp).await;
2036 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
2037 assert!(response.success);
2038 assert!(response.data.as_ref().unwrap().config.is_none());
2039 }
2040
2041 #[actix_web::test]
2042 async fn test_update_plugin_route_empty_body() {
2043 let app = test::init_service(
2044 App::new()
2045 .service(
2046 web::resource("/plugins/{plugin_id}")
2047 .route(web::patch().to(mock_update_plugin)),
2048 )
2049 .configure(init),
2050 )
2051 .await;
2052
2053 let req = test::TestRequest::patch()
2055 .uri("/plugins/test-plugin")
2056 .insert_header(("Content-Type", "application/json"))
2057 .set_json(serde_json::json!({}))
2058 .to_request();
2059
2060 let resp = test::call_service(&app, req).await;
2061 assert!(resp.status().is_success());
2062 }
2063
2064 #[actix_web::test]
2065 async fn test_update_plugin_route_all_fields() {
2066 let app = test::init_service(
2067 App::new()
2068 .service(
2069 web::resource("/plugins/{plugin_id}")
2070 .route(web::patch().to(mock_update_plugin)),
2071 )
2072 .configure(init),
2073 )
2074 .await;
2075
2076 let req = test::TestRequest::patch()
2077 .uri("/plugins/full-update-plugin")
2078 .insert_header(("Content-Type", "application/json"))
2079 .set_json(serde_json::json!({
2080 "timeout": 120,
2081 "emit_logs": true,
2082 "emit_traces": true,
2083 "raw_response": true,
2084 "allow_get_invocation": true,
2085 "forward_logs": true,
2086 "config": {
2087 "key": "value"
2088 }
2089 }))
2090 .to_request();
2091
2092 let resp = test::call_service(&app, req).await;
2093 assert!(resp.status().is_success());
2094
2095 let body = test::read_body(resp).await;
2096 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
2097 let plugin = response.data.unwrap();
2098
2099 assert_eq!(plugin.timeout, Duration::from_secs(120));
2100 assert!(plugin.emit_logs);
2101 assert!(plugin.emit_traces);
2102 assert!(plugin.raw_response);
2103 assert!(plugin.allow_get_invocation);
2104 assert!(plugin.forward_logs);
2105 assert!(plugin.config.is_some());
2106 }
2107
2108 #[actix_web::test]
2109 async fn test_update_plugin_route_invalid_json() {
2110 let app = test::init_service(
2111 App::new()
2112 .service(
2113 web::resource("/plugins/{plugin_id}")
2114 .route(web::patch().to(mock_update_plugin)),
2115 )
2116 .configure(init),
2117 )
2118 .await;
2119
2120 let req = test::TestRequest::patch()
2121 .uri("/plugins/test-plugin")
2122 .insert_header(("Content-Type", "application/json"))
2123 .set_payload("{ invalid json }")
2124 .to_request();
2125
2126 let resp = test::call_service(&app, req).await;
2127 assert!(resp.status().is_client_error());
2128 }
2129
2130 #[actix_web::test]
2131 async fn test_update_plugin_route_unknown_field_rejected() {
2132 let app = test::init_service(
2133 App::new()
2134 .service(
2135 web::resource("/plugins/{plugin_id}")
2136 .route(web::patch().to(mock_update_plugin)),
2137 )
2138 .configure(init),
2139 )
2140 .await;
2141
2142 let req = test::TestRequest::patch()
2144 .uri("/plugins/test-plugin")
2145 .insert_header(("Content-Type", "application/json"))
2146 .set_json(serde_json::json!({
2147 "timeout": 60,
2148 "unknown_field": "should_fail"
2149 }))
2150 .to_request();
2151
2152 let resp = test::call_service(&app, req).await;
2153 assert!(resp.status().is_client_error());
2154 }
2155}