Add comprehensive documentation and unit tests

Documentation:
- Add detailed README files for all services (auth, character, database, launcher, packet, utils, world)
- Create API documentation for the database service with detailed endpoint specifications
- Document database schema and relationships
- Add service architecture overviews and configuration instructions

Unit Tests:
- Implement comprehensive test suite for database repositories (user, character, session)
- Add gRPC service tests for database interactions
- Create tests for packet service components (bufferpool, connection, packets)
- Add utility service tests (health check, logging, load balancer, redis cache, service discovery)
- Implement auth service user tests
- Add character service tests

Code Structure:
- Reorganize test files into a more consistent structure
- Create a dedicated tests crate for integration testing
- Add test helpers and mock implementations for easier testing
This commit is contained in:
2025-04-09 13:29:38 -04:00
parent d47d5f44b1
commit a8755bd3de
85 changed files with 4218 additions and 764 deletions

View File

@@ -0,0 +1,64 @@
use reqwest::StatusCode;
use std::env;
use std::time::Duration;
use tokio::time::sleep;
use utils::health_check::start_health_check;
#[tokio::test]
async fn test_health_check_endpoint() {
// Set a custom port for this test to avoid conflicts
env::set_var("HEALTH_CHECK_PORT", "8099");
// Start the health check endpoint
let result = start_health_check("127.0.0.1").await;
assert!(result.is_ok(), "Failed to start health check: {:?}", result.err());
// Give the server a moment to start
sleep(Duration::from_millis(100)).await;
// Make a request to the health check endpoint
let client = reqwest::Client::new();
let response = client.get("http://127.0.0.1:8099/health")
.timeout(Duration::from_secs(2))
.send()
.await;
// Verify the response
assert!(response.is_ok(), "Failed to connect to health check endpoint: {:?}", response.err());
let response = response.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = response.text().await.unwrap();
assert_eq!(body, "OK");
// Clean up
env::remove_var("HEALTH_CHECK_PORT");
}
#[tokio::test]
async fn test_health_check_invalid_path() {
// Set a custom port for this test to avoid conflicts
env::set_var("HEALTH_CHECK_PORT", "8098");
// Start the health check endpoint
let result = start_health_check("127.0.0.1").await;
assert!(result.is_ok(), "Failed to start health check: {:?}", result.err());
// Give the server a moment to start
sleep(Duration::from_millis(100)).await;
// Make a request to an invalid path
let client = reqwest::Client::new();
let response = client.get("http://127.0.0.1:8098/invalid")
.timeout(Duration::from_secs(2))
.send()
.await;
// Verify the response
assert!(response.is_ok(), "Failed to connect to health check endpoint: {:?}", response.err());
let response = response.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
// Clean up
env::remove_var("HEALTH_CHECK_PORT");
}

View File

@@ -0,0 +1,42 @@
use std::env;
use utils::logging::setup_logging;
#[test]
fn test_logging_setup() {
// Test with default log level
env::remove_var("LOG_LEVEL");
setup_logging("test_app", &["test_crate"]);
// Test with custom log level
env::set_var("LOG_LEVEL", "debug");
setup_logging("test_app", &["test_crate"]);
// Test with invalid log level (should default to info)
env::set_var("LOG_LEVEL", "invalid_level");
setup_logging("test_app", &["test_crate"]);
// Test with multiple additional crates
setup_logging("test_app", &["test_crate1", "test_crate2", "test_crate3"]);
// Clean up
env::remove_var("LOG_LEVEL");
}
#[test]
fn test_logging_output() {
// This test is more of a smoke test to ensure logging doesn't panic
// Actual log output verification would require capturing stdout/stderr
env::set_var("LOG_LEVEL", "trace");
setup_logging("test_logging", &[]);
// Log at different levels
tracing::error!("This is an error message");
tracing::warn!("This is a warning message");
tracing::info!("This is an info message");
tracing::debug!("This is a debug message");
tracing::trace!("This is a trace message");
// Clean up
env::remove_var("LOG_LEVEL");
}

View File

@@ -0,0 +1,151 @@
use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use utils::multi_service_load_balancer::{LoadBalancingStrategy, MultiServiceLoadBalancer, ServiceId};
// Mock implementation for testing without actual service discovery
mod mock {
use super::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
// Mock version of the load balancer for testing
pub struct MockMultiServiceLoadBalancer {
strategy: LoadBalancingStrategy,
services: Arc<Mutex<HashMap<ServiceId, Vec<SocketAddr>>>>,
}
impl MockMultiServiceLoadBalancer {
pub fn new(strategy: LoadBalancingStrategy) -> Self {
MockMultiServiceLoadBalancer {
strategy,
services: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn add_service(&self, service_name: &str, service_protocol: &str, endpoints: Vec<SocketAddr>) {
let service_id = ServiceId::new(service_name, service_protocol);
let mut services = self.services.lock().unwrap();
services.insert(service_id, endpoints);
}
pub fn get_endpoint(&self, service_name: &str, service_protocol: &str) -> Option<SocketAddr> {
let service_id = ServiceId::new(service_name, service_protocol);
let services = self.services.lock().unwrap();
if let Some(endpoints) = services.get(&service_id) {
if endpoints.is_empty() {
return None;
}
match self.strategy {
LoadBalancingStrategy::Random => {
let index = rand::random::<usize>() % endpoints.len();
Some(endpoints[index])
},
LoadBalancingStrategy::RoundRobin => {
// For simplicity in tests, just return the first endpoint
Some(endpoints[0])
}
}
} else {
None
}
}
}
}
#[test]
fn test_service_id() {
let service_id1 = ServiceId::new("service1", "http");
let service_id2 = ServiceId::new("service1", "http");
let service_id3 = ServiceId::new("service2", "http");
let service_id4 = ServiceId::new("service1", "https");
// Test equality
assert_eq!(service_id1, service_id2);
assert_ne!(service_id1, service_id3);
assert_ne!(service_id1, service_id4);
// Test hash implementation
let mut set = HashSet::new();
set.insert(service_id1);
assert!(set.contains(&service_id2));
assert!(!set.contains(&service_id3));
assert!(!set.contains(&service_id4));
}
#[test]
fn test_mock_load_balancer_random() {
let lb = mock::MockMultiServiceLoadBalancer::new(LoadBalancingStrategy::Random);
// Add a service with multiple endpoints
let endpoints = vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8080),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 3)), 8080),
];
lb.add_service("test-service", "http", endpoints.clone());
// Get an endpoint
let endpoint = lb.get_endpoint("test-service", "http");
assert!(endpoint.is_some());
assert!(endpoints.contains(&endpoint.unwrap()));
// Test non-existent service
let endpoint = lb.get_endpoint("non-existent", "http");
assert!(endpoint.is_none());
}
#[test]
fn test_mock_load_balancer_round_robin() {
let lb = mock::MockMultiServiceLoadBalancer::new(LoadBalancingStrategy::RoundRobin);
// Add a service with multiple endpoints
let endpoints = vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8080),
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 3)), 8080),
];
lb.add_service("test-service", "http", endpoints);
// Get an endpoint
let endpoint = lb.get_endpoint("test-service", "http");
assert!(endpoint.is_some());
// Test empty service
lb.add_service("empty-service", "http", vec![]);
let endpoint = lb.get_endpoint("empty-service", "http");
assert!(endpoint.is_none());
}
// Integration test with the actual MultiServiceLoadBalancer
// This test is disabled by default as it requires a Consul server
#[tokio::test]
async fn test_multi_service_load_balancer() {
use std::env;
// Skip test if CONSUL_TEST_ENABLED is not set to true
if env::var("CONSUL_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping MultiServiceLoadBalancer test. Set CONSUL_TEST_ENABLED=true to run.");
return;
}
let consul_url = env::var("TEST_CONSUL_URL").unwrap_or_else(|_| "http://localhost:8500".to_string());
let service_name = env::var("TEST_CONSUL_SERVICE_NAME").unwrap_or_else(|_| "database-service".to_string());
let protocol = "tcp";
let lb = MultiServiceLoadBalancer::new(&consul_url, LoadBalancingStrategy::Random);
// Refresh service endpoints
let result = lb.refresh_service_endpoints(&service_name, protocol).await;
assert!(result.is_ok(), "Failed to refresh service endpoints: {:?}", result.err());
// Get an endpoint
let result = lb.get_endpoint(&service_name, protocol).await;
assert!(result.is_ok(), "Failed to get endpoint: {:?}", result.err());
let endpoint = result.unwrap();
assert!(endpoint.is_some(), "No endpoint found for service {}", service_name);
println!("Found endpoint for service {}: {:?}", service_name, endpoint);
}

View File

@@ -0,0 +1,152 @@
use serde::{Deserialize, Serialize};
use std::env;
use tokio::sync::Mutex;
use utils::redis_cache::{Cache, RedisCache};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestData {
id: i32,
name: String,
value: f64,
}
#[tokio::test]
async fn test_redis_cache_set_get() {
// Skip test if REDIS_TEST_ENABLED is not set to true
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
return;
}
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let cache = RedisCache::new(&redis_url);
let key = "test_key_set_get".to_string();
let test_data = TestData {
id: 1,
name: "test".to_string(),
value: 3.14,
};
// Test setting a value
let set_result = cache.set(&key, &test_data, 10).await;
assert!(set_result.is_ok(), "Failed to set value: {:?}", set_result.err());
// Test getting the value
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
assert!(get_result.is_ok(), "Failed to get value: {:?}", get_result.err());
let retrieved_data = get_result.unwrap();
assert!(retrieved_data.is_some(), "Retrieved data is None");
assert_eq!(retrieved_data.unwrap(), test_data);
}
#[tokio::test]
async fn test_redis_cache_update() {
// Skip test if REDIS_TEST_ENABLED is not set to true
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
return;
}
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let cache = RedisCache::new(&redis_url);
let key = "test_key_update".to_string();
let initial_data = TestData {
id: 2,
name: "initial".to_string(),
value: 2.71,
};
let updated_data = TestData {
id: 2,
name: "updated".to_string(),
value: 2.71,
};
// Set initial value
let set_result = cache.set(&key, &initial_data, 10).await;
assert!(set_result.is_ok(), "Failed to set initial value: {:?}", set_result.err());
// Update the value
let update_result = cache.update(&key, Some(&updated_data), Some(10)).await;
assert!(update_result.is_ok(), "Failed to update value: {:?}", update_result.err());
// Get the updated value
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
assert!(get_result.is_ok(), "Failed to get updated value: {:?}", get_result.err());
let retrieved_data = get_result.unwrap();
assert!(retrieved_data.is_some(), "Retrieved data is None");
assert_eq!(retrieved_data.unwrap(), updated_data);
}
#[tokio::test]
async fn test_redis_cache_delete() {
// Skip test if REDIS_TEST_ENABLED is not set to true
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
return;
}
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let mut cache = RedisCache::new(&redis_url);
let key = "test_key_delete".to_string();
let test_data = TestData {
id: 3,
name: "delete_test".to_string(),
value: 1.618,
};
// Set a value
let set_result = cache.set(&key, &test_data, 10).await;
assert!(set_result.is_ok(), "Failed to set value: {:?}", set_result.err());
// Delete the value
let delete_result = cache.delete(&key).await;
assert!(delete_result.is_ok(), "Failed to delete value: {:?}", delete_result.err());
// Verify the value is gone
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
assert!(get_result.is_ok(), "Failed to get value after deletion: {:?}", get_result.err());
assert!(get_result.unwrap().is_none(), "Value still exists after deletion");
}
#[tokio::test]
async fn test_redis_cache_refresh() {
// Skip test if REDIS_TEST_ENABLED is not set to true
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
return;
}
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
let cache = RedisCache::new(&redis_url);
let key = "test_key_refresh".to_string();
let test_data = TestData {
id: 4,
name: "refresh_test".to_string(),
value: 0.577,
};
// Set a value with a short TTL
let set_result = cache.set(&key, &test_data, 5).await;
assert!(set_result.is_ok(), "Failed to set value: {:?}", set_result.err());
// Refresh the TTL
let refresh_result = cache.refresh(&key, 30).await;
assert!(refresh_result.is_ok(), "Failed to refresh TTL: {:?}", refresh_result.err());
// Wait for the original TTL to expire
tokio::time::sleep(tokio::time::Duration::from_secs(6)).await;
// Verify the value still exists due to the refresh
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
assert!(get_result.is_ok(), "Failed to get value after refresh: {:?}", get_result.err());
let retrieved_data = get_result.unwrap();
assert!(retrieved_data.is_some(), "Value expired despite TTL refresh");
assert_eq!(retrieved_data.unwrap(), test_data);
}

View File

@@ -0,0 +1,84 @@
use std::env;
use std::net::SocketAddr;
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
#[tokio::test]
async fn test_get_kube_service_endpoints_by_dns() {
// Skip test if KUBE_TEST_ENABLED is not set to true
if env::var("KUBE_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping Kubernetes DNS test. Set KUBE_TEST_ENABLED=true to run.");
return;
}
// Test with a known Kubernetes service
let service_name = env::var("TEST_K8S_SERVICE_NAME").unwrap_or_else(|_| "database-service".to_string());
let port_name = env::var("TEST_K8S_PORT_NAME").unwrap_or_else(|_| "database-service".to_string());
let protocol = "tcp";
let result = get_kube_service_endpoints_by_dns(&port_name, protocol, &service_name).await;
assert!(result.is_ok(), "Failed to get Kubernetes service endpoints: {:?}", result.err());
let endpoints = result.unwrap();
assert!(!endpoints.is_empty(), "No endpoints found for service {}", service_name);
// Verify that the endpoints are valid socket addresses
for endpoint in &endpoints {
assert!(endpoint.port() > 0, "Invalid port in endpoint: {}", endpoint);
}
println!("Found {} endpoints for service {}: {:?}", endpoints.len(), service_name, endpoints);
}
#[tokio::test]
async fn test_get_service_endpoints_by_dns() {
// Skip test if CONSUL_TEST_ENABLED is not set to true
if env::var("CONSUL_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
println!("Skipping Consul DNS test. Set CONSUL_TEST_ENABLED=true to run.");
return;
}
// Test with a known Consul service
let consul_url = env::var("TEST_CONSUL_URL").unwrap_or_else(|_| "127.0.0.1:8600".to_string());
let service_name = env::var("TEST_CONSUL_SERVICE_NAME").unwrap_or_else(|_| "database-service".to_string());
let protocol = "tcp";
let result = get_service_endpoints_by_dns(&consul_url, protocol, &service_name).await;
assert!(result.is_ok(), "Failed to get Consul service endpoints: {:?}", result.err());
let endpoints = result.unwrap();
assert!(!endpoints.is_empty(), "No endpoints found for service {}", service_name);
// Verify that the endpoints are valid socket addresses
for endpoint in &endpoints {
assert!(endpoint.port() > 0, "Invalid port in endpoint: {}", endpoint);
}
println!("Found {} endpoints for service {}: {:?}", endpoints.len(), service_name, endpoints);
}
// Mock tests that don't require actual infrastructure
mod mock_tests {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr;
#[test]
fn test_socket_addr_parsing() {
// Test that we can parse socket addresses correctly
let addr_str = "127.0.0.1:8080";
let addr = SocketAddr::from_str(addr_str).unwrap();
assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
assert_eq!(addr.port(), 8080);
}
#[test]
fn test_socket_addr_formatting() {
// Test that we can format socket addresses correctly
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 9000);
let addr_str = format!("{}", addr);
assert_eq!(addr_str, "192.168.1.1:9000");
}
}