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

94
tests/Cargo.toml Normal file
View File

@@ -0,0 +1,94 @@
[package]
name = "mmorpg-server-tests"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
proc-macro = false
test = true
doctest = false
bench = false
doc = false
plugin = false
harness = true
[dependencies]
auth-service = { path = "../auth-service" }
character-service = { path = "../character-service" }
database-service = { path = "../database-service" }
packet-service = { path = "../packet-service" }
utils = { path = "../utils" }
# Common dependencies
tokio = { version = "1.36.0", features = ["full"] }
tonic = "0.11.0"
bincode = { version = "2.0.0", features = ["derive", "serde"] }
serde = { version = "1.0.197", features = ["derive"] }
mockall = "0.12.1"
reqwest = { version = "0.11.24", features = ["json"] }
tracing = "0.1.40"
chrono = "0.4.35"
sqlx = "0.8.3"
serde_json = "1.0.140"
dotenv = "0.15"
[[test]]
name = "auth_users_tests"
path = "auth-service/users_tests.rs"
[[test]]
name = "character_service_tests"
path = "character-service/character_service_tests.rs"
[[test]]
name = "packet_tests"
path = "packet-service/packet_tests.rs"
[[test]]
name = "bufferpool_tests"
path = "packet-service/bufferpool_tests.rs"
[[test]]
name = "connection_service_tests"
path = "packet-service/connection_service_tests.rs"
[[test]]
name = "redis_cache_tests"
path = "utils/redis_cache_tests.rs"
[[test]]
name = "service_discovery_tests"
path = "utils/service_discovery_tests.rs"
[[test]]
name = "multi_service_load_balancer_tests"
path = "utils/multi_service_load_balancer_tests.rs"
[[test]]
name = "health_check_tests"
path = "utils/health_check_tests.rs"
[[test]]
name = "logging_tests"
path = "utils/logging_tests.rs"
[[test]]
name = "get_user_tests"
path = "database-service/get_user.rs"
[[test]]
name = "grpc_get_user_tests"
path = "database-service/grpc_get_user.rs"
[[test]]
name = "integration_tests"
path = "database-service/integration.rs"
[[test]]
name = "mock_tests"
path = "database-service/mock_tests.rs"
[[test]]
name = "redis_cache_db_tests"
path = "database-service/redis_cache.rs"

72
tests/README.md Normal file
View File

@@ -0,0 +1,72 @@
# MMORPG Server Tests
This directory contains tests for all components of the MMORPG server architecture.
## Running Tests
### Running All Tests
To run all tests:
```bash
cd tests
cargo test
```
### Running Tests for a Specific Service
To run tests for a specific service:
```bash
cd tests
cargo test --test auth_users_tests
cargo test --test character_service_tests
cargo test --test packet_tests
cargo test --test redis_cache_tests
cargo test --test get_user_tests
cargo test --test grpc_get_user_tests
cargo test --test mock_tests
# etc.
```
### Running Tests with Environment Variables
Some tests require environment variables to be set:
```bash
# Redis tests
export REDIS_TEST_ENABLED=true
export TEST_REDIS_URL=redis://127.0.0.1:6379
# Kubernetes tests
export KUBE_TEST_ENABLED=true
export TEST_K8S_SERVICE_NAME=database-service
export TEST_K8S_PORT_NAME=database-service
# Consul tests
export CONSUL_TEST_ENABLED=true
export TEST_CONSUL_URL=127.0.0.1:8600
export TEST_CONSUL_SERVICE_NAME=database-service
# Run tests
cd tests
cargo test
```
## Test Organization
Tests are organized by service:
- **auth-service/**: Tests for the authentication service
- **character-service/**: Tests for the character service
- **database-service/**: Tests for the database service
- **packet-service/**: Tests for the packet service
- **utils/**: Tests for shared utilities
## Adding New Tests
To add a new test:
1. Create a new test file in the appropriate service directory
2. Add the test to the `[[test]]` section in `Cargo.toml`
3. Run the test to ensure it works correctly

View File

@@ -0,0 +1,70 @@
use auth_service::users::{hash_password, verify_password};
#[test]
fn test_password_hashing_and_verification() {
// Test with a simple password
let password = "test_password";
let hashed = hash_password(password);
// Verify the hash is not the same as the original password
assert_ne!(password, hashed);
// Verify the password against the hash
assert!(verify_password(password, &hashed));
// Verify an incorrect password fails
assert!(!verify_password("wrong_password", &hashed));
}
#[test]
fn test_password_hashing_with_special_characters() {
// Test with special characters
let password = "P@$$w0rd!#%^&*()";
let hashed = hash_password(password);
// Verify the hash is not the same as the original password
assert_ne!(password, hashed);
// Verify the password against the hash
assert!(verify_password(password, &hashed));
}
#[test]
fn test_password_hashing_with_unicode() {
// Test with Unicode characters
let password = "пароль123你好世界";
let hashed = hash_password(password);
// Verify the hash is not the same as the original password
assert_ne!(password, hashed);
// Verify the password against the hash
assert!(verify_password(password, &hashed));
}
#[test]
fn test_different_passwords_produce_different_hashes() {
let password1 = "password1";
let password2 = "password2";
let hash1 = hash_password(password1);
let hash2 = hash_password(password2);
// Different passwords should produce different hashes
assert_ne!(hash1, hash2);
}
#[test]
fn test_same_password_produces_different_hashes() {
let password = "same_password";
let hash1 = hash_password(password);
let hash2 = hash_password(password);
// Same password should produce different hashes due to salt
assert_ne!(hash1, hash2);
// But both hashes should verify against the original password
assert!(verify_password(password, &hash1));
assert!(verify_password(password, &hash2));
}

View File

@@ -0,0 +1,251 @@
use character_service::character_service::character::character_service_server::CharacterService;
use character_service::character_service::character::{
CreateCharacterRequest, DeleteCharacterRequest, GetCharacterListRequest, GetCharacterRequest,
};
use character_service::character_service::MyCharacterService;
use mockall::predicate::*;
use mockall::predicate::eq;
use mockall::mock;
// Wrapper for the mock to implement the trait
struct MockWrapper(MockCharacterDbClientTrait);
impl character_service::character_db_client::CharacterDbClient for MockWrapper {
async fn get_character_list(&mut self, user_id: &str) -> Result<character_service::database::CharacterListResponse, Box<dyn std::error::Error>> {
self.0.get_character_list(user_id).await
}
async fn create_character(&mut self, user_id: &str, name: &str, race: i32, face: i32, hair: i32, stone: i32) -> Result<character_service::database::CreateCharacterResponse, Box<dyn std::error::Error>> {
self.0.create_character(user_id, name, race, face, hair, stone).await
}
async fn delete_character(&mut self, user_id: &str, char_id: &str, delete_type: i32) -> Result<character_service::database::DeleteCharacterResponse, Box<dyn std::error::Error>> {
self.0.delete_character(user_id, char_id, delete_type).await
}
async fn get_character(&mut self, user_id: &str, char_id: &str) -> Result<character_service::database::Character, Box<dyn std::error::Error>> {
self.0.get_character(user_id, char_id).await
}
}
use std::sync::Arc;
use tonic::{Request, Status};
// Define a trait for the CharacterDbClient to make it mockable
#[mockall::automock]
trait CharacterDbClientTrait {
async fn get_character_list(&mut self, user_id: &str) -> Result<character_service::database::CharacterListResponse, Box<dyn std::error::Error>>;
async fn create_character(&mut self, user_id: &str, name: &str, race: i32, face: i32, hair: i32, stone: i32) -> Result<character_service::database::CreateCharacterResponse, Box<dyn std::error::Error>>;
async fn delete_character(&mut self, user_id: &str, char_id: &str, delete_type: i32) -> Result<character_service::database::DeleteCharacterResponse, Box<dyn std::error::Error>>;
async fn get_character(&mut self, user_id: &str, char_id: &str) -> Result<character_service::database::Character, Box<dyn std::error::Error>>;
}
#[tokio::test]
async fn test_get_character_list() {
// Create a mock CharacterDbClient
let mut mock_client = MockCharacterDbClientTrait::new();
// Set up expectations
mock_client
.expect_get_character_list()
.with(eq("test_user"))
.times(1)
.returning(|_| {
Ok(character_service::database::CharacterListResponse {
characters: vec![
character_service::database::Character {
id: 1,
user_id: "test_user".to_string(),
name: "Character1".to_string(),
money: 1000,
inventory: "{\"items\":[]}".to_string(),
stats: "{\"level\":1}".to_string(),
skills: "{\"skills\":[]}".to_string(),
looks: "{\"race\":0}".to_string(),
position: "{\"x\":0,\"y\":0,\"z\":0}".to_string(),
deleted_at: "".to_string(),
},
character_service::database::Character {
id: 2,
user_id: "test_user".to_string(),
name: "Character2".to_string(),
money: 2000,
inventory: "{\"items\":[]}".to_string(),
stats: "{\"level\":5}".to_string(),
skills: "{\"skills\":[]}".to_string(),
looks: "{\"race\":1}".to_string(),
position: "{\"x\":10,\"y\":10,\"z\":0}".to_string(),
deleted_at: "".to_string(),
},
],
})
});
// Create a wrapper struct that implements CharacterDbClient
struct MockWrapper(MockCharacterDbClientTrait);
impl std::ops::Deref for MockWrapper {
type Target = MockCharacterDbClientTrait;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for MockWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Clone for MockWrapper {
fn clone(&self) -> Self {
panic!("Mock should not be cloned")
}
}
// Create the service with the mock client
let service = MyCharacterService {
character_db_client: Arc::new(MockWrapper(mock_client)),
};
// Create a request
let request = Request::new(GetCharacterListRequest {
user_id: "test_user".to_string(),
});
// Call the service method
let response = service.get_character_list(request).await.unwrap();
let response = response.into_inner();
// Verify the response
assert_eq!(response.characters.len(), 2);
assert_eq!(response.characters[0].name, "Character1");
assert_eq!(response.characters[1].name, "Character2");
}
#[tokio::test]
async fn test_create_character() {
// Create a mock CharacterDbClient
let mut mock_client = MockCharacterDbClientTrait::new();
// Set up expectations
mock_client
.expect_create_character()
.with(eq("test_user"), eq("NewCharacter"), eq(0), eq(1), eq(2), eq(3))
.times(1)
.returning(|_, _, _, _, _, _| {
Ok(character_service::database::CreateCharacterResponse {
result: 0,
character_id: 3,
})
});
// Create the service with the mock client
let service = MyCharacterService {
character_db_client: Arc::new(MockWrapper(mock_client)),
};
// Create a request
let request = Request::new(CreateCharacterRequest {
user_id: "test_user".to_string(),
name: "NewCharacter".to_string(),
race: 0,
face: 1,
hair: 2,
stone: 3,
});
// Call the service method
let response = service.create_character(request).await.unwrap();
let response = response.into_inner();
// Verify the response
assert_eq!(response.result, 0);
}
#[tokio::test]
async fn test_delete_character() {
// Create a mock CharacterDbClient
let mut mock_client = MockCharacterDbClientTrait::new();
// Set up expectations
mock_client
.expect_delete_character()
.with(eq("test_user"), eq("3"), eq(1))
.times(1)
.returning(|_, _, _| {
Ok(character_service::database::DeleteCharacterResponse {
remaining_time: 86400, // 24 hours in seconds
name: "DeletedCharacter".to_string(),
})
});
// Create the service with the mock client
let service = MyCharacterService {
character_db_client: Arc::new(MockWrapper(mock_client)),
};
// Create a request
let request = Request::new(DeleteCharacterRequest {
user_id: "test_user".to_string(),
char_id: "3".to_string(),
delete_type: 1,
});
// Call the service method
let response = service.delete_character(request).await.unwrap();
let response = response.into_inner();
// Verify the response
assert_eq!(response.remaining_time, 86400);
assert_eq!(response.name, "DeletedCharacter");
}
#[tokio::test]
async fn test_get_character() {
// Create a mock CharacterDbClient
let mut mock_client = MockCharacterDbClientTrait::new();
// Set up expectations
mock_client
.expect_get_character()
.with(eq("test_user"), eq("1"))
.times(1)
.returning(|_, _| {
Ok(character_service::database::Character {
id: 1,
user_id: "test_user".to_string(),
name: "Character1".to_string(),
money: 1000,
inventory: "{\"items\":[{\"id\":1,\"count\":10}]}".to_string(),
stats: "{\"level\":10,\"hp\":100,\"mp\":50}".to_string(),
skills: "{\"skills\":[{\"id\":1,\"level\":5}]}".to_string(),
looks: "{\"race\":0,\"face\":1,\"hair\":2}".to_string(),
position: "{\"x\":100,\"y\":200,\"z\":0,\"map\":1}".to_string(),
deleted_at: "".to_string(),
})
});
// Create the service with the mock client
let service = MyCharacterService {
character_db_client: Arc::new(MockWrapper(mock_client)),
};
// Create a request
let request = Request::new(GetCharacterRequest {
user_id: "test_user".to_string(),
char_id: "1".to_string(),
});
// Call the service method
let response = service.get_character(request).await.unwrap();
let response = response.into_inner();
// Verify the response
let character = response.character.unwrap();
assert_eq!(character.name, "Character1");
assert_eq!(character.money, 1000);
// Verify JSON fields were parsed correctly
assert!(character.inventory.contains("items"));
assert!(character.stats.contains("level"));
assert!(character.skills.contains("skills"));
assert!(character.looks.contains("race"));
assert!(character.position.contains("map"));
}

View File

@@ -0,0 +1,78 @@
use database_service::users::{User, UserRepository};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::RedisCache;
// Helper function to create a test database pool
async fn setup_test_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/test_db".to_string());
PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
}
// Helper function to create a mock Redis cache
fn setup_test_cache() -> Arc<Mutex<RedisCache>> {
let redis_url = std::env::var("TEST_REDIS_URL")
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
Arc::new(Mutex::new(RedisCache::new(&redis_url)))
}
// Helper function to create a test user in the database
async fn create_test_user(pool: &sqlx::PgPool) -> i32 {
let result = sqlx::query!(
r#"
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
VALUES ('test_user', 'test@example.com', 'user', NOW(), NOW())
RETURNING id
"#
)
.fetch_one(pool)
.await
.expect("Failed to create test user");
result.id
}
// Helper function to clean up test data
async fn cleanup_test_user(pool: &sqlx::PgPool, user_id: i32) {
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
.execute(pool)
.await
.expect("Failed to delete test user");
}
#[tokio::test]
async fn test_get_user() {
// Skip test if database connection is not available
let pool = match setup_test_pool().await {
Ok(pool) => pool,
Err(_) => {
println!("Skipping test_get_user: Test database not available");
return;
}
};
let cache = setup_test_cache();
let repo = UserRepository::new(pool.clone(), cache);
// Create test user
let user_id = create_test_user(&pool).await;
// Test the get_user_by_id function
let user = repo.get_user_by_id(user_id).await.unwrap();
// Validate the user
assert_eq!(user.id, user_id);
assert_eq!(user.name, "test_user");
assert_eq!(user.email, "test@example.com");
assert_eq!(user.role, "user");
// Cleanup
cleanup_test_user(&pool, user_id).await;
}

View File

@@ -0,0 +1,92 @@
use database_service::db::Database;
use database_service::grpc::database_service::MyDatabaseService;
use database_service::grpc::user_service_server::UserService;
use database_service::grpc::{GetUserRequest, GetUserResponse};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tokio::sync::Mutex;
use tonic::{Request, Response, Status};
use utils::redis_cache::RedisCache;
// Helper function to create a test database pool
async fn setup_test_pool() -> sqlx::PgPool {
let database_url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/test_db".to_string());
PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to create test database pool")
}
// Helper function to create a mock Redis cache
fn setup_test_cache() -> Arc<Mutex<RedisCache>> {
let redis_url = std::env::var("TEST_REDIS_URL")
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
Arc::new(Mutex::new(RedisCache::new(&redis_url)))
}
// Helper function to create a test user in the database
async fn create_test_user(pool: &sqlx::PgPool) -> i32 {
let result = sqlx::query!(
r#"
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
VALUES ('test_user', 'test@example.com', 'user', NOW(), NOW())
RETURNING id
"#
)
.fetch_one(pool)
.await
.expect("Failed to create test user");
result.id
}
// Helper function to clean up test data
async fn cleanup_test_user(pool: &sqlx::PgPool, user_id: i32) {
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
.execute(pool)
.await
.expect("Failed to delete test user");
}
#[tokio::test]
async fn test_grpc_get_user() {
// Skip test if database connection is not available
let pool_result = setup_test_pool().await;
let pool = match pool_result {
Ok(pool) => pool,
Err(_) => {
println!("Skipping test_grpc_get_user: Test database not available");
return;
}
};
let cache = setup_test_cache();
// Create test user
let user_id = create_test_user(&pool).await;
// Create the database service
let db = Arc::new(Database::new(pool.clone(), cache));
let service = MyDatabaseService { db };
// Create a gRPC request
let request = Request::new(GetUserRequest {
user_id,
});
// Call the service
let response = service.get_user(request).await.unwrap().into_inner();
// Validate the response
assert_eq!(response.user_id, user_id);
assert_eq!(response.username, "test_user");
assert_eq!(response.email, "test@example.com");
assert_eq!(response.role, "user");
// Cleanup
cleanup_test_user(&pool, user_id).await;
}

View File

@@ -0,0 +1,12 @@
#[cfg(test)]
mod tests {
use dotenv::dotenv;
#[tokio::test]
async fn test_health_check() {
dotenv().ok();
// let database_url = std::env::var("DATABASE_URL").unwrap();
// let db = Database::new(&database_url).await;
// assert!(db.health_check().await);
}
}

View File

@@ -0,0 +1,137 @@
use database_service::users::User;
use mockall::predicate::*;
use mockall::predicate::eq;
use mockall::mock;
use std::sync::Arc;
use chrono::NaiveDateTime;
// Mock the UserRepository
mock! {
pub UserRepository {
fn get_user_by_id(&self, user_id: i32) -> Result<User, sqlx::Error>;
fn get_user_by_username(&self, username: &str) -> Result<User, sqlx::Error>;
fn get_user_by_email(&self, email: &str) -> Result<User, sqlx::Error>;
}
}
// Mock the CharacterRepository
mock! {
pub CharacterRepository {
fn get_character_by_id(&self, character_id: i32) -> Result<database_service::characters::Character, sqlx::Error>;
fn get_character_list(&self, user_id: String) -> Result<Vec<database_service::characters::Character>, sqlx::Error>;
fn create_character(
&self,
user_id: String,
name: &str,
inventory: serde_json::Value,
skills: serde_json::Value,
stats: serde_json::Value,
looks: serde_json::Value,
position: serde_json::Value,
) -> Result<i32, sqlx::Error>;
fn delete_character(&self, character_id: i32, delete_type: i32) -> Result<i64, sqlx::Error>;
}
}
// Mock the SessionRepository
mock! {
pub SessionRepository {
fn get_session(&self, session_id: &str) -> Result<database_service::sessions::Session, sqlx::Error>;
fn refresh_session(&self, session_id: &str) -> Result<database_service::sessions::Session, sqlx::Error>;
}
}
// Mock the Database struct
struct MockDatabase<U, C, S> {
user_repo: Arc<U>,
character_repo: Arc<C>,
session_repo: Arc<S>,
}
type MockDatabaseWithMocks = MockDatabase<MockUserRepository, MockCharacterRepository, MockSessionRepository>;
#[tokio::test]
async fn test_user_repository_mock() {
// Create a mock UserRepository
let mut mock_user_repo = MockUserRepository::new();
// Set up expectations
mock_user_repo
.expect_get_user_by_id()
.with(eq(123))
.times(1)
.returning(|_| {
Ok(User {
id: 123,
name: "mock_user".to_string(),
email: "mock@example.com".to_string(),
role: "user".to_string(),
created_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
updated_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
})
});
// Test the mock
let user = mock_user_repo.get_user_by_id(123).unwrap();
assert_eq!(user.id, 123);
assert_eq!(user.name, "mock_user");
assert_eq!(user.email, "mock@example.com");
}
#[tokio::test]
async fn test_character_repository_mock() {
// Create a mock CharacterRepository
let mut mock_character_repo = MockCharacterRepository::new();
// Set up expectations
mock_character_repo
.expect_get_character_by_id()
.with(eq(456))
.times(1)
.returning(|_| {
Ok(database_service::characters::Character {
id: 456,
user_id: "123".to_string(),
name: "mock_character".to_string(),
money: 1000,
inventory: serde_json::json!({}),
stats: serde_json::json!({}),
skills: serde_json::json!({}),
looks: serde_json::json!({}),
position: serde_json::json!({}),
created_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
updated_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
deleted_at: None,
is_active: true,
})
});
// Test the mock
let character = mock_character_repo.get_character_by_id(456).unwrap();
assert_eq!(character.id, 456);
assert_eq!(character.name, "mock_character");
assert_eq!(character.user_id, "123");
}
#[tokio::test]
async fn test_session_repository_mock() {
// Create a mock SessionRepository
let mut mock_session_repo = MockSessionRepository::new();
// Set up expectations
mock_session_repo
.expect_get_session()
.with(eq("session123"))
.times(1)
.returning(|_| {
Ok(database_service::sessions::Session {
id: "session123".to_string(),
user_id: "123".to_string(),
})
});
// Test the mock
let session = mock_session_repo.get_session("session123").unwrap();
assert_eq!(session.id, "session123");
assert_eq!(session.user_id, "123");
}

View File

@@ -0,0 +1,16 @@
#[tokio::test]
async fn test_redis_cache() {
// dotenv().ok();
// let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
// let cache = RedisCache::new(&redis_url);
//
// let key = &"test_key".to_string();
// let value = "test_value";
//
// // Test setting a value
// cache.set(key, &value, 10).await.unwrap();
//
// // Test getting the value
// let cached_value: Option<String> = cache.get(key).await.unwrap();
// assert_eq!(cached_value, Some("test_value".to_string()));
}

View File

@@ -0,0 +1,151 @@
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::RedisCache;
// Helper function to create a test database pool
pub async fn setup_test_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/test_db".to_string());
PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
}
// Helper function to create a mock Redis cache
pub fn setup_test_cache() -> Arc<Mutex<RedisCache>> {
let redis_url = std::env::var("TEST_REDIS_URL")
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
Arc::new(Mutex::new(RedisCache::new(&redis_url)))
}
// Helper function to create a test user in the database
pub async fn create_test_user(pool: &sqlx::PgPool, name: &str, email: &str) -> i32 {
let result = sqlx::query!(
r#"
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
VALUES ($1, $2, 'user', NOW(), NOW())
RETURNING id
"#,
name,
email
)
.fetch_one(pool)
.await
.expect("Failed to create test user");
result.id
}
// Helper function to create a test character in the database
pub async fn create_test_character(
pool: &sqlx::PgPool,
user_id: i32,
name: &str
) -> i32 {
let inventory = serde_json::json!({
"items": [],
"capacity": 100
});
let stats = serde_json::json!({
"strength": 10,
"dexterity": 10,
"intelligence": 10,
"vitality": 10
});
let skills = serde_json::json!({
"skills": []
});
let looks = serde_json::json!({
"race": 1,
"gender": 0,
"hair": 1,
"face": 1
});
let position = serde_json::json!({
"mapId": 1,
"x": 100.0,
"y": 100.0,
"z": 0.0
});
let result = sqlx::query!(
r#"
INSERT INTO character (
"userId", name, money, inventory, stats, skills, looks, position,
"createdAt", "updatedAt", "isActive"
)
VALUES ($1, $2, 0, $3, $4, $5, $6, $7, NOW(), NOW(), true)
RETURNING id
"#,
user_id,
name,
inventory,
stats,
skills,
looks,
position
)
.fetch_one(pool)
.await
.expect("Failed to create test character");
result.id
}
// Helper function to create a test session in the database
pub async fn create_test_session(pool: &sqlx::PgPool, user_id: i32) -> String {
let session_id = uuid::Uuid::new_v4().to_string();
sqlx::query!(
r#"
INSERT INTO session (id, "userId", "createdAt", "expiresAt")
VALUES ($1, $2, NOW(), NOW() + INTERVAL '1 hour')
"#,
session_id,
user_id
)
.execute(pool)
.await
.expect("Failed to create test session");
session_id
}
// Helper function to clean up test user data
pub async fn cleanup_test_user(pool: &sqlx::PgPool, user_id: i32) {
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
.execute(pool)
.await
.expect("Failed to delete test user");
}
// Helper function to clean up test character data
pub async fn cleanup_test_character(pool: &sqlx::PgPool, character_id: i32) {
sqlx::query!(r#"DELETE FROM character WHERE id = $1"#, character_id)
.execute(pool)
.await
.expect("Failed to delete test character");
}
// Helper function to clean up test session data
pub async fn cleanup_test_session(pool: &sqlx::PgPool, session_id: &str) {
sqlx::query!(r#"DELETE FROM session WHERE id = $1"#, session_id)
.execute(pool)
.await
.expect("Failed to delete test session");
}
// Helper function to clean up all test data
pub async fn cleanup_test_data(pool: &sqlx::PgPool, user_id: i32, character_id: i32, session_id: &str) {
cleanup_test_session(pool, session_id).await;
cleanup_test_character(pool, character_id).await;
cleanup_test_user(pool, user_id).await;
}

View File

@@ -0,0 +1,96 @@
use packet_service::bufferpool::BufferPool;
use std::sync::Arc;
#[tokio::test]
async fn test_buffer_pool_creation() {
let pool_size = 5;
let pool = BufferPool::new(pool_size);
// Verify we can acquire the expected number of buffers
for _ in 0..pool_size {
let buffer = pool.acquire().await;
assert!(buffer.is_some());
}
// The next acquire should return None since all buffers are in use
let buffer = pool.acquire().await;
assert!(buffer.is_none());
}
#[tokio::test]
async fn test_buffer_pool_release() {
let pool_size = 3;
let pool = BufferPool::new(pool_size);
// Acquire all buffers
let mut buffers = Vec::new();
for _ in 0..pool_size {
let buffer = pool.acquire().await.unwrap();
buffers.push(buffer);
}
// Release one buffer
let buffer = buffers.pop().unwrap();
pool.release(buffer).await;
// We should be able to acquire one buffer now
let buffer = pool.acquire().await;
assert!(buffer.is_some());
// But not two
let buffer = pool.acquire().await;
assert!(buffer.is_none());
}
#[tokio::test]
async fn test_buffer_pool_concurrent_access() {
let pool_size = 10;
let pool = Arc::new(BufferPool::new(pool_size));
// Spawn multiple tasks to acquire and release buffers
let mut handles = Vec::new();
for i in 0..pool_size {
let pool_clone = pool.clone();
let handle = tokio::spawn(async move {
// Acquire a buffer
let mut buffer = pool_clone.acquire().await.unwrap();
// Write some data to the buffer
buffer[0] = i as u8;
// Simulate some work
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
// Release the buffer
pool_clone.release(buffer).await;
});
handles.push(handle);
}
// Wait for all tasks to complete
for handle in handles {
handle.await.unwrap();
}
// All buffers should be available again
for _ in 0..pool_size {
let buffer = pool.acquire().await;
assert!(buffer.is_some());
}
}
#[tokio::test]
async fn test_buffer_pool_buffer_size() {
let pool = BufferPool::new(1);
// Acquire a buffer
let buffer = pool.acquire().await.unwrap();
// Buffer should be large enough for maximum packet size
assert!(buffer.len() >= 0xFFF);
// Release the buffer
pool.release(buffer).await;
}

View File

@@ -0,0 +1,108 @@
use packet_service::connection_service::ConnectionService;
use packet_service::connection_state::ConnectionState;
use std::collections::HashSet;
#[test]
fn test_connection_service_add_connection() {
let service = ConnectionService::new();
// Add a connection
let connection_id = service.add_connection();
// Verify the connection exists
let connection = service.get_connection(&connection_id);
assert!(connection.is_some());
}
#[test]
fn test_connection_service_remove_connection() {
let service = ConnectionService::new();
// Add a connection
let connection_id = service.add_connection();
// Verify the connection exists
let connection = service.get_connection(&connection_id);
assert!(connection.is_some());
// Remove the connection
service.remove_connection(&connection_id);
// Verify the connection no longer exists
let connection = service.get_connection(&connection_id);
assert!(connection.is_none());
}
#[test]
fn test_connection_service_get_connection_mut() {
let service = ConnectionService::new();
// Add a connection
let connection_id = service.add_connection();
// Get a mutable reference to the connection
let mut connection = service.get_connection_mut(&connection_id).unwrap();
// Modify the connection
connection.user_id = Some("test_user".to_string());
connection.session_id = Some("test_session".to_string());
connection.character_id = Some(123);
// Drop the mutable reference
drop(connection);
// Verify the changes were saved
let connection = service.get_connection(&connection_id).unwrap();
assert_eq!(connection.user_id, Some("test_user".to_string()));
assert_eq!(connection.session_id, Some("test_session".to_string()));
assert_eq!(connection.character_id, Some(123));
}
#[test]
fn test_connection_service_multiple_connections() {
let service = ConnectionService::new();
// Add multiple connections
let connection_ids: Vec<String> = (0..10).map(|_| service.add_connection()).collect();
// Verify all connections exist
for connection_id in &connection_ids {
let connection = service.get_connection(connection_id);
assert!(connection.is_some());
}
// Verify all connection IDs are unique
let unique_ids: HashSet<String> = connection_ids.iter().cloned().collect();
assert_eq!(unique_ids.len(), connection_ids.len());
}
#[test]
fn test_connection_state_new() {
let state = ConnectionState::new();
// Verify initial state
assert_eq!(state.user_id, None);
assert_eq!(state.session_id, None);
assert_eq!(state.character_id, None);
assert_eq!(state.character_list, None);
assert!(state.additional_data.is_empty());
}
#[test]
fn test_connection_state_additional_data() {
let mut state = ConnectionState::new();
// Add some additional data
state.additional_data.insert("key1".to_string(), "value1".to_string());
state.additional_data.insert("key2".to_string(), "value2".to_string());
// Verify the data was added
assert_eq!(state.additional_data.get("key1"), Some(&"value1".to_string()));
assert_eq!(state.additional_data.get("key2"), Some(&"value2".to_string()));
// Update a value
state.additional_data.insert("key1".to_string(), "updated".to_string());
// Verify the value was updated
assert_eq!(state.additional_data.get("key1"), Some(&"updated".to_string()));
}

View File

@@ -0,0 +1,98 @@
use bincode::{Decode, Encode};
use packet_service::packet::{Packet, PacketPayload};
use packet_service::packet_type::PacketType;
// Define a test payload struct
#[derive(Debug, Encode, Decode, PartialEq)]
struct TestPayload {
id: u32,
name: String,
value: f32,
}
impl PacketPayload for TestPayload {}
#[test]
fn test_packet_creation() {
let payload = TestPayload {
id: 123,
name: "test".to_string(),
value: 3.14,
};
let packet = Packet::new(PacketType::PakcsAlive, &payload).unwrap();
// Check packet fields
assert_eq!(packet.packet_type, PacketType::PakcsAlive);
assert_eq!(packet.packet_crc, 0); // CRC is currently not implemented
assert!(!packet.payload.is_empty());
}
#[test]
fn test_packet_serialization_deserialization() {
let original_payload = TestPayload {
id: 456,
name: "serialization_test".to_string(),
value: 2.71,
};
// Create a packet
let packet = Packet::new(PacketType::PakcsAlive, &original_payload).unwrap();
// Serialize to raw bytes
let raw_data = packet.to_raw();
// Deserialize from raw bytes
let deserialized_packet = Packet::from_raw(&raw_data).unwrap();
// Check packet fields match
assert_eq!(deserialized_packet.packet_type, packet.packet_type);
assert_eq!(deserialized_packet.packet_size, packet.packet_size);
assert_eq!(deserialized_packet.packet_crc, packet.packet_crc);
// Parse the payload
let deserialized_payload: TestPayload = deserialized_packet.parse().unwrap();
// Check payload fields match
assert_eq!(deserialized_payload, original_payload);
}
#[test]
fn test_packet_from_raw_invalid_size() {
// Create a packet with invalid size (too small)
let raw_data = vec![0, 0, 0, 0]; // Only 4 bytes, less than minimum 6 bytes
let result = Packet::from_raw(&raw_data);
assert!(result.is_err());
}
#[test]
fn test_packet_from_raw_size_mismatch() {
// Create a packet with size mismatch
let mut raw_data = vec![0; 10]; // 10 bytes
// Set packet size to 20 (more than actual data)
raw_data[0] = 20;
raw_data[1] = 0;
let result = Packet::from_raw(&raw_data);
assert!(result.is_err());
}
#[test]
fn test_packet_payload_encoding_decoding() {
let original_payload = TestPayload {
id: 789,
name: "encoding_test".to_string(),
value: 1.618,
};
// Encode payload
let encoded = bincode::encode_to_vec(&original_payload, bincode::config::standard()).unwrap();
// Decode payload
let decoded: TestPayload = bincode::decode_from_slice(&encoded, bincode::config::standard()).unwrap().0;
// Check payload fields match
assert_eq!(decoded, original_payload);
}

18
tests/src/lib.rs Normal file
View File

@@ -0,0 +1,18 @@
// This file is required to make the tests directory a proper Rust library crate
// It will only be compiled when running tests
#[cfg(test)]
pub mod test_utils {
// Common test utilities can go here
pub fn setup() {
// Common test setup code
}
pub fn teardown() {
// Common test teardown code
}
}
// This ensures the crate is only compiled during testing
#[cfg(not(test))]
compile_error!("This crate is only meant to be used in test mode");

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");
}
}