mockforge_recorder/
api.rs

1//! Management API for recorded requests
2
3use 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    sync_snapshots::EndpointTimeline,
14    test_generation::{LlmConfig, TestFormat, TestGenerationConfig, TestGenerator},
15};
16use axum::{
17    extract::{Path, Query, State},
18    http::StatusCode,
19    response::{IntoResponse, Json, Response},
20    routing::{delete, get, post},
21    Router,
22};
23use serde::{Deserialize, Serialize};
24use std::sync::Arc;
25use tracing::{debug, error};
26
27/// API state
28#[derive(Clone)]
29pub struct ApiState {
30    pub recorder: Arc<Recorder>,
31    pub sync_service: Option<Arc<SyncService>>,
32}
33
34/// Create the management API router
35pub fn create_api_router(
36    recorder: Arc<Recorder>,
37    sync_service: Option<Arc<SyncService>>,
38) -> Router {
39    let state = ApiState {
40        recorder,
41        sync_service,
42    };
43
44    Router::new()
45        // Query endpoints
46        .route("/api/recorder/requests", get(list_requests))
47        .route("/api/recorder/requests/{id}", get(get_request))
48        .route("/api/recorder/requests/{id}/response", get(get_response))
49        .route("/api/recorder/search", post(search_requests))
50
51        // Export endpoints
52        .route("/api/recorder/export/har", get(export_har))
53
54        // Control endpoints
55        .route("/api/recorder/status", get(get_status))
56        .route("/api/recorder/enable", post(enable_recording))
57        .route("/api/recorder/disable", post(disable_recording))
58        .route("/api/recorder/clear", delete(clear_recordings))
59
60        // Replay endpoints
61        .route("/api/recorder/replay/{id}", post(replay_request))
62        .route("/api/recorder/compare/{id}", post(compare_responses))
63
64        // Statistics endpoints
65        .route("/api/recorder/stats", get(get_statistics))
66
67        // Test generation endpoints
68        .route("/api/recorder/generate-tests", post(generate_tests))
69
70        // Integration testing endpoints
71        .route("/api/recorder/workflows", post(create_workflow))
72        .route("/api/recorder/workflows/{id}", get(get_workflow))
73        .route("/api/recorder/workflows/{id}/generate", post(generate_integration_test))
74
75        // Sync endpoints
76        .route("/api/recorder/sync/status", get(get_sync_status))
77        .route("/api/recorder/sync/config", get(get_sync_config))
78        .route("/api/recorder/sync/config", post(update_sync_config))
79        .route("/api/recorder/sync/now", post(sync_now))
80        .route("/api/recorder/sync/changes", get(get_sync_changes))
81
82        // Sync snapshot endpoints (Shadow Snapshot Mode)
83        .route("/api/recorder/sync/snapshots", get(list_snapshots))
84        .route("/api/recorder/sync/snapshots/{endpoint}", get(get_endpoint_timeline))
85        .route("/api/recorder/sync/snapshots/cycle/{cycle_id}", get(get_snapshots_by_cycle))
86
87        // Stub mapping conversion endpoints
88        .route("/api/recorder/convert/{id}", post(convert_to_stub))
89        .route("/api/recorder/convert/batch", post(convert_batch))
90
91        .with_state(state)
92}
93
94/// List recent requests
95async fn list_requests(
96    State(state): State<ApiState>,
97    Query(params): Query<ListParams>,
98) -> Result<Json<QueryResult>, ApiError> {
99    let limit = params.limit.unwrap_or(100);
100    let offset = params.offset.unwrap_or(0);
101
102    let filter = QueryFilter {
103        limit: Some(limit),
104        offset: Some(offset),
105        ..Default::default()
106    };
107
108    let result = execute_query(state.recorder.database(), filter).await?;
109    Ok(Json(result))
110}
111
112/// Get a single request by ID
113async fn get_request(
114    State(state): State<ApiState>,
115    Path(id): Path<String>,
116) -> Result<Json<RecordedExchange>, ApiError> {
117    let exchange = state
118        .recorder
119        .database()
120        .get_exchange(&id)
121        .await?
122        .ok_or_else(|| ApiError::NotFound(format!("Request {} not found", id)))?;
123
124    Ok(Json(exchange))
125}
126
127/// Get response for a request
128async fn get_response(
129    State(state): State<ApiState>,
130    Path(id): Path<String>,
131) -> Result<Json<serde_json::Value>, ApiError> {
132    let response = state
133        .recorder
134        .database()
135        .get_response(&id)
136        .await?
137        .ok_or_else(|| ApiError::NotFound(format!("Response for request {} not found", id)))?;
138
139    Ok(Json(serde_json::json!({
140        "request_id": response.request_id,
141        "status_code": response.status_code,
142        "headers": serde_json::from_str::<serde_json::Value>(&response.headers)?,
143        "body": response.body,
144        "body_encoding": response.body_encoding,
145        "size_bytes": response.size_bytes,
146        "timestamp": response.timestamp,
147    })))
148}
149
150/// Search requests with filters
151async fn search_requests(
152    State(state): State<ApiState>,
153    Json(filter): Json<QueryFilter>,
154) -> Result<Json<QueryResult>, ApiError> {
155    let result = execute_query(state.recorder.database(), filter).await?;
156    Ok(Json(result))
157}
158
159/// Export recordings to HAR format
160async fn export_har(
161    State(state): State<ApiState>,
162    Query(params): Query<ExportParams>,
163) -> Result<Response, ApiError> {
164    let limit = params.limit.unwrap_or(1000);
165
166    let filter = QueryFilter {
167        limit: Some(limit),
168        protocol: Some(crate::models::Protocol::Http), // HAR only supports HTTP
169        ..Default::default()
170    };
171
172    let result = execute_query(state.recorder.database(), filter).await?;
173    let har = export_to_har(&result.exchanges)?;
174    let har_json = serde_json::to_string_pretty(&har)?;
175
176    Ok((StatusCode::OK, [("content-type", "application/json")], har_json).into_response())
177}
178
179/// Get recording status
180async fn get_status(State(state): State<ApiState>) -> Json<StatusResponse> {
181    let enabled = state.recorder.is_enabled().await;
182    Json(StatusResponse { enabled })
183}
184
185/// Enable recording
186async fn enable_recording(State(state): State<ApiState>) -> Json<StatusResponse> {
187    state.recorder.enable().await;
188    debug!("Recording enabled via API");
189    Json(StatusResponse { enabled: true })
190}
191
192/// Disable recording
193async fn disable_recording(State(state): State<ApiState>) -> Json<StatusResponse> {
194    state.recorder.disable().await;
195    debug!("Recording disabled via API");
196    Json(StatusResponse { enabled: false })
197}
198
199/// Clear all recordings
200async fn clear_recordings(State(state): State<ApiState>) -> Result<Json<ClearResponse>, ApiError> {
201    state.recorder.database().clear_all().await?;
202    debug!("All recordings cleared via API");
203    Ok(Json(ClearResponse {
204        message: "All recordings cleared".to_string(),
205    }))
206}
207
208/// Replay a single request
209async fn replay_request(
210    State(state): State<ApiState>,
211    Path(id): Path<String>,
212) -> Result<Json<serde_json::Value>, ApiError> {
213    let engine = ReplayEngine::new((**state.recorder.database()).clone());
214    let result = engine.replay_request(&id).await?;
215
216    Ok(Json(serde_json::json!({
217        "request_id": result.request_id,
218        "success": result.success,
219        "message": result.message,
220        "original_status": result.original_status,
221        "replay_status": result.replay_status,
222    })))
223}
224
225/// Compare original response with a replayed/new response
226async fn compare_responses(
227    State(state): State<ApiState>,
228    Path(id): Path<String>,
229    Json(payload): Json<CompareRequest>,
230) -> Result<Json<ComparisonResult>, ApiError> {
231    let engine = ReplayEngine::new((**state.recorder.database()).clone());
232
233    let result = engine
234        .compare_responses(&id, payload.body.as_bytes(), payload.status_code, &payload.headers)
235        .await?;
236
237    Ok(Json(result))
238}
239
240/// Get statistics about recordings
241async fn get_statistics(
242    State(state): State<ApiState>,
243) -> Result<Json<StatisticsResponse>, ApiError> {
244    let db = state.recorder.database();
245    let stats = db.get_statistics().await?;
246
247    Ok(Json(StatisticsResponse {
248        total_requests: stats.total_requests,
249        by_protocol: stats.by_protocol,
250        by_status_code: stats.by_status_code,
251        avg_duration_ms: stats.avg_duration_ms,
252    }))
253}
254
255// Request/Response types
256
257#[derive(Debug, Deserialize)]
258struct ListParams {
259    limit: Option<i32>,
260    offset: Option<i32>,
261}
262
263#[derive(Debug, Deserialize)]
264struct ExportParams {
265    limit: Option<i32>,
266}
267
268#[derive(Debug, Deserialize)]
269struct CompareRequest {
270    status_code: i32,
271    headers: std::collections::HashMap<String, String>,
272    body: String,
273}
274
275#[derive(Debug, Serialize)]
276struct StatusResponse {
277    enabled: bool,
278}
279
280#[derive(Debug, Serialize)]
281struct ClearResponse {
282    message: String,
283}
284
285#[derive(Debug, Serialize)]
286struct StatisticsResponse {
287    total_requests: i64,
288    by_protocol: std::collections::HashMap<String, i64>,
289    by_status_code: std::collections::HashMap<i32, i64>,
290    avg_duration_ms: Option<f64>,
291}
292
293// Error handling
294
295#[derive(Debug)]
296enum ApiError {
297    Database(sqlx::Error),
298    Serialization(serde_json::Error),
299    NotFound(String),
300    InvalidInput(String),
301    Recorder(crate::RecorderError),
302}
303
304impl From<sqlx::Error> for ApiError {
305    fn from(err: sqlx::Error) -> Self {
306        ApiError::Database(err)
307    }
308}
309
310impl From<serde_json::Error> for ApiError {
311    fn from(err: serde_json::Error) -> Self {
312        ApiError::Serialization(err)
313    }
314}
315
316impl From<crate::RecorderError> for ApiError {
317    fn from(err: crate::RecorderError) -> Self {
318        ApiError::Recorder(err)
319    }
320}
321
322impl IntoResponse for ApiError {
323    fn into_response(self) -> Response {
324        let (status, message) = match self {
325            ApiError::Database(e) => {
326                error!("Database error: {}", e);
327                (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e))
328            }
329            ApiError::Serialization(e) => {
330                error!("Serialization error: {}", e);
331                (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization error: {}", e))
332            }
333            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
334            ApiError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
335            ApiError::Recorder(e) => {
336                error!("Recorder error: {}", e);
337                (StatusCode::INTERNAL_SERVER_ERROR, format!("Recorder error: {}", e))
338            }
339        };
340
341        (status, Json(serde_json::json!({ "error": message }))).into_response()
342    }
343}
344
345/// Test generation request
346#[derive(Debug, Serialize, Deserialize)]
347pub struct GenerateTestsRequest {
348    /// Test format to generate
349    #[serde(default = "default_format")]
350    pub format: String,
351
352    /// Filter for query
353    #[serde(flatten)]
354    pub filter: QueryFilter,
355
356    /// Test suite name
357    #[serde(default = "default_suite_name")]
358    pub suite_name: String,
359
360    /// Base URL for tests
361    pub base_url: Option<String>,
362
363    /// Use AI for test descriptions
364    #[serde(default)]
365    pub ai_descriptions: bool,
366
367    /// LLM configuration for AI descriptions
368    pub llm_config: Option<LlmConfigRequest>,
369
370    /// Include assertions
371    #[serde(default = "default_true")]
372    pub include_assertions: bool,
373
374    /// Validate response body
375    #[serde(default = "default_true")]
376    pub validate_body: bool,
377
378    /// Validate status code
379    #[serde(default = "default_true")]
380    pub validate_status: bool,
381
382    /// Validate headers
383    #[serde(default)]
384    pub validate_headers: bool,
385
386    /// Validate timing
387    #[serde(default)]
388    pub validate_timing: bool,
389
390    /// Max duration threshold for timing validation
391    pub max_duration_ms: Option<u64>,
392}
393
394fn default_format() -> String {
395    "rust_reqwest".to_string()
396}
397
398fn default_suite_name() -> String {
399    "generated_tests".to_string()
400}
401
402fn default_true() -> bool {
403    true
404}
405
406/// LLM configuration request
407#[derive(Debug, Serialize, Deserialize)]
408pub struct LlmConfigRequest {
409    /// LLM provider
410    pub provider: String,
411    /// API endpoint
412    pub api_endpoint: String,
413    /// API key
414    pub api_key: Option<String>,
415    /// Model name
416    pub model: String,
417    /// Temperature
418    #[serde(default = "default_temperature")]
419    pub temperature: f64,
420}
421
422fn default_temperature() -> f64 {
423    0.3
424}
425
426/// Generate tests from recorded requests
427async fn generate_tests(
428    State(state): State<ApiState>,
429    Json(request): Json<GenerateTestsRequest>,
430) -> Result<Json<serde_json::Value>, ApiError> {
431    debug!("Generating tests with format: {}", request.format);
432
433    // Parse test format
434    let test_format = match request.format.as_str() {
435        "rust_reqwest" => TestFormat::RustReqwest,
436        "http_file" => TestFormat::HttpFile,
437        "curl" => TestFormat::Curl,
438        "postman" => TestFormat::Postman,
439        "k6" => TestFormat::K6,
440        "python_pytest" => TestFormat::PythonPytest,
441        "javascript_jest" => TestFormat::JavaScriptJest,
442        "go_test" => TestFormat::GoTest,
443        _ => {
444            return Err(ApiError::NotFound(format!(
445                "Invalid test format: {}. Supported: rust_reqwest, http_file, curl, postman, k6, python_pytest, javascript_jest, go_test",
446                request.format
447            )));
448        }
449    };
450
451    // Convert LLM config if provided
452    let llm_config = request.llm_config.map(|cfg| LlmConfig {
453        provider: cfg.provider,
454        api_endpoint: cfg.api_endpoint,
455        api_key: cfg.api_key,
456        model: cfg.model,
457        temperature: cfg.temperature,
458    });
459
460    // Create test generation config
461    let config = TestGenerationConfig {
462        format: test_format,
463        include_assertions: request.include_assertions,
464        validate_body: request.validate_body,
465        validate_status: request.validate_status,
466        validate_headers: request.validate_headers,
467        validate_timing: request.validate_timing,
468        max_duration_ms: request.max_duration_ms,
469        suite_name: request.suite_name,
470        base_url: request.base_url,
471        ai_descriptions: request.ai_descriptions,
472        llm_config,
473        group_by_endpoint: true,
474        include_setup_teardown: true,
475        generate_fixtures: false,
476        suggest_edge_cases: false,
477        analyze_test_gaps: false,
478        deduplicate_tests: false,
479        optimize_test_order: false,
480    };
481
482    // Create test generator
483    let generator = TestGenerator::from_arc(state.recorder.database().clone(), config);
484
485    // Generate tests
486    let result = generator.generate_from_filter(request.filter).await?;
487
488    // Return result
489    Ok(Json(serde_json::json!({
490        "success": true,
491        "metadata": {
492            "suite_name": result.metadata.name,
493            "test_count": result.metadata.test_count,
494            "endpoint_count": result.metadata.endpoint_count,
495            "protocols": result.metadata.protocols,
496            "format": result.metadata.format,
497            "generated_at": result.metadata.generated_at,
498        },
499        "tests": result.tests.iter().map(|t| serde_json::json!({
500            "name": t.name,
501            "description": t.description,
502            "endpoint": t.endpoint,
503            "method": t.method,
504        })).collect::<Vec<_>>(),
505        "test_file": result.test_file,
506    })))
507}
508
509// Integration Testing Endpoints
510
511/// Create workflow request
512#[derive(Debug, Serialize, Deserialize)]
513struct CreateWorkflowRequest {
514    workflow: IntegrationWorkflow,
515}
516
517/// Create a new integration test workflow
518async fn create_workflow(
519    State(_state): State<ApiState>,
520    Json(request): Json<CreateWorkflowRequest>,
521) -> Result<Json<serde_json::Value>, ApiError> {
522    // For now, just return the workflow with success
523    // In a full implementation, this would store in a database
524    Ok(Json(serde_json::json!({
525        "success": true,
526        "workflow": request.workflow,
527        "message": "Workflow created successfully"
528    })))
529}
530
531/// Get workflow by ID
532async fn get_workflow(
533    State(_state): State<ApiState>,
534    Path(id): Path<String>,
535) -> Result<Json<serde_json::Value>, ApiError> {
536    // Mock workflow for demonstration
537    // In a full implementation, this would fetch from database
538    let workflow = IntegrationWorkflow {
539        id: id.clone(),
540        name: "Sample Workflow".to_string(),
541        description: "A sample integration test workflow".to_string(),
542        steps: vec![],
543        setup: WorkflowSetup::default(),
544        cleanup: vec![],
545        created_at: chrono::Utc::now(),
546    };
547
548    Ok(Json(serde_json::json!({
549        "success": true,
550        "workflow": workflow
551    })))
552}
553
554/// Generate integration test request
555#[derive(Debug, Deserialize)]
556struct GenerateIntegrationTestRequest {
557    workflow: IntegrationWorkflow,
558    format: String, // "rust", "python", "javascript"
559}
560
561/// Generate integration test code from workflow
562async fn generate_integration_test(
563    State(_state): State<ApiState>,
564    Path(_id): Path<String>,
565    Json(request): Json<GenerateIntegrationTestRequest>,
566) -> Result<Json<serde_json::Value>, ApiError> {
567    let generator = IntegrationTestGenerator::new(request.workflow);
568
569    let test_code = match request.format.as_str() {
570        "rust" => generator.generate_rust_test(),
571        "python" => generator.generate_python_test(),
572        "javascript" | "js" => generator.generate_javascript_test(),
573        _ => return Err(ApiError::InvalidInput(format!("Unsupported format: {}", request.format))),
574    };
575
576    Ok(Json(serde_json::json!({
577        "success": true,
578        "format": request.format,
579        "test_code": test_code,
580        "message": "Integration test generated successfully"
581    })))
582}
583
584// Sync endpoints
585
586/// Get sync status
587async fn get_sync_status(State(state): State<ApiState>) -> Result<Json<SyncStatus>, ApiError> {
588    let sync_service = state
589        .sync_service
590        .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
591
592    let status = sync_service.get_status().await;
593    Ok(Json(status))
594}
595
596/// Get sync configuration
597async fn get_sync_config(State(state): State<ApiState>) -> Result<Json<SyncConfig>, ApiError> {
598    let sync_service = state
599        .sync_service
600        .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
601
602    let config = sync_service.get_config().await;
603    Ok(Json(config))
604}
605
606/// Update sync configuration
607async fn update_sync_config(
608    State(state): State<ApiState>,
609    Json(config): Json<SyncConfig>,
610) -> Result<Json<SyncConfig>, ApiError> {
611    let sync_service = state
612        .sync_service
613        .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
614
615    sync_service.update_config(config.clone()).await;
616    Ok(Json(config))
617}
618
619/// Trigger sync now
620async fn sync_now(State(state): State<ApiState>) -> Result<Json<serde_json::Value>, ApiError> {
621    let sync_service = state
622        .sync_service
623        .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
624
625    match sync_service.sync_now().await {
626        Ok((changes, updated)) => Ok(Json(serde_json::json!({
627            "success": true,
628            "changes_detected": changes.len(),
629            "fixtures_updated": updated,
630            "changes": changes,
631            "message": format!("Sync complete: {} changes detected, {} fixtures updated", changes.len(), updated)
632        }))),
633        Err(e) => Err(ApiError::Recorder(e)),
634    }
635}
636
637/// Get sync changes (from last sync)
638async fn get_sync_changes(
639    State(state): State<ApiState>,
640) -> Result<Json<serde_json::Value>, ApiError> {
641    let sync_service = state
642        .sync_service
643        .ok_or_else(|| ApiError::NotFound("Sync service not available".to_string()))?;
644
645    let status = sync_service.get_status().await;
646
647    Ok(Json(serde_json::json!({
648        "last_sync": status.last_sync,
649        "last_changes_detected": status.last_changes_detected,
650        "last_fixtures_updated": status.last_fixtures_updated,
651        "last_error": status.last_error,
652        "total_syncs": status.total_syncs,
653        "is_running": status.is_running,
654    })))
655}
656
657/// Convert a single recording to stub mapping
658#[derive(Debug, Deserialize)]
659struct ConvertRequest {
660    format: Option<String>, // "yaml" or "json"
661    detect_dynamic_values: Option<bool>,
662}
663
664async fn convert_to_stub(
665    State(state): State<ApiState>,
666    Path(id): Path<String>,
667    Json(req): Json<ConvertRequest>,
668) -> Result<Json<serde_json::Value>, ApiError> {
669    let exchange = state
670        .recorder
671        .database()
672        .get_exchange(&id)
673        .await?
674        .ok_or_else(|| ApiError::NotFound(format!("Request {} not found", id)))?;
675
676    let detect_dynamic = req.detect_dynamic_values.unwrap_or(true);
677    let converter = StubMappingConverter::new(detect_dynamic);
678    let stub = converter.convert(&exchange)?;
679
680    let format = match req.format.as_deref() {
681        Some("json") => StubFormat::Json,
682        Some("yaml") | None => StubFormat::Yaml,
683        _ => StubFormat::Yaml,
684    };
685
686    let content = converter.to_string(&stub, format)?;
687
688    Ok(Json(serde_json::json!({
689        "request_id": id,
690        "format": match format {
691            StubFormat::Yaml => "yaml",
692            StubFormat::Json => "json",
693        },
694        "stub": stub,
695        "content": content,
696    })))
697}
698
699/// Convert multiple recordings to stub mappings
700#[derive(Debug, Deserialize)]
701struct BatchConvertRequest {
702    request_ids: Vec<String>,
703    format: Option<String>,
704    detect_dynamic_values: Option<bool>,
705    deduplicate: Option<bool>,
706}
707
708async fn convert_batch(
709    State(state): State<ApiState>,
710    Json(req): Json<BatchConvertRequest>,
711) -> Result<Json<serde_json::Value>, ApiError> {
712    let detect_dynamic = req.detect_dynamic_values.unwrap_or(true);
713    let converter = StubMappingConverter::new(detect_dynamic);
714
715    let format = match req.format.as_deref() {
716        Some("json") => StubFormat::Json,
717        Some("yaml") | None => StubFormat::Yaml,
718        _ => StubFormat::Yaml,
719    };
720
721    let mut stubs = Vec::new();
722    let mut errors = Vec::new();
723
724    for request_id in &req.request_ids {
725        match state.recorder.database().get_exchange(request_id).await {
726            Ok(Some(exchange)) => match converter.convert(&exchange) {
727                Ok(stub) => {
728                    let content = converter.to_string(&stub, format)?;
729                    stubs.push(serde_json::json!({
730                        "request_id": request_id,
731                        "stub": stub,
732                        "content": content,
733                    }));
734                }
735                Err(e) => {
736                    errors.push(format!("Failed to convert {}: {}", request_id, e));
737                }
738            },
739            Ok(None) => {
740                errors.push(format!("Request {} not found", request_id));
741            }
742            Err(e) => {
743                errors.push(format!("Database error for {}: {}", request_id, e));
744            }
745        }
746    }
747
748    // Deduplicate if requested
749    if req.deduplicate.unwrap_or(false) {
750        // Simple deduplication based on identifier
751        let mut seen = std::collections::HashSet::new();
752        stubs.retain(|stub| {
753            if let Some(id) = stub.get("stub").and_then(|s| s.get("identifier")) {
754                if let Some(id_str) = id.as_str() {
755                    return seen.insert(id_str.to_string());
756                }
757            }
758            true
759        });
760    }
761
762    Ok(Json(serde_json::json!({
763        "total": req.request_ids.len(),
764        "converted": stubs.len(),
765        "errors": errors.len(),
766        "stubs": stubs,
767        "errors_list": errors,
768    })))
769}
770
771/// List all snapshots
772#[derive(Debug, Deserialize)]
773struct SnapshotListParams {
774    limit: Option<i32>,
775    offset: Option<i32>,
776}
777
778async fn list_snapshots(
779    State(state): State<ApiState>,
780    Query(params): Query<SnapshotListParams>,
781) -> Result<Json<serde_json::Value>, ApiError> {
782    let limit = params.limit.unwrap_or(100);
783    let database = state.recorder.database();
784
785    // Get all unique endpoints to list snapshots
786    // For simplicity, we'll get snapshots for all endpoints
787    // In a real implementation, you might want to paginate differently
788    let snapshots = database.get_snapshots_for_endpoint("", None, Some(limit)).await?;
789
790    Ok(Json(serde_json::json!({
791        "snapshots": snapshots,
792        "total": snapshots.len(),
793    })))
794}
795
796/// Get timeline for a specific endpoint
797#[derive(Debug, Deserialize)]
798struct TimelineParams {
799    method: Option<String>,
800    limit: Option<i32>,
801}
802
803async fn get_endpoint_timeline(
804    State(state): State<ApiState>,
805    Path(endpoint): Path<String>,
806    Query(params): Query<TimelineParams>,
807) -> Result<Json<EndpointTimeline>, ApiError> {
808    let database = state.recorder.database();
809    let limit = params.limit.unwrap_or(100);
810
811    // Axum automatically URL-decodes path parameters
812    let snapshots = database
813        .get_snapshots_for_endpoint(&endpoint, params.method.as_deref(), Some(limit))
814        .await?;
815
816    // Build timeline data
817    let mut response_time_trends = Vec::new();
818    let mut status_code_history = Vec::new();
819    let mut error_patterns = std::collections::HashMap::new();
820
821    for snapshot in &snapshots {
822        response_time_trends.push((
823            snapshot.timestamp,
824            snapshot.response_time_after.or(snapshot.response_time_before),
825        ));
826        status_code_history.push((snapshot.timestamp, snapshot.after.status_code));
827
828        // Track error patterns
829        if snapshot.after.status_code >= 400 {
830            let key = format!("{}", snapshot.after.status_code);
831            let pattern =
832                error_patterns
833                    .entry(key)
834                    .or_insert_with(|| crate::sync_snapshots::ErrorPattern {
835                        status_code: snapshot.after.status_code,
836                        message_pattern: None,
837                        occurrences: 0,
838                        first_seen: snapshot.timestamp,
839                        last_seen: snapshot.timestamp,
840                    });
841            pattern.occurrences += 1;
842            if snapshot.timestamp < pattern.first_seen {
843                pattern.first_seen = snapshot.timestamp;
844            }
845            if snapshot.timestamp > pattern.last_seen {
846                pattern.last_seen = snapshot.timestamp;
847            }
848        }
849    }
850
851    let timeline = EndpointTimeline {
852        endpoint,
853        method: params.method.unwrap_or_else(|| "ALL".to_string()),
854        snapshots,
855        response_time_trends,
856        status_code_history,
857        error_patterns: error_patterns.into_values().collect(),
858    };
859
860    Ok(Json(timeline))
861}
862
863/// Get snapshots by sync cycle ID
864async fn get_snapshots_by_cycle(
865    State(state): State<ApiState>,
866    Path(cycle_id): Path<String>,
867) -> Result<Json<serde_json::Value>, ApiError> {
868    let database = state.recorder.database();
869
870    let snapshots = database.get_snapshots_by_cycle(&cycle_id).await?;
871
872    Ok(Json(serde_json::json!({
873        "sync_cycle_id": cycle_id,
874        "snapshots": snapshots,
875        "total": snapshots.len(),
876    })))
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882    use crate::database::RecorderDatabase;
883    use crate::models::{Protocol, RecordedRequest, RecordedResponse};
884    use axum::body::Body;
885    use axum::http::{Request, StatusCode as HttpStatusCode};
886    use tower::ServiceExt;
887
888    async fn create_test_db() -> RecorderDatabase {
889        RecorderDatabase::new_in_memory().await.unwrap()
890    }
891
892    async fn create_test_recorder() -> Arc<Recorder> {
893        let db = create_test_db().await;
894        Arc::new(Recorder::new(db))
895    }
896
897    #[tokio::test]
898    async fn test_api_state_creation() {
899        let recorder = create_test_recorder().await;
900        let state = ApiState {
901            recorder: recorder.clone(),
902            sync_service: None,
903        };
904
905        assert!(state.sync_service.is_none());
906    }
907
908    #[tokio::test]
909    async fn test_create_api_router() {
910        let recorder = create_test_recorder().await;
911        let router = create_api_router(recorder, None);
912
913        // Router should be created successfully
914        assert!(std::mem::size_of_val(&router) > 0);
915    }
916
917    #[tokio::test]
918    async fn test_get_status_enabled() {
919        let recorder = create_test_recorder().await;
920        recorder.enable().await;
921
922        let state = ApiState {
923            recorder: recorder.clone(),
924            sync_service: None,
925        };
926
927        let response = get_status(State(state)).await;
928        assert!(response.0.enabled);
929    }
930
931    #[tokio::test]
932    async fn test_get_status_disabled() {
933        let recorder = create_test_recorder().await;
934        recorder.disable().await;
935
936        let state = ApiState {
937            recorder: recorder.clone(),
938            sync_service: None,
939        };
940
941        let response = get_status(State(state)).await;
942        assert!(!response.0.enabled);
943    }
944
945    #[tokio::test]
946    async fn test_enable_recording() {
947        let recorder = create_test_recorder().await;
948        recorder.disable().await;
949
950        let state = ApiState {
951            recorder: recorder.clone(),
952            sync_service: None,
953        };
954
955        let response = enable_recording(State(state)).await;
956        assert!(response.0.enabled);
957        assert!(recorder.is_enabled().await);
958    }
959
960    #[tokio::test]
961    async fn test_disable_recording() {
962        let recorder = create_test_recorder().await;
963        recorder.enable().await;
964
965        let state = ApiState {
966            recorder: recorder.clone(),
967            sync_service: None,
968        };
969
970        let response = disable_recording(State(state)).await;
971        assert!(!response.0.enabled);
972        assert!(!recorder.is_enabled().await);
973    }
974
975    #[tokio::test]
976    async fn test_clear_recordings() {
977        let recorder = create_test_recorder().await;
978
979        // Add a test request
980        let request = RecordedRequest {
981            id: "test-1".to_string(),
982            protocol: Protocol::Http,
983            timestamp: chrono::Utc::now(),
984            method: "GET".to_string(),
985            path: "/test".to_string(),
986            query_params: None,
987            headers: "{}".to_string(),
988            body: None,
989            body_encoding: "utf8".to_string(),
990            client_ip: None,
991            trace_id: None,
992            span_id: None,
993            duration_ms: None,
994            status_code: Some(200),
995            tags: None,
996        };
997        recorder.database().insert_request(&request).await.unwrap();
998
999        let state = ApiState {
1000            recorder: recorder.clone(),
1001            sync_service: None,
1002        };
1003
1004        let response = clear_recordings(State(state)).await.unwrap();
1005        assert_eq!(response.0.message, "All recordings cleared");
1006    }
1007
1008    #[test]
1009    fn test_api_error_from_sqlx() {
1010        let err = sqlx::Error::RowNotFound;
1011        let api_err = ApiError::from(err);
1012
1013        match api_err {
1014            ApiError::Database(_) => {}
1015            _ => panic!("Expected Database error"),
1016        }
1017    }
1018
1019    #[test]
1020    fn test_api_error_from_serde() {
1021        let err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
1022        let api_err = ApiError::from(err);
1023
1024        match api_err {
1025            ApiError::Serialization(_) => {}
1026            _ => panic!("Expected Serialization error"),
1027        }
1028    }
1029
1030    #[test]
1031    fn test_api_error_into_response_not_found() {
1032        let err = ApiError::NotFound("Test not found".to_string());
1033        let response = err.into_response();
1034
1035        assert_eq!(response.status(), HttpStatusCode::NOT_FOUND);
1036    }
1037
1038    #[test]
1039    fn test_api_error_into_response_invalid_input() {
1040        let err = ApiError::InvalidInput("Invalid data".to_string());
1041        let response = err.into_response();
1042
1043        assert_eq!(response.status(), HttpStatusCode::BAD_REQUEST);
1044    }
1045
1046    #[test]
1047    fn test_list_params_defaults() {
1048        let params = ListParams {
1049            limit: None,
1050            offset: None,
1051        };
1052
1053        assert!(params.limit.is_none());
1054        assert!(params.offset.is_none());
1055    }
1056
1057    #[test]
1058    fn test_export_params_defaults() {
1059        let params = ExportParams { limit: None };
1060        assert!(params.limit.is_none());
1061    }
1062
1063    #[test]
1064    fn test_compare_request_creation() {
1065        let mut headers = std::collections::HashMap::new();
1066        headers.insert("content-type".to_string(), "application/json".to_string());
1067
1068        let req = CompareRequest {
1069            status_code: 200,
1070            headers,
1071            body: "test body".to_string(),
1072        };
1073
1074        assert_eq!(req.status_code, 200);
1075        assert_eq!(req.body, "test body");
1076        assert_eq!(req.headers.get("content-type").unwrap(), "application/json");
1077    }
1078
1079    #[test]
1080    fn test_status_response_serialization() {
1081        let response = StatusResponse { enabled: true };
1082        let json = serde_json::to_string(&response).unwrap();
1083
1084        assert!(json.contains("enabled"));
1085        assert!(json.contains("true"));
1086    }
1087
1088    #[test]
1089    fn test_clear_response_serialization() {
1090        let response = ClearResponse {
1091            message: "All cleared".to_string(),
1092        };
1093        let json = serde_json::to_string(&response).unwrap();
1094
1095        assert!(json.contains("All cleared"));
1096    }
1097
1098    #[test]
1099    fn test_statistics_response_creation() {
1100        let mut by_protocol = std::collections::HashMap::new();
1101        by_protocol.insert("http".to_string(), 100);
1102
1103        let mut by_status_code = std::collections::HashMap::new();
1104        by_status_code.insert(200, 80);
1105        by_status_code.insert(404, 20);
1106
1107        let response = StatisticsResponse {
1108            total_requests: 100,
1109            by_protocol,
1110            by_status_code,
1111            avg_duration_ms: Some(150.5),
1112        };
1113
1114        assert_eq!(response.total_requests, 100);
1115        assert_eq!(response.by_protocol.get("http").unwrap(), &100);
1116        assert_eq!(response.by_status_code.get(&200).unwrap(), &80);
1117        assert_eq!(response.avg_duration_ms, Some(150.5));
1118    }
1119
1120    #[test]
1121    fn test_default_format() {
1122        assert_eq!(default_format(), "rust_reqwest");
1123    }
1124
1125    #[test]
1126    fn test_default_suite_name() {
1127        assert_eq!(default_suite_name(), "generated_tests");
1128    }
1129
1130    #[test]
1131    fn test_default_true() {
1132        assert!(default_true());
1133    }
1134
1135    #[test]
1136    fn test_default_temperature() {
1137        assert_eq!(default_temperature(), 0.3);
1138    }
1139
1140    #[test]
1141    fn test_generate_tests_request_defaults() {
1142        let request = GenerateTestsRequest {
1143            format: default_format(),
1144            filter: QueryFilter::default(),
1145            suite_name: default_suite_name(),
1146            base_url: None,
1147            ai_descriptions: false,
1148            llm_config: None,
1149            include_assertions: default_true(),
1150            validate_body: default_true(),
1151            validate_status: default_true(),
1152            validate_headers: false,
1153            validate_timing: false,
1154            max_duration_ms: None,
1155        };
1156
1157        assert_eq!(request.format, "rust_reqwest");
1158        assert_eq!(request.suite_name, "generated_tests");
1159        assert!(request.include_assertions);
1160        assert!(request.validate_body);
1161        assert!(request.validate_status);
1162        assert!(!request.validate_headers);
1163    }
1164
1165    #[test]
1166    fn test_llm_config_request_creation() {
1167        let config = LlmConfigRequest {
1168            provider: "openai".to_string(),
1169            api_endpoint: "https://api.openai.com".to_string(),
1170            api_key: Some("secret".to_string()),
1171            model: "gpt-4".to_string(),
1172            temperature: default_temperature(),
1173        };
1174
1175        assert_eq!(config.provider, "openai");
1176        assert_eq!(config.model, "gpt-4");
1177        assert_eq!(config.temperature, 0.3);
1178    }
1179
1180    #[test]
1181    fn test_create_workflow_request_serialization() {
1182        let workflow = IntegrationWorkflow {
1183            id: "wf-1".to_string(),
1184            name: "Test Workflow".to_string(),
1185            description: "A test workflow".to_string(),
1186            steps: vec![],
1187            setup: WorkflowSetup::default(),
1188            cleanup: vec![],
1189            created_at: chrono::Utc::now(),
1190        };
1191
1192        let request = CreateWorkflowRequest { workflow };
1193        let json = serde_json::to_string(&request).unwrap();
1194
1195        assert!(json.contains("Test Workflow"));
1196    }
1197
1198    #[test]
1199    fn test_generate_integration_test_request_creation() {
1200        let workflow = IntegrationWorkflow {
1201            id: "wf-1".to_string(),
1202            name: "Test".to_string(),
1203            description: "Test".to_string(),
1204            steps: vec![],
1205            setup: WorkflowSetup::default(),
1206            cleanup: vec![],
1207            created_at: chrono::Utc::now(),
1208        };
1209
1210        let request = GenerateIntegrationTestRequest {
1211            workflow,
1212            format: "rust".to_string(),
1213        };
1214
1215        assert_eq!(request.format, "rust");
1216    }
1217
1218    #[test]
1219    fn test_convert_request_defaults() {
1220        let req = ConvertRequest {
1221            format: None,
1222            detect_dynamic_values: None,
1223        };
1224
1225        assert!(req.format.is_none());
1226        assert!(req.detect_dynamic_values.is_none());
1227    }
1228
1229    #[test]
1230    fn test_batch_convert_request_creation() {
1231        let request = BatchConvertRequest {
1232            request_ids: vec!["req-1".to_string(), "req-2".to_string()],
1233            format: Some("json".to_string()),
1234            detect_dynamic_values: Some(true),
1235            deduplicate: Some(false),
1236        };
1237
1238        assert_eq!(request.request_ids.len(), 2);
1239        assert_eq!(request.format, Some("json".to_string()));
1240        assert_eq!(request.detect_dynamic_values, Some(true));
1241        assert_eq!(request.deduplicate, Some(false));
1242    }
1243
1244    #[test]
1245    fn test_snapshot_list_params() {
1246        let params = SnapshotListParams {
1247            limit: Some(50),
1248            offset: Some(10),
1249        };
1250
1251        assert_eq!(params.limit, Some(50));
1252        assert_eq!(params.offset, Some(10));
1253    }
1254
1255    #[test]
1256    fn test_timeline_params() {
1257        let params = TimelineParams {
1258            method: Some("GET".to_string()),
1259            limit: Some(100),
1260        };
1261
1262        assert_eq!(params.method, Some("GET".to_string()));
1263        assert_eq!(params.limit, Some(100));
1264    }
1265
1266    #[tokio::test]
1267    async fn test_get_request_not_found() {
1268        let recorder = create_test_recorder().await;
1269        let state = ApiState {
1270            recorder: recorder.clone(),
1271            sync_service: None,
1272        };
1273
1274        let result = get_request(State(state), Path("non-existent".to_string())).await;
1275
1276        assert!(result.is_err());
1277        match result {
1278            Err(ApiError::NotFound(_)) => {}
1279            _ => panic!("Expected NotFound error"),
1280        }
1281    }
1282
1283    #[tokio::test]
1284    async fn test_get_response_not_found() {
1285        let recorder = create_test_recorder().await;
1286        let state = ApiState {
1287            recorder: recorder.clone(),
1288            sync_service: None,
1289        };
1290
1291        let result = get_response(State(state), Path("non-existent".to_string())).await;
1292
1293        assert!(result.is_err());
1294        match result {
1295            Err(ApiError::NotFound(_)) => {}
1296            _ => panic!("Expected NotFound error"),
1297        }
1298    }
1299
1300    #[tokio::test]
1301    async fn test_get_sync_status_no_service() {
1302        let recorder = create_test_recorder().await;
1303        let state = ApiState {
1304            recorder: recorder.clone(),
1305            sync_service: None,
1306        };
1307
1308        let result = get_sync_status(State(state)).await;
1309
1310        assert!(result.is_err());
1311        match result {
1312            Err(ApiError::NotFound(_)) => {}
1313            _ => panic!("Expected NotFound error"),
1314        }
1315    }
1316
1317    #[tokio::test]
1318    async fn test_get_sync_config_no_service() {
1319        let recorder = create_test_recorder().await;
1320        let state = ApiState {
1321            recorder: recorder.clone(),
1322            sync_service: None,
1323        };
1324
1325        let result = get_sync_config(State(state)).await;
1326
1327        assert!(result.is_err());
1328        match result {
1329            Err(ApiError::NotFound(_)) => {}
1330            _ => panic!("Expected NotFound error"),
1331        }
1332    }
1333
1334    #[tokio::test]
1335    async fn test_search_requests_empty() {
1336        let recorder = create_test_recorder().await;
1337        let state = ApiState {
1338            recorder: recorder.clone(),
1339            sync_service: None,
1340        };
1341
1342        let filter = QueryFilter::default();
1343        let result = search_requests(State(state), Json(filter)).await.unwrap();
1344
1345        assert_eq!(result.0.total, 0);
1346        assert!(result.0.exchanges.is_empty());
1347    }
1348
1349    #[tokio::test]
1350    async fn test_list_requests_with_params() {
1351        let recorder = create_test_recorder().await;
1352
1353        // Add some test requests
1354        for i in 0..5 {
1355            let request = RecordedRequest {
1356                id: format!("req-{}", i),
1357                protocol: Protocol::Http,
1358                timestamp: chrono::Utc::now(),
1359                method: "GET".to_string(),
1360                path: "/test".to_string(),
1361                query_params: None,
1362                headers: "{}".to_string(),
1363                body: None,
1364                body_encoding: "utf8".to_string(),
1365                client_ip: None,
1366                trace_id: None,
1367                span_id: None,
1368                duration_ms: None,
1369                status_code: Some(200),
1370                tags: None,
1371            };
1372            recorder.database().insert_request(&request).await.unwrap();
1373        }
1374
1375        let state = ApiState {
1376            recorder: recorder.clone(),
1377            sync_service: None,
1378        };
1379
1380        let params = ListParams {
1381            limit: Some(3),
1382            offset: Some(0),
1383        };
1384
1385        let result = list_requests(State(state), Query(params)).await.unwrap();
1386
1387        assert_eq!(result.0.exchanges.len(), 3);
1388    }
1389
1390    #[test]
1391    fn test_generate_tests_request_serialization() {
1392        let request = GenerateTestsRequest {
1393            format: "rust_reqwest".to_string(),
1394            filter: QueryFilter::default(),
1395            suite_name: "my_tests".to_string(),
1396            base_url: Some("http://localhost:8080".to_string()),
1397            ai_descriptions: true,
1398            llm_config: None,
1399            include_assertions: true,
1400            validate_body: true,
1401            validate_status: true,
1402            validate_headers: true,
1403            validate_timing: false,
1404            max_duration_ms: Some(1000),
1405        };
1406
1407        let json = serde_json::to_string(&request).unwrap();
1408
1409        assert!(json.contains("rust_reqwest"));
1410        assert!(json.contains("my_tests"));
1411        assert!(json.contains("http://localhost:8080"));
1412    }
1413}