1use crate::models::{Protocol, RecordedRequest, RecordedResponse};
7use crate::query::{execute_query, QueryFilter};
8use crate::{RecorderDatabase, RecorderError, Result};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum TestFormat {
17 RustReqwest,
19 HttpFile,
21 Curl,
23 Postman,
25 K6,
27 PythonPytest,
29 JavaScriptJest,
31 GoTest,
33 RubyRspec,
35 JavaJunit,
37 CSharpXunit,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct TestGenerationConfig {
44 pub format: TestFormat,
46 pub include_assertions: bool,
48 pub validate_body: bool,
50 pub validate_status: bool,
52 pub validate_headers: bool,
54 pub validate_timing: bool,
56 pub max_duration_ms: Option<u64>,
58 pub suite_name: String,
60 pub base_url: Option<String>,
62 pub ai_descriptions: bool,
64 pub llm_config: Option<LlmConfig>,
66 pub group_by_endpoint: bool,
68 pub include_setup_teardown: bool,
70 pub generate_fixtures: bool,
72 pub suggest_edge_cases: bool,
74 pub analyze_test_gaps: bool,
76 pub deduplicate_tests: bool,
78 pub optimize_test_order: bool,
80}
81
82impl Default for TestGenerationConfig {
83 fn default() -> Self {
84 Self {
85 format: TestFormat::RustReqwest,
86 include_assertions: true,
87 validate_body: true,
88 validate_status: true,
89 validate_headers: false,
90 validate_timing: false,
91 max_duration_ms: None,
92 suite_name: "generated_tests".to_string(),
93 base_url: Some("http://localhost:3000".to_string()),
94 ai_descriptions: false,
95 llm_config: None,
96 group_by_endpoint: true,
97 include_setup_teardown: true,
98 generate_fixtures: false,
99 suggest_edge_cases: false,
100 analyze_test_gaps: false,
101 deduplicate_tests: false,
102 optimize_test_order: false,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct LlmConfig {
110 pub provider: String,
112 pub api_endpoint: String,
114 pub api_key: Option<String>,
116 pub model: String,
118 pub temperature: f64,
120}
121
122impl Default for LlmConfig {
123 fn default() -> Self {
124 Self {
125 provider: "ollama".to_string(),
126 api_endpoint: "http://localhost:11434/api/generate".to_string(),
127 api_key: None,
128 model: "llama2".to_string(),
129 temperature: 0.3,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct GeneratedTest {
137 pub name: String,
139 pub description: String,
141 pub code: String,
143 pub request_id: String,
145 pub endpoint: String,
147 pub method: String,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct TestGenerationResult {
154 pub tests: Vec<GeneratedTest>,
156 pub metadata: TestSuiteMetadata,
158 pub test_file: String,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct TestSuiteMetadata {
165 pub name: String,
167 pub test_count: usize,
169 pub endpoint_count: usize,
171 pub protocols: Vec<Protocol>,
173 pub generated_at: chrono::DateTime<chrono::Utc>,
175 pub format: TestFormat,
177 pub fixtures: Option<Vec<TestFixture>>,
179 pub edge_cases: Option<Vec<EdgeCaseSuggestion>>,
181 pub gap_analysis: Option<TestGapAnalysis>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct TestFixture {
188 pub name: String,
190 pub description: String,
192 pub data: Value,
194 pub endpoints: Vec<String>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct EdgeCaseSuggestion {
201 pub endpoint: String,
203 pub method: String,
205 pub case_type: String,
207 pub description: String,
209 pub suggested_input: Option<Value>,
211 pub expected_behavior: String,
213 pub priority: u8,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct TestGapAnalysis {
220 pub untested_endpoints: Vec<String>,
222 pub missing_methods: HashMap<String, Vec<String>>,
224 pub missing_status_codes: HashMap<String, Vec<u16>>,
226 pub missing_error_scenarios: Vec<String>,
228 pub coverage_percentage: f64,
230 pub recommendations: Vec<String>,
232}
233
234pub struct TestGenerator {
236 database: RecorderDatabase,
237 config: TestGenerationConfig,
238}
239
240impl TestGenerator {
241 pub fn new(database: RecorderDatabase, config: TestGenerationConfig) -> Self {
243 Self { database, config }
244 }
245
246 pub fn from_arc(
248 database: std::sync::Arc<RecorderDatabase>,
249 config: TestGenerationConfig,
250 ) -> Self {
251 Self {
252 database: (*database).clone(),
253 config,
254 }
255 }
256
257 pub async fn generate_from_filter(&self, filter: QueryFilter) -> Result<TestGenerationResult> {
259 let query_result = execute_query(&self.database, filter).await?;
261
262 if query_result.exchanges.is_empty() {
263 return Err(RecorderError::InvalidFilter(
264 "No recordings found matching the filter".to_string(),
265 ));
266 }
267
268 let mut tests = Vec::new();
270 let mut endpoints = std::collections::HashSet::new();
271 let mut protocols = std::collections::HashSet::new();
272
273 for exchange in &query_result.exchanges {
274 let request = &exchange.request;
275
276 let Some(response) = &exchange.response else {
278 continue;
279 };
280
281 endpoints.insert(format!("{} {}", request.method, request.path));
282 protocols.insert(request.protocol);
283
284 let test = self.generate_test_for_exchange(request, response).await?;
285 tests.push(test);
286 }
287
288 if self.config.deduplicate_tests {
290 tests = self.deduplicate_tests(tests);
291 }
292
293 if self.config.optimize_test_order {
295 tests = self.optimize_test_order(tests);
296 }
297
298 if self.config.group_by_endpoint {
300 tests.sort_by(|a, b| a.endpoint.cmp(&b.endpoint));
301 }
302
303 let fixtures = if self.config.generate_fixtures {
305 Some(self.generate_test_fixtures(&query_result.exchanges).await?)
306 } else {
307 None
308 };
309
310 let edge_cases = if self.config.suggest_edge_cases {
311 Some(self.suggest_edge_cases(&query_result.exchanges).await?)
312 } else {
313 None
314 };
315
316 let gap_analysis = if self.config.analyze_test_gaps {
317 Some(self.analyze_test_gaps(&query_result.exchanges, &tests).await?)
318 } else {
319 None
320 };
321
322 let test_file = self.generate_test_file(&tests)?;
324
325 let metadata = TestSuiteMetadata {
327 name: self.config.suite_name.clone(),
328 test_count: tests.len(),
329 endpoint_count: endpoints.len(),
330 protocols: protocols.into_iter().collect(),
331 generated_at: chrono::Utc::now(),
332 format: self.config.format.clone(),
333 fixtures,
334 edge_cases,
335 gap_analysis,
336 };
337
338 Ok(TestGenerationResult {
339 tests,
340 metadata,
341 test_file,
342 })
343 }
344
345 async fn generate_test_for_exchange(
347 &self,
348 request: &RecordedRequest,
349 response: &RecordedResponse,
350 ) -> Result<GeneratedTest> {
351 let test_name = self.generate_test_name(request);
352 let description = if self.config.ai_descriptions {
353 self.generate_ai_description(request, response).await?
354 } else {
355 format!("Test {} {}", request.method, request.path)
356 };
357
358 let code = match self.config.format {
359 TestFormat::RustReqwest => self.generate_rust_test(request, response)?,
360 TestFormat::HttpFile => self.generate_http_file(request, response)?,
361 TestFormat::Curl => self.generate_curl(request, response)?,
362 TestFormat::Postman => self.generate_postman(request, response)?,
363 TestFormat::K6 => self.generate_k6(request, response)?,
364 TestFormat::PythonPytest => self.generate_python_test(request, response)?,
365 TestFormat::JavaScriptJest => self.generate_javascript_test(request, response)?,
366 TestFormat::GoTest => self.generate_go_test(request, response)?,
367 TestFormat::RubyRspec => self.generate_ruby_test(request, response)?,
368 TestFormat::JavaJunit => self.generate_java_test(request, response)?,
369 TestFormat::CSharpXunit => self.generate_csharp_test(request, response)?,
370 };
371
372 Ok(GeneratedTest {
373 name: test_name,
374 description,
375 code,
376 request_id: request.id.clone(),
377 endpoint: request.path.clone(),
378 method: request.method.clone(),
379 })
380 }
381
382 fn generate_test_name(&self, request: &RecordedRequest) -> String {
384 let method = request.method.to_lowercase();
385 let path = request
386 .path
387 .trim_start_matches('/')
388 .replace(['/', '-'], "_")
389 .replace("{", "")
390 .replace("}", "");
391
392 format!("test_{}_{}", method, path)
393 }
394
395 async fn generate_ai_description(
397 &self,
398 request: &RecordedRequest,
399 response: &RecordedResponse,
400 ) -> Result<String> {
401 if let Some(llm_config) = &self.config.llm_config {
402 let prompt = format!(
404 "Generate a concise test description for this API call:\n\
405 Method: {}\n\
406 Path: {}\n\
407 Status: {}\n\
408 \n\
409 Describe what this endpoint does and what the test validates in one sentence.",
410 request.method, request.path, response.status_code
411 );
412
413 match self.call_llm(llm_config, &prompt).await {
414 Ok(description) => Ok(description),
415 Err(_) => Ok(format!("Test {} {}", request.method, request.path)),
416 }
417 } else {
418 Ok(format!("Test {} {}", request.method, request.path))
419 }
420 }
421
422 async fn call_llm(&self, config: &LlmConfig, prompt: &str) -> Result<String> {
424 let client = reqwest::Client::new();
425
426 match config.provider.as_str() {
427 "ollama" => {
428 let body = serde_json::json!({
429 "model": config.model,
430 "prompt": prompt,
431 "stream": false,
432 "options": {
433 "temperature": config.temperature
434 }
435 });
436
437 let response = client
438 .post(&config.api_endpoint)
439 .json(&body)
440 .send()
441 .await
442 .map_err(|e| RecorderError::Replay(format!("LLM request failed: {}", e)))?;
443
444 let result: Value = response.json().await.map_err(|e| {
445 RecorderError::Replay(format!("Failed to parse JSON response: {}", e))
446 })?;
447
448 result
449 .get("response")
450 .and_then(|v| v.as_str())
451 .map(|s| s.trim().to_string())
452 .ok_or_else(|| RecorderError::Replay("Invalid LLM response".to_string()))
453 }
454 "openai" => {
455 let body = serde_json::json!({
456 "model": config.model,
457 "messages": [
458 {"role": "system", "content": "You are a helpful assistant that generates concise test descriptions."},
459 {"role": "user", "content": prompt}
460 ],
461 "temperature": config.temperature,
462 "max_tokens": 100
463 });
464
465 let mut request_builder = client.post(&config.api_endpoint).json(&body);
466
467 if let Some(api_key) = &config.api_key {
468 request_builder =
469 request_builder.header("Authorization", format!("Bearer {}", api_key));
470 }
471
472 let response = request_builder
473 .send()
474 .await
475 .map_err(|e| RecorderError::Replay(format!("LLM request failed: {}", e)))?;
476
477 let result: Value = response.json().await.map_err(|e| {
478 RecorderError::Replay(format!("Failed to parse JSON response: {}", e))
479 })?;
480
481 result
482 .get("choices")
483 .and_then(|v| v.get(0))
484 .and_then(|v| v.get("message"))
485 .and_then(|v| v.get("content"))
486 .and_then(|v| v.as_str())
487 .map(|s| s.trim().to_string())
488 .ok_or_else(|| RecorderError::Replay("Invalid LLM response".to_string()))
489 }
490 _ => {
491 Err(RecorderError::Replay(format!("Unsupported LLM provider: {}", config.provider)))
492 }
493 }
494 }
495
496 fn generate_rust_test(
498 &self,
499 request: &RecordedRequest,
500 response: &RecordedResponse,
501 ) -> Result<String> {
502 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
503 let url = format!("{}{}", base_url, request.path);
504
505 let mut code = String::new();
506 let test_name = self.generate_test_name(request);
507
508 code.push_str("#[tokio::test]\n");
509 code.push_str(&format!("async fn {}() {{\n", test_name));
510 code.push_str(" let client = reqwest::Client::new();\n");
511 code.push_str(&format!(
512 " let response = client.{}(\"{}\")\n",
513 request.method.to_lowercase(),
514 url
515 ));
516
517 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
519 for (key, value) in headers.iter() {
520 if key.to_lowercase() != "host" {
521 code.push_str(&format!(" .header(\"{}\", \"{}\")\n", key, value));
522 }
523 }
524 }
525
526 if let Some(body) = &request.body {
528 if !body.is_empty() {
529 code.push_str(&format!(" .body(r#\"{}\"#)\n", body));
530 }
531 }
532
533 code.push_str(" .send()\n");
534 code.push_str(" .await\n");
535 code.push_str(" .expect(\"Failed to send request\");\n\n");
536
537 if self.config.validate_status {
539 code.push_str(&format!(
540 " assert_eq!(response.status().as_u16(), {});\n",
541 response.status_code
542 ));
543 }
544
545 if self.config.validate_body && response.body.is_some() {
546 code.push_str(
547 " let body = response.text().await.expect(\"Failed to read body\");\n",
548 );
549 if let Some(body) = &response.body {
550 if let Ok(_json) = serde_json::from_str::<Value>(body) {
552 code.push_str(" let json: serde_json::Value = serde_json::from_str(&body).expect(\"Invalid JSON\");\n");
553 code.push_str(" // Validate response structure\n");
554 code.push_str(" assert!(json.is_object() || json.is_array());\n");
555 }
556 }
557 }
558
559 if self.config.validate_timing {
560 if let Some(max_duration) = self.config.max_duration_ms {
561 code.push_str(&format!(
562 " // Note: Add timing validation (max {} ms)\n",
563 max_duration
564 ));
565 }
566 }
567
568 code.push_str("}\n");
569
570 Ok(code)
571 }
572
573 fn generate_http_file(
575 &self,
576 request: &RecordedRequest,
577 _response: &RecordedResponse,
578 ) -> Result<String> {
579 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
580 let mut code = String::new();
581
582 code.push_str(&format!("### {} {}\n", request.method, request.path));
583 code.push_str(&format!("{} {}{}\n", request.method, base_url, request.path));
584
585 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
587 for (key, value) in headers.iter() {
588 if key.to_lowercase() != "host" {
589 code.push_str(&format!("{}: {}\n", key, value));
590 }
591 }
592 }
593
594 if let Some(body) = &request.body {
596 if !body.is_empty() {
597 code.push('\n');
598 code.push_str(body);
599 code.push('\n');
600 }
601 }
602
603 code.push('\n');
604 Ok(code)
605 }
606
607 fn generate_curl(
609 &self,
610 request: &RecordedRequest,
611 _response: &RecordedResponse,
612 ) -> Result<String> {
613 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
614 let url = format!("{}{}", base_url, request.path);
615
616 let mut code = format!("curl -X {} '{}'", request.method, url);
617
618 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
620 for (key, value) in headers.iter() {
621 if key.to_lowercase() != "host" {
622 code.push_str(&format!(" \\\n -H '{}: {}'", key, value));
623 }
624 }
625 }
626
627 if let Some(body) = &request.body {
629 if !body.is_empty() {
630 let escaped_body = body.replace('\'', "'\\''");
631 code.push_str(&format!(" \\\n -d '{}'", escaped_body));
632 }
633 }
634
635 Ok(code)
636 }
637
638 fn generate_postman(
640 &self,
641 request: &RecordedRequest,
642 _response: &RecordedResponse,
643 ) -> Result<String> {
644 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
645
646 let mut headers_vec = Vec::new();
647 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
648 for (key, value) in headers.iter() {
649 if key.to_lowercase() != "host" {
650 headers_vec.push(serde_json::json!({
651 "key": key,
652 "value": value
653 }));
654 }
655 }
656 }
657
658 let item = serde_json::json!({
659 "name": format!("{} {}", request.method, request.path),
660 "request": {
661 "method": request.method,
662 "header": headers_vec,
663 "url": {
664 "raw": format!("{}{}", base_url, request.path),
665 "protocol": "http",
666 "host": ["localhost"],
667 "port": "3000",
668 "path": request.path.split('/').filter(|s| !s.is_empty()).collect::<Vec<_>>()
669 },
670 "body": if let Some(body) = &request.body {
671 serde_json::json!({
672 "mode": "raw",
673 "raw": body
674 })
675 } else {
676 serde_json::json!({})
677 }
678 }
679 });
680
681 serde_json::to_string_pretty(&item).map_err(RecorderError::Serialization)
682 }
683
684 fn generate_k6(
686 &self,
687 request: &RecordedRequest,
688 response: &RecordedResponse,
689 ) -> Result<String> {
690 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
691 let url = format!("{}{}", base_url, request.path);
692
693 let mut code = String::new();
694 code.push_str(&format!(" // {} {}\n", request.method, request.path));
695 code.push_str(" {\n");
696
697 let method = request.method.to_lowercase();
698
699 code.push_str(" const params = {\n");
701 code.push_str(" headers: {\n");
702 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
703 for (key, value) in headers.iter() {
704 if key.to_lowercase() != "host" {
705 code.push_str(&format!(" '{}': '{}',\n", key, value));
706 }
707 }
708 }
709 code.push_str(" },\n");
710 code.push_str(" };\n");
711
712 if let Some(body) = &request.body {
714 if !body.is_empty() {
715 code.push_str(&format!(" const payload = `{}`;\n", body));
716 code.push_str(&format!(
717 " const res = http.{}('{}', payload, params);\n",
718 method, url
719 ));
720 } else {
721 code.push_str(&format!(
722 " const res = http.{}('{}', null, params);\n",
723 method, url
724 ));
725 }
726 } else {
727 code.push_str(&format!(" const res = http.{}('{}', null, params);\n", method, url));
728 }
729
730 if self.config.validate_status {
732 code.push_str(" check(res, {\n");
733 code.push_str(&format!(
734 " 'status is {}': (r) => r.status === {},\n",
735 response.status_code, response.status_code
736 ));
737 code.push_str(" });\n");
738 }
739
740 code.push_str(" }\n");
741 Ok(code)
742 }
743
744 fn generate_python_test(
746 &self,
747 request: &RecordedRequest,
748 response: &RecordedResponse,
749 ) -> Result<String> {
750 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
751 let url = format!("{}{}", base_url, request.path);
752 let test_name = self.generate_test_name(request);
753
754 let mut code = String::new();
755 code.push_str(&format!("def {}():\n", test_name));
756
757 code.push_str(" headers = {\n");
759 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
760 for (key, value) in headers.iter() {
761 if key.to_lowercase() != "host" {
762 code.push_str(&format!(" '{}': '{}',\n", key, value));
763 }
764 }
765 }
766 code.push_str(" }\n");
767
768 let method = request.method.to_lowercase();
770 if let Some(body) = &request.body {
771 if !body.is_empty() {
772 code.push_str(&format!(" data = r'''{}'''\n", body));
773 code.push_str(&format!(
774 " response = requests.{}('{}', headers=headers, data=data)\n",
775 method, url
776 ));
777 } else {
778 code.push_str(&format!(
779 " response = requests.{}('{}', headers=headers)\n",
780 method, url
781 ));
782 }
783 } else {
784 code.push_str(&format!(
785 " response = requests.{}('{}', headers=headers)\n",
786 method, url
787 ));
788 }
789
790 if self.config.validate_status {
792 code.push_str(&format!(
793 " assert response.status_code == {}\n",
794 response.status_code
795 ));
796 }
797
798 Ok(code)
799 }
800
801 fn generate_javascript_test(
803 &self,
804 request: &RecordedRequest,
805 response: &RecordedResponse,
806 ) -> Result<String> {
807 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
808 let url = format!("{}{}", base_url, request.path);
809 let test_name = format!("{} {}", request.method, request.path);
810
811 let mut code = String::new();
812 code.push_str(&format!("test('{}', async () => {{\n", test_name));
813
814 code.push_str(" const options = {\n");
816 code.push_str(&format!(" method: '{}',\n", request.method));
817 code.push_str(" headers: {\n");
818 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
819 for (key, value) in headers.iter() {
820 if key.to_lowercase() != "host" {
821 code.push_str(&format!(" '{}': '{}',\n", key, value));
822 }
823 }
824 }
825 code.push_str(" },\n");
826
827 if let Some(body) = &request.body {
828 if !body.is_empty() {
829 code.push_str(&format!(" body: `{}`,\n", body));
830 }
831 }
832
833 code.push_str(" };\n");
834
835 code.push_str(&format!(" const response = await fetch('{}', options);\n", url));
836
837 if self.config.validate_status {
838 code.push_str(&format!(" expect(response.status).toBe({});\n", response.status_code));
839 }
840
841 if self.config.validate_body && response.body.is_some() {
842 code.push_str(" const data = await response.json();\n");
843 code.push_str(" expect(data).toBeDefined();\n");
844 }
845
846 code.push_str("});\n");
847 Ok(code)
848 }
849
850 fn generate_go_test(
852 &self,
853 request: &RecordedRequest,
854 response: &RecordedResponse,
855 ) -> Result<String> {
856 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
857 let url = format!("{}{}", base_url, request.path);
858 let test_name = self
859 .generate_test_name(request)
860 .split('_')
861 .map(|s| {
862 let mut c = s.chars();
863 match c.next() {
864 None => String::new(),
865 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
866 }
867 })
868 .collect::<String>();
869
870 let mut code = String::new();
871 code.push_str(&format!("func {}(t *testing.T) {{\n", test_name));
872
873 if let Some(body) = &request.body {
875 if !body.is_empty() {
876 code.push_str(&format!(" body := strings.NewReader(`{}`)\n", body));
877 code.push_str(&format!(
878 " req, err := http.NewRequest(\"{}\", \"{}\", body)\n",
879 request.method, url
880 ));
881 } else {
882 code.push_str(&format!(
883 " req, err := http.NewRequest(\"{}\", \"{}\", nil)\n",
884 request.method, url
885 ));
886 }
887 } else {
888 code.push_str(&format!(
889 " req, err := http.NewRequest(\"{}\", \"{}\", nil)\n",
890 request.method, url
891 ));
892 }
893
894 code.push_str(" if err != nil {\n");
895 code.push_str(" t.Fatal(err)\n");
896 code.push_str(" }\n");
897
898 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
900 for (key, value) in headers.iter() {
901 if key.to_lowercase() != "host" {
902 code.push_str(&format!(" req.Header.Set(\"{}\", \"{}\")\n", key, value));
903 }
904 }
905 }
906
907 code.push_str(" client := &http.Client{}\n");
909 code.push_str(" resp, err := client.Do(req)\n");
910 code.push_str(" if err != nil {\n");
911 code.push_str(" t.Fatal(err)\n");
912 code.push_str(" }\n");
913 code.push_str(" defer resp.Body.Close()\n\n");
914
915 if self.config.validate_status {
917 code.push_str(&format!(" if resp.StatusCode != {} {{\n", response.status_code));
918 code.push_str(&format!(
919 " t.Errorf(\"Expected status {}, got %d\", resp.StatusCode)\n",
920 response.status_code
921 ));
922 code.push_str(" }\n");
923 }
924
925 code.push_str("}\n");
926 Ok(code)
927 }
928
929 fn generate_ruby_test(
931 &self,
932 request: &RecordedRequest,
933 response: &RecordedResponse,
934 ) -> Result<String> {
935 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
936 let url = format!("{}{}", base_url, request.path);
937 let test_name = request
938 .path
939 .trim_start_matches('/')
940 .replace(['/', '-'], " ")
941 .replace("{", "")
942 .replace("}", "");
943
944 let mut code = String::new();
945 code.push_str(&format!(" it \"should {} {}\" do\n", request.method, test_name));
946
947 let mut request_params = vec![format!("method: :{}", request.method.to_lowercase())];
949
950 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
952 let header_items: Vec<String> = headers
953 .iter()
954 .filter(|(k, _)| k.to_lowercase() != "host")
955 .map(|(k, v)| format!("'{}' => '{}'", k, v))
956 .collect();
957 if !header_items.is_empty() {
958 request_params.push(format!("headers: {{ {} }}", header_items.join(", ")));
959 }
960 }
961
962 if let Some(body) = &request.body {
964 if !body.is_empty() {
965 let escaped_body = body.replace('\'', "\\'").replace('\n', "\\n");
966 request_params.push(format!("body: '{}'", escaped_body));
967 }
968 }
969
970 code.push_str(&format!(
971 " response = HTTParty.{}('{}', {})\n",
972 request.method.to_lowercase(),
973 url,
974 request_params.join(", ")
975 ));
976
977 if self.config.validate_status {
979 code.push_str(&format!(" expect(response.code).to eq({})\n", response.status_code));
980 }
981
982 if self.config.validate_body && response.body.is_some() {
983 if let Some(body) = &response.body {
984 if serde_json::from_str::<Value>(body).is_ok() {
985 code.push_str(" expect(response.parsed_response).not_to be_nil\n");
986 }
987 }
988 }
989
990 code.push_str(" end\n");
991 Ok(code)
992 }
993
994 fn generate_java_test(
996 &self,
997 request: &RecordedRequest,
998 response: &RecordedResponse,
999 ) -> Result<String> {
1000 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
1001 let url = format!("{}{}", base_url, request.path);
1002 let test_name = self
1003 .generate_test_name(request)
1004 .split('_')
1005 .enumerate()
1006 .map(|(i, s)| {
1007 if i == 0 {
1008 s.to_string()
1009 } else {
1010 let mut c = s.chars();
1011 match c.next() {
1012 None => String::new(),
1013 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
1014 }
1015 }
1016 })
1017 .collect::<String>();
1018
1019 let mut code = String::new();
1020 code.push_str(" @Test\n");
1021 code.push_str(&format!(" public void {}() throws Exception {{\n", test_name));
1022
1023 code.push_str(" HttpRequest request = HttpRequest.newBuilder()\n");
1025 code.push_str(&format!(" .uri(URI.create(\"{}\"))\n", url));
1026 code.push_str(&format!(" .method(\"{}\", ", request.method));
1027
1028 if let Some(body) = &request.body {
1029 if !body.is_empty() {
1030 let escaped_body = body.replace('"', "\\\"").replace('\n', "\\n");
1031 code.push_str(&format!(
1032 "HttpRequest.BodyPublishers.ofString(\"{}\"))\n",
1033 escaped_body
1034 ));
1035 } else {
1036 code.push_str("HttpRequest.BodyPublishers.noBody())\n");
1037 }
1038 } else {
1039 code.push_str("HttpRequest.BodyPublishers.noBody())\n");
1040 }
1041
1042 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
1044 for (key, value) in headers.iter() {
1045 if key.to_lowercase() != "host" {
1046 code.push_str(&format!(" .header(\"{}\", \"{}\")\n", key, value));
1047 }
1048 }
1049 }
1050
1051 code.push_str(" .build();\n\n");
1052
1053 code.push_str(" HttpClient client = HttpClient.newHttpClient();\n");
1055 code.push_str(" HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());\n\n");
1056
1057 if self.config.validate_status {
1059 code.push_str(&format!(
1060 " assertEquals({}, response.statusCode());\n",
1061 response.status_code
1062 ));
1063 }
1064
1065 if self.config.validate_body && response.body.is_some() {
1066 if let Some(body) = &response.body {
1067 if serde_json::from_str::<Value>(body).is_ok() {
1068 code.push_str(" assertNotNull(response.body());\n");
1069 }
1070 }
1071 }
1072
1073 code.push_str(" }\n");
1074 Ok(code)
1075 }
1076
1077 fn generate_csharp_test(
1079 &self,
1080 request: &RecordedRequest,
1081 response: &RecordedResponse,
1082 ) -> Result<String> {
1083 let base_url = self.config.base_url.as_deref().unwrap_or("http://localhost:3000");
1084 let url = format!("{}{}", base_url, request.path);
1085 let test_name = self
1086 .generate_test_name(request)
1087 .split('_')
1088 .map(|s| {
1089 let mut c = s.chars();
1090 match c.next() {
1091 None => String::new(),
1092 Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
1093 }
1094 })
1095 .collect::<String>();
1096
1097 let mut code = String::new();
1098 code.push_str(" [Fact]\n");
1099 code.push_str(&format!(" public async Task {}Async()\n", test_name));
1100 code.push_str(" {\n");
1101
1102 code.push_str(" using var client = new HttpClient();\n");
1104
1105 let method_name = if request.method.is_empty() {
1108 "Get".to_string()
1109 } else {
1110 request.method.chars().next().unwrap_or('G').to_uppercase().collect::<String>()
1111 + &request.method.get(1..).unwrap_or("et").to_lowercase()
1112 };
1113 code.push_str(&format!(
1114 " var request = new HttpRequestMessage(HttpMethod.{}, \"{}\");\n",
1115 method_name, url
1116 ));
1117
1118 if let Ok(headers) = serde_json::from_str::<HashMap<String, String>>(&request.headers) {
1120 for (key, value) in headers.iter() {
1121 if key.to_lowercase() != "host" && key.to_lowercase() != "content-type" {
1122 code.push_str(&format!(
1123 " request.Headers.Add(\"{}\", \"{}\");\n",
1124 key, value
1125 ));
1126 }
1127 }
1128 }
1129
1130 if let Some(body) = &request.body {
1132 if !body.is_empty() {
1133 let escaped_body = body.replace('"', "\\\"").replace('\n', "\\n");
1134 code.push_str(&format!(" request.Content = new StringContent(\"{}\", Encoding.UTF8, \"application/json\");\n",
1135 escaped_body));
1136 }
1137 }
1138
1139 code.push_str(" var response = await client.SendAsync(request);\n\n");
1141
1142 if self.config.validate_status {
1144 code.push_str(&format!(
1145 " Assert.Equal({}, (int)response.StatusCode);\n",
1146 response.status_code
1147 ));
1148 }
1149
1150 if self.config.validate_body && response.body.is_some() {
1151 code.push_str(
1152 " var content = await response.Content.ReadAsStringAsync();\n",
1153 );
1154 code.push_str(" Assert.NotNull(content);\n");
1155 code.push_str(" Assert.NotEmpty(content);\n");
1156 }
1157
1158 code.push_str(" }\n");
1159 Ok(code)
1160 }
1161
1162 fn generate_test_file(&self, tests: &[GeneratedTest]) -> Result<String> {
1164 let mut file = String::new();
1165
1166 match self.config.format {
1167 TestFormat::RustReqwest => {
1168 file.push_str("// Generated test file\n");
1169 file.push_str("// Run with: cargo test\n\n");
1170 if self.config.include_setup_teardown {
1171 file.push_str("use reqwest;\n");
1172 file.push_str("use serde_json::Value;\n\n");
1173 }
1174
1175 for test in tests {
1176 file.push_str(&test.code);
1177 file.push('\n');
1178 }
1179 }
1180 TestFormat::HttpFile => {
1181 for test in tests {
1182 file.push_str(&test.code);
1183 file.push('\n');
1184 }
1185 }
1186 TestFormat::Curl => {
1187 file.push_str("#!/bin/bash\n");
1188 file.push_str("# Generated cURL commands\n\n");
1189 for test in tests {
1190 file.push_str(&format!("# {} {}\n", test.method, test.endpoint));
1191 file.push_str(&test.code);
1192 file.push_str("\n\n");
1193 }
1194 }
1195 TestFormat::Postman => {
1196 let collection = serde_json::json!({
1197 "info": {
1198 "name": self.config.suite_name,
1199 "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
1200 },
1201 "item": tests.iter().map(|t| {
1202 serde_json::from_str::<Value>(&t.code).unwrap_or(Value::Null)
1203 }).collect::<Vec<_>>()
1204 });
1205 file = serde_json::to_string_pretty(&collection)
1206 .map_err(RecorderError::Serialization)?;
1207 }
1208 TestFormat::K6 => {
1209 file.push_str("import http from 'k6/http';\n");
1210 file.push_str("import { check, sleep } from 'k6';\n\n");
1211 file.push_str("export const options = {\n");
1212 file.push_str(" vus: 10,\n");
1213 file.push_str(" duration: '30s',\n");
1214 file.push_str("};\n\n");
1215 file.push_str("export default function() {\n");
1216 for test in tests {
1217 file.push_str(&test.code);
1218 }
1219 file.push_str(" sleep(1);\n");
1220 file.push_str("}\n");
1221 }
1222 TestFormat::PythonPytest => {
1223 file.push_str("# Generated test file\n");
1224 file.push_str("# Run with: pytest\n\n");
1225 file.push_str("import requests\n");
1226 file.push_str("import pytest\n\n");
1227 for test in tests {
1228 file.push_str(&test.code);
1229 file.push('\n');
1230 }
1231 }
1232 TestFormat::JavaScriptJest => {
1233 file.push_str("// Generated test file\n");
1234 file.push_str("// Run with: npm test\n\n");
1235 file.push_str(&format!("describe('{}', () => {{\n", self.config.suite_name));
1236 for test in tests {
1237 file.push_str(" ");
1238 file.push_str(&test.code.replace("\n", "\n "));
1239 file.push('\n');
1240 }
1241 file.push_str("});\n");
1242 }
1243 TestFormat::GoTest => {
1244 file.push_str("package main\n\n");
1245 file.push_str("import (\n");
1246 file.push_str(" \"net/http\"\n");
1247 file.push_str(" \"strings\"\n");
1248 file.push_str(" \"testing\"\n");
1249 file.push_str(")\n\n");
1250 for test in tests {
1251 file.push_str(&test.code);
1252 file.push('\n');
1253 }
1254 }
1255 TestFormat::RubyRspec => {
1256 file.push_str("# Generated test file\n");
1257 file.push_str("# Run with: rspec spec/api_spec.rb\n\n");
1258 file.push_str("require 'httparty'\n");
1259 file.push_str("require 'rspec'\n\n");
1260 file.push_str(&format!("RSpec.describe '{}' do\n", self.config.suite_name));
1261 for test in tests {
1262 file.push_str(&test.code);
1263 file.push('\n');
1264 }
1265 file.push_str("end\n");
1266 }
1267 TestFormat::JavaJunit => {
1268 file.push_str("// Generated test file\n");
1269 file.push_str("// Run with: mvn test or gradle test\n\n");
1270 file.push_str("import org.junit.jupiter.api.Test;\n");
1271 file.push_str("import static org.junit.jupiter.api.Assertions.*;\n");
1272 file.push_str("import java.net.URI;\n");
1273 file.push_str("import java.net.http.HttpClient;\n");
1274 file.push_str("import java.net.http.HttpRequest;\n");
1275 file.push_str("import java.net.http.HttpResponse;\n\n");
1276 file.push_str(&format!(
1277 "public class {} {{\n",
1278 self.config.suite_name.replace("-", "_")
1279 ));
1280 for test in tests {
1281 file.push_str(&test.code);
1282 file.push('\n');
1283 }
1284 file.push_str("}\n");
1285 }
1286 TestFormat::CSharpXunit => {
1287 file.push_str("// Generated test file\n");
1288 file.push_str("// Run with: dotnet test\n\n");
1289 file.push_str("using System;\n");
1290 file.push_str("using System.Net.Http;\n");
1291 file.push_str("using System.Text;\n");
1292 file.push_str("using System.Threading.Tasks;\n");
1293 file.push_str("using Xunit;\n\n");
1294 file.push_str(&format!("namespace {}\n", self.config.suite_name.replace("-", "_")));
1295 file.push_str("{\n");
1296 file.push_str(" public class ApiTests\n");
1297 file.push_str(" {\n");
1298 for test in tests {
1299 file.push_str(&test.code);
1300 file.push('\n');
1301 }
1302 file.push_str(" }\n");
1303 file.push_str("}\n");
1304 }
1305 }
1306
1307 Ok(file)
1308 }
1309
1310 fn deduplicate_tests(&self, tests: Vec<GeneratedTest>) -> Vec<GeneratedTest> {
1312 let mut unique_tests = Vec::new();
1313 let mut seen_signatures = std::collections::HashSet::new();
1314
1315 for test in tests {
1316 let signature = format!("{}:{}:{}", test.method, test.endpoint, test.code.len());
1318
1319 if !seen_signatures.contains(&signature) {
1320 seen_signatures.insert(signature);
1321 unique_tests.push(test);
1322 }
1323 }
1324
1325 unique_tests
1326 }
1327
1328 fn optimize_test_order(&self, mut tests: Vec<GeneratedTest>) -> Vec<GeneratedTest> {
1330 tests.sort_by(|a, b| {
1335 let order_a = match a.method.as_str() {
1336 "GET" | "HEAD" => 0,
1337 "POST" | "PUT" | "PATCH" => 1,
1338 "DELETE" => 2,
1339 _ => 3,
1340 };
1341 let order_b = match b.method.as_str() {
1342 "GET" | "HEAD" => 0,
1343 "POST" | "PUT" | "PATCH" => 1,
1344 "DELETE" => 2,
1345 _ => 3,
1346 };
1347 order_a.cmp(&order_b).then_with(|| a.endpoint.cmp(&b.endpoint))
1348 });
1349
1350 tests
1351 }
1352
1353 async fn generate_test_fixtures(
1355 &self,
1356 exchanges: &[crate::models::RecordedExchange],
1357 ) -> Result<Vec<TestFixture>> {
1358 let llm_config = if let Some(config) = self.config.llm_config.as_ref() {
1359 config
1360 } else {
1361 return Ok(Vec::new());
1362 };
1363 let mut fixtures = Vec::new();
1364
1365 let mut endpoint_data: HashMap<String, Vec<&crate::models::RecordedExchange>> =
1367 HashMap::new();
1368 for exchange in exchanges {
1369 let endpoint = format!("{} {}", exchange.request.method, exchange.request.path);
1370 endpoint_data.entry(endpoint).or_default().push(exchange);
1371 }
1372
1373 for (endpoint, endpoint_exchanges) in endpoint_data.iter().take(5) {
1375 let mut sample_bodies = Vec::new();
1377 for exchange in endpoint_exchanges.iter().take(3) {
1378 if let Some(body) = &exchange.request.body {
1379 if !body.is_empty() {
1380 if let Ok(json) = serde_json::from_str::<Value>(body) {
1381 sample_bodies.push(json);
1382 }
1383 }
1384 }
1385 }
1386
1387 if sample_bodies.is_empty() {
1388 continue;
1389 }
1390
1391 let prompt = format!(
1392 "Based on these sample API request bodies for endpoint '{}', generate a reusable test fixture in JSON format:\n{}\n\nProvide a clean JSON object with varied test data including edge cases.",
1393 endpoint,
1394 serde_json::to_string_pretty(&sample_bodies).unwrap_or_default()
1395 );
1396
1397 if let Ok(response) = self.call_llm(llm_config, &prompt).await {
1398 if let Ok(data) = serde_json::from_str::<Value>(&response) {
1400 fixtures.push(TestFixture {
1401 name: format!("fixture_{}", endpoint.replace([' ', '/'], "_")),
1402 description: format!("Test fixture for {}", endpoint),
1403 data,
1404 endpoints: vec![endpoint.clone()],
1405 });
1406 }
1407 }
1408 }
1409
1410 Ok(fixtures)
1411 }
1412
1413 async fn suggest_edge_cases(
1415 &self,
1416 exchanges: &[crate::models::RecordedExchange],
1417 ) -> Result<Vec<EdgeCaseSuggestion>> {
1418 let llm_config = if let Some(config) = self.config.llm_config.as_ref() {
1419 config
1420 } else {
1421 return Ok(Vec::new());
1422 };
1423 let mut edge_cases = Vec::new();
1424
1425 let mut endpoint_data: HashMap<String, Vec<&crate::models::RecordedExchange>> =
1427 HashMap::new();
1428 for exchange in exchanges {
1429 let key = format!("{} {}", exchange.request.method, exchange.request.path);
1430 endpoint_data.entry(key).or_default().push(exchange);
1431 }
1432
1433 for (endpoint_key, endpoint_exchanges) in endpoint_data.iter().take(5) {
1434 let parts: Vec<&str> = endpoint_key.splitn(2, ' ').collect();
1435 if parts.len() != 2 {
1436 continue;
1437 }
1438 let (method, endpoint) = (parts[0], parts[1]);
1439
1440 let sample_exchange = endpoint_exchanges.first();
1442 let sample_body = sample_exchange
1443 .and_then(|e| e.request.body.as_ref())
1444 .map(|s| s.as_str())
1445 .unwrap_or("{}");
1446
1447 let prompt = format!(
1448 "Suggest 3 critical edge cases to test for this API endpoint:\n\
1449 Method: {}\n\
1450 Path: {}\n\
1451 Sample Request: {}\n\n\
1452 For each edge case, provide:\n\
1453 1. Type (e.g., 'validation', 'boundary', 'security')\n\
1454 2. Description\n\
1455 3. Expected behavior\n\
1456 4. Priority (1-5)\n\n\
1457 Format: type|description|behavior|priority",
1458 method, endpoint, sample_body
1459 );
1460
1461 if let Ok(response) = self.call_llm(llm_config, &prompt).await {
1462 for line in response.lines().take(3) {
1464 let parts: Vec<&str> = line.split('|').collect();
1465 if parts.len() >= 4 {
1466 let priority = parts[3].trim().parse::<u8>().unwrap_or(3);
1467 edge_cases.push(EdgeCaseSuggestion {
1468 endpoint: endpoint.to_string(),
1469 method: method.to_string(),
1470 case_type: parts[0].trim().to_string(),
1471 description: parts[1].trim().to_string(),
1472 suggested_input: None,
1473 expected_behavior: parts[2].trim().to_string(),
1474 priority,
1475 });
1476 }
1477 }
1478 }
1479 }
1480
1481 Ok(edge_cases)
1482 }
1483
1484 async fn analyze_test_gaps(
1486 &self,
1487 exchanges: &[crate::models::RecordedExchange],
1488 tests: &[GeneratedTest],
1489 ) -> Result<TestGapAnalysis> {
1490 let mut all_endpoints = std::collections::HashSet::new();
1492 let mut method_by_endpoint: HashMap<String, std::collections::HashSet<String>> =
1493 HashMap::new();
1494 let mut status_codes_by_endpoint: HashMap<String, std::collections::HashSet<u16>> =
1495 HashMap::new();
1496
1497 for exchange in exchanges {
1498 let endpoint = exchange.request.path.clone();
1499 let method = exchange.request.method.clone();
1500 all_endpoints.insert(endpoint.clone());
1501 method_by_endpoint.entry(endpoint.clone()).or_default().insert(method);
1502
1503 if let Some(response) = &exchange.response {
1504 let status_code = response.status_code as u16;
1505 status_codes_by_endpoint
1506 .entry(endpoint.clone())
1507 .or_default()
1508 .insert(status_code);
1509 }
1510 }
1511
1512 let mut tested_endpoints = std::collections::HashSet::new();
1514 for test in tests {
1515 tested_endpoints.insert(test.endpoint.clone());
1516 }
1517
1518 let untested_endpoints: Vec<String> =
1520 all_endpoints.difference(&tested_endpoints).cloned().collect();
1521
1522 let mut missing_methods: HashMap<String, Vec<String>> = HashMap::new();
1523 for (endpoint, methods) in &method_by_endpoint {
1524 let tested_methods: std::collections::HashSet<String> = tests
1525 .iter()
1526 .filter(|t| &t.endpoint == endpoint)
1527 .map(|t| t.method.clone())
1528 .collect();
1529
1530 let missing: Vec<String> = methods.difference(&tested_methods).cloned().collect();
1531
1532 if !missing.is_empty() {
1533 missing_methods.insert(endpoint.clone(), missing);
1534 }
1535 }
1536
1537 let mut missing_status_codes: HashMap<String, Vec<u16>> = HashMap::new();
1538 for (endpoint, codes) in &status_codes_by_endpoint {
1539 let has_error_tests = codes.iter().any(|c| *c >= 400);
1541 if has_error_tests {
1542 missing_status_codes.insert(
1543 endpoint.clone(),
1544 codes.iter().filter(|c| **c >= 400).copied().collect(),
1545 );
1546 }
1547 }
1548
1549 let missing_error_scenarios = vec![
1550 "401 Unauthorized scenarios".to_string(),
1551 "403 Forbidden scenarios".to_string(),
1552 "404 Not Found scenarios".to_string(),
1553 "429 Rate Limiting scenarios".to_string(),
1554 "500 Internal Server Error scenarios".to_string(),
1555 ];
1556
1557 let coverage_percentage = if all_endpoints.is_empty() {
1558 100.0
1559 } else {
1560 (tested_endpoints.len() as f64 / all_endpoints.len() as f64) * 100.0
1561 };
1562
1563 let mut recommendations = Vec::new();
1564 if !untested_endpoints.is_empty() {
1565 recommendations
1566 .push(format!("Add tests for {} untested endpoints", untested_endpoints.len()));
1567 }
1568 if !missing_methods.is_empty() {
1569 recommendations.push(format!(
1570 "Add tests for missing HTTP methods on {} endpoints",
1571 missing_methods.len()
1572 ));
1573 }
1574 if coverage_percentage < 80.0 {
1575 recommendations.push("Increase test coverage to at least 80%".to_string());
1576 }
1577
1578 Ok(TestGapAnalysis {
1579 untested_endpoints,
1580 missing_methods,
1581 missing_status_codes,
1582 missing_error_scenarios,
1583 coverage_percentage,
1584 recommendations,
1585 })
1586 }
1587}
1588
1589#[cfg(test)]
1590mod tests {
1591 use super::*;
1592 use crate::models::{Protocol, RecordedRequest, RecordedResponse};
1593
1594 #[tokio::test]
1595 async fn test_generate_test_name() {
1596 let database = RecorderDatabase::new_in_memory().await.unwrap();
1597 let config = TestGenerationConfig::default();
1598 let generator = TestGenerator::new(database, config);
1599
1600 let request = RecordedRequest {
1601 id: "test".to_string(),
1602 protocol: Protocol::Http,
1603 timestamp: chrono::Utc::now(),
1604 method: "GET".to_string(),
1605 path: "/api/users/123".to_string(),
1606 query_params: None,
1607 headers: "{}".to_string(),
1608 body: None,
1609 body_encoding: "utf-8".to_string(),
1610 status_code: Some(200),
1611 duration_ms: Some(50),
1612 client_ip: None,
1613 trace_id: None,
1614 span_id: None,
1615 tags: None,
1616 };
1617
1618 let name = generator.generate_test_name(&request);
1619 assert_eq!(name, "test_get_api_users_123");
1620 }
1621
1622 #[test]
1623 fn test_default_config() {
1624 let config = TestGenerationConfig::default();
1625 assert_eq!(config.format, TestFormat::RustReqwest);
1626 assert!(config.include_assertions);
1627 assert!(config.validate_body);
1628 assert!(config.validate_status);
1629 }
1630
1631 #[tokio::test]
1632 async fn test_generate_rust_test() {
1633 let database = RecorderDatabase::new_in_memory().await.unwrap();
1634 let config = TestGenerationConfig::default();
1635 let generator = TestGenerator::new(database, config);
1636
1637 let request = RecordedRequest {
1638 id: "test-1".to_string(),
1639 protocol: Protocol::Http,
1640 timestamp: chrono::Utc::now(),
1641 method: "GET".to_string(),
1642 path: "/api/users".to_string(),
1643 query_params: None,
1644 headers: r#"{"content-type":"application/json"}"#.to_string(),
1645 body: None,
1646 body_encoding: "utf-8".to_string(),
1647 status_code: Some(200),
1648 duration_ms: Some(45),
1649 client_ip: Some("127.0.0.1".to_string()),
1650 trace_id: None,
1651 span_id: None,
1652 tags: None,
1653 };
1654
1655 let response = RecordedResponse {
1656 request_id: "test-1".to_string(),
1657 status_code: 200,
1658 headers: r#"{"content-type":"application/json"}"#.to_string(),
1659 body: Some(r#"{"users":[]}"#.to_string()),
1660 body_encoding: "utf-8".to_string(),
1661 size_bytes: 12,
1662 timestamp: chrono::Utc::now(),
1663 };
1664
1665 let code = generator.generate_rust_test(&request, &response).unwrap();
1666
1667 assert!(code.contains("#[tokio::test]"));
1668 assert!(code.contains("async fn test_get_api_users()"));
1669 assert!(code.contains("reqwest::Client::new()"));
1670 assert!(code.contains("assert_eq!(response.status().as_u16(), 200)"));
1671 }
1672
1673 #[tokio::test]
1674 async fn test_generate_curl() {
1675 let database = RecorderDatabase::new_in_memory().await.unwrap();
1676 let config = TestGenerationConfig::default();
1677 let generator = TestGenerator::new(database, config);
1678
1679 let request = RecordedRequest {
1680 id: "test-2".to_string(),
1681 protocol: Protocol::Http,
1682 timestamp: chrono::Utc::now(),
1683 method: "POST".to_string(),
1684 path: "/api/users".to_string(),
1685 query_params: None,
1686 headers: r#"{"content-type":"application/json"}"#.to_string(),
1687 body: Some(r#"{"name":"John"}"#.to_string()),
1688 body_encoding: "utf-8".to_string(),
1689 status_code: Some(201),
1690 duration_ms: Some(80),
1691 client_ip: None,
1692 trace_id: None,
1693 span_id: None,
1694 tags: None,
1695 };
1696
1697 let response = RecordedResponse {
1698 request_id: "test-2".to_string(),
1699 status_code: 201,
1700 headers: r#"{}"#.to_string(),
1701 body: None,
1702 body_encoding: "utf-8".to_string(),
1703 size_bytes: 0,
1704 timestamp: chrono::Utc::now(),
1705 };
1706
1707 let code = generator.generate_curl(&request, &response).unwrap();
1708
1709 assert!(code.contains("curl -X POST"));
1710 assert!(code.contains("/api/users"));
1711 assert!(code.contains("-H 'content-type: application/json'"));
1712 assert!(code.contains(r#"-d '{"name":"John"}'"#));
1713 }
1714
1715 #[tokio::test]
1716 async fn test_generate_http_file() {
1717 let database = RecorderDatabase::new_in_memory().await.unwrap();
1718 let config = TestGenerationConfig::default();
1719 let generator = TestGenerator::new(database, config);
1720
1721 let request = RecordedRequest {
1722 id: "test-3".to_string(),
1723 protocol: Protocol::Http,
1724 timestamp: chrono::Utc::now(),
1725 method: "DELETE".to_string(),
1726 path: "/api/users/123".to_string(),
1727 query_params: None,
1728 headers: r#"{}"#.to_string(),
1729 body: None,
1730 body_encoding: "utf-8".to_string(),
1731 status_code: Some(204),
1732 duration_ms: Some(30),
1733 client_ip: None,
1734 trace_id: None,
1735 span_id: None,
1736 tags: None,
1737 };
1738
1739 let response = RecordedResponse {
1740 request_id: "test-3".to_string(),
1741 status_code: 204,
1742 headers: r#"{}"#.to_string(),
1743 body: None,
1744 body_encoding: "utf-8".to_string(),
1745 size_bytes: 0,
1746 timestamp: chrono::Utc::now(),
1747 };
1748
1749 let code = generator.generate_http_file(&request, &response).unwrap();
1750
1751 assert!(code.contains("### DELETE /api/users/123"));
1752 assert!(code.contains("DELETE http://localhost:3000/api/users/123"));
1753 }
1754
1755 #[test]
1756 fn test_llm_config_defaults() {
1757 let config = LlmConfig::default();
1758 assert_eq!(config.provider, "ollama");
1759 assert_eq!(config.model, "llama2");
1760 assert_eq!(config.temperature, 0.3);
1761 }
1762
1763 #[test]
1764 fn test_test_format_variants() {
1765 assert_eq!(TestFormat::RustReqwest, TestFormat::RustReqwest);
1766 assert_ne!(TestFormat::RustReqwest, TestFormat::Curl);
1767 assert_ne!(TestFormat::PythonPytest, TestFormat::JavaScriptJest);
1768 }
1769}