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

229
database-service/API.md Normal file
View File

@@ -0,0 +1,229 @@
# Database Service API Documentation
This document provides detailed information about the gRPC API endpoints exposed by the Database Service.
## UserService
### GetUser
Retrieves a user by their ID.
**Request:**
```protobuf
message GetUserRequest {
int32 user_id = 1;
}
```
**Response:**
```protobuf
message GetUserResponse {
int32 user_id = 1;
string username = 2;
string email = 3;
string role = 4;
}
```
**Error Codes:**
- `NOT_FOUND`: User with the specified ID does not exist
### GetUserByUsername
Retrieves a user by their username.
**Request:**
```protobuf
message GetUserByUsernameRequest {
string username = 1;
}
```
**Response:**
```protobuf
message GetUserResponse {
int32 user_id = 1;
string username = 2;
string email = 3;
string role = 4;
}
```
**Error Codes:**
- `NOT_FOUND`: User with the specified username does not exist
### GetUserByEmail
Retrieves a user by their email address.
**Request:**
```protobuf
message GetUserByEmailRequest {
string email = 1;
}
```
**Response:**
```protobuf
message GetUserResponse {
int32 user_id = 1;
string username = 2;
string email = 3;
string role = 4;
}
```
**Error Codes:**
- `NOT_FOUND`: User with the specified email does not exist
## CharacterDbService
### GetCharacter
Retrieves a character by ID.
**Request:**
```protobuf
message CharacterRequest {
string user_id = 1;
int32 character_id = 2;
}
```
**Response:**
```protobuf
message Character {
int32 id = 1;
string user_id = 2;
string name = 3;
int64 money = 4;
string inventory = 6;
string stats = 7;
string skills = 8;
string looks = 9;
string position = 10;
string created_at = 11;
string updated_at = 12;
string deleted_at = 13;
bool is_active = 14;
}
```
**Error Codes:**
- `NOT_FOUND`: Character with the specified ID does not exist
### GetCharacterList
Retrieves all characters for a user.
**Request:**
```protobuf
message CharacterListRequest {
string user_id = 1;
}
```
**Response:**
```protobuf
message CharacterListResponse {
repeated Character characters = 1;
}
```
### CreateCharacter
Creates a new character.
**Request:**
```protobuf
message CreateCharacterRequest {
string user_id = 1;
string name = 2;
string inventory = 3; // JSON serialized
string skills = 4; // JSON serialized
string stats = 5; // JSON serialized
string looks = 6; // JSON serialized
string position = 7; // JSON serialized
}
```
**Response:**
```protobuf
message CreateCharacterResponse {
int32 result = 1;
int32 character_id = 2;
}
```
**Error Codes:**
- `INTERNAL`: Failed to create character
### DeleteCharacter
Marks a character for deletion.
**Request:**
```protobuf
message DeleteCharacterRequest {
string user_id = 1;
int32 character_id = 2;
int32 delete_type = 3;
}
```
**Response:**
```protobuf
message DeleteCharacterResponse {
int64 remaining_time = 1;
string name = 2;
}
```
**Error Codes:**
- `INTERNAL`: Failed to delete character
## SessionService
### GetSession
Retrieves session information.
**Request:**
```protobuf
message GetSessionRequest {
string session_id = 1;
}
```
**Response:**
```protobuf
message GetSessionResponse {
string session_id = 1;
string user_id = 2;
}
```
**Error Codes:**
- `NOT_FOUND`: Session with the specified ID does not exist
### RefreshSession
Updates session expiration.
**Request:**
```protobuf
message RefreshSessionRequest {
string session_id = 1;
}
```
**Response:**
```protobuf
message RefreshSessionResponse {
string session_id = 1;
string user_id = 2;
}
```
**Error Codes:**
- `NOT_FOUND`: Session with the specified ID does not exist

111
database-service/README.md Normal file
View File

@@ -0,0 +1,111 @@
# Database Service
The Database Service is a core component of the MMORPG server architecture, providing centralized database access for user accounts, character data, and session management.
## Overview
The Database Service exposes gRPC endpoints that allow other services to interact with the PostgreSQL database and Redis cache. It implements the repository pattern to abstract database operations and provides efficient caching mechanisms to improve performance.
## Architecture
The service is built using the following components:
- **gRPC Server**: Exposes endpoints for user, character, and session operations
- **Repository Layer**: Implements data access logic with caching
- **PostgreSQL Database**: Stores persistent data
- **Redis Cache**: Provides high-speed caching for frequently accessed data
## Services
The Database Service exposes the following gRPC services:
### UserService
Handles user account operations:
- `GetUser`: Retrieves a user by ID
- `GetUserByUsername`: Retrieves a user by username
- `GetUserByEmail`: Retrieves a user by email
### CharacterDbService
Handles character operations:
- `GetCharacter`: Retrieves a character by ID
- `GetCharacterList`: Retrieves all characters for a user
- `CreateCharacter`: Creates a new character
- `DeleteCharacter`: Marks a character for deletion
### SessionService
Handles session management:
- `GetSession`: Retrieves session information
- `RefreshSession`: Updates session expiration
## Repositories
The service implements the repository pattern with the following repositories:
### UserRepository
Manages user data with methods for retrieving users by ID, username, email, or session.
### CharacterRepository
Manages character data with methods for creating, retrieving, and deleting characters.
### SessionRepository
Manages session data with methods for retrieving and refreshing sessions.
## Caching Strategy
The service uses Redis for caching with the following strategy:
- Cache keys follow a consistent pattern (e.g., `user:{id}`, `character:{id}`)
- Default TTL of 300 seconds (5 minutes)
- Cache invalidation on updates
- Fallback to database when cache misses
## Configuration
The service is configured using environment variables:
- `LISTEN_ADDR`: The address to listen on (default: 0.0.0.0)
- `SERVICE_PORT`: The port to listen on (default: 50052)
- `DATABASE_URL`: PostgreSQL connection string
- `REDIS_URL`: Redis connection string (default: redis://127.0.0.1:6379)
## Running the Service
```bash
# Set environment variables
export DATABASE_URL=postgres://username:password@localhost:5432/dbname
export REDIS_URL=redis://localhost:6379
# Run the service
cargo run
```
## Docker
The service can be run using Docker:
```bash
docker build -t database-service .
docker run -p 50052:50052 \
-e DATABASE_URL=postgres://username:password@host.docker.internal:5432/dbname \
-e REDIS_URL=redis://host.docker.internal:6379 \
database-service
```
## Integration with External Systems
The Database Service is designed to work with an external database schema management system:
- **Schema Management**: The database schema is managed by an external web application using better-auth
- **User Management**: User creation and updates are handled by the external system
- **Session Management**: Initial session creation is handled by the external system
The Database Service primarily provides read access to this data for other microservices, with limited write capabilities for game-specific data like characters.

129
database-service/SCHEMA.md Normal file
View File

@@ -0,0 +1,129 @@
# Database Schema
This document describes the database schema used by the Database Service.
## Tables
### User
Stores user account information.
| Column | Type | Description |
|------------|---------------------|-----------------------------------|
| id | INTEGER | Primary key |
| name | VARCHAR | Username |
| email | VARCHAR | Email address |
| role | VARCHAR | User role (e.g., admin, user) |
| createdAt | TIMESTAMP | Account creation timestamp |
| updatedAt | TIMESTAMP | Last update timestamp |
### Character
Stores character information.
| Column | Type | Description |
|------------|---------------------|-----------------------------------|
| id | INTEGER | Primary key |
| userId | INTEGER | Foreign key to User.id |
| name | VARCHAR | Character name |
| money | BIGINT | Character's currency |
| inventory | JSONB | Serialized inventory data |
| stats | JSONB | Character statistics |
| skills | JSONB | Character skills |
| looks | JSONB | Character appearance |
| position | JSONB | Character position in world |
| createdAt | TIMESTAMP | Character creation timestamp |
| updatedAt | TIMESTAMP | Last update timestamp |
| deletedAt | TIMESTAMP | Deletion timestamp (if deleted) |
| isActive | BOOLEAN | Whether character is active |
### Session
Stores session information.
| Column | Type | Description |
|------------|---------------------|-----------------------------------|
| id | VARCHAR | Session ID (primary key) |
| userId | INTEGER | Foreign key to User.id |
| createdAt | TIMESTAMP | Session creation timestamp |
| expiresAt | TIMESTAMP | Session expiration timestamp |
## JSON Structures
### Inventory
```json
{
"items": [
{
"id": 1,
"itemId": 1001,
"count": 5,
"slot": 0
}
],
"capacity": 100
}
```
### Stats
```json
{
"strength": 10,
"dexterity": 10,
"intelligence": 10,
"vitality": 10,
"hp": 100,
"mp": 100,
"level": 1,
"experience": 0
}
```
### Skills
```json
{
"skills": [
{
"id": 1,
"level": 1,
"cooldown": 0
}
]
}
```
### Looks
```json
{
"race": 1,
"gender": 0,
"hair": 1,
"face": 1,
"height": 180,
"skinColor": "#F5DEB3"
}
```
### Position
```json
{
"mapId": 1,
"x": 100.0,
"y": 100.0,
"z": 0.0,
"direction": 0
}
```
## Indexes
- `user_name_idx`: Index on User.name for fast username lookups
- `user_email_idx`: Index on User.email for fast email lookups
- `character_user_id_idx`: Index on Character.userId for fast character list retrieval
- `character_name_idx`: Index on Character.name for name uniqueness checks
- `session_user_id_idx`: Index on Session.userId for user session lookups

View File

@@ -102,19 +102,12 @@ impl CharacterRepository {
Ok(result.get("id"))
}
pub async fn delete_character(
&self,
character_id: i32,
delete_type: i32,
) -> Result<i64, sqlx::Error> {
pub async fn delete_character(&self, character_id: i32, delete_type: i32) -> Result<i64, sqlx::Error> {
let mut query = "UPDATE character SET \"updatedAt\" = NOW(), \"deletedAt\" = NOW() + '24 hours' WHERE id = $1 RETURNING \"userId\", extract(epoch from (\"deletedAt\" - now()))::BIGINT as deleted_at";
if 0 == delete_type {
query = "UPDATE character SET \"updatedAt\" = NOW(), \"deletedAt\" = null WHERE id = $1 RETURNING \"userId\", 0::BIGINT as deleted_at";
}
let result = sqlx::query(query)
.bind(character_id)
.fetch_one(&self.pool)
.await?;
let result = sqlx::query(query).bind(character_id).fetch_one(&self.pool).await?;
// Invalidate cache
let cache_key = format!("character:user:{}", result.get::<i32, &str>("user_id"));
@@ -134,10 +127,7 @@ impl CharacterRepository {
Ok(result.get::<i64, &str>("deleted_at"))
}
pub async fn get_characters_by_user(
&self,
user_id: String,
) -> Result<Vec<Character>, sqlx::Error> {
pub async fn get_characters_by_user(&self, user_id: String) -> Result<Vec<Character>, sqlx::Error> {
let cache_key = format!("character:user:{}", user_id);
// Try fetching from Redis cache

View File

@@ -1,10 +1,10 @@
use crate::characters::CharacterRepository;
use crate::sessions::SessionRepository;
use crate::users::UserRepository;
use sqlx::PgPool;
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::RedisCache;
use crate::sessions::SessionRepository;
pub struct Database {
pub user_repo: Arc<UserRepository>,
@@ -21,7 +21,7 @@ impl Database {
Self {
user_repo,
character_repo,
session_repo
session_repo,
}
}
}

View File

@@ -1,18 +1,14 @@
use crate::grpc::character_db_service_server::CharacterDbService;
use crate::grpc::database_service::MyDatabaseService;
use crate::grpc::{
Character, CharacterListRequest, CharacterListResponse, CharacterRequest,
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest,
DeleteCharacterResponse,
Character, CharacterListRequest, CharacterListResponse, CharacterRequest, CreateCharacterRequest,
CreateCharacterResponse, DeleteCharacterRequest, DeleteCharacterResponse,
};
use tonic::{Request, Response, Status};
#[tonic::async_trait]
impl CharacterDbService for MyDatabaseService {
async fn get_character(
&self,
request: Request<CharacterRequest>,
) -> Result<Response<Character>, Status> {
async fn get_character(&self, request: Request<CharacterRequest>) -> Result<Response<Character>, Status> {
let req = request.into_inner();
let repo = &self.db.character_repo;

View File

@@ -1,7 +1,7 @@
pub mod database_service;
mod character_service;
mod user_service;
pub mod database_service;
mod session_service;
mod user_service;
tonic::include_proto!("user_db_api");
tonic::include_proto!("character_db_api");

View File

@@ -1,21 +1,22 @@
use crate::grpc::database_service::MyDatabaseService;
use crate::grpc::session_service_server::SessionService;
use crate::grpc::{GetSessionRequest, GetSessionResponse, RefreshSessionRequest, RefreshSessionResponse};
use tonic::{Request, Response, Status};
use tracing::debug;
use crate::grpc::{GetSessionRequest, GetSessionResponse, RefreshSessionRequest, RefreshSessionResponse};
#[tonic::async_trait]
impl SessionService for MyDatabaseService {
async fn get_session(
&self,
request: Request<GetSessionRequest>,
) -> Result<Response<GetSessionResponse>, Status> {
async fn get_session(&self, request: Request<GetSessionRequest>) -> Result<Response<GetSessionResponse>, Status> {
let req = request.into_inner();
debug!("get_session: {:?}", req);
let session = self.db.session_repo.get_session(&req.session_id).await
let session = self
.db
.session_repo
.get_session(&req.session_id)
.await
.map_err(|_| Status::not_found("Session not found"))?;
debug!("session: {:?}", session);
Ok(Response::new(GetSessionResponse {
session_id: session.id,
@@ -23,18 +24,23 @@ impl SessionService for MyDatabaseService {
}))
}
async fn refresh_session(&self, request: Request<RefreshSessionRequest>) -> Result<Response<RefreshSessionResponse>, Status> {
async fn refresh_session(
&self,
request: Request<RefreshSessionRequest>,
) -> Result<Response<RefreshSessionResponse>, Status> {
let req = request.into_inner();
debug!("get_session: {:?}", req);
let session = self.db.session_repo.refresh_session(&req.session_id).await
let session = self
.db
.session_repo
.refresh_session(&req.session_id)
.await
.map_err(|_| Status::not_found("Session not found"))?;
let valid = true;
debug!("session: {:?}", session);
Ok(Response::new(RefreshSessionResponse {
valid
}))
Ok(Response::new(RefreshSessionResponse { valid }))
}
}

View File

@@ -1,17 +1,11 @@
use crate::grpc::database_service::MyDatabaseService;
use crate::grpc::user_service_server::UserService;
use crate::grpc::{
GetUserByEmailRequest, GetUserByUsernameRequest,
GetUserRequest, GetUserResponse,
};
use crate::grpc::{GetUserByEmailRequest, GetUserByUsernameRequest, GetUserRequest, GetUserResponse};
use tonic::{Request, Response, Status};
#[tonic::async_trait]
impl UserService for MyDatabaseService {
async fn get_user(
&self,
request: Request<GetUserRequest>,
) -> Result<Response<GetUserResponse>, Status> {
async fn get_user(&self, request: Request<GetUserRequest>) -> Result<Response<GetUserResponse>, Status> {
let req = request.into_inner();
let user = self

View File

@@ -1,5 +1,5 @@
pub mod characters;
pub mod db;
pub mod grpc;
pub mod users;
pub mod sessions;
pub mod users;

View File

@@ -1,6 +1,7 @@
use database_service::db::Database;
use database_service::grpc::character_db_service_server::CharacterDbServiceServer;
use database_service::grpc::database_service::MyDatabaseService;
use database_service::grpc::session_service_server::SessionServiceServer;
use database_service::grpc::user_service_server::UserServiceServer;
use dotenv::dotenv;
use sqlx::postgres::PgPoolOptions;
@@ -12,7 +13,6 @@ use tokio::sync::Mutex;
use tonic::transport::Server;
use tracing::{info, Level};
use tracing_subscriber::EnvFilter;
use database_service::grpc::session_service_server::SessionServiceServer;
use utils::logging;
use utils::redis_cache::RedisCache;
@@ -39,7 +39,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let my_service = MyDatabaseService { db };
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
health_reporter.set_serving::<UserServiceServer<MyDatabaseService>>().await;
health_reporter
.set_serving::<UserServiceServer<MyDatabaseService>>()
.await;
let address = SocketAddr::new(addr.parse()?, port.parse()?);
tokio::spawn(

View File

@@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Row};
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::{Cache, RedisCache};
use tracing::debug;
use utils::redis_cache::{Cache, RedisCache};
#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct Session {
@@ -24,25 +24,30 @@ impl SessionRepository {
pub async fn get_session(&self, session_id: &str) -> Result<Session, sqlx::Error> {
let cache_key = format!("session:{}", session_id);
if let Some(session) = self.cache.lock().await
.get::<Session>(&cache_key).await
if let Some(session) = self
.cache
.lock()
.await
.get::<Session>(&cache_key)
.await
.map_err(|_| sqlx::Error::RowNotFound)?
{
return Ok(session);
}
// Fetch from database
let session = sqlx::query_as::<_, Session>(
"SELECT id, \"userId\" as user_id FROM session WHERE id = $1",
)
let session = sqlx::query_as::<_, Session>("SELECT id, \"userId\" as user_id FROM session WHERE id = $1")
.bind(session_id)
.fetch_one(&self.pool)
.await?;
debug!("session: {:?}", session);
self.cache.lock().await
.set(&cache_key, &session, 300).await
self.cache
.lock()
.await
.set(&cache_key, &session, 300)
.await
.map_err(|_| sqlx::Error::RowNotFound)?;
Ok(session)
}
@@ -50,26 +55,34 @@ impl SessionRepository {
pub async fn refresh_session(&self, session_id: &str) -> Result<Session, sqlx::Error> {
let cache_key = format!("session:{}", session_id);
if let Some(session) = self.cache.lock().await
.get::<Session>(&cache_key).await
if let Some(session) = self
.cache
.lock()
.await
.get::<Session>(&cache_key)
.await
.map_err(|_| sqlx::Error::RowNotFound)?
{
self.cache.lock().await
.refresh(&cache_key, 300).await
self.cache
.lock()
.await
.refresh(&cache_key, 300)
.await
.map_err(|_| sqlx::Error::RowNotFound)?;
return Ok(session);
}
// Check to make sure the session is still valid
let session = sqlx::query_as::<_, Session>(
"SELECT id, \"userId\" as user_id FROM session WHERE id = $1",
)
let session = sqlx::query_as::<_, Session>("SELECT id, \"userId\" as user_id FROM session WHERE id = $1")
.bind(session_id)
.fetch_one(&self.pool)
.await?;
self.cache.lock().await
.set(&cache_key, &session, 300).await
self.cache
.lock()
.await
.set(&cache_key, &session, 300)
.await
.map_err(|_| sqlx::Error::RowNotFound)?;
Ok(session)
}

View File

@@ -0,0 +1,306 @@
#[cfg(test)]
mod tests {
use crate::characters::{Character, CharacterRepository};
use sqlx::{Pool, Postgres};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::RedisCache;
use serde_json::json;
// Helper function to create a test database pool
async fn create_test_pool() -> Pool<Postgres> {
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 create_mock_redis() -> 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: &Pool<Postgres>) -> i32 {
let result = sqlx::query!(
r#"
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
VALUES ('test_char_user', 'test_char@example.com', 'user', NOW(), NOW())
RETURNING id
"#
)
.fetch_one(pool)
.await
.expect("Failed to create test user");
result.id
}
// Helper function to create a test character in the database
async fn create_test_character(pool: &Pool<Postgres>, user_id: i32, name: &str) -> i32 {
let inventory = json!({
"items": [],
"capacity": 100
});
let stats = json!({
"strength": 10,
"dexterity": 10,
"intelligence": 10,
"vitality": 10
});
let skills = json!({
"skills": []
});
let looks = json!({
"race": 1,
"gender": 0,
"hair": 1,
"face": 1
});
let position = 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 clean up test data
async fn cleanup_test_data(pool: &Pool<Postgres>, user_id: i32, character_id: i32) {
sqlx::query!(r#"DELETE FROM character WHERE id = $1"#, character_id)
.execute(pool)
.await
.expect("Failed to delete test character");
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_character_by_id() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = CharacterRepository::new(pool.clone(), cache);
// Create test user and character
let user_id = create_test_user(&pool).await;
let character_id = create_test_character(&pool, user_id, "test_character").await;
// Test
let result = repo.get_character_by_id(character_id).await;
// Assert
assert!(result.is_ok());
let character = result.unwrap();
assert_eq!(character.id, character_id);
assert_eq!(character.name, "test_character");
assert_eq!(character.user_id, user_id.to_string());
// Cleanup
cleanup_test_data(&pool, user_id, character_id).await;
}
#[tokio::test]
async fn test_get_character_list() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = CharacterRepository::new(pool.clone(), cache);
// Create test user and characters
let user_id = create_test_user(&pool).await;
let character_id1 = create_test_character(&pool, user_id, "test_character1").await;
let character_id2 = create_test_character(&pool, user_id, "test_character2").await;
// Test
let result = repo.get_character_list(user_id.to_string()).await;
// Assert
assert!(result.is_ok());
let characters = result.unwrap();
assert_eq!(characters.len(), 2);
// Cleanup
cleanup_test_data(&pool, user_id, character_id1).await;
sqlx::query!(r#"DELETE FROM character WHERE id = $1"#, character_id2)
.execute(&pool)
.await
.expect("Failed to delete second test character");
}
#[tokio::test]
async fn test_create_character() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = CharacterRepository::new(pool.clone(), cache);
// Create test user
let user_id = create_test_user(&pool).await;
// Test
let inventory = json!({
"items": [],
"capacity": 100
});
let stats = json!({
"strength": 10,
"dexterity": 10,
"intelligence": 10,
"vitality": 10
});
let skills = json!({
"skills": []
});
let looks = json!({
"race": 1,
"gender": 0,
"hair": 1,
"face": 1
});
let position = json!({
"mapId": 1,
"x": 100.0,
"y": 100.0,
"z": 0.0
});
let result = repo.create_character(
user_id.to_string(),
"created_character",
inventory,
skills,
stats,
looks,
position
).await;
// Assert
assert!(result.is_ok());
let character_id = result.unwrap();
// Verify character was created
let character = repo.get_character_by_id(character_id).await.unwrap();
assert_eq!(character.name, "created_character");
// Cleanup
cleanup_test_data(&pool, user_id, character_id).await;
}
#[tokio::test]
async fn test_delete_character() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = CharacterRepository::new(pool.clone(), cache);
// Create test user and character
let user_id = create_test_user(&pool).await;
let character_id = create_test_character(&pool, user_id, "delete_test_character").await;
// Test
let result = repo.delete_character(character_id, 1).await;
// Assert
assert!(result.is_ok());
// Verify character was marked for deletion
let character = repo.get_character_by_id(character_id).await.unwrap();
assert!(character.deleted_at.is_some());
// Cleanup
cleanup_test_data(&pool, user_id, character_id).await;
}
#[tokio::test]
async fn test_get_nonexistent_character() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = CharacterRepository::new(pool.clone(), cache);
// Test
let result = repo.get_character_by_id(99999).await;
// Assert
assert!(result.is_err());
}
#[tokio::test]
async fn test_character_cache() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = CharacterRepository::new(pool.clone(), cache.clone());
// Create test user and character
let user_id = create_test_user(&pool).await;
let character_id = create_test_character(&pool, user_id, "cache_test_character").await;
// First call to populate cache
let _ = repo.get_character_by_id(character_id).await.unwrap();
// Delete from database to ensure we're getting from cache
sqlx::query!(r#"DELETE FROM character WHERE id = $1"#, character_id)
.execute(&pool)
.await
.expect("Failed to delete test character");
// Test - should still work because of cache
let result = repo.get_character_by_id(character_id).await;
// Assert
assert!(result.is_ok());
let character = result.unwrap();
assert_eq!(character.id, character_id);
assert_eq!(character.name, "cache_test_character");
// Cleanup
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
.execute(&pool)
.await
.expect("Failed to delete test user");
}
}

View File

@@ -0,0 +1,288 @@
#[cfg(test)]
mod tests {
use crate::db::Database;
use crate::grpc::character_db_service_server::{CharacterDbService, CharacterDbServiceServer};
use crate::grpc::database_service::MyDatabaseService;
use crate::grpc::user_service_server::{UserService, UserServiceServer};
use crate::grpc::session_service_server::{SessionService, SessionServiceServer};
use crate::grpc::{
Character, CharacterListRequest, CharacterRequest, CreateCharacterRequest,
DeleteCharacterRequest, GetSessionRequest, GetUserByEmailRequest, GetUserByUsernameRequest,
GetUserRequest, RefreshSessionRequest,
};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tokio::sync::Mutex;
use tonic::{Request, Response, Status};
use tonic::transport::{Channel, Server};
use utils::redis_cache::RedisCache;
use uuid::Uuid;
// Helper function to create a test database pool
async fn create_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 create_mock_redis() -> 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 ('grpc_test_user', 'grpc_test@example.com', 'user', NOW(), NOW())
RETURNING id
"#
)
.fetch_one(pool)
.await
.expect("Failed to create test user");
result.id
}
// Helper function to create a test character in the database
async fn create_test_character(pool: &sqlx::PgPool, user_id: i32) -> i32 {
let result = sqlx::query!(
r#"
INSERT INTO character (
"userId", name, money, inventory, stats, skills, looks, position,
"createdAt", "updatedAt", "isActive"
)
VALUES (
$1, 'grpc_test_character', 0,
'{"items":[]}'::jsonb,
'{"stats":{}}'::jsonb,
'{"skills":[]}'::jsonb,
'{"looks":{}}'::jsonb,
'{"position":{}}'::jsonb,
NOW(), NOW(), true
)
RETURNING id
"#,
user_id
)
.fetch_one(pool)
.await
.expect("Failed to create test character");
result.id
}
// Helper function to create a test session in the database
async fn create_test_session(pool: &sqlx::PgPool, user_id: i32) -> String {
let session_id = 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 data
async fn cleanup_test_data(pool: &sqlx::PgPool, user_id: i32, character_id: i32, session_id: &str) {
sqlx::query!(r#"DELETE FROM session WHERE id = $1"#, session_id)
.execute(pool)
.await
.expect("Failed to delete test session");
sqlx::query!(r#"DELETE FROM character WHERE id = $1"#, character_id)
.execute(pool)
.await
.expect("Failed to delete test character");
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
.execute(pool)
.await
.expect("Failed to delete test user");
}
// Helper function to start a test gRPC server
async fn start_test_server() -> (
String,
sqlx::PgPool,
i32,
i32,
String,
) {
// Create test database pool and Redis cache
let pool = create_test_pool().await;
let redis_cache = create_mock_redis();
// Create test data
let user_id = create_test_user(&pool).await;
let character_id = create_test_character(&pool, user_id).await;
let session_id = create_test_session(&pool, user_id).await;
// Create database service
let db = Arc::new(Database::new(pool.clone(), redis_cache));
let service = MyDatabaseService { db };
// Start gRPC server
let addr = "[::1]:0".parse().unwrap();
let user_service = UserServiceServer::new(service.clone());
let character_service = CharacterDbServiceServer::new(service.clone());
let session_service = SessionServiceServer::new(service);
let server = Server::builder()
.add_service(user_service)
.add_service(character_service)
.add_service(session_service)
.serve(addr);
let server_addr = server.local_addr();
tokio::spawn(server);
(
format!("http://{}", server_addr),
pool,
user_id,
character_id,
session_id,
)
}
#[tokio::test]
async fn test_user_service() {
// Start test server
let (server_addr, pool, user_id, character_id, session_id) = start_test_server().await;
// Create client
let channel = Channel::from_shared(server_addr.clone())
.unwrap()
.connect()
.await
.unwrap();
let mut client = crate::grpc::user_service_client::UserServiceClient::new(channel);
// Test GetUser
let request = Request::new(GetUserRequest { user_id });
let response = client.get_user(request).await.unwrap();
let user = response.into_inner();
assert_eq!(user.user_id, user_id);
assert_eq!(user.username, "grpc_test_user");
assert_eq!(user.email, "grpc_test@example.com");
// Test GetUserByUsername
let request = Request::new(GetUserByUsernameRequest {
username: "grpc_test_user".to_string(),
});
let response = client.get_user_by_username(request).await.unwrap();
let user = response.into_inner();
assert_eq!(user.user_id, user_id);
// Test GetUserByEmail
let request = Request::new(GetUserByEmailRequest {
email: "grpc_test@example.com".to_string(),
});
let response = client.get_user_by_email(request).await.unwrap();
let user = response.into_inner();
assert_eq!(user.user_id, user_id);
// Cleanup
cleanup_test_data(&pool, user_id, character_id, &session_id).await;
}
#[tokio::test]
async fn test_character_service() {
// Start test server
let (server_addr, pool, user_id, character_id, session_id) = start_test_server().await;
// Create client
let channel = Channel::from_shared(server_addr.clone())
.unwrap()
.connect()
.await
.unwrap();
let mut client = crate::grpc::character_db_service_client::CharacterDbServiceClient::new(channel);
// Test GetCharacter
let request = Request::new(CharacterRequest {
user_id: user_id.to_string(),
character_id,
});
let response = client.get_character(request).await.unwrap();
let character = response.into_inner();
assert_eq!(character.id, character_id);
assert_eq!(character.name, "grpc_test_character");
// Test GetCharacterList
let request = Request::new(CharacterListRequest {
user_id: user_id.to_string(),
});
let response = client.get_character_list(request).await.unwrap();
let character_list = response.into_inner();
assert_eq!(character_list.characters.len(), 1);
assert_eq!(character_list.characters[0].id, character_id);
// Cleanup
cleanup_test_data(&pool, user_id, character_id, &session_id).await;
}
#[tokio::test]
async fn test_session_service() {
// Start test server
let (server_addr, pool, user_id, character_id, session_id) = start_test_server().await;
// Create client
let channel = Channel::from_shared(server_addr.clone())
.unwrap()
.connect()
.await
.unwrap();
let mut client = crate::grpc::session_service_client::SessionServiceClient::new(channel);
// Test GetSession
let request = Request::new(GetSessionRequest {
session_id: session_id.clone(),
});
let response = client.get_session(request).await.unwrap();
let session = response.into_inner();
assert_eq!(session.session_id, session_id);
assert_eq!(session.user_id, user_id.to_string());
// Test RefreshSession
let request = Request::new(RefreshSessionRequest {
session_id: session_id.clone(),
});
let response = client.refresh_session(request).await.unwrap();
let session = response.into_inner();
assert_eq!(session.session_id, session_id);
assert_eq!(session.user_id, user_id.to_string());
// Cleanup
cleanup_test_data(&pool, user_id, character_id, &session_id).await;
}
}

View File

@@ -0,0 +1,4 @@
pub mod user_repository_tests;
pub mod character_repository_tests;
pub mod session_repository_tests;
pub mod grpc_tests;

View File

@@ -0,0 +1,176 @@
#[cfg(test)]
mod tests {
use crate::sessions::{Session, SessionRepository};
use sqlx::{Pool, Postgres};
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::RedisCache;
use uuid::Uuid;
// Helper function to create a test database pool
async fn create_test_pool() -> Pool<Postgres> {
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 create_mock_redis() -> 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: &Pool<Postgres>) -> i32 {
let result = sqlx::query!(
r#"
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
VALUES ('test_session_user', 'test_session@example.com', 'user', NOW(), NOW())
RETURNING id
"#
)
.fetch_one(pool)
.await
.expect("Failed to create test user");
result.id
}
// Helper function to create a test session in the database
async fn create_test_session(pool: &Pool<Postgres>, user_id: i32) -> String {
let session_id = 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 data
async fn cleanup_test_data(pool: &Pool<Postgres>, user_id: i32, session_id: &str) {
sqlx::query!(r#"DELETE FROM session WHERE id = $1"#, session_id)
.execute(pool)
.await
.expect("Failed to delete test session");
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_session() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = SessionRepository::new(pool.clone(), cache);
// Create test user and session
let user_id = create_test_user(&pool).await;
let session_id = create_test_session(&pool, user_id).await;
// Test
let result = repo.get_session(&session_id).await;
// Assert
assert!(result.is_ok());
let session = result.unwrap();
assert_eq!(session.id, session_id);
assert_eq!(session.user_id, user_id.to_string());
// Cleanup
cleanup_test_data(&pool, user_id, &session_id).await;
}
#[tokio::test]
async fn test_refresh_session() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = SessionRepository::new(pool.clone(), cache);
// Create test user and session
let user_id = create_test_user(&pool).await;
let session_id = create_test_session(&pool, user_id).await;
// Test
let result = repo.refresh_session(&session_id).await;
// Assert
assert!(result.is_ok());
let session = result.unwrap();
assert_eq!(session.id, session_id);
assert_eq!(session.user_id, user_id.to_string());
// Cleanup
cleanup_test_data(&pool, user_id, &session_id).await;
}
#[tokio::test]
async fn test_get_nonexistent_session() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = SessionRepository::new(pool.clone(), cache);
// Test
let result = repo.get_session("nonexistent-session-id").await;
// Assert
assert!(result.is_err());
}
#[tokio::test]
async fn test_session_cache() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = SessionRepository::new(pool.clone(), cache.clone());
// Create test user and session
let user_id = create_test_user(&pool).await;
let session_id = create_test_session(&pool, user_id).await;
// First call to populate cache
let _ = repo.get_session(&session_id).await.unwrap();
// Delete from database to ensure we're getting from cache
sqlx::query!(r#"DELETE FROM session WHERE id = $1"#, session_id)
.execute(&pool)
.await
.expect("Failed to delete test session");
// Test - should still work because of cache
let result = repo.get_session(&session_id).await;
// Assert
assert!(result.is_ok());
let session = result.unwrap();
assert_eq!(session.id, session_id);
assert_eq!(session.user_id, user_id.to_string());
// Cleanup
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
.execute(&pool)
.await
.expect("Failed to delete test user");
}
}

View File

@@ -0,0 +1,169 @@
#[cfg(test)]
mod tests {
use crate::users::{User, UserRepository};
use chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
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 create_test_pool() -> Pool<Postgres> {
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 create_mock_redis() -> 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: &Pool<Postgres>, 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 clean up test data
async fn cleanup_test_user(pool: &Pool<Postgres>, 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_by_id() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = UserRepository::new(pool.clone(), cache);
// Create test user
let user_id = create_test_user(&pool, "test_user", "test@example.com").await;
// Test
let result = repo.get_user_by_id(user_id).await;
// Assert
assert!(result.is_ok());
let user = result.unwrap();
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;
}
#[tokio::test]
async fn test_get_user_by_username() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = UserRepository::new(pool.clone(), cache);
// Create test user
let user_id = create_test_user(&pool, "test_user_by_name", "test_name@example.com").await;
// Test
let result = repo.get_user_by_username("test_user_by_name").await;
// Assert
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.id, user_id);
assert_eq!(user.name, "test_user_by_name");
assert_eq!(user.email, "test_name@example.com");
// Cleanup
cleanup_test_user(&pool, user_id).await;
}
#[tokio::test]
async fn test_get_user_by_email() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = UserRepository::new(pool.clone(), cache);
// Create test user
let user_id = create_test_user(&pool, "test_user_by_email", "test_email@example.com").await;
// Test
let result = repo.get_user_by_email("test_email@example.com").await;
// Assert
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.id, user_id);
assert_eq!(user.name, "test_user_by_email");
assert_eq!(user.email, "test_email@example.com");
// Cleanup
cleanup_test_user(&pool, user_id).await;
}
#[tokio::test]
async fn test_get_nonexistent_user() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = UserRepository::new(pool.clone(), cache);
// Test
let result = repo.get_user_by_id(99999).await;
// Assert
assert!(result.is_err());
}
#[tokio::test]
async fn test_cache_hit() {
// Setup
let pool = create_test_pool().await;
let cache = create_mock_redis();
let repo = UserRepository::new(pool.clone(), cache.clone());
// Create test user
let user_id = create_test_user(&pool, "cache_test_user", "cache_test@example.com").await;
// First call to populate cache
let _ = repo.get_user_by_id(user_id).await.unwrap();
// Delete from database to ensure we're getting from cache
cleanup_test_user(&pool, user_id).await;
// Test - should still work because of cache
let result = repo.get_user_by_id(user_id).await;
// Assert
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.id, user_id);
assert_eq!(user.name, "cache_test_user");
}
}

View File

@@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use sqlx::{FromRow};
use sqlx::FromRow;
use std::sync::Arc;
use tokio::sync::Mutex;
use utils::redis_cache::{Cache, RedisCache};
@@ -38,12 +38,11 @@ impl UserRepository {
return Ok(user);
}
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE id = $1",
)
.bind(user_id)
.fetch_one(&self.pool)
.await?;
let user =
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE id = $1")
.bind(user_id)
.fetch_one(&self.pool)
.await?;
self.cache
.lock()
@@ -68,12 +67,11 @@ impl UserRepository {
return Ok(user);
}
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE name = $1",
)
.bind(username)
.fetch_one(&self.pool)
.await?;
let user =
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE name = $1")
.bind(username)
.fetch_one(&self.pool)
.await?;
self.cache
.lock()
@@ -98,12 +96,11 @@ impl UserRepository {
return Ok(user);
}
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1",
)
.bind(email)
.fetch_one(&self.pool)
.await?;
let user =
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1")
.bind(email)
.fetch_one(&self.pool)
.await?;
self.cache
.lock()
@@ -128,12 +125,11 @@ impl UserRepository {
return Ok(user);
}
let user = sqlx::query_as::<_, User>(
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1",
)
.bind(session)
.fetch_one(&self.pool)
.await?;
let user =
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1")
.bind(session)
.fetch_one(&self.pool)
.await?;
self.cache
.lock()

View File

@@ -1,29 +0,0 @@
use tokio;
#[tokio::test]
async fn test_get_user() {
// // Set up a temporary in-memory PostgreSQL database
// let pool = PgPool::connect("postgres://user:password@localhost/test_database").await.unwrap();
//
// // Create the test table
// pool.execute(
// r#"
// CREATE TABLE users (
// user_id TEXT PRIMARY KEY,
// username TEXT NOT NULL,
// email TEXT NOT NULL,
// hashed_password TEXT NOT NULL
// );
// INSERT INTO users (user_id, username, email, hashed_password)
// VALUES ('123', 'test_user', 'test@example.com', 'hashed_password_example');
// "#,
// )
// .await
// .unwrap();
//
// // Test the `get_user` function
// let user = get_user(&pool, "123").await.unwrap();
// assert_eq!(user.user_id, "123");
// assert_eq!(user.username, "test_user");
// assert_eq!(user.email, "test@example.com");
}

View File

@@ -1,20 +0,0 @@
#[tokio::test]
async fn test_grpc_get_user() {
// let pool = setup_test_pool().await; // Set up your test pool
// let cache = setup_test_cache().await; // Set up mock Redis cache
//
// let service = MyDatabaseService { pool, cache };
//
// // Create a mock gRPC request
// let request = Request::new(GetUserRequest {
// user_id: 123,
// });
//
// // Call the service
// let response = service.get_user(request).await.unwrap().into_inner();
//
// // Validate the response
// assert_eq!(response.user_id, 123);
// assert_eq!(response.username, "test_user");
// assert_eq!(response.email, "test@example.com");
}

View File

@@ -1,12 +0,0 @@
#[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

@@ -1,16 +0,0 @@
#[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()));
}