subcog/services/
group.rs

1//! Group management service.
2//!
3//! Provides business logic for group operations within an organization.
4//! Groups enable team collaboration through shared memory graphs.
5//!
6//! # Features
7//!
8//! - **Group Management**: Create, list, and delete groups
9//! - **Member Management**: Add/remove members with role-based permissions
10//! - **Invite System**: Token-based invites with expiration and usage limits
11//!
12//! # Permissions
13//!
14//! | Operation | Required Role |
15//! |-----------|--------------|
16//! | Create group | Org member |
17//! | Delete group | Group admin |
18//! | Add member | Group admin |
19//! | Remove member | Group admin (cannot remove last admin) |
20//! | Update role | Group admin (cannot demote last admin) |
21//! | Create invite | Group admin |
22//! | List members | Group member |
23//! | Join via invite | Anyone with valid token |
24//! | Leave group | Self (cannot leave if last admin) |
25//!
26//! # Example
27//!
28//! ```rust,ignore
29//! use subcog::services::GroupService;
30//! use subcog::storage::GroupStorageFactory;
31//!
32//! let backend = GroupStorageFactory::create_in_memory()?;
33//! let service = GroupService::new(backend);
34//!
35//! // Create a group
36//! let group = service.create_group("my-org", "engineering", "Engineering team", "alice@example.com")?;
37//!
38//! // Add a member
39//! service.add_member(&group.id, "bob@example.com", GroupRole::Write, "alice@example.com")?;
40//!
41//! // Create an invite
42//! let (invite, token) = service.create_invite(&group.id, GroupRole::Read, "alice@example.com", None, None)?;
43//! println!("Share this invite: {token}");
44//! ```
45
46use std::sync::Arc;
47
48use crate::models::group::{Group, GroupId, GroupInvite, GroupMember, GroupMembership, GroupRole};
49use crate::storage::group::GroupBackend;
50use crate::{Error, Result};
51
52/// Service for group management operations.
53///
54/// Encapsulates business logic for groups, members, and invites.
55/// Uses a [`GroupBackend`] for persistence.
56pub struct GroupService {
57    backend: Arc<dyn GroupBackend>,
58}
59
60impl GroupService {
61    /// Creates a new group service with the given backend.
62    #[must_use]
63    pub fn new(backend: Arc<dyn GroupBackend>) -> Self {
64        Self { backend }
65    }
66
67    /// Creates a new group service with a default `SQLite` backend.
68    ///
69    /// Uses the user's data directory for storage.
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if the backend cannot be initialized.
74    pub fn try_default() -> crate::Result<Self> {
75        use crate::services::PathManager;
76        use crate::storage::group::SqliteGroupBackend;
77
78        let user_dir = crate::storage::get_user_data_dir()?;
79        let paths = PathManager::for_user(&user_dir);
80        let db_path = paths.index_path().join("groups.db");
81        let backend = Arc::new(SqliteGroupBackend::new(&db_path)?);
82        Ok(Self::new(backend))
83    }
84
85    // =========================================================================
86    // Group Operations
87    // =========================================================================
88
89    /// Creates a new group in the organization.
90    ///
91    /// The creator is automatically added as an admin.
92    ///
93    /// # Arguments
94    ///
95    /// * `org_id` - Organization identifier
96    /// * `name` - Group name (must be unique within org)
97    /// * `description` - Optional description
98    /// * `creator_email` - Email of the group creator
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if:
103    /// - A group with the same name already exists
104    /// - Storage cannot be accessed
105    pub fn create_group(
106        &self,
107        org_id: &str,
108        name: &str,
109        description: &str,
110        creator_email: &str,
111    ) -> Result<Group> {
112        // Validate inputs
113        if name.is_empty() {
114            return Err(Error::InvalidInput(
115                "Group name cannot be empty".to_string(),
116            ));
117        }
118        if creator_email.is_empty() {
119            return Err(Error::InvalidInput(
120                "Creator email cannot be empty".to_string(),
121            ));
122        }
123
124        // Create the group
125        let group = self
126            .backend
127            .create_group(org_id, name, description, creator_email)?;
128
129        // Add creator as admin
130        self.backend
131            .add_member(&group.id, creator_email, GroupRole::Admin, creator_email)?;
132
133        tracing::info!(
134            org_id = %org_id,
135            group_id = %group.id.as_str(),
136            group_name = %name,
137            creator = %creator_email,
138            "Group created"
139        );
140
141        Ok(group)
142    }
143
144    /// Gets a group by ID.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if storage cannot be accessed.
149    pub fn get_group(&self, group_id: &GroupId) -> Result<Option<Group>> {
150        self.backend.get_group(group_id)
151    }
152
153    /// Gets a group by name within an organization.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if storage cannot be accessed.
158    pub fn get_group_by_name(&self, org_id: &str, name: &str) -> Result<Option<Group>> {
159        self.backend.get_group_by_name(org_id, name)
160    }
161
162    /// Lists all groups in an organization.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if storage cannot be accessed.
167    pub fn list_groups(&self, org_id: &str) -> Result<Vec<Group>> {
168        self.backend.list_groups(org_id)
169    }
170
171    /// Deletes a group and all its members and invites.
172    ///
173    /// Only admins can delete groups.
174    ///
175    /// # Arguments
176    ///
177    /// * `group_id` - The group to delete
178    /// * `requester_email` - Email of the user requesting deletion
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if:
183    /// - The requester is not an admin
184    /// - Storage cannot be accessed
185    pub fn delete_group(&self, group_id: &GroupId, requester_email: &str) -> Result<bool> {
186        // Check requester is admin
187        self.require_admin(group_id, requester_email)?;
188
189        let deleted = self.backend.delete_group(group_id)?;
190
191        if deleted {
192            tracing::info!(
193                group_id = %group_id.as_str(),
194                requester = %requester_email,
195                "Group deleted"
196            );
197        }
198
199        Ok(deleted)
200    }
201
202    // =========================================================================
203    // Member Operations
204    // =========================================================================
205
206    /// Adds a member to a group.
207    ///
208    /// If the member already exists, updates their role.
209    /// Only admins can add members.
210    ///
211    /// # Arguments
212    ///
213    /// * `group_id` - The group to add to
214    /// * `email` - Email of the new member
215    /// * `role` - Role to assign
216    /// * `requester_email` - Email of the user adding the member
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if:
221    /// - The requester is not an admin
222    /// - The group doesn't exist
223    /// - Storage cannot be accessed
224    pub fn add_member(
225        &self,
226        group_id: &GroupId,
227        email: &str,
228        role: GroupRole,
229        requester_email: &str,
230    ) -> Result<GroupMember> {
231        // Check requester is admin
232        self.require_admin(group_id, requester_email)?;
233
234        let member = self
235            .backend
236            .add_member(group_id, email, role, requester_email)?;
237
238        tracing::info!(
239            group_id = %group_id.as_str(),
240            member_email = %email,
241            role = %role.as_str(),
242            added_by = %requester_email,
243            "Member added to group"
244        );
245
246        Ok(member)
247    }
248
249    /// Gets a member's record in a group.
250    ///
251    /// # Errors
252    ///
253    /// Returns an error if storage cannot be accessed.
254    pub fn get_member(&self, group_id: &GroupId, email: &str) -> Result<Option<GroupMember>> {
255        self.backend.get_member(group_id, email)
256    }
257
258    /// Updates a member's role in a group.
259    ///
260    /// Only admins can update roles. Cannot demote the last admin.
261    ///
262    /// # Arguments
263    ///
264    /// * `group_id` - The group
265    /// * `email` - The member's email
266    /// * `new_role` - The new role to assign
267    /// * `requester_email` - Email of the user updating the role
268    ///
269    /// # Errors
270    ///
271    /// Returns an error if:
272    /// - The requester is not an admin
273    /// - Demoting the last admin
274    /// - Storage cannot be accessed
275    pub fn update_member_role(
276        &self,
277        group_id: &GroupId,
278        email: &str,
279        new_role: GroupRole,
280        requester_email: &str,
281    ) -> Result<bool> {
282        // Check requester is admin
283        self.require_admin(group_id, requester_email)?;
284
285        // Check if demoting an admin
286        if let Some(member) = self.backend.get_member(group_id, email)?
287            && member.role == GroupRole::Admin
288            && new_role != GroupRole::Admin
289        {
290            // Check if this is the last admin
291            let admin_count = self.backend.count_admins(group_id)?;
292            if admin_count <= 1 {
293                return Err(Error::InvalidInput(
294                    "Cannot demote the last admin. Promote another member first.".to_string(),
295                ));
296            }
297        }
298
299        let updated = self.backend.update_member_role(group_id, email, new_role)?;
300
301        if updated {
302            tracing::info!(
303                group_id = %group_id.as_str(),
304                member_email = %email,
305                new_role = %new_role.as_str(),
306                updated_by = %requester_email,
307                "Member role updated"
308            );
309        }
310
311        Ok(updated)
312    }
313
314    /// Removes a member from a group.
315    ///
316    /// Only admins can remove members. Cannot remove the last admin.
317    ///
318    /// # Arguments
319    ///
320    /// * `group_id` - The group
321    /// * `email` - The member's email
322    /// * `requester_email` - Email of the user removing the member
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if:
327    /// - The requester is not an admin
328    /// - Removing the last admin
329    /// - Storage cannot be accessed
330    pub fn remove_member(
331        &self,
332        group_id: &GroupId,
333        email: &str,
334        requester_email: &str,
335    ) -> Result<bool> {
336        // Check requester is admin
337        self.require_admin(group_id, requester_email)?;
338
339        // Check if removing an admin
340        if let Some(member) = self.backend.get_member(group_id, email)?
341            && member.role == GroupRole::Admin
342        {
343            // Check if this is the last admin
344            let admin_count = self.backend.count_admins(group_id)?;
345            if admin_count <= 1 {
346                return Err(Error::InvalidInput(
347                    "Cannot remove the last admin. Promote another member first.".to_string(),
348                ));
349            }
350        }
351
352        let removed = self.backend.remove_member(group_id, email)?;
353
354        if removed {
355            tracing::info!(
356                group_id = %group_id.as_str(),
357                member_email = %email,
358                removed_by = %requester_email,
359                "Member removed from group"
360            );
361        }
362
363        Ok(removed)
364    }
365
366    /// Lists all members of a group.
367    ///
368    /// # Errors
369    ///
370    /// Returns an error if storage cannot be accessed.
371    pub fn list_members(&self, group_id: &GroupId) -> Result<Vec<GroupMember>> {
372        self.backend.list_members(group_id)
373    }
374
375    /// Gets all groups a user is a member of.
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if storage cannot be accessed.
380    pub fn get_user_groups(&self, org_id: &str, email: &str) -> Result<Vec<GroupMembership>> {
381        self.backend.get_user_groups(org_id, email)
382    }
383
384    /// Allows a user to leave a group.
385    ///
386    /// Cannot leave if the user is the last admin.
387    ///
388    /// # Arguments
389    ///
390    /// * `group_id` - The group to leave
391    /// * `email` - Email of the user leaving
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if:
396    /// - The user is the last admin
397    /// - Storage cannot be accessed
398    pub fn leave_group(&self, group_id: &GroupId, email: &str) -> Result<bool> {
399        // Check if leaving as the last admin
400        if let Some(member) = self.backend.get_member(group_id, email)?
401            && member.role == GroupRole::Admin
402        {
403            let admin_count = self.backend.count_admins(group_id)?;
404            if admin_count <= 1 {
405                return Err(Error::InvalidInput(
406                    "Cannot leave as the last admin. Promote another member first.".to_string(),
407                ));
408            }
409        }
410
411        let left = self.backend.remove_member(group_id, email)?;
412
413        if left {
414            tracing::info!(
415                group_id = %group_id.as_str(),
416                member_email = %email,
417                "Member left group"
418            );
419        }
420
421        Ok(left)
422    }
423
424    // =========================================================================
425    // Invite Operations
426    // =========================================================================
427
428    /// Creates an invite for a group.
429    ///
430    /// Only admins can create invites.
431    ///
432    /// # Arguments
433    ///
434    /// * `group_id` - The group to invite to
435    /// * `role` - Role to assign when joined
436    /// * `creator_email` - Email of the admin creating the invite
437    /// * `expires_in_secs` - How long until expiration (default: 7 days)
438    /// * `max_uses` - Maximum number of uses (default: unlimited)
439    ///
440    /// # Returns
441    ///
442    /// A tuple of (invite, `plaintext_token`). The token should be shared
443    /// with invitees and never stored.
444    ///
445    /// # Errors
446    ///
447    /// Returns an error if:
448    /// - The creator is not an admin
449    /// - Storage cannot be accessed
450    pub fn create_invite(
451        &self,
452        group_id: &GroupId,
453        role: GroupRole,
454        creator_email: &str,
455        expires_in_secs: Option<u64>,
456        max_uses: Option<u32>,
457    ) -> Result<(GroupInvite, String)> {
458        // Check creator is admin
459        self.require_admin(group_id, creator_email)?;
460
461        let (invite, token) =
462            self.backend
463                .create_invite(group_id, role, creator_email, expires_in_secs, max_uses)?;
464
465        tracing::info!(
466            group_id = %group_id.as_str(),
467            invite_id = %invite.id,
468            role = %role.as_str(),
469            created_by = %creator_email,
470            expires_in_secs = ?expires_in_secs,
471            max_uses = ?max_uses,
472            "Group invite created"
473        );
474
475        Ok((invite, token))
476    }
477
478    /// Joins a group using an invite token.
479    ///
480    /// Validates the token and adds the user as a member with the invite's role.
481    ///
482    /// # Arguments
483    ///
484    /// * `token` - The plaintext invite token
485    /// * `email` - Email of the user joining
486    ///
487    /// # Returns
488    ///
489    /// The created member record.
490    ///
491    /// # Errors
492    ///
493    /// Returns an error if:
494    /// - The token is invalid or expired
495    /// - Storage cannot be accessed
496    pub fn join_via_invite(&self, token: &str, email: &str) -> Result<GroupMember> {
497        // Look up invite by token hash
498        let token_hash = GroupInvite::hash_token(token);
499        let invite = self
500            .backend
501            .get_invite_by_token_hash(&token_hash)?
502            .ok_or_else(|| Error::InvalidInput("Invalid invite token".to_string()))?;
503
504        // Check if invite is valid
505        if !invite.is_valid() {
506            return Err(Error::InvalidInput(
507                "Invite is expired or has reached its usage limit".to_string(),
508            ));
509        }
510
511        // Check if user is already a member
512        if let Some(existing) = self.backend.get_member(&invite.group_id, email)? {
513            return Err(Error::InvalidInput(format!(
514                "Already a member of this group with role '{}'",
515                existing.role.as_str()
516            )));
517        }
518
519        // Add member with invite's role
520        let member = self.backend.add_member(
521            &invite.group_id,
522            email,
523            invite.role,
524            &invite.created_by, // "added by" is the invite creator
525        )?;
526
527        // Increment invite usage
528        self.backend.increment_invite_uses(&invite.id)?;
529
530        tracing::info!(
531            group_id = %invite.group_id.as_str(),
532            invite_id = %invite.id,
533            member_email = %email,
534            role = %invite.role.as_str(),
535            "Member joined via invite"
536        );
537
538        Ok(member)
539    }
540
541    /// Lists all invites for a group.
542    ///
543    /// # Arguments
544    ///
545    /// * `group_id` - The group
546    /// * `include_expired` - Whether to include expired/revoked invites
547    ///
548    /// # Errors
549    ///
550    /// Returns an error if storage cannot be accessed.
551    pub fn list_invites(
552        &self,
553        group_id: &GroupId,
554        include_expired: bool,
555    ) -> Result<Vec<GroupInvite>> {
556        self.backend.list_invites(group_id, include_expired)
557    }
558
559    /// Revokes an invite.
560    ///
561    /// Only admins can revoke invites.
562    ///
563    /// # Arguments
564    ///
565    /// * `invite_id` - The invite to revoke
566    /// * `requester_email` - Email of the admin revoking
567    ///
568    /// # Errors
569    ///
570    /// Returns an error if:
571    /// - The requester is not an admin
572    /// - Storage cannot be accessed
573    pub fn revoke_invite(&self, invite_id: &str, requester_email: &str) -> Result<bool> {
574        // Get the invite to find the group
575        let invite = self
576            .backend
577            .get_invite(invite_id)?
578            .ok_or_else(|| Error::InvalidInput("Invite not found".to_string()))?;
579
580        // Check requester is admin
581        self.require_admin(&invite.group_id, requester_email)?;
582
583        let revoked = self.backend.revoke_invite(invite_id)?;
584
585        if revoked {
586            tracing::info!(
587                invite_id = %invite_id,
588                group_id = %invite.group_id.as_str(),
589                revoked_by = %requester_email,
590                "Group invite revoked"
591            );
592        }
593
594        Ok(revoked)
595    }
596
597    /// Cleans up expired invites.
598    ///
599    /// # Returns
600    ///
601    /// Number of invites deleted.
602    ///
603    /// # Errors
604    ///
605    /// Returns an error if storage cannot be accessed.
606    pub fn cleanup_expired_invites(&self) -> Result<u64> {
607        self.backend.cleanup_expired_invites()
608    }
609
610    // =========================================================================
611    // Permission Helpers
612    // =========================================================================
613
614    /// Checks if a user has admin role in a group.
615    fn require_admin(&self, group_id: &GroupId, email: &str) -> Result<()> {
616        let member = self.backend.get_member(group_id, email)?.ok_or_else(|| {
617            Error::Unauthorized(format!("User '{email}' is not a member of this group"))
618        })?;
619
620        if member.role != GroupRole::Admin {
621            return Err(Error::Unauthorized(
622                "Admin role required for this operation".to_string(),
623            ));
624        }
625
626        Ok(())
627    }
628
629    /// Checks if a user has at least the specified role in a group.
630    ///
631    /// Role hierarchy: Admin > Write > Read
632    ///
633    /// # Errors
634    ///
635    /// Returns an error if:
636    /// - The user is not a member of the group
637    /// - The user's role is insufficient
638    pub fn require_role(&self, group_id: &GroupId, email: &str, min_role: GroupRole) -> Result<()> {
639        let member = self.backend.get_member(group_id, email)?.ok_or_else(|| {
640            Error::Unauthorized(format!("User '{email}' is not a member of this group"))
641        })?;
642
643        // Check if member's role meets the minimum requirement
644        let has_permission = match min_role {
645            GroupRole::Admin => member.role.can_manage(),
646            GroupRole::Write => member.role.can_write(),
647            GroupRole::Read => member.role.can_read(),
648        };
649
650        if !has_permission {
651            return Err(Error::Unauthorized(format!(
652                "Role '{}' or higher required, but user has '{}'",
653                min_role.as_str(),
654                member.role.as_str()
655            )));
656        }
657
658        Ok(())
659    }
660
661    /// Checks if a user is a member of a group.
662    ///
663    /// # Errors
664    ///
665    /// Returns an error if storage cannot be accessed.
666    pub fn is_member(&self, group_id: &GroupId, email: &str) -> Result<bool> {
667        Ok(self.backend.get_member(group_id, email)?.is_some())
668    }
669
670    /// Gets a user's role in a group, if they are a member.
671    ///
672    /// # Errors
673    ///
674    /// Returns an error if storage cannot be accessed.
675    pub fn get_user_role(&self, group_id: &GroupId, email: &str) -> Result<Option<GroupRole>> {
676        Ok(self.backend.get_member(group_id, email)?.map(|m| m.role))
677    }
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683    use crate::storage::group::GroupStorageFactory;
684
685    fn create_test_service() -> GroupService {
686        let backend = GroupStorageFactory::create_in_memory().expect("Failed to create backend");
687        GroupService::new(backend)
688    }
689
690    #[test]
691    fn test_create_group_adds_creator_as_admin() {
692        let service = create_test_service();
693
694        let group = service
695            .create_group("test-org", "engineering", "Eng team", "alice@example.com")
696            .expect("Failed to create group");
697
698        // Verify creator is admin
699        let member = service
700            .get_member(&group.id, "alice@example.com")
701            .expect("Failed to get member")
702            .expect("Member not found");
703
704        assert_eq!(member.role, GroupRole::Admin);
705    }
706
707    #[test]
708    fn test_add_member_requires_admin() {
709        let service = create_test_service();
710
711        // Create group (alice is admin)
712        let group = service
713            .create_group("test-org", "engineering", "", "alice@example.com")
714            .expect("Failed to create group");
715
716        // Add bob as a writer
717        service
718            .add_member(
719                &group.id,
720                "bob@example.com",
721                GroupRole::Write,
722                "alice@example.com",
723            )
724            .expect("Failed to add member");
725
726        // Bob cannot add members
727        let result = service.add_member(
728            &group.id,
729            "charlie@example.com",
730            GroupRole::Read,
731            "bob@example.com",
732        );
733        assert!(result.is_err());
734        assert!(result.unwrap_err().to_string().contains("Admin role"));
735    }
736
737    #[test]
738    fn test_cannot_remove_last_admin() {
739        let service = create_test_service();
740
741        let group = service
742            .create_group("test-org", "engineering", "", "alice@example.com")
743            .expect("Failed to create group");
744
745        // Cannot remove the only admin
746        let result = service.remove_member(&group.id, "alice@example.com", "alice@example.com");
747        assert!(result.is_err());
748        assert!(result.unwrap_err().to_string().contains("last admin"));
749    }
750
751    #[test]
752    fn test_cannot_demote_last_admin() {
753        let service = create_test_service();
754
755        let group = service
756            .create_group("test-org", "engineering", "", "alice@example.com")
757            .expect("Failed to create group");
758
759        // Cannot demote the only admin
760        let result = service.update_member_role(
761            &group.id,
762            "alice@example.com",
763            GroupRole::Write,
764            "alice@example.com",
765        );
766        assert!(result.is_err());
767        assert!(result.unwrap_err().to_string().contains("last admin"));
768    }
769
770    #[test]
771    fn test_invite_workflow() {
772        let service = create_test_service();
773
774        let group = service
775            .create_group("test-org", "engineering", "", "alice@example.com")
776            .expect("Failed to create group");
777
778        // Create invite
779        let (invite, token) = service
780            .create_invite(
781                &group.id,
782                GroupRole::Write,
783                "alice@example.com",
784                Some(3600),
785                Some(5),
786            )
787            .expect("Failed to create invite");
788
789        assert_eq!(invite.role, GroupRole::Write);
790        assert!(!token.is_empty());
791
792        // Join via invite
793        let member = service
794            .join_via_invite(&token, "bob@example.com")
795            .expect("Failed to join");
796
797        assert_eq!(member.role, GroupRole::Write);
798
799        // Cannot join again
800        let result = service.join_via_invite(&token, "bob@example.com");
801        assert!(result.is_err());
802        assert!(result.unwrap_err().to_string().contains("Already a member"));
803    }
804
805    #[test]
806    fn test_leave_group() {
807        let service = create_test_service();
808
809        let group = service
810            .create_group("test-org", "engineering", "", "alice@example.com")
811            .expect("Failed to create group");
812
813        // Add bob
814        service
815            .add_member(
816                &group.id,
817                "bob@example.com",
818                GroupRole::Write,
819                "alice@example.com",
820            )
821            .expect("Failed to add member");
822
823        // Bob can leave
824        assert!(
825            service
826                .leave_group(&group.id, "bob@example.com")
827                .expect("Failed to leave")
828        );
829
830        // Verify bob is no longer a member
831        assert!(
832            !service
833                .is_member(&group.id, "bob@example.com")
834                .expect("Failed to check membership")
835        );
836    }
837
838    #[test]
839    fn test_last_admin_cannot_leave() {
840        let service = create_test_service();
841
842        let group = service
843            .create_group("test-org", "engineering", "", "alice@example.com")
844            .expect("Failed to create group");
845
846        // Alice cannot leave as last admin
847        let result = service.leave_group(&group.id, "alice@example.com");
848        assert!(result.is_err());
849        assert!(result.unwrap_err().to_string().contains("last admin"));
850    }
851
852    #[test]
853    fn test_get_user_groups() {
854        let service = create_test_service();
855
856        // Create two groups
857        let group1 = service
858            .create_group("test-org", "engineering", "", "alice@example.com")
859            .expect("Failed to create group 1");
860        let _group2 = service
861            .create_group("test-org", "design", "", "alice@example.com")
862            .expect("Failed to create group 2");
863
864        // Add bob to engineering
865        service
866            .add_member(
867                &group1.id,
868                "bob@example.com",
869                GroupRole::Write,
870                "alice@example.com",
871            )
872            .expect("Failed to add member");
873
874        // Get alice's groups
875        let alice_groups = service
876            .get_user_groups("test-org", "alice@example.com")
877            .expect("Failed to get groups");
878        assert_eq!(alice_groups.len(), 2);
879
880        // Get bob's groups
881        let bob_groups = service
882            .get_user_groups("test-org", "bob@example.com")
883            .expect("Failed to get groups");
884        assert_eq!(bob_groups.len(), 1);
885        assert_eq!(bob_groups[0].group_id, group1.id);
886    }
887
888    #[test]
889    fn test_require_role() {
890        let service = create_test_service();
891
892        let group = service
893            .create_group("test-org", "engineering", "", "alice@example.com")
894            .expect("Failed to create group");
895
896        // Add bob as writer
897        service
898            .add_member(
899                &group.id,
900                "bob@example.com",
901                GroupRole::Write,
902                "alice@example.com",
903            )
904            .expect("Failed to add member");
905
906        // Add charlie as reader
907        service
908            .add_member(
909                &group.id,
910                "charlie@example.com",
911                GroupRole::Read,
912                "alice@example.com",
913            )
914            .expect("Failed to add member");
915
916        // Alice (admin) passes all checks
917        assert!(
918            service
919                .require_role(&group.id, "alice@example.com", GroupRole::Admin)
920                .is_ok()
921        );
922        assert!(
923            service
924                .require_role(&group.id, "alice@example.com", GroupRole::Write)
925                .is_ok()
926        );
927        assert!(
928            service
929                .require_role(&group.id, "alice@example.com", GroupRole::Read)
930                .is_ok()
931        );
932
933        // Bob (writer) passes write and read checks
934        assert!(
935            service
936                .require_role(&group.id, "bob@example.com", GroupRole::Admin)
937                .is_err()
938        );
939        assert!(
940            service
941                .require_role(&group.id, "bob@example.com", GroupRole::Write)
942                .is_ok()
943        );
944        assert!(
945            service
946                .require_role(&group.id, "bob@example.com", GroupRole::Read)
947                .is_ok()
948        );
949
950        // Charlie (reader) only passes read check
951        assert!(
952            service
953                .require_role(&group.id, "charlie@example.com", GroupRole::Admin)
954                .is_err()
955        );
956        assert!(
957            service
958                .require_role(&group.id, "charlie@example.com", GroupRole::Write)
959                .is_err()
960        );
961        assert!(
962            service
963                .require_role(&group.id, "charlie@example.com", GroupRole::Read)
964                .is_ok()
965        );
966    }
967}