use crate::CrdtManager;
use crate::crdt::EntityType;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use yrs::{Map, Transact, WriteTxn};
fn unix_timestamp() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn unix_timestamp_result() -> EntityServiceResult<i64> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.map_err(|e| EntityServiceError::Io(std::io::Error::other(format!("Time error: {}", e))))
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Entity {
pub id: String,
pub name: String,
pub entity_type: EntityType,
pub description: Option<String>,
pub created_by: String,
pub created_at: i64,
pub members: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub parent_org_id: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub network_four_words: Option<String>,
#[serde(default)]
pub is_local_only: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub linked_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_sync_at: Option<i64>,
}
impl Entity {
pub fn new_local(
name: String,
entity_type: EntityType,
description: Option<String>,
created_by: String,
) -> Self {
let now = unix_timestamp();
Self {
id: uuid::Uuid::new_v4().to_string(),
name,
entity_type,
description,
created_by,
created_at: now,
members: vec![],
parent_org_id: None,
network_four_words: None,
is_local_only: true,
linked_at: None,
last_sync_at: None,
}
}
pub fn is_linked(&self) -> bool {
self.network_four_words.is_some() && !self.is_local_only
}
pub fn link_to_network(&mut self, four_words: String) {
self.network_four_words = Some(four_words);
self.is_local_only = false;
self.linked_at = Some(unix_timestamp());
}
pub fn mark_synced(&mut self) {
self.last_sync_at = Some(unix_timestamp());
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemberInfo {
pub member_id: String,
pub role: String,
pub joined_at: i64,
pub deleted: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum EntityServiceError {
#[error("Entity not found: {0}")]
NotFound(String),
#[error("Entity already exists: {0}")]
AlreadyExists(String),
#[error("Member not found: {0}")]
MemberNotFound(String),
#[error("Member already exists: {0}")]
MemberAlreadyExists(String),
#[error("CRDT error: {0}")]
Crdt(#[from] crate::crdt_manager::CrdtError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type EntityServiceResult<T> = Result<T, EntityServiceError>;
#[derive(Debug, Clone)]
pub struct CascadeRemovalResult {
pub removed_in: Vec<(EntityType, String)>,
pub skipped_not_member: Vec<(EntityType, String)>,
pub failed: Vec<(EntityType, String, String)>,
}
pub struct EntityService {
crdt_manager: Arc<CrdtManager>,
}
impl EntityService {
pub fn new(crdt_manager: Arc<CrdtManager>) -> Self {
Self { crdt_manager }
}
pub async fn create_entity(
&self,
name: String,
entity_type: EntityType,
description: Option<String>,
created_by: String,
initial_members: Vec<String>,
) -> EntityServiceResult<Entity> {
let entity_id = Uuid::new_v4().to_string();
let now = unix_timestamp_result()?;
let entity = Entity {
id: entity_id.clone(),
name,
entity_type,
description,
created_by: created_by.clone(),
created_at: now,
members: initial_members.clone(),
parent_org_id: None,
network_four_words: None,
is_local_only: false,
linked_at: None,
last_sync_at: None,
};
self.save_entity(&entity).await?;
let mut all_members = initial_members;
if !all_members.contains(&created_by) {
all_members.push(created_by.clone());
}
for member_id in &all_members {
let role = if member_id == &created_by {
"owner"
} else {
"member"
};
self.add_member(entity_type, &entity_id, member_id, role)
.await?;
}
let entity_with_members = Entity {
members: all_members,
..entity
};
Ok(entity_with_members)
}
pub async fn get_entity(&self, entity_id: &str) -> EntityServiceResult<Entity> {
let doc_id = format!("entity:{}:metadata", entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let metadata_map = doc.get_or_insert_map("metadata");
let txn = doc.transact();
let name = CrdtManager::get_map_string(&metadata_map, &txn, "name")
.unwrap_or_else(|| "Unknown".to_string());
let entity_type_str = CrdtManager::get_map_string(&metadata_map, &txn, "entity_type")
.unwrap_or_else(|| "group".to_string());
let entity_type = match entity_type_str.as_str() {
"group" => EntityType::Group,
"channel" => EntityType::Channel,
"project" => EntityType::Project,
"organisation" => EntityType::Organisation,
_ => EntityType::Group,
};
let description = CrdtManager::get_map_string(&metadata_map, &txn, "description");
let created_by = CrdtManager::get_map_string(&metadata_map, &txn, "created_by")
.unwrap_or_else(|| "unknown".to_string());
let created_at = CrdtManager::get_map_i64(&metadata_map, &txn, "created_at").unwrap_or(0);
let parent_org_id = CrdtManager::get_map_string(&metadata_map, &txn, "parent_org_id");
let network_four_words =
CrdtManager::get_map_string(&metadata_map, &txn, "network_four_words");
let is_local_only =
CrdtManager::get_map_bool(&metadata_map, &txn, "is_local_only").unwrap_or(false);
let linked_at = CrdtManager::get_map_i64(&metadata_map, &txn, "linked_at");
let last_sync_at = CrdtManager::get_map_i64(&metadata_map, &txn, "last_sync_at");
let members = self
.list_members(entity_type, entity_id)
.await?
.into_iter()
.filter(|m| !m.deleted)
.map(|m| m.member_id)
.collect();
Ok(Entity {
id: entity_id.to_string(),
name,
entity_type,
description,
created_by,
created_at,
members,
parent_org_id,
network_four_words,
is_local_only,
linked_at,
last_sync_at,
})
}
pub async fn list_entities(&self) -> EntityServiceResult<Vec<Entity>> {
use std::fs;
let entity_dir = self
.crdt_manager
.get_storage_dir()
.join("crdt")
.join("entity");
let entity_ids =
tokio::task::spawn_blocking(move || -> Result<Vec<String>, EntityServiceError> {
if !entity_dir.exists() {
return Ok(vec![]);
}
let mut ids = Vec::new();
let entries = fs::read_dir(&entity_dir).map_err(|e| {
EntityServiceError::Io(std::io::Error::other(format!(
"Failed to read entity directory: {}",
e
)))
})?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("meta") {
continue;
}
if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
if let Ok(decoded_bytes) = hex::decode(filename)
&& let Ok(doc_id) = String::from_utf8(decoded_bytes)
{
if doc_id.starts_with("entity:") && doc_id.ends_with(":metadata") {
if let Some(entity_id) = doc_id
.strip_prefix("entity:")
.and_then(|s| s.strip_suffix(":metadata"))
{
ids.push(entity_id.to_string());
}
}
}
}
}
Ok(ids)
})
.await
.map_err(|e| {
EntityServiceError::Io(std::io::Error::other(format!(
"Blocking task failed: {}",
e
)))
})??;
let mut entities = Vec::new();
for entity_id in entity_ids {
match self.get_entity(&entity_id).await {
Ok(entity) => entities.push(entity),
Err(e) => {
tracing::warn!("Failed to load entity {}: {}", entity_id, e);
}
}
}
Ok(entities)
}
pub async fn add_member(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
role: &str,
) -> EntityServiceResult<()> {
use yrs::Doc;
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = match self.crdt_manager.load_document(&doc_id).await {
Ok(doc) => doc,
Err(_) => {
Doc::new()
}
};
let members_map = doc.get_or_insert_map("members");
{
let txn = doc.transact();
if let Some(member_data) = CrdtManager::get_nested_map(&members_map, &txn, member_id) {
let is_deleted =
CrdtManager::get_map_bool(&member_data, &txn, "deleted").unwrap_or(false);
if !is_deleted {
return Err(EntityServiceError::MemberAlreadyExists(
member_id.to_string(),
));
}
}
}
let active_members_map = doc.get_or_insert_map("active_members");
{
let mut txn = doc.transact_mut();
let member_data =
CrdtManager::get_or_create_nested_map(&members_map, &mut txn, member_id);
CrdtManager::set_map_string(&member_data, &mut txn, "member_id", member_id);
CrdtManager::set_map_string(&member_data, &mut txn, "role", role);
CrdtManager::set_map_i64(
&member_data,
&mut txn,
"joined_at",
unix_timestamp_result()?,
);
CrdtManager::set_map_bool(&member_data, &mut txn, "deleted", false);
CrdtManager::set_map_bool(&active_members_map, &mut txn, member_id, true);
}
self.crdt_manager
.save_document(&doc_id, entity_type.as_str(), entity_id, &doc)
.await?;
Ok(())
}
pub async fn remove_member(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
deleted_by: &str,
) -> EntityServiceResult<()> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let members_map = doc.get_or_insert_map("members");
let active_members_map = doc.get_or_insert_map("active_members");
{
let txn = doc.transact();
if CrdtManager::get_nested_map(&members_map, &txn, member_id).is_none() {
return Err(EntityServiceError::MemberNotFound(member_id.to_string()));
}
}
{
let mut txn = doc.transact_mut();
let member_data =
CrdtManager::get_or_create_nested_map(&members_map, &mut txn, member_id);
CrdtManager::set_map_bool(&member_data, &mut txn, "deleted", true);
CrdtManager::set_map_i64(
&member_data,
&mut txn,
"deleted_at",
unix_timestamp_result()?,
);
CrdtManager::set_map_string(&member_data, &mut txn, "deleted_by", deleted_by);
active_members_map.remove(&mut txn, member_id);
}
self.crdt_manager
.save_document(&doc_id, entity_type.as_str(), entity_id, &doc)
.await?;
Ok(())
}
pub async fn list_members(
&self,
entity_type: EntityType,
entity_id: &str,
) -> EntityServiceResult<Vec<MemberInfo>> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let mut members = Vec::new();
let members_map = doc.get_or_insert_map("members");
{
let txn = doc.transact();
const MAX_MEMBERS: u32 = 1000;
let member_count = members_map.len(&txn);
if member_count > MAX_MEMBERS {
return Err(EntityServiceError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"Too many members: {} (max: {}) for entity: {}",
member_count, MAX_MEMBERS, entity_id
),
)));
}
for (member_id, _) in members_map.iter(&txn) {
let member_id_string = member_id.to_string();
if let Some(member_data) =
CrdtManager::get_nested_map(&members_map, &txn, &member_id_string)
{
let deleted =
CrdtManager::get_map_bool(&member_data, &txn, "deleted").unwrap_or(false);
if deleted {
continue;
}
let member_id_str =
CrdtManager::get_map_string(&member_data, &txn, "member_id")
.unwrap_or_else(|| member_id_string.clone());
let role = CrdtManager::get_map_string(&member_data, &txn, "role")
.unwrap_or_else(|| "member".to_string());
let joined_at =
CrdtManager::get_map_i64(&member_data, &txn, "joined_at").unwrap_or(0);
members.push(MemberInfo {
member_id: member_id_str,
role,
joined_at,
deleted,
});
}
}
}
Ok(members)
}
pub async fn set_parent_organization(
&self,
entity_id: &str,
parent_org_id: &str,
) -> EntityServiceResult<()> {
let mut entity = self.get_entity(entity_id).await?;
entity.parent_org_id = Some(parent_org_id.to_string());
self.save_entity(&entity).await?;
Ok(())
}
async fn list_child_entities_of_org(
&self,
org_id: &str,
) -> EntityServiceResult<Vec<(EntityType, String)>> {
let entities = self.list_entities().await?;
let children = entities
.into_iter()
.filter(|e| {
e.parent_org_id.as_deref() == Some(org_id)
&& matches!(
e.entity_type,
EntityType::Channel | EntityType::Group | EntityType::Project
)
})
.map(|e| (e.entity_type, e.id))
.collect();
Ok(children)
}
pub async fn remove_organization_member(
&self,
org_id: &str,
member_id: &str,
deleted_by: &str,
) -> EntityServiceResult<CascadeRemovalResult> {
let mut result = CascadeRemovalResult {
removed_in: vec![],
skipped_not_member: vec![],
failed: vec![],
};
match self
.remove_member(EntityType::Organisation, org_id, member_id, deleted_by)
.await
{
Ok(_) => result
.removed_in
.push((EntityType::Organisation, org_id.to_string())),
Err(EntityServiceError::MemberNotFound(_)) => result
.skipped_not_member
.push((EntityType::Organisation, org_id.to_string())),
Err(e) => {
result
.failed
.push((EntityType::Organisation, org_id.to_string(), e.to_string()))
}
}
let children = self.list_child_entities_of_org(org_id).await?;
for (entity_type, entity_id) in children {
match self
.remove_member(entity_type, &entity_id, member_id, deleted_by)
.await
{
Ok(_) => result.removed_in.push((entity_type, entity_id)),
Err(EntityServiceError::MemberNotFound(_)) => {
result.skipped_not_member.push((entity_type, entity_id))
}
Err(e) => result.failed.push((entity_type, entity_id, e.to_string())),
}
}
Ok(result)
}
async fn save_entity(&self, entity: &Entity) -> EntityServiceResult<()> {
use yrs::Doc;
let doc_id = format!("entity:{}:metadata", entity.id);
let doc = Doc::new();
{
let mut txn = doc.transact_mut();
let metadata_map = txn.get_or_insert_map("metadata");
CrdtManager::set_map_string(&metadata_map, &mut txn, "name", &entity.name);
CrdtManager::set_map_string(
&metadata_map,
&mut txn,
"entity_type",
entity.entity_type.as_str(),
);
if let Some(description) = &entity.description {
CrdtManager::set_map_string(&metadata_map, &mut txn, "description", description);
}
CrdtManager::set_map_string(&metadata_map, &mut txn, "created_by", &entity.created_by);
CrdtManager::set_map_i64(&metadata_map, &mut txn, "created_at", entity.created_at);
if let Some(parent_org_id) = &entity.parent_org_id {
CrdtManager::set_map_string(
&metadata_map,
&mut txn,
"parent_org_id",
parent_org_id,
);
}
if let Some(network_four_words) = &entity.network_four_words {
CrdtManager::set_map_string(
&metadata_map,
&mut txn,
"network_four_words",
network_four_words,
);
}
CrdtManager::set_map_bool(
&metadata_map,
&mut txn,
"is_local_only",
entity.is_local_only,
);
if let Some(linked_at) = entity.linked_at {
CrdtManager::set_map_i64(&metadata_map, &mut txn, "linked_at", linked_at);
}
if let Some(last_sync_at) = entity.last_sync_at {
CrdtManager::set_map_i64(&metadata_map, &mut txn, "last_sync_at", last_sync_at);
}
}
self.crdt_manager
.save_document(&doc_id, "entity", &entity.id, &doc)
.await?;
Ok(())
}
pub async fn create_local_entity(
&self,
name: String,
entity_type: EntityType,
description: Option<String>,
created_by: String,
) -> EntityServiceResult<Entity> {
let entity = Entity::new_local(name, entity_type, description, created_by);
self.save_entity(&entity).await?;
Ok(entity)
}
pub async fn link_entity_to_network(
&self,
entity_id: &str,
four_words: &str,
) -> EntityServiceResult<Entity> {
let mut entity = self.get_entity(entity_id).await?;
entity.link_to_network(four_words.to_string());
self.save_entity(&entity).await?;
Ok(entity)
}
pub async fn mark_entity_synced(&self, entity_id: &str) -> EntityServiceResult<Entity> {
let mut entity = self.get_entity(entity_id).await?;
entity.mark_synced();
self.save_entity(&entity).await?;
Ok(entity)
}
pub async fn update_entity(
&self,
entity_id: &str,
name: Option<String>,
description: Option<Option<String>>,
) -> EntityServiceResult<Entity> {
let mut entity = self.get_entity(entity_id).await?;
if let Some(new_name) = name {
entity.name = new_name;
}
if let Some(new_description) = description {
entity.description = new_description;
}
self.save_entity(&entity).await?;
Ok(entity)
}
pub async fn delete_entity(&self, entity_id: &str) -> EntityServiceResult<()> {
let doc_id = format!("entity:{}:metadata", entity_id);
self.crdt_manager
.delete_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn import_entity(
&self,
id: String,
name: String,
entity_type: EntityType,
description: Option<String>,
created_by: String,
created_at: i64,
joiner_four_words: String,
role: String,
) -> EntityServiceResult<Entity> {
if let Ok(existing) = self.get_entity(&id).await {
return Ok(existing);
}
let entity = Entity {
id: id.clone(),
name,
entity_type,
description,
created_by,
created_at,
members: vec![joiner_four_words.clone()],
parent_org_id: None,
network_four_words: None,
is_local_only: false,
linked_at: None,
last_sync_at: None,
};
self.save_entity(&entity).await?;
self.add_member(entity_type, &id, &joiner_four_words, &role)
.await?;
Ok(entity)
}
pub async fn set_permission_override(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
resource_type: &str,
access_level: &str,
) -> EntityServiceResult<()> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let members_map = doc.get_or_insert_map("members");
{
let txn = doc.transact();
if CrdtManager::get_nested_map(&members_map, &txn, member_id).is_none() {
return Err(EntityServiceError::MemberNotFound(member_id.to_string()));
}
}
{
let mut txn = doc.transact_mut();
let member_data =
CrdtManager::get_or_create_nested_map(&members_map, &mut txn, member_id);
let overrides = CrdtManager::get_or_create_nested_map(
&member_data,
&mut txn,
"permission_overrides",
);
CrdtManager::set_map_string(&overrides, &mut txn, resource_type, access_level);
}
self.crdt_manager
.save_document(&doc_id, entity_type.as_str(), entity_id, &doc)
.await?;
Ok(())
}
pub async fn remove_permission_override(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
resource_type: &str,
) -> EntityServiceResult<()> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let members_map = doc.get_or_insert_map("members");
{
let txn = doc.transact();
if CrdtManager::get_nested_map(&members_map, &txn, member_id).is_none() {
return Err(EntityServiceError::MemberNotFound(member_id.to_string()));
}
}
{
let mut txn = doc.transact_mut();
if let Some(member_data) = CrdtManager::get_nested_map(&members_map, &txn, member_id)
&& let Some(overrides) =
CrdtManager::get_nested_map(&member_data, &txn, "permission_overrides")
{
overrides.remove(&mut txn, resource_type);
}
}
self.crdt_manager
.save_document(&doc_id, entity_type.as_str(), entity_id, &doc)
.await?;
Ok(())
}
pub async fn get_permission_overrides(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
) -> EntityServiceResult<Vec<(String, String)>> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let members_map = doc.get_or_insert_map("members");
let txn = doc.transact();
let member_data = CrdtManager::get_nested_map(&members_map, &txn, member_id)
.ok_or_else(|| EntityServiceError::MemberNotFound(member_id.to_string()))?;
let mut overrides = Vec::new();
if let Some(overrides_map) =
CrdtManager::get_nested_map(&member_data, &txn, "permission_overrides")
{
for (key, _) in overrides_map.iter(&txn) {
let resource_type = key.to_string();
if let Some(access_level) =
CrdtManager::get_map_string(&overrides_map, &txn, &resource_type)
{
overrides.push((resource_type, access_level));
}
}
}
Ok(overrides)
}
pub async fn set_member_role(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
new_role: &str,
) -> EntityServiceResult<()> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let members_map = doc.get_or_insert_map("members");
{
let txn = doc.transact();
if CrdtManager::get_nested_map(&members_map, &txn, member_id).is_none() {
return Err(EntityServiceError::MemberNotFound(member_id.to_string()));
}
}
{
let mut txn = doc.transact_mut();
let member_data =
CrdtManager::get_or_create_nested_map(&members_map, &mut txn, member_id);
CrdtManager::set_map_string(&member_data, &mut txn, "role", new_role);
}
self.crdt_manager
.save_document(&doc_id, entity_type.as_str(), entity_id, &doc)
.await?;
Ok(())
}
pub async fn get_member_role(
&self,
entity_type: EntityType,
entity_id: &str,
member_id: &str,
) -> EntityServiceResult<String> {
let doc_id = format!("{}:{}:core", entity_type.as_str(), entity_id);
let doc = self
.crdt_manager
.load_document(&doc_id)
.await
.map_err(|_| EntityServiceError::NotFound(entity_id.to_string()))?;
let members_map = doc.get_or_insert_map("members");
let txn = doc.transact();
let member_data = CrdtManager::get_nested_map(&members_map, &txn, member_id)
.ok_or_else(|| EntityServiceError::MemberNotFound(member_id.to_string()))?;
let role = CrdtManager::get_map_string(&member_data, &txn, "role")
.unwrap_or_else(|| "member".to_string());
Ok(role)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crdt_manager::CrdtManager;
use std::sync::Arc;
use tempfile::tempdir;
async fn create_test_service() -> EntityService {
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("test.db");
let crdt_manager = Arc::new(CrdtManager::new(&db_path).await.unwrap());
EntityService::new(crdt_manager)
}
#[tokio::test]
async fn test_create_entity() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
Some("A test group".to_string()),
"creator-id".to_string(),
vec!["member1".to_string(), "member2".to_string()],
)
.await
.expect("Failed to create entity");
assert_eq!(entity.name, "Test Group");
assert_eq!(entity.entity_type, EntityType::Group);
assert_eq!(entity.created_by, "creator-id");
assert!(entity.members.contains(&"creator-id".to_string()));
assert!(entity.members.contains(&"member1".to_string()));
assert!(entity.members.contains(&"member2".to_string()));
}
#[tokio::test]
async fn test_get_entity() {
let service = create_test_service().await;
let created_entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
Some("A test group".to_string()),
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let retrieved_entity = service
.get_entity(&created_entity.id)
.await
.expect("Failed to get entity");
assert_eq!(retrieved_entity.id, created_entity.id);
assert_eq!(retrieved_entity.name, "Test Group");
assert_eq!(retrieved_entity.entity_type, EntityType::Group);
}
#[tokio::test]
async fn test_get_nonexistent_entity() {
let service = create_test_service().await;
let result = service.get_entity("nonexistent").await;
assert!(matches!(result, Err(EntityServiceError::NotFound(_))));
}
#[tokio::test]
async fn test_add_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
service
.add_member(EntityType::Group, &entity.id, "new-member", "admin")
.await
.expect("Failed to add member");
let members = service
.list_members(EntityType::Group, &entity.id)
.await
.expect("Failed to list members");
assert_eq!(members.len(), 2); let new_member = members
.iter()
.find(|m| m.member_id == "new-member")
.unwrap();
assert_eq!(new_member.role, "admin");
assert!(!new_member.deleted);
}
#[tokio::test]
async fn test_add_duplicate_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
let result = service
.add_member(EntityType::Group, &entity.id, "member1", "member")
.await;
assert!(matches!(
result,
Err(EntityServiceError::MemberAlreadyExists(_))
));
}
#[tokio::test]
async fn test_remove_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.remove_member(EntityType::Group, &entity.id, "member1", "creator-id")
.await
.expect("Failed to remove member");
let members = service
.list_members(EntityType::Group, &entity.id)
.await
.expect("Failed to list members");
assert!(
!members.iter().any(|m| m.member_id == "member1"),
"Deleted member should not appear in list_members"
);
}
#[tokio::test]
async fn test_remove_nonexistent_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let result = service
.remove_member(EntityType::Group, &entity.id, "nonexistent", "creator-id")
.await;
assert!(matches!(result, Err(EntityServiceError::MemberNotFound(_))));
}
#[tokio::test]
async fn test_list_members() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string(), "member2".to_string()],
)
.await
.expect("Failed to create entity");
let members = service
.list_members(EntityType::Group, &entity.id)
.await
.expect("Failed to list members");
assert_eq!(members.len(), 3); assert!(members.iter().any(|m| m.member_id == "creator-id"));
assert!(members.iter().any(|m| m.member_id == "member1"));
assert!(members.iter().any(|m| m.member_id == "member2"));
}
#[tokio::test]
async fn test_set_permission_override() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.set_permission_override(EntityType::Group, &entity.id, "member1", "messages", "edit")
.await
.expect("Failed to set permission override");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert_eq!(overrides.len(), 1);
assert!(
overrides
.iter()
.any(|(k, v)| k == "messages" && v == "edit")
);
}
#[tokio::test]
async fn test_set_multiple_permission_overrides() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.set_permission_override(EntityType::Group, &entity.id, "member1", "messages", "edit")
.await
.expect("Failed to set override 1");
service
.set_permission_override(
EntityType::Group,
&entity.id,
"member1",
"documents",
"read_only",
)
.await
.expect("Failed to set override 2");
service
.set_permission_override(
EntityType::Group,
&entity.id,
"member1",
"settings",
"not_visible",
)
.await
.expect("Failed to set override 3");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert_eq!(overrides.len(), 3);
assert!(
overrides
.iter()
.any(|(k, v)| k == "messages" && v == "edit")
);
assert!(
overrides
.iter()
.any(|(k, v)| k == "documents" && v == "read_only")
);
assert!(
overrides
.iter()
.any(|(k, v)| k == "settings" && v == "not_visible")
);
}
#[tokio::test]
async fn test_set_permission_override_nonexistent_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let result = service
.set_permission_override(
EntityType::Group,
&entity.id,
"nonexistent",
"messages",
"edit",
)
.await;
assert!(matches!(result, Err(EntityServiceError::MemberNotFound(_))));
}
#[tokio::test]
async fn test_remove_permission_override() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.set_permission_override(EntityType::Group, &entity.id, "member1", "messages", "edit")
.await
.expect("Failed to set override");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert_eq!(overrides.len(), 1);
service
.remove_permission_override(EntityType::Group, &entity.id, "member1", "messages")
.await
.expect("Failed to remove override");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert!(overrides.is_empty());
}
#[tokio::test]
async fn test_remove_permission_override_nonexistent_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let result = service
.remove_permission_override(EntityType::Group, &entity.id, "nonexistent", "messages")
.await;
assert!(matches!(result, Err(EntityServiceError::MemberNotFound(_))));
}
#[tokio::test]
async fn test_get_permission_overrides_empty() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert!(overrides.is_empty());
}
#[tokio::test]
async fn test_get_permission_overrides_nonexistent_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let result = service
.get_permission_overrides(EntityType::Group, &entity.id, "nonexistent")
.await;
assert!(matches!(result, Err(EntityServiceError::MemberNotFound(_))));
}
#[tokio::test]
async fn test_set_member_role() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.set_member_role(EntityType::Group, &entity.id, "member1", "admin")
.await
.expect("Failed to set role");
let role = service
.get_member_role(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get role");
assert_eq!(role, "admin");
}
#[tokio::test]
async fn test_set_member_role_nonexistent_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let result = service
.set_member_role(EntityType::Group, &entity.id, "nonexistent", "admin")
.await;
assert!(matches!(result, Err(EntityServiceError::MemberNotFound(_))));
}
#[tokio::test]
async fn test_get_member_role_default() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
let role = service
.get_member_role(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get role");
assert_eq!(role, "member");
}
#[tokio::test]
async fn test_get_member_role_nonexistent_member() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec![],
)
.await
.expect("Failed to create entity");
let result = service
.get_member_role(EntityType::Group, &entity.id, "nonexistent")
.await;
assert!(matches!(result, Err(EntityServiceError::MemberNotFound(_))));
}
#[tokio::test]
async fn test_permission_override_update() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.set_permission_override(
EntityType::Group,
&entity.id,
"member1",
"messages",
"read_only",
)
.await
.expect("Failed to set override");
service
.set_permission_override(EntityType::Group, &entity.id, "member1", "messages", "edit")
.await
.expect("Failed to update override");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert_eq!(overrides.len(), 1);
assert!(
overrides
.iter()
.any(|(k, v)| k == "messages" && v == "edit")
);
}
#[tokio::test]
async fn test_permission_persistence_across_operations() {
let service = create_test_service().await;
let entity = service
.create_entity(
"Test Group".to_string(),
EntityType::Group,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create entity");
service
.set_permission_override(EntityType::Group, &entity.id, "member1", "messages", "edit")
.await
.expect("Failed to set override");
service
.set_member_role(EntityType::Group, &entity.id, "member1", "viewer")
.await
.expect("Failed to set role");
let overrides = service
.get_permission_overrides(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get overrides");
assert_eq!(overrides.len(), 1);
assert!(
overrides
.iter()
.any(|(k, v)| k == "messages" && v == "edit")
);
let role = service
.get_member_role(EntityType::Group, &entity.id, "member1")
.await
.expect("Failed to get role");
assert_eq!(role, "viewer");
}
#[tokio::test]
async fn test_different_entity_types() {
let service = create_test_service().await;
let project = service
.create_entity(
"Test Project".to_string(),
EntityType::Project,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create project");
let channel = service
.create_entity(
"Test Channel".to_string(),
EntityType::Channel,
None,
"creator-id".to_string(),
vec!["member1".to_string()],
)
.await
.expect("Failed to create channel");
service
.set_permission_override(
EntityType::Project,
&project.id,
"member1",
"kanban_boards",
"edit",
)
.await
.expect("Failed to set project override");
service
.set_permission_override(
EntityType::Channel,
&channel.id,
"member1",
"messages",
"read_only",
)
.await
.expect("Failed to set channel override");
let project_overrides = service
.get_permission_overrides(EntityType::Project, &project.id, "member1")
.await
.expect("Failed to get project overrides");
let channel_overrides = service
.get_permission_overrides(EntityType::Channel, &channel.id, "member1")
.await
.expect("Failed to get channel overrides");
assert_eq!(project_overrides.len(), 1);
assert!(
project_overrides
.iter()
.any(|(k, v)| k == "kanban_boards" && v == "edit")
);
assert_eq!(channel_overrides.len(), 1);
assert!(
channel_overrides
.iter()
.any(|(k, v)| k == "messages" && v == "read_only")
);
}
}