1use std::fmt;
24use std::sync::OnceLock;
25
26use regex::Regex;
27use serde::{Deserialize, Serialize};
28use chrono::{DateTime, Utc};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum IdentityError {
37 InvalidEmail(String),
39 InvalidSigningKey(String),
41 MissingField(&'static str),
43 InvalidIdentifier(String),
45 DuplicateMember(String),
47}
48
49impl fmt::Display for IdentityError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::InvalidEmail(email) => write!(f, "Invalid email address: {}", email),
53 Self::InvalidSigningKey(key) => write!(f, "Invalid signing key fingerprint: {}", key),
54 Self::MissingField(field) => write!(f, "Missing required field: {}", field),
55 Self::InvalidIdentifier(id) => write!(f, "Invalid identifier '{}': must be lowercase alphanumeric with hyphens", id),
56 Self::DuplicateMember(id) => write!(f, "Duplicate member: {}", id),
57 }
58 }
59}
60
61impl std::error::Error for IdentityError {}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub struct Email(String);
70
71impl Email {
72 pub fn new(email: &str) -> Result<Self, IdentityError> {
76 if validate_email(email) {
77 Ok(Self(email.to_lowercase()))
78 } else {
79 Err(IdentityError::InvalidEmail(email.to_string()))
80 }
81 }
82
83 pub fn as_str(&self) -> &str {
85 &self.0
86 }
87}
88
89impl fmt::Display for Email {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "{}", self.0)
92 }
93}
94
95impl AsRef<str> for Email {
96 fn as_ref(&self) -> &str {
97 &self.0
98 }
99}
100
101fn validate_email(email: &str) -> bool {
111 if email.contains(char::is_whitespace) {
112 return false;
113 }
114
115 let parts: Vec<&str> = email.splitn(2, '@').collect();
116 if parts.len() != 2 {
117 return false;
118 }
119
120 let local = parts[0];
121 let domain = parts[1];
122
123 if local.is_empty() || local.len() > 64 {
125 return false;
126 }
127
128 if domain.is_empty() || domain.len() > 253 {
130 return false;
131 }
132
133 if !domain.contains('.') {
135 return false;
136 }
137
138 for label in domain.split('.') {
140 if label.is_empty() || label.len() > 63 {
141 return false;
142 }
143 if label.starts_with('-') || label.ends_with('-') {
144 return false;
145 }
146 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
147 return false;
148 }
149 }
150
151 true
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
162pub struct SigningKey(String);
163
164impl SigningKey {
165 pub fn new(fingerprint: &str) -> Result<Self, IdentityError> {
170 let cleaned = fingerprint
171 .replace(' ', "")
172 .replace(':', "")
173 .to_uppercase();
174
175 if validate_fingerprint(&cleaned) {
176 Ok(Self(cleaned))
177 } else {
178 Err(IdentityError::InvalidSigningKey(fingerprint.to_string()))
179 }
180 }
181
182 pub fn as_str(&self) -> &str {
184 &self.0
185 }
186
187 pub fn long_id(&self) -> &str {
189 &self.0[24..]
190 }
191
192 pub fn short_id(&self) -> &str {
194 &self.0[32..]
195 }
196}
197
198impl fmt::Display for SigningKey {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 for (i, chunk) in self.0.as_bytes().chunks(4).enumerate() {
202 if i > 0 {
203 write!(f, " ")?;
204 }
205 write!(f, "{}", std::str::from_utf8(chunk).unwrap_or(""))?;
206 }
207 Ok(())
208 }
209}
210
211impl AsRef<str> for SigningKey {
212 fn as_ref(&self) -> &str {
213 &self.0
214 }
215}
216
217fn validate_fingerprint(cleaned: &str) -> bool {
219 cleaned.len() == 40 && cleaned.chars().all(|c| c.is_ascii_hexdigit())
220}
221
222fn validate_identifier(id: &str) -> bool {
229 if id.is_empty() || id.len() > 128 {
230 return false;
231 }
232
233 static RE: OnceLock<Regex> = OnceLock::new();
234 let re = RE.get_or_init(|| Regex::new(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$").unwrap());
235 re.is_match(id)
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct Identity {
248 pub id: String,
250 pub display_name: Option<String>,
252 pub email: Option<Email>,
254 pub signing_key: Option<SigningKey>,
256 pub created_at: DateTime<Utc>,
258 pub metadata: std::collections::HashMap<String, String>,
260}
261
262impl Identity {
263 pub fn new(id: &str) -> Result<Self, IdentityError> {
265 if !validate_identifier(id) {
266 return Err(IdentityError::InvalidIdentifier(id.to_string()));
267 }
268 Ok(Self {
269 id: id.to_string(),
270 display_name: None,
271 email: None,
272 signing_key: None,
273 created_at: Utc::now(),
274 metadata: std::collections::HashMap::new(),
275 })
276 }
277
278 pub fn builder(id: &str) -> IdentityBuilder {
280 IdentityBuilder::new(id)
281 }
282
283 pub fn set_email(&mut self, email: &str) -> Result<(), IdentityError> {
285 self.email = Some(Email::new(email)?);
286 Ok(())
287 }
288
289 pub fn set_signing_key(&mut self, fingerprint: &str) -> Result<(), IdentityError> {
291 self.signing_key = Some(SigningKey::new(fingerprint)?);
292 Ok(())
293 }
294
295 pub fn has_signing_key(&self) -> bool {
297 self.signing_key.is_some()
298 }
299
300 pub fn display(&self) -> String {
302 if let Some(ref name) = self.display_name {
303 format!("{} ({})", name, self.id)
304 } else if let Some(ref email) = self.email {
305 format!("{} <{}>", self.id, email)
306 } else {
307 self.id.clone()
308 }
309 }
310}
311
312impl fmt::Display for Identity {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314 write!(f, "{}", self.display())
315 }
316}
317
318pub struct IdentityBuilder {
324 id: String,
325 display_name: Option<String>,
326 email: Option<Email>,
327 signing_key: Option<SigningKey>,
328 metadata: std::collections::HashMap<String, String>,
329 error: Option<IdentityError>,
330}
331
332impl IdentityBuilder {
333 fn new(id: &str) -> Self {
334 Self {
335 id: id.to_string(),
336 display_name: None,
337 email: None,
338 signing_key: None,
339 metadata: std::collections::HashMap::new(),
340 error: None,
341 }
342 }
343
344 pub fn display_name(mut self, name: &str) -> Self {
346 self.display_name = Some(name.to_string());
347 self
348 }
349
350 pub fn email(mut self, email: &str) -> Result<Self, IdentityError> {
352 self.email = Some(Email::new(email)?);
353 Ok(self)
354 }
355
356 pub fn signing_key(mut self, key: SigningKey) -> Self {
358 self.signing_key = Some(key);
359 self
360 }
361
362 pub fn signing_key_str(mut self, fingerprint: &str) -> Result<Self, IdentityError> {
364 self.signing_key = Some(SigningKey::new(fingerprint)?);
365 Ok(self)
366 }
367
368 pub fn meta(mut self, key: &str, value: &str) -> Self {
370 self.metadata.insert(key.to_string(), value.to_string());
371 self
372 }
373
374 pub fn build(self) -> Result<Identity, IdentityError> {
376 if let Some(err) = self.error {
377 return Err(err);
378 }
379
380 if !validate_identifier(&self.id) {
381 return Err(IdentityError::InvalidIdentifier(self.id));
382 }
383
384 Ok(Identity {
385 id: self.id,
386 display_name: self.display_name,
387 email: self.email,
388 signing_key: self.signing_key,
389 created_at: Utc::now(),
390 metadata: self.metadata,
391 })
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct Organization {
402 pub id: String,
404 pub name: String,
406 pub description: Option<String>,
408 pub members: Vec<String>,
410 pub groups: Vec<String>,
412 pub created_at: DateTime<Utc>,
414 pub metadata: std::collections::HashMap<String, String>,
416}
417
418impl Organization {
419 pub fn new(id: &str, name: &str) -> Result<Self, IdentityError> {
421 if !validate_identifier(id) {
422 return Err(IdentityError::InvalidIdentifier(id.to_string()));
423 }
424
425 Ok(Self {
426 id: id.to_string(),
427 name: name.to_string(),
428 description: None,
429 members: Vec::new(),
430 groups: Vec::new(),
431 created_at: Utc::now(),
432 metadata: std::collections::HashMap::new(),
433 })
434 }
435
436 pub fn add_member(&mut self, identity_id: &str) -> Result<(), IdentityError> {
438 if self.members.contains(&identity_id.to_string()) {
439 return Err(IdentityError::DuplicateMember(identity_id.to_string()));
440 }
441 self.members.push(identity_id.to_string());
442 Ok(())
443 }
444
445 pub fn remove_member(&mut self, identity_id: &str) -> bool {
447 if let Some(pos) = self.members.iter().position(|m| m == identity_id) {
448 self.members.remove(pos);
449 true
450 } else {
451 false
452 }
453 }
454
455 pub fn is_member(&self, identity_id: &str) -> bool {
457 self.members.iter().any(|m| m == identity_id)
458 }
459
460 pub fn add_group(&mut self, group_id: &str) -> Result<(), IdentityError> {
462 if self.groups.contains(&group_id.to_string()) {
463 return Err(IdentityError::DuplicateMember(group_id.to_string()));
464 }
465 self.groups.push(group_id.to_string());
466 Ok(())
467 }
468
469 pub fn remove_group(&mut self, group_id: &str) -> bool {
471 if let Some(pos) = self.groups.iter().position(|g| g == group_id) {
472 self.groups.remove(pos);
473 true
474 } else {
475 false
476 }
477 }
478}
479
480impl fmt::Display for Organization {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482 write!(f, "{} ({}, {} members)", self.name, self.id, self.members.len())
483 }
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
495pub struct Group {
496 pub id: String,
498 pub name: String,
500 pub description: Option<String>,
502 pub members: Vec<String>,
504 pub created_at: DateTime<Utc>,
506 pub metadata: std::collections::HashMap<String, String>,
508}
509
510impl Group {
511 pub fn new(id: &str, name: &str) -> Result<Self, IdentityError> {
513 if !validate_identifier(id) {
514 return Err(IdentityError::InvalidIdentifier(id.to_string()));
515 }
516
517 Ok(Self {
518 id: id.to_string(),
519 name: name.to_string(),
520 description: None,
521 members: Vec::new(),
522 created_at: Utc::now(),
523 metadata: std::collections::HashMap::new(),
524 })
525 }
526
527 pub fn add_member(&mut self, identity_id: &str) -> Result<(), IdentityError> {
529 if self.members.contains(&identity_id.to_string()) {
530 return Err(IdentityError::DuplicateMember(identity_id.to_string()));
531 }
532 self.members.push(identity_id.to_string());
533 Ok(())
534 }
535
536 pub fn remove_member(&mut self, identity_id: &str) -> bool {
538 if let Some(pos) = self.members.iter().position(|m| m == identity_id) {
539 self.members.remove(pos);
540 true
541 } else {
542 false
543 }
544 }
545
546 pub fn is_member(&self, identity_id: &str) -> bool {
548 self.members.iter().any(|m| m == identity_id)
549 }
550
551 pub fn member_count(&self) -> usize {
553 self.members.len()
554 }
555}
556
557impl fmt::Display for Group {
558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559 write!(f, "{} ({}, {} members)", self.name, self.id, self.members.len())
560 }
561}
562
563#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
574 fn test_valid_emails() {
575 let valid = [
576 "user@example.com",
577 "alice.bob@sub.domain.org",
578 "test+tag@gmail.com",
579 "a@b.co",
580 "user123@test-domain.io",
581 ];
582 for email in &valid {
583 assert!(Email::new(email).is_ok(), "Expected valid: {}", email);
584 }
585 }
586
587 #[test]
588 fn test_invalid_emails() {
589 let invalid = [
590 "",
591 "noat",
592 "@domain.com",
593 "user@",
594 "user@domain", "user@.com", "user@domain.", "user@-domain.com", "user@domain-.com", "user @domain.com", "user@dom ain.com", ];
602 for email in &invalid {
603 assert!(Email::new(email).is_err(), "Expected invalid: '{}'", email);
604 }
605 }
606
607 #[test]
608 fn test_email_normalises_to_lowercase() {
609 let email = Email::new("Alice@Example.COM").unwrap();
610 assert_eq!(email.as_str(), "alice@example.com");
611 }
612
613 #[test]
616 fn test_valid_signing_keys() {
617 let fp = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2";
618 let key = SigningKey::new(fp).unwrap();
619 assert_eq!(key.as_str(), fp);
620 }
621
622 #[test]
623 fn test_signing_key_with_spaces() {
624 let fp = "A1B2 C3D4 E5F6 A1B2 C3D4 E5F6 A1B2 C3D4 E5F6 A1B2";
625 let key = SigningKey::new(fp).unwrap();
626 assert_eq!(key.as_str(), "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2");
627 }
628
629 #[test]
630 fn test_signing_key_with_colons() {
631 let fp = "A1B2:C3D4:E5F6:A1B2:C3D4:E5F6:A1B2:C3D4:E5F6:A1B2";
632 let key = SigningKey::new(fp).unwrap();
633 assert_eq!(key.as_str(), "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2");
634 }
635
636 #[test]
637 fn test_signing_key_lowercase_normalised() {
638 let fp = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
639 let key = SigningKey::new(fp).unwrap();
640 assert_eq!(key.as_str(), "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2");
641 }
642
643 #[test]
644 fn test_invalid_signing_keys() {
645 assert!(SigningKey::new("").is_err());
646 assert!(SigningKey::new("tooshort").is_err());
647 assert!(SigningKey::new("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ").is_err()); assert!(SigningKey::new("A1B2C3D4").is_err()); assert!(SigningKey::new("A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2FF").is_err()); }
651
652 #[test]
653 fn test_signing_key_ids() {
654 let fp = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2";
655 let key = SigningKey::new(fp).unwrap();
656 assert_eq!(key.long_id(), "A1B2C3D4E5F6A1B2");
657 assert_eq!(key.short_id(), "E5F6A1B2");
658 }
659
660 #[test]
661 fn test_signing_key_display() {
662 let fp = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2";
663 let key = SigningKey::new(fp).unwrap();
664 let display = format!("{}", key);
665 assert_eq!(display, "A1B2 C3D4 E5F6 A1B2 C3D4 E5F6 A1B2 C3D4 E5F6 A1B2");
666 }
667
668 #[test]
671 fn test_valid_identifiers() {
672 assert!(validate_identifier("alice"));
673 assert!(validate_identifier("bob-smith"));
674 assert!(validate_identifier("a"));
675 assert!(validate_identifier("team-42"));
676 assert!(validate_identifier("x1"));
677 }
678
679 #[test]
680 fn test_invalid_identifiers() {
681 assert!(!validate_identifier(""));
682 assert!(!validate_identifier("-starts-with-dash"));
683 assert!(!validate_identifier("ends-with-dash-"));
684 assert!(!validate_identifier("has spaces"));
685 assert!(!validate_identifier("UPPERCASE"));
686 assert!(!validate_identifier("has_underscore"));
687 assert!(!validate_identifier("has.dot"));
688 }
689
690 #[test]
693 fn test_identity_new() {
694 let id = Identity::new("alice").unwrap();
695 assert_eq!(id.id, "alice");
696 assert!(id.email.is_none());
697 assert!(id.signing_key.is_none());
698 }
699
700 #[test]
701 fn test_identity_invalid_id() {
702 assert!(Identity::new("").is_err());
703 assert!(Identity::new("UPPER").is_err());
704 assert!(Identity::new("-bad").is_err());
705 }
706
707 #[test]
708 fn test_identity_set_email() {
709 let mut id = Identity::new("alice").unwrap();
710 id.set_email("alice@example.com").unwrap();
711 assert_eq!(id.email.as_ref().unwrap().as_str(), "alice@example.com");
712 }
713
714 #[test]
715 fn test_identity_set_invalid_email() {
716 let mut id = Identity::new("alice").unwrap();
717 assert!(id.set_email("not-an-email").is_err());
718 }
719
720 #[test]
721 fn test_identity_set_signing_key() {
722 let mut id = Identity::new("alice").unwrap();
723 id.set_signing_key("A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2").unwrap();
724 assert!(id.has_signing_key());
725 }
726
727 #[test]
728 fn test_identity_builder() {
729 let key = SigningKey::new("A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2").unwrap();
730 let identity = Identity::builder("alice")
731 .email("alice@example.com").unwrap()
732 .display_name("Alice Smith")
733 .signing_key(key)
734 .meta("role", "lead")
735 .build()
736 .unwrap();
737
738 assert_eq!(identity.id, "alice");
739 assert_eq!(identity.display_name.as_deref(), Some("Alice Smith"));
740 assert_eq!(identity.email.as_ref().unwrap().as_str(), "alice@example.com");
741 assert!(identity.has_signing_key());
742 assert_eq!(identity.metadata.get("role").unwrap(), "lead");
743 }
744
745 #[test]
746 fn test_identity_builder_invalid_id() {
747 let result = Identity::builder("BAD ID").build();
748 assert!(result.is_err());
749 }
750
751 #[test]
752 fn test_identity_builder_invalid_email() {
753 let result = Identity::builder("alice").email("nope");
754 assert!(result.is_err());
755 }
756
757 #[test]
758 fn test_identity_display() {
759 let id = Identity::builder("alice")
760 .display_name("Alice Smith")
761 .build()
762 .unwrap();
763 assert_eq!(format!("{}", id), "Alice Smith (alice)");
764
765 let id2 = Identity::builder("bob")
766 .email("bob@example.com").unwrap()
767 .build()
768 .unwrap();
769 assert_eq!(format!("{}", id2), "bob <bob@example.com>");
770
771 let id3 = Identity::new("charlie").unwrap();
772 assert_eq!(format!("{}", id3), "charlie");
773 }
774
775 #[test]
778 fn test_organization_new() {
779 let org = Organization::new("acme", "Acme Corp").unwrap();
780 assert_eq!(org.id, "acme");
781 assert_eq!(org.name, "Acme Corp");
782 assert!(org.members.is_empty());
783 assert!(org.groups.is_empty());
784 }
785
786 #[test]
787 fn test_organization_invalid_id() {
788 assert!(Organization::new("BAD", "Bad Org").is_err());
789 }
790
791 #[test]
792 fn test_organization_add_member() {
793 let mut org = Organization::new("acme", "Acme Corp").unwrap();
794 org.add_member("alice").unwrap();
795 org.add_member("bob").unwrap();
796 assert!(org.is_member("alice"));
797 assert!(org.is_member("bob"));
798 assert!(!org.is_member("charlie"));
799 }
800
801 #[test]
802 fn test_organization_duplicate_member() {
803 let mut org = Organization::new("acme", "Acme Corp").unwrap();
804 org.add_member("alice").unwrap();
805 assert!(org.add_member("alice").is_err());
806 }
807
808 #[test]
809 fn test_organization_remove_member() {
810 let mut org = Organization::new("acme", "Acme Corp").unwrap();
811 org.add_member("alice").unwrap();
812 assert!(org.remove_member("alice"));
813 assert!(!org.is_member("alice"));
814 assert!(!org.remove_member("alice")); }
816
817 #[test]
818 fn test_organization_add_group() {
819 let mut org = Organization::new("acme", "Acme Corp").unwrap();
820 org.add_group("backend").unwrap();
821 assert!(org.groups.contains(&"backend".to_string()));
822 }
823
824 #[test]
825 fn test_organization_duplicate_group() {
826 let mut org = Organization::new("acme", "Acme Corp").unwrap();
827 org.add_group("backend").unwrap();
828 assert!(org.add_group("backend").is_err());
829 }
830
831 #[test]
832 fn test_organization_remove_group() {
833 let mut org = Organization::new("acme", "Acme Corp").unwrap();
834 org.add_group("backend").unwrap();
835 assert!(org.remove_group("backend"));
836 assert!(!org.remove_group("backend"));
837 }
838
839 #[test]
840 fn test_organization_display() {
841 let mut org = Organization::new("acme", "Acme Corp").unwrap();
842 org.add_member("alice").unwrap();
843 org.add_member("bob").unwrap();
844 assert_eq!(format!("{}", org), "Acme Corp (acme, 2 members)");
845 }
846
847 #[test]
850 fn test_group_new() {
851 let group = Group::new("reviewers", "Code Reviewers").unwrap();
852 assert_eq!(group.id, "reviewers");
853 assert_eq!(group.name, "Code Reviewers");
854 assert!(group.members.is_empty());
855 }
856
857 #[test]
858 fn test_group_invalid_id() {
859 assert!(Group::new("BAD", "Bad Group").is_err());
860 }
861
862 #[test]
863 fn test_group_add_member() {
864 let mut group = Group::new("reviewers", "Reviewers").unwrap();
865 group.add_member("alice").unwrap();
866 assert!(group.is_member("alice"));
867 assert_eq!(group.member_count(), 1);
868 }
869
870 #[test]
871 fn test_group_duplicate_member() {
872 let mut group = Group::new("reviewers", "Reviewers").unwrap();
873 group.add_member("alice").unwrap();
874 assert!(group.add_member("alice").is_err());
875 }
876
877 #[test]
878 fn test_group_remove_member() {
879 let mut group = Group::new("reviewers", "Reviewers").unwrap();
880 group.add_member("alice").unwrap();
881 assert!(group.remove_member("alice"));
882 assert!(!group.is_member("alice"));
883 assert_eq!(group.member_count(), 0);
884 }
885
886 #[test]
887 fn test_group_display() {
888 let mut group = Group::new("reviewers", "Code Reviewers").unwrap();
889 group.add_member("alice").unwrap();
890 assert_eq!(format!("{}", group), "Code Reviewers (reviewers, 1 members)");
891 }
892
893 #[test]
896 fn test_identity_serde_roundtrip() {
897 let identity = Identity::builder("alice")
898 .email("alice@example.com").unwrap()
899 .display_name("Alice")
900 .signing_key_str("A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2").unwrap()
901 .build()
902 .unwrap();
903
904 let json = serde_json::to_string(&identity).unwrap();
905 let deserialized: Identity = serde_json::from_str(&json).unwrap();
906
907 assert_eq!(identity.id, deserialized.id);
908 assert_eq!(identity.email, deserialized.email);
909 assert_eq!(identity.signing_key, deserialized.signing_key);
910 assert_eq!(identity.display_name, deserialized.display_name);
911 }
912
913 #[test]
914 fn test_organization_serde_roundtrip() {
915 let mut org = Organization::new("acme", "Acme Corp").unwrap();
916 org.add_member("alice").unwrap();
917 org.add_group("backend").unwrap();
918
919 let json = serde_json::to_string(&org).unwrap();
920 let deserialized: Organization = serde_json::from_str(&json).unwrap();
921
922 assert_eq!(org.id, deserialized.id);
923 assert_eq!(org.members, deserialized.members);
924 assert_eq!(org.groups, deserialized.groups);
925 }
926
927 #[test]
928 fn test_group_serde_roundtrip() {
929 let mut group = Group::new("reviewers", "Reviewers").unwrap();
930 group.add_member("alice").unwrap();
931
932 let yaml = serde_yaml::to_string(&group).unwrap();
933 let deserialized: Group = serde_yaml::from_str(&yaml).unwrap();
934
935 assert_eq!(group.id, deserialized.id);
936 assert_eq!(group.members, deserialized.members);
937 }
938}