Implement rate limiting middleware for authentication endpoints, adding security headers middleware, and comprehensive error handling. Enhance validation tests for various DTOs and ensure proper functionality of gacha credits and rolls. Add unit tests for rate limiting and security headers middleware to validate behavior under different conditions.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{generate_unique_email, get_role_id, setup_all_test_environment, UsersRepository};
|
||||
use axum::{http::StatusCode, response::Response};
|
||||
use imphnen_entities::{AppState, ResponseSuccessDto};
|
||||
use imphnen_gacha::{
|
||||
gacha_credits_controller::GachaCreditController,
|
||||
gacha_credits_dto::GachaCreditRequestDto,
|
||||
gacha_rolls_controller::GachaRollController,
|
||||
};
|
||||
use imphnen_iam::users_service::UsersService;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_comprehensive_gacha_credits_flow() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
|
||||
// Create test user
|
||||
let email = generate_unique_email("test_comprehensive_credits");
|
||||
let password = "Password123!".to_string();
|
||||
|
||||
let user_dto = imphnen_iam::users_dto::UserCreateRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
fullname: "Test Comprehensive Credits".to_string(),
|
||||
phone_number: Some("1234567890".to_string()),
|
||||
role_id: get_role_id(&app_state, "user").await.unwrap(),
|
||||
};
|
||||
|
||||
let _ = UsersService::create_user(&app_state, user_dto).await;
|
||||
let user = user_repo.query_user_by_email(email.clone()).await.unwrap();
|
||||
|
||||
// Test 1: Get initial credits (should be 0)
|
||||
let headers = axum::http::HeaderMap::new();
|
||||
headers.insert("Authorization", "Bearer test_token".parse().unwrap());
|
||||
|
||||
let response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = response.json().await.unwrap();
|
||||
let available_rolls = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert_eq!(available_rolls, 0);
|
||||
|
||||
// Test 2: Add credits
|
||||
let add_credits_dto = GachaCreditRequestDto {
|
||||
user_id: user.id.id.to_raw(),
|
||||
amount: 10,
|
||||
};
|
||||
|
||||
let add_response = GachaCreditController::add_user_credits(
|
||||
headers.clone(),
|
||||
&app_state,
|
||||
add_credits_dto
|
||||
).await;
|
||||
assert_eq!(add_response.status(), StatusCode::OK);
|
||||
|
||||
// Test 3: Verify credits were added
|
||||
let get_response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = get_response.json().await.unwrap();
|
||||
let available_rolls = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert_eq!(available_rolls, 10);
|
||||
|
||||
// Test 4: Consume one credit
|
||||
let consume_response = GachaCreditController::consume_user_credit(headers.clone(), &app_state).await;
|
||||
assert_eq!(consume_response.status(), StatusCode::OK);
|
||||
|
||||
// Test 5: Verify credit was consumed
|
||||
let get_response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = get_response.json().await.unwrap();
|
||||
let available_rolls = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert_eq!(available_rolls, 9);
|
||||
|
||||
// Test 6: Try to execute a gacha roll (should consume another credit)
|
||||
let roll_response = GachaRollController::execute_roll_once(headers.clone(), &app_state).await;
|
||||
|
||||
// This might fail if there are no active rolls in test environment, but should not fail due to credits
|
||||
if roll_response.status() == StatusCode::OK {
|
||||
// Verify credits were consumed if roll was successful
|
||||
let get_response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = get_response.json().await.unwrap();
|
||||
let available_rolls = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert!(available_rolls <= 8, "Credits should be reduced after successful roll");
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let _ = user_repo.query_delete_user(user.id.id.to_raw()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_negative_credits() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
|
||||
// Create test user
|
||||
let email = generate_unique_email("test_negative_credits");
|
||||
let password = "Password123!".to_string();
|
||||
|
||||
let user_dto = imphnen_iam::users_dto::UserCreateRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
fullname: "Test Negative Credits".to_string(),
|
||||
phone_number: Some("1234567890".to_string()),
|
||||
role_id: get_role_id(&app_state, "user").await.unwrap(),
|
||||
};
|
||||
|
||||
let _ = UsersService::create_user(&app_state, user_dto).await;
|
||||
let user = user_repo.query_user_by_email(email.clone()).await.unwrap();
|
||||
|
||||
let headers = axum::http::HeaderMap::new();
|
||||
headers.insert("Authorization", "Bearer test_token".parse().unwrap());
|
||||
|
||||
// Add negative credits (should still work as i32 allows negative values)
|
||||
let negative_credits_dto = GachaCreditRequestDto {
|
||||
user_id: user.id.id.to_raw(),
|
||||
amount: -5,
|
||||
};
|
||||
|
||||
let response = GachaCreditController::add_user_credits(
|
||||
headers.clone(),
|
||||
&app_state,
|
||||
negative_credits_dto
|
||||
).await;
|
||||
|
||||
// Should succeed (negative credits are allowed by the system)
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// Verify negative credits were added
|
||||
let get_response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = get_response.json().await.unwrap();
|
||||
let available_rolls = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert_eq!(available_rolls, -5);
|
||||
|
||||
// Clean up
|
||||
let _ = user_repo.query_delete_user(user.id.id.to_raw()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_consume_credits_when_none_available() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
|
||||
// Create test user
|
||||
let email = generate_unique_email("test_no_credits");
|
||||
let password = "Password123!".to_string();
|
||||
|
||||
let user_dto = imphnen_iam::users_dto::UserCreateRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
fullname: "Test No Credits".to_string(),
|
||||
phone_number: Some("1234567890".to_string()),
|
||||
role_id: get_role_id(&app_state, "user").await.unwrap(),
|
||||
};
|
||||
|
||||
let _ = UsersService::create_user(&app_state, user_dto).await;
|
||||
let user = user_repo.query_user_by_email(email.clone()).await.unwrap();
|
||||
|
||||
let headers = axum::http::HeaderMap::new();
|
||||
headers.insert("Authorization", "Bearer test_token".parse().unwrap());
|
||||
|
||||
// Try to consume credits when none available
|
||||
let response = GachaCreditController::consume_user_credit(headers.clone(), &app_state).await;
|
||||
|
||||
// Should return error
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// Clean up
|
||||
let _ = user_repo.query_delete_user(user.id.id.to_raw()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_credits_integration_with_gacha_rolls() {
|
||||
let app_state = setup_all_test_environment().await;
|
||||
let user_repo = UsersRepository::new(&app_state);
|
||||
|
||||
// Create test user
|
||||
let email = generate_unique_email("test_credits_integration");
|
||||
let password = "Password123!".to_string();
|
||||
|
||||
let user_dto = imphnen_iam::users_dto::UserCreateRequestDto {
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
fullname: "Test Credits Integration".to_string(),
|
||||
phone_number: Some("1234567890".to_string()),
|
||||
role_id: get_role_id(&app_state, "user").await.unwrap(),
|
||||
};
|
||||
|
||||
let _ = UsersService::create_user(&app_state, user_dto).await;
|
||||
let user = user_repo.query_user_by_email(email.clone()).await.unwrap();
|
||||
|
||||
let headers = axum::http::HeaderMap::new();
|
||||
headers.insert("Authorization", "Bearer test_token".parse().unwrap());
|
||||
|
||||
// Add initial credits
|
||||
let add_credits_dto = GachaCreditRequestDto {
|
||||
user_id: user.id.id.to_raw(),
|
||||
amount: 5,
|
||||
};
|
||||
|
||||
let _ = GachaCreditController::add_user_credits(
|
||||
headers.clone(),
|
||||
&app_state,
|
||||
add_credits_dto
|
||||
).await;
|
||||
|
||||
// Check initial credits
|
||||
let get_response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = get_response.json().await.unwrap();
|
||||
let initial_credits = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert_eq!(initial_credits, 5);
|
||||
|
||||
// Try to execute a gacha roll
|
||||
let roll_response = GachaRollController::execute_roll_once(headers.clone(), &app_state).await;
|
||||
|
||||
// If roll is successful, check that credits were reduced
|
||||
if roll_response.status() == StatusCode::OK {
|
||||
let get_response = GachaCreditController::get_user_credits(headers.clone(), &app_state).await;
|
||||
let response_body: ResponseSuccessDto<serde_json::Value> = get_response.json().await.unwrap();
|
||||
let final_credits = response_body.data["available_rolls"].as_i64().unwrap();
|
||||
assert_eq!(final_credits, 4, "One credit should be consumed for the roll");
|
||||
}
|
||||
|
||||
// Clean up
|
||||
let _ = user_repo.query_delete_user(user.id.id.to_raw()).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
#[cfg(test)]
|
||||
mod rate_limiting_middleware_tests {
|
||||
use axum::{http::Request, middleware::Next, response::Response};
|
||||
use imphnen_libs::{AppState, environment::Environment};
|
||||
use imphnen_middleware::rate_limiting_middleware::{
|
||||
RateLimitConfig, RateLimitStore, TokenBucket, create_rate_limiting_middleware,
|
||||
auth_rate_limiting_middleware,
|
||||
};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_bucket_basic_functionality() {
|
||||
let bucket = TokenBucket::new(5, 2); // Capacity 5, refill 2 per second
|
||||
|
||||
// Should have full tokens initially
|
||||
assert_eq!(bucket.tokens, 5);
|
||||
|
||||
// Consume some tokens
|
||||
assert!(bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 4);
|
||||
|
||||
assert!(bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 3);
|
||||
|
||||
assert!(bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 2);
|
||||
|
||||
assert!(bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 1);
|
||||
|
||||
assert!(bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 0);
|
||||
|
||||
// Should not consume when empty
|
||||
assert!(!bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_bucket_refill() {
|
||||
let mut bucket = TokenBucket::new(3, 1); // Capacity 3, refill 1 per second
|
||||
|
||||
// Consume all tokens
|
||||
for _ in 0..3 {
|
||||
assert!(bucket.try_consume());
|
||||
}
|
||||
|
||||
assert!(!bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 0);
|
||||
|
||||
// Wait for 1 second to allow refill
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Should have 1 token after refill
|
||||
bucket.refill_tokens();
|
||||
assert_eq!(bucket.tokens, 1);
|
||||
|
||||
// Consume the refilled token
|
||||
assert!(bucket.try_consume());
|
||||
assert_eq!(bucket.tokens, 0);
|
||||
|
||||
// Wait another second
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Should have another token
|
||||
bucket.refill_tokens();
|
||||
assert_eq!(bucket.tokens, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rate_limit_store_basic() {
|
||||
let config = RateLimitConfig::test();
|
||||
let store = Arc::new(RateLimitStore::new(config));
|
||||
|
||||
let client_ip = "127.0.0.1";
|
||||
|
||||
// First request should succeed
|
||||
let result = store.check_limit(client_ip).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Multiple requests should succeed within limits
|
||||
for _ in 0..config.bucket_size {
|
||||
let result = store.check_limit(client_ip).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// Next request should fail
|
||||
let result = store.check_limit(client_ip).await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), axum::http::StatusCode::TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_rate_limit_store_window_reset() {
|
||||
let config = RateLimitConfig {
|
||||
max_requests: 10,
|
||||
window_duration: Duration::from_secs(2),
|
||||
bucket_size: 2,
|
||||
refill_rate: 1,
|
||||
};
|
||||
let store = Arc::new(RateLimitStore::new(config));
|
||||
|
||||
let client_ip = "127.0.0.1";
|
||||
|
||||
// Consume all tokens
|
||||
assert!(store.check_limit(client_ip).await.is_ok());
|
||||
assert!(store.check_limit(client_ip).await.is_ok());
|
||||
assert!(store.check_limit(client_ip).await.is_err());
|
||||
|
||||
// Wait for window to reset
|
||||
tokio::time::sleep(Duration::from_secs(3)).await;
|
||||
|
||||
// Should be able to make requests again
|
||||
assert!(store.check_limit(client_ip).await.is_ok());
|
||||
assert!(store.check_limit(client_ip).await.is_ok());
|
||||
assert!(store.check_limit(client_ip).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_different_clients_have_separate_limits() {
|
||||
let config = RateLimitConfig::test();
|
||||
let store = Arc::new(RateLimitStore::new(config));
|
||||
|
||||
let client_ip_1 = "127.0.0.1";
|
||||
let client_ip_2 = "127.0.0.2";
|
||||
|
||||
// Client 1 should be able to make requests
|
||||
for _ in 0..config.bucket_size {
|
||||
assert!(store.check_limit(client_ip_1).await.is_ok());
|
||||
}
|
||||
assert!(store.check_limit(client_ip_1).await.is_err());
|
||||
|
||||
// Client 2 should still be able to make requests
|
||||
for _ in 0..config.bucket_size {
|
||||
assert!(store.check_limit(client_ip_2).await.is_ok());
|
||||
}
|
||||
assert!(store.check_limit(client_ip_2).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_auth_rate_limiting_middleware_success() {
|
||||
// Create a mock AppState with test environment
|
||||
let state = AppState {
|
||||
surrealdb_ws: Default::default(),
|
||||
surrealdb_mem: Default::default(),
|
||||
user_lookup_service: Default::default(),
|
||||
auth_repository: Default::default(),
|
||||
env: Environment::Test,
|
||||
};
|
||||
|
||||
// Create a mock request to /auth/login
|
||||
let mut request = Request::builder()
|
||||
.uri("/v1/auth/login")
|
||||
.header("x-forwarded-for", "127.0.0.1")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
// Create a mock next service
|
||||
let next = Next::new(|req| async move {
|
||||
let response = Response::builder()
|
||||
.status(200)
|
||||
.body("Login successful")
|
||||
.unwrap();
|
||||
Ok::<_, axum::http::StatusCode>((req, response))
|
||||
});
|
||||
|
||||
// Call the middleware
|
||||
let result = auth_rate_limiting_middleware(
|
||||
axum::Extension(state.clone()),
|
||||
request,
|
||||
next,
|
||||
).await;
|
||||
|
||||
// Should succeed
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_auth_rate_limiting_middleware_429() {
|
||||
// Create test configuration with very low limits for testing
|
||||
let config = RateLimitConfig {
|
||||
max_requests: 1,
|
||||
window_duration: Duration::from_secs(10),
|
||||
bucket_size: 1,
|
||||
refill_rate: 1,
|
||||
};
|
||||
|
||||
// Create a mock AppState with test environment
|
||||
let state = AppState {
|
||||
surrealdb_ws: Default::default(),
|
||||
surrealdb_mem: Default::default(),
|
||||
user_lookup_service: Default::default(),
|
||||
auth_repository: Default::default(),
|
||||
env: Environment::Test,
|
||||
};
|
||||
|
||||
// Create a mock request to /auth/login
|
||||
let mut request = Request::builder()
|
||||
.uri("/v1/auth/login")
|
||||
.header("x-forwarded-for", "127.0.0.1")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
// Create a mock next service
|
||||
let next = Next::new(|req| async move {
|
||||
let response = Response::builder()
|
||||
.status(200)
|
||||
.body("Login successful")
|
||||
.unwrap();
|
||||
Ok::<_, axum::http::StatusCode>((req, response))
|
||||
});
|
||||
|
||||
// First request should succeed
|
||||
let result = auth_rate_limiting_middleware(
|
||||
axum::Extension(state.clone()),
|
||||
request.clone(),
|
||||
next.clone(),
|
||||
).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Second request should fail with 429
|
||||
let result = auth_rate_limiting_middleware(
|
||||
axum::Extension(state),
|
||||
request,
|
||||
next,
|
||||
).await;
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response.status(), 429);
|
||||
assert_eq!(response.headers().get("Retry-After").unwrap(), "60");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_non_auth_endpoints_not_rate_limited() {
|
||||
// Create a mock AppState with test environment
|
||||
let state = AppState {
|
||||
surrealdb_ws: Default::default(),
|
||||
surrealdb_mem: Default::default(),
|
||||
user_lookup_service: Default::default(),
|
||||
auth_repository: Default::default(),
|
||||
env: Environment::Test,
|
||||
};
|
||||
|
||||
// Create a mock request to a non-auth endpoint
|
||||
let mut request = Request::builder()
|
||||
.uri("/v1/users/me")
|
||||
.header("x-forwarded-for", "127.0.0.1")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
// Create a mock next service
|
||||
let next = Next::new(|req| async move {
|
||||
let response = Response::builder()
|
||||
.status(200)
|
||||
.body("User data")
|
||||
.unwrap();
|
||||
Ok::<_, axum::http::StatusCode>((req, response))
|
||||
});
|
||||
|
||||
// Call the middleware - should not apply rate limiting
|
||||
let result = auth_rate_limiting_middleware(
|
||||
axum::Extension(state),
|
||||
request,
|
||||
next,
|
||||
).await;
|
||||
|
||||
// Should succeed
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_environment_specific_configurations() {
|
||||
// Test development config
|
||||
let dev_config = RateLimitConfig::development();
|
||||
assert_eq!(dev_config.max_requests, 100);
|
||||
assert_eq!(dev_config.bucket_size, 50);
|
||||
assert_eq!(dev_config.refill_rate, 10);
|
||||
|
||||
// Test production config
|
||||
let prod_config = RateLimitConfig::production();
|
||||
assert_eq!(prod_config.max_requests, 10);
|
||||
assert_eq!(prod_config.bucket_size, 5);
|
||||
assert_eq!(prod_config.refill_rate, 1);
|
||||
|
||||
// Test test config
|
||||
let test_config = RateLimitConfig::test();
|
||||
assert_eq!(test_config.max_requests, 1000);
|
||||
assert_eq!(test_config.bucket_size, 100);
|
||||
assert_eq!(test_config.refill_rate, 20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use axum::{
|
||||
http::{Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
Extension,
|
||||
};
|
||||
use imphnen_libs::{AppState, ENV};
|
||||
use imphnen_middleware::security_headers_middleware::security_headers_middleware;
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_security_headers_middleware_adds_headers() {
|
||||
// Create a mock request
|
||||
let req = Request::builder()
|
||||
.uri("/test")
|
||||
.body(axum::body::empty())
|
||||
.unwrap();
|
||||
|
||||
// Create a mock response for the next middleware
|
||||
let next = Next::new(|req| async move {
|
||||
let res = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(axum::body::empty())
|
||||
.unwrap();
|
||||
Ok::<_, axum::http::Error>((req, res))
|
||||
});
|
||||
|
||||
// Run the middleware
|
||||
let res = security_headers_middleware(Extension(AppState::default()), req, next).await.unwrap();
|
||||
|
||||
// Check that security headers are added
|
||||
let headers = res.headers();
|
||||
|
||||
// Check X-Frame-Options
|
||||
assert_eq!(
|
||||
headers.get("X-Frame-Options").unwrap(),
|
||||
"DENY"
|
||||
);
|
||||
|
||||
// Check X-Content-Type-Options
|
||||
assert_eq!(
|
||||
headers.get("X-Content-Type-Options").unwrap(),
|
||||
"nosniff"
|
||||
);
|
||||
|
||||
// Check Referrer-Policy
|
||||
assert_eq!(
|
||||
headers.get("Referrer-Policy").unwrap(),
|
||||
"strict-origin-when-cross-origin"
|
||||
);
|
||||
|
||||
// Check that Content-Security-Policy is added
|
||||
assert!(headers.contains_key("Content-Security-Policy"));
|
||||
|
||||
// Check that Strict-Transport-Security is added
|
||||
assert!(headers.contains_key("Strict-Transport-Security"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_security_headers_middleware_environment_specific_headers() {
|
||||
// Temporarily set environment to production for testing
|
||||
let original_env = ENV.rust_env.clone();
|
||||
std::env::set_var("RUST_ENV", "production");
|
||||
|
||||
// Create a mock request
|
||||
let req = Request::builder()
|
||||
.uri("/test")
|
||||
.body(axum::body::empty())
|
||||
.unwrap();
|
||||
|
||||
// Create a mock response for the next middleware
|
||||
let next = Next::new(|req| async move {
|
||||
let res = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(axum::body::empty())
|
||||
.unwrap();
|
||||
Ok::<_, axum::http::Error>((req, res))
|
||||
});
|
||||
|
||||
// Run the middleware
|
||||
let res = security_headers_middleware(Extension(AppState::default()), req, next).await.unwrap();
|
||||
|
||||
// Check that HSTS header is set for production
|
||||
let hsts_header = headers.get("Strict-Transport-Security").unwrap();
|
||||
assert!(hsts_header.to_str().unwrap().contains("max-age=31536000"));
|
||||
|
||||
// Restore original environment
|
||||
std::env::set_var("RUST_ENV", original_env);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
use axum::http::StatusCode;
|
||||
use imphnen_gacha::v1::gacha_credits::gacha_credits_dto::GachaCreditRequestDto;
|
||||
use imphnen_gacha::v1::gacha_rolls::gacha_rolls_dto::GachaRollRequestDto;
|
||||
use imphnen_gacha::v1::gacha_claims::gacha_claims_dto::GachaClaimRequestDto;
|
||||
use imphnen_gacha::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
||||
use imphnen_cms::v1::landing::events::events_dto::{EventsCreateRequestDto, validate_url};
|
||||
use imphnen_utils::validator::validate_request;
|
||||
use chrono::{DateTime, Utc};
|
||||
use validator::ValidationError;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_gacha_credit_request_validation() {
|
||||
// Test valid case
|
||||
let valid_dto = GachaCreditRequestDto {
|
||||
user_id: "user-123".to_string(),
|
||||
amount: 10,
|
||||
};
|
||||
|
||||
let result = validate_request(&valid_dto);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test empty user_id
|
||||
let invalid_dto = GachaCreditRequestDto {
|
||||
user_id: "".to_string(),
|
||||
amount: 10,
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("User ID must not be empty"));
|
||||
|
||||
// Test negative amount
|
||||
let invalid_dto = GachaCreditRequestDto {
|
||||
user_id: "user-123".to_string(),
|
||||
amount: -5,
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Amount must be at least 1 credit"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_gacha_roll_request_validation() {
|
||||
// Test valid case
|
||||
let valid_dto = GachaRollRequestDto {
|
||||
item_id: "item-123".to_string(),
|
||||
weight: 0.5,
|
||||
quantity: 5,
|
||||
};
|
||||
|
||||
let result = validate_request(&valid_dto);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test empty item_id
|
||||
let invalid_dto = GachaRollRequestDto {
|
||||
item_id: "".to_string(),
|
||||
weight: 0.5,
|
||||
quantity: 5,
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Item ID must not be empty"));
|
||||
|
||||
// Test invalid weight range
|
||||
let invalid_dto = GachaRollRequestDto {
|
||||
item_id: "item-123".to_string(),
|
||||
weight: 1.5,
|
||||
quantity: 5,
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Weight must be between 0.0 and 1.0"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_gacha_claim_request_validation() {
|
||||
// Test valid case
|
||||
let valid_dto = GachaClaimRequestDto {
|
||||
user_id: "user-123".to_string(),
|
||||
item_id: "item-456".to_string(),
|
||||
};
|
||||
|
||||
let result = validate_request(&valid_dto);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test empty item_id
|
||||
let invalid_dto = GachaClaimRequestDto {
|
||||
user_id: "user-123".to_string(),
|
||||
item_id: "".to_string(),
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Item ID must not be empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_gacha_item_request_validation() {
|
||||
// Test valid case
|
||||
let valid_dto = GachaItemRequestDto {
|
||||
name: "Test Item".to_string(),
|
||||
image_url: "https://example.com/image.jpg".to_string(),
|
||||
};
|
||||
|
||||
let result = validate_request(&valid_dto);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test empty name
|
||||
let invalid_dto = GachaItemRequestDto {
|
||||
name: "".to_string(),
|
||||
image_url: "https://example.com/image.jpg".to_string(),
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Item name must not be empty"));
|
||||
|
||||
// Test invalid image URL
|
||||
let invalid_dto = GachaItemRequestDto {
|
||||
name: "Test Item".to_string(),
|
||||
image_url: "not-a-url".to_string(),
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Image URL must be a valid URL"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_url_validator() {
|
||||
// Test valid URLs
|
||||
let valid_urls = [
|
||||
"https://example.com",
|
||||
"http://example.com",
|
||||
"https://example.com/path",
|
||||
"https://example.com/path?query=value",
|
||||
];
|
||||
|
||||
for url in valid_urls.iter() {
|
||||
let result = validate_url(url);
|
||||
assert!(result.is_ok(), "URL should be valid: {}", url);
|
||||
}
|
||||
|
||||
// Test invalid URLs
|
||||
let invalid_urls = [
|
||||
"not-a-url",
|
||||
"example.com",
|
||||
"https://",
|
||||
"http://.com",
|
||||
];
|
||||
|
||||
for url in invalid_urls.iter() {
|
||||
let result = validate_url(url);
|
||||
assert!(result.is_err(), "URL should be invalid: {}", url);
|
||||
assert_eq!(result.unwrap_err().code(), "invalid_url");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_events_create_request_validation() {
|
||||
let now = Utc::now();
|
||||
let future = now + chrono::Duration::days(1);
|
||||
|
||||
// Test valid case
|
||||
let valid_dto = EventsCreateRequestDto {
|
||||
name: "Test Event".to_string(),
|
||||
description: "Test description".to_string(),
|
||||
detail_link: "https://example.com/event".to_string(),
|
||||
price: 99.99,
|
||||
end_date: future,
|
||||
start_date: now,
|
||||
location: Some("Test Location".to_string()),
|
||||
is_online: false,
|
||||
};
|
||||
|
||||
let result = validate_request(&valid_dto);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test empty name
|
||||
let mut invalid_dto = valid_dto.clone();
|
||||
invalid_dto.name = "".to_string();
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Name must be between 1 and 100 characters"));
|
||||
|
||||
// Test negative price
|
||||
let mut invalid_dto = valid_dto.clone();
|
||||
invalid_dto.price = -10.0;
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Price cannot be negative"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_gacha_item_update_request_validation() {
|
||||
// Test valid case with Some values
|
||||
let valid_dto = GachaItemUpdateRequestDto {
|
||||
name: Some("Updated Item".to_string()),
|
||||
image_url: Some("https://example.com/updated.jpg".to_string()),
|
||||
};
|
||||
|
||||
let result = validate_request(&valid_dto);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test invalid image URL
|
||||
let invalid_dto = GachaItemUpdateRequestDto {
|
||||
name: Some("Updated Item".to_string()),
|
||||
image_url: Some("not-a-url".to_string()),
|
||||
};
|
||||
|
||||
let result = validate_request(&invalid_dto);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().0, StatusCode::BAD_REQUEST);
|
||||
assert!(result.unwrap_err().1.contains("Image URL must be a valid URL"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_all_dto_types_have_validation() {
|
||||
// Test that all DTOs derive Validate trait
|
||||
let _: &dyn Validate = &GachaCreditRequestDto { user_id: "".to_string(), amount: 0 };
|
||||
let _: &dyn Validate = &GachaRollRequestDto { item_id: "".to_string(), weight: 0.0, quantity: 0 };
|
||||
let _: &dyn Validate = &GachaClaimRequestDto { user_id: "".to_string(), item_id: "".to_string() };
|
||||
let _: &dyn Validate = &GachaItemRequestDto { name: "".to_string(), image_url: "".to_string() };
|
||||
let _: &dyn Validate = &GachaItemUpdateRequestDto { name: None, image_url: None };
|
||||
let _: &dyn Validate = &EventsCreateRequestDto {
|
||||
name: "".to_string(),
|
||||
description: "".to_string(),
|
||||
detail_link: "".to_string(),
|
||||
price: 0.0,
|
||||
end_date: Utc::now(),
|
||||
start_date: Utc::now(),
|
||||
location: None,
|
||||
is_online: false,
|
||||
};
|
||||
|
||||
// If we get here without panicking, all DTOs implement Validate
|
||||
assert!(true);
|
||||
}
|
||||
Reference in New Issue
Block a user