chore: add test coverage

This commit is contained in:
Maulana Sodiqin
2025-03-23 20:42:38 +07:00
parent 0ae1e26ba8
commit 5be737ad70
26 changed files with 1057 additions and 132 deletions
+83 -17
View File
@@ -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()),
+2
View File
@@ -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)]
+4 -91
View File
@@ -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> {
+2 -2
View File
@@ -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,
}
+2 -1
View File
@@ -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()
})