zoe_app_primitives/
group.rs

1//! # Group Management Primitives for Zoe Applications
2//!
3//! This module provides a comprehensive system for managing encrypted, distributed groups
4//! using the Digital Group Assistant (DGA) protocol. The system is built around several
5//! key architectural principles:
6//!
7//! ## 🏗️ Core Architecture
8//!
9//! ### Event-Sourced State Management
10//! Groups maintain their state through an event-sourced approach where:
11//! - All changes to group state are represented as events
12//! - Events are encrypted and stored in a distributed message system
13//! - Group state is reconstructed by replaying events in chronological order
14//! - Each group has a unique identifier that is the Blake3 hash of its creation event
15//!
16//! ### Encrypted Communication
17//! All group communication is encrypted using ChaCha20-Poly1305:
18//! - Each group has a shared encryption key derived from mnemonic phrases or generated randomly
19//! - Only participants with the correct key can decrypt and participate in group activities
20//! - Key distribution happens through secure side channels (QR codes, secure messaging, etc.)
21//!
22//! ### Identity and Membership System
23//! The system uses a sophisticated identity model:
24//! - **VerifyingKeys** are the fundamental participants (cryptographic identities)
25//! - **Aliases** allow users to present different identities within the same group
26//! - **Roles** define what actions participants can perform
27//! - **Dynamic membership** where anyone with the encryption key can participate
28//!
29//! ## 📊 Key Components
30//!
31//! ### [`GroupState`] - Unified State Management
32//! The central type that combines:
33//! - Core group information (name, settings, metadata)
34//! - Runtime member tracking with activity timestamps
35//! - Event history for audit trails and conflict resolution
36//! - Advanced identity management through [`GroupMembership`]
37//!
38//! ### [`GroupInfo`] - Event Payload
39//! Used in group creation and update events:
40//! - Immutable group information for wire protocol
41//! - Structured metadata using [`crate::Metadata`] types
42//! - Key derivation information for encryption
43//!
44//! ### [`GroupMembership`] - Identity Management
45//! Handles complex identity scenarios:
46//! - Multiple aliases per VerifyingKey
47//! - Role assignments to specific identities
48//! - Display name management
49//! - Authorization checking
50//!
51//! ## 🔐 Security Model
52//!
53//! ### Threat Model
54//! The system assumes:
55//! - Network communications may be monitored or intercepted
56//! - Relay servers are semi-trusted (honest but curious)
57//! - Participants may have their devices compromised
58//! - Group membership may change over time
59//!
60//! ### Security Properties
61//! - **Confidentiality**: All group content is encrypted end-to-end
62//! - **Authenticity**: All messages are cryptographically signed
63//! - **Forward Secrecy**: Key rotation prevents decryption of past messages
64//! - **Participation Privacy**: Membership is not revealed to non-members
65//!
66//! ## 🚀 Usage Examples
67//!
68//! ### Creating a New Group
69//! ```rust
70//! use zoe_app_primitives::{GroupInfo, GroupSettings, GroupState, Metadata, GroupKeyInfo};
71//! use zoe_wire_protocol::KeyPair;
72//! use blake3::Hash;
73//!
74//! // Define group metadata
75//! let metadata = vec![
76//!     Metadata::Description("My awesome group".to_string()),
77//!     Metadata::Generic { key: "category".to_string(), value: "work".to_string() },
78//! ];
79//!
80//! // Create group info for the creation event
81//! let group_info = GroupInfo {
82//!     name: "Development Team".to_string(),
83//!     settings: GroupSettings::default(),
84//!     key_info: GroupKeyInfo::new_chacha20_poly1305(
85//!         b"key_id_12345678".to_vec(),
86//!         zoe_wire_protocol::crypto::KeyDerivationInfo {
87//!             method: zoe_wire_protocol::crypto::KeyDerivationMethod::ChaCha20Poly1305Keygen,
88//!             salt: vec![],
89//!             argon2_params: zoe_wire_protocol::crypto::Argon2Params::default(),
90//!             context: "dga-group-key".to_string(),
91//!         },
92//!     ),
93//!     metadata,
94//! };
95//!
96//! // Create the actual group state
97//! let creator = KeyPair::generate(&mut rand::rngs::OsRng).public_key();
98//! let group_id = Hash::from([0u8; 32]); // In practice, this would be the message hash
99//! let timestamp = 1234567890;
100//!
101//! let group_state = GroupState::new(
102//!     group_id,
103//!     group_info.name.clone(),
104//!     group_info.settings.clone(),
105//!     group_info.metadata.clone(),
106//!     creator,
107//!     timestamp,
108//! );
109//! ```
110//!
111//! ### Managing Group Membership
112//! ```rust
113//! use zoe_app_primitives::{GroupMembership, IdentityType, IdentityRef, IdentityInfo};
114//! use zoe_wire_protocol::KeyPair;
115//!
116//! let mut membership = GroupMembership::new();
117//! let user_key = KeyPair::generate(&mut rand::rngs::OsRng).public_key();
118//!
119//! // Check what identities a user can act as
120//! let available_identities = membership.get_available_identities(&user_key);
121//!
122//! // Note: Currently returns empty set during ML-DSA transition
123//! assert!(available_identities.is_empty());
124//! ```
125//!
126//! ### Working with Structured Metadata
127//! ```rust
128//! use zoe_app_primitives::{GroupState, Metadata};
129//! use zoe_wire_protocol::KeyPair;
130//!
131//! # let group_state = GroupState::new(
132//! #     blake3::Hash::from([0u8; 32]),
133//! #     "Test".to_string(),
134//! #     zoe_app_primitives::GroupSettings::default(),
135//! #     vec![],
136//! #     KeyPair::generate(&mut rand::rngs::OsRng).public_key(),
137//! #     1234567890,
138//! # );
139//!
140//! // Extract description from structured metadata
141//! let description = group_state.description();
142//!
143//! // Get key-value metadata for backward compatibility
144//! let generic_metadata = group_state.generic_metadata();
145//! ```
146//!
147//! ## 🔄 State Transitions
148//!
149//! Group state changes through well-defined events:
150//! 1. **Creation**: [`GroupActivityEvent::UpdateGroup`] with initial [`GroupInfo`]
151//! 2. **Member Activity**: Any [`GroupActivityEvent`] announces participation
152//! 3. **Role Changes**: [`GroupActivityEvent::AssignRole`] updates permissions
153//! 4. **Group Updates**: [`GroupActivityEvent::UpdateGroup`] modifies group info
154//! 5. **Member Departure**: [`GroupActivityEvent::LeaveGroup`] removes from active list
155//!
156//! Each event is cryptographically signed and encrypted, ensuring authenticity and confidentiality.
157//!
158//! ## 🌐 Network Integration
159//!
160//! Groups integrate with the relay network through:
161//! - **Channel Tags**: Group ID used as message channel for event routing
162//! - **Subscription Filters**: Clients subscribe to specific group events
163//! - **Message Ordering**: Timestamp-based ordering ensures consistent state
164//! - **Catch-up Mechanism**: New participants can replay event history
165//!
166//! For more details on specific components, see the documentation for individual types.
167
168// Re-export all types from submodules for backwards compatibility
169pub mod events;
170pub mod states;
171
172pub use events::*;
173
174// Re-export the unified GroupState as the primary state type
175pub use states::{GroupMember, GroupMembership, GroupState, GroupStateError, GroupStateResult};
176
177#[cfg(test)]
178mod tests {
179    use super::events::permissions::{GroupAction, GroupPermissions, Permission};
180    use super::events::roles::GroupRole;
181    use super::events::settings::{EncryptionSettings, GroupSettings};
182    use super::events::{GroupActivityEvent, GroupInfo, GroupJoinInfo, GroupKeyInfo};
183    use crate::{Metadata, RelayEndpoint};
184    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
185    use zoe_wire_protocol::{Ed25519VerifyingKey, KeyPair, VerifyingKey};
186
187    fn create_test_verifying_key() -> VerifyingKey {
188        use rand::rngs::OsRng;
189        use zoe_wire_protocol::KeyPair;
190        let keypair = KeyPair::generate(&mut OsRng);
191        keypair.public_key()
192    }
193
194    fn create_test_ed25519_verifying_key() -> Ed25519VerifyingKey {
195        use rand::rngs::OsRng;
196        let signing_key = KeyPair::generate_ed25519(&mut OsRng);
197        match signing_key.public_key() {
198            VerifyingKey::Ed25519(key) => *key,
199            _ => panic!("Expected Ed25519 key from KeyPair::generate_ed25519"),
200        }
201    }
202
203    fn create_test_socket_addr() -> SocketAddr {
204        SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080)
205    }
206
207    fn create_test_key_derivation_info() -> zoe_wire_protocol::crypto::KeyDerivationInfo {
208        zoe_wire_protocol::crypto::KeyDerivationInfo {
209            method: zoe_wire_protocol::crypto::KeyDerivationMethod::Bip39Argon2,
210            salt: vec![1, 2, 3, 4, 5, 6, 7, 8],
211            argon2_params: zoe_wire_protocol::crypto::Argon2Params::default(),
212            context: "dga-group-key".to_string(),
213        }
214    }
215
216    fn create_test_group_key_info(key_id: Vec<u8>) -> GroupKeyInfo {
217        GroupKeyInfo::new_chacha20_poly1305(key_id, create_test_key_derivation_info())
218    }
219
220    #[test]
221    fn test_group_role_has_permission() {
222        // Test Owner permissions
223        assert!(GroupRole::Owner.has_permission(&Permission::OwnerOnly));
224        assert!(GroupRole::Owner.has_permission(&Permission::AdminOrAbove));
225        assert!(GroupRole::Owner.has_permission(&Permission::ModeratorOrAbove));
226        assert!(GroupRole::Owner.has_permission(&Permission::AllMembers));
227
228        // Test Admin permissions
229        assert!(!GroupRole::Admin.has_permission(&Permission::OwnerOnly));
230        assert!(GroupRole::Admin.has_permission(&Permission::AdminOrAbove));
231        assert!(GroupRole::Admin.has_permission(&Permission::ModeratorOrAbove));
232        assert!(GroupRole::Admin.has_permission(&Permission::AllMembers));
233
234        // Test Moderator permissions
235        assert!(!GroupRole::Moderator.has_permission(&Permission::OwnerOnly));
236        assert!(!GroupRole::Moderator.has_permission(&Permission::AdminOrAbove));
237        assert!(GroupRole::Moderator.has_permission(&Permission::ModeratorOrAbove));
238        assert!(GroupRole::Moderator.has_permission(&Permission::AllMembers));
239
240        // Test Member permissions
241        assert!(!GroupRole::Member.has_permission(&Permission::OwnerOnly));
242        assert!(!GroupRole::Member.has_permission(&Permission::AdminOrAbove));
243        assert!(!GroupRole::Member.has_permission(&Permission::ModeratorOrAbove));
244        assert!(GroupRole::Member.has_permission(&Permission::AllMembers));
245    }
246
247    #[test]
248    fn test_group_role_display_name() {
249        assert_eq!(GroupRole::Owner.display_name(), "Owner");
250        assert_eq!(GroupRole::Admin.display_name(), "Administrator");
251        assert_eq!(GroupRole::Moderator.display_name(), "Moderator");
252        assert_eq!(GroupRole::Member.display_name(), "Member");
253    }
254
255    #[test]
256    fn test_group_role_can_assign_role() {
257        // Owner can assign any role
258        assert!(GroupRole::Owner.can_assign_role(&GroupRole::Owner));
259        assert!(GroupRole::Owner.can_assign_role(&GroupRole::Admin));
260        assert!(GroupRole::Owner.can_assign_role(&GroupRole::Moderator));
261        assert!(GroupRole::Owner.can_assign_role(&GroupRole::Member));
262
263        // Admin cannot assign Owner, but can assign lower roles
264        assert!(!GroupRole::Admin.can_assign_role(&GroupRole::Owner));
265        assert!(GroupRole::Admin.can_assign_role(&GroupRole::Admin));
266        assert!(GroupRole::Admin.can_assign_role(&GroupRole::Moderator));
267        assert!(GroupRole::Admin.can_assign_role(&GroupRole::Member));
268
269        // Moderator can only assign Member role
270        assert!(!GroupRole::Moderator.can_assign_role(&GroupRole::Owner));
271        assert!(!GroupRole::Moderator.can_assign_role(&GroupRole::Admin));
272        assert!(!GroupRole::Moderator.can_assign_role(&GroupRole::Moderator));
273        assert!(GroupRole::Moderator.can_assign_role(&GroupRole::Member));
274
275        // Member cannot assign any roles
276        assert!(!GroupRole::Member.can_assign_role(&GroupRole::Owner));
277        assert!(!GroupRole::Member.can_assign_role(&GroupRole::Admin));
278        assert!(!GroupRole::Member.can_assign_role(&GroupRole::Moderator));
279        assert!(!GroupRole::Member.can_assign_role(&GroupRole::Member));
280    }
281
282    #[test]
283    fn test_group_permissions_builder() {
284        let permissions = GroupPermissions::new()
285            .update_group(Permission::AdminOrAbove)
286            .assign_roles(Permission::OwnerOnly)
287            .post_activities(Permission::AllMembers)
288            .update_encryption(Permission::OwnerOnly);
289
290        assert_eq!(permissions.update_group, Permission::AdminOrAbove);
291        assert_eq!(permissions.assign_roles, Permission::OwnerOnly);
292        assert_eq!(permissions.post_activities, Permission::AllMembers);
293        assert_eq!(permissions.update_encryption, Permission::OwnerOnly);
294    }
295
296    #[test]
297    fn test_group_permissions_can_perform_action() {
298        let permissions = GroupPermissions::default();
299
300        // Test default permissions
301        assert!(permissions.can_perform_action(&GroupRole::Owner, GroupAction::UpdateGroup));
302        assert!(permissions.can_perform_action(&GroupRole::Admin, GroupAction::UpdateGroup));
303        assert!(!permissions.can_perform_action(&GroupRole::Moderator, GroupAction::UpdateGroup));
304        assert!(!permissions.can_perform_action(&GroupRole::Member, GroupAction::UpdateGroup));
305
306        assert!(permissions.can_perform_action(&GroupRole::Owner, GroupAction::AssignRoles));
307        assert!(!permissions.can_perform_action(&GroupRole::Admin, GroupAction::AssignRoles));
308
309        assert!(permissions.can_perform_action(&GroupRole::Member, GroupAction::PostActivities));
310
311        assert!(permissions.can_perform_action(&GroupRole::Owner, GroupAction::UpdateEncryption));
312        assert!(!permissions.can_perform_action(&GroupRole::Admin, GroupAction::UpdateEncryption));
313    }
314
315    #[test]
316    fn test_group_key_info() {
317        let key_id = vec![1, 2, 3, 4];
318        let derivation_info = zoe_wire_protocol::crypto::KeyDerivationInfo {
319            method: zoe_wire_protocol::crypto::KeyDerivationMethod::Bip39Argon2,
320            salt: vec![1, 2, 3, 4, 5, 6, 7, 8],
321            argon2_params: zoe_wire_protocol::crypto::Argon2Params::default(),
322            context: "dga-group-key".to_string(),
323        };
324
325        let key_info = GroupKeyInfo::new_chacha20_poly1305(key_id.clone(), derivation_info.clone());
326
327        assert_eq!(key_info.key_id(), &key_id);
328        assert_eq!(key_info.algorithm(), "ChaCha20-Poly1305");
329        assert_eq!(key_info.derivation_info(), Some(&derivation_info));
330    }
331
332    #[test]
333    fn test_group_key_info_matches_key_id() {
334        let key_id = vec![1, 2, 3, 4];
335        let derivation_info = zoe_wire_protocol::crypto::KeyDerivationInfo {
336            method: zoe_wire_protocol::crypto::KeyDerivationMethod::Bip39Argon2,
337            salt: vec![1, 2, 3, 4, 5, 6, 7, 8],
338            argon2_params: zoe_wire_protocol::crypto::Argon2Params::default(),
339            context: "dga-group-key".to_string(),
340        };
341        let key_info = GroupKeyInfo::new_chacha20_poly1305(key_id.clone(), derivation_info);
342
343        assert!(key_info.matches_key_id(&key_id));
344        assert!(!key_info.matches_key_id(&[5, 6, 7, 8]));
345    }
346
347    #[test]
348    fn test_group_settings_builder() {
349        let permissions = GroupPermissions::default();
350        let encryption_settings = EncryptionSettings::default();
351
352        let settings = GroupSettings::new()
353            .permissions(permissions.clone())
354            .encryption_settings(encryption_settings.clone());
355
356        assert_eq!(settings.permissions, permissions);
357        assert_eq!(settings.encryption_settings, encryption_settings);
358    }
359
360    #[test]
361    fn test_encryption_settings_builder() {
362        let settings = EncryptionSettings::new().with_key_rotation(3600);
363
364        assert!(settings.key_rotation_enabled);
365        assert_eq!(settings.key_rotation_interval, Some(3600));
366    }
367
368    #[test]
369    fn test_relay_endpoint() {
370        let address = create_test_socket_addr();
371        let _public_key = create_test_verifying_key();
372
373        let endpoint = RelayEndpoint::new(address, create_test_ed25519_verifying_key())
374            .with_name("Test Relay".to_string())
375            .with_metadata(Metadata::Generic {
376                key: "region".to_string(),
377                value: "us-west".to_string(),
378            });
379
380        assert_eq!(endpoint.address, address);
381        // Note: endpoint uses Ed25519 key, not ML-DSA key
382        assert_eq!(endpoint.name, Some("Test Relay".to_string()));
383        assert_eq!(endpoint.metadata.len(), 1);
384        if let Some(Metadata::Generic { key, value }) = endpoint.metadata.first() {
385            assert_eq!(key, "region");
386            assert_eq!(value, "us-west");
387        } else {
388            panic!("Expected generic metadata");
389        }
390    }
391
392    #[test]
393    fn test_relay_endpoint_display_name() {
394        let address = create_test_socket_addr();
395        let _public_key = create_test_verifying_key();
396
397        // Without name, should use address
398        let endpoint_no_name = RelayEndpoint::new(address, create_test_ed25519_verifying_key());
399        assert_eq!(endpoint_no_name.display_name(), address.to_string());
400
401        // With name, should use name
402        let endpoint_with_name = endpoint_no_name.with_name("Test Relay".to_string());
403        assert_eq!(endpoint_with_name.display_name(), "Test Relay");
404    }
405
406    #[test]
407    fn test_group_join_info() {
408        let channel_id = "test_channel_123".to_string();
409        let group_info = GroupInfo {
410            name: "Test Group".to_string(),
411            settings: GroupSettings::default(),
412            key_info: create_test_group_key_info(vec![1, 2, 3]),
413            metadata: Vec::new(),
414        };
415        let encryption_key = [42u8; 32];
416        let key_info = create_test_group_key_info(vec![1, 2, 3]);
417        let relay_endpoint = RelayEndpoint::new(
418            create_test_socket_addr(),
419            create_test_ed25519_verifying_key(),
420        );
421
422        let join_info = GroupJoinInfo::new(
423            channel_id.clone(),
424            group_info.clone(),
425            encryption_key,
426            key_info.clone(),
427            vec![relay_endpoint.clone()],
428        )
429        .with_invitation_metadata(Metadata::Generic {
430            key: "inviter".to_string(),
431            value: "alice".to_string(),
432        });
433
434        assert_eq!(join_info.channel_id, channel_id);
435        assert_eq!(join_info.group_info, group_info);
436        assert_eq!(join_info.encryption_key, encryption_key);
437        assert_eq!(join_info.key_info, key_info);
438        assert_eq!(join_info.relay_endpoints, vec![relay_endpoint]);
439        assert_eq!(join_info.invitation_metadata.len(), 1);
440        if let Some(Metadata::Generic { key, value }) = join_info.invitation_metadata.first() {
441            assert_eq!(key, "inviter");
442            assert_eq!(value, "alice");
443        } else {
444            panic!("Expected generic metadata");
445        }
446    }
447
448    #[test]
449    fn test_group_join_info_relay_methods() {
450        let relay1 = RelayEndpoint::new(
451            create_test_socket_addr(),
452            create_test_ed25519_verifying_key(),
453        )
454        .with_name("Primary".to_string());
455        let relay2 = RelayEndpoint::new(
456            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8081),
457            create_test_ed25519_verifying_key(),
458        )
459        .with_name("Secondary".to_string());
460
461        let mut join_info = GroupJoinInfo::new(
462            "test".to_string(),
463            GroupInfo {
464                name: "Test".to_string(),
465                settings: GroupSettings::default(),
466                key_info: create_test_group_key_info(vec![1]),
467                metadata: Vec::new(),
468            },
469            [0u8; 32],
470            create_test_group_key_info(vec![1]),
471            vec![relay1.clone()],
472        );
473
474        // Test initial state
475        assert!(join_info.has_relays());
476        assert_eq!(join_info.primary_relay(), Some(&relay1));
477        assert_eq!(join_info.relays_by_priority().len(), 1);
478
479        // Add another relay
480        join_info = join_info.add_relay(relay2.clone());
481        assert_eq!(join_info.relays_by_priority().len(), 2);
482        assert_eq!(join_info.primary_relay(), Some(&relay1)); // First one is still primary
483
484        // Test with no relays
485        let empty_join_info = GroupJoinInfo::new(
486            "test".to_string(),
487            GroupInfo {
488                name: "Test".to_string(),
489                settings: GroupSettings::default(),
490                key_info: create_test_group_key_info(vec![1]),
491                metadata: Vec::new(),
492            },
493            [0u8; 32],
494            create_test_group_key_info(vec![1]),
495            vec![],
496        );
497
498        assert!(!empty_join_info.has_relays());
499        assert_eq!(empty_join_info.primary_relay(), None);
500        assert!(empty_join_info.relays_by_priority().is_empty());
501    }
502
503    #[test]
504    fn test_group_info() {
505        // Test creating a GroupInfo struct
506        let group_info = GroupInfo {
507            name: "Test Group".to_string(),
508            settings: GroupSettings::default(),
509            key_info: create_test_group_key_info(vec![1, 2, 3]),
510            metadata: Vec::new(),
511        };
512
513        // Just test that we can create and clone the struct
514        let _cloned = group_info.clone();
515    }
516
517    #[test]
518    fn test_group_permissions_default() {
519        let permissions = GroupPermissions::default();
520
521        assert_eq!(permissions.update_group, Permission::AdminOrAbove);
522        assert_eq!(permissions.assign_roles, Permission::OwnerOnly);
523        assert_eq!(permissions.post_activities, Permission::AllMembers);
524        assert_eq!(permissions.update_encryption, Permission::OwnerOnly);
525    }
526
527    #[test]
528    fn test_encryption_settings_default() {
529        let settings = EncryptionSettings::default();
530
531        assert!(!settings.key_rotation_enabled);
532        assert_eq!(settings.key_rotation_interval, None);
533    }
534
535    #[test]
536    fn test_group_settings_default() {
537        let settings = GroupSettings::default();
538
539        assert_eq!(settings.permissions, GroupPermissions::default());
540        assert_eq!(settings.encryption_settings, EncryptionSettings::default());
541    }
542
543    #[test]
544    fn test_postcard_serialization_group_activity_event() {
545        let event = GroupActivityEvent::UpdateGroup(GroupInfo {
546            name: "Test Group".to_string(),
547            settings: GroupSettings::default(),
548            key_info: create_test_group_key_info(vec![1, 2, 3]),
549            metadata: Vec::new(),
550        });
551
552        let serialized = postcard::to_stdvec(&event).expect("Failed to serialize");
553        let deserialized: GroupActivityEvent<()> =
554            postcard::from_bytes(&serialized).expect("Failed to deserialize");
555
556        assert_eq!(event, deserialized);
557    }
558
559    #[test]
560    fn test_postcard_serialization_group_role() {
561        for role in [
562            GroupRole::Owner,
563            GroupRole::Admin,
564            GroupRole::Moderator,
565            GroupRole::Member,
566        ] {
567            let serialized = postcard::to_stdvec(&role).expect("Failed to serialize");
568            let deserialized: GroupRole =
569                postcard::from_bytes(&serialized).expect("Failed to deserialize");
570            assert_eq!(role, deserialized);
571        }
572    }
573
574    #[test]
575    fn test_postcard_serialization_permission() {
576        for permission in [
577            Permission::OwnerOnly,
578            Permission::AdminOrAbove,
579            Permission::ModeratorOrAbove,
580            Permission::AllMembers,
581        ] {
582            let serialized = postcard::to_stdvec(&permission).expect("Failed to serialize");
583            let deserialized: Permission =
584                postcard::from_bytes(&serialized).expect("Failed to deserialize");
585            assert_eq!(permission, deserialized);
586        }
587    }
588
589    #[test]
590    fn test_postcard_serialization_group_join_info() {
591        let join_info = GroupJoinInfo::new(
592            "test_channel".to_string(),
593            GroupInfo {
594                name: "Test".to_string(),
595                settings: GroupSettings::default(),
596                key_info: create_test_group_key_info(vec![1, 2, 3]),
597                metadata: Vec::new(),
598            },
599            [42u8; 32],
600            create_test_group_key_info(vec![1, 2, 3]),
601            vec![RelayEndpoint::new(
602                create_test_socket_addr(),
603                create_test_ed25519_verifying_key(),
604            )],
605        );
606
607        let serialized = postcard::to_stdvec(&join_info).expect("Failed to serialize");
608        let deserialized: GroupJoinInfo =
609            postcard::from_bytes(&serialized).expect("Failed to deserialize");
610
611        assert_eq!(join_info, deserialized);
612    }
613}