1use axum::{
7 extract::{Path, Query, State},
8 http::StatusCode,
9 response::Json,
10};
11use chrono::Utc;
12use mockforge_core::request_logger::{get_global_logger, RequestLogEntry};
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value};
15use std::collections::HashMap;
16
17use crate::handlers::AdminState;
18use crate::models::ApiResponse;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct PlaygroundEndpoint {
23 pub protocol: String,
25 pub method: String,
27 pub path: String,
29 pub description: Option<String>,
31 pub enabled: bool,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ExecuteRestRequest {
38 pub method: String,
40 pub path: String,
42 pub headers: Option<HashMap<String, String>>,
44 pub body: Option<Value>,
46 pub base_url: Option<String>,
48 #[serde(default)]
50 pub use_mockai: bool,
51 pub workspace_id: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ExecuteGraphQLRequest {
58 pub query: String,
60 pub variables: Option<HashMap<String, Value>>,
62 pub operation_name: Option<String>,
64 pub base_url: Option<String>,
66 pub workspace_id: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ExecuteResponse {
73 pub status_code: u16,
75 pub headers: HashMap<String, String>,
77 pub body: Value,
79 pub response_time_ms: u64,
81 pub request_id: String,
83 pub error: Option<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct GraphQLIntrospectionResult {
90 pub schema: Value,
92 pub query_types: Vec<String>,
94 pub mutation_types: Vec<String>,
96 pub subscription_types: Vec<String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PlaygroundHistoryEntry {
103 pub id: String,
105 pub protocol: String,
107 pub method: String,
109 pub path: String,
111 pub status_code: u16,
113 pub response_time_ms: u64,
115 pub timestamp: chrono::DateTime<chrono::Utc>,
117 pub request_headers: Option<HashMap<String, String>>,
119 pub request_body: Option<Value>,
121 pub graphql_query: Option<String>,
123 pub graphql_variables: Option<HashMap<String, Value>>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct CodeSnippetRequest {
130 pub protocol: String,
132 pub method: Option<String>,
134 pub path: String,
136 pub headers: Option<HashMap<String, String>>,
138 pub body: Option<Value>,
140 pub graphql_query: Option<String>,
142 pub graphql_variables: Option<HashMap<String, Value>>,
144 pub base_url: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct CodeSnippetResponse {
151 pub snippets: HashMap<String, String>,
153}
154
155pub async fn list_playground_endpoints(
157 State(state): State<AdminState>,
158 axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
159) -> Json<ApiResponse<Vec<PlaygroundEndpoint>>> {
160 let workspace_id = params.get("workspace_id");
162 let mut endpoints = Vec::new();
163
164 if let Some(http_addr) = state.http_server_addr {
166 let mut url = format!("http://{}/__mockforge/routes", http_addr);
167
168 if let Some(ws_id) = workspace_id {
170 url = format!("{}?workspace_id={}", url, ws_id);
171 }
172
173 if let Ok(response) = reqwest::get(&url).await {
174 if response.status().is_success() {
175 if let Ok(body) = response.json::<Value>().await {
176 if let Some(routes) = body.get("routes").and_then(|r| r.as_array()) {
177 for route in routes {
178 if let Some(ws_id) = workspace_id {
180 if let Some(route_workspace) =
181 route.get("workspace_id").and_then(|w| w.as_str())
182 {
183 if route_workspace != ws_id {
184 continue; }
186 }
187 }
188
189 if let (Some(method), Some(path)) = (
190 route.get("method").and_then(|m| m.as_str()),
191 route.get("path").and_then(|p| p.as_str()),
192 ) {
193 endpoints.push(PlaygroundEndpoint {
194 protocol: "rest".to_string(),
195 method: method.to_string(),
196 path: path.to_string(),
197 description: route
198 .get("description")
199 .and_then(|d| d.as_str())
200 .map(|s| s.to_string()),
201 enabled: true,
202 });
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210
211 if state.graphql_server_addr.is_some() {
213 endpoints.push(PlaygroundEndpoint {
214 protocol: "graphql".to_string(),
215 method: "query".to_string(),
216 path: "/graphql".to_string(),
217 description: Some("GraphQL endpoint".to_string()),
218 enabled: true,
219 });
220 }
221
222 Json(ApiResponse::success(endpoints))
223}
224
225pub async fn execute_rest_request(
227 State(state): State<AdminState>,
228 axum::extract::Json(request): axum::extract::Json<ExecuteRestRequest>,
229) -> Json<ApiResponse<ExecuteResponse>> {
230 let start_time = std::time::Instant::now();
231 let request_id = uuid::Uuid::new_v4().to_string();
232
233 let base_url = request.base_url.unwrap_or_else(|| {
235 state
236 .http_server_addr
237 .map(|addr| format!("http://{}", addr))
238 .unwrap_or_else(|| "http://localhost:3000".to_string())
239 });
240
241 let url = if request.path.starts_with("http") {
243 request.path.clone()
244 } else {
245 format!("{}{}", base_url, request.path)
246 };
247
248 let client = reqwest::Client::builder()
250 .timeout(std::time::Duration::from_secs(30))
251 .build()
252 .unwrap_or_else(|_| reqwest::Client::new());
253
254 let mut http_request = match request.method.as_str() {
256 "GET" => client.get(&url),
257 "POST" => client.post(&url),
258 "PUT" => client.put(&url),
259 "DELETE" => client.delete(&url),
260 "PATCH" => client.patch(&url),
261 _ => {
262 return Json(ApiResponse::error(format!(
263 "Unsupported HTTP method: {}",
264 request.method
265 )));
266 }
267 };
268
269 let mut headers = request.headers.clone().unwrap_or_default();
271
272 if request.use_mockai {
274 headers.insert("X-MockAI-Preview".to_string(), "true".to_string());
275 }
276
277 if let Some(ws_id) = &request.workspace_id {
279 headers.insert("X-Workspace-ID".to_string(), ws_id.clone());
280 }
281
282 for (key, value) in &headers {
283 http_request = http_request.header(key, value);
284 }
285
286 if let Some(body) = &request.body {
288 http_request = http_request.json(body);
289 }
290
291 let response = http_request.send().await;
293
294 let response_time_ms = start_time.elapsed().as_millis() as u64;
295
296 match response {
297 Ok(resp) => {
298 let status_code = resp.status().as_u16();
299
300 let mut headers = HashMap::new();
302 for (key, value) in resp.headers() {
303 if let Ok(value_str) = value.to_str() {
304 headers.insert(key.to_string(), value_str.to_string());
305 }
306 }
307
308 let body = resp
310 .json::<Value>()
311 .await
312 .unwrap_or_else(|_| json!({ "error": "Failed to parse response as JSON" }));
313
314 if let Some(logger) = get_global_logger() {
316 let mut metadata = HashMap::new();
318 if let Some(ws_id) =
319 request.workspace_id.as_ref().or_else(|| headers.get("X-Workspace-ID"))
320 {
321 metadata.insert("workspace_id".to_string(), ws_id.clone());
322 }
323
324 let log_entry = RequestLogEntry {
325 id: request_id.clone(),
326 timestamp: Utc::now(),
327 server_type: "http".to_string(),
328 method: request.method.clone(),
329 path: request.path.clone(),
330 status_code,
331 response_time_ms,
332 client_ip: None,
333 user_agent: Some("MockForge-Playground".to_string()),
334 headers: headers.clone(),
335 response_size_bytes: serde_json::to_string(&body)
336 .map(|s| s.len() as u64)
337 .unwrap_or(0),
338 error_message: None,
339 metadata,
340 reality_metadata: None,
341 };
342 logger.log_request(log_entry).await;
343 }
344
345 Json(ApiResponse::success(ExecuteResponse {
346 status_code,
347 headers,
348 body: body.clone(),
349 response_time_ms,
350 request_id,
351 error: None,
352 }))
353 }
354 Err(e) => {
355 let error_msg = e.to_string();
356 Json(ApiResponse::success(ExecuteResponse {
357 status_code: 0,
358 headers: HashMap::new(),
359 body: json!({ "error": error_msg }),
360 response_time_ms,
361 request_id,
362 error: Some(error_msg),
363 }))
364 }
365 }
366}
367
368pub async fn execute_graphql_query(
370 State(state): State<AdminState>,
371 axum::extract::Json(request): axum::extract::Json<ExecuteGraphQLRequest>,
372) -> Json<ApiResponse<ExecuteResponse>> {
373 let start_time = std::time::Instant::now();
374 let request_id = uuid::Uuid::new_v4().to_string();
375
376 let base_url = request.base_url.unwrap_or_else(|| {
378 state
379 .graphql_server_addr
380 .map(|addr| format!("http://{}", addr))
381 .unwrap_or_else(|| "http://localhost:4000".to_string())
382 });
383
384 let mut graphql_body = json!({
386 "query": request.query
387 });
388
389 if let Some(variables) = &request.variables {
390 graphql_body["variables"] = json!(variables);
391 }
392
393 if let Some(operation_name) = &request.operation_name {
394 graphql_body["operationName"] = json!(operation_name);
395 }
396
397 let client = reqwest::Client::builder()
399 .timeout(std::time::Duration::from_secs(30))
400 .build()
401 .unwrap_or_else(|_| reqwest::Client::new());
402
403 let url = format!("{}/graphql", base_url);
405 let mut graphql_request = client.post(&url).header("Content-Type", "application/json");
406
407 if let Some(ws_id) = &request.workspace_id {
409 graphql_request = graphql_request.header("X-Workspace-ID", ws_id);
410 }
411
412 let response = graphql_request.json(&graphql_body).send().await;
413
414 let response_time_ms = start_time.elapsed().as_millis() as u64;
415
416 match response {
417 Ok(resp) => {
418 let status_code = resp.status().as_u16();
419
420 let mut headers = HashMap::new();
422 for (key, value) in resp.headers() {
423 if let Ok(value_str) = value.to_str() {
424 headers.insert(key.to_string(), value_str.to_string());
425 }
426 }
427
428 let body = resp
430 .json::<Value>()
431 .await
432 .unwrap_or_else(|_| json!({ "error": "Failed to parse response as JSON" }));
433
434 if let Some(logger) = get_global_logger() {
436 let mut metadata = HashMap::new();
438 if let Some(ws_id) = &request.workspace_id {
439 metadata.insert("workspace_id".to_string(), ws_id.clone());
440 }
441 metadata.insert("query".to_string(), request.query.clone());
442 if let Some(variables) = &request.variables {
443 if let Ok(vars_str) = serde_json::to_string(variables) {
444 metadata.insert("variables".to_string(), vars_str);
445 }
446 }
447
448 let has_errors = body.get("errors").is_some();
449 let log_entry = RequestLogEntry {
450 id: request_id.clone(),
451 timestamp: Utc::now(),
452 server_type: "graphql".to_string(),
453 method: "POST".to_string(),
454 path: "/graphql".to_string(),
455 status_code,
456 response_time_ms,
457 client_ip: None,
458 user_agent: Some("MockForge-Playground".to_string()),
459 headers: HashMap::new(),
460 response_size_bytes: serde_json::to_string(&body)
461 .map(|s| s.len() as u64)
462 .unwrap_or(0),
463 error_message: if has_errors {
464 Some("GraphQL errors in response".to_string())
465 } else {
466 None
467 },
468 reality_metadata: None,
469 metadata: {
470 let mut meta = HashMap::new();
471 meta.insert("query".to_string(), request.query.clone());
472 if let Some(vars) = &request.variables {
473 if let Ok(vars_str) = serde_json::to_string(vars) {
474 meta.insert("variables".to_string(), vars_str);
475 }
476 }
477 meta
478 },
479 };
480 logger.log_request(log_entry).await;
481 }
482
483 let has_errors = body.get("errors").is_some();
484 Json(ApiResponse::success(ExecuteResponse {
485 status_code,
486 headers,
487 body: body.clone(),
488 response_time_ms,
489 request_id,
490 error: if has_errors {
491 Some("GraphQL errors in response".to_string())
492 } else {
493 None
494 },
495 }))
496 }
497 Err(e) => {
498 let error_msg = e.to_string();
499 Json(ApiResponse::success(ExecuteResponse {
500 status_code: 0,
501 headers: HashMap::new(),
502 body: json!({ "error": error_msg }),
503 response_time_ms,
504 request_id,
505 error: Some(error_msg),
506 }))
507 }
508 }
509}
510
511pub async fn graphql_introspect(
513 State(state): State<AdminState>,
514) -> Json<ApiResponse<GraphQLIntrospectionResult>> {
515 let base_url = state
517 .graphql_server_addr
518 .map(|addr| format!("http://{}", addr))
519 .unwrap_or_else(|| "http://localhost:4000".to_string());
520
521 let introspection_query = r#"
523 query IntrospectionQuery {
524 __schema {
525 queryType { name }
526 mutationType { name }
527 subscriptionType { name }
528 types {
529 ...FullType
530 }
531 directives {
532 name
533 description
534 locations
535 args {
536 ...InputValue
537 }
538 }
539 }
540 }
541
542 fragment FullType on __Type {
543 kind
544 name
545 description
546 fields(includeDeprecated: true) {
547 name
548 description
549 args {
550 ...InputValue
551 }
552 type {
553 ...TypeRef
554 }
555 isDeprecated
556 deprecationReason
557 }
558 inputFields {
559 ...InputValue
560 }
561 interfaces {
562 ...TypeRef
563 }
564 enumValues(includeDeprecated: true) {
565 name
566 description
567 isDeprecated
568 deprecationReason
569 }
570 possibleTypes {
571 ...TypeRef
572 }
573 }
574
575 fragment InputValue on __InputValue {
576 name
577 description
578 type {
579 ...TypeRef
580 }
581 defaultValue
582 }
583
584 fragment TypeRef on __Type {
585 kind
586 name
587 ofType {
588 kind
589 name
590 ofType {
591 kind
592 name
593 ofType {
594 kind
595 name
596 ofType {
597 kind
598 name
599 ofType {
600 kind
601 name
602 ofType {
603 kind
604 name
605 ofType {
606 kind
607 name
608 }
609 }
610 }
611 }
612 }
613 }
614 }
615 }
616 "#;
617
618 let client = reqwest::Client::builder()
619 .timeout(std::time::Duration::from_secs(30))
620 .build()
621 .unwrap_or_else(|_| reqwest::Client::new());
622
623 let url = format!("{}/graphql", base_url);
624 let response = client
625 .post(&url)
626 .header("Content-Type", "application/json")
627 .json(&json!({
628 "query": introspection_query
629 }))
630 .send()
631 .await;
632
633 match response {
634 Ok(resp) => {
635 if let Ok(body) = resp.json::<Value>().await {
636 if let Some(data) = body.get("data").and_then(|d| d.get("__schema")) {
637 let schema = data.clone();
638
639 let query_types = schema
641 .get("queryType")
642 .and_then(|q| q.get("name"))
643 .and_then(|n| n.as_str())
644 .map(|_| vec!["Query".to_string()])
645 .unwrap_or_default();
646
647 let mutation_types = schema
648 .get("mutationType")
649 .and_then(|m| m.get("name"))
650 .and_then(|n| n.as_str())
651 .map(|_| vec!["Mutation".to_string()])
652 .unwrap_or_default();
653
654 let subscription_types = schema
655 .get("subscriptionType")
656 .and_then(|s| s.get("name"))
657 .and_then(|n| n.as_str())
658 .map(|_| vec!["Subscription".to_string()])
659 .unwrap_or_default();
660
661 Json(ApiResponse::success(GraphQLIntrospectionResult {
662 schema: schema.clone(),
663 query_types,
664 mutation_types,
665 subscription_types,
666 }))
667 } else {
668 Json(ApiResponse::error("Failed to parse introspection response".to_string()))
669 }
670 } else {
671 Json(ApiResponse::error("Failed to parse response".to_string()))
672 }
673 }
674 Err(e) => Json(ApiResponse::error(format!("Failed to execute introspection query: {}", e))),
675 }
676}
677
678pub async fn get_request_history(
680 State(_state): State<AdminState>,
681 axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
682) -> Json<ApiResponse<Vec<PlaygroundHistoryEntry>>> {
683 let logger = match get_global_logger() {
684 Some(logger) => logger,
685 None => {
686 return Json(ApiResponse::error("Request logger not initialized".to_string()));
687 }
688 };
689
690 let limit = params.get("limit").and_then(|l| l.parse::<usize>().ok()).unwrap_or(100);
692
693 let protocol_filter = params.get("protocol");
695
696 let workspace_id_filter = params.get("workspace_id");
698
699 let mut logs = if let Some(protocol) = protocol_filter {
701 logger
702 .get_logs_by_server(protocol, Some(limit * 2)) .await
704 } else {
705 logger.get_recent_logs(Some(limit * 2)).await
706 };
707
708 if let Some(ws_id) = workspace_id_filter {
710 logs.retain(|log| log.metadata.get("workspace_id").map(|w| w == ws_id).unwrap_or(false));
711 }
712
713 logs.truncate(limit);
715
716 let history: Vec<PlaygroundHistoryEntry> = logs
718 .into_iter()
719 .map(|log| {
720 let graphql_query = log.metadata.get("query").cloned();
722 let graphql_variables = log
723 .metadata
724 .get("variables")
725 .and_then(|v| serde_json::from_str::<HashMap<String, Value>>(v).ok());
726
727 PlaygroundHistoryEntry {
728 id: log.id,
729 protocol: log.server_type.clone(),
730 method: log.method.clone(),
731 path: log.path.clone(),
732 status_code: log.status_code,
733 response_time_ms: log.response_time_ms,
734 timestamp: log.timestamp,
735 request_headers: if log.server_type == "http" {
736 Some(log.headers.clone())
737 } else {
738 None
739 },
740 request_body: None, graphql_query,
742 graphql_variables,
743 }
744 })
745 .collect();
746
747 Json(ApiResponse::success(history))
748}
749
750pub async fn replay_request(
752 State(state): State<AdminState>,
753 Path(id): Path<String>,
754) -> Json<ApiResponse<ExecuteResponse>> {
755 let logger = match get_global_logger() {
756 Some(logger) => logger,
757 None => {
758 return Json(ApiResponse::error("Request logger not initialized".to_string()));
759 }
760 };
761
762 let logs = logger.get_recent_logs(None).await;
764 let log_entry = logs.into_iter().find(|log| log.id == id);
765
766 match log_entry {
767 Some(log) => {
768 if log.server_type == "graphql" {
769 if let Some(query) = log.metadata.get("query") {
771 let variables = log
772 .metadata
773 .get("variables")
774 .and_then(|v| serde_json::from_str::<HashMap<String, Value>>(v).ok());
775
776 let graphql_request = ExecuteGraphQLRequest {
777 query: query.clone(),
778 variables,
779 operation_name: None,
780 base_url: None,
781 workspace_id: log.metadata.get("workspace_id").cloned(),
782 };
783
784 execute_graphql_query(State(state), axum::extract::Json(graphql_request)).await
785 } else {
786 Json(ApiResponse::error("GraphQL query not found in log entry".to_string()))
787 }
788 } else {
789 let rest_request = ExecuteRestRequest {
791 method: log.method.clone(),
792 path: log.path.clone(),
793 headers: Some(log.headers.clone()),
794 body: None, base_url: None,
796 use_mockai: false,
797 workspace_id: log.metadata.get("workspace_id").cloned(),
798 };
799
800 execute_rest_request(State(state), axum::extract::Json(rest_request)).await
801 }
802 }
803 None => Json(ApiResponse::error(format!("Request with ID {} not found", id))),
804 }
805}
806
807pub async fn generate_code_snippet(
809 State(_state): State<AdminState>,
810 axum::extract::Json(request): axum::extract::Json<CodeSnippetRequest>,
811) -> Json<ApiResponse<CodeSnippetResponse>> {
812 let mut snippets = HashMap::new();
813
814 if request.protocol == "rest" {
815 let mut curl_parts = vec!["curl".to_string()];
817 if let Some(method) = &request.method {
818 if method != "GET" {
819 curl_parts.push(format!("-X {}", method));
820 }
821 }
822
823 if let Some(headers) = &request.headers {
824 for (key, value) in headers {
825 curl_parts.push(format!("-H \"{}: {}\"", key, value));
826 }
827 }
828
829 if let Some(body) = &request.body {
830 curl_parts.push(format!("-d '{}'", serde_json::to_string(body).unwrap_or_default()));
831 }
832
833 let url = if request.path.starts_with("http") {
834 request.path.clone()
835 } else {
836 format!("{}{}", request.base_url, request.path)
837 };
838 curl_parts.push(format!("\"{}\"", url));
839
840 snippets.insert("curl".to_string(), curl_parts.join(" \\\n "));
841
842 let mut js_code = String::new();
844 js_code.push_str("fetch(");
845 js_code.push_str(&format!("\"{}\"", url));
846 js_code.push_str(", {\n");
847
848 if let Some(method) = &request.method {
849 js_code.push_str(&format!(" method: \"{}\",\n", method));
850 }
851
852 if let Some(headers) = &request.headers {
853 js_code.push_str(" headers: {\n");
854 for (key, value) in headers {
855 js_code.push_str(&format!(" \"{}\": \"{}\",\n", key, value));
856 }
857 js_code.push_str(" },\n");
858 }
859
860 if let Some(body) = &request.body {
861 js_code.push_str(&format!(
862 " body: JSON.stringify({}),\n",
863 serde_json::to_string(body).unwrap_or_default()
864 ));
865 }
866
867 js_code.push_str("})");
868 snippets.insert("javascript".to_string(), js_code);
869
870 let mut python_code = String::new();
872 python_code.push_str("import requests\n\n");
873 python_code.push_str("response = requests.");
874
875 let method = request.method.as_deref().unwrap_or("get").to_lowercase();
876 python_code.push_str(&method);
877 python_code.push_str("(\n");
878 python_code.push_str(&format!(" \"{}\"", url));
879
880 if let Some(headers) = &request.headers {
881 python_code.push_str(",\n headers={\n");
882 for (key, value) in headers {
883 python_code.push_str(&format!(" \"{}\": \"{}\",\n", key, value));
884 }
885 python_code.push_str(" }");
886 }
887
888 if let Some(body) = &request.body {
889 python_code.push_str(",\n json=");
890 python_code.push_str(&serde_json::to_string(body).unwrap_or_default());
891 }
892
893 python_code.push_str("\n)");
894 snippets.insert("python".to_string(), python_code);
895 } else if request.protocol == "graphql" {
896 if let Some(query) = &request.graphql_query {
898 let mut curl_parts = vec!["curl".to_string(), "-X POST".to_string()];
900 curl_parts.push("-H \"Content-Type: application/json\"".to_string());
901
902 let mut graphql_body = json!({ "query": query });
903 if let Some(vars) = &request.graphql_variables {
904 graphql_body["variables"] = json!(vars);
905 }
906
907 curl_parts
908 .push(format!("-d '{}'", serde_json::to_string(&graphql_body).unwrap_or_default()));
909 curl_parts.push(format!("\"{}/graphql\"", request.base_url));
910
911 snippets.insert("curl".to_string(), curl_parts.join(" \\\n "));
912
913 let mut js_code = String::new();
915 js_code.push_str("fetch(\"");
916 js_code.push_str(&format!("{}/graphql", request.base_url));
917 js_code.push_str("\", {\n");
918 js_code.push_str(" method: \"POST\",\n");
919 js_code.push_str(" headers: {\n");
920 js_code.push_str(" \"Content-Type\": \"application/json\",\n");
921 js_code.push_str(" },\n");
922 js_code.push_str(" body: JSON.stringify({\n");
923 js_code.push_str(&format!(" query: `{}`,\n", query.replace('`', "\\`")));
924 if let Some(vars) = &request.graphql_variables {
925 js_code.push_str(" variables: ");
926 js_code.push_str(&serde_json::to_string(vars).unwrap_or_default());
927 js_code.push_str(",\n");
928 }
929 js_code.push_str(" }),\n");
930 js_code.push_str("})");
931 snippets.insert("javascript".to_string(), js_code);
932 }
933 }
934
935 Json(ApiResponse::success(CodeSnippetResponse { snippets }))
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941 use serde_json::json;
942
943 #[test]
944 fn test_code_snippet_generation_rest_get() {
945 let request = CodeSnippetRequest {
946 protocol: "rest".to_string(),
947 method: Some("GET".to_string()),
948 path: "/api/users".to_string(),
949 headers: None,
950 body: None,
951 graphql_query: None,
952 graphql_variables: None,
953 base_url: "http://localhost:3000".to_string(),
954 };
955
956 let serialized = serde_json::to_string(&request).unwrap();
958 assert!(serialized.contains("GET"));
959 assert!(serialized.contains("/api/users"));
960 }
961
962 #[test]
963 fn test_code_snippet_generation_rest_post() {
964 let request = CodeSnippetRequest {
965 protocol: "rest".to_string(),
966 method: Some("POST".to_string()),
967 path: "/api/users".to_string(),
968 headers: Some({
969 let mut h = HashMap::new();
970 h.insert("Content-Type".to_string(), "application/json".to_string());
971 h
972 }),
973 body: Some(json!({ "name": "John" })),
974 graphql_query: None,
975 graphql_variables: None,
976 base_url: "http://localhost:3000".to_string(),
977 };
978
979 let serialized = serde_json::to_string(&request).unwrap();
981 assert!(serialized.contains("POST"));
982 assert!(serialized.contains("Content-Type"));
983 }
984
985 #[test]
986 fn test_code_snippet_generation_graphql() {
987 let request = CodeSnippetRequest {
988 protocol: "graphql".to_string(),
989 method: None,
990 path: "/graphql".to_string(),
991 headers: None,
992 body: None,
993 graphql_query: Some("query { user(id: 1) { name } }".to_string()),
994 graphql_variables: None,
995 base_url: "http://localhost:4000".to_string(),
996 };
997
998 let serialized = serde_json::to_string(&request).unwrap();
1000 assert!(serialized.contains("graphql"));
1001 assert!(serialized.contains("user(id: 1)"));
1002 }
1003
1004 #[test]
1005 fn test_playground_endpoint_serialization() {
1006 let endpoint = PlaygroundEndpoint {
1007 protocol: "rest".to_string(),
1008 method: "GET".to_string(),
1009 path: "/api/users".to_string(),
1010 description: Some("Get users".to_string()),
1011 enabled: true,
1012 };
1013
1014 let serialized = serde_json::to_string(&endpoint).unwrap();
1015 assert!(serialized.contains("rest"));
1016 assert!(serialized.contains("GET"));
1017 assert!(serialized.contains("/api/users"));
1018 }
1019
1020 #[test]
1021 fn test_execute_response_serialization() {
1022 let response = ExecuteResponse {
1023 status_code: 200,
1024 headers: {
1025 let mut h = HashMap::new();
1026 h.insert("Content-Type".to_string(), "application/json".to_string());
1027 h
1028 },
1029 body: json!({ "success": true }),
1030 response_time_ms: 150,
1031 request_id: "test-id".to_string(),
1032 error: None,
1033 };
1034
1035 let serialized = serde_json::to_string(&response).unwrap();
1036 assert!(serialized.contains("200"));
1037 assert!(serialized.contains("test-id"));
1038 }
1039}