1use crate::common::Timestamp;
12#[cfg(feature = "uuid")]
13use crate::org_id::OrgId;
14#[cfg(all(not(feature = "std"), feature = "alloc"))]
15use alloc::borrow::Cow;
16#[cfg(all(not(feature = "std"), feature = "alloc", feature = "uuid"))]
17use alloc::borrow::ToOwned;
18#[cfg(all(not(feature = "std"), feature = "alloc"))]
19use alloc::string::String;
20#[cfg(all(not(feature = "std"), feature = "alloc", feature = "uuid"))]
21use alloc::string::ToString;
22#[cfg(all(not(feature = "std"), feature = "alloc", feature = "uuid"))]
23use alloc::vec::Vec;
24
25#[cfg(feature = "std")]
26use std::borrow::Cow;
27
28#[cfg(feature = "uuid")]
29use uuid::Uuid;
30
31#[cfg(feature = "serde")]
32use serde::{Deserialize, Serialize};
33
34#[derive(Clone, PartialEq, Eq, Hash)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
42#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
43#[cfg_attr(feature = "utoipa", schema(value_type = String))]
44#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
45#[cfg_attr(feature = "schemars", schemars(transparent))]
46pub struct PrincipalId(Cow<'static, str>);
47
48impl PrincipalId {
49 #[must_use]
51 pub const fn static_str(s: &'static str) -> Self {
52 Self(Cow::Borrowed(s))
53 }
54
55 #[must_use]
57 pub fn from_owned(s: String) -> Self {
58 Self(Cow::Owned(s))
59 }
60
61 #[must_use]
63 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66
67 #[cfg(feature = "uuid")]
69 #[must_use]
70 pub fn from_uuid(uuid: Uuid) -> Self {
71 Self(Cow::Owned(uuid.to_string()))
72 }
73}
74
75impl core::fmt::Debug for PrincipalId {
76 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77 f.debug_tuple("PrincipalId").field(&self.as_str()).finish()
78 }
79}
80
81impl core::fmt::Display for PrincipalId {
82 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83 f.write_str(self.as_str())
84 }
85}
86
87#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
89#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
90#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
91#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
92#[non_exhaustive]
93pub enum PrincipalKind {
94 User,
96 Service,
98 System,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct PrincipalParseError {
114 pub input: String,
116}
117
118impl core::fmt::Display for PrincipalParseError {
119 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
120 write!(
121 f,
122 "invalid Principal: expected a UUID string, got {:?}",
123 self.input
124 )
125 }
126}
127
128#[cfg(feature = "std")]
129impl std::error::Error for PrincipalParseError {}
130
131#[derive(Clone, PartialEq, Eq, Hash, Debug)]
180#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
181#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
182#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
183pub struct Principal {
184 pub id: PrincipalId,
186 pub kind: PrincipalKind,
188 #[cfg(feature = "uuid")]
191 #[cfg_attr(feature = "serde", serde(default))]
192 pub org_path: Vec<OrgId>,
193}
194
195impl Principal {
196 #[cfg(feature = "uuid")]
216 #[must_use]
217 pub fn human(uuid: Uuid) -> Self {
218 Self {
219 id: PrincipalId::from_uuid(uuid),
220 kind: PrincipalKind::User,
221 #[cfg(feature = "uuid")]
222 org_path: Vec::new(),
223 }
224 }
225
226 #[cfg(feature = "uuid")]
249 pub fn try_parse(s: &str) -> Result<Self, PrincipalParseError> {
250 Uuid::parse_str(s)
251 .map(Self::human)
252 .map_err(|_| PrincipalParseError {
253 input: s.to_owned(),
254 })
255 }
256
257 #[must_use]
270 pub fn system(id: &'static str) -> Self {
271 Self {
272 id: PrincipalId::static_str(id),
273 kind: PrincipalKind::System,
274 #[cfg(feature = "uuid")]
275 org_path: Vec::new(),
276 }
277 }
278
279 #[must_use]
289 pub fn as_str(&self) -> &str {
290 self.id.as_str()
291 }
292
293 #[cfg(feature = "uuid")]
308 #[must_use]
309 pub fn with_org_path(mut self, org_path: Vec<OrgId>) -> Self {
310 self.org_path = org_path;
311 self
312 }
313
314 #[cfg(feature = "uuid")]
329 #[must_use]
330 pub fn org_path_display(&self) -> String {
331 self.org_path
332 .iter()
333 .map(ToString::to_string)
334 .collect::<Vec<_>>()
335 .join(",")
336 }
337}
338
339impl core::fmt::Display for Principal {
340 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341 f.write_str(self.as_str())
342 }
343}
344
345#[cfg(all(feature = "arbitrary", feature = "uuid"))]
348impl<'a> arbitrary::Arbitrary<'a> for Principal {
349 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
350 let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
351 Ok(Self::human(Uuid::from_bytes(bytes)))
352 }
353}
354
355#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
359impl<'a> arbitrary::Arbitrary<'a> for Principal {
360 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
361 let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
362 Ok(Self {
363 id: PrincipalId::from_owned(s),
364 kind: PrincipalKind::System,
365 #[cfg(feature = "uuid")]
366 org_path: Vec::new(),
367 })
368 }
369}
370
371#[cfg(all(feature = "arbitrary", feature = "uuid"))]
373impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
374 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
375 let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
376 Ok(Self::from_uuid(Uuid::from_bytes(bytes)))
377 }
378}
379
380#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
381impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
382 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
383 let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
384 Ok(Self::from_owned(s))
385 }
386}
387
388#[cfg(feature = "arbitrary")]
390impl<'a> arbitrary::Arbitrary<'a> for PrincipalKind {
391 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
392 match <u8 as arbitrary::Arbitrary>::arbitrary(u)? % 3 {
393 0 => Ok(Self::User),
394 1 => Ok(Self::Service),
395 _ => Ok(Self::System),
396 }
397 }
398}
399
400#[cfg(all(feature = "proptest", feature = "uuid"))]
403impl proptest::arbitrary::Arbitrary for Principal {
404 type Parameters = ();
405 type Strategy = proptest::strategy::BoxedStrategy<Self>;
406
407 fn arbitrary_with((): ()) -> Self::Strategy {
408 use proptest::prelude::*;
409 any::<[u8; 16]>()
410 .prop_map(|b| Self::human(Uuid::from_bytes(b)))
411 .boxed()
412 }
413}
414
415#[cfg(all(feature = "proptest", not(feature = "uuid")))]
417impl proptest::arbitrary::Arbitrary for Principal {
418 type Parameters = ();
419 type Strategy = proptest::strategy::BoxedStrategy<Self>;
420
421 fn arbitrary_with((): ()) -> Self::Strategy {
422 use proptest::prelude::*;
423 any::<String>()
424 .prop_map(|s| Self {
425 id: PrincipalId::from_owned(s),
426 kind: PrincipalKind::System,
427 #[cfg(feature = "uuid")]
428 org_path: Vec::new(),
429 })
430 .boxed()
431 }
432}
433
434#[cfg(all(feature = "proptest", feature = "uuid"))]
436impl proptest::arbitrary::Arbitrary for PrincipalId {
437 type Parameters = ();
438 type Strategy = proptest::strategy::BoxedStrategy<Self>;
439
440 fn arbitrary_with((): ()) -> Self::Strategy {
441 use proptest::prelude::*;
442 any::<[u8; 16]>()
443 .prop_map(|b| Self::from_uuid(Uuid::from_bytes(b)))
444 .boxed()
445 }
446}
447
448#[cfg(all(feature = "proptest", not(feature = "uuid")))]
449impl proptest::arbitrary::Arbitrary for PrincipalId {
450 type Parameters = ();
451 type Strategy = proptest::strategy::BoxedStrategy<Self>;
452
453 fn arbitrary_with((): ()) -> Self::Strategy {
454 use proptest::prelude::*;
455 any::<String>().prop_map(|s| Self::from_owned(s)).boxed()
456 }
457}
458
459#[cfg(feature = "proptest")]
461impl proptest::arbitrary::Arbitrary for PrincipalKind {
462 type Parameters = ();
463 type Strategy = proptest::strategy::BoxedStrategy<Self>;
464
465 fn arbitrary_with((): ()) -> Self::Strategy {
466 use proptest::prelude::*;
467 prop_oneof![Just(Self::User), Just(Self::Service), Just(Self::System),].boxed()
468 }
469}
470
471#[derive(Debug, Clone, PartialEq, Eq)]
495#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
496#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
497#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
498#[cfg_attr(
500 all(feature = "arbitrary", not(feature = "chrono")),
501 derive(arbitrary::Arbitrary)
502)]
503#[cfg_attr(
504 all(feature = "proptest", not(feature = "chrono")),
505 derive(proptest_derive::Arbitrary)
506)]
507pub struct AuditInfo {
508 #[cfg_attr(
510 feature = "utoipa",
511 schema(value_type = String, format = DateTime)
512 )]
513 pub created_at: Timestamp,
514 #[cfg_attr(
516 feature = "utoipa",
517 schema(value_type = String, format = DateTime)
518 )]
519 pub updated_at: Timestamp,
520 pub created_by: Principal,
522 pub updated_by: Principal,
524}
525
526impl AuditInfo {
527 #[must_use]
543 pub fn new(
544 created_at: Timestamp,
545 updated_at: Timestamp,
546 created_by: Principal,
547 updated_by: Principal,
548 ) -> Self {
549 Self {
550 created_at,
551 updated_at,
552 created_by,
553 updated_by,
554 }
555 }
556
557 #[cfg(feature = "chrono")]
577 #[must_use]
578 pub fn now(created_by: Principal) -> Self {
579 let now = chrono::Utc::now();
580 let updated_by = created_by.clone();
581 Self {
582 created_at: now,
583 updated_at: now,
584 created_by,
585 updated_by,
586 }
587 }
588
589 #[cfg(feature = "chrono")]
606 pub fn touch(&mut self, updated_by: Principal) {
607 self.updated_at = chrono::Utc::now();
608 self.updated_by = updated_by;
609 }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq)]
639#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
640pub struct ResolvedPrincipal {
641 pub id: Principal,
643 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
647 pub display_name: Option<String>,
648}
649
650impl ResolvedPrincipal {
651 #[must_use]
653 pub fn new(id: Principal, display_name: Option<String>) -> Self {
654 Self { id, display_name }
655 }
656
657 #[must_use]
660 pub fn display(&self) -> &str {
661 self.display_name
662 .as_deref()
663 .unwrap_or_else(|| self.id.as_str())
664 }
665}
666
667impl From<Principal> for ResolvedPrincipal {
668 fn from(id: Principal) -> Self {
669 Self {
670 id,
671 display_name: None,
672 }
673 }
674}
675
676#[cfg(all(feature = "arbitrary", feature = "chrono"))]
683impl<'a> arbitrary::Arbitrary<'a> for AuditInfo {
684 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
685 let created_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
687 let updated_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
688 let created_at = chrono::DateTime::from_timestamp(created_secs.abs(), 0)
689 .unwrap_or_else(chrono::Utc::now);
690 let updated_at = chrono::DateTime::from_timestamp(updated_secs.abs(), 0)
691 .unwrap_or_else(chrono::Utc::now);
692 Ok(Self {
693 created_at,
694 updated_at,
695 created_by: Principal::arbitrary(u)?,
696 updated_by: Principal::arbitrary(u)?,
697 })
698 }
699}
700
701#[cfg(all(feature = "proptest", feature = "chrono"))]
702impl proptest::arbitrary::Arbitrary for AuditInfo {
703 type Parameters = ();
704 type Strategy = proptest::strategy::BoxedStrategy<Self>;
705
706 fn arbitrary_with((): ()) -> Self::Strategy {
707 use proptest::prelude::*;
708 (
709 0i64..=32_503_680_000i64,
710 0i64..=32_503_680_000i64,
711 any::<Principal>(),
712 any::<Principal>(),
713 )
714 .prop_map(|(cs, us, cb, ub)| Self {
715 created_at: chrono::DateTime::from_timestamp(cs, 0)
716 .unwrap_or_else(chrono::Utc::now),
717 updated_at: chrono::DateTime::from_timestamp(us, 0)
718 .unwrap_or_else(chrono::Utc::now),
719 created_by: cb,
720 updated_by: ub,
721 })
722 .boxed()
723 }
724}
725
726#[cfg(test)]
731mod tests {
732 use super::*;
733 #[cfg(feature = "uuid")]
734 use uuid::Uuid;
735
736 #[test]
739 fn principal_id_static_str() {
740 let id = PrincipalId::static_str("foo");
741 assert_eq!(id.as_str(), "foo");
742 }
743
744 #[test]
745 fn principal_id_from_owned() {
746 let id = PrincipalId::from_owned("bar".to_owned());
747 assert_eq!(id.as_str(), "bar");
748 }
749
750 #[cfg(feature = "uuid")]
751 #[test]
752 fn principal_id_from_uuid() {
753 let id = PrincipalId::from_uuid(Uuid::nil());
754 assert_eq!(id.as_str(), "00000000-0000-0000-0000-000000000000");
755 }
756
757 #[test]
758 fn principal_id_display() {
759 let id = PrincipalId::static_str("test");
760 assert_eq!(format!("{id}"), "test");
761 }
762
763 #[cfg(feature = "serde")]
764 #[test]
765 fn principal_id_serde_transparent() {
766 let id = PrincipalId::static_str("myid");
767 let json = serde_json::to_value(&id).unwrap();
768 assert_eq!(json, serde_json::json!("myid"));
769 let back: PrincipalId = serde_json::from_value(json).unwrap();
770 assert_eq!(back, id);
771 }
772
773 #[test]
776 fn principal_kind_copy_and_eq() {
777 let k1 = PrincipalKind::User;
778 let k2 = k1;
779 assert_eq!(k1, k2);
780 }
781
782 #[test]
783 fn principal_kind_all_variants() {
784 let _ = PrincipalKind::User;
785 let _ = PrincipalKind::Service;
786 let _ = PrincipalKind::System;
787 }
788
789 #[cfg(feature = "uuid")]
792 #[test]
793 fn principal_human_has_user_kind() {
794 let p = Principal::human(Uuid::nil());
795 assert_eq!(p.kind, PrincipalKind::User);
796 }
797
798 #[cfg(feature = "uuid")]
799 #[test]
800 fn principal_human_has_empty_org_path() {
801 let p = Principal::human(Uuid::nil());
802 assert!(p.org_path.is_empty());
803 }
804
805 #[test]
806 fn principal_system_has_system_kind() {
807 let p = Principal::system("s");
808 assert_eq!(p.kind, PrincipalKind::System);
809 }
810
811 #[cfg(feature = "uuid")]
812 #[test]
813 fn principal_system_has_empty_org_path() {
814 let p = Principal::system("s");
815 assert!(p.org_path.is_empty());
816 }
817
818 #[cfg(feature = "uuid")]
819 #[test]
820 fn principal_with_org_path_builder() {
821 let org_id = crate::org_id::OrgId::generate();
822 let p = Principal::system("test").with_org_path(vec![org_id]);
823 assert_eq!(p.org_path.len(), 1);
824 assert_eq!(p.org_path[0], org_id);
825 }
826
827 #[cfg(feature = "uuid")]
828 #[test]
829 fn org_path_display_empty_for_system_principal() {
830 let p = Principal::system("svc");
831 assert_eq!(p.org_path_display(), "");
832 }
833
834 #[cfg(feature = "uuid")]
835 #[test]
836 fn org_path_display_single_org() {
837 let org_id = crate::org_id::OrgId::generate();
838 let p = Principal::system("svc").with_org_path(vec![org_id]);
839 assert_eq!(p.org_path_display(), org_id.to_string());
840 }
841
842 #[cfg(feature = "uuid")]
843 #[test]
844 fn org_path_display_multiple_orgs_comma_separated() {
845 let root = crate::org_id::OrgId::generate();
846 let child = crate::org_id::OrgId::generate();
847 let p = Principal::system("svc").with_org_path(vec![root, child]);
848 assert_eq!(p.org_path_display(), format!("{root},{child}"));
849 }
850
851 #[cfg(feature = "uuid")]
852 #[test]
853 fn principal_try_parse_accepts_valid_uuid() {
854 let s = "550e8400-e29b-41d4-a716-446655440000";
855 let p = Principal::try_parse(s).expect("valid UUID should parse");
856 assert_eq!(p.as_str(), s);
857 }
858
859 #[cfg(feature = "uuid")]
860 #[test]
861 fn principal_try_parse_sets_user_kind() {
862 let p = Principal::try_parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
863 assert_eq!(p.kind, PrincipalKind::User);
864 }
865
866 #[cfg(feature = "uuid")]
867 #[test]
868 fn principal_try_parse_rejects_email_string() {
869 let err = Principal::try_parse("alice@example.com").expect_err("email must be rejected");
870 assert_eq!(err.input, "alice@example.com");
871 assert!(err.to_string().contains("alice@example.com"));
872 }
873
874 #[cfg(feature = "uuid")]
875 #[test]
876 fn principal_try_parse_rejects_empty_string() {
877 let err = Principal::try_parse("").expect_err("empty string must be rejected");
878 assert_eq!(err.input, "");
879 }
880
881 #[test]
882 fn principal_as_str_returns_id_str() {
883 let p = Principal::system("x");
884 assert_eq!(p.as_str(), "x");
885 }
886
887 #[cfg(feature = "uuid")]
888 #[test]
889 fn principal_display_forwards_to_as_str() {
890 let p = Principal::human(Uuid::nil());
891 let s = format!("{p}");
892 assert_eq!(s, Uuid::nil().to_string());
893 }
894
895 #[cfg(feature = "uuid")]
896 #[test]
897 fn principal_debug_is_not_redacted() {
898 let p = Principal::human(Uuid::nil());
899 let s = format!("{p:?}");
900 assert!(
901 s.contains(&Uuid::nil().to_string()),
902 "debug must not redact: {s}"
903 );
904 assert!(s.contains("Principal"), "debug must name the type: {s}");
905 }
906
907 #[test]
908 fn principal_equality_and_hash_across_owned_and_borrowed() {
909 use std::collections::hash_map::DefaultHasher;
910 use std::hash::{Hash, Hasher};
911
912 let p1 = Principal::system("orders.bootstrap");
913 let p2 = Principal::system("orders.bootstrap");
914 assert_eq!(p1, p2);
915
916 let mut h1 = DefaultHasher::new();
917 p1.hash(&mut h1);
918 let mut h2 = DefaultHasher::new();
919 p2.hash(&mut h2);
920 assert_eq!(h1.finish(), h2.finish());
921 }
922
923 #[cfg(feature = "uuid")]
924 #[test]
925 fn principal_clone_roundtrip() {
926 let p = Principal::human(Uuid::nil());
927 let q = p.clone();
928 assert_eq!(p, q);
929 }
930
931 #[cfg(all(feature = "serde", feature = "uuid"))]
932 #[test]
933 fn principal_serde_struct_roundtrip_human() {
934 let p = Principal::human(Uuid::nil());
935 let json = serde_json::to_value(&p).unwrap();
936 let back: Principal = serde_json::from_value(json).unwrap();
937 assert_eq!(back, p);
938 }
939
940 #[cfg(feature = "serde")]
941 #[test]
942 fn principal_serde_struct_roundtrip_system() {
943 let p = Principal::system("billing.rotation-engine");
944 let json = serde_json::to_value(&p).unwrap();
945 let back: Principal = serde_json::from_value(json).unwrap();
946 assert_eq!(back, p);
947 }
948
949 #[cfg(all(feature = "serde", feature = "uuid"))]
950 #[test]
951 fn principal_serde_includes_org_path() {
952 let p = Principal::system("test");
953 let json = serde_json::to_value(&p).unwrap();
954 assert!(json.get("org_path").is_some());
955 }
956
957 #[cfg(all(feature = "chrono", feature = "uuid"))]
960 #[test]
961 fn now_sets_created_at_and_updated_at() {
962 let actor = Principal::human(Uuid::nil());
963 let before = chrono::Utc::now();
964 let info = AuditInfo::now(actor.clone());
965 let after = chrono::Utc::now();
966
967 assert!(info.created_at >= before && info.created_at <= after);
968 assert!(info.updated_at >= before && info.updated_at <= after);
969 assert_eq!(info.created_by, actor);
970 assert_eq!(info.updated_by, actor);
971 }
972
973 #[cfg(all(feature = "chrono", feature = "serde"))]
974 #[test]
975 fn now_with_system_principal() {
976 let info = AuditInfo::now(Principal::system("billing.rotation-engine"));
977 let json = serde_json::to_value(&info).unwrap();
978 let back: AuditInfo = serde_json::from_value(json).unwrap();
979 assert_eq!(back, info);
980 }
981
982 #[cfg(all(feature = "chrono", feature = "uuid"))]
983 #[test]
984 fn touch_updates_updated_at_and_updated_by() {
985 let mut info = AuditInfo::now(Principal::human(Uuid::nil()));
986 let engine = Principal::system("billing.rotation-engine");
987 let before_touch = chrono::Utc::now();
988 info.touch(engine.clone());
989 let after_touch = chrono::Utc::now();
990
991 assert!(info.updated_at >= before_touch && info.updated_at <= after_touch);
992 assert_eq!(info.updated_by, engine);
993 }
994
995 #[cfg(all(feature = "chrono", feature = "uuid"))]
996 #[test]
997 fn new_constructor() {
998 let now = chrono::Utc::now();
999 let actor = Principal::human(Uuid::nil());
1000 let engine = Principal::system("billing.rotation-engine");
1001 let info = AuditInfo::new(now, now, actor.clone(), engine.clone());
1002 assert_eq!(info.created_at, now);
1003 assert_eq!(info.updated_at, now);
1004 assert_eq!(info.created_by, actor);
1005 assert_eq!(info.updated_by, engine);
1006 }
1007
1008 #[cfg(all(feature = "chrono", feature = "serde"))]
1009 #[test]
1010 fn serde_round_trip_with_system_actor() {
1011 let info = AuditInfo::now(Principal::system("orders.bootstrap"));
1012 let json = serde_json::to_value(&info).unwrap();
1013 let back: AuditInfo = serde_json::from_value(json).unwrap();
1014 assert_eq!(back, info);
1015 }
1016
1017 #[cfg(all(feature = "chrono", feature = "serde"))]
1018 #[test]
1019 fn serde_actor_fields_are_always_present() {
1020 let info = AuditInfo::now(Principal::system("orders.bootstrap"));
1021 let json = serde_json::to_value(&info).unwrap();
1022 assert!(
1023 json.get("created_by").is_some(),
1024 "created_by must always serialize"
1025 );
1026 assert!(
1027 json.get("updated_by").is_some(),
1028 "updated_by must always serialize"
1029 );
1030 assert_eq!(
1032 json["created_by"]["id"],
1033 serde_json::json!("orders.bootstrap")
1034 );
1035 }
1036
1037 #[test]
1040 fn principal_parse_error_display_contains_input() {
1041 let err = PrincipalParseError {
1042 input: "bad-value".to_owned(),
1043 };
1044 assert!(err.to_string().contains("bad-value"));
1045 }
1046
1047 #[cfg(feature = "std")]
1048 #[test]
1049 fn principal_parse_error_is_std_error() {
1050 let err = PrincipalParseError {
1051 input: "x".to_owned(),
1052 };
1053 let _: &dyn std::error::Error = &err;
1054 }
1055
1056 #[cfg(feature = "uuid")]
1059 #[test]
1060 fn resolved_principal_new_and_display_with_name() {
1061 let p = Principal::human(Uuid::nil());
1062 let r = ResolvedPrincipal::new(p, Some("Alice Martin".to_owned()));
1063 assert_eq!(r.display(), "Alice Martin");
1064 }
1065
1066 #[cfg(feature = "uuid")]
1067 #[test]
1068 fn resolved_principal_display_falls_back_to_uuid() {
1069 let p = Principal::human(Uuid::nil());
1070 let r = ResolvedPrincipal::new(p.clone(), None);
1071 assert_eq!(r.display(), p.as_str());
1072 }
1073
1074 #[cfg(feature = "uuid")]
1075 #[test]
1076 fn resolved_principal_from_principal() {
1077 let p = Principal::human(Uuid::nil());
1078 let r = ResolvedPrincipal::from(p.clone());
1079 assert_eq!(r.id, p);
1080 assert!(r.display_name.is_none());
1081 }
1082
1083 #[cfg(all(feature = "uuid", feature = "serde"))]
1084 #[test]
1085 fn resolved_principal_serde_omits_none_display_name() {
1086 let p = Principal::human(Uuid::nil());
1087 let r = ResolvedPrincipal::from(p);
1088 let json = serde_json::to_value(&r).unwrap();
1089 assert!(
1090 json.get("display_name").is_none(),
1091 "display_name must be absent when None"
1092 );
1093 }
1094
1095 #[cfg(all(feature = "uuid", feature = "serde"))]
1096 #[test]
1097 fn resolved_principal_serde_includes_display_name_when_set() {
1098 let p = Principal::human(Uuid::nil());
1099 let r = ResolvedPrincipal::new(p, Some("Bob".to_owned()));
1100 let json = serde_json::to_value(&r).unwrap();
1101 assert_eq!(json["display_name"], serde_json::json!("Bob"));
1102 }
1103}