chore: add test coverage
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
pub mod roles_dto;
|
||||
pub mod roles_enum;
|
||||
pub mod roles_repository;
|
||||
pub mod roles_schema;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod roles_repository_test;
|
||||
|
||||
pub use roles_dto::*;
|
||||
pub use roles_enum::*;
|
||||
pub use roles_repository::*;
|
||||
pub use roles_schema::*;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::PermissionsItemDto;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RolesRequestUpdateDto {
|
||||
pub name: Option<String>,
|
||||
pub permissions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RolesRequestCreateDto {
|
||||
pub name: String,
|
||||
pub permissions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RolesItemListDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct RolesItemDto {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_deleted: bool,
|
||||
pub permissions: Vec<PermissionsItemDto>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RolesEnum {
|
||||
Admin,
|
||||
Student,
|
||||
Staf,
|
||||
}
|
||||
|
||||
impl fmt::Display for RolesEnum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let roles_str = match self {
|
||||
RolesEnum::Admin => "Admin",
|
||||
RolesEnum::Student => "Student",
|
||||
RolesEnum::Staf => "Staf",
|
||||
};
|
||||
write!(f, "{}", roles_str)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use super::{
|
||||
RolesItemDto, RolesRequestCreateDto, RolesRequestUpdateDto, RolesSchema,
|
||||
};
|
||||
use crate::{
|
||||
get_id, make_thing, query_list_with_meta, AppState, MetaRequestDto,
|
||||
PermissionsItemDto, ResourceEnum, ResponseListSuccessDto,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use surrealdb::sql::Thing;
|
||||
use surrealdb::Uuid;
|
||||
|
||||
pub struct RolesRepository<'a> {
|
||||
state: &'a AppState,
|
||||
}
|
||||
|
||||
impl<'a> RolesRepository<'a> {
|
||||
pub fn new(state: &'a AppState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
pub async fn query_raw_role_by_id(&self, id: &str) -> Result<RolesSchema> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let role: Option<RolesSchema> =
|
||||
db.select((ResourceEnum::Roles.to_string(), id)).await?;
|
||||
match role {
|
||||
Some(r) if !r.is_deleted => Ok(r),
|
||||
_ => bail!("Role not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_role_list(
|
||||
&self,
|
||||
meta: MetaRequestDto,
|
||||
) -> Result<ResponseListSuccessDto<Vec<RolesSchema>>> {
|
||||
let mut conditions = vec!["is_deleted = false".into()];
|
||||
if meta.search.is_some() {
|
||||
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));
|
||||
}
|
||||
query_list_with_meta(
|
||||
&self.state.surrealdb_ws,
|
||||
&ResourceEnum::Roles.to_string(),
|
||||
&meta,
|
||||
conditions,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn query_role_by_name(&self, name: String) -> Result<RolesItemDto> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let sql = format!(
|
||||
"SELECT * FROM {} WHERE name = $name AND is_deleted = false",
|
||||
ResourceEnum::Roles.to_string()
|
||||
);
|
||||
let mut response: Vec<RolesSchema> =
|
||||
db.query(sql).bind(("name", name.clone())).await?.take(0)?;
|
||||
|
||||
let role = match response.pop() {
|
||||
Some(r) => r,
|
||||
None => bail!("Role not found"),
|
||||
};
|
||||
let mut permissions: Vec<PermissionsItemDto> = Vec::new();
|
||||
for permission_thing in &role.permissions {
|
||||
let result: Option<PermissionsItemDto> = db
|
||||
.select((permission_thing.tb.clone(), permission_thing.id.to_string()))
|
||||
.await?;
|
||||
if let Some(p) = result {
|
||||
permissions.push(p);
|
||||
}
|
||||
}
|
||||
Ok(RolesItemDto {
|
||||
id: role.id.id.to_raw(),
|
||||
name: role.name,
|
||||
is_deleted: role.is_deleted,
|
||||
permissions,
|
||||
created_at: role.created_at,
|
||||
updated_at: role.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn query_role_by_id(&self, id: String) -> Result<RolesItemDto> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let role: Option<RolesSchema> = db
|
||||
.select((ResourceEnum::Roles.to_string(), id.clone()))
|
||||
.await?;
|
||||
|
||||
let role = match role {
|
||||
Some(r) if !r.is_deleted => r,
|
||||
_ => bail!("Role not found"),
|
||||
};
|
||||
|
||||
let mut permissions: Vec<PermissionsItemDto> = Vec::new();
|
||||
|
||||
for permission_thing in &role.permissions {
|
||||
let result: Option<PermissionsItemDto> = db
|
||||
.select((permission_thing.tb.clone(), permission_thing.id.to_string()))
|
||||
.await?;
|
||||
if let Some(p) = result {
|
||||
permissions.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RolesItemDto {
|
||||
id: role.id.id.to_raw(),
|
||||
name: role.name,
|
||||
is_deleted: role.is_deleted,
|
||||
permissions,
|
||||
created_at: role.created_at,
|
||||
updated_at: role.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn query_create_role(
|
||||
&self,
|
||||
payload: RolesRequestCreateDto,
|
||||
) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
|
||||
let role_id = Uuid::new_v4().to_string();
|
||||
let permission_things: Vec<Thing> = payload
|
||||
.permissions
|
||||
.iter()
|
||||
.map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id))
|
||||
.collect();
|
||||
|
||||
let role = RolesSchema {
|
||||
id: make_thing(&ResourceEnum::Roles.to_string(), &role_id),
|
||||
name: payload.name,
|
||||
is_deleted: false,
|
||||
permissions: permission_things,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: Some(crate::get_iso_date()),
|
||||
};
|
||||
|
||||
let _: Option<RolesSchema> = db
|
||||
.create((&ResourceEnum::Roles.to_string(), role_id))
|
||||
.content(role)
|
||||
.await?;
|
||||
|
||||
Ok("Role with permissions created successfully".into())
|
||||
}
|
||||
|
||||
pub async fn query_update_role(
|
||||
&self,
|
||||
id: String,
|
||||
data: RolesRequestUpdateDto,
|
||||
) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let thing_id = make_thing(&ResourceEnum::Roles.to_string(), &id);
|
||||
let existing = self.query_raw_role_by_id(&id).await?;
|
||||
if existing.is_deleted {
|
||||
bail!("Role already deleted");
|
||||
}
|
||||
let permissions: Vec<Thing> = if let Some(permission_ids) = &data.permissions {
|
||||
permission_ids
|
||||
.iter()
|
||||
.map(|id| make_thing(&ResourceEnum::Permissions.to_string(), id))
|
||||
.collect()
|
||||
} else {
|
||||
existing
|
||||
.permissions
|
||||
.iter()
|
||||
.map(|p| make_thing(&ResourceEnum::Permissions.to_string(), &p.id.to_raw()))
|
||||
.collect()
|
||||
};
|
||||
let merged = RolesSchema {
|
||||
id: thing_id,
|
||||
name: data.name.unwrap_or(existing.name),
|
||||
permissions,
|
||||
is_deleted: existing.is_deleted,
|
||||
created_at: existing.created_at,
|
||||
updated_at: Some(crate::get_iso_date()),
|
||||
};
|
||||
let record: Option<RolesSchema> =
|
||||
db.update(get_id(&merged.id)?).content(merged).await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success update role".into()),
|
||||
None => bail!("Failed to update role"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_delete_role(&self, id: String) -> Result<String> {
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let role_id = make_thing(&ResourceEnum::Roles.to_string(), &id);
|
||||
let role = self.query_role_by_id(role_id.id.to_raw()).await?;
|
||||
if role.is_deleted {
|
||||
bail!("Role already deleted");
|
||||
}
|
||||
let record_key = get_id(&role_id)?;
|
||||
let record: Option<RolesSchema> = db
|
||||
.update(record_key)
|
||||
.merge(serde_json::json!({ "is_deleted": true }))
|
||||
.await?;
|
||||
match record {
|
||||
Some(_) => Ok("Success delete role".into()),
|
||||
None => bail!("Failed to delete role"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use crate::{
|
||||
apps::v1::{
|
||||
permissions::{
|
||||
permissions_repository::PermissionsRepository,
|
||||
permissions_schema::PermissionsSchema,
|
||||
},
|
||||
roles::{
|
||||
roles_dto::{RolesRequestCreateDto, RolesRequestUpdateDto},
|
||||
roles_repository::RolesRepository,
|
||||
},
|
||||
},
|
||||
create_mock_app_state, get_iso_date, make_thing, ResourceEnum,
|
||||
};
|
||||
use surrealdb::sql::Thing;
|
||||
use surrealdb::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_create_role_should_succeed() {
|
||||
let state = create_mock_app_state().await;
|
||||
let perm_repo = PermissionsRepository::new(&state);
|
||||
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())),
|
||||
name: "Read Quiz".into(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
updated_at: None,
|
||||
};
|
||||
perm_repo.query_create_permission(permission).await.unwrap();
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: "Student".into(),
|
||||
permissions: vec![perm_id.clone()],
|
||||
};
|
||||
let result = role_repo.query_create_role(payload).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_role_by_name_should_return_data() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: "Viewer".into(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo.query_role_by_name("Viewer".into()).await.unwrap();
|
||||
assert_eq!(role.name, "Viewer");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_role_by_id_should_return_data() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: "Tester".into(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo.query_role_by_name("Tester".into()).await.unwrap();
|
||||
let result = role_repo.query_role_by_id(role.id).await.unwrap();
|
||||
assert_eq!(result.name, "Tester");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_update_role_should_update_name_and_permissions() {
|
||||
let state = create_mock_app_state().await;
|
||||
let repo = RolesRepository::new(&state);
|
||||
let perm_repo = PermissionsRepository::new(&state);
|
||||
let original_perm_id = Uuid::new_v4().to_string();
|
||||
let original_perm = PermissionsSchema {
|
||||
id: make_thing(&ResourceEnum::Permissions.to_string(), &original_perm_id),
|
||||
name: "Original Permission".into(),
|
||||
is_deleted: false,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: None,
|
||||
};
|
||||
perm_repo
|
||||
.query_create_permission(original_perm)
|
||||
.await
|
||||
.unwrap();
|
||||
let create_payload = RolesRequestCreateDto {
|
||||
name: "Role For Update".into(),
|
||||
permissions: vec![original_perm_id.clone()],
|
||||
};
|
||||
repo.query_create_role(create_payload).await.unwrap();
|
||||
let existing_role = repo
|
||||
.query_role_by_name("Role For Update".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let existing_role_id = existing_role.id.clone();
|
||||
let new_perm_id = Uuid::new_v4().to_string();
|
||||
let new_perm = PermissionsSchema {
|
||||
id: make_thing(&ResourceEnum::Permissions.to_string(), &new_perm_id),
|
||||
name: "New Permission".into(),
|
||||
is_deleted: false,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: None,
|
||||
};
|
||||
perm_repo.query_create_permission(new_perm).await.unwrap();
|
||||
let update_payload = RolesRequestUpdateDto {
|
||||
name: Some("Updated Role Name".into()),
|
||||
permissions: Some(vec![new_perm_id.clone()]),
|
||||
};
|
||||
let update_result = repo
|
||||
.query_update_role(existing_role_id.clone(), update_payload)
|
||||
.await;
|
||||
assert!(update_result.is_ok());
|
||||
let updated = repo
|
||||
.query_role_by_id(existing_role_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "Updated Role Name");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_delete_role_should_soft_delete() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: "Temporary".into(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo
|
||||
.query_role_by_name("Temporary".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let result = role_repo.query_delete_role(role.id.clone()).await;
|
||||
assert!(result.is_ok());
|
||||
let deleted = role_repo.query_role_by_id(role.id).await;
|
||||
assert!(deleted.is_err());
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn test_query_update_role_should_fallback_to_existing_permissions_if_none_provided(
|
||||
) {
|
||||
let state = create_mock_app_state().await;
|
||||
let repo = RolesRepository::new(&state);
|
||||
let perm_repo = PermissionsRepository::new(&state);
|
||||
let perm_id = Uuid::new_v4().to_string();
|
||||
let permission = PermissionsSchema {
|
||||
id: make_thing(&ResourceEnum::Permissions.to_string(), &perm_id),
|
||||
name: "Permission for Fallback".into(),
|
||||
is_deleted: false,
|
||||
created_at: Some(crate::get_iso_date()),
|
||||
updated_at: None,
|
||||
};
|
||||
perm_repo.query_create_permission(permission).await.unwrap();
|
||||
let create_payload = RolesRequestCreateDto {
|
||||
name: "Role With Permission".into(),
|
||||
permissions: vec![perm_id.clone()],
|
||||
};
|
||||
repo.query_create_role(create_payload).await.unwrap();
|
||||
let existing = repo
|
||||
.query_role_by_name("Role With Permission".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let existing_id = existing.id.clone();
|
||||
let update_payload = RolesRequestUpdateDto {
|
||||
name: Some("Updated Role Name".into()),
|
||||
permissions: None,
|
||||
};
|
||||
let update_res = repo
|
||||
.query_update_role(existing_id.clone(), update_payload)
|
||||
.await;
|
||||
assert!(update_res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_role_by_name_should_fail_if_not_found() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let result = role_repo.query_role_by_name("ghost-role".into()).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_delete_role_should_fail_if_already_deleted() {
|
||||
let state = create_mock_app_state().await;
|
||||
let role_repo = RolesRepository::new(&state);
|
||||
let payload = RolesRequestCreateDto {
|
||||
name: "SoftDeleteTest".into(),
|
||||
permissions: vec![],
|
||||
};
|
||||
role_repo.query_create_role(payload.clone()).await.unwrap();
|
||||
let role = role_repo
|
||||
.query_role_by_name("SoftDeleteTest".into())
|
||||
.await
|
||||
.unwrap();
|
||||
role_repo.query_delete_role(role.id.clone()).await.unwrap();
|
||||
let result = role_repo.query_delete_role(role.id);
|
||||
assert!(result.await.is_err());
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use surrealdb::{
|
||||
sql::{Id, Thing},
|
||||
Uuid,
|
||||
};
|
||||
|
||||
use crate::ResourceEnum;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RolesSchema {
|
||||
pub id: Thing,
|
||||
pub name: String,
|
||||
pub is_deleted: bool,
|
||||
pub permissions: Vec<Thing>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for RolesSchema {
|
||||
fn default() -> Self {
|
||||
RolesSchema {
|
||||
id: Thing::from((
|
||||
ResourceEnum::Roles.to_string(),
|
||||
Id::String(Uuid::new_v4().to_string()),
|
||||
)),
|
||||
permissions: vec![Thing::from((
|
||||
ResourceEnum::Permissions.to_string(),
|
||||
Id::String(Uuid::new_v4().to_string()),
|
||||
))],
|
||||
name: String::new(),
|
||||
is_deleted: false,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user