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:
229
database-service/API.md
Normal file
229
database-service/API.md
Normal 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
111
database-service/README.md
Normal 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
129
database-service/SCHEMA.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod characters;
|
||||
pub mod db;
|
||||
pub mod grpc;
|
||||
pub mod users;
|
||||
pub mod sessions;
|
||||
pub mod users;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
306
database-service/src/tests/character_repository_tests.rs
Normal file
306
database-service/src/tests/character_repository_tests.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
288
database-service/src/tests/grpc_tests.rs
Normal file
288
database-service/src/tests/grpc_tests.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
4
database-service/src/tests/mod.rs
Normal file
4
database-service/src/tests/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod user_repository_tests;
|
||||
pub mod character_repository_tests;
|
||||
pub mod session_repository_tests;
|
||||
pub mod grpc_tests;
|
||||
176
database-service/src/tests/session_repository_tests.rs
Normal file
176
database-service/src/tests/session_repository_tests.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
169
database-service/src/tests/user_repository_tests.rs
Normal file
169
database-service/src/tests/user_repository_tests.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
Reference in New Issue
Block a user