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:
11
Cargo.toml
11
Cargo.toml
@@ -1,4 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"auth-service",
|
"auth-service",
|
||||||
"character-service",
|
"character-service",
|
||||||
@@ -6,6 +7,12 @@ members = [
|
|||||||
"packet-service",
|
"packet-service",
|
||||||
"world-service",
|
"world-service",
|
||||||
"utils",
|
"utils",
|
||||||
"launcher"
|
"launcher",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# Include tests directory only when testing
|
||||||
|
[workspace.metadata.test]
|
||||||
|
members = ["tests"]
|
||||||
|
|||||||
111
README.md
111
README.md
@@ -1,2 +1,111 @@
|
|||||||
# osirose-new
|
# MMORPG Server Architecture
|
||||||
|
|
||||||
|
A microservice-based server architecture for an MMORPG game, built with Rust.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project implements a complete server infrastructure for an MMORPG game, using a microservice architecture for scalability and maintainability. Each service is responsible for a specific domain of the game, communicating with other services via gRPC.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The server architecture consists of the following microservices:
|
||||||
|
|
||||||
|
### Core Services
|
||||||
|
|
||||||
|
- **Auth Service**: Handles user authentication, session validation, and account management
|
||||||
|
- **Character Service**: Manages character creation, deletion, and retrieval
|
||||||
|
- **Database Service**: Provides centralized database access for all services
|
||||||
|
- **Packet Service**: Handles game client communication via custom binary packets
|
||||||
|
- **World Service**: Manages game world state and character interactions
|
||||||
|
|
||||||
|
### Support Components
|
||||||
|
|
||||||
|
- **Utils**: Shared utilities used by all services
|
||||||
|
- **Launcher**: Client-side launcher for the game
|
||||||
|
|
||||||
|
## Communication Flow
|
||||||
|
|
||||||
|
1. **Client → Launcher**: User launches the game via the launcher
|
||||||
|
2. **Launcher → Packet Service**: Game client connects to the packet service
|
||||||
|
3. **Packet Service → Auth Service**: Validates user session
|
||||||
|
4. **Packet Service → Character Service**: Retrieves character data
|
||||||
|
5. **Packet Service → World Service**: Manages game world interactions
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
- **Language**: Rust
|
||||||
|
- **Communication**: gRPC, custom binary protocol
|
||||||
|
- **Database**: PostgreSQL (managed by external system)
|
||||||
|
- **Caching**: Redis
|
||||||
|
- **Service Discovery**: Kubernetes DNS, Consul
|
||||||
|
- **Metrics**: Prometheus
|
||||||
|
|
||||||
|
## External Dependencies
|
||||||
|
|
||||||
|
- **Database Schema**: Managed by an external web application using better-auth
|
||||||
|
- **User Management**: Handled by the external web application
|
||||||
|
- **Session Creation**: Initial sessions created by the external web application
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Rust (latest stable)
|
||||||
|
- PostgreSQL
|
||||||
|
- Redis
|
||||||
|
- Protobuf compiler
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running
|
||||||
|
|
||||||
|
Each service can be run individually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auth Service
|
||||||
|
cd auth-service && cargo run
|
||||||
|
|
||||||
|
# Character Service
|
||||||
|
cd character-service && cargo run
|
||||||
|
|
||||||
|
# Database Service
|
||||||
|
cd database-service && cargo run
|
||||||
|
|
||||||
|
# Packet Service
|
||||||
|
cd packet-service && cargo run
|
||||||
|
|
||||||
|
# World Service
|
||||||
|
cd world-service && cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Each service includes a Dockerfile for containerized deployment.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Each service includes its own README.md with detailed documentation:
|
||||||
|
|
||||||
|
- [Auth Service](auth-service/README.md)
|
||||||
|
- [Character Service](character-service/README.md)
|
||||||
|
- [Database Service](database-service/README.md)
|
||||||
|
- [Packet Service](packet-service/README.md)
|
||||||
|
- [World Service](world-service/README.md)
|
||||||
|
- [Utils](utils/README.md)
|
||||||
|
- [Launcher](launcher/README.md)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run tests for all services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --all
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|||||||
94
auth-service/README.md
Normal file
94
auth-service/README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Authentication Service
|
||||||
|
|
||||||
|
The Authentication Service is responsible for user authentication, session validation, and account management in the MMORPG server architecture.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Authentication Service provides gRPC endpoints for:
|
||||||
|
- User login and logout
|
||||||
|
- Session validation and refresh
|
||||||
|
|
||||||
|
It communicates with the external database service to verify user credentials and manage sessions.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The service is built using the following components:
|
||||||
|
|
||||||
|
- **gRPC Server**: Exposes authentication endpoints
|
||||||
|
- **Database Client**: Communicates with the database service for user data
|
||||||
|
- **Session Client**: Manages user sessions
|
||||||
|
- **Password Hashing**: Securely handles password verification
|
||||||
|
|
||||||
|
> **Note**: User registration and password reset functionality are handled by an external system.
|
||||||
|
|
||||||
|
## Service Endpoints
|
||||||
|
|
||||||
|
The Authentication Service exposes the following gRPC endpoints:
|
||||||
|
|
||||||
|
### Login
|
||||||
|
Authenticates a user and creates a new session.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logout
|
||||||
|
Terminates a user session.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc Logout(LogoutRequest) returns (Empty);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidateToken
|
||||||
|
Validates a JWT token.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ValidateSession
|
||||||
|
Validates a session ID.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### RefreshSession
|
||||||
|
Refreshes an existing session.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc RefreshSession(ValidateSessionRequest) returns (RefreshSessionResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The service can be configured using environment variables:
|
||||||
|
|
||||||
|
- `LISTEN_ADDR`: The address to listen on (default: "0.0.0.0")
|
||||||
|
- `SERVICE_PORT`: The port to listen on (default: "50051")
|
||||||
|
- `LOG_LEVEL`: Logging level (default: "info")
|
||||||
|
|
||||||
|
## Running the Service
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t auth-service .
|
||||||
|
docker run -p 50051:50051 auth-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with External Systems
|
||||||
|
|
||||||
|
The Authentication Service integrates with:
|
||||||
|
|
||||||
|
- **Database Service**: For user data storage and retrieval
|
||||||
|
- **Session Service**: For session management
|
||||||
|
- **External Auth System**: The service is designed to work with an external authentication system (better-auth) that manages the database schema and user accounts
|
||||||
@@ -12,10 +12,7 @@ fn main() {
|
|||||||
.build_server(false) // Generate gRPC client code
|
.build_server(false) // Generate gRPC client code
|
||||||
.compile_well_known_types(true)
|
.compile_well_known_types(true)
|
||||||
.compile_protos(
|
.compile_protos(
|
||||||
&[
|
&["../proto/user_db_api.proto", "../proto/session_db_api.proto"],
|
||||||
"../proto/user_db_api.proto",
|
|
||||||
"../proto/session_db_api.proto",
|
|
||||||
],
|
|
||||||
&["../proto"],
|
&["../proto"],
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||||
|
|||||||
@@ -1,56 +1,23 @@
|
|||||||
use crate::database::{user_service_client::UserServiceClient, GetUserByEmailRequest, GetUserByUsernameRequest, GetUserRequest, GetUserResponse};
|
use crate::database::{
|
||||||
|
user_service_client::UserServiceClient, GetUserByEmailRequest, GetUserByUsernameRequest, GetUserRequest,
|
||||||
|
GetUserResponse,
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait DatabaseClientTrait: Sized {
|
pub trait DatabaseClientTrait: Sized {
|
||||||
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>>;
|
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>>;
|
||||||
async fn get_user_by_userid(
|
async fn get_user_by_userid(&mut self, user_id: i32) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
||||||
&mut self,
|
async fn get_user_by_username(&mut self, user_id: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
||||||
user_id: i32,
|
async fn get_user_by_email(&mut self, email: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
||||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
|
||||||
async fn get_user_by_username(
|
|
||||||
&mut self,
|
|
||||||
user_id: &str,
|
|
||||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
|
||||||
async fn get_user_by_email(
|
|
||||||
&mut self,
|
|
||||||
email: &str,
|
|
||||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
|
||||||
async fn store_password_reset(
|
|
||||||
&mut self,
|
|
||||||
email: &str,
|
|
||||||
reset_token: &str,
|
|
||||||
expires_at: DateTime<Utc>,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
|
||||||
async fn get_password_reset(
|
|
||||||
&self,
|
|
||||||
reset_token: &str,
|
|
||||||
) -> Result<Option<PasswordReset>, Box<dyn std::error::Error>>;
|
|
||||||
async fn delete_password_reset(
|
|
||||||
&self,
|
|
||||||
reset_token: &str,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
|
||||||
async fn update_user_password(
|
|
||||||
&self,
|
|
||||||
email: &str,
|
|
||||||
hashed_password: &str,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DatabaseClient {
|
pub struct DatabaseClient {
|
||||||
client: UserServiceClient<Channel>,
|
client: UserServiceClient<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PasswordReset {
|
|
||||||
pub email: String,
|
|
||||||
pub reset_token: String,
|
|
||||||
pub expires_at: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl DatabaseClientTrait for DatabaseClient {
|
impl DatabaseClientTrait for DatabaseClient {
|
||||||
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
@@ -58,19 +25,13 @@ impl DatabaseClientTrait for DatabaseClient {
|
|||||||
Ok(Self { client })
|
Ok(Self { client })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_by_userid(
|
async fn get_user_by_userid(&mut self, user_id: i32) -> Result<GetUserResponse, Box<dyn std::error::Error>> {
|
||||||
&mut self,
|
|
||||||
user_id: i32,
|
|
||||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>> {
|
|
||||||
let request = tonic::Request::new(GetUserRequest { user_id });
|
let request = tonic::Request::new(GetUserRequest { user_id });
|
||||||
let response = self.client.get_user(request).await?;
|
let response = self.client.get_user(request).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_by_username(
|
async fn get_user_by_username(&mut self, username: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>> {
|
||||||
&mut self,
|
|
||||||
username: &str,
|
|
||||||
) -> Result<GetUserResponse, Box<dyn std::error::Error>> {
|
|
||||||
let request = tonic::Request::new(GetUserByUsernameRequest {
|
let request = tonic::Request::new(GetUserByUsernameRequest {
|
||||||
username: username.to_string(),
|
username: username.to_string(),
|
||||||
});
|
});
|
||||||
@@ -85,35 +46,4 @@ impl DatabaseClientTrait for DatabaseClient {
|
|||||||
let response = self.client.get_user_by_email(request).await?;
|
let response = self.client.get_user_by_email(request).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn store_password_reset(
|
|
||||||
&mut self,
|
|
||||||
_email: &str,
|
|
||||||
_reset_token: &str,
|
|
||||||
_expires_at: DateTime<Utc>,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_password_reset(
|
|
||||||
&self,
|
|
||||||
_reset_token: &str,
|
|
||||||
) -> Result<Option<PasswordReset>, Box<dyn std::error::Error>> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_password_reset(
|
|
||||||
&self,
|
|
||||||
_reset_token: &str,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_user_password(
|
|
||||||
&self,
|
|
||||||
_email: &str,
|
|
||||||
_hashed_password: &str,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
use crate::auth::auth_service_server::AuthService;
|
use crate::auth::auth_service_server::AuthService;
|
||||||
use crate::auth::{
|
use crate::auth::{
|
||||||
LoginRequest, LoginResponse, LogoutRequest, PasswordResetRequest, PasswordResetResponse,
|
LoginRequest, LoginResponse, LogoutRequest, RefreshSessionResponse, ValidateSessionRequest,
|
||||||
RefreshSessionResponse, RegisterRequest, RegisterResponse, ResetPasswordRequest,
|
ValidateSessionResponse, ValidateTokenRequest, ValidateTokenResponse,
|
||||||
ResetPasswordResponse, ValidateSessionRequest, ValidateSessionResponse, ValidateTokenRequest, ValidateTokenResponse,
|
|
||||||
};
|
};
|
||||||
use crate::common::Empty;
|
use crate::common::Empty;
|
||||||
use crate::database_client::{DatabaseClient, DatabaseClientTrait};
|
use crate::database_client::{DatabaseClient};
|
||||||
use crate::session::session_service_client::SessionServiceClient;
|
use crate::session::session_service_client::SessionServiceClient;
|
||||||
use crate::session::{GetSessionRequest, RefreshSessionRequest};
|
use crate::session::{GetSessionRequest, RefreshSessionRequest};
|
||||||
use crate::users::{hash_password, verify_user};
|
|
||||||
use chrono::{Duration, Utc};
|
|
||||||
use rand::Rng;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tonic::{Request, Response, Status};
|
use tonic::{Request, Response, Status};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
@@ -22,22 +18,19 @@ pub struct MyAuthService {
|
|||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl AuthService for MyAuthService {
|
impl AuthService for MyAuthService {
|
||||||
async fn login(
|
async fn login(&self, _request: Request<LoginRequest>) -> Result<Response<LoginResponse>, Status> {
|
||||||
&self,
|
|
||||||
request: Request<LoginRequest>,
|
|
||||||
) -> Result<Response<LoginResponse>, Status> {
|
|
||||||
Err(Status::unimplemented("login not implemented due to changes"))
|
Err(Status::unimplemented("login not implemented due to changes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn logout(&self, request: Request<LogoutRequest>) -> Result<Response<Empty>, Status> {
|
async fn logout(&self, request: Request<LogoutRequest>) -> Result<Response<Empty>, Status> {
|
||||||
let req = request.into_inner();
|
let _req = request.into_inner();
|
||||||
|
|
||||||
Ok(Response::new(Empty {}))
|
Ok(Response::new(Empty {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn validate_token(
|
async fn validate_token(
|
||||||
&self,
|
&self,
|
||||||
request: Request<ValidateTokenRequest>,
|
_request: Request<ValidateTokenRequest>,
|
||||||
) -> Result<Response<ValidateTokenResponse>, Status> {
|
) -> Result<Response<ValidateTokenResponse>, Status> {
|
||||||
Ok(Response::new(ValidateTokenResponse {
|
Ok(Response::new(ValidateTokenResponse {
|
||||||
valid: false,
|
valid: false,
|
||||||
@@ -51,20 +44,32 @@ impl AuthService for MyAuthService {
|
|||||||
request: Request<ValidateSessionRequest>,
|
request: Request<ValidateSessionRequest>,
|
||||||
) -> Result<Response<ValidateSessionResponse>, Status> {
|
) -> Result<Response<ValidateSessionResponse>, Status> {
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
let response = self.session_client.as_ref().clone()
|
let response = self
|
||||||
|
.session_client
|
||||||
|
.as_ref()
|
||||||
|
.clone()
|
||||||
.get_session(GetSessionRequest {
|
.get_session(GetSessionRequest {
|
||||||
session_id: req.session_id,
|
session_id: req.session_id,
|
||||||
}).await;
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let res = res.into_inner();
|
let res = res.into_inner();
|
||||||
debug!("Session valid: {:?}", res);
|
debug!("Session valid: {:?}", res);
|
||||||
Ok(Response::new(ValidateSessionResponse { valid: true, session_id: res.session_id.to_string(), user_id: res.user_id.to_string() }))
|
Ok(Response::new(ValidateSessionResponse {
|
||||||
|
valid: true,
|
||||||
|
session_id: res.session_id.to_string(),
|
||||||
|
user_id: res.user_id.to_string(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
debug!("Session invalid or not found: {error}");
|
debug!("Session invalid or not found: {error}");
|
||||||
Ok(Response::new(ValidateSessionResponse { valid: false, session_id: "".to_string(), user_id: "".to_string() }))
|
Ok(Response::new(ValidateSessionResponse {
|
||||||
|
valid: false,
|
||||||
|
session_id: "".to_string(),
|
||||||
|
user_id: "".to_string(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,126 +100,4 @@ impl AuthService for MyAuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register(
|
|
||||||
&self,
|
|
||||||
request: Request<RegisterRequest>,
|
|
||||||
) -> Result<Response<RegisterResponse>, Status> {
|
|
||||||
// let req = request.into_inner();
|
|
||||||
//
|
|
||||||
// // Hash the password
|
|
||||||
// let hashed_password = hash_password(&req.password);
|
|
||||||
//
|
|
||||||
// // Create user in the database
|
|
||||||
// let result = self
|
|
||||||
// .db_client
|
|
||||||
// .as_ref()
|
|
||||||
// .clone()
|
|
||||||
// .create_user(&req.username, &req.email, &hashed_password)
|
|
||||||
// .await;
|
|
||||||
//
|
|
||||||
// match result {
|
|
||||||
// Ok(user) => Ok(Response::new(RegisterResponse {
|
|
||||||
// user_id: user.user_id,
|
|
||||||
// message: "User registered successfully".into(),
|
|
||||||
// })),
|
|
||||||
// Err(e) => {
|
|
||||||
// error!("Failed to register user: {:?}", e);
|
|
||||||
// Err(Status::internal("Failed to register user"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
Err(Status::unimplemented("register not implemented"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request_password_reset(
|
|
||||||
&self,
|
|
||||||
request: Request<PasswordResetRequest>,
|
|
||||||
) -> Result<Response<PasswordResetResponse>, Status> {
|
|
||||||
let email = request.into_inner().email;
|
|
||||||
|
|
||||||
let user = self
|
|
||||||
.db_client
|
|
||||||
.as_ref()
|
|
||||||
.clone()
|
|
||||||
.get_user_by_email(&email)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Check if the email exists
|
|
||||||
if user.ok().is_some() {
|
|
||||||
// Generate a reset token
|
|
||||||
let reset_token: String = rand::thread_rng()
|
|
||||||
.sample_iter(&rand::distributions::Alphanumeric)
|
|
||||||
.take(32)
|
|
||||||
.map(char::from)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Set token expiration (e.g., 1 hour)
|
|
||||||
let expires_at = Utc::now() + Duration::hours(1);
|
|
||||||
|
|
||||||
// Store the reset token in the database
|
|
||||||
self.db_client
|
|
||||||
.as_ref()
|
|
||||||
.clone()
|
|
||||||
.store_password_reset(&email, &reset_token, expires_at)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
|
|
||||||
|
|
||||||
// Send the reset email
|
|
||||||
// send_email(&email, "Password Reset Request", &format!(
|
|
||||||
// "Click the link to reset your password: https://azgstudio.com/reset?token={}",
|
|
||||||
// reset_token
|
|
||||||
// ))
|
|
||||||
// .map_err(|e| Status::internal(format!("Email error: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(Response::new(PasswordResetResponse {
|
|
||||||
message: "Password reset email sent".to_string(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
// Respond with a generic message to avoid information leaks
|
|
||||||
Ok(Response::new(PasswordResetResponse {
|
|
||||||
message: "If the email exists, a reset link has been sent.".to_string(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reset_password(
|
|
||||||
&self,
|
|
||||||
request: Request<ResetPasswordRequest>,
|
|
||||||
) -> Result<Response<ResetPasswordResponse>, Status> {
|
|
||||||
let req = request.into_inner();
|
|
||||||
|
|
||||||
// Validate the reset token
|
|
||||||
if let Some(password_reset) = self
|
|
||||||
.db_client
|
|
||||||
.clone()
|
|
||||||
.get_password_reset(&req.reset_token)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
|
|
||||||
{
|
|
||||||
if password_reset.expires_at < Utc::now() {
|
|
||||||
return Err(Status::unauthenticated("Token expired"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the new password
|
|
||||||
let hashed_password = hash_password(&req.new_password);
|
|
||||||
|
|
||||||
// Update the user's password
|
|
||||||
self.db_client
|
|
||||||
.update_user_password(&password_reset.email, &hashed_password)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
|
|
||||||
|
|
||||||
// Delete the reset token
|
|
||||||
self.db_client
|
|
||||||
.delete_password_reset(&req.reset_token)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Status::internal(format!("Database error: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(Response::new(ResetPasswordResponse {
|
|
||||||
message: "Password successfully reset".to_string(),
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Err(Status::unauthenticated("Invalid reset token"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ use std::env;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tonic::transport::Server;
|
use tonic::transport::Server;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_subscriber::{fmt, EnvFilter};
|
|
||||||
use utils::logging;
|
use utils::logging;
|
||||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns};
|
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -23,7 +22,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Set the gRPC server address
|
// Set the gRPC server address
|
||||||
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50051".to_string());
|
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50051".to_string());
|
||||||
let db_url = format!("http://{}",get_kube_service_endpoints_by_dns("database-service","tcp","database-service").await?.get(0).unwrap());
|
let db_url = format!(
|
||||||
|
"http://{}",
|
||||||
|
get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service")
|
||||||
|
.await?
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
let db_client = Arc::new(DatabaseClient::connect(&db_url).await?);
|
let db_client = Arc::new(DatabaseClient::connect(&db_url).await?);
|
||||||
let session_client = Arc::new(SessionServiceClient::connect(db_url).await?);
|
let session_client = Arc::new(SessionServiceClient::connect(db_url).await?);
|
||||||
@@ -32,7 +37,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let address = full_addr.parse().expect("Invalid address");
|
let address = full_addr.parse().expect("Invalid address");
|
||||||
let auth_service = MyAuthService {
|
let auth_service = MyAuthService {
|
||||||
db_client,
|
db_client,
|
||||||
session_client
|
session_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::database::GetUserResponse;
|
use crate::database::GetUserResponse;
|
||||||
use crate::database_client::{DatabaseClientTrait, PasswordReset};
|
use crate::database_client::DatabaseClientTrait;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use mockall::{mock, predicate::*};
|
use mockall::{mock, predicate::*};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
@@ -15,10 +14,6 @@ mock! {
|
|||||||
async fn get_user_by_userid(&mut self, user_id: i32) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
async fn get_user_by_userid(&mut self, user_id: i32) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
||||||
async fn get_user_by_username(&mut self, user_id: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
async fn get_user_by_username(&mut self, user_id: &str) -> Result<GetUserResponse, Box<dyn std::error::Error>>;
|
||||||
async fn get_user_by_email(&mut self, email: &str) -> Result<GetUserResponse, Box<dyn Error>>;
|
async fn get_user_by_email(&mut self, email: &str) -> Result<GetUserResponse, Box<dyn Error>>;
|
||||||
async fn store_password_reset(&mut self, email: &str, reset_token: &str, expires_at: DateTime<Utc>) -> Result<(), Box<dyn Error>>;
|
|
||||||
async fn get_password_reset(&self, reset_token: &str) -> Result<Option<PasswordReset>, Box<dyn Error>>;
|
|
||||||
async fn delete_password_reset(&self, reset_token: &str) -> Result<(), Box<dyn Error>>;
|
|
||||||
async fn update_user_password(&self, email: &str, hashed_password: &str) -> Result<(), Box<dyn Error>>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use crate::session::{session_service_client::SessionServiceClient, GetSessionRequest, GetSessionResponse, RefreshSessionRequest, RefreshSessionResponse};
|
use crate::session::{
|
||||||
|
session_service_client::SessionServiceClient, GetSessionRequest, GetSessionResponse, RefreshSessionRequest,
|
||||||
|
RefreshSessionResponse,
|
||||||
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait SessionClientTrait: Sized {
|
pub trait SessionClientTrait: Sized {
|
||||||
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>>;
|
async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>>;
|
||||||
async fn get_session(
|
async fn get_session(&mut self, session_id: String) -> Result<GetSessionResponse, Box<dyn std::error::Error>>;
|
||||||
&mut self,
|
|
||||||
session_id: String,
|
|
||||||
) -> Result<GetSessionResponse, Box<dyn std::error::Error>>;
|
|
||||||
async fn refresh_session(
|
async fn refresh_session(
|
||||||
&mut self,
|
&mut self,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
@@ -29,16 +28,12 @@ impl SessionClientTrait for SessionClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_session(&mut self, session_id: String) -> Result<GetSessionResponse, Box<dyn Error>> {
|
async fn get_session(&mut self, session_id: String) -> Result<GetSessionResponse, Box<dyn Error>> {
|
||||||
let request = tonic::Request::new(GetSessionRequest {
|
let request = tonic::Request::new(GetSessionRequest { session_id });
|
||||||
session_id,
|
|
||||||
});
|
|
||||||
let response = self.client.get_session(request).await?;
|
let response = self.client.get_session(request).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
async fn refresh_session(&mut self, session_id: String) -> Result<RefreshSessionResponse, Box<dyn Error>> {
|
async fn refresh_session(&mut self, session_id: String) -> Result<RefreshSessionResponse, Box<dyn Error>> {
|
||||||
let request = tonic::Request::new(RefreshSessionRequest {
|
let request = tonic::Request::new(RefreshSessionRequest { session_id });
|
||||||
session_id,
|
|
||||||
});
|
|
||||||
let response = self.client.refresh_session(request).await?;
|
let response = self.client.refresh_session(request).await?;
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ use argon2::{
|
|||||||
pub fn hash_password(password: &str) -> String {
|
pub fn hash_password(password: &str) -> String {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
let argon2 = Argon2::default();
|
let argon2 = Argon2::default();
|
||||||
argon2
|
argon2.hash_password(password.as_ref(), &salt).unwrap().to_string()
|
||||||
.hash_password(password.as_ref(), &salt)
|
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_password(password: &str, hash: &str) -> bool {
|
pub fn verify_password(password: &str, hash: &str) -> bool {
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use dotenv::dotenv;
|
|
||||||
// use auth_service::mocks::database_client_mock::MockDatabaseClient;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_login() {
|
|
||||||
// dotenv().ok();
|
|
||||||
// let mut db_client = MockDatabaseClient::new();
|
|
||||||
//
|
|
||||||
// db_client
|
|
||||||
// .expect_get_user_by_username()
|
|
||||||
// .with(mockall::predicate::eq("test"))
|
|
||||||
// .returning(|user_id| {
|
|
||||||
// Ok(GetUserResponse {
|
|
||||||
// user_id: 1,
|
|
||||||
// username: "test".to_string(),
|
|
||||||
// email: "test@test.com".to_string(),
|
|
||||||
// hashed_password: "test".to_string(),
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// let auth_service = MyAuthService {
|
|
||||||
// db_client,
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// // Create a test LoginRequest
|
|
||||||
// let request = Request::new(LoginRequest {
|
|
||||||
// username: "test".into(),
|
|
||||||
// password: "test".into(),
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// // Call the login method
|
|
||||||
// let response = auth_service.login(request).await.unwrap().into_inner();
|
|
||||||
//
|
|
||||||
// // Verify the response
|
|
||||||
// assert!(!response.token.is_empty());
|
|
||||||
// assert_eq!(response.user_id, "1"); // Replace with the expected user ID
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_validate_token() {
|
|
||||||
dotenv().ok();
|
|
||||||
// let addr = std::env::var("DATABASE_SERVICE_ADDR").unwrap_or_else(|_| "127.0.0.1:50052".to_string());
|
|
||||||
// let db_client = DatabaseClient::connect(&addr).await.unwrap();
|
|
||||||
//
|
|
||||||
// let auth_service = MyAuthService {
|
|
||||||
// db_client,
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// // Generate a token for testing
|
|
||||||
// let token = jwt::generate_token("123", Vec::from(["".to_string()])).unwrap();
|
|
||||||
//
|
|
||||||
// // Create a ValidateTokenRequest
|
|
||||||
// let request = Request::new(ValidateTokenRequest { token });
|
|
||||||
//
|
|
||||||
// // Call the validate_token method
|
|
||||||
// let response = auth_service.validate_token(request).await.unwrap().into_inner();
|
|
||||||
//
|
|
||||||
// // Verify the response
|
|
||||||
// assert!(response.valid);
|
|
||||||
// assert_eq!(response.user_id, "123");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "chrono"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "chrono"] }
|
||||||
tonic = "0.12.3"
|
tonic = "0.12.3"
|
||||||
prost = "0.13.4"
|
prost = "0.13.4"
|
||||||
warp = "0.3.7"
|
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
serde_json = "1.0.133"
|
serde_json = "1.0.133"
|
||||||
tonic-health = "0.12.3"
|
tonic-health = "0.12.3"
|
||||||
|
|||||||
93
character-service/README.md
Normal file
93
character-service/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Character Service
|
||||||
|
|
||||||
|
The Character Service manages character creation, deletion, and retrieval in the MMORPG server architecture.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Character Service provides gRPC endpoints for:
|
||||||
|
- Retrieving character lists for users
|
||||||
|
- Creating new characters
|
||||||
|
- Deleting characters
|
||||||
|
- Retrieving detailed character information
|
||||||
|
|
||||||
|
It communicates with the database service to store and retrieve character data.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The service is built using the following components:
|
||||||
|
|
||||||
|
- **gRPC Server**: Exposes character management endpoints
|
||||||
|
- **Character DB Client**: Communicates with the database service for character data
|
||||||
|
|
||||||
|
## Service Endpoints
|
||||||
|
|
||||||
|
The Character Service exposes the following gRPC endpoints:
|
||||||
|
|
||||||
|
### GetCharacterList
|
||||||
|
Retrieves a list of characters for a user.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc GetCharacterList(GetCharacterListRequest) returns (GetCharacterListResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### CreateCharacter
|
||||||
|
Creates a new character for a user.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc CreateCharacter(CreateCharacterRequest) returns (CreateCharacterResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeleteCharacter
|
||||||
|
Marks a character for deletion or permanently deletes it.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc DeleteCharacter(DeleteCharacterRequest) returns (DeleteCharacterResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### GetCharacter
|
||||||
|
Retrieves detailed information about a specific character.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc GetCharacter(GetCharacterRequest) returns (GetCharacterResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Character Data Structure
|
||||||
|
|
||||||
|
Characters in the system have the following key attributes:
|
||||||
|
|
||||||
|
- **Basic Information**: ID, name, user ID, creation/deletion dates
|
||||||
|
- **Appearance**: Race, face, hair, stone
|
||||||
|
- **Stats**: Level, attributes (STR, DEX, INT, etc.), HP, MP, experience
|
||||||
|
- **Inventory**: Items, equipment
|
||||||
|
- **Position**: Map ID, coordinates
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The service can be configured using environment variables:
|
||||||
|
|
||||||
|
- `LISTEN_ADDR`: The address to listen on (default: "0.0.0.0")
|
||||||
|
- `SERVICE_PORT`: The port to listen on (default: "50053")
|
||||||
|
- `LOG_LEVEL`: Logging level (default: "info")
|
||||||
|
|
||||||
|
## Running the Service
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t character-service .
|
||||||
|
docker run -p 50053:50053 character-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with External Systems
|
||||||
|
|
||||||
|
The Character Service integrates with:
|
||||||
|
|
||||||
|
- **Database Service**: For character data storage and retrieval
|
||||||
|
- **Auth Service**: For user authentication and authorization
|
||||||
|
- **Packet Service**: For handling client requests related to characters
|
||||||
@@ -5,10 +5,7 @@ fn main() {
|
|||||||
.compile_well_known_types(true)
|
.compile_well_known_types(true)
|
||||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||||
.compile_protos(
|
.compile_protos(
|
||||||
&[
|
&["../proto/character_common.proto", "../proto/character.proto"],
|
||||||
"../proto/character_common.proto",
|
|
||||||
"../proto/character.proto",
|
|
||||||
],
|
|
||||||
&["../proto"],
|
&["../proto"],
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::database::character_db_service_client::CharacterDbServiceClient;
|
|
||||||
use crate::database::{
|
use crate::database::{
|
||||||
Character, CharacterListRequest, CharacterListResponse, CharacterRequest,
|
character_db_service_client::CharacterDbServiceClient, Character, CharacterListRequest, CharacterListResponse,
|
||||||
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest,
|
CharacterRequest, CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest, DeleteCharacterResponse,
|
||||||
DeleteCharacterResponse,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::character_db_client::CharacterDbClient;
|
use crate::character_db_client::CharacterDbClient;
|
||||||
use crate::character_service::character::character_service_server::CharacterService;
|
use crate::character_service::character::character_service_server::CharacterService;
|
||||||
use crate::character_service::character::{
|
use crate::character_service::character::{
|
||||||
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest,
|
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest, DeleteCharacterResponse,
|
||||||
DeleteCharacterResponse, GetCharacterListRequest, GetCharacterListResponse,
|
GetCharacterListRequest, GetCharacterListResponse, GetCharacterRequest, GetCharacterResponse,
|
||||||
GetCharacterRequest, GetCharacterResponse,
|
|
||||||
};
|
};
|
||||||
use crate::character_service::character_common::{Character, CharacterFull};
|
use crate::character_service::character_common::{Character, CharacterFull};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -69,14 +68,7 @@ impl CharacterService for MyCharacterService {
|
|||||||
.character_db_client
|
.character_db_client
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.clone()
|
.clone()
|
||||||
.create_character(
|
.create_character(&req.user_id, &req.name, req.race, req.face, req.hair, req.stone)
|
||||||
&req.user_id,
|
|
||||||
&req.name,
|
|
||||||
req.race,
|
|
||||||
req.face,
|
|
||||||
req.hair,
|
|
||||||
req.stone,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Status::aborted("Unable to create character"))?;
|
.map_err(|_| Status::aborted("Unable to create character"))?;
|
||||||
|
|
||||||
|
|||||||
5
character-service/src/database.rs
Normal file
5
character-service/src/database.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Include the generated code
|
||||||
|
tonic::include_proto!("character_db_api");
|
||||||
|
|
||||||
|
// Re-export the types we need
|
||||||
|
pub use character_db_service_client::CharacterDbServiceClient;
|
||||||
3
character-service/src/lib.rs
Normal file
3
character-service/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod character_db_client;
|
||||||
|
pub mod character_service;
|
||||||
|
pub mod database;
|
||||||
@@ -14,7 +14,7 @@ use std::sync::Arc;
|
|||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use utils::logging;
|
use utils::logging;
|
||||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns};
|
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -25,17 +25,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Set the gRPC server address
|
// Set the gRPC server address
|
||||||
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50053".to_string());
|
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50053".to_string());
|
||||||
let db_url = format!("http://{}",get_kube_service_endpoints_by_dns("database-service","tcp","database-service").await?.get(0).unwrap());
|
let db_url = format!(
|
||||||
|
"http://{}",
|
||||||
|
get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service")
|
||||||
|
.await?
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
let full_addr = format!("{}:{}", &addr, port);
|
let full_addr = format!("{}:{}", &addr, port);
|
||||||
let address = full_addr.parse().expect("Invalid address");
|
let address = full_addr.parse().expect("Invalid address");
|
||||||
let character_db_client = Arc::new(CharacterDbClient::connect(&db_url).await?);
|
let character_db_client = Arc::new(CharacterDbClient::connect(&db_url).await?);
|
||||||
let character_service = MyCharacterService {
|
let character_service = MyCharacterService { character_db_client };
|
||||||
character_db_client,
|
|
||||||
};
|
|
||||||
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||||
health_reporter.set_serving::<CharacterServiceServer<MyCharacterService>>().await;
|
health_reporter
|
||||||
|
.set_serving::<CharacterServiceServer<MyCharacterService>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
tonic::transport::Server::builder()
|
tonic::transport::Server::builder()
|
||||||
.add_service(health_service)
|
.add_service(health_service)
|
||||||
|
|||||||
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"))
|
Ok(result.get("id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_character(
|
pub async fn delete_character(&self, character_id: i32, delete_type: i32) -> Result<i64, sqlx::Error> {
|
||||||
&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";
|
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 {
|
if 0 == delete_type {
|
||||||
query = "UPDATE character SET \"updatedAt\" = NOW(), \"deletedAt\" = null WHERE id = $1 RETURNING \"userId\", 0::BIGINT as deleted_at";
|
query = "UPDATE character SET \"updatedAt\" = NOW(), \"deletedAt\" = null WHERE id = $1 RETURNING \"userId\", 0::BIGINT as deleted_at";
|
||||||
}
|
}
|
||||||
let result = sqlx::query(query)
|
let result = sqlx::query(query).bind(character_id).fetch_one(&self.pool).await?;
|
||||||
.bind(character_id)
|
|
||||||
.fetch_one(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Invalidate cache
|
// Invalidate cache
|
||||||
let cache_key = format!("character:user:{}", result.get::<i32, &str>("user_id"));
|
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"))
|
Ok(result.get::<i64, &str>("deleted_at"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_characters_by_user(
|
pub async fn get_characters_by_user(&self, user_id: String) -> Result<Vec<Character>, sqlx::Error> {
|
||||||
&self,
|
|
||||||
user_id: String,
|
|
||||||
) -> Result<Vec<Character>, sqlx::Error> {
|
|
||||||
let cache_key = format!("character:user:{}", user_id);
|
let cache_key = format!("character:user:{}", user_id);
|
||||||
|
|
||||||
// Try fetching from Redis cache
|
// Try fetching from Redis cache
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::characters::CharacterRepository;
|
use crate::characters::CharacterRepository;
|
||||||
|
use crate::sessions::SessionRepository;
|
||||||
use crate::users::UserRepository;
|
use crate::users::UserRepository;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use utils::redis_cache::RedisCache;
|
use utils::redis_cache::RedisCache;
|
||||||
use crate::sessions::SessionRepository;
|
|
||||||
|
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub user_repo: Arc<UserRepository>,
|
pub user_repo: Arc<UserRepository>,
|
||||||
@@ -21,7 +21,7 @@ impl Database {
|
|||||||
Self {
|
Self {
|
||||||
user_repo,
|
user_repo,
|
||||||
character_repo,
|
character_repo,
|
||||||
session_repo
|
session_repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
use crate::grpc::character_db_service_server::CharacterDbService;
|
use crate::grpc::character_db_service_server::CharacterDbService;
|
||||||
use crate::grpc::database_service::MyDatabaseService;
|
use crate::grpc::database_service::MyDatabaseService;
|
||||||
use crate::grpc::{
|
use crate::grpc::{
|
||||||
Character, CharacterListRequest, CharacterListResponse, CharacterRequest,
|
Character, CharacterListRequest, CharacterListResponse, CharacterRequest, CreateCharacterRequest,
|
||||||
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest,
|
CreateCharacterResponse, DeleteCharacterRequest, DeleteCharacterResponse,
|
||||||
DeleteCharacterResponse,
|
|
||||||
};
|
};
|
||||||
use tonic::{Request, Response, Status};
|
use tonic::{Request, Response, Status};
|
||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl CharacterDbService for MyDatabaseService {
|
impl CharacterDbService for MyDatabaseService {
|
||||||
async fn get_character(
|
async fn get_character(&self, request: Request<CharacterRequest>) -> Result<Response<Character>, Status> {
|
||||||
&self,
|
|
||||||
request: Request<CharacterRequest>,
|
|
||||||
) -> Result<Response<Character>, Status> {
|
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
let repo = &self.db.character_repo;
|
let repo = &self.db.character_repo;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
pub mod database_service;
|
|
||||||
mod character_service;
|
mod character_service;
|
||||||
mod user_service;
|
pub mod database_service;
|
||||||
mod session_service;
|
mod session_service;
|
||||||
|
mod user_service;
|
||||||
|
|
||||||
tonic::include_proto!("user_db_api");
|
tonic::include_proto!("user_db_api");
|
||||||
tonic::include_proto!("character_db_api");
|
tonic::include_proto!("character_db_api");
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
use crate::grpc::database_service::MyDatabaseService;
|
use crate::grpc::database_service::MyDatabaseService;
|
||||||
use crate::grpc::session_service_server::SessionService;
|
use crate::grpc::session_service_server::SessionService;
|
||||||
|
use crate::grpc::{GetSessionRequest, GetSessionResponse, RefreshSessionRequest, RefreshSessionResponse};
|
||||||
use tonic::{Request, Response, Status};
|
use tonic::{Request, Response, Status};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use crate::grpc::{GetSessionRequest, GetSessionResponse, RefreshSessionRequest, RefreshSessionResponse};
|
|
||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl SessionService for MyDatabaseService {
|
impl SessionService for MyDatabaseService {
|
||||||
async fn get_session(
|
async fn get_session(&self, request: Request<GetSessionRequest>) -> Result<Response<GetSessionResponse>, Status> {
|
||||||
&self,
|
|
||||||
request: Request<GetSessionRequest>,
|
|
||||||
) -> Result<Response<GetSessionResponse>, Status> {
|
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
debug!("get_session: {:?}", req);
|
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"))?;
|
.map_err(|_| Status::not_found("Session not found"))?;
|
||||||
|
|
||||||
debug!("session: {:?}", session);
|
debug!("session: {:?}", session);
|
||||||
@@ -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();
|
let req = request.into_inner();
|
||||||
debug!("get_session: {:?}", req);
|
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"))?;
|
.map_err(|_| Status::not_found("Session not found"))?;
|
||||||
|
|
||||||
let valid = true;
|
let valid = true;
|
||||||
|
|
||||||
debug!("session: {:?}", session);
|
debug!("session: {:?}", session);
|
||||||
Ok(Response::new(RefreshSessionResponse {
|
Ok(Response::new(RefreshSessionResponse { valid }))
|
||||||
valid
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
use crate::grpc::database_service::MyDatabaseService;
|
use crate::grpc::database_service::MyDatabaseService;
|
||||||
use crate::grpc::user_service_server::UserService;
|
use crate::grpc::user_service_server::UserService;
|
||||||
use crate::grpc::{
|
use crate::grpc::{GetUserByEmailRequest, GetUserByUsernameRequest, GetUserRequest, GetUserResponse};
|
||||||
GetUserByEmailRequest, GetUserByUsernameRequest,
|
|
||||||
GetUserRequest, GetUserResponse,
|
|
||||||
};
|
|
||||||
use tonic::{Request, Response, Status};
|
use tonic::{Request, Response, Status};
|
||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl UserService for MyDatabaseService {
|
impl UserService for MyDatabaseService {
|
||||||
async fn get_user(
|
async fn get_user(&self, request: Request<GetUserRequest>) -> Result<Response<GetUserResponse>, Status> {
|
||||||
&self,
|
|
||||||
request: Request<GetUserRequest>,
|
|
||||||
) -> Result<Response<GetUserResponse>, Status> {
|
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
|
|
||||||
let user = self
|
let user = self
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod characters;
|
pub mod characters;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod grpc;
|
pub mod grpc;
|
||||||
pub mod users;
|
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
|
pub mod users;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use database_service::db::Database;
|
use database_service::db::Database;
|
||||||
use database_service::grpc::character_db_service_server::CharacterDbServiceServer;
|
use database_service::grpc::character_db_service_server::CharacterDbServiceServer;
|
||||||
use database_service::grpc::database_service::MyDatabaseService;
|
use database_service::grpc::database_service::MyDatabaseService;
|
||||||
|
use database_service::grpc::session_service_server::SessionServiceServer;
|
||||||
use database_service::grpc::user_service_server::UserServiceServer;
|
use database_service::grpc::user_service_server::UserServiceServer;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
use sqlx::postgres::PgPoolOptions;
|
||||||
@@ -12,7 +13,6 @@ use tokio::sync::Mutex;
|
|||||||
use tonic::transport::Server;
|
use tonic::transport::Server;
|
||||||
use tracing::{info, Level};
|
use tracing::{info, Level};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
use database_service::grpc::session_service_server::SessionServiceServer;
|
|
||||||
use utils::logging;
|
use utils::logging;
|
||||||
use utils::redis_cache::RedisCache;
|
use utils::redis_cache::RedisCache;
|
||||||
|
|
||||||
@@ -39,7 +39,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let my_service = MyDatabaseService { db };
|
let my_service = MyDatabaseService { db };
|
||||||
|
|
||||||
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
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()?);
|
let address = SocketAddr::new(addr.parse()?, port.parse()?);
|
||||||
tokio::spawn(
|
tokio::spawn(
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sqlx::{FromRow, Row};
|
use sqlx::{FromRow, Row};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use utils::redis_cache::{Cache, RedisCache};
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
use utils::redis_cache::{Cache, RedisCache};
|
||||||
|
|
||||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
@@ -24,25 +24,30 @@ impl SessionRepository {
|
|||||||
pub async fn get_session(&self, session_id: &str) -> Result<Session, sqlx::Error> {
|
pub async fn get_session(&self, session_id: &str) -> Result<Session, sqlx::Error> {
|
||||||
let cache_key = format!("session:{}", session_id);
|
let cache_key = format!("session:{}", session_id);
|
||||||
|
|
||||||
if let Some(session) = self.cache.lock().await
|
if let Some(session) = self
|
||||||
.get::<Session>(&cache_key).await
|
.cache
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get::<Session>(&cache_key)
|
||||||
|
.await
|
||||||
.map_err(|_| sqlx::Error::RowNotFound)?
|
.map_err(|_| sqlx::Error::RowNotFound)?
|
||||||
{
|
{
|
||||||
return Ok(session);
|
return Ok(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch from database
|
// Fetch from database
|
||||||
let session = sqlx::query_as::<_, Session>(
|
let session = sqlx::query_as::<_, Session>("SELECT id, \"userId\" as user_id FROM session WHERE id = $1")
|
||||||
"SELECT id, \"userId\" as user_id FROM session WHERE id = $1",
|
|
||||||
)
|
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
debug!("session: {:?}", session);
|
debug!("session: {:?}", session);
|
||||||
|
|
||||||
self.cache.lock().await
|
self.cache
|
||||||
.set(&cache_key, &session, 300).await
|
.lock()
|
||||||
|
.await
|
||||||
|
.set(&cache_key, &session, 300)
|
||||||
|
.await
|
||||||
.map_err(|_| sqlx::Error::RowNotFound)?;
|
.map_err(|_| sqlx::Error::RowNotFound)?;
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
@@ -50,26 +55,34 @@ impl SessionRepository {
|
|||||||
pub async fn refresh_session(&self, session_id: &str) -> Result<Session, sqlx::Error> {
|
pub async fn refresh_session(&self, session_id: &str) -> Result<Session, sqlx::Error> {
|
||||||
let cache_key = format!("session:{}", session_id);
|
let cache_key = format!("session:{}", session_id);
|
||||||
|
|
||||||
if let Some(session) = self.cache.lock().await
|
if let Some(session) = self
|
||||||
.get::<Session>(&cache_key).await
|
.cache
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get::<Session>(&cache_key)
|
||||||
|
.await
|
||||||
.map_err(|_| sqlx::Error::RowNotFound)?
|
.map_err(|_| sqlx::Error::RowNotFound)?
|
||||||
{
|
{
|
||||||
self.cache.lock().await
|
self.cache
|
||||||
.refresh(&cache_key, 300).await
|
.lock()
|
||||||
|
.await
|
||||||
|
.refresh(&cache_key, 300)
|
||||||
|
.await
|
||||||
.map_err(|_| sqlx::Error::RowNotFound)?;
|
.map_err(|_| sqlx::Error::RowNotFound)?;
|
||||||
return Ok(session);
|
return Ok(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to make sure the session is still valid
|
// Check to make sure the session is still valid
|
||||||
let session = sqlx::query_as::<_, Session>(
|
let session = sqlx::query_as::<_, Session>("SELECT id, \"userId\" as user_id FROM session WHERE id = $1")
|
||||||
"SELECT id, \"userId\" as user_id FROM session WHERE id = $1",
|
|
||||||
)
|
|
||||||
.bind(session_id)
|
.bind(session_id)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
self.cache.lock().await
|
self.cache
|
||||||
.set(&cache_key, &session, 300).await
|
.lock()
|
||||||
|
.await
|
||||||
|
.set(&cache_key, &session, 300)
|
||||||
|
.await
|
||||||
.map_err(|_| sqlx::Error::RowNotFound)?;
|
.map_err(|_| sqlx::Error::RowNotFound)?;
|
||||||
Ok(session)
|
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 serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow};
|
use sqlx::FromRow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use utils::redis_cache::{Cache, RedisCache};
|
use utils::redis_cache::{Cache, RedisCache};
|
||||||
@@ -38,12 +38,11 @@ impl UserRepository {
|
|||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = sqlx::query_as::<_, User>(
|
let user =
|
||||||
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE id = $1",
|
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE id = $1")
|
||||||
)
|
.bind(user_id)
|
||||||
.bind(user_id)
|
.fetch_one(&self.pool)
|
||||||
.fetch_one(&self.pool)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
@@ -68,12 +67,11 @@ impl UserRepository {
|
|||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = sqlx::query_as::<_, User>(
|
let user =
|
||||||
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE name = $1",
|
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE name = $1")
|
||||||
)
|
.bind(username)
|
||||||
.bind(username)
|
.fetch_one(&self.pool)
|
||||||
.fetch_one(&self.pool)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
@@ -98,12 +96,11 @@ impl UserRepository {
|
|||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = sqlx::query_as::<_, User>(
|
let user =
|
||||||
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1",
|
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1")
|
||||||
)
|
.bind(email)
|
||||||
.bind(email)
|
.fetch_one(&self.pool)
|
||||||
.fetch_one(&self.pool)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.lock()
|
||||||
@@ -128,12 +125,11 @@ impl UserRepository {
|
|||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = sqlx::query_as::<_, User>(
|
let user =
|
||||||
"SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1",
|
sqlx::query_as::<_, User>("SELECT id, name, email, role, createdAt, updatedAt FROM user WHERE email = $1")
|
||||||
)
|
.bind(session)
|
||||||
.bind(session)
|
.fetch_one(&self.pool)
|
||||||
.fetch_one(&self.pool)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.cache
|
self.cache
|
||||||
.lock()
|
.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");
|
|
||||||
}
|
|
||||||
78
launcher/README.md
Normal file
78
launcher/README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Game Launcher
|
||||||
|
|
||||||
|
The Game Launcher is a client-side application that launches the MMORPG game client with the appropriate connection parameters.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Launcher is responsible for:
|
||||||
|
- Parsing launch URLs from the web authentication system
|
||||||
|
- Extracting connection parameters (server IP, port, session tokens)
|
||||||
|
- Launching the game client with the correct command-line arguments
|
||||||
|
- Handling updates (future functionality)
|
||||||
|
|
||||||
|
## URL Format
|
||||||
|
|
||||||
|
The launcher accepts URLs in the following format:
|
||||||
|
|
||||||
|
```
|
||||||
|
launcher://launch?ip=127.0.0.1&port=29000&session=SESSION_TOKEN&username=USERNAME
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `ip`: Game server IP address
|
||||||
|
- `port`: Game server port
|
||||||
|
- `otp`: One-time password (for direct login)
|
||||||
|
- `session`: Session token (for direct login)
|
||||||
|
- `username`: User's username
|
||||||
|
- `password`: User's password (only used for non-direct login)
|
||||||
|
|
||||||
|
## Command-Line Arguments
|
||||||
|
|
||||||
|
The launcher passes the following arguments to the game client:
|
||||||
|
|
||||||
|
- `@TRIGGER_SOFT@`: Required trigger argument
|
||||||
|
- `_server`: Server IP address
|
||||||
|
- `_port`: Server port
|
||||||
|
- `_direct`: Direct login flag
|
||||||
|
- `_otp`: One-time password
|
||||||
|
- `_session`: Session token
|
||||||
|
- `_userid`: User's username
|
||||||
|
- `_pass`: User's password
|
||||||
|
|
||||||
|
## Platform Support
|
||||||
|
|
||||||
|
The launcher supports:
|
||||||
|
- Windows: Launches TRose.exe directly
|
||||||
|
- Linux: Uses Bottles to run the Windows client
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No additional configuration is required. The launcher extracts all necessary information from the launch URL.
|
||||||
|
|
||||||
|
## Building the Launcher
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The launcher is typically invoked by clicking a link on the game's website:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a href="launcher://launch?ip=127.0.0.1&port=29000&session=SESSION_TOKEN&username=USERNAME">
|
||||||
|
Launch Game
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
It can also be run directly from the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
launcher "launcher://launch?ip=127.0.0.1&port=29000&session=SESSION_TOKEN&username=USERNAME"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with External Systems
|
||||||
|
|
||||||
|
The Launcher integrates with:
|
||||||
|
- **Web Authentication System**: Receives launch parameters via URL
|
||||||
|
- **Game Client**: Launches the client with the appropriate parameters
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
use crate::format_shell_command;
|
use crate::format_shell_command;
|
||||||
use crate::wait_for_keypress;
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::env;
|
|
||||||
use std::process::exit;
|
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn create_command() -> Command {
|
fn create_command() -> Command {
|
||||||
|
use crate::wait_for_keypress;
|
||||||
|
use std::env;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
let exe_dir_path = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
let exe_dir_path = env::current_exe().unwrap().parent().unwrap().to_path_buf();
|
||||||
|
|
||||||
// Change the working directory
|
// Change the working directory
|
||||||
@@ -76,10 +77,7 @@ pub(crate) fn launch_game(url: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command
|
command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
||||||
.stdin(Stdio::null())
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null());
|
|
||||||
|
|
||||||
info!("Executing: {:?}", format_shell_command(&command));
|
info!("Executing: {:?}", format_shell_command(&command));
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ name = "packet-service"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
# Define both a binary and a library target
|
||||||
|
[[bin]]
|
||||||
|
name = "packet-service"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "packet_service"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
mocks = []
|
mocks = []
|
||||||
consul = []
|
consul = []
|
||||||
|
|||||||
107
packet-service/README.md
Normal file
107
packet-service/README.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Packet Service
|
||||||
|
|
||||||
|
The Packet Service handles communication between game clients and the MMORPG server using a custom binary packet protocol.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Packet Service is responsible for:
|
||||||
|
- Accepting TCP connections from game clients
|
||||||
|
- Parsing binary packet data
|
||||||
|
- Routing packets to appropriate handlers
|
||||||
|
- Sending response packets back to clients
|
||||||
|
- Managing client connection state
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The service is built using the following components:
|
||||||
|
|
||||||
|
- **TCP Server**: Accepts client connections
|
||||||
|
- **Buffer Pool**: Efficiently manages memory for packet processing
|
||||||
|
- **Packet Router**: Routes packets to appropriate handlers
|
||||||
|
- **Connection Service**: Manages client connection state
|
||||||
|
- **Packet Handlers**: Process specific packet types
|
||||||
|
- **Auth/Character Clients**: Communicate with other services
|
||||||
|
|
||||||
|
## Packet Structure
|
||||||
|
|
||||||
|
Each packet follows a standard binary format:
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------------+----------------+----------------+----------------+
|
||||||
|
| Packet Size | Packet Type | Packet CRC | Payload |
|
||||||
|
| (2 bytes) | (2 bytes) | (2 bytes) | (variable) |
|
||||||
|
+----------------+----------------+----------------+----------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Packet Size**: Total size of the packet in bytes (including header)
|
||||||
|
- **Packet Type**: Identifies the packet type (see `packet_type.rs`)
|
||||||
|
- **Packet CRC**: Checksum for packet validation
|
||||||
|
- **Payload**: Packet-specific data
|
||||||
|
|
||||||
|
## Packet Types
|
||||||
|
|
||||||
|
The service supports numerous packet types for different game operations:
|
||||||
|
|
||||||
|
- **Authentication**: Login, logout, server selection
|
||||||
|
- **Character**: Character creation, deletion, selection
|
||||||
|
- **Chat**: Normal chat, whispers, shouts, party chat
|
||||||
|
- **Movement**: Position updates, teleportation
|
||||||
|
- **Combat**: Attacks, skills, damage
|
||||||
|
- **Items**: Inventory management, equipment
|
||||||
|
- **Party**: Party formation, invitations
|
||||||
|
- **Trade**: Item trading between players
|
||||||
|
- **Shop**: NPC shop interactions
|
||||||
|
|
||||||
|
See `packet_type.rs` for a complete list of supported packet types.
|
||||||
|
|
||||||
|
## Connection State
|
||||||
|
|
||||||
|
The service maintains state for each client connection, including:
|
||||||
|
|
||||||
|
- User ID
|
||||||
|
- Session ID
|
||||||
|
- Character ID
|
||||||
|
- Character list
|
||||||
|
- Additional session data
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
The service exposes Prometheus metrics for monitoring:
|
||||||
|
|
||||||
|
- Active connections
|
||||||
|
- Packets received/sent
|
||||||
|
- Packet processing time
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The service can be configured using environment variables:
|
||||||
|
|
||||||
|
- `LISTEN_ADDR`: The address to listen on (default: "0.0.0.0")
|
||||||
|
- `SERVICE_PORT`: The port to listen on (default: "29000")
|
||||||
|
- `PACKET_METRICS_PORT`: Port for Prometheus metrics (default: "9102")
|
||||||
|
- `MAX_CONCURRENT_CONNECTIONS`: Maximum allowed concurrent connections
|
||||||
|
- `BUFFER_POOL_SIZE`: Size of the packet buffer pool
|
||||||
|
- `LOG_LEVEL`: Logging level (default: "info")
|
||||||
|
|
||||||
|
## Running the Service
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t packet-service .
|
||||||
|
docker run -p 29000:29000 -p 9102:9102 packet-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with External Systems
|
||||||
|
|
||||||
|
The Packet Service integrates with:
|
||||||
|
|
||||||
|
- **Auth Service**: For user authentication and session validation
|
||||||
|
- **Character Service**: For character management
|
||||||
|
- **World Service**: For game world interactions
|
||||||
@@ -69,10 +69,7 @@ impl AuthClient {
|
|||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout(
|
pub async fn logout(&mut self, session_id: &str) -> Result<Empty, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
&mut self,
|
|
||||||
session_id: &str,
|
|
||||||
) -> Result<Empty, Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
let request = LogoutRequest {
|
let request = LogoutRequest {
|
||||||
session_id: session_id.to_string(),
|
session_id: session_id.to_string(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::character::character_service_client::CharacterServiceClient;
|
use crate::character::character_service_client::CharacterServiceClient;
|
||||||
use crate::character::{
|
use crate::character::{
|
||||||
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest,
|
CreateCharacterRequest, CreateCharacterResponse, DeleteCharacterRequest, DeleteCharacterResponse,
|
||||||
DeleteCharacterResponse, GetCharacterListRequest, GetCharacterListResponse,
|
GetCharacterListRequest, GetCharacterListResponse, GetCharacterRequest, GetCharacterResponse,
|
||||||
GetCharacterRequest, GetCharacterResponse,
|
|
||||||
};
|
};
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::Channel;
|
||||||
use utils::null_string::NullTerminatedString;
|
use utils::null_string::NullTerminatedString;
|
||||||
|
|||||||
@@ -17,15 +17,12 @@ impl ConnectionService {
|
|||||||
|
|
||||||
pub fn add_connection(&self) -> String {
|
pub fn add_connection(&self) -> String {
|
||||||
let connection_id = Uuid::new_v4().to_string();
|
let connection_id = Uuid::new_v4().to_string();
|
||||||
self.connections
|
self.connections.insert(connection_id.clone(), ConnectionState::new());
|
||||||
.insert(connection_id.clone(), ConnectionState::new());
|
|
||||||
connection_id
|
connection_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connection(&self, connection_id: &str) -> Option<ConnectionState> {
|
pub fn get_connection(&self, connection_id: &str) -> Option<ConnectionState> {
|
||||||
self.connections
|
self.connections.get(connection_id).map(|entry| entry.clone())
|
||||||
.get(connection_id)
|
|
||||||
.map(|entry| entry.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connection_mut(
|
pub fn get_connection_mut(
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ pub(crate) async fn handle_login_req(
|
|||||||
connection_id: String,
|
connection_id: String,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
debug!(
|
debug!("decoding packet payload of size {}", packet.payload.as_slice().len());
|
||||||
"decoding packet payload of size {}",
|
|
||||||
packet.payload.as_slice().len()
|
|
||||||
);
|
|
||||||
let data = CliLoginTokenReq::decode(packet.payload.as_slice())?;
|
let data = CliLoginTokenReq::decode(packet.payload.as_slice())?;
|
||||||
debug!("{:?}", data);
|
debug!("{:?}", data);
|
||||||
|
|
||||||
@@ -175,9 +172,7 @@ pub(crate) async fn handle_login_req(
|
|||||||
"name" => {
|
"name" => {
|
||||||
server_name = value;
|
server_name = value;
|
||||||
}
|
}
|
||||||
"tags" => {
|
"tags" => {}
|
||||||
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ use crate::enums::ItemType;
|
|||||||
use crate::packet::{send_packet, Packet, PacketPayload};
|
use crate::packet::{send_packet, Packet, PacketPayload};
|
||||||
use crate::packet_type::PacketType;
|
use crate::packet_type::PacketType;
|
||||||
use crate::packets::*;
|
use crate::packets::*;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tonic::{Code, Status};
|
use tonic::{Code, Status};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use utils::null_string::NullTerminatedString;
|
use utils::null_string::NullTerminatedString;
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
|
|
||||||
|
|
||||||
fn string_to_u32(s: &str) -> u32 {
|
fn string_to_u32(s: &str) -> u32 {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
@@ -80,20 +79,16 @@ pub(crate) async fn handle_char_list_req(
|
|||||||
let request = CliCharListReq::decode(packet.payload.as_slice());
|
let request = CliCharListReq::decode(packet.payload.as_slice());
|
||||||
debug!("{:?}", request);
|
debug!("{:?}", request);
|
||||||
|
|
||||||
let mut user_id= "".to_string();
|
let mut user_id = "".to_string();
|
||||||
let session_id;
|
let session_id;
|
||||||
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
||||||
user_id = state.user_id.expect("Missing user id in connection state");
|
user_id = state.user_id.expect("Missing user id in connection state");
|
||||||
session_id = state
|
session_id = state.session_id.expect("Missing session id in connection state");
|
||||||
.session_id
|
|
||||||
.expect("Missing session id in connection state");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// query the character service for the character list for this user
|
// query the character service for the character list for this user
|
||||||
let mut character_client = character_client.lock().await;
|
let mut character_client = character_client.lock().await;
|
||||||
let character_list = character_client
|
let character_list = character_client.get_character_list(&user_id).await?;
|
||||||
.get_character_list(&user_id)
|
|
||||||
.await?;
|
|
||||||
let mut characters = vec![];
|
let mut characters = vec![];
|
||||||
let mut character_id_list: Vec<u32> = Vec::new();
|
let mut character_id_list: Vec<u32> = Vec::new();
|
||||||
for character in character_list.characters {
|
for character in character_list.characters {
|
||||||
@@ -154,9 +149,7 @@ pub(crate) async fn handle_create_char_req(
|
|||||||
let session_id;
|
let session_id;
|
||||||
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
||||||
user_id = state.user_id.expect("Missing user id in connection state");
|
user_id = state.user_id.expect("Missing user id in connection state");
|
||||||
session_id = state
|
session_id = state.session_id.expect("Missing session id in connection state");
|
||||||
.session_id
|
|
||||||
.expect("Missing session id in connection state");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the data to the character service to create the character
|
// send the data to the character service to create the character
|
||||||
@@ -181,10 +174,7 @@ pub(crate) async fn handle_create_char_req(
|
|||||||
_ => srv_create_char_reply::Result::Failed,
|
_ => srv_create_char_reply::Result::Failed,
|
||||||
};
|
};
|
||||||
|
|
||||||
let data = SrvCreateCharReply {
|
let data = SrvCreateCharReply { result, platininum: 0 };
|
||||||
result,
|
|
||||||
platininum: 0,
|
|
||||||
};
|
|
||||||
let response_packet = Packet::new(PacketType::PakccCreateCharReply, &data)?;
|
let response_packet = Packet::new(PacketType::PakccCreateCharReply, &data)?;
|
||||||
send_packet(stream, &response_packet).await?;
|
send_packet(stream, &response_packet).await?;
|
||||||
|
|
||||||
@@ -209,9 +199,7 @@ pub(crate) async fn handle_delete_char_req(
|
|||||||
|
|
||||||
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
||||||
user_id = state.user_id.expect("Missing user id in connection state");
|
user_id = state.user_id.expect("Missing user id in connection state");
|
||||||
session_id = state
|
session_id = state.session_id.expect("Missing session id in connection state");
|
||||||
.session_id
|
|
||||||
.expect("Missing session id in connection state");
|
|
||||||
character_id_list = state.character_list.expect("Missing character id list");
|
character_id_list = state.character_list.expect("Missing character id list");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,10 +244,7 @@ pub(crate) async fn handle_select_char_req(
|
|||||||
let mut character_id_list: Vec<u32> = Vec::new();
|
let mut character_id_list: Vec<u32> = Vec::new();
|
||||||
if let Some(mut state) = connection_service.get_connection_mut(&connection_id) {
|
if let Some(mut state) = connection_service.get_connection_mut(&connection_id) {
|
||||||
user_id = state.user_id.clone().expect("Missing user id in connection state");
|
user_id = state.user_id.clone().expect("Missing user id in connection state");
|
||||||
character_id_list = state
|
character_id_list = state.character_list.clone().expect("Missing character id list");
|
||||||
.character_list
|
|
||||||
.clone()
|
|
||||||
.expect("Missing character id list");
|
|
||||||
state.character_id = Some(request.char_id as i8);
|
state.character_id = Some(request.char_id as i8);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,10 +259,7 @@ pub(crate) async fn handle_select_char_req(
|
|||||||
|
|
||||||
let mut character_client = character_client.lock().await;
|
let mut character_client = character_client.lock().await;
|
||||||
let character_data = character_client
|
let character_data = character_client
|
||||||
.get_character(
|
.get_character(&user_id.to_string(), character_id_list[request.char_id as usize] as u8)
|
||||||
&user_id.to_string(),
|
|
||||||
character_id_list[request.char_id as usize] as u8,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let character = character_data.character.unwrap_or_default();
|
let character = character_data.character.unwrap_or_default();
|
||||||
@@ -332,8 +314,7 @@ pub(crate) async fn handle_select_char_req(
|
|||||||
|
|
||||||
let mut effect_list: [StatusEffect; (MAX_STATUS_EFFECTS as usize)] =
|
let mut effect_list: [StatusEffect; (MAX_STATUS_EFFECTS as usize)] =
|
||||||
core::array::from_fn(|i| StatusEffect::default());
|
core::array::from_fn(|i| StatusEffect::default());
|
||||||
let mut hotbar_list: [HotbarItem; (MAX_HOTBAR_ITEMS as usize)] =
|
let mut hotbar_list: [HotbarItem; (MAX_HOTBAR_ITEMS as usize)] = core::array::from_fn(|i| HotbarItem::default());
|
||||||
core::array::from_fn(|i| HotbarItem::default());
|
|
||||||
let data = SrvSelectCharReply {
|
let data = SrvSelectCharReply {
|
||||||
race: looks.race as u8,
|
race: looks.race as u8,
|
||||||
map: position.map_id as u16,
|
map: position.map_id as u16,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::character_client::CharacterClient;
|
use crate::character_client::CharacterClient;
|
||||||
use crate::connection_service::ConnectionService;
|
use crate::connection_service::ConnectionService;
|
||||||
use crate::packet::{send_packet, Packet, PacketPayload};
|
use crate::packet::{send_packet, Packet, PacketPayload};
|
||||||
use crate::packet_type::PacketType;
|
use crate::packet_type::PacketType;
|
||||||
@@ -32,24 +32,14 @@ pub(crate) async fn handle_change_map_req(
|
|||||||
let session_id;
|
let session_id;
|
||||||
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
||||||
user_id = state.user_id.expect("Missing user id in connection state");
|
user_id = state.user_id.expect("Missing user id in connection state");
|
||||||
session_id = state
|
session_id = state.session_id.expect("Missing session id in connection state");
|
||||||
.session_id
|
char_id = state.character_id.expect("Missing character id in connection state");
|
||||||
.expect("Missing session id in connection state");
|
character_id_list = state.character_list.clone().expect("Missing character id list");
|
||||||
char_id = state
|
|
||||||
.character_id
|
|
||||||
.expect("Missing character id in connection state");
|
|
||||||
character_id_list = state
|
|
||||||
.character_list
|
|
||||||
.clone()
|
|
||||||
.expect("Missing character id list");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut character_client = character_client.lock().await;
|
let mut character_client = character_client.lock().await;
|
||||||
let character_data = character_client
|
let character_data = character_client
|
||||||
.get_character(
|
.get_character(&user_id.to_string(), character_id_list[char_id as usize] as u8)
|
||||||
&user_id.to_string(),
|
|
||||||
character_id_list[char_id as usize] as u8,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let character = character_data.character.unwrap_or_default();
|
let character = character_data.character.unwrap_or_default();
|
||||||
@@ -93,24 +83,14 @@ pub(crate) async fn handle_mouse_cmd_req(
|
|||||||
let mut char_id = 0;
|
let mut char_id = 0;
|
||||||
let mut character_id_list: Vec<u32> = Vec::new();
|
let mut character_id_list: Vec<u32> = Vec::new();
|
||||||
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
||||||
char_id = state
|
char_id = state.character_id.expect("Missing character id in connection state");
|
||||||
.character_id
|
character_id_list = state.character_list.clone().expect("Missing character id list");
|
||||||
.expect("Missing character id in connection state");
|
|
||||||
character_id_list = state
|
|
||||||
.character_list
|
|
||||||
.clone()
|
|
||||||
.expect("Missing character id list");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = SrvMouseCmd {
|
let data = SrvMouseCmd {
|
||||||
id: character_id_list[char_id as usize] as u16,
|
id: character_id_list[char_id as usize] as u16,
|
||||||
target_id: request.target_id,
|
target_id: request.target_id,
|
||||||
distance: distance(
|
distance: distance(520000 as f64, 520000 as f64, request.x as f64, request.y as f64),
|
||||||
520000 as f64,
|
|
||||||
520000 as f64,
|
|
||||||
request.x as f64,
|
|
||||||
request.y as f64,
|
|
||||||
),
|
|
||||||
x: request.x,
|
x: request.x,
|
||||||
y: request.y,
|
y: request.y,
|
||||||
z: request.z,
|
z: request.z,
|
||||||
|
|||||||
7
packet-service/src/lib.rs
Normal file
7
packet-service/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Re-export only the modules needed for tests
|
||||||
|
pub mod bufferpool;
|
||||||
|
pub mod connection_service;
|
||||||
|
pub mod connection_state;
|
||||||
|
pub mod metrics;
|
||||||
|
pub mod packet;
|
||||||
|
pub mod packet_type;
|
||||||
@@ -21,8 +21,8 @@ use tokio::{select, signal};
|
|||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||||
use utils::{health_check, logging};
|
use utils::{health_check, logging};
|
||||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns};
|
|
||||||
use warp::Filter;
|
use warp::Filter;
|
||||||
|
|
||||||
mod auth_client;
|
mod auth_client;
|
||||||
@@ -66,8 +66,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "29000".to_string());
|
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "29000".to_string());
|
||||||
let metrics_port = env::var("PACKET_METRICS_PORT").unwrap_or_else(|_| "4001".to_string());
|
let metrics_port = env::var("PACKET_METRICS_PORT").unwrap_or_else(|_| "4001".to_string());
|
||||||
let auth_url = format!("http://{}",get_kube_service_endpoints_by_dns("auth-service","tcp","auth-service").await?.get(0).unwrap());
|
let auth_url = format!(
|
||||||
let character_url = format!("http://{}",get_kube_service_endpoints_by_dns("character-service","tcp","character-service").await?.get(0).unwrap());
|
"http://{}",
|
||||||
|
get_kube_service_endpoints_by_dns("auth-service", "tcp", "auth-service")
|
||||||
|
.await?
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
let character_url = format!(
|
||||||
|
"http://{}",
|
||||||
|
get_kube_service_endpoints_by_dns("character-service", "tcp", "character-service")
|
||||||
|
.await?
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
// Start health-check endpoint
|
// Start health-check endpoint
|
||||||
health_check::start_health_check(addr.as_str()).await?;
|
health_check::start_health_check(addr.as_str()).await?;
|
||||||
@@ -109,9 +121,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
{
|
{
|
||||||
error!("Error handling connection: {}", e);
|
error!("Error handling connection: {}", e);
|
||||||
}
|
}
|
||||||
packet_router
|
packet_router.connection_service.remove_connection(&connection_id);
|
||||||
.connection_service
|
|
||||||
.remove_connection(&connection_id);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use prometheus::{
|
use prometheus::{
|
||||||
register_counter, register_gauge, register_histogram, Counter, Encoder, Gauge, Histogram,
|
register_counter, register_gauge, register_histogram, Counter, Encoder, Gauge, Histogram, TextEncoder,
|
||||||
TextEncoder,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ impl PacketRouter {
|
|||||||
Ok(packet) => {
|
Ok(packet) => {
|
||||||
debug!("Parsed Packet: {:?}", packet);
|
debug!("Parsed Packet: {:?}", packet);
|
||||||
// Handle the parsed packet (route it, process it, etc.)
|
// Handle the parsed packet (route it, process it, etc.)
|
||||||
self.route_packet(stream, packet, connection_id.clone())
|
self.route_packet(stream, packet, connection_id.clone()).await?;
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
Err(e) => warn!("Failed to parse packet: {}", e),
|
Err(e) => warn!("Failed to parse packet: {}", e),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ service AuthService {
|
|||||||
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
|
rpc ValidateToken(ValidateTokenRequest) returns (ValidateTokenResponse);
|
||||||
rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse);
|
rpc ValidateSession(ValidateSessionRequest) returns (ValidateSessionResponse);
|
||||||
rpc RefreshSession(ValidateSessionRequest) returns (RefreshSessionResponse);
|
rpc RefreshSession(ValidateSessionRequest) returns (RefreshSessionResponse);
|
||||||
rpc Register (RegisterRequest) returns (RegisterResponse);
|
|
||||||
rpc RequestPasswordReset (PasswordResetRequest) returns (PasswordResetResponse);
|
|
||||||
rpc ResetPassword (ResetPasswordRequest) returns (ResetPasswordResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
@@ -55,30 +52,4 @@ message RefreshSessionResponse {
|
|||||||
bool valid = 1;
|
bool valid = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RegisterRequest {
|
|
||||||
string username = 1;
|
|
||||||
string email = 2;
|
|
||||||
string password = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RegisterResponse {
|
|
||||||
int32 user_id = 1;
|
|
||||||
string message = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PasswordResetRequest {
|
|
||||||
string email = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PasswordResetResponse {
|
|
||||||
string message = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ResetPasswordRequest {
|
|
||||||
string reset_token = 1;
|
|
||||||
string new_password = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ResetPasswordResponse {
|
|
||||||
string message = 1;
|
|
||||||
}
|
|
||||||
|
|||||||
94
tests/Cargo.toml
Normal file
94
tests/Cargo.toml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
[package]
|
||||||
|
name = "mmorpg-server-tests"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = false
|
||||||
|
test = true
|
||||||
|
doctest = false
|
||||||
|
bench = false
|
||||||
|
doc = false
|
||||||
|
plugin = false
|
||||||
|
harness = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
auth-service = { path = "../auth-service" }
|
||||||
|
character-service = { path = "../character-service" }
|
||||||
|
database-service = { path = "../database-service" }
|
||||||
|
packet-service = { path = "../packet-service" }
|
||||||
|
utils = { path = "../utils" }
|
||||||
|
|
||||||
|
# Common dependencies
|
||||||
|
tokio = { version = "1.36.0", features = ["full"] }
|
||||||
|
tonic = "0.11.0"
|
||||||
|
bincode = { version = "2.0.0", features = ["derive", "serde"] }
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
mockall = "0.12.1"
|
||||||
|
reqwest = { version = "0.11.24", features = ["json"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
chrono = "0.4.35"
|
||||||
|
sqlx = "0.8.3"
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
dotenv = "0.15"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "auth_users_tests"
|
||||||
|
path = "auth-service/users_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "character_service_tests"
|
||||||
|
path = "character-service/character_service_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "packet_tests"
|
||||||
|
path = "packet-service/packet_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "bufferpool_tests"
|
||||||
|
path = "packet-service/bufferpool_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "connection_service_tests"
|
||||||
|
path = "packet-service/connection_service_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "redis_cache_tests"
|
||||||
|
path = "utils/redis_cache_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "service_discovery_tests"
|
||||||
|
path = "utils/service_discovery_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "multi_service_load_balancer_tests"
|
||||||
|
path = "utils/multi_service_load_balancer_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "health_check_tests"
|
||||||
|
path = "utils/health_check_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "logging_tests"
|
||||||
|
path = "utils/logging_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "get_user_tests"
|
||||||
|
path = "database-service/get_user.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "grpc_get_user_tests"
|
||||||
|
path = "database-service/grpc_get_user.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "integration_tests"
|
||||||
|
path = "database-service/integration.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "mock_tests"
|
||||||
|
path = "database-service/mock_tests.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "redis_cache_db_tests"
|
||||||
|
path = "database-service/redis_cache.rs"
|
||||||
72
tests/README.md
Normal file
72
tests/README.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# MMORPG Server Tests
|
||||||
|
|
||||||
|
This directory contains tests for all components of the MMORPG server architecture.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Running All Tests
|
||||||
|
|
||||||
|
To run all tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tests
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests for a Specific Service
|
||||||
|
|
||||||
|
To run tests for a specific service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tests
|
||||||
|
cargo test --test auth_users_tests
|
||||||
|
cargo test --test character_service_tests
|
||||||
|
cargo test --test packet_tests
|
||||||
|
cargo test --test redis_cache_tests
|
||||||
|
cargo test --test get_user_tests
|
||||||
|
cargo test --test grpc_get_user_tests
|
||||||
|
cargo test --test mock_tests
|
||||||
|
# etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests with Environment Variables
|
||||||
|
|
||||||
|
Some tests require environment variables to be set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redis tests
|
||||||
|
export REDIS_TEST_ENABLED=true
|
||||||
|
export TEST_REDIS_URL=redis://127.0.0.1:6379
|
||||||
|
|
||||||
|
# Kubernetes tests
|
||||||
|
export KUBE_TEST_ENABLED=true
|
||||||
|
export TEST_K8S_SERVICE_NAME=database-service
|
||||||
|
export TEST_K8S_PORT_NAME=database-service
|
||||||
|
|
||||||
|
# Consul tests
|
||||||
|
export CONSUL_TEST_ENABLED=true
|
||||||
|
export TEST_CONSUL_URL=127.0.0.1:8600
|
||||||
|
export TEST_CONSUL_SERVICE_NAME=database-service
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cd tests
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Organization
|
||||||
|
|
||||||
|
Tests are organized by service:
|
||||||
|
|
||||||
|
- **auth-service/**: Tests for the authentication service
|
||||||
|
- **character-service/**: Tests for the character service
|
||||||
|
- **database-service/**: Tests for the database service
|
||||||
|
- **packet-service/**: Tests for the packet service
|
||||||
|
- **utils/**: Tests for shared utilities
|
||||||
|
|
||||||
|
## Adding New Tests
|
||||||
|
|
||||||
|
To add a new test:
|
||||||
|
|
||||||
|
1. Create a new test file in the appropriate service directory
|
||||||
|
2. Add the test to the `[[test]]` section in `Cargo.toml`
|
||||||
|
3. Run the test to ensure it works correctly
|
||||||
70
tests/auth-service/users_tests.rs
Normal file
70
tests/auth-service/users_tests.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use auth_service::users::{hash_password, verify_password};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_password_hashing_and_verification() {
|
||||||
|
// Test with a simple password
|
||||||
|
let password = "test_password";
|
||||||
|
let hashed = hash_password(password);
|
||||||
|
|
||||||
|
// Verify the hash is not the same as the original password
|
||||||
|
assert_ne!(password, hashed);
|
||||||
|
|
||||||
|
// Verify the password against the hash
|
||||||
|
assert!(verify_password(password, &hashed));
|
||||||
|
|
||||||
|
// Verify an incorrect password fails
|
||||||
|
assert!(!verify_password("wrong_password", &hashed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_password_hashing_with_special_characters() {
|
||||||
|
// Test with special characters
|
||||||
|
let password = "P@$$w0rd!#%^&*()";
|
||||||
|
let hashed = hash_password(password);
|
||||||
|
|
||||||
|
// Verify the hash is not the same as the original password
|
||||||
|
assert_ne!(password, hashed);
|
||||||
|
|
||||||
|
// Verify the password against the hash
|
||||||
|
assert!(verify_password(password, &hashed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_password_hashing_with_unicode() {
|
||||||
|
// Test with Unicode characters
|
||||||
|
let password = "пароль123你好世界";
|
||||||
|
let hashed = hash_password(password);
|
||||||
|
|
||||||
|
// Verify the hash is not the same as the original password
|
||||||
|
assert_ne!(password, hashed);
|
||||||
|
|
||||||
|
// Verify the password against the hash
|
||||||
|
assert!(verify_password(password, &hashed));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_different_passwords_produce_different_hashes() {
|
||||||
|
let password1 = "password1";
|
||||||
|
let password2 = "password2";
|
||||||
|
|
||||||
|
let hash1 = hash_password(password1);
|
||||||
|
let hash2 = hash_password(password2);
|
||||||
|
|
||||||
|
// Different passwords should produce different hashes
|
||||||
|
assert_ne!(hash1, hash2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_same_password_produces_different_hashes() {
|
||||||
|
let password = "same_password";
|
||||||
|
|
||||||
|
let hash1 = hash_password(password);
|
||||||
|
let hash2 = hash_password(password);
|
||||||
|
|
||||||
|
// Same password should produce different hashes due to salt
|
||||||
|
assert_ne!(hash1, hash2);
|
||||||
|
|
||||||
|
// But both hashes should verify against the original password
|
||||||
|
assert!(verify_password(password, &hash1));
|
||||||
|
assert!(verify_password(password, &hash2));
|
||||||
|
}
|
||||||
251
tests/character-service/character_service_tests.rs
Normal file
251
tests/character-service/character_service_tests.rs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
use character_service::character_service::character::character_service_server::CharacterService;
|
||||||
|
use character_service::character_service::character::{
|
||||||
|
CreateCharacterRequest, DeleteCharacterRequest, GetCharacterListRequest, GetCharacterRequest,
|
||||||
|
};
|
||||||
|
use character_service::character_service::MyCharacterService;
|
||||||
|
use mockall::predicate::*;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use mockall::mock;
|
||||||
|
|
||||||
|
// Wrapper for the mock to implement the trait
|
||||||
|
struct MockWrapper(MockCharacterDbClientTrait);
|
||||||
|
|
||||||
|
impl character_service::character_db_client::CharacterDbClient for MockWrapper {
|
||||||
|
async fn get_character_list(&mut self, user_id: &str) -> Result<character_service::database::CharacterListResponse, Box<dyn std::error::Error>> {
|
||||||
|
self.0.get_character_list(user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_character(&mut self, user_id: &str, name: &str, race: i32, face: i32, hair: i32, stone: i32) -> Result<character_service::database::CreateCharacterResponse, Box<dyn std::error::Error>> {
|
||||||
|
self.0.create_character(user_id, name, race, face, hair, stone).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_character(&mut self, user_id: &str, char_id: &str, delete_type: i32) -> Result<character_service::database::DeleteCharacterResponse, Box<dyn std::error::Error>> {
|
||||||
|
self.0.delete_character(user_id, char_id, delete_type).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_character(&mut self, user_id: &str, char_id: &str) -> Result<character_service::database::Character, Box<dyn std::error::Error>> {
|
||||||
|
self.0.get_character(user_id, char_id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tonic::{Request, Status};
|
||||||
|
|
||||||
|
// Define a trait for the CharacterDbClient to make it mockable
|
||||||
|
#[mockall::automock]
|
||||||
|
trait CharacterDbClientTrait {
|
||||||
|
async fn get_character_list(&mut self, user_id: &str) -> Result<character_service::database::CharacterListResponse, Box<dyn std::error::Error>>;
|
||||||
|
async fn create_character(&mut self, user_id: &str, name: &str, race: i32, face: i32, hair: i32, stone: i32) -> Result<character_service::database::CreateCharacterResponse, Box<dyn std::error::Error>>;
|
||||||
|
async fn delete_character(&mut self, user_id: &str, char_id: &str, delete_type: i32) -> Result<character_service::database::DeleteCharacterResponse, Box<dyn std::error::Error>>;
|
||||||
|
async fn get_character(&mut self, user_id: &str, char_id: &str) -> Result<character_service::database::Character, Box<dyn std::error::Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_character_list() {
|
||||||
|
// Create a mock CharacterDbClient
|
||||||
|
let mut mock_client = MockCharacterDbClientTrait::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_client
|
||||||
|
.expect_get_character_list()
|
||||||
|
.with(eq("test_user"))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(character_service::database::CharacterListResponse {
|
||||||
|
characters: vec![
|
||||||
|
character_service::database::Character {
|
||||||
|
id: 1,
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
name: "Character1".to_string(),
|
||||||
|
money: 1000,
|
||||||
|
inventory: "{\"items\":[]}".to_string(),
|
||||||
|
stats: "{\"level\":1}".to_string(),
|
||||||
|
skills: "{\"skills\":[]}".to_string(),
|
||||||
|
looks: "{\"race\":0}".to_string(),
|
||||||
|
position: "{\"x\":0,\"y\":0,\"z\":0}".to_string(),
|
||||||
|
deleted_at: "".to_string(),
|
||||||
|
},
|
||||||
|
character_service::database::Character {
|
||||||
|
id: 2,
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
name: "Character2".to_string(),
|
||||||
|
money: 2000,
|
||||||
|
inventory: "{\"items\":[]}".to_string(),
|
||||||
|
stats: "{\"level\":5}".to_string(),
|
||||||
|
skills: "{\"skills\":[]}".to_string(),
|
||||||
|
looks: "{\"race\":1}".to_string(),
|
||||||
|
position: "{\"x\":10,\"y\":10,\"z\":0}".to_string(),
|
||||||
|
deleted_at: "".to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a wrapper struct that implements CharacterDbClient
|
||||||
|
struct MockWrapper(MockCharacterDbClientTrait);
|
||||||
|
impl std::ops::Deref for MockWrapper {
|
||||||
|
type Target = MockCharacterDbClientTrait;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::ops::DerefMut for MockWrapper {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Clone for MockWrapper {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
panic!("Mock should not be cloned")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the service with the mock client
|
||||||
|
let service = MyCharacterService {
|
||||||
|
character_db_client: Arc::new(MockWrapper(mock_client)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a request
|
||||||
|
let request = Request::new(GetCharacterListRequest {
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the service method
|
||||||
|
let response = service.get_character_list(request).await.unwrap();
|
||||||
|
let response = response.into_inner();
|
||||||
|
|
||||||
|
// Verify the response
|
||||||
|
assert_eq!(response.characters.len(), 2);
|
||||||
|
assert_eq!(response.characters[0].name, "Character1");
|
||||||
|
assert_eq!(response.characters[1].name, "Character2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_character() {
|
||||||
|
// Create a mock CharacterDbClient
|
||||||
|
let mut mock_client = MockCharacterDbClientTrait::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_client
|
||||||
|
.expect_create_character()
|
||||||
|
.with(eq("test_user"), eq("NewCharacter"), eq(0), eq(1), eq(2), eq(3))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _, _, _, _, _| {
|
||||||
|
Ok(character_service::database::CreateCharacterResponse {
|
||||||
|
result: 0,
|
||||||
|
character_id: 3,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the service with the mock client
|
||||||
|
let service = MyCharacterService {
|
||||||
|
character_db_client: Arc::new(MockWrapper(mock_client)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a request
|
||||||
|
let request = Request::new(CreateCharacterRequest {
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
name: "NewCharacter".to_string(),
|
||||||
|
race: 0,
|
||||||
|
face: 1,
|
||||||
|
hair: 2,
|
||||||
|
stone: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the service method
|
||||||
|
let response = service.create_character(request).await.unwrap();
|
||||||
|
let response = response.into_inner();
|
||||||
|
|
||||||
|
// Verify the response
|
||||||
|
assert_eq!(response.result, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_character() {
|
||||||
|
// Create a mock CharacterDbClient
|
||||||
|
let mut mock_client = MockCharacterDbClientTrait::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_client
|
||||||
|
.expect_delete_character()
|
||||||
|
.with(eq("test_user"), eq("3"), eq(1))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _, _| {
|
||||||
|
Ok(character_service::database::DeleteCharacterResponse {
|
||||||
|
remaining_time: 86400, // 24 hours in seconds
|
||||||
|
name: "DeletedCharacter".to_string(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the service with the mock client
|
||||||
|
let service = MyCharacterService {
|
||||||
|
character_db_client: Arc::new(MockWrapper(mock_client)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a request
|
||||||
|
let request = Request::new(DeleteCharacterRequest {
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
char_id: "3".to_string(),
|
||||||
|
delete_type: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the service method
|
||||||
|
let response = service.delete_character(request).await.unwrap();
|
||||||
|
let response = response.into_inner();
|
||||||
|
|
||||||
|
// Verify the response
|
||||||
|
assert_eq!(response.remaining_time, 86400);
|
||||||
|
assert_eq!(response.name, "DeletedCharacter");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_character() {
|
||||||
|
// Create a mock CharacterDbClient
|
||||||
|
let mut mock_client = MockCharacterDbClientTrait::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_client
|
||||||
|
.expect_get_character()
|
||||||
|
.with(eq("test_user"), eq("1"))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_, _| {
|
||||||
|
Ok(character_service::database::Character {
|
||||||
|
id: 1,
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
name: "Character1".to_string(),
|
||||||
|
money: 1000,
|
||||||
|
inventory: "{\"items\":[{\"id\":1,\"count\":10}]}".to_string(),
|
||||||
|
stats: "{\"level\":10,\"hp\":100,\"mp\":50}".to_string(),
|
||||||
|
skills: "{\"skills\":[{\"id\":1,\"level\":5}]}".to_string(),
|
||||||
|
looks: "{\"race\":0,\"face\":1,\"hair\":2}".to_string(),
|
||||||
|
position: "{\"x\":100,\"y\":200,\"z\":0,\"map\":1}".to_string(),
|
||||||
|
deleted_at: "".to_string(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the service with the mock client
|
||||||
|
let service = MyCharacterService {
|
||||||
|
character_db_client: Arc::new(MockWrapper(mock_client)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a request
|
||||||
|
let request = Request::new(GetCharacterRequest {
|
||||||
|
user_id: "test_user".to_string(),
|
||||||
|
char_id: "1".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the service method
|
||||||
|
let response = service.get_character(request).await.unwrap();
|
||||||
|
let response = response.into_inner();
|
||||||
|
|
||||||
|
// Verify the response
|
||||||
|
let character = response.character.unwrap();
|
||||||
|
assert_eq!(character.name, "Character1");
|
||||||
|
assert_eq!(character.money, 1000);
|
||||||
|
|
||||||
|
// Verify JSON fields were parsed correctly
|
||||||
|
assert!(character.inventory.contains("items"));
|
||||||
|
assert!(character.stats.contains("level"));
|
||||||
|
assert!(character.skills.contains("skills"));
|
||||||
|
assert!(character.looks.contains("race"));
|
||||||
|
assert!(character.position.contains("map"));
|
||||||
|
}
|
||||||
78
tests/database-service/get_user.rs
Normal file
78
tests/database-service/get_user.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use database_service::users::{User, UserRepository};
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use utils::redis_cache::RedisCache;
|
||||||
|
|
||||||
|
// Helper function to create a test database pool
|
||||||
|
async fn setup_test_pool() -> Result<sqlx::PgPool, sqlx::Error> {
|
||||||
|
let database_url = std::env::var("TEST_DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/test_db".to_string());
|
||||||
|
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a mock Redis cache
|
||||||
|
fn setup_test_cache() -> Arc<Mutex<RedisCache>> {
|
||||||
|
let redis_url = std::env::var("TEST_REDIS_URL")
|
||||||
|
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(RedisCache::new(&redis_url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test user in the database
|
||||||
|
async fn create_test_user(pool: &sqlx::PgPool) -> i32 {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
|
||||||
|
VALUES ('test_user', 'test@example.com', 'user', NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test user");
|
||||||
|
|
||||||
|
result.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up test data
|
||||||
|
async fn cleanup_test_user(pool: &sqlx::PgPool, user_id: i32) {
|
||||||
|
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete test user");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_user() {
|
||||||
|
// Skip test if database connection is not available
|
||||||
|
let pool = match setup_test_pool().await {
|
||||||
|
Ok(pool) => pool,
|
||||||
|
Err(_) => {
|
||||||
|
println!("Skipping test_get_user: Test database not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cache = setup_test_cache();
|
||||||
|
let repo = UserRepository::new(pool.clone(), cache);
|
||||||
|
|
||||||
|
// Create test user
|
||||||
|
let user_id = create_test_user(&pool).await;
|
||||||
|
|
||||||
|
// Test the get_user_by_id function
|
||||||
|
let user = repo.get_user_by_id(user_id).await.unwrap();
|
||||||
|
|
||||||
|
// Validate the user
|
||||||
|
assert_eq!(user.id, user_id);
|
||||||
|
assert_eq!(user.name, "test_user");
|
||||||
|
assert_eq!(user.email, "test@example.com");
|
||||||
|
assert_eq!(user.role, "user");
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
cleanup_test_user(&pool, user_id).await;
|
||||||
|
}
|
||||||
92
tests/database-service/grpc_get_user.rs
Normal file
92
tests/database-service/grpc_get_user.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use database_service::db::Database;
|
||||||
|
use database_service::grpc::database_service::MyDatabaseService;
|
||||||
|
use database_service::grpc::user_service_server::UserService;
|
||||||
|
use database_service::grpc::{GetUserRequest, GetUserResponse};
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use utils::redis_cache::RedisCache;
|
||||||
|
|
||||||
|
// Helper function to create a test database pool
|
||||||
|
async fn setup_test_pool() -> sqlx::PgPool {
|
||||||
|
let database_url = std::env::var("TEST_DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/test_db".to_string());
|
||||||
|
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test database pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a mock Redis cache
|
||||||
|
fn setup_test_cache() -> Arc<Mutex<RedisCache>> {
|
||||||
|
let redis_url = std::env::var("TEST_REDIS_URL")
|
||||||
|
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(RedisCache::new(&redis_url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test user in the database
|
||||||
|
async fn create_test_user(pool: &sqlx::PgPool) -> i32 {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
|
||||||
|
VALUES ('test_user', 'test@example.com', 'user', NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test user");
|
||||||
|
|
||||||
|
result.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up test data
|
||||||
|
async fn cleanup_test_user(pool: &sqlx::PgPool, user_id: i32) {
|
||||||
|
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete test user");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_grpc_get_user() {
|
||||||
|
// Skip test if database connection is not available
|
||||||
|
let pool_result = setup_test_pool().await;
|
||||||
|
let pool = match pool_result {
|
||||||
|
Ok(pool) => pool,
|
||||||
|
Err(_) => {
|
||||||
|
println!("Skipping test_grpc_get_user: Test database not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cache = setup_test_cache();
|
||||||
|
|
||||||
|
// Create test user
|
||||||
|
let user_id = create_test_user(&pool).await;
|
||||||
|
|
||||||
|
// Create the database service
|
||||||
|
let db = Arc::new(Database::new(pool.clone(), cache));
|
||||||
|
let service = MyDatabaseService { db };
|
||||||
|
|
||||||
|
// Create a gRPC request
|
||||||
|
let request = Request::new(GetUserRequest {
|
||||||
|
user_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call the service
|
||||||
|
let response = service.get_user(request).await.unwrap().into_inner();
|
||||||
|
|
||||||
|
// Validate the response
|
||||||
|
assert_eq!(response.user_id, user_id);
|
||||||
|
assert_eq!(response.username, "test_user");
|
||||||
|
assert_eq!(response.email, "test@example.com");
|
||||||
|
assert_eq!(response.role, "user");
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
cleanup_test_user(&pool, user_id).await;
|
||||||
|
}
|
||||||
137
tests/database-service/mock_tests.rs
Normal file
137
tests/database-service/mock_tests.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use database_service::users::User;
|
||||||
|
use mockall::predicate::*;
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use mockall::mock;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
|
// Mock the UserRepository
|
||||||
|
mock! {
|
||||||
|
pub UserRepository {
|
||||||
|
fn get_user_by_id(&self, user_id: i32) -> Result<User, sqlx::Error>;
|
||||||
|
fn get_user_by_username(&self, username: &str) -> Result<User, sqlx::Error>;
|
||||||
|
fn get_user_by_email(&self, email: &str) -> Result<User, sqlx::Error>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the CharacterRepository
|
||||||
|
mock! {
|
||||||
|
pub CharacterRepository {
|
||||||
|
fn get_character_by_id(&self, character_id: i32) -> Result<database_service::characters::Character, sqlx::Error>;
|
||||||
|
fn get_character_list(&self, user_id: String) -> Result<Vec<database_service::characters::Character>, sqlx::Error>;
|
||||||
|
fn create_character(
|
||||||
|
&self,
|
||||||
|
user_id: String,
|
||||||
|
name: &str,
|
||||||
|
inventory: serde_json::Value,
|
||||||
|
skills: serde_json::Value,
|
||||||
|
stats: serde_json::Value,
|
||||||
|
looks: serde_json::Value,
|
||||||
|
position: serde_json::Value,
|
||||||
|
) -> Result<i32, sqlx::Error>;
|
||||||
|
fn delete_character(&self, character_id: i32, delete_type: i32) -> Result<i64, sqlx::Error>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the SessionRepository
|
||||||
|
mock! {
|
||||||
|
pub SessionRepository {
|
||||||
|
fn get_session(&self, session_id: &str) -> Result<database_service::sessions::Session, sqlx::Error>;
|
||||||
|
fn refresh_session(&self, session_id: &str) -> Result<database_service::sessions::Session, sqlx::Error>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock the Database struct
|
||||||
|
struct MockDatabase<U, C, S> {
|
||||||
|
user_repo: Arc<U>,
|
||||||
|
character_repo: Arc<C>,
|
||||||
|
session_repo: Arc<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockDatabaseWithMocks = MockDatabase<MockUserRepository, MockCharacterRepository, MockSessionRepository>;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_user_repository_mock() {
|
||||||
|
// Create a mock UserRepository
|
||||||
|
let mut mock_user_repo = MockUserRepository::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_user_repo
|
||||||
|
.expect_get_user_by_id()
|
||||||
|
.with(eq(123))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(User {
|
||||||
|
id: 123,
|
||||||
|
name: "mock_user".to_string(),
|
||||||
|
email: "mock@example.com".to_string(),
|
||||||
|
role: "user".to_string(),
|
||||||
|
created_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||||
|
updated_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test the mock
|
||||||
|
let user = mock_user_repo.get_user_by_id(123).unwrap();
|
||||||
|
assert_eq!(user.id, 123);
|
||||||
|
assert_eq!(user.name, "mock_user");
|
||||||
|
assert_eq!(user.email, "mock@example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_character_repository_mock() {
|
||||||
|
// Create a mock CharacterRepository
|
||||||
|
let mut mock_character_repo = MockCharacterRepository::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_character_repo
|
||||||
|
.expect_get_character_by_id()
|
||||||
|
.with(eq(456))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(database_service::characters::Character {
|
||||||
|
id: 456,
|
||||||
|
user_id: "123".to_string(),
|
||||||
|
name: "mock_character".to_string(),
|
||||||
|
money: 1000,
|
||||||
|
inventory: serde_json::json!({}),
|
||||||
|
stats: serde_json::json!({}),
|
||||||
|
skills: serde_json::json!({}),
|
||||||
|
looks: serde_json::json!({}),
|
||||||
|
position: serde_json::json!({}),
|
||||||
|
created_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||||
|
updated_at: NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||||
|
deleted_at: None,
|
||||||
|
is_active: true,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test the mock
|
||||||
|
let character = mock_character_repo.get_character_by_id(456).unwrap();
|
||||||
|
assert_eq!(character.id, 456);
|
||||||
|
assert_eq!(character.name, "mock_character");
|
||||||
|
assert_eq!(character.user_id, "123");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_session_repository_mock() {
|
||||||
|
// Create a mock SessionRepository
|
||||||
|
let mut mock_session_repo = MockSessionRepository::new();
|
||||||
|
|
||||||
|
// Set up expectations
|
||||||
|
mock_session_repo
|
||||||
|
.expect_get_session()
|
||||||
|
.with(eq("session123"))
|
||||||
|
.times(1)
|
||||||
|
.returning(|_| {
|
||||||
|
Ok(database_service::sessions::Session {
|
||||||
|
id: "session123".to_string(),
|
||||||
|
user_id: "123".to_string(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test the mock
|
||||||
|
let session = mock_session_repo.get_session("session123").unwrap();
|
||||||
|
assert_eq!(session.id, "session123");
|
||||||
|
assert_eq!(session.user_id, "123");
|
||||||
|
}
|
||||||
151
tests/database-service/test_helpers.rs
Normal file
151
tests/database-service/test_helpers.rs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use utils::redis_cache::RedisCache;
|
||||||
|
|
||||||
|
// Helper function to create a test database pool
|
||||||
|
pub async fn setup_test_pool() -> Result<sqlx::PgPool, sqlx::Error> {
|
||||||
|
let database_url = std::env::var("TEST_DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/test_db".to_string());
|
||||||
|
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a mock Redis cache
|
||||||
|
pub fn setup_test_cache() -> Arc<Mutex<RedisCache>> {
|
||||||
|
let redis_url = std::env::var("TEST_REDIS_URL")
|
||||||
|
.unwrap_or_else(|_| "redis://localhost:6379".to_string());
|
||||||
|
|
||||||
|
Arc::new(Mutex::new(RedisCache::new(&redis_url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test user in the database
|
||||||
|
pub async fn create_test_user(pool: &sqlx::PgPool, name: &str, email: &str) -> i32 {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "user" (name, email, role, "createdAt", "updatedAt")
|
||||||
|
VALUES ($1, $2, 'user', NOW(), NOW())
|
||||||
|
RETURNING id
|
||||||
|
"#,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test user");
|
||||||
|
|
||||||
|
result.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test character in the database
|
||||||
|
pub async fn create_test_character(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
user_id: i32,
|
||||||
|
name: &str
|
||||||
|
) -> i32 {
|
||||||
|
let inventory = serde_json::json!({
|
||||||
|
"items": [],
|
||||||
|
"capacity": 100
|
||||||
|
});
|
||||||
|
|
||||||
|
let stats = serde_json::json!({
|
||||||
|
"strength": 10,
|
||||||
|
"dexterity": 10,
|
||||||
|
"intelligence": 10,
|
||||||
|
"vitality": 10
|
||||||
|
});
|
||||||
|
|
||||||
|
let skills = serde_json::json!({
|
||||||
|
"skills": []
|
||||||
|
});
|
||||||
|
|
||||||
|
let looks = serde_json::json!({
|
||||||
|
"race": 1,
|
||||||
|
"gender": 0,
|
||||||
|
"hair": 1,
|
||||||
|
"face": 1
|
||||||
|
});
|
||||||
|
|
||||||
|
let position = serde_json::json!({
|
||||||
|
"mapId": 1,
|
||||||
|
"x": 100.0,
|
||||||
|
"y": 100.0,
|
||||||
|
"z": 0.0
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO character (
|
||||||
|
"userId", name, money, inventory, stats, skills, looks, position,
|
||||||
|
"createdAt", "updatedAt", "isActive"
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, 0, $3, $4, $5, $6, $7, NOW(), NOW(), true)
|
||||||
|
RETURNING id
|
||||||
|
"#,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
inventory,
|
||||||
|
stats,
|
||||||
|
skills,
|
||||||
|
looks,
|
||||||
|
position
|
||||||
|
)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test character");
|
||||||
|
|
||||||
|
result.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a test session in the database
|
||||||
|
pub async fn create_test_session(pool: &sqlx::PgPool, user_id: i32) -> String {
|
||||||
|
let session_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO session (id, "userId", "createdAt", "expiresAt")
|
||||||
|
VALUES ($1, $2, NOW(), NOW() + INTERVAL '1 hour')
|
||||||
|
"#,
|
||||||
|
session_id,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create test session");
|
||||||
|
|
||||||
|
session_id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up test user data
|
||||||
|
pub async fn cleanup_test_user(pool: &sqlx::PgPool, user_id: i32) {
|
||||||
|
sqlx::query!(r#"DELETE FROM "user" WHERE id = $1"#, user_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete test user");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up test character data
|
||||||
|
pub async fn cleanup_test_character(pool: &sqlx::PgPool, character_id: i32) {
|
||||||
|
sqlx::query!(r#"DELETE FROM character WHERE id = $1"#, character_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete test character");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up test session data
|
||||||
|
pub async fn cleanup_test_session(pool: &sqlx::PgPool, session_id: &str) {
|
||||||
|
sqlx::query!(r#"DELETE FROM session WHERE id = $1"#, session_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to delete test session");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to clean up all test data
|
||||||
|
pub async fn cleanup_test_data(pool: &sqlx::PgPool, user_id: i32, character_id: i32, session_id: &str) {
|
||||||
|
cleanup_test_session(pool, session_id).await;
|
||||||
|
cleanup_test_character(pool, character_id).await;
|
||||||
|
cleanup_test_user(pool, user_id).await;
|
||||||
|
}
|
||||||
96
tests/packet-service/bufferpool_tests.rs
Normal file
96
tests/packet-service/bufferpool_tests.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use packet_service::bufferpool::BufferPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_buffer_pool_creation() {
|
||||||
|
let pool_size = 5;
|
||||||
|
let pool = BufferPool::new(pool_size);
|
||||||
|
|
||||||
|
// Verify we can acquire the expected number of buffers
|
||||||
|
for _ in 0..pool_size {
|
||||||
|
let buffer = pool.acquire().await;
|
||||||
|
assert!(buffer.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next acquire should return None since all buffers are in use
|
||||||
|
let buffer = pool.acquire().await;
|
||||||
|
assert!(buffer.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_buffer_pool_release() {
|
||||||
|
let pool_size = 3;
|
||||||
|
let pool = BufferPool::new(pool_size);
|
||||||
|
|
||||||
|
// Acquire all buffers
|
||||||
|
let mut buffers = Vec::new();
|
||||||
|
for _ in 0..pool_size {
|
||||||
|
let buffer = pool.acquire().await.unwrap();
|
||||||
|
buffers.push(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release one buffer
|
||||||
|
let buffer = buffers.pop().unwrap();
|
||||||
|
pool.release(buffer).await;
|
||||||
|
|
||||||
|
// We should be able to acquire one buffer now
|
||||||
|
let buffer = pool.acquire().await;
|
||||||
|
assert!(buffer.is_some());
|
||||||
|
|
||||||
|
// But not two
|
||||||
|
let buffer = pool.acquire().await;
|
||||||
|
assert!(buffer.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_buffer_pool_concurrent_access() {
|
||||||
|
let pool_size = 10;
|
||||||
|
let pool = Arc::new(BufferPool::new(pool_size));
|
||||||
|
|
||||||
|
// Spawn multiple tasks to acquire and release buffers
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..pool_size {
|
||||||
|
let pool_clone = pool.clone();
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
// Acquire a buffer
|
||||||
|
let mut buffer = pool_clone.acquire().await.unwrap();
|
||||||
|
|
||||||
|
// Write some data to the buffer
|
||||||
|
buffer[0] = i as u8;
|
||||||
|
|
||||||
|
// Simulate some work
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||||
|
|
||||||
|
// Release the buffer
|
||||||
|
pool_clone.release(buffer).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all tasks to complete
|
||||||
|
for handle in handles {
|
||||||
|
handle.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// All buffers should be available again
|
||||||
|
for _ in 0..pool_size {
|
||||||
|
let buffer = pool.acquire().await;
|
||||||
|
assert!(buffer.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_buffer_pool_buffer_size() {
|
||||||
|
let pool = BufferPool::new(1);
|
||||||
|
|
||||||
|
// Acquire a buffer
|
||||||
|
let buffer = pool.acquire().await.unwrap();
|
||||||
|
|
||||||
|
// Buffer should be large enough for maximum packet size
|
||||||
|
assert!(buffer.len() >= 0xFFF);
|
||||||
|
|
||||||
|
// Release the buffer
|
||||||
|
pool.release(buffer).await;
|
||||||
|
}
|
||||||
108
tests/packet-service/connection_service_tests.rs
Normal file
108
tests/packet-service/connection_service_tests.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use packet_service::connection_service::ConnectionService;
|
||||||
|
use packet_service::connection_state::ConnectionState;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_service_add_connection() {
|
||||||
|
let service = ConnectionService::new();
|
||||||
|
|
||||||
|
// Add a connection
|
||||||
|
let connection_id = service.add_connection();
|
||||||
|
|
||||||
|
// Verify the connection exists
|
||||||
|
let connection = service.get_connection(&connection_id);
|
||||||
|
assert!(connection.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_service_remove_connection() {
|
||||||
|
let service = ConnectionService::new();
|
||||||
|
|
||||||
|
// Add a connection
|
||||||
|
let connection_id = service.add_connection();
|
||||||
|
|
||||||
|
// Verify the connection exists
|
||||||
|
let connection = service.get_connection(&connection_id);
|
||||||
|
assert!(connection.is_some());
|
||||||
|
|
||||||
|
// Remove the connection
|
||||||
|
service.remove_connection(&connection_id);
|
||||||
|
|
||||||
|
// Verify the connection no longer exists
|
||||||
|
let connection = service.get_connection(&connection_id);
|
||||||
|
assert!(connection.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_service_get_connection_mut() {
|
||||||
|
let service = ConnectionService::new();
|
||||||
|
|
||||||
|
// Add a connection
|
||||||
|
let connection_id = service.add_connection();
|
||||||
|
|
||||||
|
// Get a mutable reference to the connection
|
||||||
|
let mut connection = service.get_connection_mut(&connection_id).unwrap();
|
||||||
|
|
||||||
|
// Modify the connection
|
||||||
|
connection.user_id = Some("test_user".to_string());
|
||||||
|
connection.session_id = Some("test_session".to_string());
|
||||||
|
connection.character_id = Some(123);
|
||||||
|
|
||||||
|
// Drop the mutable reference
|
||||||
|
drop(connection);
|
||||||
|
|
||||||
|
// Verify the changes were saved
|
||||||
|
let connection = service.get_connection(&connection_id).unwrap();
|
||||||
|
assert_eq!(connection.user_id, Some("test_user".to_string()));
|
||||||
|
assert_eq!(connection.session_id, Some("test_session".to_string()));
|
||||||
|
assert_eq!(connection.character_id, Some(123));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_service_multiple_connections() {
|
||||||
|
let service = ConnectionService::new();
|
||||||
|
|
||||||
|
// Add multiple connections
|
||||||
|
let connection_ids: Vec<String> = (0..10).map(|_| service.add_connection()).collect();
|
||||||
|
|
||||||
|
// Verify all connections exist
|
||||||
|
for connection_id in &connection_ids {
|
||||||
|
let connection = service.get_connection(connection_id);
|
||||||
|
assert!(connection.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all connection IDs are unique
|
||||||
|
let unique_ids: HashSet<String> = connection_ids.iter().cloned().collect();
|
||||||
|
assert_eq!(unique_ids.len(), connection_ids.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_state_new() {
|
||||||
|
let state = ConnectionState::new();
|
||||||
|
|
||||||
|
// Verify initial state
|
||||||
|
assert_eq!(state.user_id, None);
|
||||||
|
assert_eq!(state.session_id, None);
|
||||||
|
assert_eq!(state.character_id, None);
|
||||||
|
assert_eq!(state.character_list, None);
|
||||||
|
assert!(state.additional_data.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connection_state_additional_data() {
|
||||||
|
let mut state = ConnectionState::new();
|
||||||
|
|
||||||
|
// Add some additional data
|
||||||
|
state.additional_data.insert("key1".to_string(), "value1".to_string());
|
||||||
|
state.additional_data.insert("key2".to_string(), "value2".to_string());
|
||||||
|
|
||||||
|
// Verify the data was added
|
||||||
|
assert_eq!(state.additional_data.get("key1"), Some(&"value1".to_string()));
|
||||||
|
assert_eq!(state.additional_data.get("key2"), Some(&"value2".to_string()));
|
||||||
|
|
||||||
|
// Update a value
|
||||||
|
state.additional_data.insert("key1".to_string(), "updated".to_string());
|
||||||
|
|
||||||
|
// Verify the value was updated
|
||||||
|
assert_eq!(state.additional_data.get("key1"), Some(&"updated".to_string()));
|
||||||
|
}
|
||||||
98
tests/packet-service/packet_tests.rs
Normal file
98
tests/packet-service/packet_tests.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use packet_service::packet::{Packet, PacketPayload};
|
||||||
|
use packet_service::packet_type::PacketType;
|
||||||
|
|
||||||
|
// Define a test payload struct
|
||||||
|
#[derive(Debug, Encode, Decode, PartialEq)]
|
||||||
|
struct TestPayload {
|
||||||
|
id: u32,
|
||||||
|
name: String,
|
||||||
|
value: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PacketPayload for TestPayload {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_creation() {
|
||||||
|
let payload = TestPayload {
|
||||||
|
id: 123,
|
||||||
|
name: "test".to_string(),
|
||||||
|
value: 3.14,
|
||||||
|
};
|
||||||
|
|
||||||
|
let packet = Packet::new(PacketType::PakcsAlive, &payload).unwrap();
|
||||||
|
|
||||||
|
// Check packet fields
|
||||||
|
assert_eq!(packet.packet_type, PacketType::PakcsAlive);
|
||||||
|
assert_eq!(packet.packet_crc, 0); // CRC is currently not implemented
|
||||||
|
assert!(!packet.payload.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_serialization_deserialization() {
|
||||||
|
let original_payload = TestPayload {
|
||||||
|
id: 456,
|
||||||
|
name: "serialization_test".to_string(),
|
||||||
|
value: 2.71,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a packet
|
||||||
|
let packet = Packet::new(PacketType::PakcsAlive, &original_payload).unwrap();
|
||||||
|
|
||||||
|
// Serialize to raw bytes
|
||||||
|
let raw_data = packet.to_raw();
|
||||||
|
|
||||||
|
// Deserialize from raw bytes
|
||||||
|
let deserialized_packet = Packet::from_raw(&raw_data).unwrap();
|
||||||
|
|
||||||
|
// Check packet fields match
|
||||||
|
assert_eq!(deserialized_packet.packet_type, packet.packet_type);
|
||||||
|
assert_eq!(deserialized_packet.packet_size, packet.packet_size);
|
||||||
|
assert_eq!(deserialized_packet.packet_crc, packet.packet_crc);
|
||||||
|
|
||||||
|
// Parse the payload
|
||||||
|
let deserialized_payload: TestPayload = deserialized_packet.parse().unwrap();
|
||||||
|
|
||||||
|
// Check payload fields match
|
||||||
|
assert_eq!(deserialized_payload, original_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_from_raw_invalid_size() {
|
||||||
|
// Create a packet with invalid size (too small)
|
||||||
|
let raw_data = vec![0, 0, 0, 0]; // Only 4 bytes, less than minimum 6 bytes
|
||||||
|
|
||||||
|
let result = Packet::from_raw(&raw_data);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_from_raw_size_mismatch() {
|
||||||
|
// Create a packet with size mismatch
|
||||||
|
let mut raw_data = vec![0; 10]; // 10 bytes
|
||||||
|
|
||||||
|
// Set packet size to 20 (more than actual data)
|
||||||
|
raw_data[0] = 20;
|
||||||
|
raw_data[1] = 0;
|
||||||
|
|
||||||
|
let result = Packet::from_raw(&raw_data);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_packet_payload_encoding_decoding() {
|
||||||
|
let original_payload = TestPayload {
|
||||||
|
id: 789,
|
||||||
|
name: "encoding_test".to_string(),
|
||||||
|
value: 1.618,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encode payload
|
||||||
|
let encoded = bincode::encode_to_vec(&original_payload, bincode::config::standard()).unwrap();
|
||||||
|
|
||||||
|
// Decode payload
|
||||||
|
let decoded: TestPayload = bincode::decode_from_slice(&encoded, bincode::config::standard()).unwrap().0;
|
||||||
|
|
||||||
|
// Check payload fields match
|
||||||
|
assert_eq!(decoded, original_payload);
|
||||||
|
}
|
||||||
18
tests/src/lib.rs
Normal file
18
tests/src/lib.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// This file is required to make the tests directory a proper Rust library crate
|
||||||
|
// It will only be compiled when running tests
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test_utils {
|
||||||
|
// Common test utilities can go here
|
||||||
|
pub fn setup() {
|
||||||
|
// Common test setup code
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn teardown() {
|
||||||
|
// Common test teardown code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This ensures the crate is only compiled during testing
|
||||||
|
#[cfg(not(test))]
|
||||||
|
compile_error!("This crate is only meant to be used in test mode");
|
||||||
64
tests/utils/health_check_tests.rs
Normal file
64
tests/utils/health_check_tests.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use reqwest::StatusCode;
|
||||||
|
use std::env;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use utils::health_check::start_health_check;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check_endpoint() {
|
||||||
|
// Set a custom port for this test to avoid conflicts
|
||||||
|
env::set_var("HEALTH_CHECK_PORT", "8099");
|
||||||
|
|
||||||
|
// Start the health check endpoint
|
||||||
|
let result = start_health_check("127.0.0.1").await;
|
||||||
|
assert!(result.is_ok(), "Failed to start health check: {:?}", result.err());
|
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
// Make a request to the health check endpoint
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client.get("http://127.0.0.1:8099/health")
|
||||||
|
.timeout(Duration::from_secs(2))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Verify the response
|
||||||
|
assert!(response.is_ok(), "Failed to connect to health check endpoint: {:?}", response.err());
|
||||||
|
let response = response.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let body = response.text().await.unwrap();
|
||||||
|
assert_eq!(body, "OK");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
env::remove_var("HEALTH_CHECK_PORT");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check_invalid_path() {
|
||||||
|
// Set a custom port for this test to avoid conflicts
|
||||||
|
env::set_var("HEALTH_CHECK_PORT", "8098");
|
||||||
|
|
||||||
|
// Start the health check endpoint
|
||||||
|
let result = start_health_check("127.0.0.1").await;
|
||||||
|
assert!(result.is_ok(), "Failed to start health check: {:?}", result.err());
|
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
// Make a request to an invalid path
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client.get("http://127.0.0.1:8098/invalid")
|
||||||
|
.timeout(Duration::from_secs(2))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Verify the response
|
||||||
|
assert!(response.is_ok(), "Failed to connect to health check endpoint: {:?}", response.err());
|
||||||
|
let response = response.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
env::remove_var("HEALTH_CHECK_PORT");
|
||||||
|
}
|
||||||
42
tests/utils/logging_tests.rs
Normal file
42
tests/utils/logging_tests.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use std::env;
|
||||||
|
use utils::logging::setup_logging;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_logging_setup() {
|
||||||
|
// Test with default log level
|
||||||
|
env::remove_var("LOG_LEVEL");
|
||||||
|
setup_logging("test_app", &["test_crate"]);
|
||||||
|
|
||||||
|
// Test with custom log level
|
||||||
|
env::set_var("LOG_LEVEL", "debug");
|
||||||
|
setup_logging("test_app", &["test_crate"]);
|
||||||
|
|
||||||
|
// Test with invalid log level (should default to info)
|
||||||
|
env::set_var("LOG_LEVEL", "invalid_level");
|
||||||
|
setup_logging("test_app", &["test_crate"]);
|
||||||
|
|
||||||
|
// Test with multiple additional crates
|
||||||
|
setup_logging("test_app", &["test_crate1", "test_crate2", "test_crate3"]);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
env::remove_var("LOG_LEVEL");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_logging_output() {
|
||||||
|
// This test is more of a smoke test to ensure logging doesn't panic
|
||||||
|
// Actual log output verification would require capturing stdout/stderr
|
||||||
|
|
||||||
|
env::set_var("LOG_LEVEL", "trace");
|
||||||
|
setup_logging("test_logging", &[]);
|
||||||
|
|
||||||
|
// Log at different levels
|
||||||
|
tracing::error!("This is an error message");
|
||||||
|
tracing::warn!("This is a warning message");
|
||||||
|
tracing::info!("This is an info message");
|
||||||
|
tracing::debug!("This is a debug message");
|
||||||
|
tracing::trace!("This is a trace message");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
env::remove_var("LOG_LEVEL");
|
||||||
|
}
|
||||||
151
tests/utils/multi_service_load_balancer_tests.rs
Normal file
151
tests/utils/multi_service_load_balancer_tests.rs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
use utils::multi_service_load_balancer::{LoadBalancingStrategy, MultiServiceLoadBalancer, ServiceId};
|
||||||
|
|
||||||
|
// Mock implementation for testing without actual service discovery
|
||||||
|
mod mock {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
// Mock version of the load balancer for testing
|
||||||
|
pub struct MockMultiServiceLoadBalancer {
|
||||||
|
strategy: LoadBalancingStrategy,
|
||||||
|
services: Arc<Mutex<HashMap<ServiceId, Vec<SocketAddr>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MockMultiServiceLoadBalancer {
|
||||||
|
pub fn new(strategy: LoadBalancingStrategy) -> Self {
|
||||||
|
MockMultiServiceLoadBalancer {
|
||||||
|
strategy,
|
||||||
|
services: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_service(&self, service_name: &str, service_protocol: &str, endpoints: Vec<SocketAddr>) {
|
||||||
|
let service_id = ServiceId::new(service_name, service_protocol);
|
||||||
|
let mut services = self.services.lock().unwrap();
|
||||||
|
services.insert(service_id, endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_endpoint(&self, service_name: &str, service_protocol: &str) -> Option<SocketAddr> {
|
||||||
|
let service_id = ServiceId::new(service_name, service_protocol);
|
||||||
|
let services = self.services.lock().unwrap();
|
||||||
|
|
||||||
|
if let Some(endpoints) = services.get(&service_id) {
|
||||||
|
if endpoints.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.strategy {
|
||||||
|
LoadBalancingStrategy::Random => {
|
||||||
|
let index = rand::random::<usize>() % endpoints.len();
|
||||||
|
Some(endpoints[index])
|
||||||
|
},
|
||||||
|
LoadBalancingStrategy::RoundRobin => {
|
||||||
|
// For simplicity in tests, just return the first endpoint
|
||||||
|
Some(endpoints[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_id() {
|
||||||
|
let service_id1 = ServiceId::new("service1", "http");
|
||||||
|
let service_id2 = ServiceId::new("service1", "http");
|
||||||
|
let service_id3 = ServiceId::new("service2", "http");
|
||||||
|
let service_id4 = ServiceId::new("service1", "https");
|
||||||
|
|
||||||
|
// Test equality
|
||||||
|
assert_eq!(service_id1, service_id2);
|
||||||
|
assert_ne!(service_id1, service_id3);
|
||||||
|
assert_ne!(service_id1, service_id4);
|
||||||
|
|
||||||
|
// Test hash implementation
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
set.insert(service_id1);
|
||||||
|
assert!(set.contains(&service_id2));
|
||||||
|
assert!(!set.contains(&service_id3));
|
||||||
|
assert!(!set.contains(&service_id4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mock_load_balancer_random() {
|
||||||
|
let lb = mock::MockMultiServiceLoadBalancer::new(LoadBalancingStrategy::Random);
|
||||||
|
|
||||||
|
// Add a service with multiple endpoints
|
||||||
|
let endpoints = vec![
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8080),
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 3)), 8080),
|
||||||
|
];
|
||||||
|
lb.add_service("test-service", "http", endpoints.clone());
|
||||||
|
|
||||||
|
// Get an endpoint
|
||||||
|
let endpoint = lb.get_endpoint("test-service", "http");
|
||||||
|
assert!(endpoint.is_some());
|
||||||
|
assert!(endpoints.contains(&endpoint.unwrap()));
|
||||||
|
|
||||||
|
// Test non-existent service
|
||||||
|
let endpoint = lb.get_endpoint("non-existent", "http");
|
||||||
|
assert!(endpoint.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mock_load_balancer_round_robin() {
|
||||||
|
let lb = mock::MockMultiServiceLoadBalancer::new(LoadBalancingStrategy::RoundRobin);
|
||||||
|
|
||||||
|
// Add a service with multiple endpoints
|
||||||
|
let endpoints = vec![
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8080),
|
||||||
|
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 3)), 8080),
|
||||||
|
];
|
||||||
|
lb.add_service("test-service", "http", endpoints);
|
||||||
|
|
||||||
|
// Get an endpoint
|
||||||
|
let endpoint = lb.get_endpoint("test-service", "http");
|
||||||
|
assert!(endpoint.is_some());
|
||||||
|
|
||||||
|
// Test empty service
|
||||||
|
lb.add_service("empty-service", "http", vec![]);
|
||||||
|
let endpoint = lb.get_endpoint("empty-service", "http");
|
||||||
|
assert!(endpoint.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration test with the actual MultiServiceLoadBalancer
|
||||||
|
// This test is disabled by default as it requires a Consul server
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_multi_service_load_balancer() {
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
// Skip test if CONSUL_TEST_ENABLED is not set to true
|
||||||
|
if env::var("CONSUL_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping MultiServiceLoadBalancer test. Set CONSUL_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let consul_url = env::var("TEST_CONSUL_URL").unwrap_or_else(|_| "http://localhost:8500".to_string());
|
||||||
|
let service_name = env::var("TEST_CONSUL_SERVICE_NAME").unwrap_or_else(|_| "database-service".to_string());
|
||||||
|
let protocol = "tcp";
|
||||||
|
|
||||||
|
let lb = MultiServiceLoadBalancer::new(&consul_url, LoadBalancingStrategy::Random);
|
||||||
|
|
||||||
|
// Refresh service endpoints
|
||||||
|
let result = lb.refresh_service_endpoints(&service_name, protocol).await;
|
||||||
|
assert!(result.is_ok(), "Failed to refresh service endpoints: {:?}", result.err());
|
||||||
|
|
||||||
|
// Get an endpoint
|
||||||
|
let result = lb.get_endpoint(&service_name, protocol).await;
|
||||||
|
assert!(result.is_ok(), "Failed to get endpoint: {:?}", result.err());
|
||||||
|
|
||||||
|
let endpoint = result.unwrap();
|
||||||
|
assert!(endpoint.is_some(), "No endpoint found for service {}", service_name);
|
||||||
|
|
||||||
|
println!("Found endpoint for service {}: {:?}", service_name, endpoint);
|
||||||
|
}
|
||||||
152
tests/utils/redis_cache_tests.rs
Normal file
152
tests/utils/redis_cache_tests.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use utils::redis_cache::{Cache, RedisCache};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
struct TestData {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_redis_cache_set_get() {
|
||||||
|
// Skip test if REDIS_TEST_ENABLED is not set to true
|
||||||
|
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||||
|
let cache = RedisCache::new(&redis_url);
|
||||||
|
|
||||||
|
let key = "test_key_set_get".to_string();
|
||||||
|
let test_data = TestData {
|
||||||
|
id: 1,
|
||||||
|
name: "test".to_string(),
|
||||||
|
value: 3.14,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test setting a value
|
||||||
|
let set_result = cache.set(&key, &test_data, 10).await;
|
||||||
|
assert!(set_result.is_ok(), "Failed to set value: {:?}", set_result.err());
|
||||||
|
|
||||||
|
// Test getting the value
|
||||||
|
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
|
||||||
|
assert!(get_result.is_ok(), "Failed to get value: {:?}", get_result.err());
|
||||||
|
|
||||||
|
let retrieved_data = get_result.unwrap();
|
||||||
|
assert!(retrieved_data.is_some(), "Retrieved data is None");
|
||||||
|
assert_eq!(retrieved_data.unwrap(), test_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_redis_cache_update() {
|
||||||
|
// Skip test if REDIS_TEST_ENABLED is not set to true
|
||||||
|
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||||
|
let cache = RedisCache::new(&redis_url);
|
||||||
|
|
||||||
|
let key = "test_key_update".to_string();
|
||||||
|
let initial_data = TestData {
|
||||||
|
id: 2,
|
||||||
|
name: "initial".to_string(),
|
||||||
|
value: 2.71,
|
||||||
|
};
|
||||||
|
let updated_data = TestData {
|
||||||
|
id: 2,
|
||||||
|
name: "updated".to_string(),
|
||||||
|
value: 2.71,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set initial value
|
||||||
|
let set_result = cache.set(&key, &initial_data, 10).await;
|
||||||
|
assert!(set_result.is_ok(), "Failed to set initial value: {:?}", set_result.err());
|
||||||
|
|
||||||
|
// Update the value
|
||||||
|
let update_result = cache.update(&key, Some(&updated_data), Some(10)).await;
|
||||||
|
assert!(update_result.is_ok(), "Failed to update value: {:?}", update_result.err());
|
||||||
|
|
||||||
|
// Get the updated value
|
||||||
|
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
|
||||||
|
assert!(get_result.is_ok(), "Failed to get updated value: {:?}", get_result.err());
|
||||||
|
|
||||||
|
let retrieved_data = get_result.unwrap();
|
||||||
|
assert!(retrieved_data.is_some(), "Retrieved data is None");
|
||||||
|
assert_eq!(retrieved_data.unwrap(), updated_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_redis_cache_delete() {
|
||||||
|
// Skip test if REDIS_TEST_ENABLED is not set to true
|
||||||
|
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||||
|
let mut cache = RedisCache::new(&redis_url);
|
||||||
|
|
||||||
|
let key = "test_key_delete".to_string();
|
||||||
|
let test_data = TestData {
|
||||||
|
id: 3,
|
||||||
|
name: "delete_test".to_string(),
|
||||||
|
value: 1.618,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a value
|
||||||
|
let set_result = cache.set(&key, &test_data, 10).await;
|
||||||
|
assert!(set_result.is_ok(), "Failed to set value: {:?}", set_result.err());
|
||||||
|
|
||||||
|
// Delete the value
|
||||||
|
let delete_result = cache.delete(&key).await;
|
||||||
|
assert!(delete_result.is_ok(), "Failed to delete value: {:?}", delete_result.err());
|
||||||
|
|
||||||
|
// Verify the value is gone
|
||||||
|
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
|
||||||
|
assert!(get_result.is_ok(), "Failed to get value after deletion: {:?}", get_result.err());
|
||||||
|
assert!(get_result.unwrap().is_none(), "Value still exists after deletion");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_redis_cache_refresh() {
|
||||||
|
// Skip test if REDIS_TEST_ENABLED is not set to true
|
||||||
|
if env::var("REDIS_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping Redis test. Set REDIS_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let redis_url = env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
|
||||||
|
let cache = RedisCache::new(&redis_url);
|
||||||
|
|
||||||
|
let key = "test_key_refresh".to_string();
|
||||||
|
let test_data = TestData {
|
||||||
|
id: 4,
|
||||||
|
name: "refresh_test".to_string(),
|
||||||
|
value: 0.577,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a value with a short TTL
|
||||||
|
let set_result = cache.set(&key, &test_data, 5).await;
|
||||||
|
assert!(set_result.is_ok(), "Failed to set value: {:?}", set_result.err());
|
||||||
|
|
||||||
|
// Refresh the TTL
|
||||||
|
let refresh_result = cache.refresh(&key, 30).await;
|
||||||
|
assert!(refresh_result.is_ok(), "Failed to refresh TTL: {:?}", refresh_result.err());
|
||||||
|
|
||||||
|
// Wait for the original TTL to expire
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(6)).await;
|
||||||
|
|
||||||
|
// Verify the value still exists due to the refresh
|
||||||
|
let get_result: Result<Option<TestData>, _> = cache.get(&key).await;
|
||||||
|
assert!(get_result.is_ok(), "Failed to get value after refresh: {:?}", get_result.err());
|
||||||
|
|
||||||
|
let retrieved_data = get_result.unwrap();
|
||||||
|
assert!(retrieved_data.is_some(), "Value expired despite TTL refresh");
|
||||||
|
assert_eq!(retrieved_data.unwrap(), test_data);
|
||||||
|
}
|
||||||
84
tests/utils/service_discovery_tests.rs
Normal file
84
tests/utils/service_discovery_tests.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_kube_service_endpoints_by_dns() {
|
||||||
|
// Skip test if KUBE_TEST_ENABLED is not set to true
|
||||||
|
if env::var("KUBE_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping Kubernetes DNS test. Set KUBE_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a known Kubernetes service
|
||||||
|
let service_name = env::var("TEST_K8S_SERVICE_NAME").unwrap_or_else(|_| "database-service".to_string());
|
||||||
|
let port_name = env::var("TEST_K8S_PORT_NAME").unwrap_or_else(|_| "database-service".to_string());
|
||||||
|
let protocol = "tcp";
|
||||||
|
|
||||||
|
let result = get_kube_service_endpoints_by_dns(&port_name, protocol, &service_name).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Failed to get Kubernetes service endpoints: {:?}", result.err());
|
||||||
|
|
||||||
|
let endpoints = result.unwrap();
|
||||||
|
assert!(!endpoints.is_empty(), "No endpoints found for service {}", service_name);
|
||||||
|
|
||||||
|
// Verify that the endpoints are valid socket addresses
|
||||||
|
for endpoint in &endpoints {
|
||||||
|
assert!(endpoint.port() > 0, "Invalid port in endpoint: {}", endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Found {} endpoints for service {}: {:?}", endpoints.len(), service_name, endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_service_endpoints_by_dns() {
|
||||||
|
// Skip test if CONSUL_TEST_ENABLED is not set to true
|
||||||
|
if env::var("CONSUL_TEST_ENABLED").unwrap_or_else(|_| "false".to_string()) != "true" {
|
||||||
|
println!("Skipping Consul DNS test. Set CONSUL_TEST_ENABLED=true to run.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a known Consul service
|
||||||
|
let consul_url = env::var("TEST_CONSUL_URL").unwrap_or_else(|_| "127.0.0.1:8600".to_string());
|
||||||
|
let service_name = env::var("TEST_CONSUL_SERVICE_NAME").unwrap_or_else(|_| "database-service".to_string());
|
||||||
|
let protocol = "tcp";
|
||||||
|
|
||||||
|
let result = get_service_endpoints_by_dns(&consul_url, protocol, &service_name).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok(), "Failed to get Consul service endpoints: {:?}", result.err());
|
||||||
|
|
||||||
|
let endpoints = result.unwrap();
|
||||||
|
assert!(!endpoints.is_empty(), "No endpoints found for service {}", service_name);
|
||||||
|
|
||||||
|
// Verify that the endpoints are valid socket addresses
|
||||||
|
for endpoint in &endpoints {
|
||||||
|
assert!(endpoint.port() > 0, "Invalid port in endpoint: {}", endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Found {} endpoints for service {}: {:?}", endpoints.len(), service_name, endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock tests that don't require actual infrastructure
|
||||||
|
mod mock_tests {
|
||||||
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_socket_addr_parsing() {
|
||||||
|
// Test that we can parse socket addresses correctly
|
||||||
|
let addr_str = "127.0.0.1:8080";
|
||||||
|
let addr = SocketAddr::from_str(addr_str).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||||
|
assert_eq!(addr.port(), 8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_socket_addr_formatting() {
|
||||||
|
// Test that we can format socket addresses correctly
|
||||||
|
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 9000);
|
||||||
|
let addr_str = format!("{}", addr);
|
||||||
|
|
||||||
|
assert_eq!(addr_str, "192.168.1.1:9000");
|
||||||
|
}
|
||||||
|
}
|
||||||
81
utils/README.md
Normal file
81
utils/README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Utils Module
|
||||||
|
|
||||||
|
This module provides shared utilities used by all microservices in the MMORPG server architecture.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Service Discovery
|
||||||
|
|
||||||
|
The `service_discovery.rs` module provides functionality for discovering services in both Kubernetes and Consul environments:
|
||||||
|
|
||||||
|
- `get_service_endpoints_by_dns`: Discovers service endpoints using Consul DNS
|
||||||
|
- `get_kube_service_endpoints_by_dns`: Discovers service endpoints using Kubernetes DNS
|
||||||
|
- `get_service_info`: Retrieves detailed information about a Kubernetes service
|
||||||
|
- `get_services_by_label`: Finds Kubernetes services matching specific labels
|
||||||
|
|
||||||
|
### Redis Cache
|
||||||
|
|
||||||
|
The `redis_cache.rs` module provides a caching layer using Redis:
|
||||||
|
|
||||||
|
- Implements the `Cache` trait for standardized cache operations
|
||||||
|
- Provides methods for getting, setting, and deleting cached values
|
||||||
|
- Supports TTL (Time To Live) for cached entries
|
||||||
|
|
||||||
|
### Multi-Service Load Balancer
|
||||||
|
|
||||||
|
The `multi_service_load_balancer.rs` module provides load balancing across multiple service instances:
|
||||||
|
|
||||||
|
- Supports Random and Round-Robin load balancing strategies
|
||||||
|
- Dynamically refreshes service endpoints
|
||||||
|
- Provides failover capabilities
|
||||||
|
|
||||||
|
### Signal Handler
|
||||||
|
|
||||||
|
The `signal_handler.rs` module provides graceful shutdown capabilities:
|
||||||
|
|
||||||
|
- `wait_for_signal`: Waits for termination signals (SIGINT, SIGTERM)
|
||||||
|
- Cross-platform support for Unix and Windows signals
|
||||||
|
|
||||||
|
### Consul Registration
|
||||||
|
|
||||||
|
The `consul_registration.rs` module provides service registration with Consul:
|
||||||
|
|
||||||
|
- `register_service`: Registers a service with Consul
|
||||||
|
- `generate_service_id`: Generates unique service IDs
|
||||||
|
- `get_or_generate_service_id`: Retrieves or creates service IDs
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
The `health_check.rs` module provides HTTP health check endpoints:
|
||||||
|
|
||||||
|
- `start_health_check`: Starts a health check endpoint on a specified port
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
The `logging.rs` module provides standardized logging setup:
|
||||||
|
|
||||||
|
- `setup_logging`: Configures tracing with appropriate log levels
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Import the required utilities in your service:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use utils::logging;
|
||||||
|
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||||
|
use utils::signal_handler::wait_for_signal;
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
|
logging::setup_logging("my-service", &["my_service"]);
|
||||||
|
|
||||||
|
// Discover services
|
||||||
|
let db_url = format!("http://{}",
|
||||||
|
get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service")
|
||||||
|
.await?
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for termination signal
|
||||||
|
wait_for_signal().await;
|
||||||
|
```
|
||||||
@@ -88,19 +88,11 @@ pub async fn register_service(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn deregister_service(
|
pub async fn deregister_service(consul_url: &str, service_id: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
consul_url: &str,
|
|
||||||
service_id: &str,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let consul_deregister_url =
|
let consul_deregister_url = format!("{}/v1/agent/service/deregister/{}", consul_url, service_id);
|
||||||
format!("{}/v1/agent/service/deregister/{}", consul_url, service_id);
|
|
||||||
|
|
||||||
client
|
client.put(&consul_deregister_url).send().await?.error_for_status()?; // Ensure response is successful
|
||||||
.put(&consul_deregister_url)
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.error_for_status()?; // Ensure response is successful
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,7 @@ pub async fn start_health_check(service_address: &str) -> Result<(), Box<dyn std
|
|||||||
.map(|| warp::reply::with_status("OK", warp::http::StatusCode::OK))
|
.map(|| warp::reply::with_status("OK", warp::http::StatusCode::OK))
|
||||||
.with(log);
|
.with(log);
|
||||||
|
|
||||||
tokio::spawn(
|
tokio::spawn(warp::serve(health_route).run(health_check_endpoint_addr.to_socket_addrs()?.next().unwrap()));
|
||||||
warp::serve(health_route).run(
|
|
||||||
health_check_endpoint_addr
|
|
||||||
.to_socket_addrs()?
|
|
||||||
.next()
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
pub mod consul_registration;
|
pub mod consul_registration;
|
||||||
|
pub mod health_check;
|
||||||
|
pub mod logging;
|
||||||
|
pub mod multi_service_load_balancer;
|
||||||
pub mod null_string;
|
pub mod null_string;
|
||||||
pub mod redis_cache;
|
pub mod redis_cache;
|
||||||
pub mod service_discovery;
|
pub mod service_discovery;
|
||||||
pub mod signal_handler;
|
pub mod signal_handler;
|
||||||
pub mod multi_service_load_balancer;
|
|
||||||
pub mod logging;
|
|
||||||
pub mod health_check;
|
|
||||||
|
|||||||
@@ -6,17 +6,15 @@ pub fn setup_logging(app_name: &str, additional_crates: &[&str]) {
|
|||||||
let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
|
let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
|
||||||
|
|
||||||
let mut filter_string = format!("{app_name}={log_level},utils={log_level}");
|
let mut filter_string = format!("{app_name}={log_level},utils={log_level}");
|
||||||
for (crate_name) in additional_crates {
|
for &crate_name in additional_crates {
|
||||||
filter_string.push(',');
|
filter_string.push(',');
|
||||||
filter_string.push_str(&format!("{crate_name}={log_level}"));
|
filter_string.push_str(&format!("{crate_name}={log_level}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter = EnvFilter::try_new(filter_string)
|
let filter =
|
||||||
.unwrap_or_else(|_| EnvFilter::new(format!("{app_name}=info,utils=info")));
|
EnvFilter::try_new(filter_string).unwrap_or_else(|_| EnvFilter::new(format!("{app_name}=info,utils=info")));
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||||
.with_env_filter(filter)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
error!("Error messages enabled");
|
error!("Error messages enabled");
|
||||||
warn!("Warning messages enabled");
|
warn!("Warning messages enabled");
|
||||||
|
|||||||
@@ -1,135 +1,132 @@
|
|||||||
|
use crate::service_discovery::get_service_endpoints_by_dns;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use crate::service_discovery::get_service_endpoints_by_dns;
|
|
||||||
|
|
||||||
pub enum LoadBalancingStrategy {
|
pub enum LoadBalancingStrategy {
|
||||||
Random,
|
Random,
|
||||||
RoundRobin,
|
RoundRobin,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service identifier
|
// Service identifier
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct ServiceId {
|
pub struct ServiceId {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub protocol: String,
|
pub protocol: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceId {
|
impl ServiceId {
|
||||||
pub fn new(name: &str, protocol: &str) -> Self {
|
pub fn new(name: &str, protocol: &str) -> Self {
|
||||||
ServiceId {
|
ServiceId {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
protocol: protocol.to_string(),
|
protocol: protocol.to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-service state
|
// Per-service state
|
||||||
struct ServiceState {
|
struct ServiceState {
|
||||||
endpoints: Vec<SocketAddr>,
|
endpoints: Vec<SocketAddr>,
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceState {
|
impl ServiceState {
|
||||||
fn new(endpoints: Vec<SocketAddr>) -> Self {
|
fn new(endpoints: Vec<SocketAddr>) -> Self {
|
||||||
ServiceState {
|
ServiceState {
|
||||||
endpoints,
|
endpoints,
|
||||||
current_index: 0,
|
current_index: 0,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn get_endpoint(&mut self, strategy: &LoadBalancingStrategy) -> Option<SocketAddr> {
|
|
||||||
if self.endpoints.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match strategy {
|
fn get_endpoint(&mut self, strategy: &LoadBalancingStrategy) -> Option<SocketAddr> {
|
||||||
LoadBalancingStrategy::Random => {
|
if self.endpoints.is_empty() {
|
||||||
let mut rng = rand::thread_rng();
|
return None;
|
||||||
self.endpoints.choose(&mut rng).copied()
|
}
|
||||||
}
|
|
||||||
LoadBalancingStrategy::RoundRobin => {
|
match strategy {
|
||||||
let endpoint = self.endpoints[self.current_index].clone();
|
LoadBalancingStrategy::Random => {
|
||||||
self.current_index = (self.current_index + 1) % self.endpoints.len();
|
let mut rng = rand::thread_rng();
|
||||||
Some(endpoint)
|
self.endpoints.choose(&mut rng).copied()
|
||||||
}
|
}
|
||||||
|
LoadBalancingStrategy::RoundRobin => {
|
||||||
|
let endpoint = self.endpoints[self.current_index].clone();
|
||||||
|
self.current_index = (self.current_index + 1) % self.endpoints.len();
|
||||||
|
Some(endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MultiServiceLoadBalancer {
|
pub struct MultiServiceLoadBalancer {
|
||||||
consul_url: String,
|
consul_url: String,
|
||||||
strategy: LoadBalancingStrategy,
|
strategy: LoadBalancingStrategy,
|
||||||
services: Arc<Mutex<HashMap<ServiceId, ServiceState>>>,
|
services: Arc<Mutex<HashMap<ServiceId, ServiceState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MultiServiceLoadBalancer {
|
impl MultiServiceLoadBalancer {
|
||||||
pub fn new(consul_url: &str, strategy: LoadBalancingStrategy) -> Self {
|
pub fn new(consul_url: &str, strategy: LoadBalancingStrategy) -> Self {
|
||||||
MultiServiceLoadBalancer {
|
MultiServiceLoadBalancer {
|
||||||
consul_url: consul_url.to_string(),
|
consul_url: consul_url.to_string(),
|
||||||
strategy,
|
strategy,
|
||||||
services: Arc::new(Mutex::new(HashMap::new())),
|
services: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_endpoint(
|
|
||||||
&self,
|
|
||||||
service_name: &str,
|
|
||||||
service_protocol: &str,
|
|
||||||
) -> Result<Option<SocketAddr>, Box<dyn std::error::Error>> {
|
|
||||||
let service_id = ServiceId::new(service_name, service_protocol);
|
|
||||||
|
|
||||||
// Try to get an endpoint from the cache first
|
|
||||||
{
|
|
||||||
let mut services = self.services.lock().unwrap();
|
|
||||||
if let Some(service_state) = services.get_mut(&service_id) {
|
|
||||||
if let Some(endpoint) = service_state.get_endpoint(&self.strategy) {
|
|
||||||
return Ok(Some(endpoint));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have endpoints or they're all unavailable, refresh them
|
pub async fn get_endpoint(
|
||||||
self.refresh_service_endpoints(service_name, service_protocol).await?;
|
&self,
|
||||||
|
service_name: &str,
|
||||||
|
service_protocol: &str,
|
||||||
|
) -> Result<Option<SocketAddr>, Box<dyn std::error::Error>> {
|
||||||
|
let service_id = ServiceId::new(service_name, service_protocol);
|
||||||
|
|
||||||
// Try again after refresh
|
// Try to get an endpoint from the cache first
|
||||||
let mut services = self.services.lock().unwrap();
|
{
|
||||||
if let Some(service_state) = services.get_mut(&service_id) {
|
let mut services = self.services.lock().unwrap();
|
||||||
return Ok(service_state.get_endpoint(&self.strategy));
|
if let Some(service_state) = services.get_mut(&service_id) {
|
||||||
|
if let Some(endpoint) = service_state.get_endpoint(&self.strategy) {
|
||||||
|
return Ok(Some(endpoint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have endpoints or they're all unavailable, refresh them
|
||||||
|
self.refresh_service_endpoints(service_name, service_protocol).await?;
|
||||||
|
|
||||||
|
// Try again after refresh
|
||||||
|
let mut services = self.services.lock().unwrap();
|
||||||
|
if let Some(service_state) = services.get_mut(&service_id) {
|
||||||
|
return Ok(service_state.get_endpoint(&self.strategy));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
pub async fn refresh_service_endpoints(
|
||||||
}
|
&self,
|
||||||
|
service_name: &str,
|
||||||
|
service_protocol: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let endpoints = get_service_endpoints_by_dns(&self.consul_url, service_protocol, service_name).await?;
|
||||||
|
|
||||||
pub async fn refresh_service_endpoints(
|
let service_id = ServiceId::new(service_name, service_protocol);
|
||||||
&self,
|
let mut services = self.services.lock().unwrap();
|
||||||
service_name: &str,
|
|
||||||
service_protocol: &str,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let endpoints = get_service_endpoints_by_dns(
|
|
||||||
&self.consul_url,
|
|
||||||
service_protocol,
|
|
||||||
service_name,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
let service_id = ServiceId::new(service_name, service_protocol);
|
services.insert(service_id, ServiceState::new(endpoints));
|
||||||
let mut services = self.services.lock().unwrap();
|
Ok(())
|
||||||
|
|
||||||
services.insert(service_id, ServiceState::new(endpoints));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_all_services(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let service_ids: Vec<ServiceId> = {
|
|
||||||
let services = self.services.lock().unwrap();
|
|
||||||
services.keys().cloned().collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
for service_id in service_ids {
|
|
||||||
self.refresh_service_endpoints(&service_id.name, &service_id.protocol).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
pub async fn refresh_all_services(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
let service_ids: Vec<ServiceId> = {
|
||||||
|
let services = self.services.lock().unwrap();
|
||||||
|
services.keys().cloned().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
for service_id in service_ids {
|
||||||
|
self.refresh_service_endpoints(&service_id.name, &service_id.protocol)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use deadpool_redis::{Config, Pool, Runtime};
|
use deadpool_redis::{Config, Pool, Runtime};
|
||||||
use redis::{AsyncCommands, Commands, RedisError};
|
use redis::{AsyncCommands, RedisError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Cache {
|
pub trait Cache {
|
||||||
async fn set<T: Serialize + Send + Sync>(
|
async fn set<T: Serialize + Send + Sync>(&self, key: &String, value: &T, ttl: u64)
|
||||||
&self,
|
-> Result<(), redis::RedisError>;
|
||||||
key: &String,
|
|
||||||
value: &T,
|
|
||||||
ttl: u64,
|
|
||||||
) -> Result<(), redis::RedisError>;
|
|
||||||
|
|
||||||
async fn update<T: Serialize + Send + Sync>(
|
async fn update<T: Serialize + Send + Sync>(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
use hickory_resolver::config::*;
|
use hickory_resolver::config::*;
|
||||||
use hickory_resolver::{Resolver, TokioAsyncResolver};
|
use hickory_resolver::system_conf::read_system_conf;
|
||||||
|
use hickory_resolver::{TokioAsyncResolver};
|
||||||
|
use k8s_openapi::api::core::v1::Service;
|
||||||
|
use kube::{Api, Client};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use kube::{Client, Api};
|
|
||||||
use k8s_openapi::api::core::v1::Service;
|
|
||||||
use std::collections::{BTreeMap};
|
|
||||||
use hickory_resolver::system_conf::read_system_conf;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
pub async fn get_service_endpoints_by_dns(consul_url: &str, service_protocol: &str, service_name: &str) -> Result<Vec<SocketAddr>, Box<dyn std::error::Error>> {
|
pub async fn get_service_endpoints_by_dns(
|
||||||
|
consul_url: &str,
|
||||||
|
service_protocol: &str,
|
||||||
|
service_name: &str,
|
||||||
|
) -> Result<Vec<SocketAddr>, Box<dyn std::error::Error>> {
|
||||||
let mut rc = ResolverConfig::new();
|
let mut rc = ResolverConfig::new();
|
||||||
let url = consul_url.parse()?;
|
let url = consul_url.parse()?;
|
||||||
rc.add_name_server(NameServerConfig::new(url, Protocol::Tcp));
|
rc.add_name_server(NameServerConfig::new(url, Protocol::Tcp));
|
||||||
@@ -23,14 +27,22 @@ pub async fn get_service_endpoints_by_dns(consul_url: &str, service_protocol: &s
|
|||||||
let hostname = record.target();
|
let hostname = record.target();
|
||||||
let lookup_responses = resolver.lookup_ip(hostname.to_string()).await?;
|
let lookup_responses = resolver.lookup_ip(hostname.to_string()).await?;
|
||||||
for response in lookup_responses {
|
for response in lookup_responses {
|
||||||
endpoints.push(SocketAddr::from_str(&format!("{}:{}", &response.to_string(), record.port()))?);
|
endpoints.push(SocketAddr::from_str(&format!(
|
||||||
|
"{}:{}",
|
||||||
|
&response.to_string(),
|
||||||
|
record.port()
|
||||||
|
))?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(endpoints)
|
Ok(endpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_kube_service_endpoints_by_dns(port_name: &str, service_protocol: &str, service_name: &str) -> Result<Vec<SocketAddr>, Box<dyn std::error::Error>> {
|
pub async fn get_kube_service_endpoints_by_dns(
|
||||||
|
port_name: &str,
|
||||||
|
service_protocol: &str,
|
||||||
|
service_name: &str,
|
||||||
|
) -> Result<Vec<SocketAddr>, Box<dyn std::error::Error>> {
|
||||||
let (config, options) = read_system_conf()?;
|
let (config, options) = read_system_conf()?;
|
||||||
let resolver = TokioAsyncResolver::tokio(config, options);
|
let resolver = TokioAsyncResolver::tokio(config, options);
|
||||||
|
|
||||||
@@ -42,7 +54,11 @@ pub async fn get_kube_service_endpoints_by_dns(port_name: &str, service_protocol
|
|||||||
let hostname = record.target();
|
let hostname = record.target();
|
||||||
let lookup_responses = resolver.lookup_ip(hostname.to_string()).await?;
|
let lookup_responses = resolver.lookup_ip(hostname.to_string()).await?;
|
||||||
for response in lookup_responses {
|
for response in lookup_responses {
|
||||||
endpoints.push(SocketAddr::from_str(&format!("{}:{}", &response.to_string(), record.port()))?);
|
endpoints.push(SocketAddr::from_str(&format!(
|
||||||
|
"{}:{}",
|
||||||
|
&response.to_string(),
|
||||||
|
record.port()
|
||||||
|
))?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +74,7 @@ pub struct ServiceInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_service_info(
|
pub async fn get_service_info(
|
||||||
namespace: &str,
|
_namespace: &str,
|
||||||
service_name: &str,
|
service_name: &str,
|
||||||
) -> Result<ServiceInfo, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<ServiceInfo, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let client = Client::try_default().await?;
|
let client = Client::try_default().await?;
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ async fn terminate_signal() {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
let mut sigterm =
|
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to set up SIGTERM handler");
|
||||||
signal(SignalKind::terminate()).expect("Failed to set up SIGTERM handler");
|
|
||||||
sigterm.recv().await;
|
sigterm.recv().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let mut ctrlbreak =
|
let mut ctrlbreak = signal::windows::ctrl_break().expect("Failed to set up CTRL_BREAK handler");
|
||||||
signal::windows::ctrl_break().expect("Failed to set up CTRL_BREAK handler");
|
|
||||||
ctrlbreak.recv().await;
|
ctrlbreak.recv().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
world-service/README.md
Normal file
95
world-service/README.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# World Service
|
||||||
|
|
||||||
|
The World Service manages the game world state and character interactions in the MMORPG server architecture.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The World Service is responsible for:
|
||||||
|
- Managing character positions and movement
|
||||||
|
- Handling map changes
|
||||||
|
- Processing character interactions
|
||||||
|
- Managing NPCs, monsters, and objects
|
||||||
|
- Handling combat and skills
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The service is built using the following components:
|
||||||
|
|
||||||
|
- **gRPC Server**: Exposes world management endpoints
|
||||||
|
- **World State Manager**: Maintains the current state of the game world
|
||||||
|
- **Map Manager**: Handles map data and transitions
|
||||||
|
- **Entity Manager**: Manages characters, NPCs, and monsters
|
||||||
|
|
||||||
|
## Service Endpoints
|
||||||
|
|
||||||
|
The World Service exposes the following gRPC endpoints:
|
||||||
|
|
||||||
|
### GetCharacter
|
||||||
|
Retrieves a character's world state.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc GetCharacter(CharacterRequest) returns (CharacterResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ChangeMap
|
||||||
|
Handles a character changing maps.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc ChangeMap(ChangeMapRequest) returns (ChangeMapResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### MoveCharacter
|
||||||
|
Updates a character's position.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc MoveCharacter(CharacterMoveRequest) returns (CharacterMoveResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
### GetTargetHp
|
||||||
|
Retrieves an object's current HP.
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
rpc GetTargetHp(ObjectHpRequest) returns (ObjectHpResponse);
|
||||||
|
```
|
||||||
|
|
||||||
|
## World Data Structure
|
||||||
|
|
||||||
|
The world consists of:
|
||||||
|
|
||||||
|
- **Maps**: Different areas with unique IDs
|
||||||
|
- **Spawn Points**: Locations where characters can appear
|
||||||
|
- **NPCs**: Non-player characters with fixed positions
|
||||||
|
- **Monsters**: Hostile entities that can move and attack
|
||||||
|
- **Objects**: Interactive items in the world
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The service can be configured using environment variables:
|
||||||
|
|
||||||
|
- `LISTEN_ADDR`: The address to listen on (default: "0.0.0.0")
|
||||||
|
- `SERVICE_PORT`: The port to listen on (default: "50054")
|
||||||
|
- `LOG_LEVEL`: Logging level (default: "info")
|
||||||
|
|
||||||
|
## Running the Service
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t world-service .
|
||||||
|
docker run -p 50054:50054 world-service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with External Systems
|
||||||
|
|
||||||
|
The World Service integrates with:
|
||||||
|
|
||||||
|
- **Database Service**: For retrieving and storing world state
|
||||||
|
- **Auth Service**: For user authentication and authorization
|
||||||
|
- **Character Service**: For character data
|
||||||
|
- **Packet Service**: For handling client requests related to the world
|
||||||
@@ -11,9 +11,6 @@ fn main() {
|
|||||||
tonic_build::configure()
|
tonic_build::configure()
|
||||||
.build_server(false) // Generate gRPC client code
|
.build_server(false) // Generate gRPC client code
|
||||||
.compile_well_known_types(true)
|
.compile_well_known_types(true)
|
||||||
.compile_protos(
|
.compile_protos(&["../proto/user_db_api.proto", "../proto/auth.proto"], &["../proto"])
|
||||||
&["../proto/user_db_api.proto", "../proto/auth.proto"],
|
|
||||||
&["../proto"],
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
use std::env;
|
use std::env;
|
||||||
use utils::{health_check, logging};
|
|
||||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
|
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
|
||||||
|
use utils::{health_check, logging};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -12,7 +12,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Set the gRPC server address
|
// Set the gRPC server address
|
||||||
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50054".to_string());
|
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50054".to_string());
|
||||||
let db_url = format!("http://{}",get_kube_service_endpoints_by_dns("database-service","tcp","database-service").await?.get(0).unwrap());
|
let db_url = format!(
|
||||||
|
"http://{}",
|
||||||
|
get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service")
|
||||||
|
.await?
|
||||||
|
.get(0)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
// Register service with Consul
|
// Register service with Consul
|
||||||
health_check::start_health_check(addr.as_str()).await?;
|
health_check::start_health_check(addr.as_str()).await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user