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