grpctestify 1.5.2

gRPC testing utility written in Rust
Documentation
// Assertion Handler - handles assertion evaluation

use crate::assert::AssertionEngine;
#[cfg(test)]
use crate::parser::ast::{Section, SectionContent, SectionType};
use crate::plugins::AssertionTiming;
use crate::utils::section_content_line;
use serde_json::Value;
use std::collections::HashMap;

/// Assertion evaluation result
#[derive(Debug, Clone)]
pub struct AssertionResult {
    pub passed: bool,
    pub failure_messages: Vec<String>,
}

/// Assertion Handler - evaluates assertions
pub struct AssertionHandler {
    engine: AssertionEngine,
}

impl AssertionHandler {
    /// Create new assertion handler
    pub fn new(_verbose: bool) -> Self {
        Self {
            engine: AssertionEngine::new(),
        }
    }

    /// Evaluate assertions for a document (test-only)
    #[cfg(test)]
    pub fn evaluate_assertions(
        &self,
        sections: &[Section],
        target_value: &Value,
        headers: &HashMap<String, String>,
        trailers: &HashMap<String, String>,
    ) -> AssertionResult {
        let mut failure_messages = Vec::new();

        for section in sections {
            if section.section_type == SectionType::Asserts
                && let SectionContent::Assertions(lines) = &section.content
            {
                let results =
                    self.engine
                        .evaluate_all(lines, target_value, Some(headers), Some(trailers));

                for (idx, result) in results.iter().enumerate() {
                    let line_num = section_content_line(section.start_line, idx);
                    let context = format!("at line {}", line_num);
                    append_single_failure(result, &context, &mut failure_messages);
                }
            }
        }

        AssertionResult {
            passed: failure_messages.is_empty(),
            failure_messages,
        }
    }

    /// Evaluate assertions for a specific section (test-only)
    #[cfg(test)]
    pub fn evaluate_section_assertions(
        &self,
        section: &Section,
        target_value: &Value,
        headers: &HashMap<String, String>,
        trailers: &HashMap<String, String>,
    ) -> AssertionResult {
        let mut failure_messages = Vec::new();

        if section.section_type == SectionType::Asserts
            && let SectionContent::Assertions(lines) = &section.content
        {
            let results =
                self.engine
                    .evaluate_all(lines, target_value, Some(headers), Some(trailers));

            for (idx, result) in results.iter().enumerate() {
                let line_num = section_content_line(section.start_line, idx);
                let context = format!("at line {}", line_num);
                append_single_failure(result, &context, &mut failure_messages);
            }
        }

        AssertionResult {
            passed: failure_messages.is_empty(),
            failure_messages,
        }
    }

    /// Check if section has assertions (test-only)
    #[cfg(test)]
    pub fn has_assertions(&self, section: &Section) -> bool {
        section.section_type == SectionType::Asserts
    }

    /// Get assertion lines from section (test-only)
    #[cfg(test)]
    pub fn get_assertion_lines<'a>(&self, section: &'a Section) -> Vec<&'a String> {
        if let SectionContent::Assertions(lines) = &section.content {
            lines.iter().collect()
        } else {
            Vec::new()
        }
    }

    /// Evaluate a single assertion (test-only)
    #[cfg(test)]
    pub fn evaluate_single_assertion(
        &self,
        assertion: &str,
        target_value: &Value,
        headers: Option<&HashMap<String, String>>,
        trailers: Option<&HashMap<String, String>>,
    ) -> Result<crate::assert::AssertionResult, String> {
        self.engine
            .evaluate(assertion, target_value, headers, trailers)
            .map_err(|e| e.to_string())
    }

    /// Evaluate assertions for a section (convenience method for runner.rs)
    #[allow(clippy::too_many_arguments)]
    pub fn evaluate_assertions_for_section(
        &self,
        lines: &[String],
        target_value: &Value,
        headers: &HashMap<String, String>,
        trailers: &HashMap<String, String>,
        section_context: &str,
        start_line: usize,
        timing: Option<&AssertionTiming>,
    ) -> AssertionResult {
        let mut failure_messages = Vec::new();

        let results = self.engine.evaluate_all_with_timing(
            lines,
            target_value,
            Some(headers),
            Some(trailers),
            timing,
        );

        for (idx, result) in results.iter().enumerate() {
            let line_num = section_content_line(start_line, idx);
            let context = format!("{} (assertion at line {})", section_context, line_num);
            append_single_failure(result, &context, &mut failure_messages);
        }

        AssertionResult {
            passed: failure_messages.is_empty(),
            failure_messages,
        }
    }
}

fn append_single_failure(
    result: &crate::assert::AssertionResult,
    context: &str,
    failure_messages: &mut Vec<String>,
) {
    match result {
        crate::assert::AssertionResult::Fail {
            message,
            expected,
            actual,
        } => {
            failure_messages.push(format!("Assertion failed {}: {}", context, message));
            if let (Some(exp), Some(act)) = (expected, actual) {
                failure_messages.push(format!("    Expected: {}\n    Actual:   {}", exp, act));
            }
        }
        crate::assert::AssertionResult::Error(msg) => {
            failure_messages.push(format!("Assertion error {}: {}", context, msg));
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_evaluate_assertions_pass() {
        let handler = AssertionHandler::new(false);
        let sections = vec![Section {
            section_type: SectionType::Asserts,
            content: SectionContent::Assertions(vec![".id == 123".to_string()]),
            inline_options: Default::default(),
            raw_content: "".to_string(),
            start_line: 0,
            end_line: 0,
        }];

        let target = json!({"id": 123, "name": "test"});
        let headers = HashMap::new();
        let trailers = HashMap::new();

        let result = handler.evaluate_assertions(&sections, &target, &headers, &trailers);
        assert!(result.passed);
        assert!(result.failure_messages.is_empty());
    }

    #[test]
    fn test_evaluate_assertions_fail() {
        let handler = AssertionHandler::new(false);
        let sections = vec![Section {
            section_type: SectionType::Asserts,
            content: SectionContent::Assertions(vec![".id == 456".to_string()]),
            inline_options: Default::default(),
            raw_content: "".to_string(),
            start_line: 0,
            end_line: 0,
        }];

        let target = json!({"id": 123, "name": "test"});
        let headers = HashMap::new();
        let trailers = HashMap::new();

        let result = handler.evaluate_assertions(&sections, &target, &headers, &trailers);
        assert!(!result.passed);
        assert!(!result.failure_messages.is_empty());
    }

    #[test]
    fn test_has_assertions() {
        let handler = AssertionHandler::new(false);
        let asserts_section = Section {
            section_type: SectionType::Asserts,
            content: SectionContent::Assertions(vec![]),
            inline_options: Default::default(),
            raw_content: "".to_string(),
            start_line: 0,
            end_line: 0,
        };

        let other_section = Section {
            section_type: SectionType::Response,
            content: SectionContent::Json(json!({})),
            inline_options: Default::default(),
            raw_content: "".to_string(),
            start_line: 0,
            end_line: 0,
        };

        assert!(handler.has_assertions(&asserts_section));
        assert!(!handler.has_assertions(&other_section));
    }

    #[test]
    fn test_get_assertion_lines() {
        let handler = AssertionHandler::new(false);
        let section = Section {
            section_type: SectionType::Asserts,
            content: SectionContent::Assertions(vec![
                ".id == 123".to_string(),
                ".name == \"test\"".to_string(),
            ]),
            inline_options: Default::default(),
            raw_content: "".to_string(),
            start_line: 0,
            end_line: 0,
        };

        let lines = handler.get_assertion_lines(&section);
        assert_eq!(lines.len(), 2);
    }

    #[test]
    fn test_evaluate_single_assertion() {
        let handler = AssertionHandler::new(false);
        let target = json!({"id": 123, "name": "test"});
        let headers = HashMap::new();
        let trailers = HashMap::new();

        let result = handler.evaluate_single_assertion(
            ".id == 123",
            &target,
            Some(&headers),
            Some(&trailers),
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_evaluate_section_assertions_empty() {
        let handler = AssertionHandler::new(false);
        let section = Section {
            section_type: SectionType::Asserts,
            content: SectionContent::Assertions(vec![]),
            inline_options: Default::default(),
            raw_content: "".to_string(),
            start_line: 0,
            end_line: 0,
        };

        let target = json!({"id": 123});
        let headers = HashMap::new();
        let trailers = HashMap::new();

        let result = handler.evaluate_section_assertions(&section, &target, &headers, &trailers);
        assert!(result.passed);
    }
}