mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
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:
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user