feat: Add comprehensive unit tests for charging engine components

- Add credit_management_tests.rs with tests for SubscriberAccount, UsageEvent, UsageType, AccountStatus, and integer arithmetic operations
- Add error_tests.rs with tests for ChargingError Display implementation, cloning, and log_error function
- Add rating_billing_tests.rs with tests for ChargingRule variants, as_str conversion, billing period parsing, timestamp filtering, and cost calculations
- Add validation_tests.rs with tests for validate_bytes, validate
This commit is contained in:
nutcas3
2026-04-25 03:17:21 +03:00
parent 4cdecfc932
commit 05eb31e117
4 changed files with 455 additions and 0 deletions
@@ -0,0 +1,135 @@
use charging_engine::charging::types::{SubscriberAccount, AccountStatus, UsageEvent, UsageType};
use chrono::Utc;
#[test]
fn test_subscriber_account_creation() {
let account = SubscriberAccount {
imsi: "123456789012345".to_string(),
balance: 1000,
data_limit: 1_000_000_000,
data_used: 0,
voice_limit: 1000,
voice_used: 0,
sms_limit: 100,
sms_used: 0,
status: AccountStatus::Active,
last_updated: Utc::now(),
};
assert_eq!(account.imsi, "123456789012345");
assert_eq!(account.balance, 1000);
assert_eq!(account.data_limit, 1_000_000_000);
assert!(matches!(account.status, AccountStatus::Active));
}
#[test]
fn test_usage_event_creation() {
let event = UsageEvent {
imsi: "123456789012345".to_string(),
session_id: "session-123".to_string(),
usage_type: UsageType::Data,
volume: 1_000_000,
timestamp: Utc::now(),
rate: 0.01,
cost: 10.0,
};
assert_eq!(event.imsi, "123456789012345");
assert_eq!(event.volume, 1_000_000);
assert!(matches!(event.usage_type, UsageType::Data));
assert_eq!(event.cost, 10.0);
}
#[test]
fn test_usage_type_data() {
let data_type = UsageType::Data;
let voice_type = UsageType::Voice;
let sms_type = UsageType::SMS;
assert!(matches!(data_type, UsageType::Data));
assert!(matches!(voice_type, UsageType::Voice));
assert!(matches!(sms_type, UsageType::SMS));
}
#[test]
fn test_account_status_variants() {
let active = AccountStatus::Active;
let suspended = AccountStatus::Suspended;
let terminated = AccountStatus::Terminated;
let blocked = AccountStatus::Blocked;
assert!(matches!(active, AccountStatus::Active));
assert!(matches!(suspended, AccountStatus::Suspended));
assert!(matches!(terminated, AccountStatus::Terminated));
assert!(matches!(blocked, AccountStatus::Blocked));
}
#[test]
fn test_checked_add_no_overflow() {
let value: u64 = u64::MAX - 100;
let result = value.checked_add(100);
assert!(result.is_some());
assert_eq!(result.unwrap(), u64::MAX);
}
#[test]
fn test_checked_add_overflow() {
let value: u64 = u64::MAX;
let result = value.checked_add(1);
assert!(result.is_none());
}
#[test]
fn test_checked_sub_no_underflow() {
let value: u64 = 100;
let result = value.checked_sub(50);
assert!(result.is_some());
assert_eq!(result.unwrap(), 50);
}
#[test]
fn test_checked_sub_underflow() {
let value: u64 = 10;
let result = value.checked_sub(20);
assert!(result.is_none());
}
#[test]
fn test_integer_arithmetic_bytes_to_mb() {
let bytes: u64 = 2_097_152; // 2 MB
let mb = bytes / 1_048_576;
let remainder = bytes % 1_048_576;
assert_eq!(mb, 2);
assert_eq!(remainder, 0);
}
#[test]
fn test_integer_arithmetic_bytes_to_mb_with_remainder() {
let bytes: u64 = 1_500_000; // 1.43 MB
let mb = bytes / 1_048_576;
let remainder = bytes % 1_048_576;
assert_eq!(mb, 1);
assert_eq!(remainder, 451_424);
}
#[test]
fn test_integer_arithmetic_seconds_to_minutes() {
let seconds: u64 = 120; // 2 minutes
let minutes = seconds / 60;
let remainder = seconds % 60;
assert_eq!(minutes, 2);
assert_eq!(remainder, 0);
}
#[test]
fn test_integer_arithmetic_seconds_to_minutes_with_remainder() {
let seconds: u64 = 90; // 1.5 minutes
let minutes = seconds / 60;
let remainder = seconds % 60;
assert_eq!(minutes, 1);
assert_eq!(remainder, 30);
}
+51
View File
@@ -0,0 +1,51 @@
use charging_engine::errors::{ChargingError, log_error};
#[test]
fn test_charging_error_display() {
let err = ChargingError::RedisConnection("connection failed".to_string());
assert_eq!(err.to_string(), "Redis connection error: connection failed");
let err = ChargingError::RedisOperation("operation failed".to_string());
assert_eq!(err.to_string(), "Redis operation error: operation failed");
let err = ChargingError::DatabaseError("query failed".to_string());
assert_eq!(err.to_string(), "Database error: query failed");
let err = ChargingError::SubscriberNotFound("12345".to_string());
assert_eq!(err.to_string(), "Subscriber not found: 12345");
let err = ChargingError::RatingPlanNotFound("basic".to_string());
assert_eq!(err.to_string(), "Rating plan not found: basic");
let err = ChargingError::InsufficientCredit { available: 100, requested: 200 };
assert_eq!(err.to_string(), "Insufficient credit: available=100, requested=200");
let err = ChargingError::UsageBlocked("user blocked".to_string());
assert_eq!(err.to_string(), "Usage blocked: user blocked");
let err = ChargingError::InvalidInput("bad input".to_string());
assert_eq!(err.to_string(), "Invalid input: bad input");
let err = ChargingError::SerializationError("json error".to_string());
assert_eq!(err.to_string(), "Serialization error: json error");
let err = ChargingError::InternalError("internal error".to_string());
assert_eq!(err.to_string(), "Internal error: internal error");
}
#[test]
fn test_charging_error_clone() {
let err1 = ChargingError::RedisConnection("test".to_string());
let err2 = err1.clone();
assert_eq!(err1.to_string(), err2.to_string());
}
#[test]
fn test_log_error() {
// This test just ensures log_error doesn't panic
log_error(&ChargingError::RedisConnection("test".to_string()));
log_error(&ChargingError::DatabaseError("test".to_string()));
log_error(&ChargingError::SubscriberNotFound("123".to_string()));
log_error(&ChargingError::InsufficientCredit { available: 100, requested: 200 });
log_error(&ChargingError::InvalidInput("test".to_string()));
}
@@ -0,0 +1,142 @@
use charging_engine::charging::types::{ChargingRule, UsageEvent, UsageType};
use chrono::{Utc, Datelike};
#[test]
fn test_charging_rule_variants() {
let allowed = ChargingRule::Allowed;
let insufficient_credit = ChargingRule::InsufficientCredit;
let data_limit = ChargingRule::DataLimitExceeded;
let voice_limit = ChargingRule::VoiceLimitExceeded;
let sms_limit = ChargingRule::SmsLimitExceeded;
let user_blocked = ChargingRule::UserBlocked;
let blocked = ChargingRule::Blocked;
assert!(matches!(allowed, ChargingRule::Allowed));
assert!(matches!(insufficient_credit, ChargingRule::InsufficientCredit));
assert!(matches!(data_limit, ChargingRule::DataLimitExceeded));
assert!(matches!(voice_limit, ChargingRule::VoiceLimitExceeded));
assert!(matches!(sms_limit, ChargingRule::SmsLimitExceeded));
assert!(matches!(user_blocked, ChargingRule::UserBlocked));
assert!(matches!(blocked, ChargingRule::Blocked));
}
#[test]
fn test_charging_rule_as_str() {
assert_eq!(ChargingRule::Allowed.as_str(), "ALLOWED");
assert_eq!(ChargingRule::InsufficientCredit.as_str(), "INSUFFICIENT_CREDIT");
assert_eq!(ChargingRule::DataLimitExceeded.as_str(), "DATA_LIMIT_EXCEEDED");
assert_eq!(ChargingRule::VoiceLimitExceeded.as_str(), "VOICE_LIMIT_EXCEEDED");
assert_eq!(ChargingRule::SmsLimitExceeded.as_str(), "SMS_LIMIT_EXCEEDED");
assert_eq!(ChargingRule::UserBlocked.as_str(), "USER_BLOCKED");
assert_eq!(ChargingRule::Blocked.as_str(), "BLOCKED");
}
#[test]
fn test_charging_rule_equality() {
assert_eq!(ChargingRule::Allowed, ChargingRule::Allowed);
assert_eq!(ChargingRule::InsufficientCredit, ChargingRule::InsufficientCredit);
assert_ne!(ChargingRule::Allowed, ChargingRule::Blocked);
}
#[test]
fn test_charging_rule_clone() {
let rule1 = ChargingRule::DataLimitExceeded;
let rule2 = rule1.clone();
assert_eq!(rule1, rule2);
}
#[test]
fn test_billing_period_parsing() {
let period = "2024-01";
let parts: Vec<&str> = period.split('-').collect();
assert_eq!(parts.len(), 2);
let year = parts[0].parse::<i32>().unwrap();
let month = parts.get(1).and_then(|m| m.parse::<u32>().ok()).unwrap();
assert_eq!(year, 2024);
assert_eq!(month, 1);
}
#[test]
fn test_billing_period_parsing_invalid() {
let period = "invalid";
let parts: Vec<&str> = period.split('-').collect();
let year = parts[0].parse::<i32>().ok();
assert!(year.is_none());
}
#[test]
fn test_usage_event_timestamp_filtering() {
let event = UsageEvent {
imsi: "123456789012345".to_string(),
session_id: "session-123".to_string(),
usage_type: UsageType::Data,
volume: 1_000_000,
timestamp: Utc::now(),
rate: 0.01,
cost: 10.0,
};
let current_year = event.timestamp.year();
let current_month = event.timestamp.month();
assert!(current_year > 2020);
assert!(current_month >= 1 && current_month <= 12);
}
#[test]
fn test_cost_calculation_data() {
let volume_bytes: u64 = 2_097_152; // 2 MB
let data_rate: f64 = 0.01;
let data_mb = volume_bytes / 1_048_576;
let remainder_bytes = volume_bytes % 1_048_576;
let cost_full_mb = data_mb as f64 * data_rate;
let cost_partial = (remainder_bytes as f64 / 1_048_576.0) * data_rate;
let total_cost = cost_full_mb + cost_partial;
assert_eq!(data_mb, 2);
assert_eq!(cost_full_mb, 0.02);
assert!(total_cost >= 0.02);
}
#[test]
fn test_cost_calculation_voice() {
let volume_seconds: u64 = 120; // 2 minutes
let voice_rate: f64 = 0.05;
let voice_minutes = volume_seconds / 60;
let remainder_seconds = volume_seconds % 60;
let cost_full_min = voice_minutes as f64 * voice_rate;
let cost_partial = (remainder_seconds as f64 / 60.0) * voice_rate;
let total_cost = cost_full_min + cost_partial;
assert_eq!(voice_minutes, 2);
assert_eq!(cost_full_min, 0.10);
assert!(total_cost >= 0.10);
}
#[test]
fn test_cost_calculation_sms() {
let volume: u64 = 10;
let sms_rate: f64 = 0.10;
let cost = volume as f64 * sms_rate;
assert_eq!(cost, 1.0);
}
#[test]
fn test_rate_calculation() {
let cost: f64 = 10.0;
let volume: u64 = 1_000_000;
if volume > 0 {
let rate = cost / volume as f64;
assert!(rate > 0.0);
assert!(rate < cost);
}
}
@@ -0,0 +1,127 @@
use charging_engine::errors::{validate_bytes, validate_session_id, validate_amount, validate_ip, ChargingError};
#[test]
fn test_validate_bytes_zero() {
let result = validate_bytes(0);
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("cannot be zero")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_bytes_exceeds_limit() {
std::env::set_var("MAX_BYTES_LIMIT", "1000000000000");
let result = validate_bytes(2_000_000_000_000); // 2TB
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(_) => (),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_bytes_valid() {
let result = validate_bytes(1_000_000_000); // 1GB
assert!(result.is_ok());
}
#[test]
fn test_validate_session_id_empty() {
let result = validate_session_id("");
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("cannot be empty")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_session_id_too_long() {
let result = validate_session_id(&"a".repeat(65));
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("too long")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_session_id_valid() {
let result = validate_session_id("session-123");
assert!(result.is_ok());
}
#[test]
fn test_validate_amount_zero() {
let result = validate_amount(0.0);
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("must be positive")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_amount_negative() {
let result = validate_amount(-1.0);
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("must be positive")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_amount_exceeds_limit() {
std::env::set_var("MAX_AMOUNT_LIMIT", "10000");
let result = validate_amount(20_000.0);
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(_) => (),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_amount_valid() {
let result = validate_amount(100.0);
assert!(result.is_ok());
}
#[test]
fn test_validate_ip_empty() {
let result = validate_ip("");
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("cannot be empty")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_ip_invalid_format() {
let result = validate_ip("invalid");
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("Invalid IP address format")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_ip_zero_octet() {
let result = validate_ip("0.0.0.0");
assert!(result.is_err());
match result.unwrap_err() {
ChargingError::InvalidInput(msg) => assert!(msg.contains("cannot be 0.0.0.0")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn test_validate_ip_valid() {
let result = validate_ip("192.168.1.1");
assert!(result.is_ok());
}