catalyst 0.2.4

A lightweight API testing tool
Documentation
use crate::checker::parse_tests;
use crate::debug;
use crate::engine::variables::load_env_files;
use crate::http::client::HttpClient;
use crate::models::test::Test;
use colored::*;
use serde_json::Value;
use std::collections::HashMap;
use std::env;

fn get_terminal_width() -> usize {
    env::var("COLUMNS")
        .ok()
        .and_then(|cols| cols.parse().ok())
        .unwrap_or(80)
}

#[derive(Debug)]
pub struct TestResult {
    pub name: String,
    pub success: bool,
    pub expected_status: u16,
    pub actual_status: u16,
    pub response_body: Option<Value>,
    pub headers: HashMap<String, String>,
    pub messages: Vec<String>,
}

pub struct TestRunner {
    pub variables: HashMap<String, String>,
    pub results: Vec<TestResult>,
    pub disable_color: bool,
}

impl TestRunner {
    pub fn new(disable_color: bool) -> Self {
        TestRunner {
            variables: HashMap::new(),
            results: Vec::new(),
            disable_color,
        }
    }

    async fn execute_test(&mut self, test: &Test, client: &HttpClient) -> TestResult {
        debug!(
            "Variables before test '{}': {:?}",
            test.name, self.variables
        );
        let result = crate::engine::execution::run(client, test, &mut self.variables).await;
        debug!("Variables after test '{}': {:?}", test.name, self.variables);
        TestResult {
            name: test.name.clone(),
            success: result.success,
            expected_status: result.status.0,
            actual_status: result.status.1,
            response_body: result.body,
            headers: result.headers,
            messages: result.errors,
        }
    }

    pub async fn execute_tests(
        &mut self,
        filter: Option<String>,
        verbose: bool,
        file: Option<String>,
    ) {
        load_env_files();

        let test_suite = match parse_tests(file.as_deref()) {
            Ok(suite) => suite,
            Err(e) => {
                eprintln!("{}", format!("Failed to parse tests: {}", e).red());
                return;
            }
        };

        let client = HttpClient::new(&test_suite.config);
        let mut skipped = 0;
        let total = test_suite.tests.len();

        for test in test_suite.tests.iter() {
            if let Some(ref f) = filter {
                if !test.name.contains(f) {
                    skipped += 1;
                    if verbose {
                        println!("{} {}", "SKIP".yellow(), test.name);
                    }
                    continue;
                }
            }

            let result = self.execute_test(test, &client).await;
            let status_matches = result.expected_status == result.actual_status;

            if verbose {
                println!("\n{}", "".repeat(get_terminal_width()).blue());
                println!("Test: {}", test.name.bold());
                println!(
                    "Endpoint: {} {}",
                    test.method.yellow(),
                    test.endpoint.yellow()
                );
                println!(
                    "Success: {}",
                    if result.success {
                        "Yes".green()
                    } else {
                        "No".red()
                    }
                );

                let status_display = format!(
                    "Status: {} (expected {})",
                    if status_matches {
                        result.actual_status.to_string().green()
                    } else {
                        result.actual_status.to_string().red()
                    },
                    result.expected_status.to_string().bold()
                );
                println!("{}", status_display);

                if let Some(body) = &result.response_body {
                    println!(
                        "\nResponse Body: {}",
                        serde_json::to_string_pretty(body).unwrap_or(body.to_string())
                    );
                }

                if let (Some(expected_body), Some(actual_body)) =
                    (&test.expected_body, &result.response_body)
                {
                    println!("\nBody comparison:");
                    let body_matches = expected_body == actual_body;
                    if body_matches {
                        println!("  {}", "✓ Body matches expected value".green());
                    } else {
                        println!("  {}", "✗ Body differs from expected value".red());
                        println!(
                            "    Expected: {}",
                            serde_json::to_string_pretty(&expected_body)
                                .unwrap()
                                .green()
                        );
                        println!(
                            "    Actual:   {}",
                            serde_json::to_string_pretty(&actual_body).unwrap().red()
                        );
                    }
                }

                if !result.messages.is_empty() {
                    println!("\nMessages:");
                    for msg in &result.messages {
                        println!("  {} {}", "-".bold(), msg.red());
                    }
                }
            } else if !self.disable_color {
                let status_indicator = if status_matches {
                    "".green()
                } else {
                    "".red()
                };
                println!(
                    "{} {} {}",
                    status_indicator,
                    test.name,
                    if !status_matches {
                        format!(
                            "(expected {}, got {})",
                            result.expected_status,
                            result.actual_status.to_string().red()
                        )
                    } else {
                        format!(
                            "(expected {}, got {})",
                            result.expected_status,
                            result.actual_status.to_string().green()
                        )
                    }
                );

                if !result.success {
                    if let (Some(expected_body), Some(actual_body)) =
                        (&test.expected_body, &result.response_body)
                    {
                        if expected_body != actual_body {
                            println!("  {}", "Body mismatch".red());
                        }
                    }
                    for msg in &result.messages {
                        println!("  {} {}", "-".bold(), msg.red());
                    }
                }
            } else {
                println!("{} {}", if result.success { "" } else { "" }, test.name);
                if !result.success {
                    println!("  Expected status: {}", result.expected_status);
                    println!("  Actual status: {}", result.actual_status);
                    for msg in &result.messages {
                        println!("  - {}", msg);
                    }
                }
            }

            self.results.push(result);
        }

        if !self.disable_color {
            println!("\n{}", "".repeat(get_terminal_width()).blue());
            let success_count = self.results.iter().filter(|r| r.success).count();
            let fail_count = self.results.len() - success_count;

            println!("\nSummary:");
            if success_count > 0 {
                print!("{} passed", format!("{} tests", success_count).green());
            }
            if fail_count > 0 {
                if success_count > 0 {
                    print!(", ");
                }
                print!("{} failed", format!("{} tests", fail_count).red());
            }
            if skipped > 0 {
                if success_count > 0 || fail_count > 0 {
                    print!(", ");
                }
                print!("{} skipped", format!("{} tests", skipped).yellow());
            }
            println!(" (total: {})", total);
        }
    }
}