chore: add test coverage
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
apps::v1::users_router, create_mock_app_state, UsersActiveInactiveRequestDto,
|
||||
UsersCreateRequestDto, UsersRepository, UsersUpdateRequestDto,
|
||||
apps::v1::users_router, create_mock_app_state, RolesEnum, RolesRepository,
|
||||
UsersActiveInactiveRequestDto, UsersCreateRequestDto, UsersRepository,
|
||||
UsersUpdateRequestDto,
|
||||
};
|
||||
use axum::{http::StatusCode, Extension};
|
||||
use axum_test::TestServer;
|
||||
@@ -20,6 +21,12 @@ async fn test_get_user_list_should_return_200() {
|
||||
#[tokio::test]
|
||||
async fn test_create_user_should_return_201() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id;
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
.layer(Extension(state));
|
||||
@@ -27,6 +34,7 @@ async fn test_create_user_should_return_201() {
|
||||
let payload = UsersCreateRequestDto {
|
||||
fullname: "Create Data #1".into(),
|
||||
email: "test@create.com".into(),
|
||||
role_id,
|
||||
password: "Password1!".into(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
@@ -41,6 +49,12 @@ async fn test_create_user_should_return_201() {
|
||||
#[tokio::test]
|
||||
async fn test_delete_user_should_return_200() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id;
|
||||
let repo = UsersRepository::new(&state);
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
@@ -51,6 +65,7 @@ async fn test_delete_user_should_return_200() {
|
||||
email: "test@create.com".into(),
|
||||
password: "Password1!".into(),
|
||||
student_type: "general".into(),
|
||||
role_id,
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: true,
|
||||
referral_code: None,
|
||||
@@ -69,6 +84,12 @@ async fn test_delete_user_should_return_200() {
|
||||
#[tokio::test]
|
||||
async fn test_get_user_detail_should_return_200() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id;
|
||||
let repo = UsersRepository::new(&state);
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
@@ -77,6 +98,7 @@ async fn test_get_user_detail_should_return_200() {
|
||||
let payload = UsersCreateRequestDto {
|
||||
fullname: "Detail User".into(),
|
||||
email: "detail@test.com".into(),
|
||||
role_id,
|
||||
password: "Password1!".into(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
@@ -94,6 +116,12 @@ async fn test_get_user_detail_should_return_200() {
|
||||
#[tokio::test]
|
||||
async fn test_activate_user_should_return_200() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id;
|
||||
let repo = UsersRepository::new(&state);
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
@@ -103,6 +131,7 @@ async fn test_activate_user_should_return_200() {
|
||||
fullname: "Inactive User".into(),
|
||||
email: "inactive@test.com".into(),
|
||||
password: "Password1!".into(),
|
||||
role_id,
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: false,
|
||||
@@ -122,6 +151,12 @@ async fn test_activate_user_should_return_200() {
|
||||
#[tokio::test]
|
||||
async fn test_update_user_should_return_200() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
let repo = UsersRepository::new(&state);
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
@@ -131,6 +166,7 @@ async fn test_update_user_should_return_200() {
|
||||
fullname: "Old Name".into(),
|
||||
email: "update@test.com".into(),
|
||||
password: "Password1!".into(),
|
||||
role_id: role_id.clone(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: true,
|
||||
@@ -145,6 +181,7 @@ async fn test_update_user_should_return_200() {
|
||||
email: "update@test.com".into(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
role_id: role_id.clone(),
|
||||
is_active: true,
|
||||
referral_code: None,
|
||||
referred_by: None,
|
||||
@@ -165,6 +202,12 @@ async fn test_update_user_should_return_200() {
|
||||
#[tokio::test]
|
||||
async fn test_create_user_should_fail_if_email_taken() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
.layer(Extension(state.clone()));
|
||||
@@ -174,6 +217,7 @@ async fn test_create_user_should_fail_if_email_taken() {
|
||||
email: "taken@example.com".into(),
|
||||
password: "Password1!".into(),
|
||||
student_type: "general".into(),
|
||||
role_id,
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: true,
|
||||
referral_code: None,
|
||||
@@ -203,6 +247,12 @@ async fn test_get_user_detail_should_fail_if_not_found() {
|
||||
#[tokio::test]
|
||||
async fn test_delete_user_should_fail_if_already_deleted() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
let repo = UsersRepository::new(&state);
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
@@ -211,6 +261,7 @@ async fn test_delete_user_should_fail_if_already_deleted() {
|
||||
let payload = UsersCreateRequestDto {
|
||||
fullname: "To Be Deleted".into(),
|
||||
email: "delete-me@example.com".into(),
|
||||
role_id,
|
||||
password: "Password1!".into(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
@@ -232,6 +283,12 @@ async fn test_delete_user_should_fail_if_already_deleted() {
|
||||
#[tokio::test]
|
||||
async fn test_update_user_should_fail_if_user_not_found() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
.layer(Extension(state));
|
||||
@@ -241,6 +298,7 @@ async fn test_update_user_should_fail_if_user_not_found() {
|
||||
email: "nonexistent@test.com".into(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
role_id,
|
||||
is_active: true,
|
||||
referral_code: None,
|
||||
referred_by: None,
|
||||
@@ -330,21 +388,6 @@ async fn test_list_users_should_fallback_on_invalid_order() {
|
||||
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;
|
||||
@@ -389,12 +432,19 @@ async fn test_user_detail_should_fail_if_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 role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
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 {
|
||||
role_id,
|
||||
fullname: "Soft Deleted".into(),
|
||||
email: "softdeleted@test.com".into(),
|
||||
password: "Password1!".into(),
|
||||
@@ -417,12 +467,19 @@ async fn test_user_detail_should_fail_if_user_is_soft_deleted() {
|
||||
#[tokio::test]
|
||||
async fn test_update_user_should_fail_if_user_is_deleted() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
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 {
|
||||
role_id: role_id.clone(),
|
||||
fullname: "To Be Deleted".into(),
|
||||
email: "deleteduser@test.com".into(),
|
||||
password: "Password1!".into(),
|
||||
@@ -437,6 +494,7 @@ async fn test_update_user_should_fail_if_user_is_deleted() {
|
||||
let user_id = user.id.id.to_raw();
|
||||
let _ = repo.query_delete_user(user_id.clone()).await.unwrap();
|
||||
let update_payload = UsersUpdateRequestDto {
|
||||
role_id: role_id.clone(),
|
||||
fullname: "Should Fail".into(),
|
||||
email: "deleteduser@test.com".into(),
|
||||
student_type: "general".into(),
|
||||
@@ -462,6 +520,12 @@ async fn test_update_user_should_fail_if_user_is_deleted() {
|
||||
#[tokio::test]
|
||||
async fn test_update_user_should_fail_if_payload_invalid() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let role = role_repo
|
||||
.query_role_by_name(RolesEnum::Student.to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let role_id = role.id.clone();
|
||||
let repo = UsersRepository::new(&state);
|
||||
let app = axum::Router::new()
|
||||
.nest("/v1/users", users_router())
|
||||
@@ -470,6 +534,7 @@ async fn test_update_user_should_fail_if_payload_invalid() {
|
||||
let payload = UsersCreateRequestDto {
|
||||
fullname: "Invalid Payload".into(),
|
||||
email: "invalid@test.com".into(),
|
||||
role_id: role_id.clone(),
|
||||
password: "Password1!".into(),
|
||||
student_type: "general".into(),
|
||||
phone_number: "081234567890".into(),
|
||||
@@ -487,6 +552,7 @@ async fn test_update_user_should_fail_if_payload_invalid() {
|
||||
phone_number: "081234567890".into(),
|
||||
is_active: true,
|
||||
referral_code: None,
|
||||
role_id: role_id.clone(),
|
||||
referred_by: None,
|
||||
identity_number: Some("1234567890123456".into()),
|
||||
religion: Some("Islam".into()),
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct UsersCreateRequestDto {
|
||||
pub referral_code: Option<String>,
|
||||
pub referred_by: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
|
||||
@@ -75,6 +76,7 @@ pub struct UsersUpdateRequestDto {
|
||||
pub gender: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{UsersActiveInactiveSchema, UsersSchema, UsersSetNewPasswordSchema};
|
||||
use crate::{
|
||||
get_id, make_thing, AppState, CountResult, MetaRequestDto, MetaResponseDto,
|
||||
ResourceEnum, ResponseListSuccessDto,
|
||||
get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto, ResourceEnum,
|
||||
ResponseListSuccessDto,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
@@ -20,102 +20,15 @@ impl<'a> UsersRepository<'a> {
|
||||
) -> Result<ResponseListSuccessDto<Vec<UsersSchema>>> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let table = ResourceEnum::Users.to_string();
|
||||
|
||||
let page = meta.page.unwrap_or(1);
|
||||
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 mut sql = format!("SELECT * FROM {}", table);
|
||||
let mut conditions = vec!["is_deleted = false".to_string()];
|
||||
|
||||
if meta.search.is_some() {
|
||||
conditions.push("string::contains(fullname, $search)".to_string());
|
||||
conditions.push("string::contains(name, $search)".into());
|
||||
}
|
||||
|
||||
if meta.filter_by.is_some() && meta.filter.is_some() {
|
||||
let filter_by = meta.filter_by.as_ref().unwrap();
|
||||
conditions.push(format!("{} = $filter", filter_by));
|
||||
}
|
||||
|
||||
if !conditions.is_empty() {
|
||||
sql.push_str(" WHERE ");
|
||||
sql.push_str(&conditions.join(" AND "));
|
||||
}
|
||||
|
||||
if let Some(sort_by) = &meta.sort_by {
|
||||
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(" LIMIT $per_page START $start");
|
||||
|
||||
let mut query = db.query(sql);
|
||||
|
||||
if let Some(search) = meta.search.clone() {
|
||||
query = query.bind(("search", search));
|
||||
}
|
||||
|
||||
if let Some(filter_val) = meta.filter.clone() {
|
||||
if let Ok(b) = filter_val.parse::<bool>() {
|
||||
query = query.bind(("filter", b));
|
||||
} else if let Ok(i) = filter_val.parse::<i64>() {
|
||||
query = query.bind(("filter", i));
|
||||
} else {
|
||||
query = query.bind(("filter", filter_val));
|
||||
}
|
||||
}
|
||||
|
||||
query = query.bind(("per_page", per_page)).bind(("start", start));
|
||||
|
||||
let users: Vec<UsersSchema> = query.await?.take(0)?;
|
||||
|
||||
let mut count_sql =
|
||||
format!("SELECT count() FROM {} WHERE is_deleted = false", table);
|
||||
if meta.filter_by.is_some() && meta.filter.is_some() {
|
||||
let filter_by = meta.filter_by.as_ref().unwrap();
|
||||
count_sql.push_str(&format!(" AND {} = $filter", filter_by));
|
||||
}
|
||||
|
||||
let mut count_query = db.query(count_sql);
|
||||
|
||||
if let Some(filter_val) = meta.filter.clone() {
|
||||
if let Ok(b) = filter_val.parse::<bool>() {
|
||||
count_query = count_query.bind(("filter", b));
|
||||
} else if let Ok(i) = filter_val.parse::<i64>() {
|
||||
count_query = count_query.bind(("filter", i));
|
||||
} else {
|
||||
count_query = count_query.bind(("filter", filter_val));
|
||||
}
|
||||
}
|
||||
|
||||
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
|
||||
let meta_res = MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
};
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: users,
|
||||
meta: Some(meta_res),
|
||||
})
|
||||
query_list_with_meta::<UsersSchema>(db, &table, &meta, conditions).await
|
||||
}
|
||||
|
||||
pub async fn query_user_by_email(&self, email: String) -> Result<UsersSchema> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::create_mock_app_state;
|
||||
use crate::{create_mock_app_state, ResourceEnum};
|
||||
use crate::{
|
||||
make_thing, MetaRequestDto, UsersActiveInactiveSchema, UsersRepository,
|
||||
UsersSchema, UsersSetNewPasswordSchema,
|
||||
@@ -23,7 +23,7 @@ fn create_test_user(email: &str, fullname: &str, is_active: bool) -> UsersSchema
|
||||
gender: None,
|
||||
birthdate: None,
|
||||
is_profile_completed: false,
|
||||
role: make_thing("roles", "user"),
|
||||
role: make_thing(&ResourceEnum::Roles.to_string(), "user"),
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ impl UsersService {
|
||||
return common_response(status, &message);
|
||||
}
|
||||
let repo = UsersRepository::new(state);
|
||||
|
||||
if repo
|
||||
.query_user_by_email(new_user.email.clone())
|
||||
.await
|
||||
@@ -61,6 +60,7 @@ impl UsersService {
|
||||
{
|
||||
return common_response(StatusCode::BAD_REQUEST, "User already exists");
|
||||
}
|
||||
let role_thing = make_thing(&ResourceEnum::Roles.to_string(), &new_user.role_id);
|
||||
match repo
|
||||
.query_create_user(UsersSchema {
|
||||
email: new_user.email.clone(),
|
||||
@@ -71,6 +71,7 @@ impl UsersService {
|
||||
referred_by: new_user.referred_by.clone(),
|
||||
student_type: new_user.student_type.clone(),
|
||||
is_active: new_user.is_active.clone(),
|
||||
role: role_thing,
|
||||
is_profile_completed: false,
|
||||
..Default::default()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user