Skip to main content

mockforge_ui/handlers/
chains.rs

1//! Chain management proxy handlers
2//!
3//! These handlers proxy chain-related requests from the Admin UI to the main HTTP server
4
5use axum::{
6    extract::{Path, State},
7    http::StatusCode,
8    response::{IntoResponse, Response},
9    Json,
10};
11use serde_json::Value;
12
13use super::AdminState;
14
15/// Proxy chain list requests to the main HTTP server
16pub async fn proxy_chains_list(State(state): State<AdminState>) -> Response {
17    proxy_to_http_server(&state, "/chains", None).await
18}
19
20/// Proxy chain creation requests to the main HTTP server
21pub async fn proxy_chains_create(
22    State(state): State<AdminState>,
23    Json(body): Json<Value>,
24) -> Response {
25    proxy_to_http_server(&state, "/chains", Some(body)).await
26}
27
28/// Proxy get chain requests to the main HTTP server
29pub async fn proxy_chain_get(State(state): State<AdminState>, Path(id): Path<String>) -> Response {
30    proxy_to_http_server(&state, &format!("/chains/{}", id), None).await
31}
32
33/// Proxy chain update requests to the main HTTP server
34pub async fn proxy_chain_update(
35    State(state): State<AdminState>,
36    Path(id): Path<String>,
37    Json(body): Json<Value>,
38) -> Response {
39    proxy_to_http_server(&state, &format!("/chains/{}", id), Some(body)).await
40}
41
42/// Proxy chain delete requests to the main HTTP server
43pub async fn proxy_chain_delete(
44    State(state): State<AdminState>,
45    Path(id): Path<String>,
46) -> Response {
47    proxy_to_http_server(&state, &format!("/chains/{}", id), None).await
48}
49
50/// Proxy chain execute requests to the main HTTP server
51pub async fn proxy_chain_execute(
52    State(state): State<AdminState>,
53    Path(id): Path<String>,
54    Json(body): Json<Value>,
55) -> Response {
56    proxy_to_http_server(&state, &format!("/chains/{}/execute", id), Some(body)).await
57}
58
59/// Proxy chain validate requests to the main HTTP server
60pub async fn proxy_chain_validate(
61    State(state): State<AdminState>,
62    Path(id): Path<String>,
63) -> Response {
64    proxy_to_http_server(&state, &format!("/chains/{}/validate", id), None).await
65}
66
67/// Proxy chain history requests to the main HTTP server
68pub async fn proxy_chain_history(
69    State(state): State<AdminState>,
70    Path(id): Path<String>,
71) -> Response {
72    proxy_to_http_server(&state, &format!("/chains/{}/history", id), None).await
73}
74
75/// Helper function to proxy requests to the main HTTP server
76async fn proxy_to_http_server(state: &AdminState, path: &str, body: Option<Value>) -> Response {
77    let Some(http_addr) = state.http_server_addr else {
78        return (StatusCode::SERVICE_UNAVAILABLE, "HTTP server address not configured")
79            .into_response();
80    };
81
82    let url = format!("http://{}/__mockforge{}", http_addr, path);
83
84    let client = reqwest::Client::new();
85    let mut request_builder = if body.is_some() {
86        client.post(&url)
87    } else {
88        client.get(&url)
89    };
90
91    if let Some(json_body) = body {
92        request_builder = request_builder.json(&json_body);
93    }
94
95    match request_builder.send().await {
96        Ok(response) => {
97            let status = response.status();
98            match response.text().await {
99                Ok(text) => {
100                    // Try to parse as JSON, otherwise return as text
101                    if let Ok(json) = serde_json::from_str::<Value>(&text) {
102                        (
103                            StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::OK),
104                            Json(json),
105                        )
106                            .into_response()
107                    } else {
108                        (StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::OK), text)
109                            .into_response()
110                    }
111                }
112                Err(e) => {
113                    (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to read response: {}", e))
114                        .into_response()
115                }
116            }
117        }
118        Err(e) => {
119            (StatusCode::BAD_GATEWAY, format!("Failed to proxy request: {}", e)).into_response()
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::net::SocketAddr;
128
129    fn create_test_state(http_addr: Option<SocketAddr>) -> AdminState {
130        AdminState::new(
131            http_addr, None, None, None, false, 8080, None, None, None, None, None, None, None,
132            None,
133        )
134    }
135
136    // ==================== Proxy to HTTP Server Tests ====================
137
138    #[tokio::test]
139    async fn test_proxy_to_http_server_no_addr() {
140        let state = create_test_state(None);
141        let response = proxy_to_http_server(&state, "/test", None).await;
142
143        // Response should indicate service unavailable
144        // We can't easily extract status from Response, but we verify it compiles
145        let _ = response;
146    }
147
148    #[tokio::test]
149    async fn test_proxy_to_http_server_with_path() {
150        let state = create_test_state(None);
151        let response = proxy_to_http_server(&state, "/chains/123", None).await;
152        let _ = response;
153    }
154
155    #[tokio::test]
156    async fn test_proxy_to_http_server_with_body() {
157        let state = create_test_state(None);
158        let body = serde_json::json!({"name": "test-chain"});
159        let response = proxy_to_http_server(&state, "/chains", Some(body)).await;
160        let _ = response;
161    }
162
163    // ==================== AdminState Tests ====================
164
165    #[test]
166    fn test_admin_state_creation() {
167        let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
168        let state = create_test_state(Some(addr));
169        assert!(state.http_server_addr.is_some());
170    }
171
172    #[test]
173    fn test_admin_state_no_http_addr() {
174        let state = create_test_state(None);
175        assert!(state.http_server_addr.is_none());
176    }
177
178    // ==================== Path Construction Tests ====================
179
180    #[test]
181    fn test_chain_path_construction() {
182        let chain_id = "chain-123";
183        let path = format!("/chains/{}", chain_id);
184        assert_eq!(path, "/chains/chain-123");
185    }
186
187    #[test]
188    fn test_chain_execute_path_construction() {
189        let chain_id = "exec-chain";
190        let path = format!("/chains/{}/execute", chain_id);
191        assert_eq!(path, "/chains/exec-chain/execute");
192    }
193
194    #[test]
195    fn test_chain_validate_path_construction() {
196        let chain_id = "validate-chain";
197        let path = format!("/chains/{}/validate", chain_id);
198        assert_eq!(path, "/chains/validate-chain/validate");
199    }
200
201    #[test]
202    fn test_chain_history_path_construction() {
203        let chain_id = "history-chain";
204        let path = format!("/chains/{}/history", chain_id);
205        assert_eq!(path, "/chains/history-chain/history");
206    }
207
208    // ==================== URL Construction Tests ====================
209
210    #[test]
211    fn test_url_construction_chains_endpoint() {
212        let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
213        let path = "/chains";
214        let url = format!("http://{}/__mockforge{}", addr, path);
215        assert_eq!(url, "http://127.0.0.1:8080/__mockforge/chains");
216    }
217
218    #[test]
219    fn test_url_construction_with_id() {
220        let addr: SocketAddr = "192.168.1.1:3000".parse().unwrap();
221        let chain_id = "abc123";
222        let url = format!("http://{}/__mockforge/chains/{}", addr, chain_id);
223        assert_eq!(url, "http://192.168.1.1:3000/__mockforge/chains/abc123");
224    }
225
226    #[test]
227    fn test_url_construction_ipv6() {
228        let addr: SocketAddr = "[::1]:8080".parse().unwrap();
229        let path = "/chains";
230        let url = format!("http://{}/__mockforge{}", addr, path);
231        assert_eq!(url, "http://[::1]:8080/__mockforge/chains");
232    }
233
234    // ==================== Request Body Tests ====================
235
236    #[test]
237    fn test_chain_create_body() {
238        let body = serde_json::json!({
239            "name": "test-chain",
240            "steps": [
241                {"type": "http", "endpoint": "/api/users"},
242                {"type": "transform", "expression": "$.data"}
243            ]
244        });
245
246        assert!(body.get("name").is_some());
247        assert!(body.get("steps").is_some());
248        let steps = body.get("steps").unwrap().as_array().unwrap();
249        assert_eq!(steps.len(), 2);
250    }
251
252    #[test]
253    fn test_chain_update_body() {
254        let body = serde_json::json!({
255            "name": "updated-chain",
256            "enabled": true,
257            "timeout_ms": 5000
258        });
259
260        assert_eq!(body.get("name").unwrap(), "updated-chain");
261        assert_eq!(body.get("enabled").unwrap(), true);
262    }
263
264    #[test]
265    fn test_chain_execute_body() {
266        let body = serde_json::json!({
267            "input": {
268                "user_id": 123,
269                "action": "fetch"
270            },
271            "options": {
272                "timeout": 10000,
273                "retry": true
274            }
275        });
276
277        assert!(body.get("input").is_some());
278        assert!(body.get("options").is_some());
279    }
280
281    // ==================== Edge Cases ====================
282
283    #[test]
284    fn test_chain_id_with_special_characters() {
285        let chain_id = "chain-with-dashes_and_underscores";
286        let path = format!("/chains/{}", chain_id);
287        assert!(path.contains(chain_id));
288    }
289
290    #[test]
291    fn test_chain_id_uuid_format() {
292        let chain_id = "550e8400-e29b-41d4-a716-446655440000";
293        let path = format!("/chains/{}", chain_id);
294        assert!(path.contains(chain_id));
295    }
296
297    #[test]
298    fn test_empty_body_is_none() {
299        let body: Option<Value> = None;
300        assert!(body.is_none());
301    }
302
303    #[test]
304    fn test_empty_json_body() {
305        let body = serde_json::json!({});
306        assert!(body.is_object());
307        assert!(body.as_object().unwrap().is_empty());
308    }
309
310    // ==================== Request Method Selection Tests ====================
311
312    #[test]
313    fn test_method_selection_with_body() {
314        let body: Option<Value> = Some(serde_json::json!({"test": true}));
315        // With body -> POST
316        assert!(body.is_some());
317    }
318
319    #[test]
320    fn test_method_selection_without_body() {
321        let body: Option<Value> = None;
322        // Without body -> GET
323        assert!(body.is_none());
324    }
325
326    // ==================== Status Code Tests ====================
327
328    #[test]
329    fn test_status_code_conversion_success() {
330        let status = StatusCode::OK.as_u16();
331        let converted = StatusCode::from_u16(status);
332        assert!(converted.is_ok());
333        assert_eq!(converted.unwrap(), StatusCode::OK);
334    }
335
336    #[test]
337    fn test_status_code_conversion_not_found() {
338        let status = 404u16;
339        let converted = StatusCode::from_u16(status);
340        assert!(converted.is_ok());
341        assert_eq!(converted.unwrap(), StatusCode::NOT_FOUND);
342    }
343
344    #[test]
345    fn test_status_code_conversion_server_error() {
346        let status = 500u16;
347        let converted = StatusCode::from_u16(status);
348        assert!(converted.is_ok());
349        assert_eq!(converted.unwrap(), StatusCode::INTERNAL_SERVER_ERROR);
350    }
351
352    #[test]
353    fn test_status_code_conversion_created() {
354        let status = 201u16;
355        let converted = StatusCode::from_u16(status);
356        assert!(converted.is_ok());
357        assert_eq!(converted.unwrap(), StatusCode::CREATED);
358    }
359
360    #[test]
361    fn test_status_code_conversion_bad_gateway() {
362        let status = 502u16;
363        let converted = StatusCode::from_u16(status);
364        assert!(converted.is_ok());
365        assert_eq!(converted.unwrap(), StatusCode::BAD_GATEWAY);
366    }
367}