1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct IntegrationWorkflow {
14 pub id: String,
16 pub name: String,
18 pub description: String,
20 pub steps: Vec<WorkflowStep>,
22 pub setup: WorkflowSetup,
24 pub cleanup: Vec<WorkflowStep>,
26 pub created_at: DateTime<Utc>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct WorkflowSetup {
33 pub variables: HashMap<String, String>,
35 pub base_url: String,
37 pub headers: HashMap<String, String>,
39 pub timeout_ms: u64,
41}
42
43impl Default for WorkflowSetup {
44 fn default() -> Self {
45 Self {
46 variables: HashMap::new(),
47 base_url: "http://localhost:3000".to_string(),
48 headers: HashMap::new(),
49 timeout_ms: 30000,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct WorkflowStep {
57 pub id: String,
59 pub name: String,
61 pub description: String,
63 pub request: StepRequest,
65 pub validation: StepValidation,
67 pub extract: Vec<VariableExtraction>,
69 pub condition: Option<StepCondition>,
71 pub delay_ms: Option<u64>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct StepRequest {
78 pub method: String,
80 pub path: String,
82 pub headers: HashMap<String, String>,
84 pub body: Option<String>,
86 pub query_params: HashMap<String, String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct StepValidation {
93 pub status_code: Option<u16>,
95 pub body_assertions: Vec<BodyAssertion>,
97 pub header_assertions: Vec<HeaderAssertion>,
99 pub max_response_time_ms: Option<u64>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct BodyAssertion {
106 pub path: String,
108 pub assertion_type: AssertionType,
110 pub expected: Value,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
116#[serde(rename_all = "snake_case")]
117pub enum AssertionType {
118 Equals,
120 NotEquals,
122 Contains,
124 Matches,
126 GreaterThan,
128 LessThan,
130 Exists,
132 NotNull,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct HeaderAssertion {
139 pub name: String,
141 pub expected: String,
143 pub regex: bool,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct VariableExtraction {
150 pub name: String,
152 pub source: ExtractionSource,
154 pub pattern: String,
156 pub default: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
162#[serde(rename_all = "snake_case")]
163pub enum ExtractionSource {
164 Body,
166 Header,
168 StatusCode,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct StepCondition {
175 pub variable: String,
177 pub operator: ConditionOperator,
179 pub value: String,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
185#[serde(rename_all = "snake_case")]
186pub enum ConditionOperator {
187 Equals,
189 NotEquals,
191 Contains,
193 Exists,
195 GreaterThan,
197 LessThan,
199}
200
201#[derive(Debug, Clone)]
203pub struct WorkflowState {
204 pub variables: HashMap<String, String>,
206 pub history: Vec<StepExecution>,
208 pub current_step: usize,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct StepExecution {
215 pub step_id: String,
217 pub step_name: String,
219 pub executed_at: DateTime<Utc>,
221 pub request: ExecutedRequest,
223 pub response: ExecutedResponse,
225 pub validation_result: ValidationResult,
227 pub extracted_variables: HashMap<String, String>,
229 pub duration_ms: u64,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ExecutedRequest {
236 pub method: String,
238 pub url: String,
240 pub headers: HashMap<String, String>,
242 pub body: Option<String>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct ExecutedResponse {
249 pub status_code: u16,
251 pub headers: HashMap<String, String>,
253 pub body: String,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct ValidationResult {
260 pub success: bool,
262 pub assertions: Vec<AssertionResult>,
264 pub errors: Vec<String>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct AssertionResult {
271 pub description: String,
273 pub success: bool,
275 pub expected: String,
277 pub actual: String,
279}
280
281pub struct IntegrationTestGenerator {
283 workflow: IntegrationWorkflow,
285}
286
287impl IntegrationTestGenerator {
288 pub fn new(workflow: IntegrationWorkflow) -> Self {
290 Self { workflow }
291 }
292
293 pub fn generate_rust_test(&self) -> String {
295 let mut code = String::new();
296
297 code.push_str("use reqwest;\n");
299 code.push_str("use serde_json::{json, Value};\n");
300 code.push_str("use std::collections::HashMap;\n\n");
301
302 code.push_str("#[tokio::test]\n");
304 code.push_str(&format!("async fn test_{}() {{\n", self.sanitize_name(&self.workflow.name)));
305
306 code.push_str(" let client = reqwest::Client::new();\n");
308 code.push_str(&format!(" let base_url = \"{}\";\n", self.workflow.setup.base_url));
309
310 code.push_str(" let mut variables: HashMap<String, String> = HashMap::new();\n");
312 for (key, value) in &self.workflow.setup.variables {
313 code.push_str(&format!(
314 " variables.insert(\"{}\".to_string(), \"{}\".to_string());\n",
315 key, value
316 ));
317 }
318 code.push('\n');
319
320 for (idx, step) in self.workflow.steps.iter().enumerate() {
322 code.push_str(&format!(" // Step {}: {}\n", idx + 1, step.name));
323
324 if let Some(condition) = &step.condition {
326 code.push_str(&self.generate_condition_check(condition));
327 }
328
329 code.push_str(&format!(
331 " let url_{} = format!(\"{{}}{}\"",
332 idx,
333 self.replace_vars(&step.request.path)
334 ));
335 code.push_str(", base_url");
336 for var in self.extract_variables(&step.request.path) {
338 code.push_str(&format!(", variables.get(\"{}\").unwrap_or(&String::new())", var));
339 }
340 code.push_str(");\n");
341
342 code.push_str(&format!(
344 " let mut request_{} = client.{}(&url_{})",
345 idx,
346 step.request.method.to_lowercase(),
347 idx
348 ));
349
350 if !step.request.headers.is_empty() {
352 code.push('\n');
353 for (key, value) in &step.request.headers {
354 let value_with_vars = self.replace_vars(value);
355 code.push_str(&format!(" .header(\"{}\", {})\n", key, value_with_vars));
356 }
357 }
358
359 if let Some(body) = &step.request.body {
361 let body_with_vars = self.replace_vars(body);
362 code.push_str(&format!(" .body({})\n", body_with_vars));
363 }
364
365 code.push_str(";\n\n");
366
367 code.push_str(&format!(
369 " let response_{} = request_{}.send().await.expect(\"Request failed\");\n",
370 idx, idx
371 ));
372
373 if let Some(status) = step.validation.status_code {
375 code.push_str(&format!(
376 " assert_eq!(response_{}.status().as_u16(), {});\n",
377 idx, status
378 ));
379 }
380
381 if !step.extract.is_empty() {
383 code.push_str(&format!(
384 " let body_{} = response_{}.text().await.expect(\"Failed to read body\");\n",
385 idx, idx
386 ));
387 code.push_str(&format!(" let json_{}: Value = serde_json::from_str(&body_{}).expect(\"Invalid JSON\");\n", idx, idx));
388
389 for extraction in &step.extract {
390 if extraction.source == ExtractionSource::Body {
391 code.push_str(&format!(" variables.insert(\"{}\".to_string(), json_{}[\"{}\"].as_str().unwrap_or(\"{}\").to_string());\n",
392 extraction.name, idx, extraction.pattern, extraction.default.as_deref().unwrap_or("")));
393 }
394 }
395 }
396
397 if let Some(delay) = step.delay_ms {
399 code.push_str(&format!(
400 " tokio::time::sleep(tokio::time::Duration::from_millis({})).await;\n",
401 delay
402 ));
403 }
404
405 code.push('\n');
406 }
407
408 code.push_str("}\n");
409 code
410 }
411
412 pub fn generate_python_test(&self) -> String {
414 let mut code = String::new();
415
416 code.push_str("import requests\n");
418 code.push_str("import time\n");
419 code.push_str("import pytest\n\n");
420
421 code.push_str(&format!("def test_{}():\n", self.sanitize_name(&self.workflow.name)));
423
424 code.push_str(&format!(" base_url = '{}'\n", self.workflow.setup.base_url));
426 code.push_str(" variables = {}\n");
427 for (key, value) in &self.workflow.setup.variables {
428 code.push_str(&format!(" variables['{}'] = '{}'\n", key, value));
429 }
430 code.push('\n');
431
432 for (idx, step) in self.workflow.steps.iter().enumerate() {
434 code.push_str(&format!(" # Step {}: {}\n", idx + 1, step.name));
435
436 let path = self.replace_vars_python(&step.request.path);
438 code.push_str(&format!(" url = f'{{base_url}}{}'\n", path));
439
440 let method = step.request.method.to_lowercase();
442 code.push_str(&format!(" response = requests.{}(url", method));
443
444 if !step.request.headers.is_empty() {
446 code.push_str(", headers={");
447 let headers: Vec<String> = step
448 .request
449 .headers
450 .iter()
451 .map(|(k, v)| format!("'{}': '{}'", k, self.replace_vars_python(v)))
452 .collect();
453 code.push_str(&headers.join(", "));
454 code.push('}');
455 }
456
457 if let Some(body) = &step.request.body {
459 code.push_str(&format!(", json={}", self.replace_vars_python(body)));
460 }
461
462 code.push_str(")\n");
463
464 if let Some(status) = step.validation.status_code {
466 code.push_str(&format!(" assert response.status_code == {}\n", status));
467 }
468
469 for extraction in &step.extract {
471 if extraction.source == ExtractionSource::Body {
472 code.push_str(&format!(
473 " variables['{}'] = response.json().get('{}', '{}')\n",
474 extraction.name,
475 extraction.pattern,
476 extraction.default.as_deref().unwrap_or("")
477 ));
478 }
479 }
480
481 if let Some(delay) = step.delay_ms {
483 code.push_str(&format!(" time.sleep({:.2})\n", delay as f64 / 1000.0));
484 }
485
486 code.push('\n');
487 }
488
489 code
490 }
491
492 pub fn generate_javascript_test(&self) -> String {
494 let mut code = String::new();
495
496 code.push_str(&format!("describe('{}', () => {{\n", self.workflow.name));
498 code.push_str(&format!(" it('{}', async () => {{\n", self.workflow.description));
499
500 code.push_str(&format!(" const baseUrl = '{}';\n", self.workflow.setup.base_url));
502 code.push_str(" const variables = {};\n");
503 for (key, value) in &self.workflow.setup.variables {
504 code.push_str(&format!(" variables['{}'] = '{}';\n", key, value));
505 }
506 code.push('\n');
507
508 for (idx, step) in self.workflow.steps.iter().enumerate() {
510 code.push_str(&format!(" // Step {}: {}\n", idx + 1, step.name));
511
512 let path = self.replace_vars_js(&step.request.path);
514 code.push_str(&format!(" const url{} = `${{baseUrl}}{}`;\n", idx, path));
515
516 code.push_str(&format!(" const response{} = await fetch(url{}, {{\n", idx, idx));
518 code.push_str(&format!(" method: '{}',\n", step.request.method.to_uppercase()));
519
520 if !step.request.headers.is_empty() {
522 code.push_str(" headers: {\n");
523 for (key, value) in &step.request.headers {
524 code.push_str(&format!(
525 " '{}': '{}',\n",
526 key,
527 self.replace_vars_js(value)
528 ));
529 }
530 code.push_str(" },\n");
531 }
532
533 if let Some(body) = &step.request.body {
535 code.push_str(&format!(
536 " body: JSON.stringify({}),\n",
537 self.replace_vars_js(body)
538 ));
539 }
540
541 code.push_str(" });\n");
542
543 if let Some(status) = step.validation.status_code {
545 code.push_str(&format!(" expect(response{}.status).toBe({});\n", idx, status));
546 }
547
548 if !step.extract.is_empty() {
550 code.push_str(&format!(" const data{} = await response{}.json();\n", idx, idx));
551 for extraction in &step.extract {
552 if extraction.source == ExtractionSource::Body {
553 code.push_str(&format!(
554 " variables['{}'] = data{}.{} || '{}';\n",
555 extraction.name,
556 idx,
557 extraction.pattern,
558 extraction.default.as_deref().unwrap_or("")
559 ));
560 }
561 }
562 }
563
564 if let Some(delay) = step.delay_ms {
566 code.push_str(&format!(
567 " await new Promise(resolve => setTimeout(resolve, {}));\n",
568 delay
569 ));
570 }
571
572 code.push('\n');
573 }
574
575 code.push_str(" });\n");
576 code.push_str("});\n");
577 code
578 }
579
580 fn sanitize_name(&self, name: &str) -> String {
582 name.to_lowercase()
583 .replace([' ', '-'], "_")
584 .chars()
585 .filter(|c| c.is_alphanumeric() || *c == '_')
586 .collect()
587 }
588
589 fn extract_variables(&self, text: &str) -> Vec<String> {
590 let mut vars = Vec::new();
591 let mut in_var = false;
592 let mut current_var = String::new();
593
594 for c in text.chars() {
595 if c == '{' {
596 in_var = true;
597 } else if c == '}' && in_var {
598 if !current_var.is_empty() {
599 vars.push(current_var.clone());
600 current_var.clear();
601 }
602 in_var = false;
603 } else if in_var {
604 current_var.push(c);
605 }
606 }
607
608 vars
609 }
610
611 fn replace_vars(&self, text: &str) -> String {
612 let vars = self.extract_variables(text);
613 if vars.is_empty() {
614 return format!("\"{}\"", text);
615 }
616
617 let mut result = text.to_string();
618 for var in vars {
619 result = result.replace(&format!("{{{}}}", var), "{}");
620 }
621 format!("\"{}\"", result)
622 }
623
624 fn replace_vars_python(&self, text: &str) -> String {
625 let mut result = text.to_string();
626 for var in self.extract_variables(text) {
627 result = result.replace(&format!("{{{}}}", var), &format!("{{variables['{}']}}", var));
628 }
629 result
630 }
631
632 fn replace_vars_js(&self, text: &str) -> String {
633 let mut result = text.to_string();
634 for var in self.extract_variables(text) {
635 result = result.replace(&format!("{{{}}}", var), &format!("${{variables['{}']}}", var));
636 }
637 result
638 }
639
640 fn generate_condition_check(&self, condition: &StepCondition) -> String {
641 let mut code = String::new();
642 code.push_str(&format!(
643 " if let Some(val) = variables.get(\"{}\") {{\n",
644 condition.variable
645 ));
646
647 let check = match condition.operator {
648 ConditionOperator::Equals => format!("val == \"{}\"", condition.value),
649 ConditionOperator::NotEquals => format!("val != \"{}\"", condition.value),
650 ConditionOperator::Contains => format!("val.contains(\"{}\")", condition.value),
651 ConditionOperator::Exists => "true".to_string(),
652 ConditionOperator::GreaterThan => {
653 format!("val.parse::<f64>().unwrap_or(0.0) > {}", condition.value)
654 }
655 ConditionOperator::LessThan => {
656 format!("val.parse::<f64>().unwrap_or(0.0) < {}", condition.value)
657 }
658 };
659
660 code.push_str(&format!(" if !({}) {{\n", check));
661 code.push_str(" return; // Skip this step\n");
662 code.push_str(" }\n");
663 code.push_str(" }\n");
664 code
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn test_workflow_creation() {
674 let workflow = IntegrationWorkflow {
675 id: "test-1".to_string(),
676 name: "User Registration Flow".to_string(),
677 description: "Test user registration and login".to_string(),
678 steps: vec![],
679 setup: WorkflowSetup::default(),
680 cleanup: vec![],
681 created_at: Utc::now(),
682 };
683
684 assert_eq!(workflow.name, "User Registration Flow");
685 }
686
687 #[test]
688 fn test_variable_extraction() {
689 let gen = IntegrationTestGenerator::new(IntegrationWorkflow {
690 id: "test".to_string(),
691 name: "test".to_string(),
692 description: "".to_string(),
693 steps: vec![],
694 setup: WorkflowSetup::default(),
695 cleanup: vec![],
696 created_at: Utc::now(),
697 });
698
699 let vars = gen.extract_variables("/api/users/{user_id}/posts/{post_id}");
700 assert_eq!(vars, vec!["user_id", "post_id"]);
701 }
702}