mudra-cli 0.1.0

A robust, high-performance currency converter with caching and CLI interface
Documentation
//! Unit tests for conversion types and logic

use super::*;
use crate::CurrencyError;

#[cfg(test)]
mod currency_tests {
    use super::*;

    #[test]
    fn test_currency_creation_valid() {
        let usd = Currency::new("USD").unwrap();
        assert_eq!(usd.code(), "USD");

        let eur = Currency::new("eur").unwrap(); // Should convert to uppercase
        assert_eq!(eur.code(), "EUR");
    }

    #[test]
    fn test_currency_creation_invalid() {
        // Too short
        assert!(Currency::new("US").is_err());

        // Too long
        assert!(Currency::new("USDD").is_err());

        // Contains numbers
        assert!(Currency::new("US1").is_err());

        // Contains special characters
        assert!(Currency::new("US$").is_err());

        // Empty string
        assert!(Currency::new("").is_err());
    }

    #[test]
    fn test_currency_equality() {
        let usd1 = Currency::new("USD").unwrap();
        let usd2 = Currency::new("usd").unwrap();
        let eur = Currency::new("EUR").unwrap();

        assert!(usd1.is_same_as(&usd2));
        assert!(!usd1.is_same_as(&eur));
    }

    #[test]
    fn test_currency_display() {
        let usd = Currency::new("USD").unwrap();
        assert_eq!(format!("{}", usd), "USD");
    }

    #[test]
    fn test_currency_try_from() {
        let usd: Currency = "USD".try_into().unwrap();
        assert_eq!(usd.code(), "USD");

        let invalid: std::result::Result<Currency, CurrencyError> = "INVALID".try_into();
        assert!(invalid.is_err());
    }
}

#[cfg(test)]
mod money_tests {
    use super::*;

    #[test]
    fn test_money_creation_valid() {
        let currency = Currency::new("USD").unwrap();
        let money = Money::new(100.0, currency).unwrap();

        assert_eq!(money.amount(), 100.0);
        assert_eq!(money.currency().code(), "USD");
    }

    #[test]
    fn test_money_from_code() {
        let money = Money::from_code(50.0, "EUR").unwrap();
        assert_eq!(money.amount(), 50.0);
        assert_eq!(money.currency().code(), "EUR");
    }

    #[test]
    fn test_money_invalid_amounts() {
        // Negative amount
        assert!(Money::from_code(-100.0, "USD").is_err());

        // NaN
        assert!(Money::from_code(f64::NAN, "USD").is_err());

        // Infinity
        assert!(Money::from_code(f64::INFINITY, "USD").is_err());

        // Too large
        assert!(Money::from_code(1e15, "USD").is_err());
    }

    #[test]
    fn test_money_rounding() {
        let money = Money::from_code(123.456789, "USD").unwrap();
        let rounded = money.round(2);

        assert_eq!(rounded.amount(), 123.46);
        assert_eq!(rounded.currency().code(), "USD");
    }

    #[test]
    fn test_money_display() {
        let money = Money::from_code(123.45, "USD").unwrap();
        assert_eq!(format!("{}", money), "123.45 USD");
    }
}

#[cfg(test)]
mod conversion_request_tests {
    use super::*;

    #[test]
    fn test_conversion_request_creation() {
        let from = Money::from_code(100.0, "USD").unwrap();
        let to = Currency::new("EUR").unwrap();
        let request = ConversionRequest::new(from, to);

        assert_eq!(request.from.amount(), 100.0);
        assert_eq!(request.from.currency().code(), "USD");
        assert_eq!(request.to.code(), "EUR");
    }

    #[test]
    fn test_conversion_request_from_components() {
        let request = ConversionRequest::from_components(50.0, "GBP", "JPY").unwrap();

        assert_eq!(request.from.amount(), 50.0);
        assert_eq!(request.from.currency().code(), "GBP");
        assert_eq!(request.to.code(), "JPY");
    }

    #[test]
    fn test_same_currency_detection() {
        let same_request = ConversionRequest::from_components(100.0, "USD", "USD").unwrap();
        assert!(same_request.is_same_currency());

        let different_request = ConversionRequest::from_components(100.0, "USD", "EUR").unwrap();
        assert!(!different_request.is_same_currency());
    }

    #[test]
    fn test_conversion_request_invalid_inputs() {
        // Invalid source currency
        assert!(ConversionRequest::from_components(100.0, "INVALID", "EUR").is_err());

        // Invalid target currency
        assert!(ConversionRequest::from_components(100.0, "USD", "INVALID").is_err());

        // Invalid amount
        assert!(ConversionRequest::from_components(-100.0, "USD", "EUR").is_err());
    }
}

#[cfg(test)]
mod conversion_result_tests {
    use super::*;

    #[test]
    fn test_conversion_result_creation() {
        let request = ConversionRequest::from_components(100.0, "USD", "EUR").unwrap();
        let result_money = Money::from_code(85.0, "EUR").unwrap();

        let result = ConversionResult::new(request, result_money, 0.85, ConversionType::Direct);

        assert_eq!(result.exchange_rate, 0.85);
        assert_eq!(result.result.amount(), 85.0);
        assert_eq!(result.conversion_type, ConversionType::Direct);
        assert!(result.timestamp > 0);
    }

    #[test]
    fn test_conversion_type_variants() {
        let request = ConversionRequest::from_components(100.0, "USD", "EUR").unwrap();
        let result_money = Money::from_code(85.0, "EUR").unwrap();

        // Test same currency
        let same_result = ConversionResult::new(
            request.clone(),
            result_money.clone(),
            1.0,
            ConversionType::SameCurrency,
        );
        assert_eq!(same_result.conversion_type, ConversionType::SameCurrency);

        // Test cross conversion
        let cross_result = ConversionResult::new(
            request.clone(),
            result_money.clone(),
            0.85,
            ConversionType::Cross {
                via_currency: "USD".to_string(),
            },
        );
        if let ConversionType::Cross { via_currency } = cross_result.conversion_type {
            assert_eq!(via_currency, "USD");
        } else {
            panic!("Expected Cross conversion type");
        }
    }

    #[test]
    fn test_conversion_result_summary() {
        let request = ConversionRequest::from_components(100.0, "USD", "EUR").unwrap();
        let result_money = Money::from_code(85.0, "EUR").unwrap();

        let result = ConversionResult::new(request, result_money, 0.85, ConversionType::Direct);

        let summary = result.summary();
        assert!(summary.contains("100.00 USD"));
        assert!(summary.contains("85.00 EUR"));
        assert!(summary.contains("0.850000"));
    }
}

#[cfg(test)]
mod property_tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn test_currency_code_roundtrip(
            code in "[A-Z]{3}"
        ) {
            let currency = Currency::new(&code).unwrap();
            assert_eq!(currency.code(), code);
        }

        #[test]
        fn test_money_rounding_properties(
            amount in 0.01f64..1_000_000.0,
            places in 0u32..6u32
        ) {
            let money = Money::from_code(amount, "USD").unwrap();
            let rounded = money.round(places);

            // Rounded amount should be different or equal
            let expected_multiplier = 10_f64.powi(places as i32);
            let expected = (amount * expected_multiplier).round() / expected_multiplier;
            assert!((rounded.amount() - expected).abs() < f64::EPSILON);
        }

        #[test]
        fn test_conversion_request_validation(
            amount in 0.01f64..1_000_000.0,
            from_code in "[A-Z]{3}",
            to_code in "[A-Z]{3}"
        ) {
            let request = ConversionRequest::from_components(amount, &from_code, &to_code);

            // Should always succeed with valid inputs
            assert!(request.is_ok());

            let req = request.unwrap();
            assert_eq!(req.from.amount(), amount);
            assert_eq!(req.from.currency().code(), from_code);
            assert_eq!(req.to.code(), to_code);
        }
    }
}