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 Device,
105 Agent,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct PrincipalParseError {
124 pub input: String,
126}
127
128impl core::fmt::Display for PrincipalParseError {
129 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
130 write!(
131 f,
132 "invalid Principal: expected a UUID string, got {:?}",
133 self.input
134 )
135 }
136}
137
138#[cfg(feature = "std")]
139impl std::error::Error for PrincipalParseError {}
140
141#[derive(Clone, PartialEq, Eq, Hash, Debug)]
190#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
191#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
192#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
193pub struct Principal {
194 pub id: PrincipalId,
196 pub kind: PrincipalKind,
198 #[cfg(feature = "uuid")]
201 #[cfg_attr(feature = "serde", serde(default))]
202 pub org_path: Vec<OrgId>,
203}
204
205impl Principal {
206 #[cfg(feature = "uuid")]
226 #[must_use]
227 pub fn human(uuid: Uuid) -> Self {
228 Self {
229 id: PrincipalId::from_uuid(uuid),
230 kind: PrincipalKind::User,
231 #[cfg(feature = "uuid")]
232 org_path: Vec::new(),
233 }
234 }
235
236 #[cfg(feature = "uuid")]
259 pub fn try_parse(s: &str) -> Result<Self, PrincipalParseError> {
260 Uuid::parse_str(s)
261 .map(Self::human)
262 .map_err(|_| PrincipalParseError {
263 input: s.to_owned(),
264 })
265 }
266
267 #[cfg(feature = "uuid")]
286 #[must_use]
287 pub fn device(uuid: Uuid) -> Self {
288 Self {
289 id: PrincipalId::from_uuid(uuid),
290 kind: PrincipalKind::Device,
291 #[cfg(feature = "uuid")]
292 org_path: Vec::new(),
293 }
294 }
295
296 #[must_use]
313 pub fn agent(id: &'static str) -> Self {
314 Self {
315 id: PrincipalId::static_str(id),
316 kind: PrincipalKind::Agent,
317 #[cfg(feature = "uuid")]
318 org_path: Vec::new(),
319 }
320 }
321
322 #[must_use]
335 pub fn system(id: &'static str) -> Self {
336 Self {
337 id: PrincipalId::static_str(id),
338 kind: PrincipalKind::System,
339 #[cfg(feature = "uuid")]
340 org_path: Vec::new(),
341 }
342 }
343
344 #[must_use]
354 pub fn as_str(&self) -> &str {
355 self.id.as_str()
356 }
357
358 #[cfg(feature = "uuid")]
373 #[must_use]
374 pub fn with_org_path(mut self, org_path: Vec<OrgId>) -> Self {
375 self.org_path = org_path;
376 self
377 }
378
379 #[cfg(feature = "uuid")]
394 #[must_use]
395 pub fn org_path_display(&self) -> String {
396 self.org_path
397 .iter()
398 .map(ToString::to_string)
399 .collect::<Vec<_>>()
400 .join(",")
401 }
402}
403
404impl core::fmt::Display for Principal {
405 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
406 f.write_str(self.as_str())
407 }
408}
409
410#[cfg(all(feature = "arbitrary", feature = "uuid"))]
413impl<'a> arbitrary::Arbitrary<'a> for Principal {
414 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
415 let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
416 Ok(Self::human(Uuid::from_bytes(bytes)))
417 }
418}
419
420#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
424impl<'a> arbitrary::Arbitrary<'a> for Principal {
425 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
426 let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
427 Ok(Self {
428 id: PrincipalId::from_owned(s),
429 kind: PrincipalKind::System,
430 #[cfg(feature = "uuid")]
431 org_path: Vec::new(),
432 })
433 }
434}
435
436#[cfg(all(feature = "arbitrary", feature = "uuid"))]
438impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
439 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
440 let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
441 Ok(Self::from_uuid(Uuid::from_bytes(bytes)))
442 }
443}
444
445#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
446impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
447 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
448 let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
449 Ok(Self::from_owned(s))
450 }
451}
452
453#[cfg(feature = "arbitrary")]
455impl<'a> arbitrary::Arbitrary<'a> for PrincipalKind {
456 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
457 match <u8 as arbitrary::Arbitrary>::arbitrary(u)? % 5 {
458 0 => Ok(Self::User),
459 1 => Ok(Self::Service),
460 2 => Ok(Self::System),
461 3 => Ok(Self::Device),
462 _ => Ok(Self::Agent),
463 }
464 }
465}
466
467#[cfg(all(feature = "proptest", feature = "uuid"))]
470impl proptest::arbitrary::Arbitrary for Principal {
471 type Parameters = ();
472 type Strategy = proptest::strategy::BoxedStrategy<Self>;
473
474 fn arbitrary_with((): ()) -> Self::Strategy {
475 use proptest::prelude::*;
476 any::<[u8; 16]>()
477 .prop_map(|b| Self::human(Uuid::from_bytes(b)))
478 .boxed()
479 }
480}
481
482#[cfg(all(feature = "proptest", not(feature = "uuid")))]
484impl proptest::arbitrary::Arbitrary for Principal {
485 type Parameters = ();
486 type Strategy = proptest::strategy::BoxedStrategy<Self>;
487
488 fn arbitrary_with((): ()) -> Self::Strategy {
489 use proptest::prelude::*;
490 any::<String>()
491 .prop_map(|s| Self {
492 id: PrincipalId::from_owned(s),
493 kind: PrincipalKind::System,
494 #[cfg(feature = "uuid")]
495 org_path: Vec::new(),
496 })
497 .boxed()
498 }
499}
500
501#[cfg(all(feature = "proptest", feature = "uuid"))]
503impl proptest::arbitrary::Arbitrary for PrincipalId {
504 type Parameters = ();
505 type Strategy = proptest::strategy::BoxedStrategy<Self>;
506
507 fn arbitrary_with((): ()) -> Self::Strategy {
508 use proptest::prelude::*;
509 any::<[u8; 16]>()
510 .prop_map(|b| Self::from_uuid(Uuid::from_bytes(b)))
511 .boxed()
512 }
513}
514
515#[cfg(all(feature = "proptest", not(feature = "uuid")))]
516impl proptest::arbitrary::Arbitrary for PrincipalId {
517 type Parameters = ();
518 type Strategy = proptest::strategy::BoxedStrategy<Self>;
519
520 fn arbitrary_with((): ()) -> Self::Strategy {
521 use proptest::prelude::*;
522 any::<String>().prop_map(|s| Self::from_owned(s)).boxed()
523 }
524}
525
526#[cfg(feature = "proptest")]
528impl proptest::arbitrary::Arbitrary for PrincipalKind {
529 type Parameters = ();
530 type Strategy = proptest::strategy::BoxedStrategy<Self>;
531
532 fn arbitrary_with((): ()) -> Self::Strategy {
533 use proptest::prelude::*;
534 prop_oneof![
535 Just(Self::User),
536 Just(Self::Service),
537 Just(Self::System),
538 Just(Self::Device),
539 Just(Self::Agent),
540 ]
541 .boxed()
542 }
543}
544
545#[derive(Debug, Clone, PartialEq, Eq)]
569#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
570#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
571#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
572#[cfg_attr(
574 all(feature = "arbitrary", not(feature = "chrono")),
575 derive(arbitrary::Arbitrary)
576)]
577#[cfg_attr(
578 all(feature = "proptest", not(feature = "chrono")),
579 derive(proptest_derive::Arbitrary)
580)]
581pub struct AuditInfo {
582 #[cfg_attr(
584 feature = "utoipa",
585 schema(value_type = String, format = DateTime)
586 )]
587 pub created_at: Timestamp,
588 #[cfg_attr(
590 feature = "utoipa",
591 schema(value_type = String, format = DateTime)
592 )]
593 pub updated_at: Timestamp,
594 pub created_by: Principal,
596 pub updated_by: Principal,
598}
599
600impl AuditInfo {
601 #[must_use]
617 pub fn new(
618 created_at: Timestamp,
619 updated_at: Timestamp,
620 created_by: Principal,
621 updated_by: Principal,
622 ) -> Self {
623 Self {
624 created_at,
625 updated_at,
626 created_by,
627 updated_by,
628 }
629 }
630
631 #[cfg(feature = "chrono")]
651 #[must_use]
652 pub fn now(created_by: Principal) -> Self {
653 let now = chrono::Utc::now();
654 let updated_by = created_by.clone();
655 Self {
656 created_at: now,
657 updated_at: now,
658 created_by,
659 updated_by,
660 }
661 }
662
663 #[cfg(feature = "chrono")]
680 pub fn touch(&mut self, updated_by: Principal) {
681 self.updated_at = chrono::Utc::now();
682 self.updated_by = updated_by;
683 }
684}
685
686#[derive(Debug, Clone, PartialEq, Eq)]
713#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
714pub struct ResolvedPrincipal {
715 pub id: Principal,
717 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
721 pub display_name: Option<String>,
722}
723
724impl ResolvedPrincipal {
725 #[must_use]
727 pub fn new(id: Principal, display_name: Option<String>) -> Self {
728 Self { id, display_name }
729 }
730
731 #[must_use]
734 pub fn display(&self) -> &str {
735 self.display_name
736 .as_deref()
737 .unwrap_or_else(|| self.id.as_str())
738 }
739}
740
741impl From<Principal> for ResolvedPrincipal {
742 fn from(id: Principal) -> Self {
743 Self {
744 id,
745 display_name: None,
746 }
747 }
748}
749
750#[cfg(all(feature = "arbitrary", feature = "chrono"))]
757impl<'a> arbitrary::Arbitrary<'a> for AuditInfo {
758 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
759 let created_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
761 let updated_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
762 let created_at = chrono::DateTime::from_timestamp(created_secs.abs(), 0)
763 .unwrap_or_else(chrono::Utc::now);
764 let updated_at = chrono::DateTime::from_timestamp(updated_secs.abs(), 0)
765 .unwrap_or_else(chrono::Utc::now);
766 Ok(Self {
767 created_at,
768 updated_at,
769 created_by: Principal::arbitrary(u)?,
770 updated_by: Principal::arbitrary(u)?,
771 })
772 }
773}
774
775#[cfg(all(feature = "proptest", feature = "chrono"))]
776impl proptest::arbitrary::Arbitrary for AuditInfo {
777 type Parameters = ();
778 type Strategy = proptest::strategy::BoxedStrategy<Self>;
779
780 fn arbitrary_with((): ()) -> Self::Strategy {
781 use proptest::prelude::*;
782 (
783 0i64..=32_503_680_000i64,
784 0i64..=32_503_680_000i64,
785 any::<Principal>(),
786 any::<Principal>(),
787 )
788 .prop_map(|(cs, us, cb, ub)| Self {
789 created_at: chrono::DateTime::from_timestamp(cs, 0)
790 .unwrap_or_else(chrono::Utc::now),
791 updated_at: chrono::DateTime::from_timestamp(us, 0)
792 .unwrap_or_else(chrono::Utc::now),
793 created_by: cb,
794 updated_by: ub,
795 })
796 .boxed()
797 }
798}
799
800#[cfg(test)]
805mod tests {
806 use super::*;
807 #[cfg(feature = "uuid")]
808 use uuid::Uuid;
809
810 #[test]
813 fn principal_id_static_str() {
814 let id = PrincipalId::static_str("foo");
815 assert_eq!(id.as_str(), "foo");
816 }
817
818 #[test]
819 fn principal_id_from_owned() {
820 let id = PrincipalId::from_owned("bar".to_owned());
821 assert_eq!(id.as_str(), "bar");
822 }
823
824 #[cfg(feature = "uuid")]
825 #[test]
826 fn principal_id_from_uuid() {
827 let id = PrincipalId::from_uuid(Uuid::nil());
828 assert_eq!(id.as_str(), "00000000-0000-0000-0000-000000000000");
829 }
830
831 #[test]
832 fn principal_id_display() {
833 let id = PrincipalId::static_str("test");
834 assert_eq!(format!("{id}"), "test");
835 }
836
837 #[cfg(feature = "serde")]
838 #[test]
839 fn principal_id_serde_transparent() {
840 let id = PrincipalId::static_str("myid");
841 let json = serde_json::to_value(&id).unwrap();
842 assert_eq!(json, serde_json::json!("myid"));
843 let back: PrincipalId = serde_json::from_value(json).unwrap();
844 assert_eq!(back, id);
845 }
846
847 #[test]
850 fn principal_kind_copy_and_eq() {
851 let k1 = PrincipalKind::User;
852 let k2 = k1;
853 assert_eq!(k1, k2);
854 }
855
856 #[test]
857 fn principal_kind_all_variants() {
858 let _ = PrincipalKind::User;
859 let _ = PrincipalKind::Service;
860 let _ = PrincipalKind::System;
861 let _ = PrincipalKind::Device;
862 let _ = PrincipalKind::Agent;
863 }
864
865 #[cfg(feature = "uuid")]
868 #[test]
869 fn principal_human_has_user_kind() {
870 let p = Principal::human(Uuid::nil());
871 assert_eq!(p.kind, PrincipalKind::User);
872 }
873
874 #[cfg(feature = "uuid")]
875 #[test]
876 fn principal_human_has_empty_org_path() {
877 let p = Principal::human(Uuid::nil());
878 assert!(p.org_path.is_empty());
879 }
880
881 #[test]
882 fn principal_system_has_system_kind() {
883 let p = Principal::system("s");
884 assert_eq!(p.kind, PrincipalKind::System);
885 }
886
887 #[cfg(feature = "uuid")]
888 #[test]
889 fn principal_system_has_empty_org_path() {
890 let p = Principal::system("s");
891 assert!(p.org_path.is_empty());
892 }
893
894 #[cfg(feature = "uuid")]
895 #[test]
896 fn principal_device_has_device_kind() {
897 let p = Principal::device(Uuid::nil());
898 assert_eq!(p.kind, PrincipalKind::Device);
899 }
900
901 #[cfg(feature = "uuid")]
902 #[test]
903 fn principal_device_id_is_uuid_string() {
904 let id = Uuid::new_v4();
905 let p = Principal::device(id);
906 assert_eq!(p.as_str(), id.to_string());
907 }
908
909 #[cfg(feature = "uuid")]
910 #[test]
911 fn principal_device_has_empty_org_path() {
912 let p = Principal::device(Uuid::nil());
913 assert!(p.org_path.is_empty());
914 }
915
916 #[test]
917 fn principal_agent_has_agent_kind() {
918 let p = Principal::agent("ops.triage-agent");
919 assert_eq!(p.kind, PrincipalKind::Agent);
920 }
921
922 #[test]
923 fn principal_agent_preserves_static_id() {
924 let p = Principal::agent("sdr.outreach-bot");
925 assert_eq!(p.as_str(), "sdr.outreach-bot");
926 }
927
928 #[cfg(feature = "uuid")]
929 #[test]
930 fn principal_agent_has_empty_org_path() {
931 let p = Principal::agent("svc");
932 assert!(p.org_path.is_empty());
933 }
934
935 #[cfg(feature = "uuid")]
936 #[test]
937 fn principal_with_org_path_builder() {
938 let org_id = crate::org_id::OrgId::generate();
939 let p = Principal::system("test").with_org_path(vec![org_id]);
940 assert_eq!(p.org_path.len(), 1);
941 assert_eq!(p.org_path[0], org_id);
942 }
943
944 #[cfg(feature = "uuid")]
945 #[test]
946 fn org_path_display_empty_for_system_principal() {
947 let p = Principal::system("svc");
948 assert_eq!(p.org_path_display(), "");
949 }
950
951 #[cfg(feature = "uuid")]
952 #[test]
953 fn org_path_display_single_org() {
954 let org_id = crate::org_id::OrgId::generate();
955 let p = Principal::system("svc").with_org_path(vec![org_id]);
956 assert_eq!(p.org_path_display(), org_id.to_string());
957 }
958
959 #[cfg(feature = "uuid")]
960 #[test]
961 fn org_path_display_multiple_orgs_comma_separated() {
962 let root = crate::org_id::OrgId::generate();
963 let child = crate::org_id::OrgId::generate();
964 let p = Principal::system("svc").with_org_path(vec![root, child]);
965 assert_eq!(p.org_path_display(), format!("{root},{child}"));
966 }
967
968 #[cfg(feature = "uuid")]
969 #[test]
970 fn principal_try_parse_accepts_valid_uuid() {
971 let s = "550e8400-e29b-41d4-a716-446655440000";
972 let p = Principal::try_parse(s).expect("valid UUID should parse");
973 assert_eq!(p.as_str(), s);
974 }
975
976 #[cfg(feature = "uuid")]
977 #[test]
978 fn principal_try_parse_sets_user_kind() {
979 let p = Principal::try_parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
980 assert_eq!(p.kind, PrincipalKind::User);
981 }
982
983 #[cfg(feature = "uuid")]
984 #[test]
985 fn principal_try_parse_rejects_email_string() {
986 let err = Principal::try_parse("alice@example.com").expect_err("email must be rejected");
987 assert_eq!(err.input, "alice@example.com");
988 assert!(err.to_string().contains("alice@example.com"));
989 }
990
991 #[cfg(feature = "uuid")]
992 #[test]
993 fn principal_try_parse_rejects_empty_string() {
994 let err = Principal::try_parse("").expect_err("empty string must be rejected");
995 assert_eq!(err.input, "");
996 }
997
998 #[test]
999 fn principal_as_str_returns_id_str() {
1000 let p = Principal::system("x");
1001 assert_eq!(p.as_str(), "x");
1002 }
1003
1004 #[cfg(feature = "uuid")]
1005 #[test]
1006 fn principal_display_forwards_to_as_str() {
1007 let p = Principal::human(Uuid::nil());
1008 let s = format!("{p}");
1009 assert_eq!(s, Uuid::nil().to_string());
1010 }
1011
1012 #[cfg(feature = "uuid")]
1013 #[test]
1014 fn principal_debug_is_not_redacted() {
1015 let p = Principal::human(Uuid::nil());
1016 let s = format!("{p:?}");
1017 assert!(
1018 s.contains(&Uuid::nil().to_string()),
1019 "debug must not redact: {s}"
1020 );
1021 assert!(s.contains("Principal"), "debug must name the type: {s}");
1022 }
1023
1024 #[test]
1025 fn principal_equality_and_hash_across_owned_and_borrowed() {
1026 use std::collections::hash_map::DefaultHasher;
1027 use std::hash::{Hash, Hasher};
1028
1029 let p1 = Principal::system("orders.bootstrap");
1030 let p2 = Principal::system("orders.bootstrap");
1031 assert_eq!(p1, p2);
1032
1033 let mut h1 = DefaultHasher::new();
1034 p1.hash(&mut h1);
1035 let mut h2 = DefaultHasher::new();
1036 p2.hash(&mut h2);
1037 assert_eq!(h1.finish(), h2.finish());
1038 }
1039
1040 #[cfg(feature = "uuid")]
1041 #[test]
1042 fn principal_clone_roundtrip() {
1043 let p = Principal::human(Uuid::nil());
1044 let q = p.clone();
1045 assert_eq!(p, q);
1046 }
1047
1048 #[cfg(all(feature = "serde", feature = "uuid"))]
1049 #[test]
1050 fn principal_serde_struct_roundtrip_human() {
1051 let p = Principal::human(Uuid::nil());
1052 let json = serde_json::to_value(&p).unwrap();
1053 let back: Principal = serde_json::from_value(json).unwrap();
1054 assert_eq!(back, p);
1055 }
1056
1057 #[cfg(feature = "serde")]
1058 #[test]
1059 fn principal_serde_struct_roundtrip_system() {
1060 let p = Principal::system("billing.rotation-engine");
1061 let json = serde_json::to_value(&p).unwrap();
1062 let back: Principal = serde_json::from_value(json).unwrap();
1063 assert_eq!(back, p);
1064 }
1065
1066 #[cfg(all(feature = "serde", feature = "uuid"))]
1067 #[test]
1068 fn principal_serde_includes_org_path() {
1069 let p = Principal::system("test");
1070 let json = serde_json::to_value(&p).unwrap();
1071 assert!(json.get("org_path").is_some());
1072 }
1073
1074 #[cfg(all(feature = "chrono", feature = "uuid"))]
1077 #[test]
1078 fn now_sets_created_at_and_updated_at() {
1079 let actor = Principal::human(Uuid::nil());
1080 let before = chrono::Utc::now();
1081 let info = AuditInfo::now(actor.clone());
1082 let after = chrono::Utc::now();
1083
1084 assert!(info.created_at >= before && info.created_at <= after);
1085 assert!(info.updated_at >= before && info.updated_at <= after);
1086 assert_eq!(info.created_by, actor);
1087 assert_eq!(info.updated_by, actor);
1088 }
1089
1090 #[cfg(all(feature = "chrono", feature = "serde"))]
1091 #[test]
1092 fn now_with_system_principal() {
1093 let info = AuditInfo::now(Principal::system("billing.rotation-engine"));
1094 let json = serde_json::to_value(&info).unwrap();
1095 let back: AuditInfo = serde_json::from_value(json).unwrap();
1096 assert_eq!(back, info);
1097 }
1098
1099 #[cfg(all(feature = "chrono", feature = "uuid"))]
1100 #[test]
1101 fn touch_updates_updated_at_and_updated_by() {
1102 let mut info = AuditInfo::now(Principal::human(Uuid::nil()));
1103 let engine = Principal::system("billing.rotation-engine");
1104 let before_touch = chrono::Utc::now();
1105 info.touch(engine.clone());
1106 let after_touch = chrono::Utc::now();
1107
1108 assert!(info.updated_at >= before_touch && info.updated_at <= after_touch);
1109 assert_eq!(info.updated_by, engine);
1110 }
1111
1112 #[cfg(all(feature = "chrono", feature = "uuid"))]
1113 #[test]
1114 fn new_constructor() {
1115 let now = chrono::Utc::now();
1116 let actor = Principal::human(Uuid::nil());
1117 let engine = Principal::system("billing.rotation-engine");
1118 let info = AuditInfo::new(now, now, actor.clone(), engine.clone());
1119 assert_eq!(info.created_at, now);
1120 assert_eq!(info.updated_at, now);
1121 assert_eq!(info.created_by, actor);
1122 assert_eq!(info.updated_by, engine);
1123 }
1124
1125 #[cfg(all(feature = "chrono", feature = "serde"))]
1126 #[test]
1127 fn serde_round_trip_with_system_actor() {
1128 let info = AuditInfo::now(Principal::system("orders.bootstrap"));
1129 let json = serde_json::to_value(&info).unwrap();
1130 let back: AuditInfo = serde_json::from_value(json).unwrap();
1131 assert_eq!(back, info);
1132 }
1133
1134 #[cfg(all(feature = "chrono", feature = "serde"))]
1135 #[test]
1136 fn serde_actor_fields_are_always_present() {
1137 let info = AuditInfo::now(Principal::system("orders.bootstrap"));
1138 let json = serde_json::to_value(&info).unwrap();
1139 assert!(
1140 json.get("created_by").is_some(),
1141 "created_by must always serialize"
1142 );
1143 assert!(
1144 json.get("updated_by").is_some(),
1145 "updated_by must always serialize"
1146 );
1147 assert_eq!(
1149 json["created_by"]["id"],
1150 serde_json::json!("orders.bootstrap")
1151 );
1152 }
1153
1154 #[test]
1157 fn principal_parse_error_display_contains_input() {
1158 let err = PrincipalParseError {
1159 input: "bad-value".to_owned(),
1160 };
1161 assert!(err.to_string().contains("bad-value"));
1162 }
1163
1164 #[cfg(feature = "std")]
1165 #[test]
1166 fn principal_parse_error_is_std_error() {
1167 let err = PrincipalParseError {
1168 input: "x".to_owned(),
1169 };
1170 let _: &dyn std::error::Error = &err;
1171 }
1172
1173 #[cfg(feature = "uuid")]
1176 #[test]
1177 fn resolved_principal_new_and_display_with_name() {
1178 let p = Principal::human(Uuid::nil());
1179 let r = ResolvedPrincipal::new(p, Some("Alice Martin".to_owned()));
1180 assert_eq!(r.display(), "Alice Martin");
1181 }
1182
1183 #[cfg(feature = "uuid")]
1184 #[test]
1185 fn resolved_principal_display_falls_back_to_uuid() {
1186 let p = Principal::human(Uuid::nil());
1187 let r = ResolvedPrincipal::new(p.clone(), None);
1188 assert_eq!(r.display(), p.as_str());
1189 }
1190
1191 #[cfg(feature = "uuid")]
1192 #[test]
1193 fn resolved_principal_from_principal() {
1194 let p = Principal::human(Uuid::nil());
1195 let r = ResolvedPrincipal::from(p.clone());
1196 assert_eq!(r.id, p);
1197 assert!(r.display_name.is_none());
1198 }
1199
1200 #[cfg(all(feature = "uuid", feature = "serde"))]
1201 #[test]
1202 fn resolved_principal_serde_omits_none_display_name() {
1203 let p = Principal::human(Uuid::nil());
1204 let r = ResolvedPrincipal::from(p);
1205 let json = serde_json::to_value(&r).unwrap();
1206 assert!(
1207 json.get("display_name").is_none(),
1208 "display_name must be absent when None"
1209 );
1210 }
1211
1212 #[cfg(all(feature = "uuid", feature = "serde"))]
1213 #[test]
1214 fn resolved_principal_serde_includes_display_name_when_set() {
1215 let p = Principal::human(Uuid::nil());
1216 let r = ResolvedPrincipal::new(p, Some("Bob".to_owned()));
1217 let json = serde_json::to_value(&r).unwrap();
1218 assert_eq!(json["display_name"], serde_json::json!("Bob"));
1219 }
1220}