1use crate::{
4 diff::ComparisonResult,
5 har_export::export_to_har,
6 integration_testing::{IntegrationTestGenerator, IntegrationWorkflow, WorkflowSetup},
7 models::RecordedExchange,
8 query::{execute_query, QueryFilter, QueryResult},
9 recorder::Recorder,
10 replay::ReplayEngine,
11 stub_mapping::{StubFormat, StubMappingConverter},
12 sync::{SyncConfig, SyncService, SyncStatus},
13 test_generation::{LlmConfig, TestFormat, TestGenerationConfig, TestGenerator},
14};
15use axum::{
16 extract::{Path, Query, State},
17 http::StatusCode,
18 response::{IntoResponse, Json, Response},
19 routing::{delete, get, post},
20 Router,
21};
22use serde::{Deserialize, Serialize};
23use std::sync::Arc;
24use tracing::{debug, error};
25
26#[derive(Clone)]
28pub struct ApiState {
29 pub recorder: Arc<Recorder>,
30 pub sync_service: Option<Arc<SyncService>>,
31}
32
33pub fn create_api_router(
35 recorder: Arc<Recorder>,
36 sync_service: Option<Arc<SyncService>>,
37) -> Router {
38 let state = ApiState {
39 recorder,
40 sync_service,
41 };
42
43 Router::new()
44 .route("/api/recorder/requests", get(list_requests))
46 .route("/api/recorder/requests/:id", get(get_request))
47 .route("/api/recorder/requests/:id/response", get(get_response))
48 .route("/api/recorder/search", post(search_requests))
49
50 .route("/api/recorder/export/har", get(export_har))
52
53 .route("/api/recorder/status", get(get_status))
55 .route("/api/recorder/enable", post(enable_recording))
56 .route("/api/recorder/disable", post(disable_recording))
57 .route("/api/recorder/clear", delete(clear_recordings))
58
59 .route("/api/recorder/replay/:id", post(replay_request))
61 .route("/api/recorder/compare/:id", post(compare_responses))
62
63 .route("/api/recorder/stats", get(get_statistics))
65
66 .route("/api/recorder/generate-tests", post(generate_tests))
68
69 .route("/api/recorder/workflows", post(create_workflow))
71 .route("/api/recorder/workflows/:id", get(get_workflow))
72 .route("/api/recorder/workflows/:id/generate", post(generate_integration_test))
73
74 .route("/api/recorder/sync/status", get(get_sync_status))
76 .route("/api/recorder/sync/config", get(get_sync_config))
77 .route("/api/recorder/sync/config", post(update_sync_config))
78 .route("/api/recorder/sync/now", post(sync_now))
79 .route("/api/recorder/sync/changes", get(get_sync_changes))
80
81 .route("/api/recorder/convert/:id", post(convert_to_stub))
83 .route("/api/recorder/convert/batch", post(convert_batch))
84
85 .with_state(state)
86}
87
88async fn list_requests(
90 State(state): State<ApiState>,
91 Query(params): Query<ListParams>,
92) -> Result<Json<QueryResult>, ApiError> {
93 let limit = params.limit.unwrap_or(100);
94 let offset = params.offset.unwrap_or(0);
95
96 let filter = QueryFilter {
97 limit: Some(limit),
98 offset: Some(offset),
99 ..Default::default()
100 };
101
102 let result = execute_query(state.recorder.database(), filter).await?;
103 Ok(Json(result))
104}
105
106async fn get_request(
108 State(state): State<ApiState>,
109 Path(id): Path<String>,
110) -> Result<Json<RecordedExchange>, ApiError> {
111 let exchange = state
112 .recorder
113 .database()
114 .get_exchange(&id)
115 .await?
116 .ok_or_else(|| ApiError::NotFound(format!("Request {} not found", id)))?;
117
118 Ok(Json(exchange))
119}
120
121async fn get_response(
123 State(state): State<ApiState>,
124 Path(id): Path<String>,
125) -> Result<Json<serde_json::Value>, ApiError> {
126 let response = state
127 .recorder
128 .database()
129 .get_response(&id)
130 .await?
131 .ok_or_else(|| ApiError::NotFound(format!("Response for request {} not found", id)))?;
132
133 Ok(Json(serde_json::json!({
134 "request_id": response.request_id,
135 "status_code": response.status_code,
136 "headers": serde_json::from_str::<serde_json::Value>(&response.headers)?,
137 "body": response.body,
138 "body_encoding": response.body_encoding,
139 "size_bytes": response.size_bytes,
140 "timestamp": response.timestamp,
141 })))
142}
143
144async fn search_requests(
146 State(state): State<ApiState>,
147 Json(filter): Json<QueryFilter>,
148) -> Result<Json<QueryResult>, ApiError> {
149 let result = execute_query(state.recorder.database(), filter).await?;
150 Ok(Json(result))
151}
152
153async fn export_har(
155 State(state): State<ApiState>,
156 Query(params): Query<ExportParams>,
157) -> Result<Response, ApiError> {
158 let limit = params.limit.unwrap_or(1000);
159
160 let filter = QueryFilter {
161 limit: Some(limit),
162 protocol: Some(crate::models::Protocol::Http), ..Default::default()
164 };
165
166 let result = execute_query(state.recorder.database(), filter).await?;
167 let har = export_to_har(&result.exchanges)?;
168 let har_json = serde_json::to_string_pretty(&har)?;
169
170 Ok((StatusCode::OK, [("content-type", "application/json")], har_json).into_response())
171}
172
173async fn get_status(State(state): State<ApiState>) -> Json<StatusResponse> {
175 let enabled = state.recorder.is_enabled().await;
176 Json(StatusResponse { enabled })
177}
178
179async fn enable_recording(State(state): State<ApiState>) -> Json<StatusResponse> {
181 state.recorder.enable().await;
182 debug!("Recording enabled via API");
183 Json(StatusResponse { enabled: true })
184}
185
186async fn disable_recording(State(state): State<ApiState>) -> Json<StatusResponse> {
188 state.recorder.disable().await;
189 debug!("Recording disabled via API");
190 Json(StatusResponse { enabled: false })
191}
192
193async fn clear_recordings(State(state): State<ApiState>) -> Result<Json<ClearResponse>, ApiError> {
195 state.recorder.database().clear_all().await?;
196 debug!("All recordings cleared via API");
197 Ok(Json(ClearResponse {
198 message: "All recordings cleared".to_string(),
199 }))
200}
201
202async fn replay_request(
204 State(state): State<ApiState>,
205 Path(id): Path<String>,
206) -> Result<Json<serde_json::Value>, ApiError> {
207 let engine = ReplayEngine::new((**state.recorder.database()).clone());
208 let result = engine.replay_request(&id).await?;
209
210 Ok(Json(serde_json::json!({
211 "request_id": result.request_id,
212 "success": result.success,
213 "message": result.message,
214 "original_status": result.original_status,
215 "replay_status": result.replay_status,
216 })))
217}
218
219async fn compare_responses(
221 State(state): State<ApiState>,
222 Path(id): Path<String>,
223 Json(payload): Json<CompareRequest>,
224) -> Result<Json<ComparisonResult>, ApiError> {
225 let engine = ReplayEngine::new((**state.recorder.database()).clone());
226
227 let result = engine
228 .compare_responses(&id, payload.body.as_bytes(), payload.status_code, &payload.headers)
229 .await?;
230
231 Ok(Json(result))
232}
233
234async fn get_statistics(
236 State(state): State<ApiState>,
237) -> Result<Json<StatisticsResponse>, ApiError> {
238 let db = state.recorder.database();
239 let stats = db.get_statistics().await?;
240
241 Ok(Json(StatisticsResponse {
242 total_requests: stats.total_requests,
243 by_protocol: stats.by_protocol,
244 by_status_code: stats.by_status_code,
245 avg_duration_ms: stats.avg_duration_ms,
246 }))
247}
248
249#[derive(Debug, Deserialize)]
252struct ListParams {
253 limit: Option<i32>,
254 offset: Option<i32>,
255}
256
257#[derive(Debug, Deserialize)]
258struct ExportParams {
259 limit: Option<i32>,
260}
261
262#[derive(Debug, Deserialize)]
263struct CompareRequest {
264 status_code: i32,
265 headers: std::collections::HashMap<String, String>,
266 body: String,
267}
268
269#[derive(Debug, Serialize)]
270struct StatusResponse {
271 enabled: bool,
272}
273
274#[derive(Debug, Serialize)]
275struct ClearResponse {
276 message: String,
277}
278
279#[derive(Debug, Serialize)]
280struct StatisticsResponse {
281 total_requests: i64,
282 by_protocol: std::collections::HashMap<String, i64>,
283 by_status_code: std::collections::HashMap<i32, i64>,
284 avg_duration_ms: Option<f64>,
285}
286
287#[derive(Debug)]
290enum ApiError {
291 Database(sqlx::Error),
292 Serialization(serde_json::Error),
293 NotFound(String),
294 InvalidInput(String),
295 Recorder(crate::RecorderError),
296}
297
298impl From<sqlx::Error> for ApiError {
299 fn from(err: sqlx::Error) -> Self {
300 ApiError::Database(err)
301 }
302}
303
304impl From<serde_json::Error> for ApiError {
305 fn from(err: serde_json::Error) -> Self {
306 ApiError::Serialization(err)
307 }
308}
309
310impl From<crate::RecorderError> for ApiError {
311 fn from(err: crate::RecorderError) -> Self {
312 ApiError::Recorder(err)
313 }
314}
315
316impl IntoResponse for ApiError {
317 fn into_response(self) -> Response {
318 let (status, message) = match self {
319 ApiError::Database(e) => {
320 error!("Database error: {}", e);
321 (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e))
322 }
323 ApiError::Serialization(e) => {
324 error!("Serialization error: {}", e);
325 (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))
326 }
327 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
328 ApiError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
329 ApiError::Recorder(e) => {
330 error!("Recorder error: {}", e);
331 (StatusCode::INTERNAL_SERVER_ERROR, format!("Recorder error: {}", e))
332 }
333 };
334
335 (status, Json(serde_json::json!({ "error": message }))).into_response()
336 }
337}
338
339#[derive(Debug, Deserialize)]
341pub struct GenerateTestsRequest {
342 #[serde(default = "default_format")]
344 pub format: String,
345
346 #[serde(flatten)]
348 pub filter: QueryFilter,
349
350 #[serde(default = "default_suite_name")]
352 pub suite_name: String,
353
354 pub base_url: Option<String>,
356
357 #[serde(default)]
359 pub ai_descriptions: bool,
360
361 pub llm_config: Option<LlmConfigRequest>,
363
364 #[serde(default = "default_true")]
366 pub include_assertions: bool,
367
368 #[serde(default = "default_true")]
370 pub validate_body: bool,
371
372 #[serde(default = "default_true")]
374 pub validate_status: bool,
375
376 #[serde(default)]
378 pub validate_headers: bool,
379
380 #[serde(default)]
382 pub validate_timing: bool,
383
384 pub max_duration_ms: Option<u64>,
386}
387
388fn default_format() -> String {
389 "rust_reqwest".to_string()
390}
391
392fn default_suite_name() -> String {
393 "generated_tests".to_string()
394}
395
396fn default_true() -> bool {
397 true
398}
399
400#[derive(Debug, Deserialize)]
402pub struct LlmConfigRequest {
403 pub provider: String,
405 pub api_endpoint: String,
407 pub api_key: Option<String>,
409 pub model: String,
411 #[serde(default = "default_temperature")]
413 pub temperature: f64,
414}
415
416fn default_temperature() -> f64 {
417 0.3
418}
419
420async fn generate_tests(
422 State(state): State<ApiState>,
423 Json(request): Json<GenerateTestsRequest>,
424) -> Result<Json<serde_json::Value>, ApiError> {
425 debug!("Generating tests with format: {}", request.format);
426
427 let test_format = match request.format.as_str() {
429 "rust_reqwest" => TestFormat::RustReqwest,
430 "http_file" => TestFormat::HttpFile,
431 "curl" => TestFormat::Curl,
432 "postman" => TestFormat::Postman,
433 "k6" => TestFormat::K6,
434 "python_pytest" => TestFormat::PythonPytest,
435 "javascript_jest" => TestFormat::JavaScriptJest,
436 "go_test" => TestFormat::GoTest,
437 _ => {
438 return Err(ApiError::NotFound(format!(
439 "Invalid test format: {}. Supported: rust_reqwest, http_file, curl, postman, k6, python_pytest, javascript_jest, go_test",
440 request.format
441 )));
442 }
443 };
444
445 let llm_config = request.llm_config.map(|cfg| LlmConfig {
447 provider: cfg.provider,
448 api_endpoint: cfg.api_endpoint,
449 api_key: cfg.api_key,
450 model: cfg.model,
451 temperature: cfg.temperature,
452 });
453
454 let config = TestGenerationConfig {
456 format: test_format,
457 include_assertions: request.include_assertions,
458 validate_body: request.validate_body,
459 validate_status: request.validate_status,
460 validate_headers: request.validate_headers,
461 validate_timing: request.validate_timing,
462 max_duration_ms: request.max_duration_ms,
463 suite_name: request.suite_name,
464 base_url: request.base_url,
465 ai_descriptions: request.ai_descriptions,
466 llm_config,
467 group_by_endpoint: true,
468 include_setup_teardown: true,
469 generate_fixtures: false,
470 suggest_edge_cases: false,
471 analyze_test_gaps: false,
472 deduplicate_tests: false,
473 optimize_test_order: false,
474 };
475
476 let generator = TestGenerator::from_arc(state.recorder.database().clone(), config);
478
479 let result = generator.generate_from_filter(request.filter).await?;
481
482 Ok(Json(serde_json::json!({
484 "success": true,
485 "metadata": {
486 "suite_name": result.metadata.name,
487 "test_count": result.metadata.test_count,
488 "endpoint_count": result.metadata.endpoint_count,
489 "protocols": result.metadata.protocols,
490 "format": result.metadata.format,
491 "generated_at": result.metadata.generated_at,
492 },
493 "tests": result.tests.iter().map(|t| serde_json::json!({
494 "name": t.name,
495 "description": t.description,
496 "endpoint": t.endpoint,
497 "method": t.method,
498 })).collect::<Vec<_>>(),
499 "test_file": result.test_file,
500 })))
501}
502
503#[derive(Debug, Deserialize)]
507struct CreateWorkflowRequest {
508 workflow: IntegrationWorkflow,
509}
510
511async fn create_workflow(
513 State(_state): State<ApiState>,
514 Json(request): Json<CreateWorkflowRequest>,
515) -> Result<Json<serde_json::Value>, ApiError> {
516 Ok(Json(serde_json::json!({
519 "success": true,
520 "workflow": request.workflow,
521 "message": "Workflow created successfully"
522 })))
523}
524
525async fn get_workflow(
527 State(_state): State<ApiState>,
528 Path(id): Path<String>,
529) -> Result<Json<serde_json::Value>, ApiError> {
530 let workflow = IntegrationWorkflow {
533 id: id.clone(),
534 name: "Sample Workflow".to_string(),
535 description: "A sample integration test workflow".to_string(),
536 steps: vec![],
537 setup: WorkflowSetup::default(),
538 cleanup: vec![],
539 created_at: chrono::Utc::now(),
540 };
541
542 Ok(Json(serde_json::json!({
543 "success": true,
544 "workflow": workflow
545 })))
546}
547
548#[derive(Debug, Deserialize)]
550struct GenerateIntegrationTestRequest {
551 workflow: IntegrationWorkflow,
552 format: String, }
554
555async fn generate_integration_test(
557 State(_state): State<ApiState>,
558 Path(_id): Path<String>,
559 Json(request): Json<GenerateIntegrationTestRequest>,
560) -> Result<Json<serde_json::Value>, ApiError> {
561 let generator = IntegrationTestGenerator::new(request.workflow);
562
563 let test_code = match request.format.as_str() {
564 "rust" => generator.generate_rust_test(),
565 "python" => generator.generate_python_test(),
566 "javascript" | "js" => generator.generate_javascript_test(),
567 _ => return Err(ApiError::InvalidInput(format!("Unsupported format: {}", request.format))),
568 };
569
570 Ok(Json(serde_json::json!({
571 "success": true,
572 "format": request.format,
573 "test_code": test_code,
574 "message": "Integration test generated successfully"
575 })))
576}
577
578async fn get_sync_status(State(state): State<ApiState>) -> Result<Json<SyncStatus>, ApiError> {
582 let sync_service = state
583 .sync_service
584 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
585
586 let status = sync_service.get_status().await;
587 Ok(Json(status))
588}
589
590async fn get_sync_config(State(state): State<ApiState>) -> Result<Json<SyncConfig>, ApiError> {
592 let sync_service = state
593 .sync_service
594 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
595
596 let config = sync_service.get_config().await;
597 Ok(Json(config))
598}
599
600async fn update_sync_config(
602 State(state): State<ApiState>,
603 Json(config): Json<SyncConfig>,
604) -> Result<Json<SyncConfig>, ApiError> {
605 let sync_service = state
606 .sync_service
607 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
608
609 sync_service.update_config(config.clone()).await;
610 Ok(Json(config))
611}
612
613async fn sync_now(State(state): State<ApiState>) -> Result<Json<serde_json::Value>, ApiError> {
615 let sync_service = state
616 .sync_service
617 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
618
619 match sync_service.sync_now().await {
620 Ok((changes, updated)) => Ok(Json(serde_json::json!({
621 "success": true,
622 "changes_detected": changes.len(),
623 "fixtures_updated": updated,
624 "changes": changes,
625 "message": format!("Sync complete: {} changes detected, {} fixtures updated", changes.len(), updated)
626 }))),
627 Err(e) => Err(ApiError::Recorder(e)),
628 }
629}
630
631async fn get_sync_changes(
633 State(state): State<ApiState>,
634) -> Result<Json<serde_json::Value>, ApiError> {
635 let sync_service = state
636 .sync_service
637 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
638
639 let status = sync_service.get_status().await;
640
641 Ok(Json(serde_json::json!({
642 "last_sync": status.last_sync,
643 "last_changes_detected": status.last_changes_detected,
644 "last_fixtures_updated": status.last_fixtures_updated,
645 "last_error": status.last_error,
646 "total_syncs": status.total_syncs,
647 "is_running": status.is_running,
648 })))
649}
650
651#[derive(Debug, Deserialize)]
653struct ConvertRequest {
654 format: Option<String>, detect_dynamic_values: Option<bool>,
656}
657
658async fn convert_to_stub(
659 State(state): State<ApiState>,
660 Path(id): Path<String>,
661 Json(req): Json<ConvertRequest>,
662) -> Result<Json<serde_json::Value>, ApiError> {
663 let exchange = state
664 .recorder
665 .database()
666 .get_exchange(&id)
667 .await?
668 .ok_or_else(|| ApiError::NotFound(format!("Request {} not found", id)))?;
669
670 let detect_dynamic = req.detect_dynamic_values.unwrap_or(true);
671 let converter = StubMappingConverter::new(detect_dynamic);
672 let stub = converter.convert(&exchange)?;
673
674 let format = match req.format.as_deref() {
675 Some("json") => StubFormat::Json,
676 Some("yaml") | None => StubFormat::Yaml,
677 _ => StubFormat::Yaml,
678 };
679
680 let content = converter.to_string(&stub, format)?;
681
682 Ok(Json(serde_json::json!({
683 "request_id": id,
684 "format": match format {
685 StubFormat::Yaml => "yaml",
686 StubFormat::Json => "json",
687 },
688 "stub": stub,
689 "content": content,
690 })))
691}
692
693#[derive(Debug, Deserialize)]
695struct BatchConvertRequest {
696 request_ids: Vec<String>,
697 format: Option<String>,
698 detect_dynamic_values: Option<bool>,
699 deduplicate: Option<bool>,
700}
701
702async fn convert_batch(
703 State(state): State<ApiState>,
704 Json(req): Json<BatchConvertRequest>,
705) -> Result<Json<serde_json::Value>, ApiError> {
706 let detect_dynamic = req.detect_dynamic_values.unwrap_or(true);
707 let converter = StubMappingConverter::new(detect_dynamic);
708
709 let format = match req.format.as_deref() {
710 Some("json") => StubFormat::Json,
711 Some("yaml") | None => StubFormat::Yaml,
712 _ => StubFormat::Yaml,
713 };
714
715 let mut stubs = Vec::new();
716 let mut errors = Vec::new();
717
718 for request_id in &req.request_ids {
719 match state.recorder.database().get_exchange(request_id).await {
720 Ok(Some(exchange)) => match converter.convert(&exchange) {
721 Ok(stub) => {
722 let content = converter.to_string(&stub, format)?;
723 stubs.push(serde_json::json!({
724 "request_id": request_id,
725 "stub": stub,
726 "content": content,
727 }));
728 }
729 Err(e) => {
730 errors.push(format!("Failed to convert {}: {}", request_id, e));
731 }
732 },
733 Ok(None) => {
734 errors.push(format!("Request {} not found", request_id));
735 }
736 Err(e) => {
737 errors.push(format!("Database error for {}: {}", request_id, e));
738 }
739 }
740 }
741
742 if req.deduplicate.unwrap_or(false) {
744 let mut seen = std::collections::HashSet::new();
746 stubs.retain(|stub| {
747 if let Some(id) = stub.get("stub").and_then(|s| s.get("identifier")) {
748 if let Some(id_str) = id.as_str() {
749 return seen.insert(id_str.to_string());
750 }
751 }
752 true
753 });
754 }
755
756 Ok(Json(serde_json::json!({
757 "total": req.request_ids.len(),
758 "converted": stubs.len(),
759 "errors": errors.len(),
760 "stubs": stubs,
761 "errors_list": errors,
762 })))
763}