chore: complete unit test of user and authentication system
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
v1::{
|
v1::{
|
||||||
auth, AuthLoginRequestDto, AuthLoginResponsetDto, AuthResendOtpRequestDto,
|
auth, users, AuthLoginRequestDto, AuthLoginResponsetDto,
|
||||||
AuthVerifyEmailRequestDto,
|
AuthResendOtpRequestDto, AuthVerifyEmailRequestDto,
|
||||||
},
|
},
|
||||||
AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, MessageResponseDto,
|
AuthNewPasswordRequestDto, AuthRefreshTokenRequestDto, MessageResponseDto,
|
||||||
MetaRequestDto, MetaResponseDto, ResponseSuccessDto, TokenDto,
|
MetaRequestDto, MetaResponseDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
TokenDto, UsersItemDto,
|
||||||
};
|
};
|
||||||
|
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
@@ -21,7 +22,13 @@ use utoipa::{
|
|||||||
auth::auth_controller::post_resend_otp,
|
auth::auth_controller::post_resend_otp,
|
||||||
auth::auth_controller::post_refresh_token,
|
auth::auth_controller::post_refresh_token,
|
||||||
auth::auth_controller::post_forgot_password,
|
auth::auth_controller::post_forgot_password,
|
||||||
auth::auth_controller::post_new_password
|
auth::auth_controller::post_new_password,
|
||||||
|
users::users_controller::post_create_user,
|
||||||
|
users::users_controller::put_update_user,
|
||||||
|
users::users_controller::patch_user_active_status,
|
||||||
|
users::users_controller::delete_user,
|
||||||
|
users::users_controller::get_user_by_id,
|
||||||
|
users::users_controller::get_user_list
|
||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
@@ -36,6 +43,7 @@ use utoipa::{
|
|||||||
AuthRefreshTokenRequestDto,
|
AuthRefreshTokenRequestDto,
|
||||||
ResponseSuccessDto<TokenDto>,
|
ResponseSuccessDto<TokenDto>,
|
||||||
ResponseSuccessDto<AuthLoginResponsetDto>,
|
ResponseSuccessDto<AuthLoginResponsetDto>,
|
||||||
|
ResponseListSuccessDto<Vec<UsersItemDto>>
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
info(
|
info(
|
||||||
|
|||||||
+3
-2
@@ -10,8 +10,9 @@ pub use users::*;
|
|||||||
pub async fn routes() -> Router {
|
pub async fn routes() -> Router {
|
||||||
let public_routes = Router::new().nest("/auth", auth_router());
|
let public_routes = Router::new().nest("/auth", auth_router());
|
||||||
|
|
||||||
let protected_routes =
|
let protected_routes = Router::new()
|
||||||
Router::new().layer(from_fn(auth::auth_middleware::auth_middleware));
|
.nest("/users", users_router())
|
||||||
|
.layer(from_fn(auth::auth_middleware::auth_middleware));
|
||||||
|
|
||||||
Router::new().merge(public_routes).merge(protected_routes)
|
Router::new().merge(public_routes).merge(protected_routes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
|
use axum::{
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod users_controller;
|
||||||
pub mod users_dto;
|
pub mod users_dto;
|
||||||
pub mod users_repository;
|
pub mod users_repository;
|
||||||
pub mod users_schema;
|
pub mod users_schema;
|
||||||
|
pub mod users_service;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod users_controller_test;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod users_repository_test;
|
pub mod users_repository_test;
|
||||||
|
|
||||||
|
pub use users_controller::*;
|
||||||
pub use users_dto::*;
|
pub use users_dto::*;
|
||||||
pub use users_repository::*;
|
pub use users_repository::*;
|
||||||
pub use users_schema::*;
|
pub use users_schema::*;
|
||||||
|
pub use users_service::*;
|
||||||
|
|
||||||
|
pub fn users_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(get_user_list))
|
||||||
|
.route("/activate/{id}", put(patch_user_active_status))
|
||||||
|
.route("/create", post(post_create_user))
|
||||||
|
.route("/delete/{id}", delete(delete_user))
|
||||||
|
.route("/detail/{id}", get(get_user_by_id))
|
||||||
|
.route("/update/{id}", put(put_update_user))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use axum::{Extension, Json};
|
||||||
|
|
||||||
|
use crate::{v1::users_service::UsersService, AppState, MetaRequestDto};
|
||||||
|
use crate::{
|
||||||
|
MessageResponseDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
|
UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersItemDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::UsersUpdateRequestDto;
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/users",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
("filter" = Option<String>, Query, description = "Filter value"),
|
||||||
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get user list", body = ResponseListSuccessDto<Vec<UsersItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn get_user_list(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Query(meta): Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
UsersService::get_user_list(&state, meta).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/v1/users/detail/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "User ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get user by ID", body = ResponseSuccessDto<UsersItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn get_user_by_id(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
UsersService::get_user_by_id(&state, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/v1/users/create",
|
||||||
|
request_body = UsersCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Create new user", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn post_create_user(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Json(payload): Json<UsersCreateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
UsersService::create_user(&state, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/users/update/{id}",
|
||||||
|
request_body = UsersCreateRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Update user", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn put_update_user(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Json(payload): Json<UsersUpdateRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
UsersService::update_user(&state, id, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/v1/users/activate/{id}",
|
||||||
|
request_body = UsersActiveInactiveRequestDto,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Set user active/inactive", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn patch_user_active_status(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<String>,
|
||||||
|
Json(payload): Json<UsersActiveInactiveRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
UsersService::set_user_active_status(&state, id, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/v1/users/delete/{id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Soft delete user", body = MessageResponseDto)
|
||||||
|
),
|
||||||
|
tag = "Users"
|
||||||
|
)]
|
||||||
|
pub async fn delete_user(
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
UsersService::delete_user(&state, id).await
|
||||||
|
}
|
||||||
@@ -0,0 +1,507 @@
|
|||||||
|
use crate::{
|
||||||
|
apps::v1::users_router, create_mock_app_state, UsersActiveInactiveRequestDto,
|
||||||
|
UsersCreateRequestDto, UsersRepository, UsersUpdateRequestDto,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, Extension};
|
||||||
|
use axum_test::TestServer;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_user_list_should_return_200() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server.get("/v1/users?page=1&per_page=10").await;
|
||||||
|
let status = res.status_code();
|
||||||
|
assert_eq!(status, StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user_should_return_201() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Create Data #1".into(),
|
||||||
|
email: "test@create.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
let res = server.post("/v1/users/create").json(&payload).await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_should_return_200() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Create Data #1".into(),
|
||||||
|
email: "test@create.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
let create_res = server.post("/v1/users/create").json(&payload).await;
|
||||||
|
assert_eq!(create_res.status_code(), StatusCode::CREATED);
|
||||||
|
let detail_res = repo.query_user_by_email(payload.email);
|
||||||
|
let user_id = detail_res.await.unwrap().id.id.to_raw();
|
||||||
|
let delete_res = server
|
||||||
|
.delete(&format!("/v1/users/delete/{}", user_id))
|
||||||
|
.await;
|
||||||
|
assert_eq!(delete_res.status_code(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_user_detail_should_return_200() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Detail User".into(),
|
||||||
|
email: "detail@test.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
let res = server.get(&format!("/v1/users/detail/{}", user_id)).await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_activate_user_should_return_200() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Inactive User".into(),
|
||||||
|
email: "inactive@test.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: false,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
let res = server
|
||||||
|
.put(&format!("/v1/users/activate/{}", user_id))
|
||||||
|
.json(&UsersActiveInactiveRequestDto { is_active: true })
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_user_should_return_200() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Old Name".into(),
|
||||||
|
email: "update@test.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
let update_payload = UsersUpdateRequestDto {
|
||||||
|
fullname: "Updated Name".into(),
|
||||||
|
email: "update@test.com".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
identity_number: Some("1234567890123456".into()),
|
||||||
|
religion: Some("Islam".into()),
|
||||||
|
gender: Some("Laki-laki".into()),
|
||||||
|
birthdate: Some("2000-01-01".into()),
|
||||||
|
avatar: None,
|
||||||
|
};
|
||||||
|
let res = server
|
||||||
|
.put(&format!("/v1/users/update/{}", user_id))
|
||||||
|
.json(&update_payload)
|
||||||
|
.await;
|
||||||
|
let status = res.status_code();
|
||||||
|
assert_eq!(status, StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user_should_fail_if_email_taken() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "User Satu".into(),
|
||||||
|
email: "taken@example.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
let res1 = server.post("/v1/users/create").json(&payload).await;
|
||||||
|
assert_eq!(res1.status_code(), StatusCode::CREATED);
|
||||||
|
let res2 = server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let body2 = res2.text();
|
||||||
|
assert_eq!(res2.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
assert!(body2.contains("User already exists"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_user_detail_should_fail_if_not_found() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server.get("/v1/users/detail/non-existent-id").await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delete_user_should_fail_if_already_deleted() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "To Be Deleted".into(),
|
||||||
|
email: "delete-me@example.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let id = user.id.id.to_raw();
|
||||||
|
let res1 = server.delete(&format!("/v1/users/delete/{}", id)).await;
|
||||||
|
assert_eq!(res1.status_code(), StatusCode::OK);
|
||||||
|
let res2 = server.delete(&format!("/v1/users/delete/{}", id)).await;
|
||||||
|
assert_eq!(res2.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = res2.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_user_should_fail_if_user_not_found() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let update_payload = UsersUpdateRequestDto {
|
||||||
|
fullname: "Does Not Exist".into(),
|
||||||
|
email: "nonexistent@test.com".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
identity_number: Some("1234567890123456".into()),
|
||||||
|
religion: Some("Islam".into()),
|
||||||
|
gender: Some("Laki-laki".into()),
|
||||||
|
birthdate: Some("2000-01-01".into()),
|
||||||
|
avatar: None,
|
||||||
|
};
|
||||||
|
let res = server
|
||||||
|
.put("/v1/users/update/non-existent-id")
|
||||||
|
.json(&update_payload)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_activate_user_should_fail_if_user_not_found() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersActiveInactiveRequestDto { is_active: true };
|
||||||
|
let res = server
|
||||||
|
.put("/v1/users/activate/non-existent-id")
|
||||||
|
.json(&payload)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_create_user_should_fail_if_payload_invalid() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let invalid_payload = serde_json::json!({});
|
||||||
|
let res = server.post("/v1/users/create").json(&invalid_payload).await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_users_should_fail_with_invalid_pagination() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server.get("/v1/users?page=0&per_page=0").await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(
|
||||||
|
body.contains("Invalid pagination") || body.contains("per_page"),
|
||||||
|
"Expected pagination error, got: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_users_should_ignore_invalid_sort_field() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server
|
||||||
|
.get("/v1/users?page=1&per_page=10&sort_by=nonexistent_field")
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_users_should_fallback_on_invalid_order() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server
|
||||||
|
.get("/v1/users?page=1&per_page=10&sort_by=email&order=invalid")
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_users_with_search_should_return_empty_if_no_match() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server
|
||||||
|
.get("/v1/users?page=1&per_page=10&search=no-user-should-match-this")
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
let body: serde_json::Value = res.json();
|
||||||
|
assert_eq!(body["data"].as_array().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_users_with_invalid_filter_by_should_return_empty() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server
|
||||||
|
.get("/v1/users?page=1&per_page=10&filter_by=unknown_field&filter=value")
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::OK);
|
||||||
|
let body: serde_json::Value = res.json();
|
||||||
|
assert_eq!(body["data"].as_array().unwrap().len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_user_detail_should_fail_with_invalid_id_format() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server.get("/v1/users/detail/!@#invalid-id").await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found") || body.contains("not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_user_detail_should_fail_if_user_not_found() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let res = server.get("/v1/users/detail/random-id-123456").await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_user_detail_should_fail_if_user_is_soft_deleted() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Soft Deleted".into(),
|
||||||
|
email: "softdeleted@test.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
let _ = repo.query_delete_user(user_id.clone()).await.unwrap();
|
||||||
|
let res = server.get(&format!("/v1/users/detail/{}", user_id)).await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_user_should_fail_if_user_is_deleted() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "To Be Deleted".into(),
|
||||||
|
email: "deleteduser@test.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
let _ = repo.query_delete_user(user_id.clone()).await.unwrap();
|
||||||
|
let update_payload = UsersUpdateRequestDto {
|
||||||
|
fullname: "Should Fail".into(),
|
||||||
|
email: "deleteduser@test.com".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
identity_number: Some("1234567890123456".into()),
|
||||||
|
religion: Some("Islam".into()),
|
||||||
|
gender: Some("Laki-laki".into()),
|
||||||
|
birthdate: Some("2000-01-01".into()),
|
||||||
|
avatar: None,
|
||||||
|
};
|
||||||
|
let res = server
|
||||||
|
.put(&format!("/v1/users/update/{}", user_id))
|
||||||
|
.json(&update_payload)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(body.contains("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update_user_should_fail_if_payload_invalid() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.nest("/v1/users", users_router())
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
let server = TestServer::new(app).unwrap();
|
||||||
|
let payload = UsersCreateRequestDto {
|
||||||
|
fullname: "Invalid Payload".into(),
|
||||||
|
email: "invalid@test.com".into(),
|
||||||
|
password: "Password1!".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
};
|
||||||
|
server.post("/v1/users/create").json(&payload).await;
|
||||||
|
let user = repo.query_user_by_email(payload.email).await.unwrap();
|
||||||
|
let user_id = user.id.id.to_raw();
|
||||||
|
let update_payload = UsersUpdateRequestDto {
|
||||||
|
fullname: "".into(),
|
||||||
|
email: "invalid@test.com".into(),
|
||||||
|
student_type: "general".into(),
|
||||||
|
phone_number: "081234567890".into(),
|
||||||
|
is_active: true,
|
||||||
|
referral_code: None,
|
||||||
|
referred_by: None,
|
||||||
|
identity_number: Some("1234567890123456".into()),
|
||||||
|
religion: Some("Islam".into()),
|
||||||
|
gender: Some("Laki-laki".into()),
|
||||||
|
birthdate: Some("2000-01-01".into()),
|
||||||
|
avatar: None,
|
||||||
|
};
|
||||||
|
let res = server
|
||||||
|
.put(&format!("/v1/users/update/{}", user_id))
|
||||||
|
.json(&update_payload)
|
||||||
|
.await;
|
||||||
|
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
|
||||||
|
let body = res.text();
|
||||||
|
assert!(
|
||||||
|
body.contains("Fullname") || body.contains("at least have 2 character"),
|
||||||
|
"Gagal validasi fullname: {body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,80 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
lazy_static! {
|
||||||
|
static ref PASSWORD_REGEX: Regex = Regex::new(r"^[A-Za-z\d@$!%*?&]{8,}$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
pub struct UsersCreateRequestDto {
|
pub struct UsersCreateRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub fullname: String,
|
#[validate(length(
|
||||||
|
min = 8,
|
||||||
|
message = "Password must have at least 8 characters"
|
||||||
|
))]
|
||||||
|
#[validate(regex(
|
||||||
|
path = "PASSWORD_REGEX",
|
||||||
|
message = "Password must include uppercase, lowercase, number, and special character"
|
||||||
|
))]
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||||
|
pub fullname: String,
|
||||||
|
#[validate(length(min = 1, message = "Student type is required"))]
|
||||||
|
pub student_type: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 10,
|
||||||
|
message = "Phone number at least have 10 character"
|
||||||
|
))]
|
||||||
|
pub phone_number: String,
|
||||||
|
#[validate(length(
|
||||||
|
max = 4,
|
||||||
|
message = "Referal code cannot be more than 4 character"
|
||||||
|
))]
|
||||||
|
pub referral_code: Option<String>,
|
||||||
|
pub referred_by: Option<String>,
|
||||||
|
pub is_active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||||
|
pub struct UsersUpdateRequestDto {
|
||||||
|
#[validate(
|
||||||
|
length(min = 1, message = "Email cannot be empty"),
|
||||||
|
email(message = "Email not valid")
|
||||||
|
)]
|
||||||
|
pub email: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 8,
|
||||||
|
message = "Password must have at least 8 characters"
|
||||||
|
))]
|
||||||
|
#[validate(length(min = 2, message = "Fullname at least have 2 character"))]
|
||||||
|
pub fullname: String,
|
||||||
|
#[validate(length(min = 1, message = "Student type is required"))]
|
||||||
|
pub student_type: String,
|
||||||
|
#[validate(length(
|
||||||
|
min = 10,
|
||||||
|
message = "Phone number at least have 10 character"
|
||||||
|
))]
|
||||||
|
pub phone_number: String,
|
||||||
|
#[validate(length(
|
||||||
|
max = 4,
|
||||||
|
message = "Referal code cannot be more than 4 character"
|
||||||
|
))]
|
||||||
|
pub referral_code: Option<String>,
|
||||||
|
pub referred_by: Option<String>,
|
||||||
|
pub is_active: bool,
|
||||||
|
#[validate(length(min = 16, message = "NIK at must have 16 character"))]
|
||||||
|
pub identity_number: Option<String>,
|
||||||
|
pub religion: Option<String>,
|
||||||
|
pub gender: Option<String>,
|
||||||
|
pub birthdate: Option<String>,
|
||||||
|
pub avatar: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
@@ -15,13 +84,18 @@ pub struct UsersItemDto {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub phone_number: String,
|
pub phone_number: String,
|
||||||
pub referral_code: Option<String>,
|
|
||||||
pub referred_by: Option<String>,
|
pub referred_by: Option<String>,
|
||||||
pub identity_number: Option<String>,
|
pub referral_code: Option<String>,
|
||||||
pub is_active: bool,
|
|
||||||
pub student_type: String,
|
pub student_type: String,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_profile_completed: bool,
|
||||||
|
pub identity_number: Option<String>,
|
||||||
pub religion: Option<String>,
|
pub religion: Option<String>,
|
||||||
pub gender: Option<String>,
|
pub gender: Option<String>,
|
||||||
pub birthdate: Option<String>,
|
pub birthdate: Option<String>,
|
||||||
pub is_profile_completed: bool,
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct UsersActiveInactiveRequestDto {
|
||||||
|
pub is_active: bool,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use super::{UsersActiveInactiveSchema, UsersSchema, UsersSetNewPasswordSchema};
|
use super::{UsersActiveInactiveSchema, UsersSchema, UsersSetNewPasswordSchema};
|
||||||
use crate::{
|
use crate::{
|
||||||
get_id, AppState, CountResult, MetaRequestDto, MetaResponseDto, ResourceEnum,
|
get_id, make_thing, AppState, CountResult, MetaRequestDto, MetaResponseDto,
|
||||||
ResponseListSuccessDto,
|
ResourceEnum, ResponseListSuccessDto,
|
||||||
};
|
};
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
@@ -23,6 +23,11 @@ impl<'a> UsersRepository<'a> {
|
|||||||
|
|
||||||
let page = meta.page.unwrap_or(1);
|
let page = meta.page.unwrap_or(1);
|
||||||
let per_page = meta.per_page.unwrap_or(10);
|
let per_page = meta.per_page.unwrap_or(10);
|
||||||
|
|
||||||
|
if page < 1 || per_page < 1 {
|
||||||
|
bail!("Invalid pagination: page and per_page must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
let start = (page - 1) * per_page;
|
let start = (page - 1) * per_page;
|
||||||
|
|
||||||
let mut sql = format!("SELECT * FROM {}", table);
|
let mut sql = format!("SELECT * FROM {}", table);
|
||||||
@@ -43,7 +48,17 @@ impl<'a> UsersRepository<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sort_by) = &meta.sort_by {
|
if let Some(sort_by) = &meta.sort_by {
|
||||||
let order = meta.order.clone().unwrap_or_else(|| "ASC".to_string());
|
let order = match meta
|
||||||
|
.order
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "ASC".to_string())
|
||||||
|
.to_uppercase()
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"ASC" => "ASC",
|
||||||
|
"DESC" => "DESC",
|
||||||
|
_ => "ASC", // fallback kalau order invalid
|
||||||
|
};
|
||||||
sql.push_str(&format!(" ORDER BY {} {}", sort_by, order));
|
sql.push_str(&format!(" ORDER BY {} {}", sort_by, order));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +161,16 @@ impl<'a> UsersRepository<'a> {
|
|||||||
pub async fn query_update_user(&self, data: UsersSchema) -> Result<String> {
|
pub async fn query_update_user(&self, data: UsersSchema) -> Result<String> {
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let record_key = get_id(&data.id)?;
|
let record_key = get_id(&data.id)?;
|
||||||
let record: Option<UsersSchema> = db.update(record_key).merge(data).await?;
|
let existing = self.query_user_by_id(data.id.id.to_raw()).await?;
|
||||||
|
if existing.is_deleted {
|
||||||
|
bail!("User already deleted");
|
||||||
|
}
|
||||||
|
let merged = UsersSchema {
|
||||||
|
password: existing.password,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
..data.clone()
|
||||||
|
};
|
||||||
|
let record: Option<UsersSchema> = db.update(record_key).merge(merged).await?;
|
||||||
match record {
|
match record {
|
||||||
Some(_) => Ok("Success update user".into()),
|
Some(_) => Ok("Success update user".into()),
|
||||||
None => bail!("Failed to update user"),
|
None => bail!("Failed to update user"),
|
||||||
@@ -160,6 +184,9 @@ impl<'a> UsersRepository<'a> {
|
|||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let user = self.query_user_by_email(email.clone()).await?;
|
let user = self.query_user_by_email(email.clone()).await?;
|
||||||
|
if user.is_deleted {
|
||||||
|
bail!("User not found");
|
||||||
|
}
|
||||||
let record_key = get_id(&user.id)?;
|
let record_key = get_id(&user.id)?;
|
||||||
let record: Option<UsersSchema> = db
|
let record: Option<UsersSchema> = db
|
||||||
.update(record_key)
|
.update(record_key)
|
||||||
@@ -173,6 +200,24 @@ impl<'a> UsersRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn query_active_inactive_user_by_id(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
data: UsersActiveInactiveSchema,
|
||||||
|
) -> Result<String> {
|
||||||
|
let db = &self.state.surrealdb_ws;
|
||||||
|
let record: Option<UsersSchema> = db
|
||||||
|
.update((ResourceEnum::Users.to_string(), id))
|
||||||
|
.merge(UsersActiveInactiveSchema {
|
||||||
|
is_active: data.is_active,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
match record {
|
||||||
|
Some(_) => Ok("Success update user".into()),
|
||||||
|
None => bail!("Failed to update user"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn query_update_password_user(
|
pub async fn query_update_password_user(
|
||||||
&self,
|
&self,
|
||||||
email: String,
|
email: String,
|
||||||
@@ -193,9 +238,13 @@ impl<'a> UsersRepository<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn query_delete_user(&self, email: String) -> Result<String> {
|
pub async fn query_delete_user(&self, id: String) -> Result<String> {
|
||||||
let db = &self.state.surrealdb_ws;
|
let db = &self.state.surrealdb_ws;
|
||||||
let user = self.query_user_by_email(email.clone()).await?;
|
let user_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
|
let user = self.query_user_by_id(user_id.id.to_raw()).await?;
|
||||||
|
if user.is_deleted {
|
||||||
|
bail!("User already deleted");
|
||||||
|
}
|
||||||
let record_key = get_id(&user.id)?;
|
let record_key = get_id(&user.id)?;
|
||||||
let record: Option<UsersSchema> = db
|
let record: Option<UsersSchema> = db
|
||||||
.update(record_key)
|
.update(record_key)
|
||||||
|
|||||||
@@ -164,9 +164,17 @@ async fn test_query_delete_user() {
|
|||||||
let email = "deleteuser@example.com";
|
let email = "deleteuser@example.com";
|
||||||
let user = create_test_user(email, "Delete User", true);
|
let user = create_test_user(email, "Delete User", true);
|
||||||
repo.query_create_user(user.clone()).await.unwrap();
|
repo.query_create_user(user.clone()).await.unwrap();
|
||||||
let delete_result = repo.query_delete_user(email.to_string()).await;
|
let user_detail = repo
|
||||||
|
.query_user_by_email(email.to_string().clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let delete_result = repo
|
||||||
|
.query_delete_user(user_detail.id.id.to_raw().clone())
|
||||||
|
.await;
|
||||||
assert!(delete_result.is_ok());
|
assert!(delete_result.is_ok());
|
||||||
let fetch_result = repo.query_user_by_email(email.to_string()).await;
|
let fetch_result = repo
|
||||||
|
.query_user_by_email(user_detail.id.id.to_raw().clone())
|
||||||
|
.await;
|
||||||
assert!(fetch_result.is_err());
|
assert!(fetch_result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +182,7 @@ async fn test_query_delete_user() {
|
|||||||
async fn test_delete_non_existent_user_should_fail() {
|
async fn test_delete_non_existent_user_should_fail() {
|
||||||
let app_state = create_mock_app_state().await;
|
let app_state = create_mock_app_state().await;
|
||||||
let repo = UsersRepository::new(&app_state);
|
let repo = UsersRepository::new(&app_state);
|
||||||
let result = repo
|
let result = repo.query_delete_user("lklklklk".to_string()).await;
|
||||||
.query_delete_user("nonexistent@example.com".to_string())
|
|
||||||
.await;
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert_eq!(result.unwrap_err().to_string(), "User not found");
|
assert_eq!(result.unwrap_err().to_string(), "User not found");
|
||||||
}
|
}
|
||||||
@@ -188,9 +194,24 @@ async fn test_delete_user_twice_should_fail_on_second_attempt() {
|
|||||||
let email = "twice@example.com";
|
let email = "twice@example.com";
|
||||||
let user = create_test_user(email, "Delete Twice", true);
|
let user = create_test_user(email, "Delete Twice", true);
|
||||||
repo.query_create_user(user.clone()).await.unwrap();
|
repo.query_create_user(user.clone()).await.unwrap();
|
||||||
let first = repo.query_delete_user(email.to_string()).await;
|
let first = repo.query_delete_user(user.id.id.to_raw()).await;
|
||||||
assert!(first.is_ok());
|
assert!(first.is_ok());
|
||||||
let second = repo.query_delete_user(email.to_string()).await;
|
let second = repo.query_delete_user(user.id.id.to_raw()).await;
|
||||||
assert!(second.is_err());
|
assert!(second.is_err());
|
||||||
assert_eq!(second.unwrap_err().to_string(), "User not found");
|
assert_eq!(second.unwrap_err().to_string(), "User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_query_update_user_should_succeed() {
|
||||||
|
let state = create_mock_app_state().await;
|
||||||
|
let repo = UsersRepository::new(&state);
|
||||||
|
let mut user = create_test_user("update@example.com", "Old Name", true);
|
||||||
|
repo.query_create_user(user.clone()).await.unwrap();
|
||||||
|
user.fullname = "Updated Name".into();
|
||||||
|
user.phone_number = "089876543210".into();
|
||||||
|
let result = repo.query_update_user(user.clone()).await;
|
||||||
|
assert!(result.is_ok(), "Update failed: {:?}", result.err());
|
||||||
|
let updated = repo.query_user_by_id(user.id.id.to_raw()).await.unwrap();
|
||||||
|
assert_eq!(updated.fullname, "Updated Name");
|
||||||
|
assert_eq!(updated.phone_number, "089876543210");
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
use crate::{
|
||||||
|
common_response, get_iso_date, hash_password, make_thing, success_list_response,
|
||||||
|
success_response, validate_request, ResourceEnum, ResponseSuccessDto,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
AppState, MetaRequestDto, ResponseListSuccessDto, UsersActiveInactiveSchema,
|
||||||
|
UsersRepository, UsersSchema, UsersSetNewPasswordSchema,
|
||||||
|
};
|
||||||
|
use axum::{http::StatusCode, response::Response};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersUpdateRequestDto,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct UsersService;
|
||||||
|
|
||||||
|
impl UsersService {
|
||||||
|
pub async fn get_user_list(state: &AppState, meta: MetaRequestDto) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_user_list(meta).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let response = ResponseListSuccessDto {
|
||||||
|
data: data.data,
|
||||||
|
meta: data.meta,
|
||||||
|
};
|
||||||
|
success_list_response(response)
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_by_email(state: &AppState, email: String) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_user_by_email(email).await {
|
||||||
|
Ok(user) => success_response(ResponseSuccessDto { data: user }),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_by_id(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_user_by_id(id).await {
|
||||||
|
Ok(user) => success_response(ResponseSuccessDto { data: user }),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_user(
|
||||||
|
state: &AppState,
|
||||||
|
new_user: UsersCreateRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
if let Err((status, message)) = validate_request(&new_user) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
|
||||||
|
if repo
|
||||||
|
.query_user_by_email(new_user.email.clone())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User already exists");
|
||||||
|
}
|
||||||
|
match repo
|
||||||
|
.query_create_user(UsersSchema {
|
||||||
|
email: new_user.email.clone(),
|
||||||
|
fullname: new_user.fullname.clone(),
|
||||||
|
password: hash_password(&new_user.password).unwrap(),
|
||||||
|
phone_number: new_user.phone_number.clone(),
|
||||||
|
referral_code: new_user.referral_code.clone(),
|
||||||
|
referred_by: new_user.referred_by.clone(),
|
||||||
|
student_type: new_user.student_type.clone(),
|
||||||
|
is_active: new_user.is_active.clone(),
|
||||||
|
is_profile_completed: false,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::CREATED, &msg),
|
||||||
|
Err(err) => {
|
||||||
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
user: UsersUpdateRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
|
||||||
|
if let Err((status, message)) = validate_request(&user) {
|
||||||
|
return common_response(status, &message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_id = make_thing(&ResourceEnum::Users.to_string(), &id);
|
||||||
|
let role_id = make_thing(&ResourceEnum::Roles.to_string(), "");
|
||||||
|
|
||||||
|
let updated_user = UsersSchema {
|
||||||
|
id: user_id,
|
||||||
|
fullname: user.fullname,
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number,
|
||||||
|
referral_code: user.referral_code,
|
||||||
|
referred_by: user.referred_by,
|
||||||
|
identity_number: user.identity_number,
|
||||||
|
is_active: user.is_active,
|
||||||
|
student_type: user.student_type,
|
||||||
|
religion: user.religion,
|
||||||
|
gender: user.gender,
|
||||||
|
birthdate: user.birthdate,
|
||||||
|
avatar: user.avatar,
|
||||||
|
is_profile_completed: false,
|
||||||
|
role: role_id,
|
||||||
|
updated_at: Some(get_iso_date()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match repo.query_update_user(updated_user).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_user_active_status(
|
||||||
|
state: &AppState,
|
||||||
|
id: String,
|
||||||
|
status: UsersActiveInactiveRequestDto,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
|
||||||
|
if repo.query_user_by_id(id.clone()).await.is_err() {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
|
}
|
||||||
|
match repo
|
||||||
|
.query_active_inactive_user_by_id(
|
||||||
|
id,
|
||||||
|
UsersActiveInactiveSchema {
|
||||||
|
is_active: status.is_active,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_user_password(
|
||||||
|
state: &AppState,
|
||||||
|
email: String,
|
||||||
|
new_password: UsersSetNewPasswordSchema,
|
||||||
|
) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
match repo.query_update_password_user(email, new_password).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_user(state: &AppState, id: String) -> Response {
|
||||||
|
let repo = UsersRepository::new(state);
|
||||||
|
if repo.query_user_by_id(id.clone()).await.is_err() {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "User not found");
|
||||||
|
}
|
||||||
|
match repo.query_delete_user(id).await {
|
||||||
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user