1use axum::{
7 extract::{Path, State},
8 response::Json,
9};
10use chrono::Utc;
11use mockforge_core::request_logger::{get_global_logger, RequestLogEntry};
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14use std::collections::HashMap;
15
16use crate::handlers::AdminState;
17use crate::models::ApiResponse;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PlaygroundEndpoint {
22 pub protocol: String,
24 pub method: String,
26 pub path: String,
28 pub description: Option<String>,
30 pub enabled: bool,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ExecuteRestRequest {
37 pub method: String,
39 pub path: String,
41 pub headers: Option<HashMap<String, String>>,
43 pub body: Option<Value>,
45 pub base_url: Option<String>,
47 #[serde(default)]
49 pub use_mockai: bool,
50 pub workspace_id: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ExecuteGraphQLRequest {
57 pub query: String,
59 pub variables: Option<HashMap<String, Value>>,
61 pub operation_name: Option<String>,
63 pub base_url: Option<String>,
65 pub workspace_id: Option<String>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ExecuteResponse {
72 pub status_code: u16,
74 pub headers: HashMap<String, String>,
76 pub body: Value,
78 pub response_time_ms: u64,
80 pub request_id: String,
82 pub error: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct GraphQLIntrospectionResult {
89 pub schema: Value,
91 pub query_types: Vec<String>,
93 pub mutation_types: Vec<String>,
95 pub subscription_types: Vec<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PlaygroundHistoryEntry {
102 pub id: String,
104 pub protocol: String,
106 pub method: String,
108 pub path: String,
110 pub status_code: u16,
112 pub response_time_ms: u64,
114 pub timestamp: chrono::DateTime<chrono::Utc>,
116 pub request_headers: Option<HashMap<String, String>>,
118 pub request_body: Option<Value>,
120 pub graphql_query: Option<String>,
122 pub graphql_variables: Option<HashMap<String, Value>>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CodeSnippetRequest {
129 pub protocol: String,
131 pub method: Option<String>,
133 pub path: String,
135 pub headers: Option<HashMap<String, String>>,
137 pub body: Option<Value>,
139 pub graphql_query: Option<String>,
141 pub graphql_variables: Option<HashMap<String, Value>>,
143 pub base_url: String,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct CodeSnippetResponse {
150 pub snippets: HashMap<String, String>,
152}
153
154pub async fn list_playground_endpoints(
156 State(state): State<AdminState>,
157 axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
158) -> Json<ApiResponse<Vec<PlaygroundEndpoint>>> {
159 let workspace_id = params.get("workspace_id");
161 let mut endpoints = Vec::new();
162
163 if let Some(http_addr) = state.http_server_addr {
165 let mut url = format!("http://{}/__mockforge/routes", http_addr);
166
167 if let Some(ws_id) = workspace_id {
169 url = format!("{}?workspace_id={}", url, ws_id);
170 }
171
172 if let Ok(response) = reqwest::get(&url).await {
173 if response.status().is_success() {
174 if let Ok(body) = response.json::<Value>().await {
175 if let Some(routes) = body.get("routes").and_then(|r| r.as_array()) {
176 for route in routes {
177 if let Some(ws_id) = workspace_id {
179 if let Some(route_workspace) =
180 route.get("workspace_id").and_then(|w| w.as_str())
181 {
182 if route_workspace != ws_id {
183 continue; }
185 }
186 }
187
188 if let (Some(method), Some(path)) = (
189 route.get("method").and_then(|m| m.as_str()),
190 route.get("path").and_then(|p| p.as_str()),
191 ) {
192 endpoints.push(PlaygroundEndpoint {
193 protocol: "rest".to_string(),
194 method: method.to_string(),
195 path: path.to_string(),
196 description: route
197 .get("description")
198 .and_then(|d| d.as_str())
199 .map(|s| s.to_string()),
200 enabled: true,
201 });
202 }
203 }
204 }
205 }
206 }
207 }
208 }
209
210 if state.graphql_server_addr.is_some() {
212 endpoints.push(PlaygroundEndpoint {
213 protocol: "graphql".to_string(),
214 method: "query".to_string(),
215 path: "/graphql".to_string(),
216 description: Some("GraphQL endpoint".to_string()),
217 enabled: true,
218 });
219 }
220
221 Json(ApiResponse::success(endpoints))
222}
223
224pub async fn execute_rest_request(
226 State(state): State<AdminState>,
227 axum::extract::Json(request): axum::extract::Json<ExecuteRestRequest>,
228) -> Json<ApiResponse<ExecuteResponse>> {
229 let start_time = std::time::Instant::now();
230 let request_id = uuid::Uuid::new_v4().to_string();
231
232 let base_url = request.base_url.unwrap_or_else(|| {
234 state
235 .http_server_addr
236 .map(|addr| format!("http://{}", addr))
237 .unwrap_or_else(|| "http://localhost:3000".to_string())
238 });
239
240 let url = if request.path.starts_with("http") {
242 request.path.clone()
243 } else {
244 format!("{}{}", base_url, request.path)
245 };
246
247 let client = reqwest::Client::builder()
249 .timeout(std::time::Duration::from_secs(30))
250 .build()
251 .unwrap_or_else(|_| reqwest::Client::new());
252
253 let mut http_request = match request.method.as_str() {
255 "GET" => client.get(&url),
256 "POST" => client.post(&url),
257 "PUT" => client.put(&url),
258 "DELETE" => client.delete(&url),
259 "PATCH" => client.patch(&url),
260 _ => {
261 return Json(ApiResponse::error(format!(
262 "Unsupported HTTP method: {}",
263 request.method
264 )));
265 }
266 };
267
268 let mut headers = request.headers.clone().unwrap_or_default();
270
271 if request.use_mockai {
273 headers.insert("X-MockAI-Preview".to_string(), "true".to_string());
274 }
275
276 if let Some(ws_id) = &request.workspace_id {
278 headers.insert("X-Workspace-ID".to_string(), ws_id.clone());
279 }
280
281 for (key, value) in &headers {
282 http_request = http_request.header(key, value);
283 }
284
285 if let Some(body) = &request.body {
287 http_request = http_request.json(body);
288 }
289
290 let response = http_request.send().await;
292
293 let response_time_ms = start_time.elapsed().as_millis() as u64;
294
295 match response {
296 Ok(resp) => {
297 let status_code = resp.status().as_u16();
298
299 let mut headers = HashMap::new();
301 for (key, value) in resp.headers() {
302 if let Ok(value_str) = value.to_str() {
303 headers.insert(key.to_string(), value_str.to_string());
304 }
305 }
306
307 let body = resp
309 .json::<Value>()
310 .await
311 .unwrap_or_else(|_| json!({ "error": "Failed to parse response as JSON" }));
312
313 if let Some(logger) = get_global_logger() {
315 let mut metadata = HashMap::new();
317 if let Some(ws_id) =
318 request.workspace_id.as_ref().or_else(|| headers.get("X-Workspace-ID"))
319 {
320 metadata.insert("workspace_id".to_string(), ws_id.clone());
321 }
322
323 let log_entry = RequestLogEntry {
324 id: request_id.clone(),
325 timestamp: Utc::now(),
326 server_type: "http".to_string(),
327 method: request.method.clone(),
328 path: request.path.clone(),
329 status_code,
330 response_time_ms,
331 client_ip: None,
332 user_agent: Some("MockForge-Playground".to_string()),
333 headers: headers.clone(),
334 query_params: HashMap::new(), 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 query_params: HashMap::new(), response_size_bytes: serde_json::to_string(&body)
462 .map(|s| s.len() as u64)
463 .unwrap_or(0),
464 error_message: if has_errors {
465 Some("GraphQL errors in response".to_string())
466 } else {
467 None
468 },
469 reality_metadata: None,
470 metadata: {
471 let mut meta = HashMap::new();
472 meta.insert("query".to_string(), request.query.clone());
473 if let Some(vars) = &request.variables {
474 if let Ok(vars_str) = serde_json::to_string(vars) {
475 meta.insert("variables".to_string(), vars_str);
476 }
477 }
478 meta
479 },
480 };
481 logger.log_request(log_entry).await;
482 }
483
484 let has_errors = body.get("errors").is_some();
485 Json(ApiResponse::success(ExecuteResponse {
486 status_code,
487 headers,
488 body: body.clone(),
489 response_time_ms,
490 request_id,
491 error: if has_errors {
492 Some("GraphQL errors in response".to_string())
493 } else {
494 None
495 },
496 }))
497 }
498 Err(e) => {
499 let error_msg = e.to_string();
500 Json(ApiResponse::success(ExecuteResponse {
501 status_code: 0,
502 headers: HashMap::new(),
503 body: json!({ "error": error_msg }),
504 response_time_ms,
505 request_id,
506 error: Some(error_msg),
507 }))
508 }
509 }
510}
511
512pub async fn graphql_introspect(
514 State(state): State<AdminState>,
515) -> Json<ApiResponse<GraphQLIntrospectionResult>> {
516 let base_url = state
518 .graphql_server_addr
519 .map(|addr| format!("http://{}", addr))
520 .unwrap_or_else(|| "http://localhost:4000".to_string());
521
522 let introspection_query = r#"
524 query IntrospectionQuery {
525 __schema {
526 queryType { name }
527 mutationType { name }
528 subscriptionType { name }
529 types {
530 ...FullType
531 }
532 directives {
533 name
534 description
535 locations
536 args {
537 ...InputValue
538 }
539 }
540 }
541 }
542
543 fragment FullType on __Type {
544 kind
545 name
546 description
547 fields(includeDeprecated: true) {
548 name
549 description
550 args {
551 ...InputValue
552 }
553 type {
554 ...TypeRef
555 }
556 isDeprecated
557 deprecationReason
558 }
559 inputFields {
560 ...InputValue
561 }
562 interfaces {
563 ...TypeRef
564 }
565 enumValues(includeDeprecated: true) {
566 name
567 description
568 isDeprecated
569 deprecationReason
570 }
571 possibleTypes {
572 ...TypeRef
573 }
574 }
575
576 fragment InputValue on __InputValue {
577 name
578 description
579 type {
580 ...TypeRef
581 }
582 defaultValue
583 }
584
585 fragment TypeRef on __Type {
586 kind
587 name
588 ofType {
589 kind
590 name
591 ofType {
592 kind
593 name
594 ofType {
595 kind
596 name
597 ofType {
598 kind
599 name
600 ofType {
601 kind
602 name
603 ofType {
604 kind
605 name
606 ofType {
607 kind
608 name
609 }
610 }
611 }
612 }
613 }
614 }
615 }
616 }
617 "#;
618
619 let client = reqwest::Client::builder()
620 .timeout(std::time::Duration::from_secs(30))
621 .build()
622 .unwrap_or_else(|_| reqwest::Client::new());
623
624 let url = format!("{}/graphql", base_url);
625 let response = client
626 .post(&url)
627 .header("Content-Type", "application/json")
628 .json(&json!({
629 "query": introspection_query
630 }))
631 .send()
632 .await;
633
634 match response {
635 Ok(resp) => {
636 if let Ok(body) = resp.json::<Value>().await {
637 if let Some(data) = body.get("data").and_then(|d| d.get("__schema")) {
638 let schema = data.clone();
639
640 let query_types = schema
642 .get("queryType")
643 .and_then(|q| q.get("name"))
644 .and_then(|n| n.as_str())
645 .map(|_| vec!["Query".to_string()])
646 .unwrap_or_default();
647
648 let mutation_types = schema
649 .get("mutationType")
650 .and_then(|m| m.get("name"))
651 .and_then(|n| n.as_str())
652 .map(|_| vec!["Mutation".to_string()])
653 .unwrap_or_default();
654
655 let subscription_types = schema
656 .get("subscriptionType")
657 .and_then(|s| s.get("name"))
658 .and_then(|n| n.as_str())
659 .map(|_| vec!["Subscription".to_string()])
660 .unwrap_or_default();
661
662 Json(ApiResponse::success(GraphQLIntrospectionResult {
663 schema: schema.clone(),
664 query_types,
665 mutation_types,
666 subscription_types,
667 }))
668 } else {
669 Json(ApiResponse::error("Failed to parse introspection response".to_string()))
670 }
671 } else {
672 Json(ApiResponse::error("Failed to parse response".to_string()))
673 }
674 }
675 Err(e) => Json(ApiResponse::error(format!("Failed to execute introspection query: {}", e))),
676 }
677}
678
679pub async fn get_request_history(
681 State(_state): State<AdminState>,
682 axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
683) -> Json<ApiResponse<Vec<PlaygroundHistoryEntry>>> {
684 let logger = match get_global_logger() {
685 Some(logger) => logger,
686 None => {
687 return Json(ApiResponse::error("Request logger not initialized".to_string()));
688 }
689 };
690
691 let limit = params.get("limit").and_then(|l| l.parse::<usize>().ok()).unwrap_or(100);
693
694 let protocol_filter = params.get("protocol");
696
697 let workspace_id_filter = params.get("workspace_id");
699
700 let mut logs = if let Some(protocol) = protocol_filter {
702 logger
703 .get_logs_by_server(protocol, Some(limit * 2)) .await
705 } else {
706 logger.get_recent_logs(Some(limit * 2)).await
707 };
708
709 if let Some(ws_id) = workspace_id_filter {
711 logs.retain(|log| log.metadata.get("workspace_id").map(|w| w == ws_id).unwrap_or(false));
712 }
713
714 logs.truncate(limit);
716
717 let history: Vec<PlaygroundHistoryEntry> = logs
719 .into_iter()
720 .map(|log| {
721 let graphql_query = log.metadata.get("query").cloned();
723 let graphql_variables = log
724 .metadata
725 .get("variables")
726 .and_then(|v| serde_json::from_str::<HashMap<String, Value>>(v).ok());
727
728 PlaygroundHistoryEntry {
729 id: log.id,
730 protocol: log.server_type.clone(),
731 method: log.method.clone(),
732 path: log.path.clone(),
733 status_code: log.status_code,
734 response_time_ms: log.response_time_ms,
735 timestamp: log.timestamp,
736 request_headers: if log.server_type == "http" {
737 Some(log.headers.clone())
738 } else {
739 None
740 },
741 request_body: None, graphql_query,
743 graphql_variables,
744 }
745 })
746 .collect();
747
748 Json(ApiResponse::success(history))
749}
750
751pub async fn replay_request(
753 State(state): State<AdminState>,
754 Path(id): Path<String>,
755) -> Json<ApiResponse<ExecuteResponse>> {
756 let logger = match get_global_logger() {
757 Some(logger) => logger,
758 None => {
759 return Json(ApiResponse::error("Request logger not initialized".to_string()));
760 }
761 };
762
763 let logs = logger.get_recent_logs(None).await;
765 let log_entry = logs.into_iter().find(|log| log.id == id);
766
767 match log_entry {
768 Some(log) => {
769 if log.server_type == "graphql" {
770 if let Some(query) = log.metadata.get("query") {
772 let variables = log
773 .metadata
774 .get("variables")
775 .and_then(|v| serde_json::from_str::<HashMap<String, Value>>(v).ok());
776
777 let graphql_request = ExecuteGraphQLRequest {
778 query: query.clone(),
779 variables,
780 operation_name: None,
781 base_url: None,
782 workspace_id: log.metadata.get("workspace_id").cloned(),
783 };
784
785 execute_graphql_query(State(state), axum::extract::Json(graphql_request)).await
786 } else {
787 Json(ApiResponse::error("GraphQL query not found in log entry".to_string()))
788 }
789 } else {
790 let rest_request = ExecuteRestRequest {
792 method: log.method.clone(),
793 path: log.path.clone(),
794 headers: Some(log.headers.clone()),
795 body: None, base_url: None,
797 use_mockai: false,
798 workspace_id: log.metadata.get("workspace_id").cloned(),
799 };
800
801 execute_rest_request(State(state), axum::extract::Json(rest_request)).await
802 }
803 }
804 None => Json(ApiResponse::error(format!("Request with ID {} not found", id))),
805 }
806}
807
808pub async fn generate_code_snippet(
810 State(_state): State<AdminState>,
811 axum::extract::Json(request): axum::extract::Json<CodeSnippetRequest>,
812) -> Json<ApiResponse<CodeSnippetResponse>> {
813 let mut snippets = HashMap::new();
814
815 if request.protocol == "rest" {
816 let mut curl_parts = vec!["curl".to_string()];
818 if let Some(method) = &request.method {
819 if method != "GET" {
820 curl_parts.push(format!("-X {}", method));
821 }
822 }
823
824 if let Some(headers) = &request.headers {
825 for (key, value) in headers {
826 curl_parts.push(format!("-H \"{}: {}\"", key, value));
827 }
828 }
829
830 if let Some(body) = &request.body {
831 curl_parts.push(format!("-d '{}'", serde_json::to_string(body).unwrap_or_default()));
832 }
833
834 let url = if request.path.starts_with("http") {
835 request.path.clone()
836 } else {
837 format!("{}{}", request.base_url, request.path)
838 };
839 curl_parts.push(format!("\"{}\"", url));
840
841 snippets.insert("curl".to_string(), curl_parts.join(" \\\n "));
842
843 let mut js_code = String::new();
845 js_code.push_str("fetch(");
846 js_code.push_str(&format!("\"{}\"", url));
847 js_code.push_str(", {\n");
848
849 if let Some(method) = &request.method {
850 js_code.push_str(&format!(" method: \"{}\",\n", method));
851 }
852
853 if let Some(headers) = &request.headers {
854 js_code.push_str(" headers: {\n");
855 for (key, value) in headers {
856 js_code.push_str(&format!(" \"{}\": \"{}\",\n", key, value));
857 }
858 js_code.push_str(" },\n");
859 }
860
861 if let Some(body) = &request.body {
862 js_code.push_str(&format!(
863 " body: JSON.stringify({}),\n",
864 serde_json::to_string(body).unwrap_or_default()
865 ));
866 }
867
868 js_code.push_str("})");
869 snippets.insert("javascript".to_string(), js_code);
870
871 let mut python_code = String::new();
873 python_code.push_str("import requests\n\n");
874 python_code.push_str("response = requests.");
875
876 let method = request.method.as_deref().unwrap_or("get").to_lowercase();
877 python_code.push_str(&method);
878 python_code.push_str("(\n");
879 python_code.push_str(&format!(" \"{}\"", url));
880
881 if let Some(headers) = &request.headers {
882 python_code.push_str(",\n headers={\n");
883 for (key, value) in headers {
884 python_code.push_str(&format!(" \"{}\": \"{}\",\n", key, value));
885 }
886 python_code.push_str(" }");
887 }
888
889 if let Some(body) = &request.body {
890 python_code.push_str(",\n json=");
891 python_code.push_str(&serde_json::to_string(body).unwrap_or_default());
892 }
893
894 python_code.push_str("\n)");
895 snippets.insert("python".to_string(), python_code);
896 } else if request.protocol == "graphql" {
897 if let Some(query) = &request.graphql_query {
899 let mut curl_parts = vec!["curl".to_string(), "-X POST".to_string()];
901 curl_parts.push("-H \"Content-Type: application/json\"".to_string());
902
903 let mut graphql_body = json!({ "query": query });
904 if let Some(vars) = &request.graphql_variables {
905 graphql_body["variables"] = json!(vars);
906 }
907
908 curl_parts
909 .push(format!("-d '{}'", serde_json::to_string(&graphql_body).unwrap_or_default()));
910 curl_parts.push(format!("\"{}/graphql\"", request.base_url));
911
912 snippets.insert("curl".to_string(), curl_parts.join(" \\\n "));
913
914 let mut js_code = String::new();
916 js_code.push_str("fetch(\"");
917 js_code.push_str(&format!("{}/graphql", request.base_url));
918 js_code.push_str("\", {\n");
919 js_code.push_str(" method: \"POST\",\n");
920 js_code.push_str(" headers: {\n");
921 js_code.push_str(" \"Content-Type\": \"application/json\",\n");
922 js_code.push_str(" },\n");
923 js_code.push_str(" body: JSON.stringify({\n");
924 js_code.push_str(&format!(" query: `{}`,\n", query.replace('`', "\\`")));
925 if let Some(vars) = &request.graphql_variables {
926 js_code.push_str(" variables: ");
927 js_code.push_str(&serde_json::to_string(vars).unwrap_or_default());
928 js_code.push_str(",\n");
929 }
930 js_code.push_str(" }),\n");
931 js_code.push_str("})");
932 snippets.insert("javascript".to_string(), js_code);
933 }
934 }
935
936 Json(ApiResponse::success(CodeSnippetResponse { snippets }))
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use serde_json::json;
943
944 #[test]
945 fn test_code_snippet_generation_rest_get() {
946 let request = CodeSnippetRequest {
947 protocol: "rest".to_string(),
948 method: Some("GET".to_string()),
949 path: "/api/users".to_string(),
950 headers: None,
951 body: None,
952 graphql_query: None,
953 graphql_variables: None,
954 base_url: "http://localhost:3000".to_string(),
955 };
956
957 let serialized = serde_json::to_string(&request).unwrap();
959 assert!(serialized.contains("GET"));
960 assert!(serialized.contains("/api/users"));
961 }
962
963 #[test]
964 fn test_code_snippet_generation_rest_post() {
965 let request = CodeSnippetRequest {
966 protocol: "rest".to_string(),
967 method: Some("POST".to_string()),
968 path: "/api/users".to_string(),
969 headers: Some({
970 let mut h = HashMap::new();
971 h.insert("Content-Type".to_string(), "application/json".to_string());
972 h
973 }),
974 body: Some(json!({ "name": "John" })),
975 graphql_query: None,
976 graphql_variables: None,
977 base_url: "http://localhost:3000".to_string(),
978 };
979
980 let serialized = serde_json::to_string(&request).unwrap();
982 assert!(serialized.contains("POST"));
983 assert!(serialized.contains("Content-Type"));
984 }
985
986 #[test]
987 fn test_code_snippet_generation_graphql() {
988 let request = CodeSnippetRequest {
989 protocol: "graphql".to_string(),
990 method: None,
991 path: "/graphql".to_string(),
992 headers: None,
993 body: None,
994 graphql_query: Some("query { user(id: 1) { name } }".to_string()),
995 graphql_variables: None,
996 base_url: "http://localhost:4000".to_string(),
997 };
998
999 let serialized = serde_json::to_string(&request).unwrap();
1001 assert!(serialized.contains("graphql"));
1002 assert!(serialized.contains("user(id: 1)"));
1003 }
1004
1005 #[test]
1006 fn test_playground_endpoint_serialization() {
1007 let endpoint = PlaygroundEndpoint {
1008 protocol: "rest".to_string(),
1009 method: "GET".to_string(),
1010 path: "/api/users".to_string(),
1011 description: Some("Get users".to_string()),
1012 enabled: true,
1013 };
1014
1015 let serialized = serde_json::to_string(&endpoint).unwrap();
1016 assert!(serialized.contains("rest"));
1017 assert!(serialized.contains("GET"));
1018 assert!(serialized.contains("/api/users"));
1019 }
1020
1021 #[test]
1022 fn test_execute_response_serialization() {
1023 let response = ExecuteResponse {
1024 status_code: 200,
1025 headers: {
1026 let mut h = HashMap::new();
1027 h.insert("Content-Type".to_string(), "application/json".to_string());
1028 h
1029 },
1030 body: json!({ "success": true }),
1031 response_time_ms: 150,
1032 request_id: "test-id".to_string(),
1033 error: None,
1034 };
1035
1036 let serialized = serde_json::to_string(&response).unwrap();
1037 assert!(serialized.contains("200"));
1038 assert!(serialized.contains("test-id"));
1039 }
1040}