1use axum::{
6 extract::{Path, State},
7 http::StatusCode,
8 response::{IntoResponse, Json},
9 routing::{get, post},
10 Router,
11};
12use mockforge_core::config::ServerConfig;
13use mockforge_core::import::asyncapi_import::import_asyncapi_spec;
14use mockforge_core::import::openapi_import::import_openapi_spec;
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use tokio::sync::RwLock;
18use tracing::*;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct EndpointConfig {
23 pub id: String,
25 pub protocol: Protocol,
27 pub name: String,
29 pub description: Option<String>,
31 pub enabled: bool,
33 pub config: EndpointProtocolConfig,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39#[serde(rename_all = "lowercase")]
40pub enum Protocol {
41 Http,
43 Grpc,
45 Websocket,
47 Graphql,
49 Mqtt,
51 Smtp,
53 Kafka,
55 Amqp,
57 Ftp,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(tag = "type")]
64pub enum EndpointProtocolConfig {
65 Http(HttpEndpointConfig),
67 Grpc(GrpcEndpointConfig),
69 Websocket(WebsocketEndpointConfig),
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct HttpEndpointConfig {
76 pub method: String,
78 pub path: String,
80 pub request: Option<HttpRequestConfig>,
82 pub response: HttpResponseConfig,
84 pub behavior: Option<EndpointBehavior>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct HttpRequestConfig {
91 pub validation: Option<ValidationConfig>,
93 pub headers: Option<Vec<HeaderConfig>>,
95 pub query_params: Option<Vec<QueryParamConfig>>,
97 pub body_schema: Option<serde_json::Value>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct HttpResponseConfig {
104 pub status: u16,
106 pub headers: Option<Vec<HeaderConfig>>,
108 pub body: ResponseBody,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "type")]
115pub enum ResponseBody {
116 Static {
118 content: serde_json::Value,
120 },
121 Template {
123 template: String,
125 },
126 Faker {
128 schema: serde_json::Value,
130 },
131 AI {
133 prompt: String,
135 },
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct HeaderConfig {
141 pub name: String,
143 pub value: String,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct QueryParamConfig {
150 pub name: String,
152 pub required: bool,
154 pub schema: Option<serde_json::Value>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ValidationConfig {
161 pub mode: ValidationMode,
163 pub schema: Option<serde_json::Value>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "lowercase")]
170pub enum ValidationMode {
171 Off,
173 Warn,
175 Enforce,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct EndpointBehavior {
182 pub latency: Option<LatencyConfig>,
184 pub failure: Option<FailureConfig>,
186 pub traffic_shaping: Option<TrafficShapingConfig>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct LatencyConfig {
193 pub base_ms: u64,
195 pub jitter_ms: u64,
197 pub distribution: LatencyDistribution,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "lowercase")]
204pub enum LatencyDistribution {
205 Fixed,
207 Normal {
209 std_dev_ms: f64,
211 },
212 Pareto {
214 shape: f64,
216 },
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct FailureConfig {
222 pub error_rate: f64,
224 pub status_codes: Vec<u16>,
226 pub error_message: Option<String>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct TrafficShapingConfig {
233 pub bandwidth_limit_bps: Option<u64>,
235 pub packet_loss_rate: Option<f64>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct GrpcEndpointConfig {
242 pub service: String,
244 pub method: String,
246 pub proto_file: String,
248 pub request_type: String,
250 pub response_type: String,
252 pub response: GrpcResponseConfig,
254 pub behavior: Option<EndpointBehavior>,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct GrpcResponseConfig {
261 pub body: ResponseBody,
263 pub metadata: Option<Vec<HeaderConfig>>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct WebsocketEndpointConfig {
270 pub path: String,
272 pub on_connect: Option<WebsocketAction>,
274 pub on_message: Option<WebsocketAction>,
276 pub on_disconnect: Option<WebsocketAction>,
278 pub behavior: Option<EndpointBehavior>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284#[serde(tag = "type")]
285pub enum WebsocketAction {
286 Send {
288 message: ResponseBody,
290 },
291 Broadcast {
293 message: ResponseBody,
295 },
296 Echo,
298 Close {
300 code: u16,
302 reason: String,
304 },
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ValidationResult {
310 pub valid: bool,
312 pub errors: Vec<ValidationError>,
314 pub warnings: Vec<String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ValidationError {
321 pub field: String,
323 pub message: String,
325}
326
327#[derive(Clone)]
329pub struct UIBuilderState {
330 pub endpoints: Arc<RwLock<Vec<EndpointConfig>>>,
332 pub server_config: Arc<RwLock<ServerConfig>>,
334}
335
336impl UIBuilderState {
337 pub fn new(server_config: ServerConfig) -> Self {
342 Self {
343 endpoints: Arc::new(RwLock::new(Vec::new())),
344 server_config: Arc::new(RwLock::new(server_config)),
345 }
346 }
347}
348
349async fn list_endpoints(State(state): State<UIBuilderState>) -> Json<serde_json::Value> {
351 let endpoints = state.endpoints.read().await;
352 Json(serde_json::json!({
353 "endpoints": *endpoints,
354 "total": endpoints.len(),
355 "enabled": endpoints.iter().filter(|e| e.enabled).count(),
356 "by_protocol": {
357 "http": endpoints.iter().filter(|e| e.protocol == Protocol::Http).count(),
358 "grpc": endpoints.iter().filter(|e| e.protocol == Protocol::Grpc).count(),
359 "websocket": endpoints.iter().filter(|e| e.protocol == Protocol::Websocket).count(),
360 }
361 }))
362}
363
364async fn get_endpoint(
366 State(state): State<UIBuilderState>,
367 Path(id): Path<String>,
368) -> Result<Json<EndpointConfig>, StatusCode> {
369 let endpoints = state.endpoints.read().await;
370 endpoints
371 .iter()
372 .find(|e| e.id == id)
373 .cloned()
374 .map(Json)
375 .ok_or(StatusCode::NOT_FOUND)
376}
377
378async fn create_endpoint(
380 State(state): State<UIBuilderState>,
381 Json(mut endpoint): Json<EndpointConfig>,
382) -> Result<Json<EndpointConfig>, StatusCode> {
383 let mut endpoints = state.endpoints.write().await;
384
385 if endpoint.id.is_empty() {
387 endpoint.id = uuid::Uuid::new_v4().to_string();
388 }
389
390 if endpoints.iter().any(|e| e.id == endpoint.id) {
392 return Err(StatusCode::CONFLICT);
393 }
394
395 info!(
396 endpoint_id = %endpoint.id,
397 protocol = ?endpoint.protocol,
398 "Creating new endpoint"
399 );
400
401 endpoints.push(endpoint.clone());
402 Ok(Json(endpoint))
403}
404
405async fn update_endpoint(
407 State(state): State<UIBuilderState>,
408 Path(id): Path<String>,
409 Json(updated): Json<EndpointConfig>,
410) -> Result<Json<EndpointConfig>, StatusCode> {
411 let mut endpoints = state.endpoints.write().await;
412
413 let endpoint = endpoints.iter_mut().find(|e| e.id == id).ok_or(StatusCode::NOT_FOUND)?;
414
415 info!(
416 endpoint_id = %id,
417 protocol = ?updated.protocol,
418 "Updating endpoint"
419 );
420
421 *endpoint = updated.clone();
422 Ok(Json(updated))
423}
424
425async fn delete_endpoint(
427 State(state): State<UIBuilderState>,
428 Path(id): Path<String>,
429) -> Result<StatusCode, StatusCode> {
430 let mut endpoints = state.endpoints.write().await;
431
432 let index = endpoints.iter().position(|e| e.id == id).ok_or(StatusCode::NOT_FOUND)?;
433
434 info!(endpoint_id = %id, "Deleting endpoint");
435
436 endpoints.remove(index);
437 Ok(StatusCode::NO_CONTENT)
438}
439
440async fn validate_endpoint(
442 State(_state): State<UIBuilderState>,
443 Json(endpoint): Json<EndpointConfig>,
444) -> Json<ValidationResult> {
445 let mut errors = Vec::new();
446 let mut warnings = Vec::new();
447
448 match &endpoint.config {
450 EndpointProtocolConfig::Http(http_config) => {
451 let valid_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
453 if !valid_methods.contains(&http_config.method.to_uppercase().as_str()) {
454 errors.push(ValidationError {
455 field: "method".to_string(),
456 message: format!("Invalid HTTP method: {}", http_config.method),
457 });
458 }
459
460 if !http_config.path.starts_with('/') {
462 errors.push(ValidationError {
463 field: "path".to_string(),
464 message: "Path must start with /".to_string(),
465 });
466 }
467
468 if !(100..600).contains(&http_config.response.status) {
470 errors.push(ValidationError {
471 field: "status".to_string(),
472 message: "Status code must be between 100 and 599".to_string(),
473 });
474 }
475 }
476 EndpointProtocolConfig::Grpc(grpc_config) => {
477 if grpc_config.service.is_empty() {
479 errors.push(ValidationError {
480 field: "service".to_string(),
481 message: "Service name is required".to_string(),
482 });
483 }
484 if grpc_config.method.is_empty() {
485 errors.push(ValidationError {
486 field: "method".to_string(),
487 message: "Method name is required".to_string(),
488 });
489 }
490 }
491 EndpointProtocolConfig::Websocket(ws_config) => {
492 if !ws_config.path.starts_with('/') {
494 errors.push(ValidationError {
495 field: "path".to_string(),
496 message: "Path must start with /".to_string(),
497 });
498 }
499 }
500 }
501
502 if let Some(EndpointProtocolConfig::Http(http_config)) = Some(&endpoint.config) {
504 if let Some(behavior) = &http_config.behavior {
505 if let Some(failure) = &behavior.failure {
506 if failure.error_rate > 0.5 {
507 warnings.push("High error rate configured (>50%)".to_string());
508 }
509 }
510 }
511 }
512
513 Json(ValidationResult {
514 valid: errors.is_empty(),
515 errors,
516 warnings,
517 })
518}
519
520async fn export_config(
522 State(state): State<UIBuilderState>,
523) -> Result<impl IntoResponse, StatusCode> {
524 let server_config = state.server_config.read().await;
525
526 match serde_yaml::to_string(&*server_config) {
527 Ok(yaml) => Ok((StatusCode::OK, [("Content-Type", "application/x-yaml")], yaml)),
528 Err(e) => {
529 error!(error = %e, "Failed to serialize config to YAML");
530 Err(StatusCode::INTERNAL_SERVER_ERROR)
531 }
532 }
533}
534
535#[derive(Debug, Deserialize)]
537struct ImportRequest {
538 config: String,
539 format: ConfigFormat,
540}
541
542#[derive(Debug, Deserialize)]
543#[serde(rename_all = "lowercase")]
544enum ConfigFormat {
545 Yaml,
546 Json,
547}
548
549async fn import_config(
550 State(state): State<UIBuilderState>,
551 Json(request): Json<ImportRequest>,
552) -> Result<Json<serde_json::Value>, StatusCode> {
553 let config: ServerConfig = match request.format {
554 ConfigFormat::Yaml => serde_yaml::from_str(&request.config).map_err(|e| {
555 error!(error = %e, "Failed to parse YAML config");
556 StatusCode::BAD_REQUEST
557 })?,
558 ConfigFormat::Json => serde_json::from_str(&request.config).map_err(|e| {
559 error!(error = %e, "Failed to parse JSON config");
560 StatusCode::BAD_REQUEST
561 })?,
562 };
563
564 let mut server_config = state.server_config.write().await;
565 *server_config = config;
566
567 info!("Configuration imported successfully");
568
569 Ok(Json(serde_json::json!({
570 "success": true,
571 "message": "Configuration imported successfully"
572 })))
573}
574
575async fn get_config(State(state): State<UIBuilderState>) -> Json<ServerConfig> {
577 let config = state.server_config.read().await;
578 Json(config.clone())
579}
580
581async fn update_config(
583 State(state): State<UIBuilderState>,
584 Json(new_config): Json<ServerConfig>,
585) -> Result<Json<ServerConfig>, StatusCode> {
586 let mut config = state.server_config.write().await;
587 *config = new_config.clone();
588
589 info!("Server configuration updated");
590
591 Ok(Json(new_config))
592}
593
594#[derive(Debug, Deserialize)]
596struct ImportOpenApiRequest {
597 content: String,
598 base_url: Option<String>,
599 auto_enable: Option<bool>,
600}
601
602#[derive(Debug, Serialize)]
604struct ImportOpenApiResponse {
605 success: bool,
606 endpoints_created: usize,
607 warnings: Vec<String>,
608 spec_info: OpenApiSpecInfoResponse,
609}
610
611#[derive(Debug, Serialize)]
612struct OpenApiSpecInfoResponse {
613 title: String,
614 version: String,
615 description: Option<String>,
616 openapi_version: String,
617 servers: Vec<String>,
618}
619
620async fn import_openapi_spec_handler(
622 State(state): State<UIBuilderState>,
623 Json(request): Json<ImportOpenApiRequest>,
624) -> Result<Json<ImportOpenApiResponse>, (StatusCode, Json<serde_json::Value>)> {
625 info!("Importing OpenAPI specification");
626
627 let import_result = import_openapi_spec(&request.content, request.base_url.as_deref())
629 .map_err(|e| {
630 error!(error = %e, "Failed to import OpenAPI spec");
631 (
632 StatusCode::BAD_REQUEST,
633 Json(serde_json::json!({
634 "error": "Failed to import OpenAPI specification",
635 "details": e
636 })),
637 )
638 })?;
639
640 let auto_enable = request.auto_enable.unwrap_or(true);
641 let mut endpoints = state.endpoints.write().await;
642 let mut created_count = 0;
643
644 for route in import_result.routes {
646 let endpoint_id = uuid::Uuid::new_v4().to_string();
647
648 let response_body = ResponseBody::Static {
650 content: route.response.body,
651 };
652
653 let response_headers: Option<Vec<HeaderConfig>> = if route.response.headers.is_empty() {
655 None
656 } else {
657 Some(
658 route
659 .response
660 .headers
661 .into_iter()
662 .map(|(name, value)| HeaderConfig { name, value })
663 .collect(),
664 )
665 };
666
667 let endpoint = EndpointConfig {
668 id: endpoint_id.clone(),
669 protocol: Protocol::Http,
670 name: format!("{} {}", route.method.to_uppercase(), route.path),
671 description: Some(format!(
672 "Auto-generated from OpenAPI spec: {} v{}",
673 import_result.spec_info.title, import_result.spec_info.version
674 )),
675 enabled: auto_enable,
676 config: EndpointProtocolConfig::Http(HttpEndpointConfig {
677 method: route.method.to_uppercase(),
678 path: route.path,
679 request: None,
680 response: HttpResponseConfig {
681 status: route.response.status,
682 headers: response_headers,
683 body: response_body,
684 },
685 behavior: None,
686 }),
687 };
688
689 info!(
690 endpoint_id = %endpoint_id,
691 method = %endpoint.name,
692 "Created endpoint from OpenAPI spec"
693 );
694
695 endpoints.push(endpoint);
696 created_count += 1;
697 }
698
699 Ok(Json(ImportOpenApiResponse {
700 success: true,
701 endpoints_created: created_count,
702 warnings: import_result.warnings,
703 spec_info: OpenApiSpecInfoResponse {
704 title: import_result.spec_info.title,
705 version: import_result.spec_info.version,
706 description: import_result.spec_info.description,
707 openapi_version: import_result.spec_info.openapi_version,
708 servers: import_result.spec_info.servers,
709 },
710 }))
711}
712
713async fn export_openapi_spec_handler(
715 State(state): State<UIBuilderState>,
716) -> Result<impl IntoResponse, StatusCode> {
717 let endpoints = state.endpoints.read().await;
718 let server_config = state.server_config.read().await;
719
720 let http_host = &server_config.http.host;
722 let http_port = server_config.http.port;
723
724 let mut spec = serde_json::json!({
725 "openapi": "3.0.0",
726 "info": {
727 "title": "MockForge Generated API",
728 "version": "1.0.0",
729 "description": "API specification generated from MockForge endpoints"
730 },
731 "servers": [
732 {
733 "url": format!("http://{}:{}", http_host, http_port)
734 }
735 ],
736 "paths": {}
737 });
738
739 let paths = spec["paths"].as_object_mut().unwrap();
740
741 for endpoint in endpoints.iter() {
743 if endpoint.protocol != Protocol::Http {
744 continue; }
746
747 if let EndpointProtocolConfig::Http(http_config) = &endpoint.config {
748 let path = &http_config.path;
749
750 if !paths.contains_key(path) {
751 paths.insert(path.clone(), serde_json::json!({}));
752 }
753
754 let method = http_config.method.to_lowercase();
755
756 let response_body_content = match &http_config.response.body {
758 ResponseBody::Static { content } => content.clone(),
759 ResponseBody::Template { template } => serde_json::json!({
760 "type": "string",
761 "example": template
762 }),
763 ResponseBody::Faker { schema } => schema.clone(),
764 ResponseBody::AI { prompt } => serde_json::json!({
765 "type": "string",
766 "description": prompt
767 }),
768 };
769
770 let operation = serde_json::json!({
771 "summary": &endpoint.name,
772 "description": endpoint.description.as_ref().unwrap_or(&String::new()),
773 "operationId": &endpoint.id,
774 "responses": {
775 http_config.response.status.to_string(): {
776 "description": format!("Response with status {}", http_config.response.status),
777 "content": {
778 "application/json": {
779 "schema": {
780 "type": "object"
781 },
782 "example": response_body_content
783 }
784 }
785 }
786 }
787 });
788
789 paths[path][&method] = operation;
790 }
791 }
792
793 match serde_json::to_string_pretty(&spec) {
794 Ok(json) => Ok((StatusCode::OK, [("Content-Type", "application/json")], json)),
795 Err(e) => {
796 error!(error = %e, "Failed to serialize OpenAPI spec to JSON");
797 Err(StatusCode::INTERNAL_SERVER_ERROR)
798 }
799 }
800}
801
802#[derive(Debug, Deserialize)]
804struct ImportAsyncApiRequest {
805 content: String,
806 base_url: Option<String>,
807 auto_enable: Option<bool>,
808}
809
810#[derive(Debug, Serialize)]
812struct ImportAsyncApiResponse {
813 success: bool,
814 endpoints_created: usize,
815 warnings: Vec<String>,
816 spec_info: AsyncApiSpecInfoResponse,
817}
818
819#[derive(Debug, Serialize)]
820struct AsyncApiSpecInfoResponse {
821 title: String,
822 version: String,
823 description: Option<String>,
824 asyncapi_version: String,
825 servers: Vec<String>,
826}
827
828async fn import_asyncapi_spec_handler(
830 State(state): State<UIBuilderState>,
831 Json(request): Json<ImportAsyncApiRequest>,
832) -> Result<Json<ImportAsyncApiResponse>, (StatusCode, Json<serde_json::Value>)> {
833 info!("Importing AsyncAPI specification");
834
835 let import_result = import_asyncapi_spec(&request.content, request.base_url.as_deref())
837 .map_err(|e| {
838 error!(error = %e, "Failed to import AsyncAPI spec");
839 (
840 StatusCode::BAD_REQUEST,
841 Json(serde_json::json!({
842 "error": "Failed to import AsyncAPI specification",
843 "details": e
844 })),
845 )
846 })?;
847
848 let auto_enable = request.auto_enable.unwrap_or(true);
849 let mut endpoints = state.endpoints.write().await;
850 let mut created_count = 0;
851
852 for channel in import_result.channels {
854 let endpoint_id = uuid::Uuid::new_v4().to_string();
855
856 let (protocol, config) = match channel.protocol {
858 mockforge_core::import::asyncapi_import::ChannelProtocol::Websocket => {
859 let on_message = if let Some(op) = channel.operations.first() {
861 if let Some(example) = &op.example_message {
862 Some(WebsocketAction::Send {
863 message: ResponseBody::Static {
864 content: example.clone(),
865 },
866 })
867 } else {
868 Some(WebsocketAction::Echo)
869 }
870 } else {
871 Some(WebsocketAction::Echo)
872 };
873
874 (
875 Protocol::Websocket,
876 EndpointProtocolConfig::Websocket(WebsocketEndpointConfig {
877 path: channel.path.clone(),
878 on_connect: None,
879 on_message,
880 on_disconnect: None,
881 behavior: None,
882 }),
883 )
884 }
885 _ => {
886 warn!(
889 "Skipping channel '{}' with protocol {:?} - not yet supported for UI Builder",
890 channel.name, channel.protocol
891 );
892 continue;
893 }
894 };
895
896 let endpoint = EndpointConfig {
897 id: endpoint_id.clone(),
898 protocol,
899 name: format!("{} - {}", channel.name, channel.path),
900 description: channel.description.or_else(|| {
901 Some(format!(
902 "Auto-generated from AsyncAPI spec: {} v{}",
903 import_result.spec_info.title, import_result.spec_info.version
904 ))
905 }),
906 enabled: auto_enable,
907 config,
908 };
909
910 info!(
911 endpoint_id = %endpoint_id,
912 name = %endpoint.name,
913 "Created endpoint from AsyncAPI spec"
914 );
915
916 endpoints.push(endpoint);
917 created_count += 1;
918 }
919
920 Ok(Json(ImportAsyncApiResponse {
921 success: true,
922 endpoints_created: created_count,
923 warnings: import_result.warnings,
924 spec_info: AsyncApiSpecInfoResponse {
925 title: import_result.spec_info.title,
926 version: import_result.spec_info.version,
927 description: import_result.spec_info.description,
928 asyncapi_version: import_result.spec_info.asyncapi_version,
929 servers: import_result.spec_info.servers,
930 },
931 }))
932}
933
934pub async fn resolve_response_body_tokens(
936 body: &ResponseBody,
937) -> Result<serde_json::Value, String> {
938 use crate::token_response::resolve_response_tokens;
939
940 match body {
941 ResponseBody::Static { content } => resolve_response_tokens(content.clone()).await,
942 ResponseBody::Template { template } => {
943 let value = serde_json::Value::String(template.clone());
945 resolve_response_tokens(value).await
946 }
947 ResponseBody::Faker { schema } => {
948 resolve_response_tokens(schema.clone()).await
950 }
951 ResponseBody::AI { prompt: _ } => {
952 Ok(serde_json::json!({"_ai_prompt": true}))
955 }
956 }
957}
958
959pub fn create_ui_builder_router(state: UIBuilderState) -> Router {
961 Router::new()
962 .route("/endpoints", get(list_endpoints).post(create_endpoint))
963 .route(
964 "/endpoints/{id}",
965 get(get_endpoint).put(update_endpoint).delete(delete_endpoint),
966 )
967 .route("/endpoints/validate", post(validate_endpoint))
968 .route("/config", get(get_config).put(update_config))
969 .route("/config/export", get(export_config))
970 .route("/config/import", post(import_config))
971 .route("/openapi/import", post(import_openapi_spec_handler))
972 .route("/openapi/export", get(export_openapi_spec_handler))
973 .route("/asyncapi/import", post(import_asyncapi_spec_handler))
974 .with_state(state)
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn test_endpoint_serialization() {
983 let endpoint = EndpointConfig {
984 id: "test-1".to_string(),
985 protocol: Protocol::Http,
986 name: "Test Endpoint".to_string(),
987 description: Some("A test endpoint".to_string()),
988 enabled: true,
989 config: EndpointProtocolConfig::Http(HttpEndpointConfig {
990 method: "GET".to_string(),
991 path: "/test".to_string(),
992 request: None,
993 response: HttpResponseConfig {
994 status: 200,
995 headers: None,
996 body: ResponseBody::Static {
997 content: serde_json::json!({"message": "Hello"}),
998 },
999 },
1000 behavior: None,
1001 }),
1002 };
1003
1004 let json = serde_json::to_string(&endpoint).unwrap();
1005 let deserialized: EndpointConfig = serde_json::from_str(&json).unwrap();
1006
1007 assert_eq!(endpoint.id, deserialized.id);
1008 assert_eq!(endpoint.protocol, deserialized.protocol);
1009 }
1010
1011 #[test]
1012 fn test_validation() {
1013 let endpoint = EndpointConfig {
1015 id: "test-1".to_string(),
1016 protocol: Protocol::Http,
1017 name: "Test".to_string(),
1018 description: None,
1019 enabled: true,
1020 config: EndpointProtocolConfig::Http(HttpEndpointConfig {
1021 method: "INVALID".to_string(),
1022 path: "/test".to_string(),
1023 request: None,
1024 response: HttpResponseConfig {
1025 status: 200,
1026 headers: None,
1027 body: ResponseBody::Static {
1028 content: serde_json::json!({}),
1029 },
1030 },
1031 behavior: None,
1032 }),
1033 };
1034
1035 assert_eq!(endpoint.protocol, Protocol::Http);
1037 }
1038}