Files
imphnen-backend-service/imphnen-iam/src/v1/teams/teams_repository.rs
T
MythEclipse 39b75f410f Add unit tests for TeamsRepository and TeamsService
- Implement tests for team creation, retrieval, updating, and deletion in TeamsRepository.
- Add tests for team member management including adding and removing members.
- Create tests for team invitations and searching teams.
- Ensure proper cleanup of test data after each test case.
- Validate unauthorized operations for team management.
2025-09-22 14:59:38 +07:00

447 lines
13 KiB
Rust

use super::{
TeamsDetailQueryDto, TeamsListQueryDto, TeamsListItemDto, TeamsSchema,
TeamMembersSchema, TeamInvitationsSchema, TeamMembersQueryDto, TeamInvitationsQueryDto,
TeamsSearchQueryDto
};
use imphnen_libs::{
AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto
};
use imphnen_utils::get_id;
use surrealdb::sql::Thing;
use anyhow::{Result, bail};
use imphnen_utils::{DetailQueryBuilder, QueryListBuilder, make_thing_from_enum};
use serde_json;
use std::time::Instant;
pub struct TeamsRepository<'a> {
state: &'a AppState,
}
impl<'a> TeamsRepository<'a> {
pub fn new(state: &'a AppState) -> Self {
Self { state }
}
pub async fn query_team_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<TeamsListItemDto>>> {
let now = Instant::now();
let result: ResponseListSuccessDto<Vec<TeamsListQueryDto>> =
QueryListBuilder::new(
&self.state.surrealdb_ws,
&ResourceEnum::Teams.to_string(),
&meta,
)
.with_condition("is_deleted = false AND is_active = true")
.search_field("name")
.select_fields(vec!["*"])
.build()
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_team_list' took: {elapsed:.2?}");
}
let data = result
.data
.into_iter()
.map(TeamsListQueryDto::from)
.collect();
Ok(ResponseListSuccessDto {
data,
meta: result.meta,
})
}
pub async fn query_team_by_id(&self, id: &Thing) -> Result<TeamsDetailQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Teams.to_string())
.with_id(id.id.to_raw())
.with_select_fields(vec!["*"]);
let sql = builder.build();
let result: Option<TeamsDetailQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_team_by_id' took: {elapsed:.2?}");
}
let Some(team) = result else {
bail!("Team not found");
};
if team.is_deleted {
bail!("Team not found");
}
Ok(TeamsDetailQueryDto::from(team))
}
pub async fn query_create_team(&self, data: TeamsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record: Option<TeamsSchema> = db
.create(ResourceEnum::Teams.to_string())
.content(data)
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_team' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success create team".into()),
None => bail!("Failed to create team"),
}
}
pub async fn query_update_team(&self, data: TeamsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?;
let existing = self.query_team_by_id(&data.id).await?;
if existing.is_deleted {
bail!("Team already deleted");
}
let merged = TeamsSchema {
created_at: existing.created_at,
..data.clone()
};
let record: Option<TeamsSchema> = db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_team' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success update team".into()),
None => bail!("Failed to update team"),
}
}
pub async fn query_delete_team(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let team = self.query_team_by_id(&make_thing_from_enum(ResourceEnum::Teams, &id)).await?;
if team.is_deleted {
bail!("Team not found");
}
let record_key = get_id(&team.id)?;
let record: Option<TeamsSchema> = db
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_delete_team' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success delete team".into()),
None => bail!("Failed to delete team"),
}
}
pub async fn query_add_team_member(&self, data: TeamMembersSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record: Option<TeamMembersSchema> = db
.create(ResourceEnum::TeamMembers.to_string())
.content(data)
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_add_team_member' took: {elapsed:.2?}");
}
match record {
Some(saved_member) => {
println!("Member saved with ID: {:?}", saved_member.id);
Ok("Success add team member".into())
},
None => bail!("Failed to add team member"),
}
}
pub async fn query_team_members(&self, team_id: &Thing) -> Result<Vec<TeamMembersQueryDto>> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT * FROM {} WHERE team_id = type::thing('{}', '{}') AND is_active = true",
ResourceEnum::TeamMembers,
team_id.tb,
team_id.id.to_raw()
);
let mut result = db.query(sql).await?;
let members: Vec<TeamMembersQueryDto> = match result.take(0) {
Ok(members) => members,
Err(e) => {
println!("Error getting team members: {:?}", e);
vec![]
}
};
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_team_members' returned {} members", members.len());
println!("Query 'query_team_members' took: {elapsed:.2?}");
}
Ok(members)
}
pub async fn query_user_teams(&self, user_id: &Thing) -> Result<Vec<TeamsDetailQueryDto>> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT team.* FROM {} AS membership
INNER JOIN {} AS team ON membership.team_id = team.id
WHERE membership.user_id = $user_id
AND membership.is_active = true
AND team.is_deleted = false
AND team.is_active = true",
ResourceEnum::TeamMembers,
ResourceEnum::Teams
);
let mut result = db.query(sql).bind(("user_id", user_id.id.to_raw())).await?;
let teams: Vec<TeamsDetailQueryDto> = result.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_user_teams' took: {elapsed:.2?}");
}
Ok(teams)
}
pub async fn query_is_team_member(&self, team_id: &Thing, user_id: &Thing) -> Result<bool> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT COUNT() AS member_count FROM {} WHERE team_id = type::thing('{}', '{}') AND user_id = type::thing('{}', '{}') AND is_active = true",
ResourceEnum::TeamMembers,
team_id.tb,
team_id.id.to_raw(),
user_id.tb,
user_id.id.to_raw()
);
let mut result = db.query(sql).await?;
// Use COUNT query to avoid serialization issues with enum values
let count_result: Vec<serde_json::Value> = match result.take(0) {
Ok(count_result) => count_result,
Err(e) => {
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Error checking team membership: {:?}", e);
}
vec![]
}
};
let member_count = if let Some(first_result) = count_result.first() {
if let Some(count_val) = first_result.get("member_count") {
count_val.as_u64().unwrap_or(0)
} else {
0
}
} else {
0
};
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_is_team_member' found {} matching members", member_count);
println!("Query 'query_is_team_member' took: {elapsed:.2?}");
}
Ok(member_count > 0)
}
pub async fn query_create_invitation(&self, data: TeamInvitationsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record: Option<TeamInvitationsSchema> = db
.create(ResourceEnum::TeamInvitations.to_string())
.content(data)
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_invitation' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success create invitation".into()),
None => bail!("Failed to create invitation"),
}
}
pub async fn query_invitation_by_token(&self, token: &str) -> Result<TeamInvitationsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let sql = format!(
"SELECT * FROM {} WHERE invite_code = $invite_code AND status = 'pending' LIMIT 1",
ResourceEnum::TeamInvitations
);
let mut result = db.query(sql).bind(("invite_code", token.to_string())).await?;
let invitation: Option<TeamInvitationsQueryDto> = result.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_invitation_by_token' took: {elapsed:.2?}");
}
invitation.ok_or_else(|| anyhow::anyhow!("Invitation not found"))
}
pub async fn query_update_invitation(&self, data: TeamInvitationsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record_key = get_id(&data.id)?;
let record: Option<TeamInvitationsSchema> = db.update(record_key).merge(data).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_invitation' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success update invitation".into()),
None => bail!("Failed to update invitation"),
}
}
pub async fn query_search_teams(
&self,
search_params: TeamsSearchQueryDto,
) -> Result<ResponseListSuccessDto<Vec<TeamsListItemDto>>> {
let now = Instant::now();
let page = search_params.page.unwrap_or(1);
let per_page = search_params.per_page.unwrap_or(10);
let mut conditions = vec!["is_deleted = false".to_string(), "is_active = true".to_string()];
if let Some(open) = search_params.open {
if open {
conditions.push("is_open = true".to_string());
}
}
if let Some(location) = &search_params.location {
conditions.push(format!("location CONTAINS '{}'", location));
}
let mut query_conditions = conditions.join(" AND ");
if let Some(query) = &search_params.query {
query_conditions = format!("({}) AND (name CONTAINS '{}' OR description CONTAINS '{}')", query_conditions, query, query);
}
if let Some(skills) = &search_params.skills {
for skill in skills.iter() {
query_conditions = format!("{} AND skills_required CONTAINS '{}'", query_conditions, skill);
}
}
let meta = MetaRequestDto {
page: Some(page.try_into().unwrap()),
per_page: Some(per_page.try_into().unwrap()),
search: None, // Don't use built-in search since we're doing custom filtering
sort_by: Some("created_at".to_string()),
order: Some("DESC".to_string()),
filter: None,
filter_by: None,
};
let result: ResponseListSuccessDto<Vec<TeamsListQueryDto>> = QueryListBuilder::new(
&self.state.surrealdb_ws,
&ResourceEnum::Teams.to_string(),
&meta,
)
.with_condition(&query_conditions)
.select_fields(vec!["*"])
.build()
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_search_teams' took: {elapsed:.2?}");
}
let data = result
.data
.into_iter()
.map(TeamsListQueryDto::from)
.collect();
Ok(ResponseListSuccessDto {
data,
meta: result.meta,
})
}
pub async fn query_remove_team_member(&self, team_id: &Thing, user_id: &Thing) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let sql = format!(
"UPDATE {} SET is_active = false WHERE team_id = type::thing('{}', '{}') AND user_id = type::thing('{}', '{}')",
ResourceEnum::TeamMembers,
team_id.tb,
team_id.id.to_raw(),
user_id.tb,
user_id.id.to_raw()
);
// Execute the query but don't try to parse the result as it can contain complex enum values
let mut result = db.query(sql).await?;
// Just consume the result without trying to deserialize it to avoid serialization errors
let _: Result<Vec<serde_json::Value>, _> = result.take(0);
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_remove_team_member' took: {elapsed:.2?}");
}
Ok("Success remove team member".into())
}
}