1use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9
10use crate::crdt_manager::CrdtManager;
11use crate::entity_service::EntityService;
12use crate::invite::{Invite, InviteActionError, InviteStatus};
13use crate::legacy_crdt::EntityType;
14use crate::permissions::{MemberPermissions, ResourceType};
15
16#[derive(Debug, Clone)]
20pub struct InviteRequest {
21 pub recipient_id: String,
23 pub entity_type: EntityType,
25 pub entity_id: String,
27 pub role: String,
29 pub message: Option<String>,
31 pub expires_in_hours: Option<u32>,
33}
34
35impl InviteRequest {
36 pub fn new(
38 recipient_id: impl Into<String>,
39 entity_type: EntityType,
40 entity_id: impl Into<String>,
41 role: impl Into<String>,
42 ) -> Self {
43 Self {
44 recipient_id: recipient_id.into(),
45 entity_type,
46 entity_id: entity_id.into(),
47 role: role.into(),
48 message: None,
49 expires_in_hours: None,
50 }
51 }
52
53 pub fn with_message(mut self, message: impl Into<String>) -> Self {
55 self.message = Some(message.into());
56 self
57 }
58
59 pub fn with_expiration(mut self, hours: u32) -> Self {
61 self.expires_in_hours = Some(hours);
62 self
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum InviteServiceError {
69 EntityNotFound(String),
71
72 MemberNotFound {
74 entity_id: String,
75 member_id: String,
76 },
77
78 InviteNotFound(String),
80
81 PermissionDenied(String),
83
84 InvalidFourWords(String),
86
87 InviteExpired,
89
90 AlreadyResolved(InviteStatus),
92
93 RoleEscalation {
95 granter_role: String,
96 target_role: String,
97 },
98
99 AlreadyMember {
101 entity_id: String,
102 member_id: String,
103 },
104
105 CrdtError(String),
107
108 EntityServiceError(String),
110
111 InviteActionFailed(String),
113}
114
115impl std::fmt::Display for InviteServiceError {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 match self {
118 Self::EntityNotFound(id) => write!(f, "entity not found: {}", id),
119 Self::MemberNotFound {
120 entity_id,
121 member_id,
122 } => {
123 write!(
124 f,
125 "member '{}' not found in entity '{}'",
126 member_id, entity_id
127 )
128 }
129 Self::InviteNotFound(id) => write!(f, "invite not found: {}", id),
130 Self::PermissionDenied(msg) => write!(f, "permission denied: {}", msg),
131 Self::InvalidFourWords(id) => write!(f, "invalid four-word identity: {}", id),
132 Self::InviteExpired => write!(f, "invite has expired"),
133 Self::AlreadyResolved(status) => {
134 write!(f, "invite already resolved with status: {}", status)
135 }
136 Self::RoleEscalation {
137 granter_role,
138 target_role,
139 } => {
140 write!(
141 f,
142 "cannot grant '{}' role when you have '{}' role",
143 target_role, granter_role
144 )
145 }
146 Self::AlreadyMember {
147 entity_id,
148 member_id,
149 } => {
150 write!(f, "'{}' is already a member of '{}'", member_id, entity_id)
151 }
152 Self::CrdtError(msg) => write!(f, "CRDT error: {}", msg),
153 Self::EntityServiceError(msg) => write!(f, "entity service error: {}", msg),
154 Self::InviteActionFailed(msg) => write!(f, "invite action failed: {}", msg),
155 }
156 }
157}
158
159impl std::error::Error for InviteServiceError {}
160
161impl From<InviteActionError> for InviteServiceError {
162 fn from(err: InviteActionError) -> Self {
163 match err {
164 InviteActionError::AlreadyResolved(status) => Self::AlreadyResolved(status),
165 InviteActionError::Expired => Self::InviteExpired,
166 InviteActionError::NotRecipient { expected, actual } => {
167 Self::PermissionDenied(format!(
168 "not the invite recipient: expected '{}', got '{}'",
169 expected, actual
170 ))
171 }
172 }
173 }
174}
175
176pub type InviteServiceResult<T> = Result<T, InviteServiceError>;
178
179fn role_rank(role: &str) -> u8 {
183 match role.to_lowercase().as_str() {
184 "owner" => 5,
185 "admin" => 4,
186 "member" => 3,
187 "viewer" => 2,
188 "guest" => 1,
189 _ => 0,
190 }
191}
192
193pub fn can_grant_role(granter_role: &str, target_role: &str) -> bool {
197 role_rank(granter_role) >= role_rank(target_role)
198}
199
200pub fn validate_four_words_format(identity: &str) -> bool {
205 let parts: Vec<&str> = identity.split('-').collect();
206 parts.len() == 4
207 && parts
208 .iter()
209 .all(|w| !w.is_empty() && w.chars().all(|c| c.is_alphabetic()))
210}
211
212pub struct InviteService {
220 crdt_manager: Arc<CrdtManager>,
222
223 entity_service: Arc<EntityService>,
225
226 invite_cache: RwLock<HashMap<String, Invite>>,
229
230 recipient_index: RwLock<HashMap<String, Vec<String>>>,
232
233 entity_index: RwLock<HashMap<String, Vec<String>>>,
235}
236
237impl InviteService {
238 pub fn new(crdt_manager: Arc<CrdtManager>, entity_service: Arc<EntityService>) -> Self {
240 Self {
241 crdt_manager,
242 entity_service,
243 invite_cache: RwLock::new(HashMap::new()),
244 recipient_index: RwLock::new(HashMap::new()),
245 entity_index: RwLock::new(HashMap::new()),
246 }
247 }
248
249 pub async fn create_invite(
263 &self,
264 creator_id: &str,
265 request: InviteRequest,
266 ) -> InviteServiceResult<Invite> {
267 if !validate_four_words_format(&request.recipient_id) {
269 return Err(InviteServiceError::InvalidFourWords(
270 request.recipient_id.clone(),
271 ));
272 }
273
274 let creator_perms = self
276 .get_member_permissions(request.entity_type, &request.entity_id, creator_id)
277 .await?;
278
279 if !creator_perms.can_edit(ResourceType::Members) {
281 return Err(InviteServiceError::PermissionDenied(
282 "must have Edit access to Members to create invites".to_string(),
283 ));
284 }
285
286 if !can_grant_role(&creator_perms.role, &request.role) {
288 return Err(InviteServiceError::RoleEscalation {
289 granter_role: creator_perms.role.clone(),
290 target_role: request.role.clone(),
291 });
292 }
293
294 if self
296 .is_member(
297 request.entity_type,
298 &request.entity_id,
299 &request.recipient_id,
300 )
301 .await
302 {
303 return Err(InviteServiceError::AlreadyMember {
304 entity_id: request.entity_id.clone(),
305 member_id: request.recipient_id.clone(),
306 });
307 }
308
309 let invite = Invite::new(
311 creator_id.to_string(),
312 request.recipient_id,
313 request.entity_id,
314 request.entity_type,
315 request.role,
316 request.message,
317 request.expires_in_hours,
318 );
319
320 self.store_invite(&invite).await?;
322
323 Ok(invite)
324 }
325
326 pub async fn accept_invite(
342 &self,
343 recipient_id: &str,
344 invite_id: &str,
345 ) -> InviteServiceResult<()> {
346 let mut invite = self.get_invite(invite_id).await?;
348
349 invite.accept(recipient_id)?;
351
352 self.entity_service
354 .add_member(
355 invite.entity_type,
356 &invite.entity_id,
357 recipient_id,
358 &invite.role,
359 )
360 .await
361 .map_err(|e| InviteServiceError::EntityServiceError(e.to_string()))?;
362
363 self.update_invite(&invite).await?;
365
366 Ok(())
367 }
368
369 pub async fn reject_invite(
382 &self,
383 recipient_id: &str,
384 invite_id: &str,
385 ) -> InviteServiceResult<()> {
386 let mut invite = self.get_invite(invite_id).await?;
387 invite.reject(recipient_id)?;
388 self.update_invite(&invite).await?;
389 Ok(())
390 }
391
392 pub async fn revoke_invite(
407 &self,
408 revoker_id: &str,
409 invite_id: &str,
410 ) -> InviteServiceResult<()> {
411 let mut invite = self.get_invite(invite_id).await?;
412
413 if invite.creator_id != revoker_id {
415 let perms = self
416 .get_member_permissions(invite.entity_type, &invite.entity_id, revoker_id)
417 .await?;
418
419 if !perms.can_edit(ResourceType::Members) {
420 return Err(InviteServiceError::PermissionDenied(
421 "only creator or admin can revoke invites".to_string(),
422 ));
423 }
424 }
425
426 invite.revoke(revoker_id)?;
427 self.update_invite(&invite).await?;
428 Ok(())
429 }
430
431 pub async fn list_pending_invites(
435 &self,
436 recipient_id: &str,
437 ) -> InviteServiceResult<Vec<Invite>> {
438 self.list_invites_for_recipient(recipient_id, Some(InviteStatus::Pending))
439 .await
440 }
441
442 pub async fn list_all_invites_for_recipient(
444 &self,
445 recipient_id: &str,
446 ) -> InviteServiceResult<Vec<Invite>> {
447 self.list_invites_for_recipient(recipient_id, None).await
448 }
449
450 pub async fn list_entity_invites(
464 &self,
465 requester_id: &str,
466 entity_type: EntityType,
467 entity_id: &str,
468 ) -> InviteServiceResult<Vec<Invite>> {
469 let perms = self
471 .get_member_permissions(entity_type, entity_id, requester_id)
472 .await?;
473
474 if !perms.can_view(ResourceType::Members) {
475 return Err(InviteServiceError::PermissionDenied(
476 "must have access to Members to view invites".to_string(),
477 ));
478 }
479
480 self.get_invites_for_entity(entity_type, entity_id).await
481 }
482
483 pub async fn get_invite(&self, invite_id: &str) -> InviteServiceResult<Invite> {
485 {
487 let cache = self
488 .invite_cache
489 .read()
490 .map_err(|_| InviteServiceError::CrdtError("cache lock poisoned".to_string()))?;
491 if let Some(invite) = cache.get(invite_id) {
492 return Ok(invite.clone());
493 }
494 }
495
496 self.load_invite_from_crdt(invite_id).await
498 }
499
500 async fn get_member_permissions(
506 &self,
507 entity_type: EntityType,
508 entity_id: &str,
509 member_id: &str,
510 ) -> InviteServiceResult<MemberPermissions> {
511 let role = self
513 .entity_service
514 .get_member_role(entity_type, entity_id, member_id)
515 .await
516 .map_err(|e| {
517 let err_str = e.to_string();
519 if err_str.contains("not found") || err_str.contains("not a member") {
520 InviteServiceError::MemberNotFound {
521 entity_id: entity_id.to_string(),
522 member_id: member_id.to_string(),
523 }
524 } else {
525 InviteServiceError::EntityServiceError(err_str)
526 }
527 })?;
528
529 let overrides = self
531 .entity_service
532 .get_permission_overrides(entity_type, entity_id, member_id)
533 .await
534 .unwrap_or_default();
535
536 let mut perms = MemberPermissions::new(member_id.to_string(), role);
538 for (resource_str, level_str) in overrides {
539 if let (Ok(resource), Ok(level)) = (resource_str.parse(), level_str.parse()) {
540 perms.overrides.insert(resource, level);
541 }
542 }
543
544 Ok(perms)
545 }
546
547 async fn is_member(&self, entity_type: EntityType, entity_id: &str, member_id: &str) -> bool {
549 self.entity_service
550 .get_member_role(entity_type, entity_id, member_id)
551 .await
552 .is_ok()
553 }
554
555 async fn store_invite(&self, invite: &Invite) -> InviteServiceResult<()> {
557 {
559 let mut cache = self
560 .invite_cache
561 .write()
562 .map_err(|_| InviteServiceError::CrdtError("cache lock poisoned".to_string()))?;
563 cache.insert(invite.id.clone(), invite.clone());
564 }
565
566 {
568 let mut index = self
569 .recipient_index
570 .write()
571 .map_err(|_| InviteServiceError::CrdtError("index lock poisoned".to_string()))?;
572 index
573 .entry(invite.recipient_id.clone())
574 .or_default()
575 .push(invite.id.clone());
576 }
577
578 {
580 let entity_key = format!("{:?}:{}", invite.entity_type, invite.entity_id);
581 let mut index = self
582 .entity_index
583 .write()
584 .map_err(|_| InviteServiceError::CrdtError("index lock poisoned".to_string()))?;
585 index.entry(entity_key).or_default().push(invite.id.clone());
586 }
587
588 self.store_invite_in_crdt(invite).await
590 }
591
592 async fn update_invite(&self, invite: &Invite) -> InviteServiceResult<()> {
594 {
596 let mut cache = self
597 .invite_cache
598 .write()
599 .map_err(|_| InviteServiceError::CrdtError("cache lock poisoned".to_string()))?;
600 cache.insert(invite.id.clone(), invite.clone());
601 }
602
603 self.store_invite_in_crdt(invite).await
605 }
606
607 async fn store_invite_in_crdt(&self, invite: &Invite) -> InviteServiceResult<()> {
617 use yrs::{Doc, Map, Transact};
618
619 let doc_id = self.entity_invite_doc_id(invite.entity_type, &invite.entity_id);
620 let entity_type_str = format!("{:?}", invite.entity_type).to_lowercase();
621
622 let doc = match self.crdt_manager.load_document(&doc_id).await {
624 Ok(doc) => doc,
625 Err(crate::crdt_manager::CrdtError::DocumentNotFound(_)) => Doc::new(),
626 Err(e) => return Err(InviteServiceError::CrdtError(e.to_string())),
627 };
628
629 let invite_json = serde_json::to_string(invite).map_err(|e| {
631 InviteServiceError::CrdtError(format!("Failed to serialize invite: {}", e))
632 })?;
633
634 {
636 let invites_map = doc.get_or_insert_map("invites");
637 let mut txn = doc.transact_mut();
638 invites_map.insert(&mut txn, invite.id.clone(), invite_json);
639 }
640
641 self.crdt_manager
643 .save_document(&doc_id, &entity_type_str, &invite.entity_id, &doc)
644 .await
645 .map_err(|e| InviteServiceError::CrdtError(e.to_string()))?;
646
647 self.update_global_invite_index(&invite.id, &doc_id).await?;
649
650 Ok(())
651 }
652
653 async fn update_global_invite_index(
655 &self,
656 invite_id: &str,
657 entity_doc_id: &str,
658 ) -> InviteServiceResult<()> {
659 use yrs::{Doc, Map, Transact};
660
661 let index_doc_id = self.global_invite_index_doc_id();
662
663 let index_doc = match self.crdt_manager.load_document(&index_doc_id).await {
665 Ok(doc) => doc,
666 Err(crate::crdt_manager::CrdtError::DocumentNotFound(_)) => Doc::new(),
667 Err(e) => return Err(InviteServiceError::CrdtError(e.to_string())),
668 };
669
670 {
672 let index_map = index_doc.get_or_insert_map("invite_to_entity");
673 let mut txn = index_doc.transact_mut();
674 index_map.insert(&mut txn, invite_id, entity_doc_id);
675 }
676
677 self.crdt_manager
680 .save_document(&index_doc_id, "invites", "global", &index_doc)
681 .await
682 .map_err(|e| InviteServiceError::CrdtError(e.to_string()))?;
683
684 Ok(())
685 }
686
687 async fn load_invite_from_crdt(&self, invite_id: &str) -> InviteServiceResult<Invite> {
692 use yrs::{Map, Transact};
693
694 {
696 let cache = self
697 .invite_cache
698 .read()
699 .map_err(|_| InviteServiceError::CrdtError("cache lock poisoned".to_string()))?;
700 if let Some(invite) = cache.get(invite_id) {
701 return Ok(invite.clone());
702 }
703 }
704
705 let index_doc_id = self.global_invite_index_doc_id();
707 let index_doc = match self.crdt_manager.load_document(&index_doc_id).await {
708 Ok(doc) => doc,
709 Err(crate::crdt_manager::CrdtError::DocumentNotFound(_)) => {
710 return Err(InviteServiceError::InviteNotFound(invite_id.to_string()));
711 }
712 Err(e) => return Err(InviteServiceError::CrdtError(e.to_string())),
713 };
714
715 let entity_doc_id = {
717 let index_map = index_doc.get_or_insert_map("invite_to_entity");
718 let txn = index_doc.transact();
719 match index_map.get(&txn, invite_id) {
720 Some(yrs::Out::Any(yrs::Any::String(s))) => s.to_string(),
721 _ => return Err(InviteServiceError::InviteNotFound(invite_id.to_string())),
722 }
723 };
724
725 let entity_doc = self
727 .crdt_manager
728 .load_document(&entity_doc_id)
729 .await
730 .map_err(|e| InviteServiceError::CrdtError(e.to_string()))?;
731
732 let invites_map = entity_doc.get_or_insert_map("invites");
734 let txn = entity_doc.transact();
735 let invite_json = match invites_map.get(&txn, invite_id) {
736 Some(yrs::Out::Any(yrs::Any::String(s))) => s.to_string(),
737 _ => return Err(InviteServiceError::InviteNotFound(invite_id.to_string())),
738 };
739
740 let invite: Invite = serde_json::from_str(&invite_json).map_err(|e| {
741 InviteServiceError::CrdtError(format!("Failed to deserialize invite: {}", e))
742 })?;
743
744 Ok(invite)
745 }
746
747 #[allow(dead_code)]
752 async fn load_invites_for_entity_from_crdt(
753 &self,
754 entity_type: EntityType,
755 entity_id: &str,
756 ) -> InviteServiceResult<Vec<Invite>> {
757 use yrs::{Map, Transact};
758
759 let doc_id = self.entity_invite_doc_id(entity_type, entity_id);
760
761 let doc = match self.crdt_manager.load_document(&doc_id).await {
763 Ok(doc) => doc,
764 Err(crate::crdt_manager::CrdtError::DocumentNotFound(_)) => {
765 return Ok(Vec::new()); }
767 Err(e) => return Err(InviteServiceError::CrdtError(e.to_string())),
768 };
769
770 let invites_map = doc.get_or_insert_map("invites");
772 let txn = doc.transact();
773
774 let mut invites = Vec::new();
775 for (key, value) in invites_map.iter(&txn) {
776 if let yrs::Out::Any(yrs::Any::String(invite_json)) = value {
777 let invite: Invite = serde_json::from_str(invite_json.as_ref()).map_err(|e| {
778 InviteServiceError::CrdtError(format!(
779 "Failed to deserialize invite {}: {}",
780 key, e
781 ))
782 })?;
783 invites.push(invite);
784 }
785 }
786
787 Ok(invites)
788 }
789
790 #[allow(dead_code)]
798 async fn store_recipient_index_in_crdt(
799 &self,
800 recipient_id: &str,
801 invite_ids: &[String],
802 ) -> InviteServiceResult<()> {
803 use yrs::{Doc, Map, Transact};
804
805 let doc_id = self.recipient_index_doc_id(recipient_id);
806
807 let doc = match self.crdt_manager.load_document(&doc_id).await {
809 Ok(doc) => doc,
810 Err(crate::crdt_manager::CrdtError::DocumentNotFound(_)) => Doc::new(),
811 Err(e) => return Err(InviteServiceError::CrdtError(e.to_string())),
812 };
813
814 let invite_ids_json = serde_json::to_string(invite_ids).map_err(|e| {
816 InviteServiceError::CrdtError(format!("Failed to serialize invite IDs: {}", e))
817 })?;
818
819 {
821 let index_map = doc.get_or_insert_map("recipient_invites");
822 let mut txn = doc.transact_mut();
823 index_map.insert(&mut txn, recipient_id, invite_ids_json);
824 }
825
826 self.crdt_manager
828 .save_document(&doc_id, "invites", recipient_id, &doc)
829 .await
830 .map_err(|e| InviteServiceError::CrdtError(e.to_string()))?;
831
832 Ok(())
833 }
834
835 #[allow(dead_code)]
840 async fn load_recipient_index_from_crdt(
841 &self,
842 recipient_id: &str,
843 ) -> InviteServiceResult<Vec<String>> {
844 use yrs::{Map, Transact};
845
846 let doc_id = self.recipient_index_doc_id(recipient_id);
847
848 let doc = match self.crdt_manager.load_document(&doc_id).await {
850 Ok(doc) => doc,
851 Err(crate::crdt_manager::CrdtError::DocumentNotFound(_)) => {
852 return Ok(Vec::new()); }
854 Err(e) => return Err(InviteServiceError::CrdtError(e.to_string())),
855 };
856
857 let index_map = doc.get_or_insert_map("recipient_invites");
859 let txn = doc.transact();
860
861 let invite_ids_json = match index_map.get(&txn, recipient_id) {
862 Some(yrs::Out::Any(yrs::Any::String(s))) => s.to_string(),
863 _ => return Ok(Vec::new()),
864 };
865
866 let invite_ids: Vec<String> = serde_json::from_str(&invite_ids_json).map_err(|e| {
867 InviteServiceError::CrdtError(format!("Failed to deserialize invite IDs: {}", e))
868 })?;
869
870 Ok(invite_ids)
871 }
872
873 fn entity_invite_doc_id(&self, entity_type: EntityType, entity_id: &str) -> String {
878 let entity_type_str = format!("{:?}", entity_type).to_lowercase();
879 format!("{}:{}:invites", entity_type_str, entity_id)
880 }
881
882 #[allow(dead_code)]
889 fn recipient_index_doc_id(&self, recipient_id: &str) -> String {
890 format!("invites:{}:index", recipient_id)
891 }
892
893 fn global_invite_index_doc_id(&self) -> String {
897 "invites:global:index".to_string()
898 }
899
900 async fn list_invites_for_recipient(
902 &self,
903 recipient_id: &str,
904 status_filter: Option<InviteStatus>,
905 ) -> InviteServiceResult<Vec<Invite>> {
906 let invite_ids = {
907 let index = self
908 .recipient_index
909 .read()
910 .map_err(|_| InviteServiceError::CrdtError("index lock poisoned".to_string()))?;
911 index.get(recipient_id).cloned().unwrap_or_default()
912 };
913
914 let cache = self
915 .invite_cache
916 .read()
917 .map_err(|_| InviteServiceError::CrdtError("cache lock poisoned".to_string()))?;
918 let mut result = Vec::new();
919
920 for invite_id in invite_ids {
921 if let Some(invite) = cache.get(&invite_id) {
922 if let Some(filter) = status_filter {
924 if invite.status == filter {
925 if filter == InviteStatus::Pending && invite.is_expired() {
927 continue;
928 }
929 result.push(invite.clone());
930 }
931 } else {
932 result.push(invite.clone());
933 }
934 }
935 }
936
937 Ok(result)
938 }
939
940 async fn get_invites_for_entity(
942 &self,
943 entity_type: EntityType,
944 entity_id: &str,
945 ) -> InviteServiceResult<Vec<Invite>> {
946 let entity_key = format!("{:?}:{}", entity_type, entity_id);
947 let invite_ids = {
948 let index = self
949 .entity_index
950 .read()
951 .map_err(|_| InviteServiceError::CrdtError("lock poisoned".to_string()))?;
952 index.get(&entity_key).cloned().unwrap_or_default()
953 };
954
955 let cache = self
956 .invite_cache
957 .read()
958 .map_err(|_| InviteServiceError::CrdtError("cache lock poisoned".to_string()))?;
959 let mut result = Vec::new();
960
961 for invite_id in invite_ids {
962 if let Some(invite) = cache.get(&invite_id) {
963 result.push(invite.clone());
964 }
965 }
966
967 Ok(result)
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 #[allow(dead_code)]
978 struct MockEntityService {
979 members: RwLock<HashMap<String, HashMap<String, String>>>, permission_overrides: RwLock<HashMap<String, Vec<(String, String)>>>, }
982
983 #[allow(dead_code)]
984 impl MockEntityService {
985 fn new() -> Self {
986 Self {
987 members: RwLock::new(HashMap::new()),
988 permission_overrides: RwLock::new(HashMap::new()),
989 }
990 }
991
992 fn add_member_sync(
993 &self,
994 entity_type: EntityType,
995 entity_id: &str,
996 member_id: &str,
997 role: &str,
998 ) {
999 let entity_key = format!("{:?}:{}", entity_type, entity_id);
1000 let mut members = self.members.write().unwrap();
1001 members
1002 .entry(entity_key)
1003 .or_default()
1004 .insert(member_id.to_string(), role.to_string());
1005 }
1006
1007 fn get_role(
1008 &self,
1009 entity_type: EntityType,
1010 entity_id: &str,
1011 member_id: &str,
1012 ) -> Option<String> {
1013 let entity_key = format!("{:?}:{}", entity_type, entity_id);
1014 let members = self.members.read().unwrap();
1015 members
1016 .get(&entity_key)
1017 .and_then(|m| m.get(member_id).cloned())
1018 }
1019
1020 fn set_permission_override(
1021 &self,
1022 entity_type: EntityType,
1023 entity_id: &str,
1024 member_id: &str,
1025 resource: &str,
1026 level: &str,
1027 ) {
1028 let key = format!("{:?}:{}:{}", entity_type, entity_id, member_id);
1029 let mut overrides = self.permission_overrides.write().unwrap();
1030 overrides
1031 .entry(key)
1032 .or_default()
1033 .push((resource.to_string(), level.to_string()));
1034 }
1035
1036 fn get_overrides(
1037 &self,
1038 entity_type: EntityType,
1039 entity_id: &str,
1040 member_id: &str,
1041 ) -> Vec<(String, String)> {
1042 let key = format!("{:?}:{}:{}", entity_type, entity_id, member_id);
1043 let overrides = self.permission_overrides.read().unwrap();
1044 overrides.get(&key).cloned().unwrap_or_default()
1045 }
1046 }
1047
1048 #[test]
1053 fn test_role_rank() {
1054 assert_eq!(role_rank("owner"), 5);
1055 assert_eq!(role_rank("admin"), 4);
1056 assert_eq!(role_rank("member"), 3);
1057 assert_eq!(role_rank("viewer"), 2);
1058 assert_eq!(role_rank("guest"), 1);
1059 assert_eq!(role_rank("unknown"), 0);
1060 }
1061
1062 #[test]
1063 fn test_role_rank_case_insensitive() {
1064 assert_eq!(role_rank("OWNER"), 5);
1065 assert_eq!(role_rank("Owner"), 5);
1066 assert_eq!(role_rank("ADMIN"), 4);
1067 }
1068
1069 #[test]
1070 fn test_can_grant_role_same_level() {
1071 assert!(can_grant_role("member", "member"));
1072 assert!(can_grant_role("admin", "admin"));
1073 assert!(can_grant_role("owner", "owner"));
1074 }
1075
1076 #[test]
1077 fn test_can_grant_role_higher_to_lower() {
1078 assert!(can_grant_role("owner", "admin"));
1079 assert!(can_grant_role("owner", "member"));
1080 assert!(can_grant_role("owner", "viewer"));
1081 assert!(can_grant_role("owner", "guest"));
1082 assert!(can_grant_role("admin", "member"));
1083 assert!(can_grant_role("member", "viewer"));
1084 }
1085
1086 #[test]
1087 fn test_can_grant_role_lower_to_higher_fails() {
1088 assert!(!can_grant_role("guest", "viewer"));
1089 assert!(!can_grant_role("viewer", "member"));
1090 assert!(!can_grant_role("member", "admin"));
1091 assert!(!can_grant_role("admin", "owner"));
1092 }
1093
1094 #[test]
1095 fn test_validate_four_words_format_valid() {
1096 assert!(validate_four_words_format("alice-bob-carol-dave"));
1097 assert!(validate_four_words_format("ocean-forest-moon-star"));
1098 assert!(validate_four_words_format("a-b-c-d"));
1099 }
1100
1101 #[test]
1102 fn test_validate_four_words_format_invalid() {
1103 assert!(!validate_four_words_format("alice-bob-carol")); assert!(!validate_four_words_format("alice-bob-carol-dave-eve")); assert!(!validate_four_words_format("alice--bob-carol")); assert!(!validate_four_words_format("alice-bob123-carol-dave")); assert!(!validate_four_words_format("")); assert!(!validate_four_words_format("----")); }
1110
1111 #[test]
1116 fn test_error_display() {
1117 let err = InviteServiceError::EntityNotFound("entity-123".to_string());
1118 assert!(err.to_string().contains("entity-123"));
1119
1120 let err = InviteServiceError::MemberNotFound {
1121 entity_id: "entity-1".to_string(),
1122 member_id: "alice-a-b-c".to_string(),
1123 };
1124 assert!(err.to_string().contains("entity-1"));
1125 assert!(err.to_string().contains("alice-a-b-c"));
1126
1127 let err = InviteServiceError::RoleEscalation {
1128 granter_role: "member".to_string(),
1129 target_role: "admin".to_string(),
1130 };
1131 assert!(err.to_string().contains("member"));
1132 assert!(err.to_string().contains("admin"));
1133
1134 let err = InviteServiceError::AlreadyMember {
1135 entity_id: "entity-1".to_string(),
1136 member_id: "bob-b-c-d".to_string(),
1137 };
1138 assert!(err.to_string().contains("already a member"));
1139 }
1140
1141 #[test]
1142 fn test_error_from_invite_action_error() {
1143 let action_err = InviteActionError::Expired;
1144 let service_err: InviteServiceError = action_err.into();
1145 assert!(matches!(service_err, InviteServiceError::InviteExpired));
1146
1147 let action_err = InviteActionError::AlreadyResolved(InviteStatus::Accepted);
1148 let service_err: InviteServiceError = action_err.into();
1149 assert!(matches!(
1150 service_err,
1151 InviteServiceError::AlreadyResolved(InviteStatus::Accepted)
1152 ));
1153
1154 let action_err = InviteActionError::NotRecipient {
1155 expected: "alice-a-b-c".to_string(),
1156 actual: "bob-d-e-f".to_string(),
1157 };
1158 let service_err: InviteServiceError = action_err.into();
1159 assert!(matches!(
1160 service_err,
1161 InviteServiceError::PermissionDenied(_)
1162 ));
1163 }
1164
1165 #[test]
1170 fn test_invite_request_new_sets_required_fields() {
1171 let request = InviteRequest::new(
1172 "alice-bob-carol-dave",
1173 EntityType::Organisation,
1174 "org-123",
1175 "member",
1176 );
1177
1178 assert_eq!(request.recipient_id, "alice-bob-carol-dave");
1179 assert_eq!(request.entity_type, EntityType::Organisation);
1180 assert_eq!(request.entity_id, "org-123");
1181 assert_eq!(request.role, "member");
1182 assert!(request.message.is_none());
1183 assert!(request.expires_in_hours.is_none());
1184 }
1185
1186 #[test]
1187 fn test_invite_request_new_accepts_string_types() {
1188 let req1 = InviteRequest::new("alice-a-b-c", EntityType::Group, "group-1", "viewer");
1190 assert_eq!(req1.recipient_id, "alice-a-b-c");
1191
1192 let req2 = InviteRequest::new(
1194 String::from("bob-d-e-f"),
1195 EntityType::Channel,
1196 String::from("channel-1"),
1197 String::from("member"),
1198 );
1199 assert_eq!(req2.recipient_id, "bob-d-e-f");
1200 assert_eq!(req2.entity_id, "channel-1");
1201 assert_eq!(req2.role, "member");
1202 }
1203
1204 #[test]
1205 fn test_invite_request_with_message() {
1206 let request = InviteRequest::new("alice-a-b-c", EntityType::Project, "proj-1", "admin")
1207 .with_message("Welcome to the team!");
1208
1209 assert_eq!(request.message, Some("Welcome to the team!".to_string()));
1210 assert_eq!(request.recipient_id, "alice-a-b-c");
1212 assert_eq!(request.role, "admin");
1213 assert!(request.expires_in_hours.is_none());
1214 }
1215
1216 #[test]
1217 fn test_invite_request_with_expiration() {
1218 let request = InviteRequest::new("bob-x-y-z", EntityType::Organisation, "org-1", "member")
1219 .with_expiration(48);
1220
1221 assert_eq!(request.expires_in_hours, Some(48));
1222 assert_eq!(request.recipient_id, "bob-x-y-z");
1224 assert!(request.message.is_none());
1225 }
1226
1227 #[test]
1228 fn test_invite_request_builder_chaining() {
1229 let request = InviteRequest::new("carol-m-n-o", EntityType::Group, "group-42", "viewer")
1230 .with_message("Join our group!")
1231 .with_expiration(24);
1232
1233 assert_eq!(request.recipient_id, "carol-m-n-o");
1234 assert_eq!(request.entity_type, EntityType::Group);
1235 assert_eq!(request.entity_id, "group-42");
1236 assert_eq!(request.role, "viewer");
1237 assert_eq!(request.message, Some("Join our group!".to_string()));
1238 assert_eq!(request.expires_in_hours, Some(24));
1239 }
1240
1241 #[test]
1242 fn test_invite_request_builder_order_independence() {
1243 let req1 = InviteRequest::new("a-b-c-d", EntityType::Channel, "ch-1", "member")
1245 .with_expiration(12)
1246 .with_message("Hello");
1247
1248 let req2 = InviteRequest::new("a-b-c-d", EntityType::Channel, "ch-1", "member")
1250 .with_message("Hello")
1251 .with_expiration(12);
1252
1253 assert_eq!(req1.message, req2.message);
1254 assert_eq!(req1.expires_in_hours, req2.expires_in_hours);
1255 assert_eq!(req1.recipient_id, req2.recipient_id);
1256 assert_eq!(req1.role, req2.role);
1257 }
1258
1259 #[test]
1260 fn test_invite_request_with_all_entity_types() {
1261 let entity_types = [
1262 EntityType::Organisation,
1263 EntityType::Group,
1264 EntityType::Channel,
1265 EntityType::Project,
1266 ];
1267
1268 for entity_type in entity_types {
1269 let request = InviteRequest::new("test-a-b-c", entity_type, "entity-id", "member");
1270 assert_eq!(request.entity_type, entity_type);
1271 }
1272 }
1273
1274 #[test]
1275 fn test_invite_request_with_all_roles() {
1276 let roles = ["owner", "admin", "member", "viewer", "guest"];
1277
1278 for role in roles {
1279 let request = InviteRequest::new("test-a-b-c", EntityType::Group, "group-1", role);
1280 assert_eq!(request.role, role);
1281 }
1282 }
1283
1284 #[test]
1285 fn test_invite_request_empty_message_allowed() {
1286 let request =
1287 InviteRequest::new("alice-a-b-c", EntityType::Organisation, "org-1", "member")
1288 .with_message("");
1289
1290 assert_eq!(request.message, Some(String::new()));
1291 }
1292
1293 #[test]
1294 fn test_invite_request_zero_expiration_allowed() {
1295 let request = InviteRequest::new("alice-a-b-c", EntityType::Group, "group-1", "viewer")
1297 .with_expiration(0);
1298
1299 assert_eq!(request.expires_in_hours, Some(0));
1300 }
1301
1302 #[test]
1303 fn test_invite_request_long_expiration() {
1304 let request = InviteRequest::new("alice-a-b-c", EntityType::Project, "proj-1", "member")
1306 .with_expiration(720);
1307
1308 assert_eq!(request.expires_in_hours, Some(720));
1309 }
1310
1311 use crate::crdt_manager::CrdtManager;
1316 use tempfile::tempdir;
1317
1318 async fn create_test_invite_service() -> (InviteService, Arc<EntityService>, String) {
1320 let temp_dir = tempdir().unwrap();
1321 let db_path = temp_dir.path().join("test.db");
1322 let crdt_manager = Arc::new(CrdtManager::new(&db_path).await.unwrap());
1323 let entity_service = Arc::new(EntityService::new(crdt_manager.clone()));
1324 let invite_service = InviteService::new(crdt_manager, entity_service.clone());
1325 let temp_path = temp_dir.path().to_string_lossy().to_string();
1327 std::mem::forget(temp_dir); (invite_service, entity_service, temp_path)
1329 }
1330
1331 #[tokio::test]
1332 async fn test_create_invite_invalid_recipient_format() {
1333 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1334
1335 let org = entity_service
1337 .create_entity(
1338 "Test Org".to_string(),
1339 EntityType::Organisation,
1340 Some("A test org".to_string()),
1341 "owner-abc-def-ghi".to_string(),
1342 vec![],
1343 )
1344 .await
1345 .expect("Failed to create org");
1346
1347 let request = InviteRequest::new(
1349 "invalid-format", EntityType::Organisation,
1351 &org.id,
1352 "member",
1353 );
1354
1355 let result = invite_service
1356 .create_invite("owner-abc-def-ghi", request)
1357 .await;
1358
1359 assert!(matches!(
1360 result,
1361 Err(InviteServiceError::InvalidFourWords(_))
1362 ));
1363 }
1364
1365 #[tokio::test]
1366 async fn test_create_invite_creator_not_member() {
1367 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1368
1369 let org = entity_service
1371 .create_entity(
1372 "Test Org".to_string(),
1373 EntityType::Organisation,
1374 Some("A test org".to_string()),
1375 "owner-abc-def-ghi".to_string(),
1376 vec![],
1377 )
1378 .await
1379 .expect("Failed to create org");
1380
1381 let request = InviteRequest::new(
1383 "alice-bob-carol-dave",
1384 EntityType::Organisation,
1385 &org.id,
1386 "member",
1387 );
1388
1389 let result = invite_service
1390 .create_invite("other-user-not-member", request)
1391 .await;
1392
1393 assert!(matches!(
1395 result,
1396 Err(InviteServiceError::MemberNotFound { .. })
1397 ));
1398 }
1399
1400 #[tokio::test]
1401 async fn test_create_invite_success() {
1402 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1403
1404 let creator_id = "owner-abc-def-ghi";
1406 let org = entity_service
1407 .create_entity(
1408 "Test Org".to_string(),
1409 EntityType::Organisation,
1410 Some("A test org".to_string()),
1411 creator_id.to_string(),
1412 vec![],
1413 )
1414 .await
1415 .expect("Failed to create org");
1416
1417 entity_service
1419 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1420 .await
1421 .expect("Failed to set role");
1422
1423 let request = InviteRequest::new(
1425 "alice-bob-carol-dave",
1426 EntityType::Organisation,
1427 &org.id,
1428 "member",
1429 )
1430 .with_message("Welcome to the team!")
1431 .with_expiration(48);
1432
1433 let invite = invite_service
1434 .create_invite(creator_id, request)
1435 .await
1436 .expect("Failed to create invite");
1437
1438 assert_eq!(invite.creator_id, creator_id);
1439 assert_eq!(invite.recipient_id, "alice-bob-carol-dave");
1440 assert_eq!(invite.entity_id, org.id);
1441 assert_eq!(invite.role, "member");
1442 assert_eq!(invite.message, Some("Welcome to the team!".to_string()));
1443 assert_eq!(invite.status, InviteStatus::Pending);
1444 assert!(invite.is_valid());
1445 }
1446
1447 #[tokio::test]
1448 async fn test_create_invite_role_escalation_blocked() {
1449 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1450
1451 let owner_id = "owner-abc-def-ghi";
1453 let org = entity_service
1454 .create_entity(
1455 "Test Org".to_string(),
1456 EntityType::Organisation,
1457 Some("A test org".to_string()),
1458 owner_id.to_string(),
1459 vec![],
1460 )
1461 .await
1462 .expect("Failed to create org");
1463
1464 let member_id = "member-xyz-uvw-rst";
1466 entity_service
1467 .add_member(EntityType::Organisation, &org.id, member_id, "member")
1468 .await
1469 .expect("Failed to add member");
1470
1471 let request = InviteRequest::new(
1473 "alice-bob-carol-dave",
1474 EntityType::Organisation,
1475 &org.id,
1476 "admin", );
1478
1479 let result = invite_service.create_invite(member_id, request).await;
1480
1481 assert!(
1484 matches!(result, Err(InviteServiceError::PermissionDenied(_))),
1485 "Expected PermissionDenied, got: {:?}",
1486 result
1487 );
1488 }
1489
1490 #[tokio::test]
1491 async fn test_create_invite_role_escalation_by_admin() {
1492 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1493
1494 let owner_id = "owner-abc-def-ghi";
1496 let org = entity_service
1497 .create_entity(
1498 "Test Org".to_string(),
1499 EntityType::Organisation,
1500 Some("A test org".to_string()),
1501 owner_id.to_string(),
1502 vec![],
1503 )
1504 .await
1505 .expect("Failed to create org");
1506
1507 let admin_id = "admin-xyz-uvw-rst";
1509 entity_service
1510 .add_member(EntityType::Organisation, &org.id, admin_id, "admin")
1511 .await
1512 .expect("Failed to add admin");
1513
1514 let request = InviteRequest::new(
1516 "alice-bob-carol-dave",
1517 EntityType::Organisation,
1518 &org.id,
1519 "owner", );
1521
1522 let result = invite_service.create_invite(admin_id, request).await;
1523
1524 assert!(
1525 matches!(result, Err(InviteServiceError::RoleEscalation { .. })),
1526 "Expected RoleEscalation, got: {:?}",
1527 result
1528 );
1529 }
1530
1531 #[tokio::test]
1532 async fn test_get_invite() {
1533 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1534
1535 let creator_id = "owner-abc-def-ghi";
1537 let org = entity_service
1538 .create_entity(
1539 "Test Org".to_string(),
1540 EntityType::Organisation,
1541 None,
1542 creator_id.to_string(),
1543 vec![],
1544 )
1545 .await
1546 .expect("Failed to create org");
1547
1548 entity_service
1549 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1550 .await
1551 .expect("Failed to set role");
1552
1553 let request = InviteRequest::new(
1555 "alice-bob-carol-dave",
1556 EntityType::Organisation,
1557 &org.id,
1558 "member",
1559 );
1560
1561 let created_invite = invite_service
1562 .create_invite(creator_id, request)
1563 .await
1564 .expect("Failed to create invite");
1565
1566 let retrieved_invite = invite_service
1568 .get_invite(&created_invite.id)
1569 .await
1570 .expect("Failed to get invite");
1571
1572 assert_eq!(retrieved_invite.id, created_invite.id);
1573 assert_eq!(retrieved_invite.creator_id, created_invite.creator_id);
1574 assert_eq!(retrieved_invite.recipient_id, created_invite.recipient_id);
1575 }
1576
1577 #[tokio::test]
1578 async fn test_get_invite_not_found() {
1579 let (invite_service, _entity_service, _temp) = create_test_invite_service().await;
1580
1581 let result = invite_service.get_invite("nonexistent-id").await;
1582 assert!(matches!(result, Err(InviteServiceError::InviteNotFound(_))));
1583 }
1584
1585 #[tokio::test]
1586 async fn test_accept_invite_success() {
1587 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1588
1589 let creator_id = "owner-abc-def-ghi";
1591 let org = entity_service
1592 .create_entity(
1593 "Test Org".to_string(),
1594 EntityType::Organisation,
1595 None,
1596 creator_id.to_string(),
1597 vec![],
1598 )
1599 .await
1600 .expect("Failed to create org");
1601
1602 entity_service
1603 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1604 .await
1605 .expect("Failed to set role");
1606
1607 let recipient_id = "alice-bob-carol-dave";
1609 let request = InviteRequest::new(recipient_id, EntityType::Organisation, &org.id, "member");
1610
1611 let invite = invite_service
1612 .create_invite(creator_id, request)
1613 .await
1614 .expect("Failed to create invite");
1615
1616 invite_service
1618 .accept_invite(recipient_id, &invite.id)
1619 .await
1620 .expect("Failed to accept invite");
1621
1622 let accepted_invite = invite_service
1624 .get_invite(&invite.id)
1625 .await
1626 .expect("Failed to get invite");
1627
1628 assert_eq!(accepted_invite.status, InviteStatus::Accepted);
1629
1630 let role = entity_service
1632 .get_member_role(EntityType::Organisation, &org.id, recipient_id)
1633 .await
1634 .expect("Failed to get role");
1635
1636 assert_eq!(role, "member");
1637 }
1638
1639 #[tokio::test]
1640 async fn test_accept_invite_wrong_recipient() {
1641 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1642
1643 let creator_id = "owner-abc-def-ghi";
1645 let org = entity_service
1646 .create_entity(
1647 "Test Org".to_string(),
1648 EntityType::Organisation,
1649 None,
1650 creator_id.to_string(),
1651 vec![],
1652 )
1653 .await
1654 .expect("Failed to create org");
1655
1656 entity_service
1657 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1658 .await
1659 .expect("Failed to set role");
1660
1661 let request = InviteRequest::new(
1663 "alice-bob-carol-dave",
1664 EntityType::Organisation,
1665 &org.id,
1666 "member",
1667 );
1668
1669 let invite = invite_service
1670 .create_invite(creator_id, request)
1671 .await
1672 .expect("Failed to create invite");
1673
1674 let result = invite_service
1676 .accept_invite("bob-xyz-uvw-rst", &invite.id)
1677 .await;
1678
1679 assert!(matches!(
1680 result,
1681 Err(InviteServiceError::PermissionDenied(_))
1682 ));
1683 }
1684
1685 #[tokio::test]
1686 async fn test_reject_invite_success() {
1687 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1688
1689 let creator_id = "owner-abc-def-ghi";
1691 let org = entity_service
1692 .create_entity(
1693 "Test Org".to_string(),
1694 EntityType::Organisation,
1695 None,
1696 creator_id.to_string(),
1697 vec![],
1698 )
1699 .await
1700 .expect("Failed to create org");
1701
1702 entity_service
1703 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1704 .await
1705 .expect("Failed to set role");
1706
1707 let recipient_id = "alice-bob-carol-dave";
1709 let request = InviteRequest::new(recipient_id, EntityType::Organisation, &org.id, "member");
1710
1711 let invite = invite_service
1712 .create_invite(creator_id, request)
1713 .await
1714 .expect("Failed to create invite");
1715
1716 invite_service
1718 .reject_invite(recipient_id, &invite.id)
1719 .await
1720 .expect("Failed to reject invite");
1721
1722 let rejected_invite = invite_service
1724 .get_invite(&invite.id)
1725 .await
1726 .expect("Failed to get invite");
1727
1728 assert_eq!(rejected_invite.status, InviteStatus::Rejected);
1729
1730 let role_result = entity_service
1732 .get_member_role(EntityType::Organisation, &org.id, recipient_id)
1733 .await;
1734
1735 assert!(role_result.is_err());
1736 }
1737
1738 #[tokio::test]
1739 async fn test_revoke_invite_by_creator() {
1740 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1741
1742 let creator_id = "owner-abc-def-ghi";
1744 let org = entity_service
1745 .create_entity(
1746 "Test Org".to_string(),
1747 EntityType::Organisation,
1748 None,
1749 creator_id.to_string(),
1750 vec![],
1751 )
1752 .await
1753 .expect("Failed to create org");
1754
1755 entity_service
1756 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1757 .await
1758 .expect("Failed to set role");
1759
1760 let request = InviteRequest::new(
1762 "alice-bob-carol-dave",
1763 EntityType::Organisation,
1764 &org.id,
1765 "member",
1766 );
1767
1768 let invite = invite_service
1769 .create_invite(creator_id, request)
1770 .await
1771 .expect("Failed to create invite");
1772
1773 invite_service
1775 .revoke_invite(creator_id, &invite.id)
1776 .await
1777 .expect("Failed to revoke invite");
1778
1779 let revoked_invite = invite_service
1781 .get_invite(&invite.id)
1782 .await
1783 .expect("Failed to get invite");
1784
1785 assert_eq!(revoked_invite.status, InviteStatus::Revoked);
1786 }
1787
1788 #[tokio::test]
1789 async fn test_revoke_invite_by_admin() {
1790 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1791
1792 let owner_id = "owner-abc-def-ghi";
1794 let admin_id = "admin-xyz-uvw-rst";
1795 let org = entity_service
1796 .create_entity(
1797 "Test Org".to_string(),
1798 EntityType::Organisation,
1799 None,
1800 owner_id.to_string(),
1801 vec![],
1802 )
1803 .await
1804 .expect("Failed to create org");
1805
1806 entity_service
1807 .set_member_role(EntityType::Organisation, &org.id, owner_id, "owner")
1808 .await
1809 .expect("Failed to set owner role");
1810
1811 entity_service
1812 .add_member(EntityType::Organisation, &org.id, admin_id, "admin")
1813 .await
1814 .expect("Failed to add admin");
1815
1816 let request = InviteRequest::new(
1818 "alice-bob-carol-dave",
1819 EntityType::Organisation,
1820 &org.id,
1821 "member",
1822 );
1823
1824 let invite = invite_service
1825 .create_invite(owner_id, request)
1826 .await
1827 .expect("Failed to create invite");
1828
1829 invite_service
1831 .revoke_invite(admin_id, &invite.id)
1832 .await
1833 .expect("Admin should be able to revoke");
1834
1835 let revoked_invite = invite_service
1836 .get_invite(&invite.id)
1837 .await
1838 .expect("Failed to get invite");
1839
1840 assert_eq!(revoked_invite.status, InviteStatus::Revoked);
1841 }
1842
1843 #[tokio::test]
1844 async fn test_list_pending_invites() {
1845 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1846
1847 let creator_id = "owner-abc-def-ghi";
1849 let recipient_id = "alice-bob-carol-dave";
1850 let org = entity_service
1851 .create_entity(
1852 "Test Org".to_string(),
1853 EntityType::Organisation,
1854 None,
1855 creator_id.to_string(),
1856 vec![],
1857 )
1858 .await
1859 .expect("Failed to create org");
1860
1861 entity_service
1862 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1863 .await
1864 .expect("Failed to set role");
1865
1866 for i in 0..3 {
1868 let new_org = entity_service
1869 .create_entity(
1870 format!("Org {}", i),
1871 EntityType::Organisation,
1872 None,
1873 creator_id.to_string(),
1874 vec![],
1875 )
1876 .await
1877 .expect("Failed to create org");
1878
1879 entity_service
1880 .set_member_role(EntityType::Organisation, &new_org.id, creator_id, "owner")
1881 .await
1882 .expect("Failed to set role");
1883
1884 let request = InviteRequest::new(
1885 recipient_id,
1886 EntityType::Organisation,
1887 &new_org.id,
1888 "member",
1889 );
1890
1891 invite_service
1892 .create_invite(creator_id, request)
1893 .await
1894 .expect("Failed to create invite");
1895 }
1896
1897 let pending = invite_service
1899 .list_pending_invites(recipient_id)
1900 .await
1901 .expect("Failed to list invites");
1902
1903 assert_eq!(pending.len(), 3);
1904 for invite in pending {
1905 assert_eq!(invite.status, InviteStatus::Pending);
1906 assert_eq!(invite.recipient_id, recipient_id);
1907 }
1908 }
1909
1910 #[tokio::test]
1911 async fn test_list_entity_invites() {
1912 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1913
1914 let creator_id = "owner-abc-def-ghi";
1916 let org = entity_service
1917 .create_entity(
1918 "Test Org".to_string(),
1919 EntityType::Organisation,
1920 None,
1921 creator_id.to_string(),
1922 vec![],
1923 )
1924 .await
1925 .expect("Failed to create org");
1926
1927 entity_service
1928 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1929 .await
1930 .expect("Failed to set role");
1931
1932 let recipients = [
1934 "alice-bob-carol-dave",
1935 "bob-xyz-uvw-rst",
1936 "carol-mno-pqr-stu",
1937 ];
1938 for recipient_id in recipients {
1939 let request =
1940 InviteRequest::new(recipient_id, EntityType::Organisation, &org.id, "member");
1941
1942 invite_service
1943 .create_invite(creator_id, request)
1944 .await
1945 .expect("Failed to create invite");
1946 }
1947
1948 let entity_invites = invite_service
1950 .list_entity_invites(creator_id, EntityType::Organisation, &org.id)
1951 .await
1952 .expect("Failed to list entity invites");
1953
1954 assert_eq!(entity_invites.len(), 3);
1955 for invite in entity_invites {
1956 assert_eq!(invite.entity_id, org.id);
1957 }
1958 }
1959
1960 #[tokio::test]
1961 async fn test_already_member_blocked() {
1962 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
1963
1964 let creator_id = "owner-abc-def-ghi";
1966 let existing_member = "existing-mem-ber-xyz";
1967 let org = entity_service
1968 .create_entity(
1969 "Test Org".to_string(),
1970 EntityType::Organisation,
1971 None,
1972 creator_id.to_string(),
1973 vec![],
1974 )
1975 .await
1976 .expect("Failed to create org");
1977
1978 entity_service
1979 .set_member_role(EntityType::Organisation, &org.id, creator_id, "owner")
1980 .await
1981 .expect("Failed to set role");
1982
1983 entity_service
1985 .add_member(EntityType::Organisation, &org.id, existing_member, "member")
1986 .await
1987 .expect("Failed to add member");
1988
1989 let request =
1991 InviteRequest::new(existing_member, EntityType::Organisation, &org.id, "member");
1992
1993 let result = invite_service.create_invite(creator_id, request).await;
1994
1995 assert!(matches!(
1996 result,
1997 Err(InviteServiceError::AlreadyMember { .. })
1998 ));
1999 }
2000
2001 #[tokio::test]
2002 async fn test_full_invite_lifecycle() {
2003 let (invite_service, entity_service, _temp) = create_test_invite_service().await;
2004
2005 let owner_id = "owner-abc-def-ghi";
2007 let recipient_id = "alice-bob-carol-dave";
2008
2009 let org = entity_service
2010 .create_entity(
2011 "Test Org".to_string(),
2012 EntityType::Organisation,
2013 Some("A collaborative org".to_string()),
2014 owner_id.to_string(),
2015 vec![],
2016 )
2017 .await
2018 .expect("Failed to create org");
2019
2020 entity_service
2021 .set_member_role(EntityType::Organisation, &org.id, owner_id, "owner")
2022 .await
2023 .expect("Failed to set role");
2024
2025 let request = InviteRequest::new(recipient_id, EntityType::Organisation, &org.id, "member")
2027 .with_message("Welcome!")
2028 .with_expiration(24);
2029
2030 let invite = invite_service
2031 .create_invite(owner_id, request)
2032 .await
2033 .expect("Failed to create invite");
2034
2035 assert_eq!(invite.status, InviteStatus::Pending);
2036 assert!(invite.is_valid());
2037
2038 let pending = invite_service
2040 .list_pending_invites(recipient_id)
2041 .await
2042 .expect("Failed to list");
2043
2044 assert_eq!(pending.len(), 1);
2045 assert_eq!(pending[0].id, invite.id);
2046
2047 invite_service
2049 .accept_invite(recipient_id, &invite.id)
2050 .await
2051 .expect("Failed to accept");
2052
2053 let accepted = invite_service
2055 .get_invite(&invite.id)
2056 .await
2057 .expect("Failed to get");
2058
2059 assert_eq!(accepted.status, InviteStatus::Accepted);
2060 assert!(!accepted.is_valid()); let role = entity_service
2064 .get_member_role(EntityType::Organisation, &org.id, recipient_id)
2065 .await
2066 .expect("Failed to get role");
2067
2068 assert_eq!(role, "member");
2069
2070 let pending_after = invite_service
2072 .list_pending_invites(recipient_id)
2073 .await
2074 .expect("Failed to list");
2075
2076 assert!(pending_after.is_empty());
2077 }
2078}
2079
2080#[cfg(test)]
2081mod proptests {
2082 use super::*;
2083 use proptest::prelude::*;
2084
2085 fn role_strategy() -> impl Strategy<Value = String> {
2087 prop_oneof![
2088 Just("owner".to_string()),
2089 Just("admin".to_string()),
2090 Just("member".to_string()),
2091 Just("viewer".to_string()),
2092 Just("guest".to_string()),
2093 ]
2094 }
2095
2096 fn four_word_identity() -> impl Strategy<Value = String> {
2098 proptest::collection::vec("[a-z]{3,8}", 4).prop_map(|words| words.join("-"))
2099 }
2100
2101 proptest! {
2102 #[test]
2104 fn prop_role_rank_monotonic(
2105 role1 in role_strategy(),
2106 role2 in role_strategy(),
2107 ) {
2108 let rank1 = role_rank(&role1);
2109 let rank2 = role_rank(&role2);
2110
2111 prop_assert_eq!(can_grant_role(&role1, &role2), rank1 >= rank2);
2113 }
2114
2115 #[test]
2117 fn prop_can_grant_role_reflexive(role in role_strategy()) {
2118 prop_assert!(can_grant_role(&role, &role));
2119 }
2120
2121 #[test]
2123 fn prop_can_grant_role_transitive(
2124 role_a in role_strategy(),
2125 role_b in role_strategy(),
2126 role_c in role_strategy(),
2127 ) {
2128 if can_grant_role(&role_a, &role_b) && can_grant_role(&role_b, &role_c) {
2130 prop_assert!(can_grant_role(&role_a, &role_c));
2131 }
2132 }
2133
2134 #[test]
2136 fn prop_valid_four_words_accepted(identity in four_word_identity()) {
2137 prop_assert!(validate_four_words_format(&identity));
2138 }
2139
2140 #[test]
2142 fn prop_wrong_word_count_rejected(
2143 word_count in (1usize..10).prop_filter("not 4", |&c| c != 4),
2144 ) {
2145 let words: Vec<String> = (0..word_count).map(|i| format!("word{}", i)).collect();
2146 let identity = words.join("-");
2147 prop_assert!(!validate_four_words_format(&identity));
2148 }
2149
2150 #[test]
2152 fn prop_unknown_role_rank_zero(
2153 unknown in "[a-z]{5,10}".prop_filter("not standard", |s| {
2154 !["owner", "admin", "member", "viewer", "guest"].contains(&s.as_str())
2155 }),
2156 ) {
2157 prop_assert_eq!(role_rank(&unknown), 0);
2158 }
2159
2160 #[test]
2162 fn prop_invite_request_preserves_required_fields(
2163 recipient in four_word_identity(),
2164 entity_id in "[a-z0-9-]{8,16}",
2165 role in role_strategy(),
2166 message in proptest::option::of("[a-zA-Z0-9 ]{0,50}"),
2167 hours in proptest::option::of(0u32..1000),
2168 ) {
2169 let mut request = InviteRequest::new(
2170 recipient.clone(),
2171 EntityType::Organisation,
2172 entity_id.clone(),
2173 role.clone(),
2174 );
2175
2176 if let Some(msg) = &message {
2177 request = request.with_message(msg.clone());
2178 }
2179 if let Some(h) = hours {
2180 request = request.with_expiration(h);
2181 }
2182
2183 prop_assert_eq!(request.recipient_id, recipient);
2185 prop_assert_eq!(request.entity_id, entity_id);
2186 prop_assert_eq!(request.role, role);
2187 prop_assert_eq!(request.entity_type, EntityType::Organisation);
2188 }
2189
2190 #[test]
2192 fn prop_builder_order_independence(
2193 recipient in four_word_identity(),
2194 entity_id in "[a-z0-9-]{8,16}",
2195 role in role_strategy(),
2196 message in "[a-zA-Z0-9 ]{1,30}",
2197 hours in 1u32..500,
2198 ) {
2199 let req1 = InviteRequest::new(
2201 recipient.clone(),
2202 EntityType::Group,
2203 entity_id.clone(),
2204 role.clone(),
2205 )
2206 .with_message(message.clone())
2207 .with_expiration(hours);
2208
2209 let req2 = InviteRequest::new(
2211 recipient,
2212 EntityType::Group,
2213 entity_id,
2214 role,
2215 )
2216 .with_expiration(hours)
2217 .with_message(message);
2218
2219 prop_assert_eq!(req1.recipient_id, req2.recipient_id);
2220 prop_assert_eq!(req1.entity_id, req2.entity_id);
2221 prop_assert_eq!(req1.role, req2.role);
2222 prop_assert_eq!(req1.message, req2.message);
2223 prop_assert_eq!(req1.expires_in_hours, req2.expires_in_hours);
2224 }
2225 }
2226}
2227
2228#[cfg(test)]
2236mod crdt_proptests {
2237 use super::*;
2238 use crate::invite::{Invite, InviteActionError, InviteStatus};
2239 use chrono::{Duration, Utc};
2240 use proptest::prelude::*;
2241
2242 fn four_word_identity() -> impl Strategy<Value = String> {
2244 proptest::collection::vec("[a-z]{3,8}", 4).prop_map(|words| words.join("-"))
2245 }
2246
2247 fn role_strategy() -> impl Strategy<Value = String> {
2249 prop_oneof![
2250 Just("owner".to_string()),
2251 Just("admin".to_string()),
2252 Just("member".to_string()),
2253 Just("viewer".to_string()),
2254 Just("guest".to_string()),
2255 ]
2256 }
2257
2258 fn entity_type_strategy() -> impl Strategy<Value = EntityType> {
2260 prop_oneof![
2261 Just(EntityType::Organisation),
2262 Just(EntityType::Group),
2263 Just(EntityType::Channel),
2264 Just(EntityType::Project),
2265 ]
2266 }
2267
2268 fn terminal_status_strategy() -> impl Strategy<Value = InviteStatus> {
2270 prop_oneof![
2271 Just(InviteStatus::Accepted),
2272 Just(InviteStatus::Rejected),
2273 Just(InviteStatus::Expired),
2274 Just(InviteStatus::Revoked),
2275 ]
2276 }
2277
2278 fn invite_strategy() -> impl Strategy<Value = Invite> {
2280 (
2281 four_word_identity(),
2282 four_word_identity(),
2283 "[a-z0-9-]{8,16}",
2284 entity_type_strategy(),
2285 role_strategy(),
2286 proptest::option::of("[a-zA-Z0-9 ]{1,50}"),
2287 proptest::option::of(1u32..1000),
2288 )
2289 .prop_map(
2290 |(creator, recipient, entity_id, entity_type, role, message, expires)| {
2291 Invite::new(
2292 creator,
2293 recipient,
2294 entity_id,
2295 entity_type,
2296 role,
2297 message,
2298 expires,
2299 )
2300 },
2301 )
2302 }
2303
2304 proptest! {
2305 #[test]
2310 fn prop_terminal_status_is_permanent(
2311 invite in invite_strategy(),
2312 terminal_status in terminal_status_strategy(),
2313 actor in four_word_identity(),
2314 ) {
2315 let mut invite = invite;
2316 let recipient = invite.recipient_id.clone();
2317 invite.status = terminal_status;
2319 invite.resolved_at = Some(Utc::now().timestamp_millis());
2320
2321 let accept_result = invite.accept(&recipient);
2323 prop_assert!(accept_result.is_err());
2324 if let Err(InviteActionError::AlreadyResolved(s)) = accept_result {
2325 prop_assert_eq!(s, terminal_status);
2326 } else {
2327 prop_assert!(false, "Expected AlreadyResolved error");
2328 }
2329 prop_assert_eq!(invite.status, terminal_status);
2330
2331 let reject_result = invite.reject(&recipient);
2333 prop_assert!(reject_result.is_err());
2334 prop_assert_eq!(invite.status, terminal_status);
2335
2336 let revoke_result = invite.revoke(&actor);
2338 prop_assert!(revoke_result.is_err());
2339 prop_assert_eq!(invite.status, terminal_status);
2340
2341 let expire_result = invite.mark_expired();
2343 prop_assert!(expire_result.is_err());
2344 prop_assert_eq!(invite.status, terminal_status);
2345 }
2346
2347 #[test]
2351 fn prop_accept_idempotent_after_success(
2352 invite in invite_strategy(),
2353 ) {
2354 let mut invite = invite;
2355 let recipient = invite.recipient_id.clone();
2356
2357 let result1 = invite.accept(&recipient);
2359 prop_assert!(result1.is_ok());
2360 prop_assert_eq!(invite.status, InviteStatus::Accepted);
2361
2362 let resolved_at1 = invite.resolved_at;
2363
2364 let result2 = invite.accept(&recipient);
2366 prop_assert!(matches!(result2, Err(InviteActionError::AlreadyResolved(InviteStatus::Accepted))));
2367
2368 prop_assert_eq!(invite.status, InviteStatus::Accepted);
2370 prop_assert_eq!(invite.resolved_at, resolved_at1);
2371 }
2372
2373 #[test]
2375 fn prop_reject_idempotent_after_success(
2376 invite in invite_strategy(),
2377 ) {
2378 let mut invite = invite;
2379 let recipient = invite.recipient_id.clone();
2380
2381 let result1 = invite.reject(&recipient);
2383 prop_assert!(result1.is_ok());
2384 prop_assert_eq!(invite.status, InviteStatus::Rejected);
2385
2386 let resolved_at1 = invite.resolved_at;
2387
2388 let result2 = invite.reject(&recipient);
2390 prop_assert!(matches!(result2, Err(InviteActionError::AlreadyResolved(InviteStatus::Rejected))));
2391
2392 prop_assert_eq!(invite.status, InviteStatus::Rejected);
2394 prop_assert_eq!(invite.resolved_at, resolved_at1);
2395 }
2396
2397 #[test]
2399 fn prop_revoke_idempotent_after_success(
2400 invite in invite_strategy(),
2401 revoker in four_word_identity(),
2402 ) {
2403 let mut invite = invite;
2404
2405 let result1 = invite.revoke(&revoker);
2407 prop_assert!(result1.is_ok());
2408 prop_assert_eq!(invite.status, InviteStatus::Revoked);
2409
2410 let resolved_at1 = invite.resolved_at;
2411
2412 let result2 = invite.revoke(&revoker);
2414 prop_assert!(matches!(result2, Err(InviteActionError::AlreadyResolved(InviteStatus::Revoked))));
2415
2416 prop_assert_eq!(invite.status, InviteStatus::Revoked);
2418 prop_assert_eq!(invite.resolved_at, resolved_at1);
2419 }
2420
2421 #[test]
2425 fn prop_expiration_deterministic(
2426 invite in invite_strategy(),
2427 hours_offset in -100i64..100,
2428 ) {
2429 let check_time = Utc::now() + Duration::hours(hours_offset);
2430
2431 let expired1 = invite.is_expired_at(check_time);
2433 let expired2 = invite.is_expired_at(check_time);
2434 let expired3 = invite.is_expired_at(check_time);
2435
2436 prop_assert_eq!(expired1, expired2);
2437 prop_assert_eq!(expired2, expired3);
2438 }
2439
2440 #[test]
2442 fn prop_expiration_monotonic(
2443 invite in invite_strategy(),
2444 ) {
2445 if let Some(expires_at) = invite.expires_at {
2446 let after_expiry = chrono::DateTime::from_timestamp_millis(expires_at + 1000)
2448 .unwrap_or_else(Utc::now);
2449 let later_still = after_expiry + Duration::hours(1);
2450
2451 prop_assert!(invite.is_expired_at(after_expiry));
2453 prop_assert!(invite.is_expired_at(later_still));
2454 }
2455 }
2456
2457 #[test]
2459 fn prop_is_valid_consistent(
2460 invite in invite_strategy(),
2461 hours_offset in -100i64..100,
2462 ) {
2463 let check_time = Utc::now() + Duration::hours(hours_offset);
2464
2465 let is_valid = invite.is_valid_at(check_time);
2466 let is_pending = invite.is_pending();
2467 let is_expired = invite.is_expired_at(check_time);
2468
2469 prop_assert_eq!(is_valid, is_pending && !is_expired);
2471 }
2472
2473 #[test]
2475 fn prop_only_recipient_can_accept_or_reject(
2476 invite in invite_strategy(),
2477 wrong_actor in four_word_identity(),
2478 ) {
2479 prop_assume!(wrong_actor != invite.recipient_id);
2481
2482 let mut invite_for_accept = invite.clone();
2483 let mut invite_for_reject = invite;
2484 let expected_recipient_accept = invite_for_accept.recipient_id.clone();
2485 let expected_recipient_reject = invite_for_reject.recipient_id.clone();
2486
2487 let accept_result = invite_for_accept.accept(&wrong_actor);
2489 prop_assert!(accept_result.is_err());
2490 if let Err(InviteActionError::NotRecipient { expected, actual }) = accept_result {
2491 prop_assert_eq!(expected, expected_recipient_accept);
2492 prop_assert_eq!(actual, wrong_actor.clone());
2493 } else {
2494 prop_assert!(false, "Expected NotRecipient error");
2495 }
2496 prop_assert!(invite_for_accept.is_pending()); let reject_result = invite_for_reject.reject(&wrong_actor);
2500 prop_assert!(reject_result.is_err());
2501 if let Err(InviteActionError::NotRecipient { expected, actual }) = reject_result {
2502 prop_assert_eq!(expected, expected_recipient_reject);
2503 prop_assert_eq!(actual, wrong_actor);
2504 } else {
2505 prop_assert!(false, "Expected NotRecipient error");
2506 }
2507 prop_assert!(invite_for_reject.is_pending()); }
2509
2510 #[test]
2515 fn prop_revoke_accepts_any_actor(
2516 invite in invite_strategy(),
2517 any_actor in four_word_identity(),
2518 ) {
2519 let mut invite = invite;
2520
2521 let result = invite.revoke(&any_actor);
2523 prop_assert!(result.is_ok());
2524 prop_assert_eq!(invite.status, InviteStatus::Revoked);
2525 prop_assert_eq!(invite.resolved_by, Some(any_actor));
2526 }
2527
2528 #[test]
2532 fn prop_serialization_roundtrip(
2533 invite in invite_strategy(),
2534 ) {
2535 let json = serde_json::to_string(&invite)
2537 .expect("serialization should succeed");
2538
2539 let restored: Invite = serde_json::from_str(&json)
2541 .expect("deserialization should succeed");
2542
2543 prop_assert_eq!(invite.id, restored.id);
2545 prop_assert_eq!(invite.creator_id, restored.creator_id);
2546 prop_assert_eq!(invite.recipient_id, restored.recipient_id);
2547 prop_assert_eq!(invite.entity_id, restored.entity_id);
2548 prop_assert_eq!(invite.entity_type, restored.entity_type);
2549 prop_assert_eq!(invite.role, restored.role);
2550 prop_assert_eq!(invite.message, restored.message);
2551 prop_assert_eq!(invite.status, restored.status);
2552 prop_assert_eq!(invite.created_at, restored.created_at);
2553 prop_assert_eq!(invite.expires_at, restored.expires_at);
2554 prop_assert_eq!(invite.resolved_at, restored.resolved_at);
2555 prop_assert_eq!(invite.resolved_by, restored.resolved_by);
2556 }
2557
2558 #[test]
2560 fn prop_invite_status_serialization(
2561 status in prop_oneof![
2562 Just(InviteStatus::Pending),
2563 Just(InviteStatus::Accepted),
2564 Just(InviteStatus::Rejected),
2565 Just(InviteStatus::Expired),
2566 Just(InviteStatus::Revoked),
2567 ],
2568 ) {
2569 let json = serde_json::to_string(&status)
2571 .expect("status serialization should succeed");
2572
2573 let restored: InviteStatus = serde_json::from_str(&json)
2575 .expect("status deserialization should succeed");
2576
2577 prop_assert_eq!(status, restored);
2578
2579 let display = status.to_string();
2581 let parsed: InviteStatus = display.parse()
2582 .expect("status parse should succeed");
2583 prop_assert_eq!(status, parsed);
2584 }
2585
2586 #[test]
2588 fn prop_resolution_records_metadata(
2589 invite in invite_strategy(),
2590 actor in four_word_identity(),
2591 ) {
2592 let mut invite_accept = invite.clone();
2594 let recipient = invite_accept.recipient_id.clone();
2595 if invite_accept.accept(&recipient).is_ok() {
2596 prop_assert!(invite_accept.resolved_at.is_some());
2597 prop_assert_eq!(invite_accept.resolved_by, Some(recipient.clone()));
2598 }
2599
2600 let mut invite_reject = invite.clone();
2602 let recipient = invite_reject.recipient_id.clone();
2603 if invite_reject.reject(&recipient).is_ok() {
2604 prop_assert!(invite_reject.resolved_at.is_some());
2605 prop_assert_eq!(invite_reject.resolved_by, Some(recipient.clone()));
2606 }
2607
2608 let mut invite_revoke = invite;
2610 if invite_revoke.revoke(&actor).is_ok() {
2611 prop_assert!(invite_revoke.resolved_at.is_some());
2612 prop_assert_eq!(invite_revoke.resolved_by, Some(actor));
2613 }
2614 }
2615
2616 #[test]
2618 fn prop_new_invite_pending(
2619 creator in four_word_identity(),
2620 recipient in four_word_identity(),
2621 entity_id in "[a-z0-9-]{8,16}",
2622 entity_type in entity_type_strategy(),
2623 role in role_strategy(),
2624 ) {
2625 let invite = Invite::new(
2626 creator,
2627 recipient,
2628 entity_id,
2629 entity_type,
2630 role,
2631 None,
2632 None,
2633 );
2634
2635 prop_assert!(invite.is_pending());
2636 prop_assert!(!invite.is_resolved());
2637 prop_assert!(invite.resolved_at.is_none());
2638 prop_assert!(invite.resolved_by.is_none());
2639 }
2640
2641 #[test]
2643 fn prop_no_expiration_never_expires(
2644 creator in four_word_identity(),
2645 recipient in four_word_identity(),
2646 entity_id in "[a-z0-9-]{8,16}",
2647 hours_in_future in 0i64..100000,
2648 ) {
2649 let invite = Invite::new(
2650 creator,
2651 recipient,
2652 entity_id,
2653 EntityType::Organisation,
2654 "member".to_string(),
2655 None,
2656 None, );
2658
2659 let future_time = Utc::now() + Duration::hours(hours_in_future);
2661 prop_assert!(!invite.is_expired_at(future_time));
2662 }
2663
2664 #[test]
2669 fn prop_first_action_wins(
2670 invite in invite_strategy(),
2671 action_type in 0u8..3,
2672 revoker in four_word_identity(),
2673 ) {
2674 let mut invite = invite;
2675 let recipient = invite.recipient_id.clone();
2676
2677 let first_status = match action_type {
2679 0 => {
2680 let _ = invite.accept(&recipient);
2681 InviteStatus::Accepted
2682 }
2683 1 => {
2684 let _ = invite.reject(&recipient);
2685 InviteStatus::Rejected
2686 }
2687 _ => {
2688 let _ = invite.revoke(&revoker);
2689 InviteStatus::Revoked
2690 }
2691 };
2692
2693 prop_assert_eq!(invite.status, first_status);
2695 prop_assert!(invite.is_resolved());
2696
2697 prop_assert!(invite.accept(&recipient).is_err());
2699 prop_assert!(invite.reject(&recipient).is_err());
2700 prop_assert!(invite.revoke(&revoker).is_err());
2701 }
2702 }
2703}
2704
2705#[cfg(test)]
2710mod crdt_storage_tests {
2711 use super::*;
2712 use crate::crdt_manager::CrdtManager;
2713 use crate::entity_service::EntityService;
2714 use crate::invite::{Invite, InviteStatus};
2715 use crate::legacy_crdt::EntityType;
2716 use std::sync::Arc;
2717 use tempfile::TempDir;
2718
2719 async fn create_test_crdt_manager() -> (Arc<CrdtManager>, TempDir) {
2721 let temp_dir = TempDir::new().expect("failed to create temp dir");
2722 let crdt_manager = CrdtManager::new(temp_dir.path())
2723 .await
2724 .expect("failed to create CrdtManager");
2725 (Arc::new(crdt_manager), temp_dir)
2726 }
2727
2728 fn create_test_entity_service(crdt_manager: Arc<CrdtManager>) -> Arc<EntityService> {
2730 Arc::new(EntityService::new(crdt_manager))
2731 }
2732
2733 async fn create_test_invite_service() -> (InviteService, TempDir) {
2735 let (crdt_manager, temp_dir) = create_test_crdt_manager().await;
2736 let entity_service = create_test_entity_service(crdt_manager.clone());
2737 let service = InviteService::new(crdt_manager, entity_service);
2738 (service, temp_dir)
2739 }
2740
2741 fn create_test_invite(recipient: &str, entity_type: EntityType, entity_id: &str) -> Invite {
2743 Invite::new(
2744 "alice-test-four-word".to_string(),
2745 recipient.to_string(),
2746 entity_id.to_string(),
2747 entity_type,
2748 "member".to_string(),
2749 Some("Welcome!".to_string()),
2750 Some(24), )
2752 }
2753
2754 #[tokio::test]
2759 async fn test_store_and_load_invite_from_crdt() {
2760 let (service, _temp_dir) = create_test_invite_service().await;
2761
2762 let invite = create_test_invite("bob-test-four-word", EntityType::Organisation, "org-123");
2763 let invite_id = invite.id.clone();
2764
2765 service
2767 .store_invite_in_crdt(&invite)
2768 .await
2769 .expect("failed to store invite");
2770
2771 let loaded = service
2773 .load_invite_from_crdt(&invite_id)
2774 .await
2775 .expect("failed to load invite");
2776
2777 assert_eq!(loaded.id, invite.id);
2779 assert_eq!(loaded.creator_id, invite.creator_id);
2780 assert_eq!(loaded.recipient_id, invite.recipient_id);
2781 assert_eq!(loaded.entity_id, invite.entity_id);
2782 assert_eq!(loaded.entity_type, invite.entity_type);
2783 assert_eq!(loaded.role, invite.role);
2784 assert_eq!(loaded.message, invite.message);
2785 assert_eq!(loaded.status, invite.status);
2786 assert_eq!(loaded.created_at, invite.created_at);
2787 assert_eq!(loaded.expires_at, invite.expires_at);
2788 }
2789
2790 #[tokio::test]
2791 async fn test_load_nonexistent_invite_returns_not_found() {
2792 let (service, _temp_dir) = create_test_invite_service().await;
2793
2794 let result = service.load_invite_from_crdt("nonexistent-invite-id").await;
2795
2796 assert!(matches!(result, Err(InviteServiceError::InviteNotFound(_))));
2797 }
2798
2799 #[tokio::test]
2800 async fn test_store_invite_updates_existing() {
2801 let (service, _temp_dir) = create_test_invite_service().await;
2802
2803 let mut invite =
2804 create_test_invite("bob-test-four-word", EntityType::Organisation, "org-123");
2805 let invite_id = invite.id.clone();
2806
2807 service
2809 .store_invite_in_crdt(&invite)
2810 .await
2811 .expect("failed to store invite");
2812
2813 let recipient = invite.recipient_id.clone();
2815 invite.accept(&recipient).expect("failed to accept");
2816
2817 service
2819 .store_invite_in_crdt(&invite)
2820 .await
2821 .expect("failed to store updated invite");
2822
2823 let loaded = service
2825 .load_invite_from_crdt(&invite_id)
2826 .await
2827 .expect("failed to load invite");
2828
2829 assert_eq!(loaded.status, InviteStatus::Accepted);
2830 assert!(loaded.resolved_at.is_some());
2831 assert_eq!(loaded.resolved_by, Some(recipient));
2832 }
2833
2834 #[tokio::test]
2835 async fn test_load_invites_for_entity_from_crdt() {
2836 let (service, _temp_dir) = create_test_invite_service().await;
2837
2838 let entity_type = EntityType::Organisation;
2839 let entity_id = "org-456";
2840
2841 let invite1 = create_test_invite("bob-one-two-three", entity_type, entity_id);
2843 let invite2 = create_test_invite("carol-four-five-six", entity_type, entity_id);
2844 let invite3 = create_test_invite("dave-seven-eight-nine", EntityType::Group, "group-789");
2845
2846 service
2847 .store_invite_in_crdt(&invite1)
2848 .await
2849 .expect("store 1");
2850 service
2851 .store_invite_in_crdt(&invite2)
2852 .await
2853 .expect("store 2");
2854 service
2855 .store_invite_in_crdt(&invite3)
2856 .await
2857 .expect("store 3");
2858
2859 let invites = service
2861 .load_invites_for_entity_from_crdt(entity_type, entity_id)
2862 .await
2863 .expect("failed to load entity invites");
2864
2865 assert_eq!(invites.len(), 2);
2867
2868 let invite_ids: Vec<&str> = invites.iter().map(|i| i.id.as_str()).collect();
2869 assert!(invite_ids.contains(&invite1.id.as_str()));
2870 assert!(invite_ids.contains(&invite2.id.as_str()));
2871 assert!(!invite_ids.contains(&invite3.id.as_str()));
2872 }
2873
2874 #[tokio::test]
2875 async fn test_load_invites_for_empty_entity_returns_empty_vec() {
2876 let (service, _temp_dir) = create_test_invite_service().await;
2877
2878 let invites = service
2879 .load_invites_for_entity_from_crdt(EntityType::Organisation, "nonexistent-entity")
2880 .await
2881 .expect("should return empty vec");
2882
2883 assert!(invites.is_empty());
2884 }
2885
2886 #[tokio::test]
2887 async fn test_crdt_storage_survives_service_restart() {
2888 let temp_dir = TempDir::new().expect("failed to create temp dir");
2889
2890 let invite_id: String;
2891 let original_recipient: String;
2892
2893 {
2895 let crdt_manager = Arc::new(
2896 CrdtManager::new(temp_dir.path())
2897 .await
2898 .expect("failed to create CrdtManager"),
2899 );
2900 let entity_service = create_test_entity_service(crdt_manager.clone());
2901 let service = InviteService::new(crdt_manager, entity_service);
2902
2903 let invite =
2904 create_test_invite("bob-test-four-word", EntityType::Organisation, "org-123");
2905 invite_id = invite.id.clone();
2906 original_recipient = invite.recipient_id.clone();
2907
2908 service
2909 .store_invite_in_crdt(&invite)
2910 .await
2911 .expect("failed to store invite");
2912 }
2913
2914 {
2916 let crdt_manager = Arc::new(
2917 CrdtManager::new(temp_dir.path())
2918 .await
2919 .expect("failed to create CrdtManager"),
2920 );
2921 let entity_service = create_test_entity_service(crdt_manager.clone());
2922 let service = InviteService::new(crdt_manager, entity_service);
2923
2924 let loaded = service
2925 .load_invite_from_crdt(&invite_id)
2926 .await
2927 .expect("failed to load invite after restart");
2928
2929 assert_eq!(loaded.id, invite_id);
2930 assert_eq!(loaded.recipient_id, original_recipient);
2931 assert_eq!(loaded.status, InviteStatus::Pending);
2932 }
2933 }
2934
2935 #[tokio::test]
2936 async fn test_multiple_entities_have_separate_crdt_docs() {
2937 let (service, _temp_dir) = create_test_invite_service().await;
2938
2939 let org_invite =
2941 create_test_invite("bob-org-four-word", EntityType::Organisation, "org-123");
2942 let group_invite =
2943 create_test_invite("carol-group-four-word", EntityType::Group, "group-456");
2944 let project_invite =
2945 create_test_invite("dave-project-four-word", EntityType::Project, "project-789");
2946
2947 service
2948 .store_invite_in_crdt(&org_invite)
2949 .await
2950 .expect("store org");
2951 service
2952 .store_invite_in_crdt(&group_invite)
2953 .await
2954 .expect("store group");
2955 service
2956 .store_invite_in_crdt(&project_invite)
2957 .await
2958 .expect("store project");
2959
2960 let org_invites = service
2962 .load_invites_for_entity_from_crdt(EntityType::Organisation, "org-123")
2963 .await
2964 .expect("org invites");
2965 assert_eq!(org_invites.len(), 1);
2966 assert_eq!(org_invites[0].id, org_invite.id);
2967
2968 let group_invites = service
2969 .load_invites_for_entity_from_crdt(EntityType::Group, "group-456")
2970 .await
2971 .expect("group invites");
2972 assert_eq!(group_invites.len(), 1);
2973 assert_eq!(group_invites[0].id, group_invite.id);
2974
2975 let project_invites = service
2976 .load_invites_for_entity_from_crdt(EntityType::Project, "project-789")
2977 .await
2978 .expect("project invites");
2979 assert_eq!(project_invites.len(), 1);
2980 assert_eq!(project_invites[0].id, project_invite.id);
2981 }
2982
2983 #[tokio::test]
2984 async fn test_store_invite_with_all_statuses() {
2985 let (service, _temp_dir) = create_test_invite_service().await;
2986
2987 let statuses = [
2989 InviteStatus::Pending,
2990 InviteStatus::Accepted,
2991 InviteStatus::Rejected,
2992 InviteStatus::Revoked,
2993 InviteStatus::Expired,
2994 ];
2995
2996 for (i, status) in statuses.iter().enumerate() {
2997 let mut invite = create_test_invite(
2998 &format!("bob-{}-four-word", i),
2999 EntityType::Organisation,
3000 &format!("org-status-{}", i),
3001 );
3002
3003 match status {
3005 InviteStatus::Pending => {}
3006 InviteStatus::Accepted => {
3007 let recipient = invite.recipient_id.clone();
3008 invite.accept(&recipient).expect("accept");
3009 }
3010 InviteStatus::Rejected => {
3011 let recipient = invite.recipient_id.clone();
3012 invite.reject(&recipient).expect("reject");
3013 }
3014 InviteStatus::Revoked => {
3015 invite.revoke("admin-a-b-c").expect("revoke");
3016 }
3017 InviteStatus::Expired => {
3018 invite.mark_expired().expect("mark expired");
3019 }
3020 }
3021
3022 let invite_id = invite.id.clone();
3023 service
3024 .store_invite_in_crdt(&invite)
3025 .await
3026 .unwrap_or_else(|_| panic!("store {} failed", status));
3027
3028 let loaded = service
3029 .load_invite_from_crdt(&invite_id)
3030 .await
3031 .unwrap_or_else(|_| panic!("load {} failed", status));
3032
3033 assert_eq!(loaded.status, *status, "status mismatch for {}", status);
3034 }
3035 }
3036
3037 #[tokio::test]
3038 async fn test_recipient_index_persisted_in_crdt() {
3039 let temp_dir = TempDir::new().expect("failed to create temp dir");
3040
3041 let recipient = "bob-index-test-word";
3042 let invite1_id: String;
3043 let invite2_id: String;
3044
3045 {
3047 let crdt_manager = Arc::new(
3048 CrdtManager::new(temp_dir.path())
3049 .await
3050 .expect("failed to create CrdtManager"),
3051 );
3052 let entity_service = create_test_entity_service(crdt_manager.clone());
3053 let service = InviteService::new(crdt_manager, entity_service);
3054
3055 let invite1 = create_test_invite(recipient, EntityType::Organisation, "org-1");
3056 let invite2 = create_test_invite(recipient, EntityType::Group, "group-1");
3057 invite1_id = invite1.id.clone();
3058 invite2_id = invite2.id.clone();
3059
3060 service
3061 .store_invite_in_crdt(&invite1)
3062 .await
3063 .expect("store 1");
3064 service
3065 .store_invite_in_crdt(&invite2)
3066 .await
3067 .expect("store 2");
3068
3069 service
3071 .store_recipient_index_in_crdt(recipient, &[invite1_id.clone(), invite2_id.clone()])
3072 .await
3073 .expect("store index");
3074 }
3075
3076 {
3078 let crdt_manager = Arc::new(
3079 CrdtManager::new(temp_dir.path())
3080 .await
3081 .expect("failed to create CrdtManager"),
3082 );
3083 let entity_service = create_test_entity_service(crdt_manager.clone());
3084 let service = InviteService::new(crdt_manager, entity_service);
3085
3086 let invite_ids = service
3087 .load_recipient_index_from_crdt(recipient)
3088 .await
3089 .expect("load index");
3090
3091 assert_eq!(invite_ids.len(), 2);
3092 assert!(invite_ids.contains(&invite1_id));
3093 assert!(invite_ids.contains(&invite2_id));
3094 }
3095 }
3096}