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 sync::{SyncConfig, SyncService, SyncStatus},
12 test_generation::{LlmConfig, TestFormat, TestGenerationConfig, TestGenerator},
13};
14use axum::{
15 extract::{Path, Query, State},
16 http::StatusCode,
17 response::{IntoResponse, Json, Response},
18 routing::{delete, get, post},
19 Router,
20};
21use serde::{Deserialize, Serialize};
22use std::sync::Arc;
23use tracing::{debug, error};
24
25#[derive(Clone)]
27pub struct ApiState {
28 pub recorder: Arc<Recorder>,
29 pub sync_service: Option<Arc<SyncService>>,
30}
31
32pub fn create_api_router(
34 recorder: Arc<Recorder>,
35 sync_service: Option<Arc<SyncService>>,
36) -> Router {
37 let state = ApiState {
38 recorder,
39 sync_service,
40 };
41
42 Router::new()
43 .route("/api/recorder/requests", get(list_requests))
45 .route("/api/recorder/requests/:id", get(get_request))
46 .route("/api/recorder/requests/:id/response", get(get_response))
47 .route("/api/recorder/search", post(search_requests))
48
49 .route("/api/recorder/export/har", get(export_har))
51
52 .route("/api/recorder/status", get(get_status))
54 .route("/api/recorder/enable", post(enable_recording))
55 .route("/api/recorder/disable", post(disable_recording))
56 .route("/api/recorder/clear", delete(clear_recordings))
57
58 .route("/api/recorder/replay/:id", post(replay_request))
60 .route("/api/recorder/compare/:id", post(compare_responses))
61
62 .route("/api/recorder/stats", get(get_statistics))
64
65 .route("/api/recorder/generate-tests", post(generate_tests))
67
68 .route("/api/recorder/workflows", post(create_workflow))
70 .route("/api/recorder/workflows/:id", get(get_workflow))
71 .route("/api/recorder/workflows/:id/generate", post(generate_integration_test))
72
73 .route("/api/recorder/sync/status", get(get_sync_status))
75 .route("/api/recorder/sync/config", get(get_sync_config))
76 .route("/api/recorder/sync/config", post(update_sync_config))
77 .route("/api/recorder/sync/now", post(sync_now))
78 .route("/api/recorder/sync/changes", get(get_sync_changes))
79
80 .with_state(state)
81}
82
83async fn list_requests(
85 State(state): State<ApiState>,
86 Query(params): Query<ListParams>,
87) -> Result<Json<QueryResult>, ApiError> {
88 let limit = params.limit.unwrap_or(100);
89 let offset = params.offset.unwrap_or(0);
90
91 let filter = QueryFilter {
92 limit: Some(limit),
93 offset: Some(offset),
94 ..Default::default()
95 };
96
97 let result = execute_query(state.recorder.database(), filter).await?;
98 Ok(Json(result))
99}
100
101async fn get_request(
103 State(state): State<ApiState>,
104 Path(id): Path<String>,
105) -> Result<Json<RecordedExchange>, ApiError> {
106 let exchange = state
107 .recorder
108 .database()
109 .get_exchange(&id)
110 .await?
111 .ok_or_else(|| ApiError::NotFound(format!("Request {} not found", id)))?;
112
113 Ok(Json(exchange))
114}
115
116async fn get_response(
118 State(state): State<ApiState>,
119 Path(id): Path<String>,
120) -> Result<Json<serde_json::Value>, ApiError> {
121 let response = state
122 .recorder
123 .database()
124 .get_response(&id)
125 .await?
126 .ok_or_else(|| ApiError::NotFound(format!("Response for request {} not found", id)))?;
127
128 Ok(Json(serde_json::json!({
129 "request_id": response.request_id,
130 "status_code": response.status_code,
131 "headers": serde_json::from_str::<serde_json::Value>(&response.headers)?,
132 "body": response.body,
133 "body_encoding": response.body_encoding,
134 "size_bytes": response.size_bytes,
135 "timestamp": response.timestamp,
136 })))
137}
138
139async fn search_requests(
141 State(state): State<ApiState>,
142 Json(filter): Json<QueryFilter>,
143) -> Result<Json<QueryResult>, ApiError> {
144 let result = execute_query(state.recorder.database(), filter).await?;
145 Ok(Json(result))
146}
147
148async fn export_har(
150 State(state): State<ApiState>,
151 Query(params): Query<ExportParams>,
152) -> Result<Response, ApiError> {
153 let limit = params.limit.unwrap_or(1000);
154
155 let filter = QueryFilter {
156 limit: Some(limit),
157 protocol: Some(crate::models::Protocol::Http), ..Default::default()
159 };
160
161 let result = execute_query(state.recorder.database(), filter).await?;
162 let har = export_to_har(&result.exchanges)?;
163 let har_json = serde_json::to_string_pretty(&har)?;
164
165 Ok((StatusCode::OK, [("content-type", "application/json")], har_json).into_response())
166}
167
168async fn get_status(State(state): State<ApiState>) -> Json<StatusResponse> {
170 let enabled = state.recorder.is_enabled().await;
171 Json(StatusResponse { enabled })
172}
173
174async fn enable_recording(State(state): State<ApiState>) -> Json<StatusResponse> {
176 state.recorder.enable().await;
177 debug!("Recording enabled via API");
178 Json(StatusResponse { enabled: true })
179}
180
181async fn disable_recording(State(state): State<ApiState>) -> Json<StatusResponse> {
183 state.recorder.disable().await;
184 debug!("Recording disabled via API");
185 Json(StatusResponse { enabled: false })
186}
187
188async fn clear_recordings(State(state): State<ApiState>) -> Result<Json<ClearResponse>, ApiError> {
190 state.recorder.database().clear_all().await?;
191 debug!("All recordings cleared via API");
192 Ok(Json(ClearResponse {
193 message: "All recordings cleared".to_string(),
194 }))
195}
196
197async fn replay_request(
199 State(state): State<ApiState>,
200 Path(id): Path<String>,
201) -> Result<Json<serde_json::Value>, ApiError> {
202 let engine = ReplayEngine::new((**state.recorder.database()).clone());
203 let result = engine.replay_request(&id).await?;
204
205 Ok(Json(serde_json::json!({
206 "request_id": result.request_id,
207 "success": result.success,
208 "message": result.message,
209 "original_status": result.original_status,
210 "replay_status": result.replay_status,
211 })))
212}
213
214async fn compare_responses(
216 State(state): State<ApiState>,
217 Path(id): Path<String>,
218 Json(payload): Json<CompareRequest>,
219) -> Result<Json<ComparisonResult>, ApiError> {
220 let engine = ReplayEngine::new((**state.recorder.database()).clone());
221
222 let result = engine
223 .compare_responses(&id, payload.body.as_bytes(), payload.status_code, &payload.headers)
224 .await?;
225
226 Ok(Json(result))
227}
228
229async fn get_statistics(
231 State(state): State<ApiState>,
232) -> Result<Json<StatisticsResponse>, ApiError> {
233 let db = state.recorder.database();
234 let stats = db.get_statistics().await?;
235
236 Ok(Json(StatisticsResponse {
237 total_requests: stats.total_requests,
238 by_protocol: stats.by_protocol,
239 by_status_code: stats.by_status_code,
240 avg_duration_ms: stats.avg_duration_ms,
241 }))
242}
243
244#[derive(Debug, Deserialize)]
247struct ListParams {
248 limit: Option<i32>,
249 offset: Option<i32>,
250}
251
252#[derive(Debug, Deserialize)]
253struct ExportParams {
254 limit: Option<i32>,
255}
256
257#[derive(Debug, Deserialize)]
258struct CompareRequest {
259 status_code: i32,
260 headers: std::collections::HashMap<String, String>,
261 body: String,
262}
263
264#[derive(Debug, Serialize)]
265struct StatusResponse {
266 enabled: bool,
267}
268
269#[derive(Debug, Serialize)]
270struct ClearResponse {
271 message: String,
272}
273
274#[derive(Debug, Serialize)]
275struct StatisticsResponse {
276 total_requests: i64,
277 by_protocol: std::collections::HashMap<String, i64>,
278 by_status_code: std::collections::HashMap<i32, i64>,
279 avg_duration_ms: Option<f64>,
280}
281
282#[derive(Debug)]
285enum ApiError {
286 Database(sqlx::Error),
287 Serialization(serde_json::Error),
288 NotFound(String),
289 InvalidInput(String),
290 Recorder(crate::RecorderError),
291}
292
293impl From<sqlx::Error> for ApiError {
294 fn from(err: sqlx::Error) -> Self {
295 ApiError::Database(err)
296 }
297}
298
299impl From<serde_json::Error> for ApiError {
300 fn from(err: serde_json::Error) -> Self {
301 ApiError::Serialization(err)
302 }
303}
304
305impl From<crate::RecorderError> for ApiError {
306 fn from(err: crate::RecorderError) -> Self {
307 ApiError::Recorder(err)
308 }
309}
310
311impl IntoResponse for ApiError {
312 fn into_response(self) -> Response {
313 let (status, message) = match self {
314 ApiError::Database(e) => {
315 error!("Database error: {}", e);
316 (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e))
317 }
318 ApiError::Serialization(e) => {
319 error!("Serialization error: {}", e);
320 (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))
321 }
322 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
323 ApiError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
324 ApiError::Recorder(e) => {
325 error!("Recorder error: {}", e);
326 (StatusCode::INTERNAL_SERVER_ERROR, format!("Recorder error: {}", e))
327 }
328 };
329
330 (status, Json(serde_json::json!({ "error": message }))).into_response()
331 }
332}
333
334#[derive(Debug, Deserialize)]
336pub struct GenerateTestsRequest {
337 #[serde(default = "default_format")]
339 pub format: String,
340
341 #[serde(flatten)]
343 pub filter: QueryFilter,
344
345 #[serde(default = "default_suite_name")]
347 pub suite_name: String,
348
349 pub base_url: Option<String>,
351
352 #[serde(default)]
354 pub ai_descriptions: bool,
355
356 pub llm_config: Option<LlmConfigRequest>,
358
359 #[serde(default = "default_true")]
361 pub include_assertions: bool,
362
363 #[serde(default = "default_true")]
365 pub validate_body: bool,
366
367 #[serde(default = "default_true")]
369 pub validate_status: bool,
370
371 #[serde(default)]
373 pub validate_headers: bool,
374
375 #[serde(default)]
377 pub validate_timing: bool,
378
379 pub max_duration_ms: Option<u64>,
381}
382
383fn default_format() -> String {
384 "rust_reqwest".to_string()
385}
386
387fn default_suite_name() -> String {
388 "generated_tests".to_string()
389}
390
391fn default_true() -> bool {
392 true
393}
394
395#[derive(Debug, Deserialize)]
397pub struct LlmConfigRequest {
398 pub provider: String,
400 pub api_endpoint: String,
402 pub api_key: Option<String>,
404 pub model: String,
406 #[serde(default = "default_temperature")]
408 pub temperature: f64,
409}
410
411fn default_temperature() -> f64 {
412 0.3
413}
414
415async fn generate_tests(
417 State(state): State<ApiState>,
418 Json(request): Json<GenerateTestsRequest>,
419) -> Result<Json<serde_json::Value>, ApiError> {
420 debug!("Generating tests with format: {}", request.format);
421
422 let test_format = match request.format.as_str() {
424 "rust_reqwest" => TestFormat::RustReqwest,
425 "http_file" => TestFormat::HttpFile,
426 "curl" => TestFormat::Curl,
427 "postman" => TestFormat::Postman,
428 "k6" => TestFormat::K6,
429 "python_pytest" => TestFormat::PythonPytest,
430 "javascript_jest" => TestFormat::JavaScriptJest,
431 "go_test" => TestFormat::GoTest,
432 _ => {
433 return Err(ApiError::NotFound(format!(
434 "Invalid test format: {}. Supported: rust_reqwest, http_file, curl, postman, k6, python_pytest, javascript_jest, go_test",
435 request.format
436 )));
437 }
438 };
439
440 let llm_config = request.llm_config.map(|cfg| LlmConfig {
442 provider: cfg.provider,
443 api_endpoint: cfg.api_endpoint,
444 api_key: cfg.api_key,
445 model: cfg.model,
446 temperature: cfg.temperature,
447 });
448
449 let config = TestGenerationConfig {
451 format: test_format,
452 include_assertions: request.include_assertions,
453 validate_body: request.validate_body,
454 validate_status: request.validate_status,
455 validate_headers: request.validate_headers,
456 validate_timing: request.validate_timing,
457 max_duration_ms: request.max_duration_ms,
458 suite_name: request.suite_name,
459 base_url: request.base_url,
460 ai_descriptions: request.ai_descriptions,
461 llm_config,
462 group_by_endpoint: true,
463 include_setup_teardown: true,
464 generate_fixtures: false,
465 suggest_edge_cases: false,
466 analyze_test_gaps: false,
467 deduplicate_tests: false,
468 optimize_test_order: false,
469 };
470
471 let generator = TestGenerator::from_arc(state.recorder.database().clone(), config);
473
474 let result = generator.generate_from_filter(request.filter).await?;
476
477 Ok(Json(serde_json::json!({
479 "success": true,
480 "metadata": {
481 "suite_name": result.metadata.name,
482 "test_count": result.metadata.test_count,
483 "endpoint_count": result.metadata.endpoint_count,
484 "protocols": result.metadata.protocols,
485 "format": result.metadata.format,
486 "generated_at": result.metadata.generated_at,
487 },
488 "tests": result.tests.iter().map(|t| serde_json::json!({
489 "name": t.name,
490 "description": t.description,
491 "endpoint": t.endpoint,
492 "method": t.method,
493 })).collect::<Vec<_>>(),
494 "test_file": result.test_file,
495 })))
496}
497
498#[derive(Debug, Deserialize)]
502struct CreateWorkflowRequest {
503 workflow: IntegrationWorkflow,
504}
505
506async fn create_workflow(
508 State(_state): State<ApiState>,
509 Json(request): Json<CreateWorkflowRequest>,
510) -> Result<Json<serde_json::Value>, ApiError> {
511 Ok(Json(serde_json::json!({
514 "success": true,
515 "workflow": request.workflow,
516 "message": "Workflow created successfully"
517 })))
518}
519
520async fn get_workflow(
522 State(_state): State<ApiState>,
523 Path(id): Path<String>,
524) -> Result<Json<serde_json::Value>, ApiError> {
525 let workflow = IntegrationWorkflow {
528 id: id.clone(),
529 name: "Sample Workflow".to_string(),
530 description: "A sample integration test workflow".to_string(),
531 steps: vec![],
532 setup: WorkflowSetup::default(),
533 cleanup: vec![],
534 created_at: chrono::Utc::now(),
535 };
536
537 Ok(Json(serde_json::json!({
538 "success": true,
539 "workflow": workflow
540 })))
541}
542
543#[derive(Debug, Deserialize)]
545struct GenerateIntegrationTestRequest {
546 workflow: IntegrationWorkflow,
547 format: String, }
549
550async fn generate_integration_test(
552 State(_state): State<ApiState>,
553 Path(_id): Path<String>,
554 Json(request): Json<GenerateIntegrationTestRequest>,
555) -> Result<Json<serde_json::Value>, ApiError> {
556 let generator = IntegrationTestGenerator::new(request.workflow);
557
558 let test_code = match request.format.as_str() {
559 "rust" => generator.generate_rust_test(),
560 "python" => generator.generate_python_test(),
561 "javascript" | "js" => generator.generate_javascript_test(),
562 _ => return Err(ApiError::InvalidInput(format!("Unsupported format: {}", request.format))),
563 };
564
565 Ok(Json(serde_json::json!({
566 "success": true,
567 "format": request.format,
568 "test_code": test_code,
569 "message": "Integration test generated successfully"
570 })))
571}
572
573async fn get_sync_status(State(state): State<ApiState>) -> Result<Json<SyncStatus>, ApiError> {
577 let sync_service = state
578 .sync_service
579 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
580
581 let status = sync_service.get_status().await;
582 Ok(Json(status))
583}
584
585async fn get_sync_config(State(state): State<ApiState>) -> Result<Json<SyncConfig>, ApiError> {
587 let sync_service = state
588 .sync_service
589 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
590
591 let config = sync_service.get_config().await;
592 Ok(Json(config))
593}
594
595async fn update_sync_config(
597 State(state): State<ApiState>,
598 Json(config): Json<SyncConfig>,
599) -> Result<Json<SyncConfig>, ApiError> {
600 let sync_service = state
601 .sync_service
602 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
603
604 sync_service.update_config(config.clone()).await;
605 Ok(Json(config))
606}
607
608async fn sync_now(State(state): State<ApiState>) -> Result<Json<serde_json::Value>, ApiError> {
610 let sync_service = state
611 .sync_service
612 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
613
614 match sync_service.sync_now().await {
615 Ok((changes, updated)) => Ok(Json(serde_json::json!({
616 "success": true,
617 "changes_detected": changes.len(),
618 "fixtures_updated": updated,
619 "changes": changes,
620 "message": format!("Sync complete: {} changes detected, {} fixtures updated", changes.len(), updated)
621 }))),
622 Err(e) => Err(ApiError::Recorder(e)),
623 }
624}
625
626async fn get_sync_changes(
628 State(state): State<ApiState>,
629) -> Result<Json<serde_json::Value>, ApiError> {
630 let sync_service = state
631 .sync_service
632 .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
633
634 let status = sync_service.get_status().await;
635
636 Ok(Json(serde_json::json!({
637 "last_sync": status.last_sync,
638 "last_changes_detected": status.last_changes_detected,
639 "last_fixtures_updated": status.last_fixtures_updated,
640 "last_error": status.last_error,
641 "total_syncs": status.total_syncs,
642 "is_running": status.is_running,
643 })))
644}