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(http_addr, None, None, None, false, 8080, None, None, None, None, None)
131    }
132
133    // ==================== Proxy to HTTP Server Tests ====================
134
135    #[tokio::test]
136    async fn test_proxy_to_http_server_no_addr() {
137        let state = create_test_state(None);
138        let response = proxy_to_http_server(&state, "/test", None).await;
139
140        // Response should indicate service unavailable
141        // We can't easily extract status from Response, but we verify it compiles
142        let _ = response;
143    }
144
145    #[tokio::test]
146    async fn test_proxy_to_http_server_with_path() {
147        let state = create_test_state(None);
148        let response = proxy_to_http_server(&state, "/chains/123", None).await;
149        let _ = response;
150    }
151
152    #[tokio::test]
153    async fn test_proxy_to_http_server_with_body() {
154        let state = create_test_state(None);
155        let body = serde_json::json!({"name": "test-chain"});
156        let response = proxy_to_http_server(&state, "/chains", Some(body)).await;
157        let _ = response;
158    }
159
160    // ==================== AdminState Tests ====================
161
162    #[test]
163    fn test_admin_state_creation() {
164        let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
165        let state = create_test_state(Some(addr));
166        assert!(state.http_server_addr.is_some());
167    }
168
169    #[test]
170    fn test_admin_state_no_http_addr() {
171        let state = create_test_state(None);
172        assert!(state.http_server_addr.is_none());
173    }
174
175    // ==================== Path Construction Tests ====================
176
177    #[test]
178    fn test_chain_path_construction() {
179        let chain_id = "chain-123";
180        let path = format!("/chains/{}", chain_id);
181        assert_eq!(path, "/chains/chain-123");
182    }
183
184    #[test]
185    fn test_chain_execute_path_construction() {
186        let chain_id = "exec-chain";
187        let path = format!("/chains/{}/execute", chain_id);
188        assert_eq!(path, "/chains/exec-chain/execute");
189    }
190
191    #[test]
192    fn test_chain_validate_path_construction() {
193        let chain_id = "validate-chain";
194        let path = format!("/chains/{}/validate", chain_id);
195        assert_eq!(path, "/chains/validate-chain/validate");
196    }
197
198    #[test]
199    fn test_chain_history_path_construction() {
200        let chain_id = "history-chain";
201        let path = format!("/chains/{}/history", chain_id);
202        assert_eq!(path, "/chains/history-chain/history");
203    }
204
205    // ==================== URL Construction Tests ====================
206
207    #[test]
208    fn test_url_construction_chains_endpoint() {
209        let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
210        let path = "/chains";
211        let url = format!("http://{}/__mockforge{}", addr, path);
212        assert_eq!(url, "http://127.0.0.1:8080/__mockforge/chains");
213    }
214
215    #[test]
216    fn test_url_construction_with_id() {
217        let addr: SocketAddr = "192.168.1.1:3000".parse().unwrap();
218        let chain_id = "abc123";
219        let url = format!("http://{}/__mockforge/chains/{}", addr, chain_id);
220        assert_eq!(url, "http://192.168.1.1:3000/__mockforge/chains/abc123");
221    }
222
223    #[test]
224    fn test_url_construction_ipv6() {
225        let addr: SocketAddr = "[::1]:8080".parse().unwrap();
226        let path = "/chains";
227        let url = format!("http://{}/__mockforge{}", addr, path);
228        assert_eq!(url, "http://[::1]:8080/__mockforge/chains");
229    }
230
231    // ==================== Request Body Tests ====================
232
233    #[test]
234    fn test_chain_create_body() {
235        let body = serde_json::json!({
236            "name": "test-chain",
237            "steps": [
238                {"type": "http", "endpoint": "/api/users"},
239                {"type": "transform", "expression": "$.data"}
240            ]
241        });
242
243        assert!(body.get("name").is_some());
244        assert!(body.get("steps").is_some());
245        let steps = body.get("steps").unwrap().as_array().unwrap();
246        assert_eq!(steps.len(), 2);
247    }
248
249    #[test]
250    fn test_chain_update_body() {
251        let body = serde_json::json!({
252            "name": "updated-chain",
253            "enabled": true,
254            "timeout_ms": 5000
255        });
256
257        assert_eq!(body.get("name").unwrap(), "updated-chain");
258        assert_eq!(body.get("enabled").unwrap(), true);
259    }
260
261    #[test]
262    fn test_chain_execute_body() {
263        let body = serde_json::json!({
264            "input": {
265                "user_id": 123,
266                "action": "fetch"
267            },
268            "options": {
269                "timeout": 10000,
270                "retry": true
271            }
272        });
273
274        assert!(body.get("input").is_some());
275        assert!(body.get("options").is_some());
276    }
277
278    // ==================== Edge Cases ====================
279
280    #[test]
281    fn test_chain_id_with_special_characters() {
282        let chain_id = "chain-with-dashes_and_underscores";
283        let path = format!("/chains/{}", chain_id);
284        assert!(path.contains(chain_id));
285    }
286
287    #[test]
288    fn test_chain_id_uuid_format() {
289        let chain_id = "550e8400-e29b-41d4-a716-446655440000";
290        let path = format!("/chains/{}", chain_id);
291        assert!(path.contains(chain_id));
292    }
293
294    #[test]
295    fn test_empty_body_is_none() {
296        let body: Option<Value> = None;
297        assert!(body.is_none());
298    }
299
300    #[test]
301    fn test_empty_json_body() {
302        let body = serde_json::json!({});
303        assert!(body.is_object());
304        assert!(body.as_object().unwrap().is_empty());
305    }
306
307    // ==================== Request Method Selection Tests ====================
308
309    #[test]
310    fn test_method_selection_with_body() {
311        let body: Option<Value> = Some(serde_json::json!({"test": true}));
312        // With body -> POST
313        assert!(body.is_some());
314    }
315
316    #[test]
317    fn test_method_selection_without_body() {
318        let body: Option<Value> = None;
319        // Without body -> GET
320        assert!(body.is_none());
321    }
322
323    // ==================== Status Code Tests ====================
324
325    #[test]
326    fn test_status_code_conversion_success() {
327        let status = reqwest::StatusCode::OK.as_u16();
328        let converted = StatusCode::from_u16(status);
329        assert!(converted.is_ok());
330        assert_eq!(converted.unwrap(), StatusCode::OK);
331    }
332
333    #[test]
334    fn test_status_code_conversion_not_found() {
335        let status = 404u16;
336        let converted = StatusCode::from_u16(status);
337        assert!(converted.is_ok());
338        assert_eq!(converted.unwrap(), StatusCode::NOT_FOUND);
339    }
340
341    #[test]
342    fn test_status_code_conversion_server_error() {
343        let status = 500u16;
344        let converted = StatusCode::from_u16(status);
345        assert!(converted.is_ok());
346        assert_eq!(converted.unwrap(), StatusCode::INTERNAL_SERVER_ERROR);
347    }
348
349    #[test]
350    fn test_status_code_conversion_created() {
351        let status = 201u16;
352        let converted = StatusCode::from_u16(status);
353        assert!(converted.is_ok());
354        assert_eq!(converted.unwrap(), StatusCode::CREATED);
355    }
356
357    #[test]
358    fn test_status_code_conversion_bad_gateway() {
359        let status = 502u16;
360        let converted = StatusCode::from_u16(status);
361        assert!(converted.is_ok());
362        assert_eq!(converted.unwrap(), StatusCode::BAD_GATEWAY);
363    }
364}