Skip to main content

voirs_cli/commands/
test_api.rs

1//! API Testing Tools for VoiRS Server
2//!
3//! This module provides comprehensive API testing functionality for the VoiRS HTTP server.
4//! It includes endpoint validation, performance measurements, authentication testing,
5//! and detailed test reporting.
6//!
7//! # Features
8//!
9//! - **Endpoint Testing**: Test all server endpoints (health, voices, synthesize, stats)
10//! - **Authentication**: Validate API key authentication mechanisms
11//! - **Performance Metrics**: Measure response times, throughput, latency
12//! - **Contract Validation**: Verify API responses match expected schemas
13//! - **Test Reports**: Generate detailed JSON/Markdown test reports
14//! - **Load Testing**: Basic concurrent request testing
15//!
16//! # Usage
17//!
18//! ```bash
19//! # Test server at localhost:8080
20//! voirs test-api localhost:8080
21//!
22//! # Test with API key authentication
23//! voirs test-api localhost:8080 --api-key YOUR_KEY
24//!
25//! # Generate detailed report
26//! voirs test-api localhost:8080 --report report.json
27//!
28//! # Run load test
29//! voirs test-api localhost:8080 --load-test --concurrent 10
30//! ```
31
32use anyhow::{Context, Result};
33use chrono::{DateTime, Utc};
34use reqwest::Client;
35use serde::{Deserialize, Serialize};
36use std::time::{Duration, Instant};
37
38/// API test configuration
39#[derive(Debug, Clone)]
40pub struct ApiTestConfig {
41    /// Server URL (e.g., "http://localhost:8080")
42    pub server_url: String,
43    /// Optional API key for authentication
44    pub api_key: Option<String>,
45    /// Timeout for requests in seconds
46    pub timeout_secs: u64,
47    /// Number of concurrent requests for load testing
48    pub concurrent_requests: usize,
49    /// Enable verbose output
50    pub verbose: bool,
51}
52
53impl Default for ApiTestConfig {
54    fn default() -> Self {
55        Self {
56            server_url: "http://localhost:8080".to_string(),
57            api_key: None,
58            timeout_secs: 30,
59            concurrent_requests: 1,
60            verbose: false,
61        }
62    }
63}
64
65/// Test result for a single endpoint
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct EndpointTestResult {
68    /// Endpoint path
69    pub endpoint: String,
70    /// HTTP method
71    pub method: String,
72    /// Success status
73    pub success: bool,
74    /// Response status code
75    pub status_code: u16,
76    /// Response time in milliseconds
77    pub response_time_ms: u64,
78    /// Response body size in bytes
79    pub response_size_bytes: usize,
80    /// Error message if failed
81    pub error: Option<String>,
82    /// Timestamp of the test
83    pub timestamp: DateTime<Utc>,
84}
85
86impl EndpointTestResult {
87    /// Create a successful test result
88    fn success(
89        endpoint: String,
90        method: String,
91        status_code: u16,
92        response_time_ms: u64,
93        response_size_bytes: usize,
94    ) -> Self {
95        Self {
96            endpoint,
97            method,
98            success: true,
99            status_code,
100            response_time_ms,
101            response_size_bytes,
102            error: None,
103            timestamp: Utc::now(),
104        }
105    }
106
107    /// Create a failed test result
108    fn failure(endpoint: String, method: String, error: String) -> Self {
109        Self {
110            endpoint,
111            method,
112            success: false,
113            status_code: 0,
114            response_time_ms: 0,
115            response_size_bytes: 0,
116            error: Some(error),
117            timestamp: Utc::now(),
118        }
119    }
120}
121
122/// Complete API test report
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ApiTestReport {
125    /// Server URL tested
126    pub server_url: String,
127    /// Test start time
128    pub start_time: DateTime<Utc>,
129    /// Test end time
130    pub end_time: DateTime<Utc>,
131    /// Total duration in seconds
132    pub duration_secs: f64,
133    /// Individual endpoint results
134    pub endpoint_results: Vec<EndpointTestResult>,
135    /// Overall statistics
136    pub statistics: TestStatistics,
137    /// Load test results if applicable
138    pub load_test: Option<LoadTestResults>,
139}
140
141impl ApiTestReport {
142    /// Calculate success rate percentage
143    pub fn success_rate(&self) -> f64 {
144        if self.endpoint_results.is_empty() {
145            return 0.0;
146        }
147        let successful = self.endpoint_results.iter().filter(|r| r.success).count();
148        (successful as f64 / self.endpoint_results.len() as f64) * 100.0
149    }
150
151    /// Generate markdown report
152    pub fn to_markdown(&self) -> String {
153        let mut md = String::new();
154        md.push_str("# VoiRS API Test Report\n\n");
155        md.push_str(&format!("**Server**: {}\n", self.server_url));
156        md.push_str(&format!(
157            "**Date**: {}\n",
158            self.start_time.format("%Y-%m-%d %H:%M:%S UTC")
159        ));
160        md.push_str(&format!("**Duration**: {:.2}s\n\n", self.duration_secs));
161
162        md.push_str("## Summary\n\n");
163        md.push_str(&format!(
164            "- **Success Rate**: {:.1}%\n",
165            self.success_rate()
166        ));
167        md.push_str(&format!(
168            "- **Total Tests**: {}\n",
169            self.endpoint_results.len()
170        ));
171        md.push_str(&format!(
172            "- **Passed**: {}\n",
173            self.endpoint_results.iter().filter(|r| r.success).count()
174        ));
175        md.push_str(&format!(
176            "- **Failed**: {}\n",
177            self.endpoint_results.iter().filter(|r| !r.success).count()
178        ));
179        md.push_str(&format!(
180            "- **Avg Response Time**: {:.0}ms\n\n",
181            self.statistics.avg_response_time_ms
182        ));
183
184        md.push_str("## Endpoint Results\n\n");
185        md.push_str("| Endpoint | Method | Status | Response Time | Size | Result |\n");
186        md.push_str("|----------|--------|--------|---------------|------|--------|\n");
187
188        for result in &self.endpoint_results {
189            let status_emoji = if result.success { "āœ…" } else { "āŒ" };
190            md.push_str(&format!(
191                "| {} | {} | {} | {}ms | {} bytes | {} |\n",
192                result.endpoint,
193                result.method,
194                result.status_code,
195                result.response_time_ms,
196                result.response_size_bytes,
197                status_emoji
198            ));
199        }
200
201        if let Some(load_test) = &self.load_test {
202            md.push_str("\n## Load Test Results\n\n");
203            md.push_str(&format!(
204                "- **Concurrent Requests**: {}\n",
205                load_test.concurrent_requests
206            ));
207            md.push_str(&format!(
208                "- **Total Requests**: {}\n",
209                load_test.total_requests
210            ));
211            md.push_str(&format!(
212                "- **Successful**: {}\n",
213                load_test.successful_requests
214            ));
215            md.push_str(&format!("- **Failed**: {}\n", load_test.failed_requests));
216            md.push_str(&format!(
217                "- **Avg Latency**: {:.0}ms\n",
218                load_test.avg_latency_ms
219            ));
220            md.push_str(&format!(
221                "- **Min Latency**: {}ms\n",
222                load_test.min_latency_ms
223            ));
224            md.push_str(&format!(
225                "- **Max Latency**: {}ms\n",
226                load_test.max_latency_ms
227            ));
228            md.push_str(&format!(
229                "- **Throughput**: {:.2} req/s\n",
230                load_test.requests_per_second
231            ));
232        }
233
234        md
235    }
236}
237
238/// Test statistics summary
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct TestStatistics {
241    /// Total number of tests
242    pub total_tests: usize,
243    /// Number of successful tests
244    pub successful_tests: usize,
245    /// Number of failed tests
246    pub failed_tests: usize,
247    /// Average response time in milliseconds
248    pub avg_response_time_ms: f64,
249    /// Minimum response time in milliseconds
250    pub min_response_time_ms: u64,
251    /// Maximum response time in milliseconds
252    pub max_response_time_ms: u64,
253}
254
255/// Load test results
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct LoadTestResults {
258    /// Number of concurrent requests
259    pub concurrent_requests: usize,
260    /// Total requests sent
261    pub total_requests: usize,
262    /// Successful requests
263    pub successful_requests: usize,
264    /// Failed requests
265    pub failed_requests: usize,
266    /// Average latency in milliseconds
267    pub avg_latency_ms: f64,
268    /// Minimum latency in milliseconds
269    pub min_latency_ms: u64,
270    /// Maximum latency in milliseconds
271    pub max_latency_ms: u64,
272    /// Requests per second
273    pub requests_per_second: f64,
274}
275
276/// API tester
277pub struct ApiTester {
278    config: ApiTestConfig,
279    client: Client,
280}
281
282impl ApiTester {
283    /// Create a new API tester
284    pub fn new(config: ApiTestConfig) -> Result<Self> {
285        let client = Client::builder()
286            .timeout(Duration::from_secs(config.timeout_secs))
287            .build()
288            .context("Failed to create HTTP client")?;
289
290        Ok(Self { config, client })
291    }
292
293    /// Run all API tests
294    pub async fn run_tests(&self) -> Result<ApiTestReport> {
295        let start_time = Utc::now();
296        let mut results = Vec::new();
297
298        println!("šŸš€ Starting API tests for {}", self.config.server_url);
299        println!();
300
301        // Test health endpoint
302        println!("Testing /api/v1/health...");
303        results.push(self.test_health().await);
304
305        // Test voices endpoint
306        println!("Testing /api/v1/voices...");
307        results.push(self.test_voices().await);
308
309        // Test stats endpoint
310        println!("Testing /api/v1/stats...");
311        results.push(self.test_stats().await);
312
313        // Test synthesize endpoint
314        println!("Testing /api/v1/synthesize...");
315        results.push(self.test_synthesize().await);
316
317        // Test authentication if API key provided
318        if self.config.api_key.is_some() {
319            println!("Testing /api/v1/auth/info...");
320            results.push(self.test_auth_info().await);
321        }
322
323        let end_time = Utc::now();
324        let duration_secs = (end_time - start_time).num_milliseconds() as f64 / 1000.0;
325
326        // Calculate statistics
327        let statistics = self.calculate_statistics(&results);
328
329        // Run load test if configured
330        let load_test = if self.config.concurrent_requests > 1 {
331            println!("\nšŸ”„ Running load test...");
332            Some(self.run_load_test().await?)
333        } else {
334            None
335        };
336
337        Ok(ApiTestReport {
338            server_url: self.config.server_url.clone(),
339            start_time,
340            end_time,
341            duration_secs,
342            endpoint_results: results,
343            statistics,
344            load_test,
345        })
346    }
347
348    /// Test health endpoint
349    async fn test_health(&self) -> EndpointTestResult {
350        let url = format!("{}/api/v1/health", self.config.server_url);
351        let start = Instant::now();
352
353        match self.client.get(&url).send().await {
354            Ok(response) => {
355                let elapsed = start.elapsed().as_millis() as u64;
356                let status = response.status().as_u16();
357                match response.bytes().await {
358                    Ok(body) => EndpointTestResult::success(
359                        "/api/v1/health".to_string(),
360                        "GET".to_string(),
361                        status,
362                        elapsed,
363                        body.len(),
364                    ),
365                    Err(e) => EndpointTestResult::failure(
366                        "/api/v1/health".to_string(),
367                        "GET".to_string(),
368                        format!("Failed to read response body: {}", e),
369                    ),
370                }
371            }
372            Err(e) => EndpointTestResult::failure(
373                "/api/v1/health".to_string(),
374                "GET".to_string(),
375                format!("Request failed: {}", e),
376            ),
377        }
378    }
379
380    /// Test voices endpoint
381    async fn test_voices(&self) -> EndpointTestResult {
382        let url = format!("{}/api/v1/voices", self.config.server_url);
383        let start = Instant::now();
384
385        let mut request = self.client.get(&url);
386        if let Some(api_key) = &self.config.api_key {
387            request = request.header("Authorization", format!("Bearer {}", api_key));
388        }
389
390        match request.send().await {
391            Ok(response) => {
392                let elapsed = start.elapsed().as_millis() as u64;
393                let status = response.status().as_u16();
394                match response.bytes().await {
395                    Ok(body) => EndpointTestResult::success(
396                        "/api/v1/voices".to_string(),
397                        "GET".to_string(),
398                        status,
399                        elapsed,
400                        body.len(),
401                    ),
402                    Err(e) => EndpointTestResult::failure(
403                        "/api/v1/voices".to_string(),
404                        "GET".to_string(),
405                        format!("Failed to read response body: {}", e),
406                    ),
407                }
408            }
409            Err(e) => EndpointTestResult::failure(
410                "/api/v1/voices".to_string(),
411                "GET".to_string(),
412                format!("Request failed: {}", e),
413            ),
414        }
415    }
416
417    /// Test stats endpoint
418    async fn test_stats(&self) -> EndpointTestResult {
419        let url = format!("{}/api/v1/stats", self.config.server_url);
420        let start = Instant::now();
421
422        let mut request = self.client.get(&url);
423        if let Some(api_key) = &self.config.api_key {
424            request = request.header("Authorization", format!("Bearer {}", api_key));
425        }
426
427        match request.send().await {
428            Ok(response) => {
429                let elapsed = start.elapsed().as_millis() as u64;
430                let status = response.status().as_u16();
431                match response.bytes().await {
432                    Ok(body) => EndpointTestResult::success(
433                        "/api/v1/stats".to_string(),
434                        "GET".to_string(),
435                        status,
436                        elapsed,
437                        body.len(),
438                    ),
439                    Err(e) => EndpointTestResult::failure(
440                        "/api/v1/stats".to_string(),
441                        "GET".to_string(),
442                        format!("Failed to read response body: {}", e),
443                    ),
444                }
445            }
446            Err(e) => EndpointTestResult::failure(
447                "/api/v1/stats".to_string(),
448                "GET".to_string(),
449                format!("Request failed: {}", e),
450            ),
451        }
452    }
453
454    /// Test synthesize endpoint
455    async fn test_synthesize(&self) -> EndpointTestResult {
456        let url = format!("{}/api/v1/synthesize", self.config.server_url);
457        let start = Instant::now();
458
459        let body = serde_json::json!({
460            "text": "Hello, this is a test.",
461            "voice": "default",
462        });
463
464        let mut request = self.client.post(&url).json(&body);
465        if let Some(api_key) = &self.config.api_key {
466            request = request.header("Authorization", format!("Bearer {}", api_key));
467        }
468
469        match request.send().await {
470            Ok(response) => {
471                let elapsed = start.elapsed().as_millis() as u64;
472                let status = response.status().as_u16();
473                match response.bytes().await {
474                    Ok(body_bytes) => EndpointTestResult::success(
475                        "/api/v1/synthesize".to_string(),
476                        "POST".to_string(),
477                        status,
478                        elapsed,
479                        body_bytes.len(),
480                    ),
481                    Err(e) => EndpointTestResult::failure(
482                        "/api/v1/synthesize".to_string(),
483                        "POST".to_string(),
484                        format!("Failed to read response body: {}", e),
485                    ),
486                }
487            }
488            Err(e) => EndpointTestResult::failure(
489                "/api/v1/synthesize".to_string(),
490                "POST".to_string(),
491                format!("Request failed: {}", e),
492            ),
493        }
494    }
495
496    /// Test auth info endpoint
497    async fn test_auth_info(&self) -> EndpointTestResult {
498        let url = format!("{}/api/v1/auth/info", self.config.server_url);
499        let start = Instant::now();
500
501        let mut request = self.client.get(&url);
502        if let Some(api_key) = &self.config.api_key {
503            request = request.header("Authorization", format!("Bearer {}", api_key));
504        }
505
506        match request.send().await {
507            Ok(response) => {
508                let elapsed = start.elapsed().as_millis() as u64;
509                let status = response.status().as_u16();
510                match response.bytes().await {
511                    Ok(body) => EndpointTestResult::success(
512                        "/api/v1/auth/info".to_string(),
513                        "GET".to_string(),
514                        status,
515                        elapsed,
516                        body.len(),
517                    ),
518                    Err(e) => EndpointTestResult::failure(
519                        "/api/v1/auth/info".to_string(),
520                        "GET".to_string(),
521                        format!("Failed to read response body: {}", e),
522                    ),
523                }
524            }
525            Err(e) => EndpointTestResult::failure(
526                "/api/v1/auth/info".to_string(),
527                "GET".to_string(),
528                format!("Request failed: {}", e),
529            ),
530        }
531    }
532
533    /// Calculate test statistics
534    fn calculate_statistics(&self, results: &[EndpointTestResult]) -> TestStatistics {
535        let total_tests = results.len();
536        let successful_tests = results.iter().filter(|r| r.success).count();
537        let failed_tests = total_tests - successful_tests;
538
539        let response_times: Vec<u64> = results
540            .iter()
541            .filter(|r| r.success)
542            .map(|r| r.response_time_ms)
543            .collect();
544
545        let avg_response_time_ms = if response_times.is_empty() {
546            0.0
547        } else {
548            response_times.iter().sum::<u64>() as f64 / response_times.len() as f64
549        };
550
551        let min_response_time_ms = response_times.iter().min().copied().unwrap_or(0);
552        let max_response_time_ms = response_times.iter().max().copied().unwrap_or(0);
553
554        TestStatistics {
555            total_tests,
556            successful_tests,
557            failed_tests,
558            avg_response_time_ms,
559            min_response_time_ms,
560            max_response_time_ms,
561        }
562    }
563
564    /// Run load test
565    async fn run_load_test(&self) -> Result<LoadTestResults> {
566        let url = format!("{}/api/v1/health", self.config.server_url);
567        let concurrent = self.config.concurrent_requests;
568        let total_requests = concurrent * 10; // 10 rounds per worker
569
570        let start = Instant::now();
571        let mut handles = Vec::new();
572
573        for _ in 0..concurrent {
574            let client = self.client.clone();
575            let url = url.clone();
576            let handle = tokio::spawn(async move {
577                let mut results = Vec::new();
578                for _ in 0..10 {
579                    let req_start = Instant::now();
580                    let result = client.get(&url).send().await;
581                    let elapsed = req_start.elapsed().as_millis() as u64;
582                    results.push((result.is_ok(), elapsed));
583                }
584                results
585            });
586            handles.push(handle);
587        }
588
589        let mut all_results = Vec::new();
590        for handle in handles {
591            let results = handle.await?;
592            all_results.extend(results);
593        }
594
595        let duration = start.elapsed();
596        let successful = all_results.iter().filter(|(ok, _)| *ok).count();
597        let failed = total_requests - successful;
598
599        let latencies: Vec<u64> = all_results.iter().map(|(_, latency)| *latency).collect();
600        let avg_latency_ms = latencies.iter().sum::<u64>() as f64 / latencies.len() as f64;
601        let min_latency_ms = latencies.iter().min().copied().unwrap_or(0);
602        let max_latency_ms = latencies.iter().max().copied().unwrap_or(0);
603        let requests_per_second = total_requests as f64 / duration.as_secs_f64();
604
605        Ok(LoadTestResults {
606            concurrent_requests: concurrent,
607            total_requests,
608            successful_requests: successful,
609            failed_requests: failed,
610            avg_latency_ms,
611            min_latency_ms,
612            max_latency_ms,
613            requests_per_second,
614        })
615    }
616}
617
618/// Print test report to console
619pub fn print_report(report: &ApiTestReport) {
620    println!("\n{}", "=".repeat(60));
621    println!("  VoiRS API Test Report");
622    println!("{}\n", "=".repeat(60));
623
624    println!("Server: {}", report.server_url);
625    println!("Duration: {:.2}s", report.duration_secs);
626    println!();
627
628    println!("Summary:");
629    println!("  Success Rate: {:.1}%", report.success_rate());
630    println!("  Total Tests: {}", report.endpoint_results.len());
631    println!(
632        "  Passed: {}",
633        report.endpoint_results.iter().filter(|r| r.success).count()
634    );
635    println!(
636        "  Failed: {}",
637        report
638            .endpoint_results
639            .iter()
640            .filter(|r| !r.success)
641            .count()
642    );
643    println!(
644        "  Avg Response Time: {:.0}ms",
645        report.statistics.avg_response_time_ms
646    );
647    println!();
648
649    println!("Endpoint Results:");
650    for result in &report.endpoint_results {
651        let status = if result.success {
652            "āœ… PASS"
653        } else {
654            "āŒ FAIL"
655        };
656        println!(
657            "  {} {} {} - {} - {}ms",
658            status, result.method, result.endpoint, result.status_code, result.response_time_ms
659        );
660        if let Some(error) = &result.error {
661            println!("    Error: {}", error);
662        }
663    }
664
665    if let Some(load_test) = &report.load_test {
666        println!();
667        println!("Load Test Results:");
668        println!("  Concurrent Requests: {}", load_test.concurrent_requests);
669        println!("  Total Requests: {}", load_test.total_requests);
670        println!("  Successful: {}", load_test.successful_requests);
671        println!("  Failed: {}", load_test.failed_requests);
672        println!("  Avg Latency: {:.0}ms", load_test.avg_latency_ms);
673        println!("  Min Latency: {}ms", load_test.min_latency_ms);
674        println!("  Max Latency: {}ms", load_test.max_latency_ms);
675        println!("  Throughput: {:.2} req/s", load_test.requests_per_second);
676    }
677
678    println!("\n{}\n", "=".repeat(60));
679}
680
681/// Run API tests with the given configuration
682pub async fn run_api_tests(
683    server_url: String,
684    api_key: Option<String>,
685    concurrent: Option<usize>,
686    report_path: Option<String>,
687    verbose: bool,
688) -> Result<()> {
689    let config = ApiTestConfig {
690        server_url,
691        api_key,
692        timeout_secs: 30,
693        concurrent_requests: concurrent.unwrap_or(1),
694        verbose,
695    };
696
697    let tester = ApiTester::new(config)?;
698    let report = tester.run_tests().await?;
699
700    // Print report to console
701    print_report(&report);
702
703    // Save report if path provided
704    if let Some(path) = report_path {
705        if path.ends_with(".json") {
706            let json = serde_json::to_string_pretty(&report)
707                .context("Failed to serialize report to JSON")?;
708            std::fs::write(&path, json).context("Failed to write JSON report")?;
709            println!("šŸ“„ JSON report saved to: {}", path);
710        } else if path.ends_with(".md") {
711            let markdown = report.to_markdown();
712            std::fs::write(&path, markdown).context("Failed to write Markdown report")?;
713            println!("šŸ“„ Markdown report saved to: {}", path);
714        } else {
715            anyhow::bail!("Report path must end with .json or .md");
716        }
717    }
718
719    // Exit with error code if any tests failed
720    if report.statistics.failed_tests > 0 {
721        anyhow::bail!("{} test(s) failed", report.statistics.failed_tests);
722    }
723
724    Ok(())
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn test_api_test_config_default() {
733        let config = ApiTestConfig::default();
734        assert_eq!(config.server_url, "http://localhost:8080");
735        assert_eq!(config.timeout_secs, 30);
736        assert_eq!(config.concurrent_requests, 1);
737        assert!(!config.verbose);
738    }
739
740    #[test]
741    fn test_endpoint_test_result_success() {
742        let result = EndpointTestResult::success(
743            "/api/v1/health".to_string(),
744            "GET".to_string(),
745            200,
746            150,
747            1024,
748        );
749
750        assert!(result.success);
751        assert_eq!(result.endpoint, "/api/v1/health");
752        assert_eq!(result.method, "GET");
753        assert_eq!(result.status_code, 200);
754        assert_eq!(result.response_time_ms, 150);
755        assert_eq!(result.response_size_bytes, 1024);
756        assert!(result.error.is_none());
757    }
758
759    #[test]
760    fn test_endpoint_test_result_failure() {
761        let result = EndpointTestResult::failure(
762            "/api/v1/health".to_string(),
763            "GET".to_string(),
764            "Connection refused".to_string(),
765        );
766
767        assert!(!result.success);
768        assert_eq!(result.endpoint, "/api/v1/health");
769        assert_eq!(result.method, "GET");
770        assert_eq!(result.error, Some("Connection refused".to_string()));
771    }
772
773    #[test]
774    fn test_api_test_report_success_rate() {
775        let results = vec![
776            EndpointTestResult::success(
777                "/api/v1/health".to_string(),
778                "GET".to_string(),
779                200,
780                100,
781                512,
782            ),
783            EndpointTestResult::success(
784                "/api/v1/voices".to_string(),
785                "GET".to_string(),
786                200,
787                150,
788                1024,
789            ),
790            EndpointTestResult::failure(
791                "/api/v1/synth".to_string(),
792                "POST".to_string(),
793                "Error".to_string(),
794            ),
795        ];
796
797        let report = ApiTestReport {
798            server_url: "http://localhost:8080".to_string(),
799            start_time: Utc::now(),
800            end_time: Utc::now(),
801            duration_secs: 1.5,
802            endpoint_results: results,
803            statistics: TestStatistics {
804                total_tests: 3,
805                successful_tests: 2,
806                failed_tests: 1,
807                avg_response_time_ms: 125.0,
808                min_response_time_ms: 100,
809                max_response_time_ms: 150,
810            },
811            load_test: None,
812        };
813
814        assert!((report.success_rate() - 66.666).abs() < 0.01);
815    }
816
817    #[test]
818    fn test_markdown_report_generation() {
819        let results = vec![EndpointTestResult::success(
820            "/api/v1/health".to_string(),
821            "GET".to_string(),
822            200,
823            100,
824            512,
825        )];
826
827        let report = ApiTestReport {
828            server_url: "http://localhost:8080".to_string(),
829            start_time: Utc::now(),
830            end_time: Utc::now(),
831            duration_secs: 0.5,
832            endpoint_results: results,
833            statistics: TestStatistics {
834                total_tests: 1,
835                successful_tests: 1,
836                failed_tests: 0,
837                avg_response_time_ms: 100.0,
838                min_response_time_ms: 100,
839                max_response_time_ms: 100,
840            },
841            load_test: None,
842        };
843
844        let markdown = report.to_markdown();
845        assert!(markdown.contains("# VoiRS API Test Report"));
846        assert!(markdown.contains("http://localhost:8080"));
847        assert!(markdown.contains("/api/v1/health"));
848    }
849}