From 465cbee2c379fbd6b2d48b07a98806758f0620ce Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Wed, 21 May 2025 19:37:30 +0700 Subject: [PATCH] feat: finishing gacha feature --- .../src/v1/gacha_claim/gacha_claim_schema.rs | 23 +- .../v1/gacha_roll/gacha_roll_controller.rs | 2 +- .../src/v1/gacha_roll/gacha_roll_dto.rs | 5 +- .../v1/gacha_roll/gacha_roll_repository.rs | 9 +- .../src/v1/gacha_roll/gacha_roll_service.rs | 33 +- imphnen-iam/src/v1/auth/auth_dto.rs | 27 +- tests/src/iam/auth/auth_controller_test.rs | 667 ------------------ tests/src/iam/auth/mod.rs | 2 - tests/src/iam/roles/roles_repository_test.rs | 12 +- tests/src/iam/users/users_repository_test.rs | 15 +- 10 files changed, 80 insertions(+), 715 deletions(-) delete mode 100644 tests/src/iam/auth/auth_controller_test.rs diff --git a/imphnen-gacha/src/v1/gacha_claim/gacha_claim_schema.rs b/imphnen-gacha/src/v1/gacha_claim/gacha_claim_schema.rs index 6afe4ac..f60a44e 100644 --- a/imphnen-gacha/src/v1/gacha_claim/gacha_claim_schema.rs +++ b/imphnen-gacha/src/v1/gacha_claim/gacha_claim_schema.rs @@ -1,4 +1,5 @@ -use crate::{ResourceEnum, make_thing}; +use crate::{GachaRollQueryDto, ResourceEnum, make_thing}; +use imphnen_iam::get_iso_date; use serde::{Deserialize, Serialize}; use surrealdb::{Uuid, sql::Thing}; @@ -30,8 +31,8 @@ impl Default for GachaClaimSchema { &Uuid::new_v4().to_string(), ), is_deleted: false, - created_at: None, - updated_at: None, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), } } } @@ -46,8 +47,20 @@ impl GachaClaimSchema { user: make_thing(&ResourceEnum::Users.to_string(), &dto.user_id), item: make_thing(&ResourceEnum::GachaItems.to_string(), &dto.item_id), is_deleted: false, - created_at: None, - updated_at: None, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), + } + } + + pub fn roll(roll: GachaRollQueryDto, user_id: Thing) -> Self { + Self { + id: make_thing( + &ResourceEnum::GachaClaims.to_string(), + &Uuid::new_v4().to_string(), + ), + user: user_id, + item: roll.item.id.clone(), + ..Default::default() } } } diff --git a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_controller.rs b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_controller.rs index 4c9a418..d251245 100644 --- a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_controller.rs +++ b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_controller.rs @@ -88,7 +88,7 @@ pub async fn post_execute_gacha_roll( ) .await { - Ok(_) => GachaRollService::execute_roll_once(&state).await, + Ok(_) => GachaRollService::execute_roll_once(headers, &state).await, Err(response) => response, } } diff --git a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_dto.rs b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_dto.rs index 953df9d..2ec4efb 100644 --- a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_dto.rs +++ b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_dto.rs @@ -8,7 +8,6 @@ use validator::Validate; pub struct GachaRollRequestDto { #[validate(length(min = 1, message = "Item ID must not be empty"))] pub item_id: String, - #[validate(range(min = 1, message = "Weight must be greater than zero"))] pub weight: f32, #[validate(range(min = 1, message = "Quantity must be at least 1"))] pub quantity: i32, @@ -18,7 +17,7 @@ pub struct GachaRollRequestDto { pub struct GachaRollItemDto { pub id: String, pub item: GachaItemDto, - pub weight: String, + pub weight: f32, pub quantity: i32, pub is_deleted: bool, pub created_at: Option, @@ -30,7 +29,7 @@ impl GachaRollItemDto { Self { id: dto.id.id.to_raw(), item: GachaItemDto::from(dto.item.clone()), - weight: dto.weight.to_string(), + weight: dto.weight.clone(), quantity: dto.quantity, is_deleted: dto.is_deleted, created_at: dto.created_at.clone(), diff --git a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_repository.rs b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_repository.rs index 0ccefb1..5517cf5 100644 --- a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_repository.rs +++ b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_repository.rs @@ -2,6 +2,7 @@ use super::GachaRollQueryDto; use super::GachaRollSchema; use crate::{AppState, DetailQueryBuilder, ResourceEnum}; use anyhow::{Result, bail}; +use imphnen_iam::ListQueryBuilder; use rand::prelude::*; use rand::rng; use rand_distr::weighted::WeightedIndex; @@ -50,12 +51,10 @@ impl<'a> GachaRollRepository<'a> { pub async fn query_all_active_rolls(&self) -> Result> { let db = &self.state.surrealdb_ws; - let sql = DetailQueryBuilder::new(ResourceEnum::GachaRolls.to_string()) - .with_where("is_deleted") - .where_value("false") + let builder = ListQueryBuilder::new(ResourceEnum::GachaRolls.to_string()) .with_select_fields(vec!["*"]) - .with_fetch("item") - .build(); + .with_fetch(Some(vec!["item"])); + let sql = builder.build(); let result: Vec = db.query(sql).await?.take(0)?; Ok(result) } diff --git a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_service.rs b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_service.rs index 8e5ea37..aab5d80 100644 --- a/imphnen-gacha/src/v1/gacha_roll/gacha_roll_service.rs +++ b/imphnen-gacha/src/v1/gacha_roll/gacha_roll_service.rs @@ -1,10 +1,11 @@ use crate::{ - AppState, GachaRollItemDto, GachaRollRepository, GachaRollRequestDto, - GachaRollSchema, ResponseSuccessDto, common_response, success_response, - validate_request, + AppState, GachaClaimRepository, GachaClaimSchema, GachaRollItemDto, + GachaRollRepository, GachaRollRequestDto, GachaRollSchema, ResponseSuccessDto, + common_response, success_response, validate_request, }; -use axum::http::StatusCode; +use axum::http::{HeaderMap, StatusCode}; use axum::response::Response; +use imphnen_iam::{UsersRepository, extract_email}; pub struct GachaRollService; @@ -34,13 +35,29 @@ impl GachaRollService { } } - pub async fn execute_roll_once(state: &AppState) -> Response { + pub async fn execute_roll_once(headers: HeaderMap, state: &AppState) -> Response { let repo = GachaRollRepository::new(state); + let repo_claim = GachaClaimRepository::new(state); + let repo_user = UsersRepository::new(state); + let Some(email) = extract_email(&headers) else { + return common_response(StatusCode::UNAUTHORIZED, "Unauthorized"); + }; + let Ok(user) = repo_user.query_user_by_email(email.to_string()).await else { + return common_response(StatusCode::NOT_FOUND, "User not found"); + }; match repo.query_all_active_rolls().await { Ok(rolls) => match GachaRollRepository::roll_once(&rolls) { - Some(roll) => success_response(ResponseSuccessDto { - data: GachaRollItemDto::from(&roll), - }), + Some(roll) => { + let claim = GachaClaimSchema::roll(roll.clone(), user.id); + match repo_claim.query_create_gacha_claim(claim).await { + Ok(_) => success_response(ResponseSuccessDto { + data: GachaRollItemDto::from(&roll), + }), + Err(e) => { + common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()) + } + } + } None => common_response(StatusCode::NOT_FOUND, "No rollable item available"), }, Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), diff --git a/imphnen-iam/src/v1/auth/auth_dto.rs b/imphnen-iam/src/v1/auth/auth_dto.rs index d4f0883..f3a1a0c 100644 --- a/imphnen-iam/src/v1/auth/auth_dto.rs +++ b/imphnen-iam/src/v1/auth/auth_dto.rs @@ -1,24 +1,13 @@ -use lazy_static::lazy_static; -use regex::Regex; +use crate::UsersDetailItemDto; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use validator::{Validate, ValidationError}; -use crate::UsersDetailItemDto; - -lazy_static! { - static ref PASSWORD_REGEX: Regex = Regex::new( - r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$" - ) - .unwrap(); -} - fn validate_password_complexity(password: &str) -> Result<(), ValidationError> { let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase()); let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase()); let has_digit = password.chars().any(|c| c.is_ascii_digit()); let has_special = password.chars().any(|c| "@$!%*?&".contains(c)); - if has_uppercase && has_lowercase && has_digit && has_special { Ok(()) } else { @@ -98,10 +87,14 @@ pub struct AuthRefreshTokenRequestDto { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] pub struct AuthNewPasswordRequestDto { - pub token: String, #[validate(length(min = 1, message = "Token cannot be empty"))] - #[validate(regex( - path = "PASSWORD_REGEX", + pub token: String, + #[validate(length( + min = 8, + message = "Password must have at least 8 characters" + ))] + #[validate(custom( + function = "validate_password_complexity", message = "Password must include uppercase, lowercase, number, and special character" ))] pub password: String, @@ -118,8 +111,8 @@ pub struct AuthSetNewPasswordRequestDto { min = 8, message = "Password must have at least 8 characters" ))] - #[validate(regex( - path = "PASSWORD_REGEX", + #[validate(custom( + function = "validate_password_complexity", message = "Password must include uppercase, lowercase, number, and special character" ))] pub password: String, diff --git a/tests/src/iam/auth/auth_controller_test.rs b/tests/src/iam/auth/auth_controller_test.rs deleted file mode 100644 index ca2b5a7..0000000 --- a/tests/src/iam/auth/auth_controller_test.rs +++ /dev/null @@ -1,667 +0,0 @@ -use crate::{ - AuthOtpSchema, AuthRepository, OtpManager, ResourceEnum, RolesRepository, - UsersRepository, auth::auth_router, create_mock_app_state, create_test_user, - encode_refresh_token, -}; -use axum::{Extension, http::StatusCode}; -use axum_test::TestServer; -use serde_json::json; -use surrealdb::Uuid; - -#[tokio::test] -async fn test_login_should_fail_with_invalid_user() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ - "email": "nonexistent@example.com", - "password": "wrongpassword" - }); - let res = server.post("/v1/auth/login").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::UNAUTHORIZED); - let body = res.text(); - println!("🧪 Response Body: {}", body); -} - -#[tokio::test] -async fn test_login_should_fail_with_wrong_password() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())); - let server = TestServer::new(app).unwrap(); - let repo = UsersRepository::new(&state); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let mut user = create_test_user("user@example.com", "User Satu", true, &role_id); - user.password = crate::hash_password("correctpassword").unwrap(); - repo.query_create_user(user).await.unwrap(); - let payload = json!({ - "email": "user@example.com", - "password": "wrongpassword" - }); - let res = server.post("/v1/auth/login").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - println!("🧪 Wrong password: {}", body); -} - -#[tokio::test] -async fn test_login_should_fail_if_user_not_active() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())); - let server = TestServer::new(app).unwrap(); - let repo = UsersRepository::new(&state); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let mut user = create_test_user( - "inactive-again@example.com", - "Inactive User", - false, - &role_id, - ); - user.password = crate::hash_password("secret").unwrap(); - repo.query_create_user(user).await.unwrap(); - let payload = json!({ - "email": "inactive-again@example.com", - "password": "secret" - }); - let res = server.post("/v1/auth/login").json(&payload).await; - dbg!(res.text()); - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - println!("🧪 Not active user: {}", body); -} - -#[tokio::test] -async fn test_login_should_succeed() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())); - let server = TestServer::new(app).unwrap(); - let repo = UsersRepository::new(&state); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let mut user = - create_test_user("active@example.com", "Active User", true, &role_id); - user.password = crate::hash_password("secret").unwrap(); - repo.query_create_user(user).await.unwrap(); - let payload = json!({ - "email": "active@example.com", - "password": "secret" - }); - let res = server.post("/v1/auth/login").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::OK); - let json = res.json::(); - assert!(json["data"]["token"]["access_token"].is_string()); - assert!(json["data"]["user"]["email"] == "active@example.com"); -} - -#[tokio::test] -async fn test_login_should_fail_if_payload_empty() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({}); - let res = server.post("/v1/auth/login").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::UNPROCESSABLE_ENTITY); -} - -#[tokio::test] -async fn test_login_should_fail_if_password_empty() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ - "email": "test@test.com", - "password": "" - }); - let res = server.post("/v1/auth/login").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -async fn test_login_should_fail_if_email_not_valid() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ - "email": "test", - "password": "test123" - }); - let res = server.post("/v1/auth/login").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -async fn test_register_should_succeed() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())); - let server = TestServer::new(app).unwrap(); - let random_email = format!("validuser+{}@example.com", Uuid::new_v4()); - let payload = json!({ - "email": random_email, - "password": "Validpass1!", - "fullname": "Valid User", - "student_type": "regular", - "phone_number": "0812345678", - "reffered_by": "Facebook", - "refferal_code": "KFNB" - }); - let res = server.post("/v1/auth/register").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::CREATED); -} - -#[tokio::test] -async fn test_register_should_fail_if_email_already_taken() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())); - let server = TestServer::new(app).unwrap(); - let repo = UsersRepository::new(&state); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let mut user = - create_test_user("duplicate@example.com", "User Exists", false, &role_id); - user.password = crate::hash_password("secret").unwrap(); - repo.query_create_user(user).await.unwrap(); - let payload = json!({ - "fullname": "User Exists", - "email": "duplicate@example.com", - "password": "secret", - "student_type": "general", - "phone_number": "081234567890", - "referral_code": null, - "referred_by": null - }); - let res = server.post("/v1/auth/register").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -async fn test_register_should_fail_if_email_invalid() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ - "fullname": "Invalid Email", - "email": "invalid-email", - "password": "secret", - "student_type": "general", - "phone_number": "081234567890", - "referral_code": null, - "referred_by": null - }); - let res = server.post("/v1/auth/register").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -async fn test_register_should_fail_if_password_empty() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - - let payload = json!({ - "fullname": "No Password", - "email": "nopass@example.com", - "password": "", - "student_type": "general", - "phone_number": "081234567890", - "referral_code": null, - "referred_by": null - }); - let res = server.post("/v1/auth/register").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); -} - -#[tokio::test] -async fn test_register_should_fail_if_payload_empty() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({}); - let res = server.post("/v1/auth/register").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::UNPROCESSABLE_ENTITY); -} - -#[tokio::test] -async fn test_send_otp_should_succeed() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ - "email": "user@example.com" - }); - let res = server.post("/v1/auth/send-otp").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::OK); - let json = res.json::(); - assert_eq!(json["message"], "OTP resent successfully"); -} - -#[tokio::test] -async fn test_send_otp_should_fail_if_email_empty() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ "email": "" }); - let res = server.post("/v1/auth/send-otp").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let json = res.json::(); - assert_eq!(json["message"], "Email cannot be empty, Email not valid"); -} - -#[tokio::test] -async fn test_send_otp_should_fail_with_invalid_email_format() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - - let payload = json!({ "email": "not-an-email" }); - let res = server.post("/v1/auth/send-otp").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let json = res.json::(); - assert_eq!(json["message"], "Email not valid"); -} - -#[tokio::test] -async fn test_send_otp_should_fail_if_payload_empty() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({}); - let res = server.post("/v1/auth/send-otp").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::UNPROCESSABLE_ENTITY); -} - -#[tokio::test] -async fn test_send_otp_should_fail_if_otp_store_fails() { - let mut state = create_mock_app_state().await; - state.surrealdb_mem = surrealdb::Surreal::new::(()) - .await - .expect("failed to create mock surrealdb_mem"); - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = serde_json::json!({ "email": "fail-store@example.com" }); - let res = server.post("/v1/auth/send-otp").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!( - body.contains("Specify a namespace to use") || body.contains("Failed store otp"), - "Expected namespace setup error or store failure, got: {body}" - ); -} - -#[tokio::test] -async fn test_forgot_password_should_succeed() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())); - let server = TestServer::new(app).unwrap(); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let mut user = - create_test_user("forgot@example.com", "Forgot User", true, &role_id); - user.password = crate::hash_password("secret").unwrap(); - let user_repo = UsersRepository::new(&state); - user_repo.query_create_user(user).await.unwrap(); - let payload = json!({ "email": "forgot@example.com" }); - let res = server.post("/v1/auth/forgot").json(&payload).await; - dbg!(res.text()); - assert_eq!(res.status_code(), StatusCode::OK); - let body = res.text(); - assert!( - body.contains("Reset Password request send"), - "Unexpected response: {body}" - ); -} - -#[tokio::test] -async fn test_forgot_password_should_fail_if_user_not_found() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ "email": "notfound@example.com" }); - let res = server.post("/v1/auth/forgot").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!( - body.contains("User not found"), - "Expected 'User not found', got: {body}" - ); -} - -#[tokio::test] -async fn test_forgot_password_should_fail_if_email_invalid() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ "email": "" }); - let res = server.post("/v1/auth/forgot").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!( - body.contains("Email not valid") || body.contains("User not found"), - "Expected email error, got: {body}" - ); -} - -#[tokio::test] -async fn test_refresh_should_succeed() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let valid_token = encode_refresh_token("refreshme@example.com".into()).unwrap(); - let payload = json!({ - "refresh_token": valid_token - }); - let res = server.post("/v1/auth/refresh").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::OK); - let body = res.json::(); - assert!(body["data"]["access_token"].is_string()); - assert!(body["data"]["refresh_token"].is_string()); -} - -#[tokio::test] -async fn test_refresh_should_fail_with_invalid_token() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({ - "refresh_token": "invalid.token.value" - }); - let res = server.post("/v1/auth/refresh").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::UNAUTHORIZED); - let body = res.text(); - assert!(body.contains("Invalid refresh token")); -} - -#[tokio::test] -async fn test_refresh_should_fail_with_empty_payload() { - let state = create_mock_app_state().await; - let app = axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)); - let server = TestServer::new(app).unwrap(); - let payload = json!({}); - let res = server.post("/v1/auth/refresh").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::UNPROCESSABLE_ENTITY); -} - -#[tokio::test] -async fn test_verify_email_should_succeed() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())), - ) - .unwrap(); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let user = create_test_user("verify@example.com", "Verify User", false, &role_id); - let otp = OtpManager::generate_otp(); - let user_repo = UsersRepository::new(&state); - let auth_repo = AuthRepository::new(&state); - user_repo.query_create_user(user.clone()).await.unwrap(); - auth_repo - .query_store_otp(user.email.clone(), otp) - .await - .unwrap(); - let payload = json!({ - "email": user.email, - "otp": otp - }); - let res = server.post("/v1/auth/verify-email").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::OK); - let body = res.text(); - assert!(body.contains("Email verified successfully")); -} - -#[tokio::test] -async fn test_verify_email_should_fail_with_wrong_otp() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())), - ) - .unwrap(); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let user = create_test_user("wrongotp@example.com", "Wrong OTP", false, &role_id); - let otp = OtpManager::generate_otp(); - let user_repo = UsersRepository::new(&state); - let auth_repo = AuthRepository::new(&state); - user_repo.query_create_user(user.clone()).await.unwrap(); - auth_repo - .query_store_otp(user.email.clone(), otp) - .await - .unwrap(); - let payload = json!({ - "email": user.email, - "otp": 999_999 - }); - let res = server.post("/v1/auth/verify-email").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!(body.contains("Failed to verify OTP")); -} - -#[tokio::test] -async fn test_verify_email_should_fail_if_otp_expired() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())), - ) - .unwrap(); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let user = create_test_user("expired@example.com", "Expired OTP", false, &role_id); - let user_repo = UsersRepository::new(&state); - user_repo.query_create_user(user.clone()).await.unwrap(); - use chrono::{Duration, Utc}; - let expired_otp = AuthOtpSchema { - otp: 123456, - expires_at: Utc::now() - Duration::minutes(1), - }; - let _: Option = state - .surrealdb_mem - .create((ResourceEnum::OtpCache.to_string(), user.email.clone())) - .content(expired_otp) - .await - .unwrap(); - let payload = json!({ - "email": user.email, - "otp": 123456 - }); - let res = server.post("/v1/auth/verify-email").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!(body.contains("OTP expired")); -} - -#[tokio::test] -async fn test_verify_email_should_fail_with_empty_payload() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)), - ) - .unwrap(); - let res = server.post("/v1/auth/verify-email").json(&json!({})).await; - assert_eq!(res.status_code(), StatusCode::UNPROCESSABLE_ENTITY); -} - -#[tokio::test] -async fn test_new_password_should_succeed() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state.clone())), - ) - .unwrap(); - let email = "resetme@example.com"; - let repo = UsersRepository::new(&state); - let role_repo = RolesRepository::new(&state); - let role_id = role_repo - .query_role_by_name("Student".to_string()) - .await - .unwrap() - .id; - let mut user = create_test_user(email, "Reset User", true, &role_id); - user.password = crate::hash_password("oldpass123!").unwrap(); - repo.query_create_user(user).await.unwrap(); - let token = crate::encode_reset_password_token(email.to_string()).unwrap(); - let payload = json!({ - "token": token, - "password": "Newpass123!" - }); - let res = server.post("/v1/auth/new-password").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::OK); - let body = res.text(); - assert!(body.contains("Success update password user")); -} - -#[tokio::test] -async fn test_new_password_should_fail_if_token_is_empty() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)), - ) - .unwrap(); - let payload = json!({ - "token": "", - "password": "Newpass123!" - }); - let res = server.post("/v1/auth/new-password").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!( - body.contains("Invalid or missing token"), - "Gagal deteksi token kosong: {body}" - ); -} - -#[tokio::test] -async fn test_new_password_should_fail_if_password_is_weak() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)), - ) - .unwrap(); - let email = "weakpass@example.com"; - let token = crate::encode_reset_password_token(email.to_string()).unwrap(); - let payload = json!({ - "token": token, - "password": "123" - }); - let res = server.post("/v1/auth/new-password").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!( - body.contains("Password must have at least") - || body.contains("Password must include"), - "Pesan error nggak sesuai: {body}" - ); -} - -#[tokio::test] -async fn test_new_password_should_fail_if_user_not_found() { - let state = create_mock_app_state().await; - let server = TestServer::new( - axum::Router::new() - .nest("/v1/auth", auth_router()) - .layer(Extension(state)), - ) - .unwrap(); - let token = - crate::encode_reset_password_token("ghost@example.com".into()).unwrap(); - let payload = json!({ - "token": token, - "password": "Validpass123!" - }); - let res = server.post("/v1/auth/new-password").json(&payload).await; - assert_eq!(res.status_code(), StatusCode::BAD_REQUEST); - let body = res.text(); - assert!(body.contains("User not found")); -} diff --git a/tests/src/iam/auth/mod.rs b/tests/src/iam/auth/mod.rs index 63301aa..4d30d1c 100644 --- a/tests/src/iam/auth/mod.rs +++ b/tests/src/iam/auth/mod.rs @@ -1,4 +1,2 @@ #[cfg(test)] -pub mod auth_controller_test; -#[cfg(test)] pub mod auth_repository_test; diff --git a/tests/src/iam/roles/roles_repository_test.rs b/tests/src/iam/roles/roles_repository_test.rs index 79d3e63..34d4c25 100644 --- a/tests/src/iam/roles/roles_repository_test.rs +++ b/tests/src/iam/roles/roles_repository_test.rs @@ -74,7 +74,7 @@ async fn test_query_update_role_should_update_name_and_permissions() { let original_perm_id = Uuid::new_v4().to_string(); let original_perm = PermissionsSchema { id: make_thing(&ResourceEnum::Permissions.to_string(), &original_perm_id), - name: "Original Permission".into(), + name: generate_unique_name("original_permission"), is_deleted: false, created_at: Some(crate::get_iso_date()), updated_at: None, @@ -83,13 +83,14 @@ async fn test_query_update_role_should_update_name_and_permissions() { .query_create_permission(original_perm) .await .unwrap(); + let role_upadate_name = generate_unique_name("role_for_update"); let create_payload = RolesRequestCreateDto { - name: "Role For Update".into(), + name: role_upadate_name.clone(), permissions: vec![original_perm_id.clone()], }; repo.query_create_role(create_payload).await.unwrap(); let existing_role = repo - .query_role_by_name("Role For Update".into()) + .query_role_by_name(role_upadate_name.clone()) .await .unwrap(); let existing_role_id = existing_role.id.clone(); @@ -101,9 +102,10 @@ async fn test_query_update_role_should_update_name_and_permissions() { created_at: Some(crate::get_iso_date()), updated_at: None, }; + let new_role_name = generate_unique_name("updated_role_name"); perm_repo.query_create_permission(new_perm).await.unwrap(); let update_payload = RolesRequestUpdateDto { - name: Some("Updated Role Name".into()), + name: Some(new_role_name.clone()), permissions: Some(vec![new_perm_id.clone()]), overwrite: None, }; @@ -115,7 +117,7 @@ async fn test_query_update_role_should_update_name_and_permissions() { .query_role_by_id(existing_role_id.clone()) .await .unwrap(); - assert_eq!(updated.name, "Updated Role Name"); + assert_eq!(updated.name, new_role_name.clone()); } #[tokio::test] diff --git a/tests/src/iam/users/users_repository_test.rs b/tests/src/iam/users/users_repository_test.rs index 1c4ba75..30246c9 100644 --- a/tests/src/iam/users/users_repository_test.rs +++ b/tests/src/iam/users/users_repository_test.rs @@ -1,13 +1,24 @@ +use imphnen_iam::RolesRequestCreateDto; use surrealdb::Uuid; use crate::{MetaRequestDto, UsersRepository}; use crate::{RolesRepository, create_mock_app_state, create_test_user}; async fn get_role_id(state: &crate::AppState) -> String { - RolesRepository::new(state) + let repo = RolesRepository::new(state); + if let Ok(existing) = repo.query_role_by_name("User".into()).await { + return existing.id; + } + let _ = repo + .query_create_role(RolesRequestCreateDto { + name: "User".into(), + permissions: vec![], + }) + .await; + repo .query_role_by_name("User".into()) .await - .expect("Role not found") + .expect("Role not found after creation") .id }