use audb::model::project::Test;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
pub struct TestGenerator {
pub base_url: String,
}
impl TestGenerator {
pub fn new() -> Self {
Self {
base_url: "http://localhost:8080".to_string(),
}
}
pub fn generate(&self, test: &Test) -> TokenStream {
let test_name = format_ident!("{}", self.sanitize_test_name(&test.name));
let doc_comment = if let Some(ref doc) = test.doc_comment {
let doc_lines: Vec<_> = doc.lines().map(|line| quote! { #[doc = #line] }).collect();
quote! { #(#doc_lines)* }
} else {
quote! {}
};
let steps: Vec<_> = test
.steps
.iter()
.enumerate()
.map(|(idx, step)| self.generate_step(step, idx))
.collect();
quote! {
#doc_comment
#[tokio::test]
async fn #test_name() {
let client = reqwest::Client::new();
#(#steps)*
}
}
}
fn generate_step(&self, step: &audb::model::project::TestStep, index: usize) -> TokenStream {
let description = &step.description;
let _step_comment = format!("Step {}: {}", index + 1, description);
let (method, endpoint, body) = self.parse_action(&step.action);
let base_url = &self.base_url;
let request = match method.to_uppercase().as_str() {
"GET" => {
quote! {
let response = client
.get(&format!("{}{}", #base_url, #endpoint))
.send()
.await
.expect("Failed to send request");
}
}
"POST" => {
let body_json = body.unwrap_or_else(|| "{}".to_string());
quote! {
let body = serde_json::json!(#body_json);
let response = client
.post(&format!("{}{}", #base_url, #endpoint))
.json(&body)
.send()
.await
.expect("Failed to send request");
}
}
"PUT" => {
let body_json = body.unwrap_or_else(|| "{}".to_string());
quote! {
let body = serde_json::json!(#body_json);
let response = client
.put(&format!("{}{}", #base_url, #endpoint))
.json(&body)
.send()
.await
.expect("Failed to send request");
}
}
"PATCH" => {
let body_json = body.unwrap_or_else(|| "{}".to_string());
quote! {
let body = serde_json::json!(#body_json);
let response = client
.patch(&format!("{}{}", #base_url, #endpoint))
.json(&body)
.send()
.await
.expect("Failed to send request");
}
}
"DELETE" => {
quote! {
let response = client
.delete(&format!("{}{}", #base_url, #endpoint))
.send()
.await
.expect("Failed to send request");
}
}
_ => {
quote! {
let response = client
.get(&format!("{}{}", #base_url, #endpoint))
.send()
.await
.expect("Failed to send request");
}
}
};
let assertions: Vec<_> = step
.expectations
.iter()
.map(|expectation| self.generate_assertion(expectation))
.collect();
quote! {
{
#request
#(#assertions)*
}
}
}
fn parse_action(&self, action: &str) -> (String, String, Option<String>) {
let parts: Vec<&str> = action.splitn(3, ' ').collect();
if parts.len() >= 2 {
let method = parts[0].to_string();
let endpoint = parts[1].to_string();
let body = if parts.len() == 3 {
Some(parts[2].to_string())
} else {
None
};
(method, endpoint, body)
} else {
("GET".to_string(), action.to_string(), None)
}
}
fn generate_assertion(&self, expectation: &str) -> TokenStream {
if expectation.starts_with("status:") {
let status_code = expectation.trim_start_matches("status:");
let code: u16 = status_code.parse().unwrap_or(200);
quote! {
assert_eq!(response.status().as_u16(), #code, "Expected status {}", #code);
}
} else if expectation.starts_with("body.") {
let parts: Vec<&str> = expectation.splitn(2, ':').collect();
if parts.len() == 2 {
let path = parts[0].trim_start_matches("body.");
let expected_value = parts[1];
quote! {
let body: serde_json::Value = response.json().await.expect("Failed to parse JSON");
assert_eq!(
body[#path].as_str().unwrap_or(""),
#expected_value,
"Expected {} to be {}",
#path,
#expected_value
);
}
} else {
quote! {}
}
} else if expectation.starts_with("header.") {
let parts: Vec<&str> = expectation.splitn(2, ':').collect();
if parts.len() == 2 {
let header_name = parts[0].trim_start_matches("header.");
let expected_value = parts[1];
quote! {
let header_value = response
.headers()
.get(#header_name)
.and_then(|h| h.to_str().ok())
.unwrap_or("");
assert_eq!(
header_value,
#expected_value,
"Expected header {} to be {}",
#header_name,
#expected_value
);
}
} else {
quote! {}
}
} else {
quote! {
}
}
}
fn sanitize_test_name(&self, name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect()
}
pub fn generate_all(&self, tests: &[&Test]) -> TokenStream {
let test_code: Vec<_> = tests.iter().map(|t| self.generate(t)).collect();
quote! {
#(#test_code)*
}
}
pub fn generate_test_module(&self, tests: &[&Test]) -> TokenStream {
let all_tests = self.generate_all(tests);
quote! {
#[cfg(test)]
mod integration_tests {
use super::*;
#all_tests
}
}
}
}
impl Default for TestGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use audb::model::project::TestStep;
#[test]
fn test_generate_simple_test() {
let test = Test {
name: "test_get_users".to_string(),
steps: vec![TestStep {
description: "Get all users".to_string(),
action: "GET /api/users".to_string(),
expectations: vec!["status:200".to_string()],
}],
doc_comment: None,
};
let generator = TestGenerator::new();
let code = generator.generate(&test);
let code_str = code.to_string();
assert!(code_str.contains("async fn test_get_users"));
assert!(code_str.contains("get"));
assert!(code_str.contains("/api/users"));
assert!(code_str.contains("assert_eq"));
}
#[test]
fn test_generate_post_request() {
let test = Test {
name: "test_create_user".to_string(),
steps: vec![TestStep {
description: "Create a new user".to_string(),
action: r#"POST /api/users {"name": "John"}"#.to_string(),
expectations: vec!["status:201".to_string()],
}],
doc_comment: None,
};
let generator = TestGenerator::new();
let code = generator.generate(&test);
let code_str = code.to_string();
assert!(code_str.contains("post"));
assert!(code_str.contains("json"));
}
#[test]
fn test_multiple_steps() {
let test = Test {
name: "test_user_flow".to_string(),
steps: vec![
TestStep {
description: "Create user".to_string(),
action: "POST /api/users".to_string(),
expectations: vec!["status:201".to_string()],
},
TestStep {
description: "Get user".to_string(),
action: "GET /api/users/1".to_string(),
expectations: vec!["status:200".to_string()],
},
],
doc_comment: None,
};
let generator = TestGenerator::new();
let code = generator.generate(&test);
let code_str = code.to_string();
assert!(code_str.contains("post"));
assert!(code_str.contains("get"));
}
#[test]
fn test_parse_action() {
let generator = TestGenerator::new();
let (method1, endpoint1, body1) = generator.parse_action("GET /api/users");
assert_eq!(method1, "GET");
assert_eq!(endpoint1, "/api/users");
assert!(body1.is_none());
let (method2, endpoint2, body2) =
generator.parse_action(r#"POST /api/users {"name": "John"}"#);
assert_eq!(method2, "POST");
assert_eq!(endpoint2, "/api/users");
assert!(body2.is_some());
}
#[test]
fn test_status_assertion() {
let generator = TestGenerator::new();
let assertion = generator.generate_assertion("status:200");
let code_str = assertion.to_string();
assert!(code_str.contains("200"));
assert!(code_str.contains("status"));
}
#[test]
fn test_body_assertion() {
let generator = TestGenerator::new();
let assertion = generator.generate_assertion("body.name:John");
let code_str = assertion.to_string();
assert!(code_str.contains("name"));
assert!(code_str.contains("John"));
}
#[test]
fn test_header_assertion() {
let generator = TestGenerator::new();
let assertion = generator.generate_assertion("header.content-type:application/json");
let code_str = assertion.to_string();
assert!(code_str.contains("content-type"));
assert!(code_str.contains("application/json"));
}
#[test]
fn test_sanitize_test_name() {
let generator = TestGenerator::new();
assert_eq!(
generator.sanitize_test_name("Test User Flow"),
"test_user_flow"
);
assert_eq!(
generator.sanitize_test_name("test-with-dashes"),
"test_with_dashes"
);
assert_eq!(generator.sanitize_test_name("Test123"), "test123");
}
#[test]
fn test_with_doc_comment() {
let test = Test {
name: "test_example".to_string(),
steps: vec![],
doc_comment: Some("This is a test example".to_string()),
};
let generator = TestGenerator::new();
let code = generator.generate(&test);
let code_str = code.to_string();
assert!(code_str.contains("This is a test example"));
}
#[test]
fn test_custom_base_url() {
let mut generator = TestGenerator::new();
generator.base_url = "http://localhost:3000".to_string();
let test = Test {
name: "test".to_string(),
steps: vec![TestStep {
description: "Test".to_string(),
action: "GET /api/test".to_string(),
expectations: vec![],
}],
doc_comment: None,
};
let code = generator.generate(&test);
let code_str = code.to_string();
assert!(code_str.contains("http://localhost:3000"));
}
#[test]
fn test_generate_test_module() {
let test1 = Test {
name: "test1".to_string(),
steps: vec![],
doc_comment: None,
};
let test2 = Test {
name: "test2".to_string(),
steps: vec![],
doc_comment: None,
};
let generator = TestGenerator::new();
let code = generator.generate_test_module(&[&test1, &test2]);
let code_str = code.to_string();
assert!(code_str.contains("mod integration_tests"));
assert!(code_str.contains("async fn test1"));
assert!(code_str.contains("async fn test2"));
}
}