openzeppelin_relayer/api/routes/
plugin.rs1use std::collections::HashMap;
5
6use crate::{
7 api::controllers::plugin,
8 models::{DefaultAppState, PaginationQuery, PluginCallRequest},
9};
10use actix_web::{get, post, web, HttpRequest, Responder};
11
12#[get("/plugins")]
14async fn list_plugins(
15 query: web::Query<PaginationQuery>,
16 data: web::ThinData<DefaultAppState>,
17) -> impl Responder {
18 plugin::list_plugins(query.into_inner(), data).await
19}
20
21fn extract_headers(http_req: &HttpRequest) -> HashMap<String, Vec<String>> {
23 let mut headers: HashMap<String, Vec<String>> = HashMap::new();
24 for (name, value) in http_req.headers().iter() {
25 if let Ok(value_str) = value.to_str() {
26 headers
27 .entry(name.as_str().to_string())
28 .or_default()
29 .push(value_str.to_string());
30 }
31 }
32 headers
33}
34
35#[post("/plugins/{plugin_id}/call")]
37async fn plugin_call(
38 plugin_id: web::Path<String>,
39 http_req: HttpRequest,
40 req: web::Json<PluginCallRequest>,
41 data: web::ThinData<DefaultAppState>,
42) -> impl Responder {
43 let mut plugin_call_request = req.into_inner();
44 plugin_call_request.headers = Some(extract_headers(&http_req));
45 plugin::call_plugin(plugin_id.into_inner(), plugin_call_request, data).await
46}
47
48pub fn init(cfg: &mut web::ServiceConfig) {
50 cfg.service(plugin_call); cfg.service(list_plugins); }
54
55#[cfg(test)]
56mod tests {
57 use std::time::Duration;
58
59 use super::*;
60 use crate::{models::PluginModel, services::plugins::PluginCallResponse};
61 use actix_web::{test, App, HttpResponse};
62
63 async fn mock_plugin_call() -> impl Responder {
64 HttpResponse::Ok().json(PluginCallResponse {
65 result: serde_json::Value::Null,
66 metadata: None,
67 })
68 }
69
70 async fn mock_list_plugins() -> impl Responder {
71 HttpResponse::Ok().json(vec![
72 PluginModel {
73 id: "test-plugin".to_string(),
74 path: "test-path".to_string(),
75 timeout: Duration::from_secs(69),
76 emit_logs: false,
77 emit_traces: false,
78 },
79 PluginModel {
80 id: "test-plugin2".to_string(),
81 path: "test-path2".to_string(),
82 timeout: Duration::from_secs(69),
83 emit_logs: false,
84 emit_traces: false,
85 },
86 ])
87 }
88
89 #[actix_web::test]
90 async fn test_plugin_call() {
91 let app = test::init_service(
92 App::new()
93 .service(
94 web::resource("/plugins/{plugin_id}/call")
95 .route(web::post().to(mock_plugin_call)),
96 )
97 .configure(init),
98 )
99 .await;
100
101 let req = test::TestRequest::post()
102 .uri("/plugins/test-plugin/call")
103 .insert_header(("Content-Type", "application/json"))
104 .set_json(serde_json::json!({
105 "params": serde_json::Value::Null,
106 }))
107 .to_request();
108 let resp = test::call_service(&app, req).await;
109
110 assert!(resp.status().is_success());
111
112 let body = test::read_body(resp).await;
113 let plugin_call_response: PluginCallResponse = serde_json::from_slice(&body).unwrap();
114 assert!(plugin_call_response.result.is_null());
115 }
116
117 #[actix_web::test]
118 async fn test_list_plugins() {
119 let app = test::init_service(
120 App::new()
121 .service(web::resource("/plugins").route(web::get().to(mock_list_plugins)))
122 .configure(init),
123 )
124 .await;
125
126 let req = test::TestRequest::get().uri("/plugins").to_request();
127 let resp = test::call_service(&app, req).await;
128
129 assert!(resp.status().is_success());
130
131 let body = test::read_body(resp).await;
132 let plugin_call_response: Vec<PluginModel> = serde_json::from_slice(&body).unwrap();
133
134 assert_eq!(plugin_call_response.len(), 2);
135 assert_eq!(plugin_call_response[0].id, "test-plugin");
136 assert_eq!(plugin_call_response[0].path, "test-path");
137 assert_eq!(plugin_call_response[1].id, "test-plugin2");
138 assert_eq!(plugin_call_response[1].path, "test-path2");
139 }
140
141 #[actix_web::test]
142 async fn test_plugin_call_extracts_headers() {
143 let app = test::init_service(
145 App::new()
146 .service(
147 web::resource("/plugins/{plugin_id}/call")
148 .route(web::post().to(mock_plugin_call)),
149 )
150 .configure(init),
151 )
152 .await;
153
154 let req = test::TestRequest::post()
155 .uri("/plugins/test-plugin/call")
156 .insert_header(("Content-Type", "application/json"))
157 .insert_header(("X-Custom-Header", "custom-value"))
158 .insert_header(("Authorization", "Bearer test-token"))
159 .insert_header(("X-Request-Id", "req-12345"))
160 .insert_header(("Accept", "application/json"))
162 .set_json(serde_json::json!({
163 "params": {"test": "data"},
164 }))
165 .to_request();
166
167 let resp = test::call_service(&app, req).await;
168 assert!(resp.status().is_success());
169 }
170
171 #[actix_web::test]
172 async fn test_extract_headers_unit() {
173 use actix_web::test::TestRequest;
175
176 let req = TestRequest::default()
177 .insert_header(("X-Custom-Header", "value1"))
178 .insert_header(("Authorization", "Bearer token"))
179 .insert_header(("Content-Type", "application/json"))
180 .to_http_request();
181
182 let headers = extract_headers(&req);
183
184 assert_eq!(
185 headers.get("x-custom-header"),
186 Some(&vec!["value1".to_string()])
187 );
188 assert_eq!(
189 headers.get("authorization"),
190 Some(&vec!["Bearer token".to_string()])
191 );
192 assert_eq!(
193 headers.get("content-type"),
194 Some(&vec!["application/json".to_string()])
195 );
196 }
197
198 #[actix_web::test]
199 async fn test_extract_headers_multi_value() {
200 use actix_web::test::TestRequest;
201
202 let req = TestRequest::default()
204 .insert_header(("X-Values", "value1"))
205 .to_http_request();
206
207 let headers = extract_headers(&req);
208
209 let values = headers.get("x-values").unwrap();
211 assert_eq!(values.len(), 1);
212 assert_eq!(values[0], "value1");
213 }
214
215 #[actix_web::test]
216 async fn test_extract_headers_empty() {
217 use actix_web::test::TestRequest;
218
219 let req = TestRequest::default().to_http_request();
220 let headers = extract_headers(&req);
221
222 let _ = headers.len();
225 }
226}