chore: implement unit test

This commit is contained in:
Maulana Sodiqin
2025-03-23 07:56:43 +07:00
parent 093e290221
commit 79721b7af6
17 changed files with 1384 additions and 26 deletions
+3
View File
@@ -2,6 +2,9 @@ pub mod users_dto;
pub mod users_repository;
pub mod users_schema;
#[cfg(test)]
pub mod users_repository_test;
pub use users_dto::*;
pub use users_repository::*;
pub use users_schema::*;
+12 -1
View File
@@ -10,7 +10,18 @@ pub struct UsersCreateRequestDto {
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct UsersItemDto {
pub email: String,
pub id: String,
pub fullname: String,
pub email: String,
pub avatar: Option<String>,
pub phone_number: String,
pub referral_code: Option<String>,
pub referred_by: Option<String>,
pub identity_number: Option<String>,
pub is_active: bool,
pub student_type: String,
pub religion: Option<String>,
pub gender: Option<String>,
pub birthdate: Option<String>,
pub is_profile_completed: bool,
}
+114 -10
View File
@@ -1,5 +1,8 @@
use super::{UsersActiveInactiveSchema, UsersSchema, UsersSetNewPasswordSchema};
use crate::{get_id, AppState, ResourceEnum};
use crate::{
get_id, AppState, CountResult, MetaRequestDto, MetaResponseDto, ResourceEnum,
ResponseListSuccessDto,
};
use anyhow::{bail, Result};
pub struct UsersRepository<'a> {
@@ -11,10 +14,99 @@ impl<'a> UsersRepository<'a> {
Self { state }
}
pub async fn query_user_list(
&self,
meta: MetaRequestDto,
) -> 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);
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());
}
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 = meta.order.clone().unwrap_or_else(|| "ASC".to_string());
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),
})
}
pub async fn query_user_by_email(&self, email: String) -> Result<UsersSchema> {
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT * FROM {} WHERE email = $email",
"SELECT * FROM {} WHERE email = $email AND is_deleted = false",
ResourceEnum::Users.to_string()
);
let mut response: Vec<UsersSchema> = db
@@ -22,22 +114,20 @@ impl<'a> UsersRepository<'a> {
.bind(("email", email.clone()))
.await?
.take(0)?;
if let Some(user) = response.pop() {
Ok(user)
} else {
bail!("User not found")
match response.pop() {
Some(user) => Ok(user),
None => bail!("User not found"),
}
}
pub async fn query_user_by_id(&self, id: String) -> Result<UsersSchema> {
let db = &self.state.surrealdb_ws;
let result = db
let result: Option<UsersSchema> = db
.select((ResourceEnum::Users.to_string(), id.clone()))
.await?;
match result {
Some(response) => Ok(response),
None => bail!("User not found"),
Some(response) if !response.is_deleted => Ok(response),
_ => bail!("User not found"),
}
}
@@ -102,4 +192,18 @@ impl<'a> UsersRepository<'a> {
None => bail!("Failed to update password user"),
}
}
pub async fn query_delete_user(&self, email: String) -> Result<String> {
let db = &self.state.surrealdb_ws;
let user = self.query_user_by_email(email.clone()).await?;
let record_key = get_id(&user.id)?;
let record: Option<UsersSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
.await?;
match record {
Some(_) => Ok("Success delete user".into()),
None => bail!("Failed to delete user"),
}
}
}
+196
View File
@@ -0,0 +1,196 @@
use crate::create_mock_app_state;
use crate::{
make_thing, MetaRequestDto, UsersActiveInactiveSchema, UsersRepository,
UsersSchema, UsersSetNewPasswordSchema,
};
use surrealdb::Uuid;
fn create_test_user(email: &str, fullname: &str, is_active: bool) -> UsersSchema {
UsersSchema {
id: make_thing("app_users", &Uuid::new_v4().to_string()),
email: email.to_string(),
fullname: fullname.to_string(),
password: "password".to_string(),
is_deleted: false,
avatar: None,
phone_number: "081234567890".to_string(),
referral_code: None,
referred_by: None,
identity_number: None,
is_active,
student_type: "general".to_string(),
religion: None,
gender: None,
birthdate: None,
is_profile_completed: false,
role: make_thing("roles", "user"),
created_at: None,
updated_at: None,
}
}
#[tokio::test]
async fn test_create_and_get_user() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let user = create_test_user("testuser@example.com", "Test User", true);
let create_result = repo.query_create_user(user.clone()).await;
assert!(create_result.is_ok());
let fetched = repo
.query_user_by_email("testuser@example.com".into())
.await;
assert!(fetched.is_ok());
assert_eq!(fetched.unwrap().email, "testuser@example.com");
}
#[tokio::test]
async fn test_update_password_user() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let email = "changepass@example.com";
let user = create_test_user(email, "Change Password", true);
repo.query_create_user(user).await.unwrap();
let result = repo
.query_update_password_user(
email.into(),
UsersSetNewPasswordSchema {
password: "newpass".into(),
},
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_query_user_list_with_pagination_and_filter() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
for i in 0..10 {
let email = format!("user{}@example.com", i);
let fullname = format!("User {}", i);
let is_active = i % 2 == 0;
let user = create_test_user(&email, &fullname, is_active);
repo.query_create_user(user).await.unwrap();
}
let meta = MetaRequestDto {
page: Some(1),
per_page: Some(5),
search: None,
sort_by: Some("email".into()),
order: Some("ASC".into()),
filter: Some("true".into()),
filter_by: Some("is_active".into()),
};
let result = repo.query_user_list(meta).await.unwrap();
assert!(result.data.len() <= 5);
assert!(result.data.iter().all(|u| u.is_active));
assert!(result.meta.as_ref().unwrap().total.is_some());
}
#[tokio::test]
async fn test_query_user_list_basic() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
for i in 0..10 {
let email = format!("basic{}@example.com", i);
let user = create_test_user(&email, &format!("Basic User {}", i), true);
repo.query_create_user(user).await.unwrap();
}
let meta = MetaRequestDto {
page: Some(1),
per_page: Some(5),
search: None,
sort_by: None,
order: None,
filter: None,
filter_by: None,
};
let result = repo.query_user_list(meta).await.unwrap();
assert!(result.meta.as_ref().unwrap().total.unwrap() >= 1);
assert_eq!(result.meta.as_ref().unwrap().page.unwrap(), 1);
assert_eq!(result.meta.as_ref().unwrap().per_page.unwrap(), 5);
}
#[tokio::test]
async fn test_query_active_inactive_user() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let email = "inactive@example.com";
let user = create_test_user(email, "Inactive User", false);
repo.query_create_user(user).await.unwrap();
let result = repo
.query_active_inactive_user(
email.into(),
UsersActiveInactiveSchema { is_active: true },
)
.await;
assert!(result.is_ok());
let updated = repo
.query_user_by_email("inactive@example.com".into())
.await
.unwrap();
assert_eq!(updated.is_active, true);
}
#[tokio::test]
async fn test_query_user_by_invalid_email_should_fail() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let result = repo
.query_user_by_email("nonexistent@example.com".into())
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_query_update_password_for_nonexistent_user_should_fail() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let result = repo
.query_update_password_user(
"ghost@example.com".into(),
UsersSetNewPasswordSchema {
password: "secret".into(),
},
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_query_delete_user() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let email = "deleteuser@example.com";
let user = create_test_user(email, "Delete User", true);
repo.query_create_user(user.clone()).await.unwrap();
let delete_result = repo.query_delete_user(email.to_string()).await;
assert!(delete_result.is_ok());
let fetch_result = repo.query_user_by_email(email.to_string()).await;
assert!(fetch_result.is_err());
}
#[tokio::test]
async fn test_delete_non_existent_user_should_fail() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let result = repo
.query_delete_user("nonexistent@example.com".to_string())
.await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "User not found");
}
#[tokio::test]
async fn test_delete_user_twice_should_fail_on_second_attempt() {
let app_state = create_mock_app_state().await;
let repo = UsersRepository::new(&app_state);
let email = "twice@example.com";
let user = create_test_user(email, "Delete Twice", true);
repo.query_create_user(user.clone()).await.unwrap();
let first = repo.query_delete_user(email.to_string()).await;
assert!(first.is_ok());
let second = repo.query_delete_user(email.to_string()).await;
assert!(second.is_err());
assert_eq!(second.unwrap_err().to_string(), "User not found");
}
+2
View File
@@ -17,6 +17,7 @@ pub struct UsersSchema {
pub referred_by: Option<String>,
pub identity_number: Option<String>,
pub is_active: bool,
pub is_deleted: bool,
pub student_type: String,
pub religion: Option<String>,
pub gender: Option<String>,
@@ -43,6 +44,7 @@ impl Default for UsersSchema {
referred_by: None,
identity_number: None,
is_active: false,
is_deleted: false,
student_type: String::new(),
religion: None,
gender: None,