use currency_converter::{
Config, ConversionRequest, CurrencyClient, CurrencyConverter, ExchangeRateService, Result,
};
use serde_json::json;
use std::collections::HashMap;
use wiremock::{
Mock, MockServer, ResponseTemplate,
matchers::{method, path, path_regex},
};
async fn setup_mock_server() -> MockServer {
MockServer::start().await
}
fn create_test_client(server_uri: &str) -> CurrencyClient {
let config = Config::new()
.with_base_url(server_uri)
.with_api_key("test-api-key");
CurrencyClient::with_config(config).unwrap()
}
fn mock_exchange_rates_response() -> serde_json::Value {
json!({
"result": "success",
"base_code": "USD",
"time_last_update_unix": 1640995200,
"time_next_update_unix": 1641081600,
"conversion_rates": {
"EUR": 0.8823,
"GBP": 0.7404,
"JPY": 115.108,
"CAD": 1.2773,
"AUD": 1.3943,
"CHF": 0.9178
}
})
}
fn mock_historical_rates_response() -> serde_json::Value {
json!({
"result": "success",
"base_code": "USD",
"year": 2024,
"month": 1,
"day": 1,
"conversion_rates": {
"EUR": 0.9020,
"GBP": 0.7850,
"JPY": 120.500,
"CAD": 1.3200,
"AUD": 1.4500
}
})
}
#[tokio::test]
async fn test_basic_currency_conversion() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/USD"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_exchange_rates_response()))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let request = ConversionRequest::from_components(100.0, "USD", "EUR")?;
let result = converter.convert(request).await?;
assert_eq!(result.request.from.amount(), 100.0);
assert_eq!(result.request.from.currency().code(), "USD");
assert_eq!(result.request.to.code(), "EUR");
assert!((result.exchange_rate - 0.8823).abs() < 0.0001);
assert!((result.result.amount() - 88.23).abs() < 0.01);
Ok(())
}
#[tokio::test]
async fn test_same_currency_conversion() -> Result<()> {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let request = ConversionRequest::from_components(100.0, "USD", "USD")?;
let result = converter.convert(request).await?;
assert_eq!(result.exchange_rate, 1.0);
assert_eq!(result.result.amount(), 100.0);
assert_eq!(
result.conversion_type,
currency_converter::ConversionType::SameCurrency
);
Ok(())
}
#[tokio::test]
async fn test_cross_currency_conversion() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/USD"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_exchange_rates_response()))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let request = ConversionRequest::from_components(100.0, "EUR", "GBP")?;
let result = converter.convert(request).await?;
if let currency_converter::ConversionType::Cross { via_currency } = result.conversion_type {
assert_eq!(via_currency, "USD");
} else {
panic!("Expected cross conversion");
}
let expected_rate = 0.7404 / 0.8823; assert!((result.exchange_rate - expected_rate).abs() < 0.001);
Ok(())
}
#[tokio::test]
async fn test_batch_conversion() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/USD"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_exchange_rates_response()))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let requests = vec![
ConversionRequest::from_components(100.0, "USD", "EUR")?,
ConversionRequest::from_components(50.0, "USD", "GBP")?,
ConversionRequest::from_components(200.0, "USD", "JPY")?,
];
let results = converter.convert_batch(requests).await;
assert_eq!(results.len(), 3);
let eur_result = results[0].as_ref().unwrap();
assert!((eur_result.result.amount() - 88.23).abs() < 0.01);
let gbp_result = results[1].as_ref().unwrap();
assert!((gbp_result.result.amount() - 37.02).abs() < 0.01);
let jpy_result = results[2].as_ref().unwrap();
assert!((jpy_result.result.amount() - 23021.6).abs() < 0.1);
Ok(())
}
#[tokio::test]
async fn test_historical_conversion() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/history/test-api-key/USD/2024-01-01"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_historical_rates_response()))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let request = currency_converter::api::types::HistoricalConversionRequest {
amount: 100.0,
from: "USD".to_string(),
to: "EUR".to_string(),
date: "2024-01-01".to_string(),
};
let result = service.convert_historical(request).await?;
assert_eq!(result.base_code, "USD");
assert_eq!(result.target_code, "EUR");
assert!((result.conversion_rate - 0.9020).abs() < 0.0001);
assert!((result.conversion_result - 90.20).abs() < 0.01);
Ok(())
}
#[tokio::test]
async fn test_caching_behavior() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/USD"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_exchange_rates_response()))
.expect(1) .mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let request1 = ConversionRequest::from_components(100.0, "USD", "EUR")?;
let result1 = converter.convert(request1).await?;
let request2 = ConversionRequest::from_components(200.0, "USD", "EUR")?;
let result2 = converter.convert(request2).await?;
assert_eq!(result1.exchange_rate, result2.exchange_rate);
let stats = converter.exchange_service.get_cache_stats();
assert!(stats.hits > 0);
assert_eq!(stats.total_requests, 2);
Ok(())
}
#[tokio::test]
async fn test_error_handling_invalid_currency() -> Result<()> {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let result = ConversionRequest::from_components(100.0, "INVALID", "USD");
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_error_handling_invalid_amount() -> Result<()> {
let mock_server = setup_mock_server().await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let converter = CurrencyConverter::new(service);
let result = ConversionRequest::from_components(-100.0, "USD", "EUR");
assert!(result.is_err());
let result = ConversionRequest::from_components(f64::NAN, "USD", "EUR");
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_api_error_handling() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/INVALID"))
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
"result": "error",
"error-type": "unsupported-code"
})))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let result = service.get_latest_rates("INVALID").await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_network_timeout_handling() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/USD"))
.respond_with(
ResponseTemplate::new(200)
.set_delay(std::time::Duration::from_secs(60)) .set_body_json(mock_exchange_rates_response()),
)
.mount(&mock_server)
.await;
let config = Config::new()
.with_base_url(&mock_server.uri())
.with_api_key("test-api-key")
.with_timeout(std::time::Duration::from_millis(100));
let client = CurrencyClient::with_config(config).unwrap();
let service = ExchangeRateService::new(client);
let result = service.get_latest_rates("USD").await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_cache_cleanup_and_stats() -> Result<()> {
let mock_server = setup_mock_server().await;
Mock::given(method("GET"))
.and(path_regex(r"/test-api-key/latest/USD"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_exchange_rates_response()))
.mount(&mock_server)
.await;
let client = create_test_client(&mock_server.uri());
let service = ExchangeRateService::new(client);
let _rates = service.get_latest_rates("USD").await?;
let stats_before = service.get_cache_stats();
assert!(stats_before.cached_entries > 0);
service.clear_cache().await;
let stats_after = service.get_cache_stats();
assert_eq!(stats_after.cached_entries, 0);
Ok(())
}