cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
//! Additional tests for error types
//!
//! Tests for edge cases including:
//! - Error chain traversal
//! - Display formatting variations
//! - All accessor methods

use cruxi::{CodedError, CruxiError, ValidationError};
use std::error::Error;

// ============================================================================
// CODED ERROR DISPLAY VARIATIONS
// ============================================================================

#[test]
fn coded_error_display_priority() {
    // Reason takes priority over title
    let err = CodedError::new("TEST")
        .with_title("Short Title")
        .with_reason("Detailed reason message");

    assert_eq!(err.to_string(), "Detailed reason message");
}

#[test]
fn coded_error_display_title_when_no_reason() {
    let err = CodedError::new("TEST").with_title("Short Title");

    assert_eq!(err.to_string(), "Short Title");
}

#[test]
fn coded_error_display_code_fallback() {
    let err = CodedError::new("MY_ERROR_CODE");

    assert_eq!(err.to_string(), "cruxi: coded error [MY_ERROR_CODE]");
}

// ============================================================================
// CODED ERROR ACCESSORS
// ============================================================================

#[test]
fn coded_error_code_accessor() {
    let err = CodedError::new("USER_NOT_FOUND");
    assert_eq!(err.code(), "USER_NOT_FOUND");
}

#[test]
fn coded_error_instance_accessor() {
    let err = CodedError::new("TEST").with_instance("req-12345-abc");

    assert_eq!(err.instance(), Some("req-12345-abc"));
}

#[test]
fn coded_error_instance_none() {
    let err = CodedError::new("TEST");

    assert_eq!(err.instance(), None);
}

#[test]
fn coded_error_title_accessor() {
    let err = CodedError::new("TEST").with_title("User Not Found");

    assert_eq!(err.title(), Some("User Not Found"));
}

#[test]
fn coded_error_title_none() {
    let err = CodedError::new("TEST");

    assert_eq!(err.title(), None);
}

#[test]
fn coded_error_reason_accessor() {
    let err = CodedError::new("TEST").with_reason("The user with ID 123 was not found");

    assert_eq!(err.reason(), Some("The user with ID 123 was not found"));
}

#[test]
fn coded_error_reason_none() {
    let err = CodedError::new("TEST");

    assert_eq!(err.reason(), None);
}

// ============================================================================
// ERROR CHAIN TRAVERSAL
// ============================================================================

#[test]
fn coded_error_single_source() {
    let inner = CodedError::new("DB_CONNECTION_FAILED").with_reason("Connection timed out");
    let outer = CodedError::new("USER_FETCH_FAILED")
        .with_reason("Could not fetch user")
        .with_source(inner);

    let source = outer.source();
    assert!(source.is_some());
}

#[test]
fn coded_error_chain_three_levels() {
    let level1 = CodedError::new("LEVEL1").with_reason("Root cause");
    let level2 = CodedError::new("LEVEL2")
        .with_reason("Middle cause")
        .with_source(level1);
    let level3 = CodedError::new("LEVEL3")
        .with_reason("Top level error")
        .with_source(level2);

    // Traverse the chain
    let source1 = level3.source().expect("Should have source");
    let source2 = source1.source().expect("Should have nested source");

    // Third level shouldn't have source
    assert!(source2.source().is_none());
}

#[test]
fn coded_error_chain_downcast() {
    let inner = CodedError::new("INNER").with_reason("inner cause");
    let outer = CodedError::new("OUTER")
        .with_reason("outer reason")
        .with_source(inner);

    // Get source and downcast
    let source = outer.source().expect("Should have source");
    let inner_coded = source.downcast_ref::<CodedError>();

    assert!(inner_coded.is_some());
    assert_eq!(inner_coded.unwrap().code(), "INNER");
}

#[test]
fn coded_error_no_source() {
    let err = CodedError::new("STANDALONE");

    assert!(err.source().is_none());
}

// ============================================================================
// ERROR CLONING
// ============================================================================

#[test]
fn coded_error_clone_preserves_all_fields() {
    let original = CodedError::new("ORIGINAL")
        .with_title("Original Title")
        .with_reason("Original reason")
        .with_instance("req-123");

    let cloned = original.clone();

    assert_eq!(cloned.code(), "ORIGINAL");
    assert_eq!(cloned.title(), Some("Original Title"));
    assert_eq!(cloned.reason(), Some("Original reason"));
    assert_eq!(cloned.instance(), Some("req-123"));
}

#[test]
fn coded_error_clone_with_source() {
    let inner = CodedError::new("INNER");
    let outer = CodedError::new("OUTER").with_source(inner);

    let cloned = outer.clone();

    assert!(cloned.source().is_some());
}

// ============================================================================
// VALIDATION ERROR TESTS
// ============================================================================

#[test]
fn validation_error_accessors() {
    let err = ValidationError::new("email", "must be valid email format");

    assert_eq!(err.field(), "email");
    assert_eq!(err.message(), "must be valid email format");
}

#[test]
fn validation_error_display() {
    let err = ValidationError::new("password", "must be at least 8 characters");

    assert_eq!(
        err.to_string(),
        "cruxi: validation error: password: must be at least 8 characters"
    );
}

#[test]
fn validation_error_empty_field_name() {
    let err = ValidationError::new("", "field is empty");

    assert_eq!(err.to_string(), "cruxi: validation error: : field is empty");
}

#[test]
fn validation_error_empty_message() {
    let err = ValidationError::new("field", "");

    assert_eq!(err.to_string(), "cruxi: validation error: field: ");
}

#[test]
fn validation_error_clone() {
    let original = ValidationError::new("username", "already taken");
    let cloned = original.clone();

    assert_eq!(cloned.field(), "username");
    assert_eq!(cloned.message(), "already taken");
}

// ============================================================================
// CRUXI ERROR TESTS
// ============================================================================

#[test]
fn cruxi_error_unauthorized_display() {
    let err = CruxiError::Unauthorized;

    assert_eq!(err.to_string(), "cruxi: unauthorized");
}

#[test]
fn cruxi_error_equality() {
    assert_eq!(CruxiError::Unauthorized, CruxiError::Unauthorized);
}

#[test]
fn cruxi_error_copy() {
    let err1 = CruxiError::Unauthorized;
    let err2 = err1; // Copy

    assert_eq!(err1, err2);
}

// ============================================================================
// ERROR DEBUG FORMATTING
// ============================================================================

#[test]
fn coded_error_debug() {
    let err = CodedError::new("TEST")
        .with_title("Title")
        .with_reason("Reason")
        .with_instance("inst-1");

    let debug = format!("{:?}", err);

    assert!(debug.contains("CodedError"));
    assert!(debug.contains("TEST"));
    assert!(debug.contains("Title"));
    assert!(debug.contains("Reason"));
    assert!(debug.contains("inst-1"));
}

#[test]
fn validation_error_debug() {
    let err = ValidationError::new("field", "message");
    let debug = format!("{:?}", err);

    assert!(debug.contains("ValidationError"));
    assert!(debug.contains("field"));
    assert!(debug.contains("message"));
}

#[test]
fn cruxi_error_debug() {
    let err = CruxiError::Unauthorized;
    let debug = format!("{:?}", err);

    assert!(debug.contains("Unauthorized"));
}

// ============================================================================
// BUILDER PATTERN CHAINING
// ============================================================================

#[test]
fn coded_error_builder_chain_order_independent() {
    let err1 = CodedError::new("TEST")
        .with_title("Title")
        .with_reason("Reason")
        .with_instance("inst");

    let err2 = CodedError::new("TEST")
        .with_instance("inst")
        .with_reason("Reason")
        .with_title("Title");

    assert_eq!(err1.code(), err2.code());
    assert_eq!(err1.title(), err2.title());
    assert_eq!(err1.reason(), err2.reason());
    assert_eq!(err1.instance(), err2.instance());
}

#[test]
fn coded_error_overwrite_fields() {
    let err = CodedError::new("TEST")
        .with_title("First Title")
        .with_title("Second Title");

    assert_eq!(err.title(), Some("Second Title"));
}