chore: update query list

This commit is contained in:
Maulana Sodiqin
2025-05-20 16:57:45 +07:00
parent 055ef6ac8e
commit d906fc4747
18 changed files with 165 additions and 1139 deletions
+8
View File
@@ -102,6 +102,14 @@ For production deployment:
Adjust the port and environment variables as needed.
## How to Run the Tests
1. **Run the tests**:
```sh
cargo test
```
## How to Contribute
1. **Fork the repository** and clone it locally.
@@ -38,7 +38,7 @@ impl GachaClaimItemDto {
Self {
id: dto.id.id.to_raw(),
user: UsersDetailItemDto::from(&dto.user),
item: GachaItemDto::from(&dto.item),
item: GachaItemDto::from(dto.item.clone()),
is_deleted: dto.is_deleted,
created_at: dto.created_at.clone(),
updated_at: dto.updated_at.clone(),
@@ -21,7 +21,7 @@ pub struct GachaItemDto {
}
impl GachaItemDto {
pub fn from(dto: &GachaItemSchema) -> Self {
pub fn from(dto: GachaItemSchema) -> Self {
Self {
id: dto.id.id.to_raw(),
name: dto.name.clone(),
@@ -1,9 +1,10 @@
use super::GachaItemSchema;
use crate::{
AppState, GachaItemDto, MetaRequestDto, ResourceEnum, ResponseListSuccessDto,
get_id, make_thing, query_list_with_meta,
get_id, make_thing,
};
use anyhow::{Result, bail};
use imphnen_iam::QueryListBuilder;
pub struct GachaItemRepository<'a> {
state: &'a AppState,
@@ -19,24 +20,23 @@ impl<'a> GachaItemRepository<'a> {
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<GachaItemDto>>> {
let raw_result: ResponseListSuccessDto<Vec<GachaItemSchema>> =
query_list_with_meta(
QueryListBuilder::new(
&self.state.surrealdb_ws,
&ResourceEnum::GachaItems.to_string(),
&meta,
vec!["is_deleted = false".into()],
None,
"name",
Some(vec!["*"]),
None,
)
.with_condition("is_deleted = false")
.search_field("name")
.select_fields(vec!["*"])
.build()
.await?;
let transformed_data = raw_result
let data = raw_result
.data
.into_iter()
.map(|gacha_item| GachaItemDto::from(&gacha_item))
.map(GachaItemDto::from)
.collect();
Ok(ResponseListSuccessDto {
data: transformed_data,
data,
meta: raw_result.meta,
})
}
@@ -31,7 +31,7 @@ impl GachaItemService {
let repo = GachaItemRepository::new(state);
match repo.query_gacha_item_by_id(id).await {
Ok(item) => success_response(ResponseSuccessDto {
data: GachaItemDto::from(&item),
data: GachaItemDto::from(item),
}),
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
}
@@ -29,7 +29,7 @@ impl GachaRollItemDto {
pub fn from(dto: &GachaRollQueryDto) -> Self {
Self {
id: dto.id.id.to_raw(),
item: GachaItemDto::from(&dto.item),
item: GachaItemDto::from(dto.item.clone()),
weight: dto.weight.to_string(),
quantity: dto.quantity,
is_deleted: dto.is_deleted,
@@ -1,10 +1,9 @@
use super::{PermissionsItemDto, PermissionsSchema};
use crate::{
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id,
make_thing, query_list_with_meta,
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id, make_thing,
};
use anyhow::{Result, bail};
use imphnen_utils::{DetailQueryBuilder, extract_id};
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, extract_id};
pub struct PermissionsRepository<'a> {
state: &'a AppState,
@@ -20,22 +19,23 @@ impl<'a> PermissionsRepository<'a> {
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<PermissionsItemDto>>> {
let raw_result: ResponseListSuccessDto<Vec<PermissionsSchema>> =
query_list_with_meta(
QueryListBuilder::new(
&self.state.surrealdb_ws,
&ResourceEnum::Permissions.to_string(),
&meta,
vec!["is_deleted = false".into()],
None,
"name",
Some(vec!["*"]),
None,
)
.with_condition("is_deleted = false")
.search_field("name")
.select_fields(vec!["*"])
.build()
.await?;
let transformed_data = raw_result
.data
.into_iter()
.map(|permission| PermissionsSchema::list(&permission))
.collect();
Ok(ResponseListSuccessDto {
data: transformed_data,
meta: raw_result.meta,
+12 -75
View File
@@ -3,11 +3,10 @@ use super::{
RolesRequestUpdateDto, RolesSchema,
};
use crate::{
AppState, MetaRequestDto, PermissionsItemDto, ResourceEnum,
ResponseListSuccessDto, extract_id, get_id, make_thing, query_list_with_meta,
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id, make_thing,
};
use anyhow::{Result, bail};
use imphnen_utils::DetailQueryBuilder;
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder};
use surrealdb::Uuid;
use surrealdb::sql::Thing;
@@ -24,36 +23,23 @@ impl<'a> RolesRepository<'a> {
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<RolesListItemDto>>> {
let mut conditions = vec!["is_deleted = false".into()];
if let Some(_search) = meta.search.as_deref().filter(|s| !s.is_empty()) {
conditions.push("string::contains(name ?? '', $search)".into());
}
if let (Some(filter_by), Some(filter_val)) =
(meta.filter_by.as_ref(), meta.filter.as_ref())
{
if !filter_val.is_empty() {
conditions.push(format!("{} = $filter", filter_by));
}
}
let raw_result: ResponseListSuccessDto<Vec<RolesSchema>> = query_list_with_meta(
let result: ResponseListSuccessDto<Vec<RolesSchema>> = QueryListBuilder::new(
&self.state.surrealdb_ws,
&ResourceEnum::Roles.to_string(),
&meta,
conditions,
None,
"name",
None,
None,
)
.with_condition("is_deleted = false")
.search_field("name")
.build()
.await?;
let data = raw_result
let data = result
.data
.into_iter()
.map(|role| RolesSchema::list(&role))
.collect();
Ok(ResponseListSuccessDto {
data,
meta: raw_result.meta,
meta: result.meta,
})
}
@@ -65,16 +51,8 @@ impl<'a> RolesRepository<'a> {
let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string())
.with_where("name")
.where_value(name.clone())
.with_select_fields(vec![
"id",
"name",
"permissions",
"created_at",
"updated_at",
"is_deleted",
])
.with_select_fields(vec!["*"])
.with_fetch("permissions");
let sql = builder.build();
let result: Option<RolesDetailQueryDto> = builder
.apply_bindings(db.query(sql).bind(("name", name)))
@@ -84,38 +62,14 @@ impl<'a> RolesRepository<'a> {
Some(r) if !r.is_deleted => r,
_ => bail!("Role not found"),
};
let permissions = role
.permissions
.into_iter()
.map(|perm| PermissionsItemDto {
id: extract_id(&perm.id),
name: perm.name,
created_at: perm.created_at,
updated_at: perm.updated_at,
})
.collect();
Ok(RolesDetailItemDto {
id: extract_id(&role.id),
name: role.name,
is_deleted: role.is_deleted,
permissions,
created_at: role.created_at,
updated_at: role.updated_at,
})
Ok(RolesDetailItemDto::from(&role))
}
pub async fn query_role_by_id(&self, id: String) -> Result<RolesDetailItemDto> {
let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Roles.to_string())
.with_id(&id)
.with_select_fields(vec![
"id",
"name",
"is_deleted",
"permissions",
"created_at",
"updated_at",
])
.with_select_fields(vec!["*"])
.with_fetch("permissions");
let sql = builder.build();
let result: Option<RolesDetailQueryDto> =
@@ -124,24 +78,7 @@ impl<'a> RolesRepository<'a> {
Some(r) if !r.is_deleted => r,
_ => bail!("Role not found"),
};
let permissions = role
.permissions
.into_iter()
.map(|perm| PermissionsItemDto {
id: extract_id(&perm.id),
name: perm.name,
created_at: perm.created_at,
updated_at: perm.updated_at,
})
.collect();
Ok(RolesDetailItemDto {
id: extract_id(&role.id),
name: role.name,
is_deleted: role.is_deleted,
permissions,
created_at: role.created_at,
updated_at: role.updated_at,
})
Ok(RolesDetailItemDto::from(&role))
}
pub async fn query_create_role(
+15 -16
View File
@@ -1,10 +1,9 @@
use super::{UsersDetailQueryDto, UsersListItemDto, UsersListQueryDto, UsersSchema};
use crate::{
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id,
make_thing, query_list_with_meta,
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto, get_id, make_thing,
};
use anyhow::{Result, bail};
use imphnen_utils::DetailQueryBuilder;
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder};
use surrealdb::{Surreal, engine::remote::ws::Client};
pub struct UsersRepository<'a> {
@@ -35,23 +34,23 @@ impl<'a> UsersRepository<'a> {
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<UsersListItemDto>>> {
let db = &self.state.surrealdb_ws;
let result = query_list_with_meta::<UsersListQueryDto>(
db,
&ResourceEnum::Users.to_string(),
&meta,
vec!["is_deleted = false".into()],
None,
"fullname",
Some(vec!["*"]),
Some(vec!["role", "role.permissions"]),
)
.await?;
let result: ResponseListSuccessDto<Vec<UsersListQueryDto>> =
QueryListBuilder::new(
&self.state.surrealdb_ws,
&ResourceEnum::Users.to_string(),
&meta,
)
.with_condition("is_deleted = false")
.search_field("fullname")
.select_fields(vec!["*"])
.fetch_fields(vec!["role", "role.permissions"])
.build()
.await?;
let data = result
.data
.into_iter()
.map(UsersListQueryDto::from)
.collect::<Vec<_>>();
.collect();
Ok(ResponseListSuccessDto {
data,
meta: result.meta,
+101 -63
View File
@@ -1,78 +1,116 @@
use super::{ListQueryBuilder, bind_filter_value};
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
use anyhow::Result;
use serde::{Serialize, de::DeserializeOwned};
use surrealdb::{Surreal, engine::remote::ws::Client};
pub async fn query_list_with_meta<T>(
db: &Surreal<Client>,
table: &str,
meta: &MetaRequestDto,
pub struct QueryListBuilder<'a> {
db: &'a Surreal<Client>,
table: &'a str,
meta: &'a MetaRequestDto,
conditions: Vec<String>,
custom_select: Option<String>,
search_field: &str,
select_fields: Option<Vec<&str>>,
fetch_fields: Option<Vec<&str>>,
) -> Result<ResponseListSuccessDto<Vec<T>>>
where
T: DeserializeOwned + Serialize,
{
let page = meta.page.unwrap_or(1).max(1);
let per_page = meta.per_page.unwrap_or(10).max(1);
let start = (page - 1) * per_page;
search_field: String,
select_fields: Option<Vec<&'a str>>,
fetch_fields: Option<Vec<&'a str>>,
}
let sql = custom_select.unwrap_or_else(|| {
ListQueryBuilder::from_meta(
impl<'a> QueryListBuilder<'a> {
pub fn new(
db: &'a Surreal<Client>,
table: &'a str,
meta: &'a MetaRequestDto,
) -> Self {
Self {
db,
table,
meta,
search_field,
select_fields,
fetch_fields,
conditions: vec![],
search_field: "name".to_string(),
select_fields: None,
fetch_fields: None,
}
}
pub fn search_field(mut self, field: &'a str) -> Self {
self.search_field = field.to_string();
self
}
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
self.select_fields = Some(fields);
self
}
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
self.fetch_fields = Some(fields);
self
}
pub fn with_condition(mut self, condition: &str) -> Self {
self.conditions.push(condition.to_string());
self
}
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
where
T: DeserializeOwned + Serialize,
{
let page = self.meta.page.unwrap_or(1).max(1);
let per_page = self.meta.per_page.unwrap_or(10).max(1);
let start = (page - 1) * per_page;
let sql = crate::ListQueryBuilder::from_meta(
self.table,
self.meta,
&self.search_field,
self.select_fields,
self.fetch_fields,
)
.build()
});
.build();
let mut query_exec = db.query(sql);
if let Some(search) = &meta.search {
if !search.is_empty() {
query_exec = query_exec.bind(("search", search.to_lowercase().clone()));
let mut query_exec = self.db.query(sql);
if let Some(search) = &self.meta.search {
if !search.is_empty() {
query_exec = query_exec.bind(("search", search.to_lowercase()));
}
}
}
if let Some(filter_val) = &meta.filter {
query_exec = bind_filter_value(query_exec, filter_val.clone());
}
query_exec = query_exec
.bind(("per_page", per_page))
.bind(("start", start));
let raw: Vec<T> = query_exec.await?.take(0)?;
let mut count_query = db.query(format!(
"SELECT count() FROM {} {}",
table,
if conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
if let Some(filter_val) = &self.meta.filter {
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
}
));
if let Some(search) = &meta.search {
if !search.is_empty() {
count_query = count_query.bind(("search", search.clone()));
}
}
if let Some(filter_val) = &meta.filter {
count_query = bind_filter_value(count_query, filter_val.clone());
}
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
let total = count_result.first().map(|c| c.count);
query_exec = query_exec
.bind(("per_page", per_page))
.bind(("start", start));
Ok(ResponseListSuccessDto {
data: raw,
meta: Some(MetaResponseDto {
page: Some(page),
per_page: Some(per_page),
total,
}),
})
let raw: Vec<T> = query_exec.await?.take(0)?;
let mut count_query = self.db.query(format!(
"SELECT count() FROM {} {}",
self.table,
if self.conditions.is_empty() {
"".into()
} else {
format!("WHERE {}", self.conditions.join(" AND "))
}
));
if let Some(search) = &self.meta.search {
if !search.is_empty() {
count_query = count_query.bind(("search", search.clone()));
}
}
if let Some(filter_val) = &self.meta.filter {
count_query = crate::bind_filter_value(count_query, filter_val.clone());
}
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
let total = count_result.first().map(|c| c.count);
Ok(ResponseListSuccessDto {
data: raw,
meta: Some(MetaResponseDto {
page: Some(page),
per_page: Some(per_page),
total,
}),
})
}
}
-2
View File
@@ -1,4 +1,2 @@
#[cfg(test)]
pub mod permissions_controller_test;
#[cfg(test)]
pub mod permissions_repository_test;
@@ -1,171 +0,0 @@
use crate::{
create_mock_app_state, permissions_router,
v1::{
permissions_dto::PermissionsRequestDto,
permissions_repository::PermissionsRepository,
},
AppState,
};
use axum::{http::StatusCode, Extension, Router};
use axum_test::TestServer;
pub fn create_test_app(state: AppState) -> TestServer {
let app = Router::new()
.nest("/v1/permissions", permissions_router())
.layer(Extension(state));
TestServer::new(app).unwrap()
}
async fn create_dummy_permission(
repo: &PermissionsRepository<'_>,
name: &str,
) -> String {
let dto = crate::v1::permissions_schema::PermissionsSchema {
name: name.into(),
..Default::default()
};
let _ = repo.query_create_permission(dto.clone()).await.unwrap();
let found = repo.query_permission_by_name(name.into()).await.unwrap();
found.id.id.to_raw()
}
async fn delete_dummy_permission(repo: &PermissionsRepository<'_>, id: String) {
let _ = repo.query_delete_permission(id).await;
}
#[tokio::test]
async fn test_create_permission_should_return_201() {
let state = create_mock_app_state().await;
let repo = PermissionsRepository::new(&state);
let server = create_test_app(state.clone());
let payload = PermissionsRequestDto {
name: "Create Permission".into(),
};
let res = server.post("/v1/permissions/create").json(&payload).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::CREATED);
let id = repo
.query_permission_by_name(payload.name.clone())
.await
.unwrap()
.id
.id
.to_raw();
delete_dummy_permission(&repo, id).await;
}
#[tokio::test]
async fn test_get_permission_list_should_return_200() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let res = server.get("/v1/permissions?page=1&per_page=10").await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
}
#[tokio::test]
async fn test_get_permission_by_id_should_return_200() {
let state = create_mock_app_state().await;
let repo = PermissionsRepository::new(&state);
let id = create_dummy_permission(&repo, "Get Permission").await;
let server = create_test_app(state.clone());
let res = server.get(&format!("/v1/permissions/detail/{}", id)).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
delete_dummy_permission(&repo, id).await;
}
#[tokio::test]
async fn test_update_permission_should_return_200() {
let state = create_mock_app_state().await;
let repo = PermissionsRepository::new(&state);
let id = create_dummy_permission(&repo, "To Update").await;
let server = create_test_app(state.clone());
let payload = PermissionsRequestDto {
name: "Updated Permission".into(),
};
let res = server
.put(&format!("/v1/permissions/update/{}", id.clone()))
.json(&payload)
.await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
delete_dummy_permission(&repo, id).await;
}
#[tokio::test]
async fn test_delete_permission_should_return_200() {
let state = create_mock_app_state().await;
let repo = PermissionsRepository::new(&state);
let id = create_dummy_permission(&repo, "To Delete").await;
let server = create_test_app(state);
let res = server
.delete(&format!("/v1/permissions/delete/{}", id))
.await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
}
#[tokio::test]
async fn test_create_duplicate_permission_should_return_409() {
let state = create_mock_app_state().await;
let repo = PermissionsRepository::new(&state);
let name = "Duplicate Permission";
let id = create_dummy_permission(&repo, name).await;
let server = create_test_app(state.clone());
let payload = PermissionsRequestDto { name: name.into() };
let res = server.post("/v1/permissions/create").json(&payload).await;
assert_eq!(res.status_code(), StatusCode::CONFLICT);
delete_dummy_permission(&repo, id).await;
}
#[tokio::test]
async fn test_delete_nonexistent_permission_should_return_404() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let res = server.delete("/v1/permissions/delete/nonexistent-id").await;
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_create_permission_with_empty_name_should_return_400() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let payload = PermissionsRequestDto { name: "".into() };
let res = server.post("/v1/permissions/create").json(&payload).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_update_nonexistent_permission_should_return_404() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let payload = PermissionsRequestDto {
name: "Nonexistent".into(),
};
let res = server
.put("/v1/permissions/update/non-exist")
.json(&payload)
.await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_get_permission_by_invalid_id_should_return_404() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let res = server.get("/v1/permissions/detail/invalid-id").await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_get_permission_list_with_invalid_pagination_should_return_400() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let res = server.get("/v1/permissions?page=abc&per_page=xyz").await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
}
-2
View File
@@ -1,4 +1,2 @@
#[cfg(test)]
pub mod roles_controller_test;
#[cfg(test)]
pub mod roles_repository_test;
@@ -1,174 +0,0 @@
use crate::AppState;
use crate::{
RolesRequestCreateDto, RolesRequestUpdateDto, create_mock_app_state, roles_router,
v1::roles_repository::RolesRepository,
};
use axum::Extension;
use axum::Router;
use axum::http::StatusCode;
use axum_test::TestServer;
pub fn create_test_app(state: AppState) -> TestServer {
let app = Router::new()
.nest("/v1/roles", roles_router())
.layer(Extension(state));
TestServer::new(app).unwrap()
}
async fn delete_dummy_role(state: AppState, id: String) {
let repo = RolesRepository::new(&state);
let _ = repo.query_delete_role(id).await;
}
#[tokio::test]
async fn test_get_role_list_should_return_200() {
let state = create_mock_app_state().await;
let app = axum::Router::new()
.nest("/v1/roles", roles_router())
.layer(axum::Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let res = server.get("/v1/roles?page=1&per_page=10").await;
assert_eq!(res.status_code(), StatusCode::OK);
}
#[tokio::test]
async fn test_post_create_role_should_return_201() {
let state = create_mock_app_state().await;
let app = axum::Router::new()
.nest("/v1/roles", roles_router())
.layer(axum::Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let payload = RolesRequestCreateDto {
name: "Test Role #1".into(),
permissions: vec![],
};
let res = server.post("/v1/roles/create").json(&payload).await;
let repo = RolesRepository::new(&state);
assert_eq!(res.status_code(), StatusCode::CREATED);
let id = repo
.query_role_by_name(payload.name.clone())
.await
.unwrap()
.id;
delete_dummy_role(state, id).await;
}
#[tokio::test]
async fn test_put_update_role_should_return_200() {
let state = create_mock_app_state().await;
let repo = RolesRepository::new(&state);
let _ = repo
.query_create_role(RolesRequestCreateDto {
name: "Role To Update".into(),
permissions: vec![],
})
.await
.unwrap();
let existing = repo
.query_role_by_name("Role To Update".into())
.await
.unwrap();
let id = existing.id.clone();
let server = create_test_app(state.clone());
let payload = RolesRequestUpdateDto {
name: Some("Updated Role".into()),
permissions: Some(vec![]),
overwrite: None,
};
let res = server
.put(&format!("/v1/roles/update/{}", id))
.json(&payload)
.await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
let id = repo
.query_role_by_name(payload.name.clone().unwrap())
.await
.unwrap()
.id;
delete_dummy_role(state.clone(), id).await;
}
#[tokio::test]
async fn test_delete_role_should_return_200() {
let state = create_mock_app_state().await;
let repo = RolesRepository::new(&state);
let _ = repo
.query_create_role(RolesRequestCreateDto {
name: "Role To Delete".into(),
permissions: vec![],
})
.await
.unwrap();
let existing = repo
.query_role_by_name("Role To Delete".into())
.await
.unwrap();
let id = existing.id;
let server = create_test_app(state);
let res = server.delete(&format!("/v1/roles/delete/{}", id)).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::OK);
}
#[tokio::test]
async fn test_post_create_role_with_empty_name_should_return_400() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let payload = RolesRequestCreateDto {
name: "".into(),
permissions: vec![],
};
let res = server.post("/v1/roles/create").json(&payload).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_put_update_nonexistent_role_should_return_404() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let payload = RolesRequestUpdateDto {
name: Some("Does Not Exist".into()),
permissions: Some(vec![]),
overwrite: None,
};
let res = server
.put("/v1/roles/update/nonexistent-id")
.json(&payload)
.await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_delete_nonexistent_role_should_return_404() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let res = server.delete("/v1/roles/delete/nonexistent-id").await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_get_role_list_with_invalid_page_params_should_return_400() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let res = server.get("/v1/roles?page=abc&per_page=xyz").await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_create_duplicate_role_should_return_409() {
let state = create_mock_app_state().await;
let server = create_test_app(state);
let payload = RolesRequestCreateDto {
name: "Admin".into(),
permissions: vec![],
};
let res = server.post("/v1/roles/create").json(&payload).await;
dbg!(res.text());
assert_eq!(res.status_code(), StatusCode::CONFLICT);
}
+3 -4
View File
@@ -10,7 +10,6 @@ use crate::{
},
};
use surrealdb::Uuid;
use surrealdb::sql::Thing;
#[tokio::test]
async fn test_query_create_role_should_succeed() {
@@ -19,15 +18,15 @@ async fn test_query_create_role_should_succeed() {
let role_repo = RolesRepository::new(&state);
let perm_id = Uuid::new_v4().to_string();
let permission = PermissionsSchema {
id: Thing::from((ResourceEnum::Permissions.to_string(), perm_id.clone())),
id: make_thing(&ResourceEnum::Permissions.to_string(), &perm_id.clone()),
name: "Read Quiz".into(),
is_deleted: false,
created_at: Some(get_iso_date()),
updated_at: None,
updated_at: Some(get_iso_date()),
};
perm_repo.query_create_permission(permission).await.unwrap();
let payload = RolesRequestCreateDto {
name: "Student".into(),
name: "User".into(),
permissions: vec![perm_id.clone()],
};
let result = role_repo.query_create_role(payload).await;
-2
View File
@@ -1,4 +1,2 @@
#[cfg(test)]
pub mod users_controller_test;
#[cfg(test)]
pub mod users_repository_test;
@@ -1,602 +0,0 @@
use crate::{
RolesEnum, RolesRepository, UsersActiveInactiveRequestDto, UsersCreateRequestDto,
UsersRepository, UsersUpdateRequestDto, users_router,
};
use axum::{Extension, http::StatusCode};
use axum_test::TestServer;
use imphnen_utils::create_mock_app_state;
use surrealdb::Uuid;
#[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_list_users_should_fail_with_invalid_per_page() {
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=0").await;
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_list_users_should_fail_with_invalid_page() {
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=10").await;
dbg!(res.text());
dbg!(res.status_code());
assert_eq!(res.status_code(), StatusCode::BAD_REQUEST);
}
#[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=invalid_field")
.await;
assert_eq!(res.status_code(), StatusCode::OK);
}
#[tokio::test]
async fn test_list_users_with_search_no_match_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&search=nonexistinguserxyz")
.await;
let body: serde_json::Value = res.json();
assert_eq!(res.status_code(), StatusCode::OK);
assert_eq!(body["data"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_list_users_should_return_empty_on_invalid_filter() {
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=is_active&filter=maybe")
.await;
let body: serde_json::Value = res.json();
assert_eq!(res.status_code(), StatusCode::OK);
assert_eq!(body["data"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_get_user_list_with_search_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&search=maulana")
.await;
let status = res.status_code();
dbg!(res.text());
assert_eq!(status, StatusCode::OK);
}
#[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::User.to_string())
.await
.unwrap();
let role_id = role.id;
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: format!("test-{}@create.com", Uuid::new_v4()).into(),
role_id,
password: "Password1!".into(),
phone_number: "081234567890".into(),
is_active: true,
};
let res = server.post("/v1/users/create").json(&payload).await;
assert_eq!(res.status_code(), StatusCode::CREATED);
}
#[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::User.to_string())
.await
.unwrap();
let role_id = role.id;
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(),
role_id,
password: "Password1!".into(),
phone_number: "081234567890".into(),
is_active: true,
};
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_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::User.to_string())
.await
.expect("Failed to get role");
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: "Test Delete Me".into(),
email: "test@delete-me.com".into(),
password: "Password1!".into(),
role_id: role.id.clone(),
phone_number: "081234567890".into(),
is_active: true,
};
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.clone()).await;
assert!(detail_res.is_ok(), "User not found after creation");
let user = detail_res.unwrap();
let user_id_for_delete = user.id.id.to_raw();
let delete_res = server
.delete(&format!("/v1/users/delete/{}", user_id_for_delete))
.await;
assert_eq!(delete_res.status_code(), StatusCode::OK);
}
#[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::User.to_string())
.await
.unwrap();
let role_id = role.id;
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: format!("inactive-{}@test.com", Uuid::new_v4()).into(),
password: "Password1!".into(),
role_id,
phone_number: "081234567890".into(),
is_active: false,
};
let res_create = server.post("/v1/users/create").json(&payload).await;
dbg!(res_create.text());
let user = repo.query_user_by_email(payload.email).await.unwrap();
dbg!(user.fullname);
let user_id = user.id.id.to_raw();
let res = server
.put(&format!("/v1/users/activate/{}", user_id))
.json(&UsersActiveInactiveRequestDto { is_active: true })
.await;
dbg!(res.text());
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 role_repo = RolesRepository::new(&state);
let role = role_repo
.query_role_by_name(RolesEnum::User.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 unique_email = format!("update_{}@test.com", Uuid::new_v4());
let payload = UsersCreateRequestDto {
fullname: "Old Name".into(),
email: unique_email.clone(),
password: "Password1!".into(),
role_id: role_id.clone(),
phone_number: "081234567890".into(),
is_active: true,
};
let create_res = server.post("/v1/users/create").json(&payload).await;
assert_eq!(create_res.status_code(), StatusCode::CREATED);
let user = repo.query_user_by_email(unique_email).await.unwrap();
let user_id = user.id.id.to_raw();
let update_payload = UsersUpdateRequestDto {
fullname: "Updated Name".into(),
email: payload.email.clone(),
phone_number: "081234567890".into(),
role_id: user.role.id.id.to_raw(),
is_active: true,
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();
let body = res.text();
assert_eq!(status, StatusCode::OK, "Response body: {}", body);
}
#[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::User.to_string())
.await
.unwrap();
let role_id = role.id.clone();
let app = axum::Router::new()
.nest("/v1/users", users_router())
.layer(Extension(state.clone()));
let server = TestServer::new(app).unwrap();
let email = format!("test_{}@example.com", Uuid::new_v4());
let payload = UsersCreateRequestDto {
fullname: "User Satu".into(),
email: email.clone(),
password: "Password1!".into(),
role_id,
phone_number: "081234567890".into(),
is_active: true,
};
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 role_repo = RolesRepository::new(&state);
let role = role_repo
.query_role_by_name(RolesEnum::User.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 {
fullname: "To Be Deleted".into(),
email: "delete-me@example.com".into(),
role_id,
password: "Password1!".into(),
phone_number: "081234567890".into(),
is_active: true,
};
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 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::User.to_string())
.await
.unwrap();
let role_id = role.id.clone();
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(),
phone_number: "081234567890".into(),
role_id,
is_active: true,
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_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_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 role_repo = RolesRepository::new(&state);
let role = role_repo
.query_role_by_name(RolesEnum::User.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(),
phone_number: "081234567890".into(),
is_active: true,
};
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;
dbg!(res.text());
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 role_repo = RolesRepository::new(&state);
let role = role_repo
.query_role_by_name(RolesEnum::User.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(),
phone_number: "081234567890".into(),
is_active: true,
};
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 {
role_id: role_id.clone(),
fullname: "Should Fail".into(),
email: "deleteduser@test.com".into(),
phone_number: "081234567890".into(),
is_active: true,
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 already 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::User.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 {
fullname: "Invalid Payload".into(),
email: "invalid@test.com".into(),
role_id: role_id.clone(),
password: "Password1!".into(),
phone_number: "081234567890".into(),
is_active: true,
};
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(),
phone_number: "081234567890".into(),
is_active: true,
role_id: role_id.clone(),
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}"
);
}
+3 -5
View File
@@ -24,9 +24,7 @@ async fn test_create_and_get_user() {
create_test_user(&email, "Test User", true, &get_role_id(&app_state).await);
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;
let fetched = repo.query_user_by_email(email.clone().into()).await;
assert!(fetched.is_ok());
assert_eq!(fetched.unwrap().email, email.clone());
}
@@ -91,7 +89,7 @@ async fn test_query_user_list_basic() {
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 email = &generate_unique_email("deleteuser");
let user =
create_test_user(email, "Delete User", true, &get_role_id(&app_state).await);
repo.query_create_user(user.clone()).await.unwrap();
@@ -128,7 +126,7 @@ async fn test_delete_user_twice_should_fail_on_second_attempt() {
assert!(first.is_ok());
let second = repo.query_delete_user(user.id.id.to_raw()).await;
assert!(second.is_err());
assert_eq!(second.unwrap_err().to_string(), "User already deleted");
assert_eq!(second.unwrap_err().to_string(), "User not found");
}
#[tokio::test]