1use crate::{
4 diff::ComparisonResult,
5 har_export::export_to_har,
6 integration_testing::{IntegrationTestGenerator, IntegrationWorkflow, WorkflowSetup},
7 models::RecordedExchange,
8 query::{execute_query, QueryFilter, QueryResult},
9 recorder::Recorder,
10 replay::ReplayEngine,
11 stub_mapping::{StubFormat, StubMappingConverter},
12 sync::{SyncConfig, SyncService, SyncStatus},
13 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#[derive(Clone)]
29pub struct ApiState {
30 pub recorder: Arc<Recorder>,
31 pub sync_service: Option<Arc<SyncService>>,
32}
33
34pub 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 .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 .route("/api/recorder/export/har", get(export_har))
53
54 .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 .route("/api/recorder/replay/{id}", post(replay_request))
62 .route("/api/recorder/compare/{id}", post(compare_responses))
63
64 .route("/api/recorder/stats", get(get_statistics))
66
67 .route("/api/recorder/generate-tests", post(generate_tests))
69
70 .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 .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 .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 .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
94async 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
112async 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
127async 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
150async 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
159async 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), ..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
179async fn get_status(State(state): State<ApiState>) -> Json<StatusResponse> {
181 let enabled = state.recorder.is_enabled().await;
182 Json(StatusResponse { enabled })
183}
184
185async 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
192async 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
199async 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
208async 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
225async 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
240async 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#[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#[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#[derive(Debug, Serialize, Deserialize)]
347pub struct GenerateTestsRequest {
348 #[serde(default = "default_format")]
350 pub format: String,
351
352 #[serde(flatten)]
354 pub filter: QueryFilter,
355
356 #[serde(default = "default_suite_name")]
358 pub suite_name: String,
359
360 pub base_url: Option<String>,
362
363 #[serde(default)]
365 pub ai_descriptions: bool,
366
367 pub llm_config: Option<LlmConfigRequest>,
369
370 #[serde(default = "default_true")]
372 pub include_assertions: bool,
373
374 #[serde(default = "default_true")]
376 pub validate_body: bool,
377
378 #[serde(default = "default_true")]
380 pub validate_status: bool,
381
382 #[serde(default)]
384 pub validate_headers: bool,
385
386 #[serde(default)]
388 pub validate_timing: bool,
389
390 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#[derive(Debug, Serialize, Deserialize)]
408pub struct LlmConfigRequest {
409 pub provider: String,
411 pub api_endpoint: String,
413 pub api_key: Option<String>,
415 pub model: String,
417 #[serde(default = "default_temperature")]
419 pub temperature: f64,
420}
421
422fn default_temperature() -> f64 {
423 0.3
424}
425
426async 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 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 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 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 let generator = TestGenerator::from_arc(state.recorder.database().clone(), config);
484
485 let result = generator.generate_from_filter(request.filter).await?;
487
488 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#[derive(Debug, Serialize, Deserialize)]
513struct CreateWorkflowRequest {
514 workflow: IntegrationWorkflow,
515}
516
517async fn create_workflow(
519 State(_state): State<ApiState>,
520 Json(request): Json<CreateWorkflowRequest>,
521) -> Result<Json<serde_json::Value>, ApiError> {
522 Ok(Json(serde_json::json!({
525 "success": true,
526 "workflow": request.workflow,
527 "message": "Workflow created successfully"
528 })))
529}
530
531async fn get_workflow(
533 State(_state): State<ApiState>,
534 Path(id): Path<String>,
535) -> Result<Json<serde_json::Value>, ApiError> {
536 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#[derive(Debug, Deserialize)]
556struct GenerateIntegrationTestRequest {
557 workflow: IntegrationWorkflow,
558 format: String, }
560
561async 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
584async 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
596async 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
606async 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
619async 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
637async 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#[derive(Debug, Deserialize)]
659struct ConvertRequest {
660 format: Option<String>, 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#[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 if req.deduplicate.unwrap_or(false) {
750 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#[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 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#[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 let snapshots = database
813 .get_snapshots_for_endpoint(&endpoint, params.method.as_deref(), Some(limit))
814 .await?;
815
816 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 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
863async 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 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 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 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}