cruxi 0.2.0

Minimal, transport-agnostic hexagonal architecture framework
Documentation
//! Additional handler and validator tests
//!
//! Tests for edge cases including:
//! - Handler composition
//! - Validator combinations
//! - Error propagation

use cruxi::{
    CodedError, Context, FailValidator, Handler, HandlerFn, PassValidator, ServiceFn,
    ValidatingHandler, ValidatingHandlerError, Validator, ValidatorFn,
};

// ============================================================================
// HANDLER FN TESTS
// ============================================================================

#[test]
fn handler_fn_returns_error() {
    let handler: HandlerFn<_, _, CodedError> =
        HandlerFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> {
            if req < 0 {
                Err(CodedError::new("NEGATIVE_VALUE"))
            } else {
                Ok(req * 2)
            }
        });

    let result = handler.handle(&Context::new(), -5);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err().code(), "NEGATIVE_VALUE");
}

#[test]
fn handler_fn_uses_context() {
    #[derive(Clone)]
    struct UserId(u64);

    let handler: HandlerFn<_, _, CodedError> =
        HandlerFn::new(|ctx: &Context, _req: ()| -> Result<u64, CodedError> {
            ctx.get::<UserId>()
                .map(|u| u.0)
                .ok_or_else(|| CodedError::new("NO_USER_ID"))
        });

    let ctx = Context::new().with_value(UserId(42));
    let result = handler.handle(&ctx, ());

    assert_eq!(result.ok(), Some(42));
}

#[test]
fn handler_fn_context_deadline_check() {
    use std::time::Duration;

    let handler: HandlerFn<_, _, CodedError> =
        HandlerFn::new(|ctx: &Context, _req: ()| -> Result<bool, CodedError> { Ok(ctx.is_done()) });

    // Not expired
    let ctx = Context::with_timeout(Duration::from_secs(60));
    assert_eq!(handler.handle(&ctx, ()).ok(), Some(false));

    // Expired
    let ctx = Context::with_timeout(Duration::from_nanos(1));
    std::thread::sleep(Duration::from_millis(1));
    assert_eq!(handler.handle(&ctx, ()).ok(), Some(true));
}

// ============================================================================
// VALIDATING HANDLER TESTS
// ============================================================================

#[test]
fn validating_handler_passes_request_through() {
    let validator =
        ValidatorFn::new(|_ctx: &Context, _req: &String| -> Result<(), CodedError> { Ok(()) });

    let service = ServiceFn::new(|_ctx: &Context, req: String| -> Result<usize, CodedError> {
        Ok(req.len())
    });

    let handler = ValidatingHandler::new(validator, service);
    let result = handler.handle(&Context::new(), "hello world".to_string());

    assert_eq!(result.ok(), Some(11));
}

#[test]
fn validating_handler_validation_error_stops_service() {
    let call_count = std::cell::RefCell::new(0);

    let validator = ValidatorFn::new(|_ctx: &Context, _req: &i32| -> Result<(), CodedError> {
        Err(CodedError::new("ALWAYS_FAIL"))
    });

    let service = ServiceFn::new(|_ctx: &Context, _req: i32| -> Result<i32, CodedError> {
        *call_count.borrow_mut() += 1;
        Ok(42)
    });

    let handler = ValidatingHandler::new(validator, service);
    let result = handler.handle(&Context::new(), 123);

    assert!(result.is_err());
    assert_eq!(*call_count.borrow(), 0); // Service should not be called
}

#[test]
fn validating_handler_error_is_validation_error() {
    let validator = ValidatorFn::new(|_ctx: &Context, _req: &i32| -> Result<(), CodedError> {
        Err(CodedError::new("VALIDATION_FAILED"))
    });

    let service = ServiceFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req) });

    let handler = ValidatingHandler::new(validator, service);
    let result = handler.handle(&Context::new(), 42);

    match result {
        Err(ValidatingHandlerError::Validation(e)) => {
            assert_eq!(e.code(), "VALIDATION_FAILED");
        }
        Err(ValidatingHandlerError::Service(e)) => {
            panic!("Expected validation error, got service error: {e}");
        }
        Ok(value) => panic!("Expected validation error, got success value: {value}"),
    }
}

#[test]
fn validating_handler_error_is_service_error() {
    let validator =
        ValidatorFn::new(|_ctx: &Context, _req: &i32| -> Result<(), CodedError> { Ok(()) });

    let service = ServiceFn::new(|_ctx: &Context, _req: i32| -> Result<i32, CodedError> {
        Err(CodedError::new("SERVICE_FAILED"))
    });

    let handler = ValidatingHandler::new(validator, service);
    let result = handler.handle(&Context::new(), 42);

    match result {
        Err(ValidatingHandlerError::Service(e)) => {
            assert_eq!(e.code(), "SERVICE_FAILED");
        }
        Err(ValidatingHandlerError::Validation(e)) => {
            panic!("Expected service error, got validation error: {e}");
        }
        Ok(value) => panic!("Expected service error, got success value: {value}"),
    }
}

// ============================================================================
// VALIDATOR TESTS
// ============================================================================

#[test]
fn validator_fn_multiple_conditions() {
    #[derive(Clone)]
    struct UserInput {
        username: String,
        age: u8,
    }

    let validator = ValidatorFn::new(
        |_ctx: &Context, input: &UserInput| -> Result<(), CodedError> {
            if input.username.is_empty() {
                return Err(CodedError::new("USERNAME_REQUIRED"));
            }
            if input.username.len() < 3 {
                return Err(CodedError::new("USERNAME_TOO_SHORT"));
            }
            if input.age < 18 {
                return Err(CodedError::new("AGE_REQUIREMENT"));
            }
            Ok(())
        },
    );

    // Valid
    let valid = UserInput {
        username: "johndoe".to_string(),
        age: 25,
    };
    assert!(validator.validate(&Context::new(), &valid).is_ok());

    // Empty username
    let empty_name = UserInput {
        username: "".to_string(),
        age: 25,
    };
    assert_eq!(
        validator
            .validate(&Context::new(), &empty_name)
            .err()
            .map(|e| e.code().to_string()),
        Some("USERNAME_REQUIRED".to_string())
    );

    // Short username
    let short_name = UserInput {
        username: "ab".to_string(),
        age: 25,
    };
    assert_eq!(
        validator
            .validate(&Context::new(), &short_name)
            .err()
            .map(|e| e.code().to_string()),
        Some("USERNAME_TOO_SHORT".to_string())
    );

    // Underage
    let underage = UserInput {
        username: "johndoe".to_string(),
        age: 16,
    };
    assert_eq!(
        validator
            .validate(&Context::new(), &underage)
            .err()
            .map(|e| e.code().to_string()),
        Some("AGE_REQUIREMENT".to_string())
    );
}

#[test]
fn pass_validator_always_passes() {
    let validator = PassValidator;

    // Any value should pass
    assert!(validator.validate(&Context::new(), &42i32).is_ok());
    assert!(validator.validate(&Context::new(), &"any string").is_ok());
    assert!(validator.validate(&Context::new(), &vec![1, 2, 3]).is_ok());
}

#[test]
fn fail_validator_always_fails() {
    let validator = FailValidator::new(CodedError::new("CIRCUIT_BREAKER"));

    let result = validator.validate(&Context::new(), &42i32);
    assert!(result.is_err());
    assert_eq!(result.unwrap_err().code(), "CIRCUIT_BREAKER");
}

#[test]
fn fail_validator_preserves_error_details() {
    let validator = FailValidator::new(
        CodedError::new("MAINTENANCE")
            .with_title("System Maintenance")
            .with_reason("The system is currently under maintenance"),
    );

    let result = validator.validate(&Context::new(), &"any");
    let err = result.unwrap_err();

    assert_eq!(err.code(), "MAINTENANCE");
    assert_eq!(err.title(), Some("System Maintenance"));
    assert_eq!(
        err.reason(),
        Some("The system is currently under maintenance")
    );
}

// ============================================================================
// VALIDATOR WITH CONTEXT
// ============================================================================

#[test]
fn validator_uses_context() {
    #[derive(Clone)]
    struct RequesterId;

    let validator = ValidatorFn::new(|ctx: &Context, _req: &i32| -> Result<(), CodedError> {
        match ctx.get::<RequesterId>() {
            Some(_) => Ok(()),
            None => Err(CodedError::new("NO_REQUESTER")),
        }
    });

    // Without context value
    let result = validator.validate(&Context::new(), &42);
    assert!(result.is_err());

    // With context value
    let ctx = Context::new().with_value(RequesterId);
    let result = validator.validate(&ctx, &42);
    assert!(result.is_ok());
}

// ============================================================================
// ERROR DISPLAY AND SOURCE
// ============================================================================

#[test]
fn validating_handler_error_display_validation() {
    let err: ValidatingHandlerError<CodedError, CodedError> =
        ValidatingHandlerError::Validation(CodedError::new("VAL_ERROR").with_reason("bad input"));

    let display = err.to_string();
    assert!(display.contains("validation error"));
    assert!(display.contains("bad input"));
}

#[test]
fn validating_handler_error_display_service() {
    let err: ValidatingHandlerError<CodedError, CodedError> =
        ValidatingHandlerError::Service(CodedError::new("SVC_ERROR").with_reason("service failed"));

    let display = err.to_string();
    assert!(display.contains("service error"));
    assert!(display.contains("service failed"));
}

#[test]
fn validating_handler_error_source() {
    use std::error::Error;

    let inner = CodedError::new("INNER");
    let err: ValidatingHandlerError<CodedError, CodedError> =
        ValidatingHandlerError::Validation(inner);

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