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 '{expected_route}' for URI '{uri}'"
994 );
995 }
996 }
997
998 #[actix_web::test]
1000 async fn test_route_from_query_parameter() {
1001 let captured = web::Data::new(CapturedRequest::default());
1002 let app = test::init_service(
1003 App::new()
1004 .app_data(captured.clone())
1005 .service(
1006 web::resource("/plugins/{plugin_id}/call{route:.*}")
1007 .route(web::post().to(capturing_plugin_call_handler)),
1008 )
1009 .configure(init),
1010 )
1011 .await;
1012
1013 let req = test::TestRequest::post()
1015 .uri("/plugins/test/call?route=/verify")
1016 .insert_header(("Content-Type", "application/json"))
1017 .set_json(serde_json::json!({}))
1018 .to_request();
1019
1020 test::call_service(&app, req).await;
1021
1022 let captured_req = captured.get().expect("Request should have been captured");
1023 assert_eq!(
1024 captured_req.route,
1025 Some("/verify".to_string()),
1026 "Route should be extracted from query parameter"
1027 );
1028 }
1029
1030 #[actix_web::test]
1032 async fn test_path_route_takes_precedence_over_query() {
1033 let captured = web::Data::new(CapturedRequest::default());
1034 let app = test::init_service(
1035 App::new()
1036 .app_data(captured.clone())
1037 .service(
1038 web::resource("/plugins/{plugin_id}/call{route:.*}")
1039 .route(web::post().to(capturing_plugin_call_handler)),
1040 )
1041 .configure(init),
1042 )
1043 .await;
1044
1045 let req = test::TestRequest::post()
1047 .uri("/plugins/test/call/settle?route=/verify")
1048 .insert_header(("Content-Type", "application/json"))
1049 .set_json(serde_json::json!({}))
1050 .to_request();
1051
1052 test::call_service(&app, req).await;
1053
1054 let captured_req = captured.get().expect("Request should have been captured");
1055 assert_eq!(
1056 captured_req.route,
1057 Some("/settle".to_string()),
1058 "Path route should take precedence over query parameter"
1059 );
1060 }
1061
1062 #[actix_web::test]
1064 async fn test_query_params_extracted_and_passed() {
1065 let captured = web::Data::new(CapturedRequest::default());
1066 let app = test::init_service(
1067 App::new()
1068 .app_data(captured.clone())
1069 .service(
1070 web::resource("/plugins/{plugin_id}/call{route:.*}")
1071 .route(web::post().to(capturing_plugin_call_handler)),
1072 )
1073 .configure(init),
1074 )
1075 .await;
1076
1077 let req = test::TestRequest::post()
1078 .uri("/plugins/test/call?token=abc123&action=verify&tag=a&tag=b")
1079 .insert_header(("Content-Type", "application/json"))
1080 .set_json(serde_json::json!({}))
1081 .to_request();
1082
1083 test::call_service(&app, req).await;
1084
1085 let captured_req = captured.get().expect("Request should have been captured");
1086 let query = captured_req.query.expect("Query params should be present");
1087
1088 assert_eq!(
1089 query.get("token"),
1090 Some(&vec!["abc123".to_string()]),
1091 "Token query param should be extracted"
1092 );
1093 assert_eq!(
1094 query.get("action"),
1095 Some(&vec!["verify".to_string()]),
1096 "Action query param should be extracted"
1097 );
1098 assert_eq!(
1099 query.get("tag"),
1100 Some(&vec!["a".to_string(), "b".to_string()]),
1101 "Multiple tag query params should be extracted as vector"
1102 );
1103 }
1104
1105 #[actix_web::test]
1107 async fn test_method_field_set_correctly_for_post() {
1108 let captured = web::Data::new(CapturedRequest::default());
1109 let app = test::init_service(
1110 App::new()
1111 .app_data(captured.clone())
1112 .service(
1113 web::resource("/plugins/{plugin_id}/call{route:.*}")
1114 .route(web::post().to(capturing_plugin_call_handler)),
1115 )
1116 .configure(init),
1117 )
1118 .await;
1119
1120 let req = test::TestRequest::post()
1121 .uri("/plugins/test/call")
1122 .insert_header(("Content-Type", "application/json"))
1123 .set_json(serde_json::json!({}))
1124 .to_request();
1125
1126 test::call_service(&app, req).await;
1127
1128 let captured_req = captured.get().expect("Request should have been captured");
1129 assert_eq!(
1130 captured_req.method,
1131 Some("POST".to_string()),
1132 "Method should be set to POST for POST requests"
1133 );
1134 }
1135
1136 #[actix_web::test]
1138 async fn test_invalid_json_returns_proper_error() {
1139 use actix_web::test::TestRequest;
1140
1141 let http_req = TestRequest::default().to_http_request();
1142
1143 let result =
1145 build_plugin_call_request_from_post_body("/test", &http_req, b"{invalid json here}");
1146 assert!(result.is_err(), "Invalid JSON should return error");
1147 let err_response = result.unwrap_err();
1148 assert_eq!(
1149 err_response.status(),
1150 actix_web::http::StatusCode::BAD_REQUEST,
1151 "Invalid JSON should return 400 Bad Request"
1152 );
1153 let body_bytes = actix_web::body::to_bytes(err_response.into_body())
1154 .await
1155 .unwrap();
1156 let body_str = std::str::from_utf8(&body_bytes).unwrap();
1157 assert!(
1158 body_str.contains("Invalid JSON"),
1159 "Error message should contain 'Invalid JSON', got: {body_str}"
1160 );
1161
1162 let result =
1164 build_plugin_call_request_from_post_body("/test", &http_req, b"{'key': 'value'}");
1165 assert!(
1166 result.is_err(),
1167 "Invalid JSON with single quotes should return error"
1168 );
1169
1170 let result = build_plugin_call_request_from_post_body("/test", &http_req, b"{key: value}");
1172 assert!(
1173 result.is_err(),
1174 "Invalid JSON with unquoted keys should return error"
1175 );
1176 }
1177
1178 #[actix_web::test]
1180 async fn test_query_params_with_edge_cases() {
1181 use actix_web::test::TestRequest;
1182
1183 let req = TestRequest::default()
1185 .uri("/test?flag&key=value")
1186 .to_http_request();
1187 let params = extract_query_params(&req);
1188
1189 assert_eq!(
1190 params.get("flag"),
1191 Some(&vec!["".to_string()]),
1192 "Flag parameter without value should have empty string value"
1193 );
1194 assert_eq!(
1195 params.get("key"),
1196 Some(&vec!["value".to_string()]),
1197 "Key with value should be extracted correctly"
1198 );
1199
1200 let req = TestRequest::default()
1202 .uri("/test?empty=&flag")
1203 .to_http_request();
1204 let params = extract_query_params(&req);
1205
1206 assert_eq!(
1207 params.get("empty"),
1208 Some(&vec!["".to_string()]),
1209 "Empty value should be preserved"
1210 );
1211 assert_eq!(
1212 params.get("flag"),
1213 Some(&vec!["".to_string()]),
1214 "Flag without value should also have empty string"
1215 );
1216 }
1217
1218 #[actix_web::test]
1220 async fn test_extract_headers_preserves_original_case_in_values() {
1221 use actix_web::test::TestRequest;
1222
1223 let req = TestRequest::default()
1224 .insert_header(("X-Mixed-Case", "Value-With-CAPS"))
1225 .insert_header(("X-Lower", "lowercase-value"))
1226 .insert_header(("X-Upper", "UPPERCASE-VALUE"))
1227 .to_http_request();
1228
1229 let headers = extract_headers(&req);
1230
1231 let mixed_case_values = headers.get("x-mixed-case").unwrap();
1233 assert_eq!(
1234 mixed_case_values[0], "Value-With-CAPS",
1235 "Header values should preserve original case"
1236 );
1237
1238 let lower_values = headers.get("x-lower").unwrap();
1239 assert_eq!(lower_values[0], "lowercase-value");
1240
1241 let upper_values = headers.get("x-upper").unwrap();
1242 assert_eq!(upper_values[0], "UPPERCASE-VALUE");
1243 }
1244
1245 #[actix_web::test]
1247 async fn test_very_long_query_string() {
1248 use actix_web::test::TestRequest;
1249
1250 let long_value = "a".repeat(1000);
1252 let uri = format!("/test?data={long_value}");
1253
1254 let req = TestRequest::default().uri(&uri).to_http_request();
1255
1256 let params = extract_query_params(&req);
1257 assert_eq!(
1258 params.get("data").unwrap()[0].len(),
1259 1000,
1260 "Long query parameter value should be handled correctly"
1261 );
1262 assert_eq!(
1263 params.get("data").unwrap()[0],
1264 long_value,
1265 "Long query parameter value should match"
1266 );
1267 }
1268
1269 #[actix_web::test]
1271 async fn test_params_field_with_complex_nested_structure() {
1272 use actix_web::test::TestRequest;
1273
1274 let http_req = TestRequest::default().to_http_request();
1275 let complex_json = serde_json::json!({
1276 "params": {
1277 "user": {
1278 "name": "alice",
1279 "metadata": {
1280 "tags": ["a", "b", "c"],
1281 "score": 100
1282 }
1283 },
1284 "action": "verify"
1285 }
1286 });
1287
1288 let body = serde_json::to_vec(&complex_json).unwrap();
1289 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
1290
1291 assert_eq!(
1293 req.params.get("user").unwrap().get("name").unwrap(),
1294 "alice",
1295 "Nested user name should be preserved"
1296 );
1297 assert!(
1298 req.params.get("user").unwrap().get("metadata").is_some(),
1299 "Nested metadata should be preserved"
1300 );
1301 let metadata = req.params.get("user").unwrap().get("metadata").unwrap();
1302 assert_eq!(
1303 metadata.get("score").unwrap(),
1304 100,
1305 "Nested score should be preserved"
1306 );
1307 assert!(
1308 metadata.get("tags").is_some(),
1309 "Nested tags array should be preserved"
1310 );
1311 }
1312
1313 #[actix_web::test]
1318 async fn test_get_restriction_logic_through_route_handler() {
1319 use crate::utils::mocks::mockutils::create_mock_app_state;
1320 use std::time::Duration;
1321
1322 let plugin_disabled = PluginModel {
1324 id: "plugin-no-get".to_string(),
1325 path: "test-path".to_string(),
1326 timeout: Duration::from_secs(60),
1327 emit_logs: false,
1328 emit_traces: false,
1329 raw_response: false,
1330 allow_get_invocation: false,
1331 config: None,
1332 forward_logs: false,
1333 };
1334
1335 let plugin_enabled = PluginModel {
1337 id: "plugin-with-get".to_string(),
1338 path: "test-path".to_string(),
1339 timeout: Duration::from_secs(60),
1340 emit_logs: false,
1341 emit_traces: false,
1342 raw_response: false,
1343 allow_get_invocation: true,
1344 config: None,
1345 forward_logs: false,
1346 };
1347
1348 let app_state = create_mock_app_state(
1349 None,
1350 None,
1351 None,
1352 None,
1353 Some(vec![plugin_disabled.clone(), plugin_enabled.clone()]),
1354 None,
1355 )
1356 .await;
1357
1358 let plugin_repo = app_state.plugin_repository.clone();
1361 let found_plugin = plugin_repo
1362 .get_by_id("non-existent")
1363 .await
1364 .expect("Repository call should succeed");
1365 assert!(
1366 found_plugin.is_none(),
1367 "Non-existent plugin should return None (would trigger 404 in route handler)"
1368 );
1369
1370 let found_plugin = plugin_repo
1372 .get_by_id("plugin-no-get")
1373 .await
1374 .expect("Repository call should succeed");
1375 assert!(found_plugin.is_some(), "Plugin should exist in repository");
1376 let plugin = found_plugin.unwrap();
1377 assert!(
1378 !plugin.allow_get_invocation,
1379 "Plugin should have allow_get_invocation=false (would trigger 405 in route handler)"
1380 );
1381
1382 let found_plugin = plugin_repo
1384 .get_by_id("plugin-with-get")
1385 .await
1386 .expect("Repository call should succeed");
1387 assert!(found_plugin.is_some(), "Plugin should exist in repository");
1388 assert!(
1389 found_plugin.unwrap().allow_get_invocation,
1390 "Plugin should have allow_get_invocation=true (would proceed in route handler)"
1391 );
1392 }
1393
1394 #[actix_web::test]
1396 async fn test_error_responses_contain_appropriate_messages() {
1397 use crate::models::ApiResponse;
1398 use actix_web::test::TestRequest;
1399
1400 let http_req = TestRequest::default().to_http_request();
1401
1402 let result = build_plugin_call_request_from_post_body("/test", &http_req, b"{invalid}");
1404
1405 assert!(result.is_err());
1406 let err_response = result.unwrap_err();
1407 assert_eq!(
1408 err_response.status(),
1409 actix_web::http::StatusCode::BAD_REQUEST,
1410 "Should return 400 Bad Request"
1411 );
1412
1413 let body_bytes = actix_web::body::to_bytes(err_response.into_body())
1415 .await
1416 .unwrap();
1417 let api_response: ApiResponse<()> =
1418 serde_json::from_slice(&body_bytes).expect("Response should be valid JSON");
1419 assert!(
1420 !api_response.success,
1421 "Error response should have success=false"
1422 );
1423 assert!(
1424 api_response.error.is_some(),
1425 "Error response should contain error message"
1426 );
1427 assert!(
1428 api_response.error.unwrap().contains("Invalid JSON"),
1429 "Error message should mention 'Invalid JSON'"
1430 );
1431 }
1432
1433 #[actix_web::test]
1435 async fn test_query_params_with_special_characters_and_encoding() {
1436 use actix_web::test::TestRequest;
1437
1438 let req = TestRequest::default()
1440 .uri("/test?key=value%20with%20spaces&symbol=%26%3D%3F&unicode=%E4%B8%AD%E6%96%87")
1441 .to_http_request();
1442
1443 let params = extract_query_params(&req);
1444
1445 assert_eq!(
1446 params.get("key"),
1447 Some(&vec!["value with spaces".to_string()]),
1448 "URL-encoded spaces should be decoded"
1449 );
1450 assert_eq!(
1451 params.get("symbol"),
1452 Some(&vec!["&=?".to_string()]),
1453 "URL-encoded special characters should be decoded"
1454 );
1455 assert_eq!(
1456 params.get("unicode"),
1457 Some(&vec!["䏿–‡".to_string()]),
1458 "URL-encoded Unicode should be decoded"
1459 );
1460 }
1461
1462 #[actix_web::test]
1464 async fn test_params_without_wrapper_correctly_wrapped() {
1465 use actix_web::test::TestRequest;
1466
1467 let http_req = TestRequest::default().to_http_request();
1468
1469 let body_without_params = serde_json::json!({
1471 "user": "alice",
1472 "action": "transfer",
1473 "amount": 100
1474 });
1475
1476 let body = serde_json::to_vec(&body_without_params).unwrap();
1477 let req = build_plugin_call_request_from_post_body("/test", &http_req, &body).unwrap();
1478
1479 assert_eq!(
1481 req.params.get("user").unwrap(),
1482 "alice",
1483 "User field should be in params"
1484 );
1485 assert_eq!(
1486 req.params.get("action").unwrap(),
1487 "transfer",
1488 "Action field should be in params"
1489 );
1490 assert_eq!(
1491 req.params.get("amount").unwrap(),
1492 100,
1493 "Amount field should be in params"
1494 );
1495 }
1496
1497 #[actix_web::test]
1499 async fn test_method_field_set_correctly_for_get() {
1500 let captured = web::Data::new(CapturedRequest::default());
1501 let app = test::init_service(
1502 App::new()
1503 .app_data(captured.clone())
1504 .service(
1505 web::resource("/plugins/{plugin_id}/call{route:.*}")
1506 .route(web::get().to(capturing_plugin_call_get_handler)),
1507 )
1508 .configure(init),
1509 )
1510 .await;
1511
1512 let req = test::TestRequest::get()
1513 .uri("/plugins/test/call/verify?token=abc123")
1514 .to_request();
1515
1516 test::call_service(&app, req).await;
1517
1518 let captured_req = captured.get().expect("Request should have been captured");
1519 assert_eq!(
1520 captured_req.method,
1521 Some("GET".to_string()),
1522 "Method should be set to GET for GET requests"
1523 );
1524 assert_eq!(
1525 captured_req.params,
1526 serde_json::json!({}),
1527 "GET requests should have empty params object"
1528 );
1529 }
1530
1531 #[actix_web::test]
1534 async fn test_invalid_json_through_route_handler() {
1535 let captured = web::Data::new(CapturedRequest::default());
1536 let app = test::init_service(
1537 App::new()
1538 .app_data(captured.clone())
1539 .service(
1540 web::resource("/plugins/{plugin_id}/call{route:.*}")
1541 .route(web::post().to(capturing_plugin_call_handler)),
1542 )
1543 .configure(init),
1544 )
1545 .await;
1546
1547 let req = test::TestRequest::post()
1548 .uri("/plugins/test-plugin/call")
1549 .insert_header(("Content-Type", "application/json"))
1550 .set_payload("{invalid json syntax}")
1551 .to_request();
1552
1553 let resp = test::call_service(&app, req).await;
1554
1555 assert_eq!(
1556 resp.status(),
1557 actix_web::http::StatusCode::BAD_REQUEST,
1558 "Invalid JSON should return 400 Bad Request through route handler"
1559 );
1560
1561 let body = test::read_body(resp).await;
1563 let body_str = std::str::from_utf8(&body).unwrap();
1564 assert!(
1565 body_str.contains("Invalid JSON"),
1566 "Error message should contain 'Invalid JSON', got: {body_str}"
1567 );
1568
1569 assert!(
1571 captured.get().is_none(),
1572 "Invalid JSON request should not be captured"
1573 );
1574 }
1575
1576 #[actix_web::test]
1579 async fn test_plugin_call_request_with_various_param_types() {
1580 use actix_web::test::TestRequest;
1581
1582 let http_req = TestRequest::default().to_http_request();
1583
1584 let body_with_string_params = serde_json::json!({
1586 "params": "this is a string, not an object"
1587 });
1588 let body = serde_json::to_vec(&body_with_string_params).unwrap();
1589 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1590 assert!(result.is_ok(), "Params as string should be valid");
1591 let req = result.unwrap();
1592 assert_eq!(
1593 req.params,
1594 serde_json::json!("this is a string, not an object"),
1595 "String params should be preserved"
1596 );
1597
1598 let body_with_array_params = serde_json::json!({
1600 "params": [1, 2, 3, "four"]
1601 });
1602 let body = serde_json::to_vec(&body_with_array_params).unwrap();
1603 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1604 assert!(result.is_ok(), "Params as array should be valid");
1605 let req = result.unwrap();
1606 assert_eq!(
1607 req.params,
1608 serde_json::json!([1, 2, 3, "four"]),
1609 "Array params should be preserved"
1610 );
1611
1612 let body_with_number_params = serde_json::json!({
1614 "params": 42
1615 });
1616 let body = serde_json::to_vec(&body_with_number_params).unwrap();
1617 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1618 assert!(result.is_ok(), "Params as number should be valid");
1619 let req = result.unwrap();
1620 assert_eq!(
1621 req.params,
1622 serde_json::json!(42),
1623 "Number params should be preserved"
1624 );
1625
1626 let body_with_bool_params = serde_json::json!({
1628 "params": true
1629 });
1630 let body = serde_json::to_vec(&body_with_bool_params).unwrap();
1631 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1632 assert!(result.is_ok(), "Params as boolean should be valid");
1633 let req = result.unwrap();
1634 assert_eq!(
1635 req.params,
1636 serde_json::json!(true),
1637 "Boolean params should be preserved"
1638 );
1639 }
1640
1641 #[actix_web::test]
1643 async fn test_request_without_content_type_header() {
1644 let captured = web::Data::new(CapturedRequest::default());
1645 let app = test::init_service(
1646 App::new()
1647 .app_data(captured.clone())
1648 .service(
1649 web::resource("/plugins/{plugin_id}/call{route:.*}")
1650 .route(web::post().to(capturing_plugin_call_handler)),
1651 )
1652 .configure(init),
1653 )
1654 .await;
1655
1656 let req = test::TestRequest::post()
1658 .uri("/plugins/test-plugin/call")
1659 .set_payload("{\"test\": \"data\"}")
1660 .to_request();
1661
1662 let resp = test::call_service(&app, req).await;
1663
1664 assert!(
1667 resp.status().is_success(),
1668 "Request without Content-Type should be handled gracefully, got status: {}",
1669 resp.status()
1670 );
1671
1672 let captured_req = captured.get();
1674 assert!(
1675 captured_req.is_some(),
1676 "Request without Content-Type should still be processed"
1677 );
1678 }
1679
1680 #[actix_web::test]
1682 async fn test_very_large_request_body() {
1683 use actix_web::test::TestRequest;
1684
1685 let http_req = TestRequest::default().to_http_request();
1686
1687 let large_data = "x".repeat(10000);
1689 let large_json = serde_json::json!({
1690 "params": {
1691 "large_data": large_data,
1692 "count": 10000
1693 }
1694 });
1695
1696 let body = serde_json::to_vec(&large_json).unwrap();
1697 let result = build_plugin_call_request_from_post_body("/test", &http_req, &body);
1698
1699 assert!(result.is_ok(), "Large request body should be handled");
1700 let req = result.unwrap();
1701 assert_eq!(
1702 req.params
1703 .get("large_data")
1704 .unwrap()
1705 .as_str()
1706 .unwrap()
1707 .len(),
1708 10000,
1709 "Large data should be preserved correctly"
1710 );
1711 }
1712
1713 #[actix_web::test]
1715 async fn test_nested_routes_with_special_characters() {
1716 use actix_web::test::TestRequest;
1717
1718 let http_req = TestRequest::default().to_http_request();
1719 let body = serde_json::to_vec(&serde_json::json!({"test": "data"})).unwrap();
1720
1721 let test_cases = vec![
1722 ("/api/v1/verify", "/api/v1/verify"),
1723 ("/settle/transaction", "/settle/transaction"),
1724 ("/path-with-dashes", "/path-with-dashes"),
1725 ("/path_with_underscores", "/path_with_underscores"),
1726 ("/path.with.dots", "/path.with.dots"),
1727 ("/path%20with%20spaces", "/path%20with%20spaces"), ];
1729
1730 for (route, expected_route) in test_cases {
1731 let req = build_plugin_call_request_from_post_body(route, &http_req, &body).unwrap();
1732 assert_eq!(
1733 req.route,
1734 Some(expected_route.to_string()),
1735 "Route '{route}' should be preserved as '{expected_route}'"
1736 );
1737 }
1738 }
1739
1740 #[actix_web::test]
1741 async fn test_plugin_call_route_handler_post() {
1742 use crate::utils::mocks::mockutils::create_mock_app_state;
1743 use std::time::Duration;
1744
1745 let plugin = PluginModel {
1746 id: "test-plugin".to_string(),
1747 path: "test-path".to_string(),
1748 timeout: Duration::from_secs(60),
1749 emit_logs: false,
1750 emit_traces: false,
1751 raw_response: false,
1752 allow_get_invocation: false,
1753 config: None,
1754 forward_logs: false,
1755 };
1756
1757 let app_state =
1758 create_mock_app_state(None, None, None, None, Some(vec![plugin]), None).await;
1759
1760 let app = test::init_service(
1761 App::new()
1762 .app_data(web::Data::new(web::ThinData(app_state)))
1763 .configure(init),
1764 )
1765 .await;
1766
1767 let req = test::TestRequest::post()
1768 .uri("/plugins/test-plugin/call")
1769 .insert_header(("Content-Type", "application/json"))
1770 .set_json(serde_json::json!({"params": {"test": "data"}}))
1771 .to_request();
1772
1773 let resp = test::call_service(&app, req).await;
1774
1775 assert!(
1778 resp.status().is_server_error() || resp.status().is_client_error(),
1779 "Route handler should be executed, got status: {}",
1780 resp.status()
1781 );
1782 }
1783
1784 #[actix_web::test]
1787 async fn test_plugin_call_get_route_handler_allowed() {
1788 use crate::utils::mocks::mockutils::create_mock_app_state;
1789 use std::time::Duration;
1790
1791 let plugin = PluginModel {
1792 id: "test-plugin-with-get".to_string(),
1793 path: "test-path".to_string(),
1794 timeout: Duration::from_secs(60),
1795 emit_logs: false,
1796 emit_traces: false,
1797 raw_response: false,
1798 allow_get_invocation: true, config: None,
1800 forward_logs: false,
1801 };
1802
1803 let app_state =
1804 create_mock_app_state(None, None, None, None, Some(vec![plugin]), None).await;
1805
1806 let app = test::init_service(
1807 App::new()
1808 .app_data(web::Data::new(web::ThinData(app_state)))
1809 .configure(init),
1810 )
1811 .await;
1812
1813 let req = test::TestRequest::get()
1814 .uri("/plugins/test-plugin-with-get/call?token=abc123")
1815 .to_request();
1816
1817 let resp = test::call_service(&app, req).await;
1818
1819 assert!(
1822 resp.status().is_server_error() || resp.status().is_client_error(),
1823 "Route handler should be executed, got status: {}",
1824 resp.status()
1825 );
1826 }
1827
1828 async fn mock_get_plugin(path: web::Path<String>) -> impl Responder {
1834 let plugin_id = path.into_inner();
1835
1836 if plugin_id == "not-found" {
1837 return HttpResponse::NotFound().json(ApiResponse::<()>::error(format!(
1838 "Plugin with id {plugin_id} not found"
1839 )));
1840 }
1841
1842 let plugin = PluginModel {
1843 id: plugin_id,
1844 path: "test-path.ts".to_string(),
1845 timeout: Duration::from_secs(30),
1846 emit_logs: true,
1847 emit_traces: false,
1848 raw_response: false,
1849 allow_get_invocation: true,
1850 config: None,
1851 forward_logs: true,
1852 };
1853
1854 HttpResponse::Ok().json(ApiResponse::success(plugin))
1855 }
1856
1857 #[actix_web::test]
1858 async fn test_get_plugin_route_success() {
1859 let app = test::init_service(
1860 App::new()
1861 .service(
1862 web::resource("/plugins/{plugin_id}").route(web::get().to(mock_get_plugin)),
1863 )
1864 .configure(init),
1865 )
1866 .await;
1867
1868 let req = test::TestRequest::get()
1869 .uri("/plugins/my-plugin")
1870 .to_request();
1871
1872 let resp = test::call_service(&app, req).await;
1873 assert!(resp.status().is_success());
1874
1875 let body = test::read_body(resp).await;
1876 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
1877 assert!(response.success);
1878 assert_eq!(response.data.as_ref().unwrap().id, "my-plugin");
1879 assert!(response.data.as_ref().unwrap().emit_logs);
1880 assert!(response.data.as_ref().unwrap().forward_logs);
1881 }
1882
1883 #[actix_web::test]
1884 async fn test_get_plugin_route_not_found() {
1885 let app = test::init_service(
1886 App::new()
1887 .service(
1888 web::resource("/plugins/{plugin_id}").route(web::get().to(mock_get_plugin)),
1889 )
1890 .configure(init),
1891 )
1892 .await;
1893
1894 let req = test::TestRequest::get()
1895 .uri("/plugins/not-found")
1896 .to_request();
1897
1898 let resp = test::call_service(&app, req).await;
1899 assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND);
1900 }
1901
1902 async fn mock_update_plugin(
1908 path: web::Path<String>,
1909 body: web::Json<UpdatePluginRequest>,
1910 ) -> impl Responder {
1911 let plugin_id = path.into_inner();
1912 let update = body.into_inner();
1913
1914 let updated_plugin = PluginModel {
1916 id: plugin_id,
1917 path: "test-path".to_string(),
1918 timeout: Duration::from_secs(update.timeout.unwrap_or(30)),
1919 emit_logs: update.emit_logs.unwrap_or(false),
1920 emit_traces: update.emit_traces.unwrap_or(false),
1921 raw_response: update.raw_response.unwrap_or(false),
1922 allow_get_invocation: update.allow_get_invocation.unwrap_or(false),
1923 config: update.config.flatten(),
1924 forward_logs: update.forward_logs.unwrap_or(false),
1925 };
1926
1927 HttpResponse::Ok().json(ApiResponse::success(updated_plugin))
1928 }
1929
1930 #[actix_web::test]
1931 async fn test_update_plugin_route_success() {
1932 let app = test::init_service(
1933 App::new()
1934 .service(
1935 web::resource("/plugins/{plugin_id}")
1936 .route(web::patch().to(mock_update_plugin)),
1937 )
1938 .configure(init),
1939 )
1940 .await;
1941
1942 let req = test::TestRequest::patch()
1943 .uri("/plugins/test-plugin")
1944 .insert_header(("Content-Type", "application/json"))
1945 .set_json(serde_json::json!({
1946 "timeout": 60,
1947 "emit_logs": true,
1948 "forward_logs": true
1949 }))
1950 .to_request();
1951
1952 let resp = test::call_service(&app, req).await;
1953 assert!(resp.status().is_success());
1954
1955 let body = test::read_body(resp).await;
1956 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
1957 assert!(response.success);
1958 assert_eq!(response.data.as_ref().unwrap().id, "test-plugin");
1959 assert_eq!(
1960 response.data.as_ref().unwrap().timeout,
1961 Duration::from_secs(60)
1962 );
1963 assert!(response.data.as_ref().unwrap().emit_logs);
1964 assert!(response.data.as_ref().unwrap().forward_logs);
1965 }
1966
1967 #[actix_web::test]
1968 async fn test_update_plugin_route_with_config() {
1969 let app = test::init_service(
1970 App::new()
1971 .service(
1972 web::resource("/plugins/{plugin_id}")
1973 .route(web::patch().to(mock_update_plugin)),
1974 )
1975 .configure(init),
1976 )
1977 .await;
1978
1979 let req = test::TestRequest::patch()
1980 .uri("/plugins/my-plugin")
1981 .insert_header(("Content-Type", "application/json"))
1982 .set_json(serde_json::json!({
1983 "config": {
1984 "feature_enabled": true,
1985 "api_key": "secret123"
1986 }
1987 }))
1988 .to_request();
1989
1990 let resp = test::call_service(&app, req).await;
1991 assert!(resp.status().is_success());
1992
1993 let body = test::read_body(resp).await;
1994 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
1995 assert!(response.success);
1996 assert!(response.data.as_ref().unwrap().config.is_some());
1997 let config = response.data.as_ref().unwrap().config.as_ref().unwrap();
1998 assert_eq!(
1999 config.get("feature_enabled"),
2000 Some(&serde_json::json!(true))
2001 );
2002 assert_eq!(config.get("api_key"), Some(&serde_json::json!("secret123")));
2003 }
2004
2005 #[actix_web::test]
2006 async fn test_update_plugin_route_clear_config() {
2007 let app = test::init_service(
2008 App::new()
2009 .service(
2010 web::resource("/plugins/{plugin_id}")
2011 .route(web::patch().to(mock_update_plugin)),
2012 )
2013 .configure(init),
2014 )
2015 .await;
2016
2017 let req = test::TestRequest::patch()
2018 .uri("/plugins/test-plugin")
2019 .insert_header(("Content-Type", "application/json"))
2020 .set_json(serde_json::json!({
2021 "config": null
2022 }))
2023 .to_request();
2024
2025 let resp = test::call_service(&app, req).await;
2026 assert!(resp.status().is_success());
2027
2028 let body = test::read_body(resp).await;
2029 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
2030 assert!(response.success);
2031 assert!(response.data.as_ref().unwrap().config.is_none());
2032 }
2033
2034 #[actix_web::test]
2035 async fn test_update_plugin_route_empty_body() {
2036 let app = test::init_service(
2037 App::new()
2038 .service(
2039 web::resource("/plugins/{plugin_id}")
2040 .route(web::patch().to(mock_update_plugin)),
2041 )
2042 .configure(init),
2043 )
2044 .await;
2045
2046 let req = test::TestRequest::patch()
2048 .uri("/plugins/test-plugin")
2049 .insert_header(("Content-Type", "application/json"))
2050 .set_json(serde_json::json!({}))
2051 .to_request();
2052
2053 let resp = test::call_service(&app, req).await;
2054 assert!(resp.status().is_success());
2055 }
2056
2057 #[actix_web::test]
2058 async fn test_update_plugin_route_all_fields() {
2059 let app = test::init_service(
2060 App::new()
2061 .service(
2062 web::resource("/plugins/{plugin_id}")
2063 .route(web::patch().to(mock_update_plugin)),
2064 )
2065 .configure(init),
2066 )
2067 .await;
2068
2069 let req = test::TestRequest::patch()
2070 .uri("/plugins/full-update-plugin")
2071 .insert_header(("Content-Type", "application/json"))
2072 .set_json(serde_json::json!({
2073 "timeout": 120,
2074 "emit_logs": true,
2075 "emit_traces": true,
2076 "raw_response": true,
2077 "allow_get_invocation": true,
2078 "forward_logs": true,
2079 "config": {
2080 "key": "value"
2081 }
2082 }))
2083 .to_request();
2084
2085 let resp = test::call_service(&app, req).await;
2086 assert!(resp.status().is_success());
2087
2088 let body = test::read_body(resp).await;
2089 let response: ApiResponse<PluginModel> = serde_json::from_slice(&body).unwrap();
2090 let plugin = response.data.unwrap();
2091
2092 assert_eq!(plugin.timeout, Duration::from_secs(120));
2093 assert!(plugin.emit_logs);
2094 assert!(plugin.emit_traces);
2095 assert!(plugin.raw_response);
2096 assert!(plugin.allow_get_invocation);
2097 assert!(plugin.forward_logs);
2098 assert!(plugin.config.is_some());
2099 }
2100
2101 #[actix_web::test]
2102 async fn test_update_plugin_route_invalid_json() {
2103 let app = test::init_service(
2104 App::new()
2105 .service(
2106 web::resource("/plugins/{plugin_id}")
2107 .route(web::patch().to(mock_update_plugin)),
2108 )
2109 .configure(init),
2110 )
2111 .await;
2112
2113 let req = test::TestRequest::patch()
2114 .uri("/plugins/test-plugin")
2115 .insert_header(("Content-Type", "application/json"))
2116 .set_payload("{ invalid json }")
2117 .to_request();
2118
2119 let resp = test::call_service(&app, req).await;
2120 assert!(resp.status().is_client_error());
2121 }
2122
2123 #[actix_web::test]
2124 async fn test_update_plugin_route_unknown_field_rejected() {
2125 let app = test::init_service(
2126 App::new()
2127 .service(
2128 web::resource("/plugins/{plugin_id}")
2129 .route(web::patch().to(mock_update_plugin)),
2130 )
2131 .configure(init),
2132 )
2133 .await;
2134
2135 let req = test::TestRequest::patch()
2137 .uri("/plugins/test-plugin")
2138 .insert_header(("Content-Type", "application/json"))
2139 .set_json(serde_json::json!({
2140 "timeout": 60,
2141 "unknown_field": "should_fail"
2142 }))
2143 .to_request();
2144
2145 let resp = test::call_service(&app, req).await;
2146 assert!(resp.status().is_client_error());
2147 }
2148}