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(Clone, Copy, PartialEq, Eq, Hash, Debug)]
122#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
123#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
124#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
125pub enum DeviceLeaseKind {
126 Connection,
130 RequestStream,
134}
135
136#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
150#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
151#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
153pub struct DeviceLease {
154 pub kind: DeviceLeaseKind,
157 pub max_concurrent: Option<u32>,
161 pub refresh_secs: u32,
164}
165
166impl DeviceLease {
167 pub const MAX_REFRESH_SECS: u32 = 3_600;
171
172 #[must_use]
189 pub fn new(kind: DeviceLeaseKind, max_concurrent: Option<u32>, refresh_secs: u32) -> Self {
190 Self {
191 kind,
192 max_concurrent,
193 refresh_secs: refresh_secs.min(Self::MAX_REFRESH_SECS),
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct PrincipalParseError {
210 pub input: String,
212}
213
214impl core::fmt::Display for PrincipalParseError {
215 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
216 write!(
217 f,
218 "invalid Principal: expected a UUID string, got {:?}",
219 self.input
220 )
221 }
222}
223
224#[cfg(feature = "std")]
225impl std::error::Error for PrincipalParseError {}
226
227#[derive(Clone, PartialEq, Eq, Hash, Debug)]
276#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
277#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
278#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
279pub struct Principal {
280 pub id: PrincipalId,
282 pub kind: PrincipalKind,
284 #[cfg(feature = "uuid")]
287 #[cfg_attr(feature = "serde", serde(default))]
288 pub org_path: Vec<OrgId>,
289}
290
291impl Principal {
292 #[cfg(feature = "uuid")]
312 #[must_use]
313 pub fn human(uuid: Uuid) -> Self {
314 Self {
315 id: PrincipalId::from_uuid(uuid),
316 kind: PrincipalKind::User,
317 #[cfg(feature = "uuid")]
318 org_path: Vec::new(),
319 }
320 }
321
322 #[cfg(feature = "uuid")]
345 pub fn try_parse(s: &str) -> Result<Self, PrincipalParseError> {
346 Uuid::parse_str(s)
347 .map(Self::human)
348 .map_err(|_| PrincipalParseError {
349 input: s.to_owned(),
350 })
351 }
352
353 #[cfg(feature = "uuid")]
372 #[must_use]
373 pub fn device(uuid: Uuid) -> Self {
374 Self {
375 id: PrincipalId::from_uuid(uuid),
376 kind: PrincipalKind::Device,
377 #[cfg(feature = "uuid")]
378 org_path: Vec::new(),
379 }
380 }
381
382 #[must_use]
399 pub fn agent(id: &'static str) -> Self {
400 Self {
401 id: PrincipalId::static_str(id),
402 kind: PrincipalKind::Agent,
403 #[cfg(feature = "uuid")]
404 org_path: Vec::new(),
405 }
406 }
407
408 #[must_use]
421 pub fn system(id: &'static str) -> Self {
422 Self {
423 id: PrincipalId::static_str(id),
424 kind: PrincipalKind::System,
425 #[cfg(feature = "uuid")]
426 org_path: Vec::new(),
427 }
428 }
429
430 #[must_use]
440 pub fn as_str(&self) -> &str {
441 self.id.as_str()
442 }
443
444 #[cfg(feature = "uuid")]
459 #[must_use]
460 pub fn with_org_path(mut self, org_path: Vec<OrgId>) -> Self {
461 self.org_path = org_path;
462 self
463 }
464
465 #[cfg(feature = "uuid")]
480 #[must_use]
481 pub fn org_path_display(&self) -> String {
482 self.org_path
483 .iter()
484 .map(ToString::to_string)
485 .collect::<Vec<_>>()
486 .join(",")
487 }
488}
489
490impl core::fmt::Display for Principal {
491 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
492 f.write_str(self.as_str())
493 }
494}
495
496#[cfg(all(feature = "arbitrary", feature = "uuid"))]
499impl<'a> arbitrary::Arbitrary<'a> for Principal {
500 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
501 let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
502 Ok(Self::human(Uuid::from_bytes(bytes)))
503 }
504}
505
506#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
510impl<'a> arbitrary::Arbitrary<'a> for Principal {
511 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
512 let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
513 Ok(Self {
514 id: PrincipalId::from_owned(s),
515 kind: PrincipalKind::System,
516 #[cfg(feature = "uuid")]
517 org_path: Vec::new(),
518 })
519 }
520}
521
522#[cfg(all(feature = "arbitrary", feature = "uuid"))]
524impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
525 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
526 let bytes = <[u8; 16] as arbitrary::Arbitrary>::arbitrary(u)?;
527 Ok(Self::from_uuid(Uuid::from_bytes(bytes)))
528 }
529}
530
531#[cfg(all(feature = "arbitrary", not(feature = "uuid")))]
532impl<'a> arbitrary::Arbitrary<'a> for PrincipalId {
533 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
534 let s = <String as arbitrary::Arbitrary>::arbitrary(u)?;
535 Ok(Self::from_owned(s))
536 }
537}
538
539#[cfg(feature = "arbitrary")]
541impl<'a> arbitrary::Arbitrary<'a> for PrincipalKind {
542 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
543 match <u8 as arbitrary::Arbitrary>::arbitrary(u)? % 5 {
544 0 => Ok(Self::User),
545 1 => Ok(Self::Service),
546 2 => Ok(Self::System),
547 3 => Ok(Self::Device),
548 _ => Ok(Self::Agent),
549 }
550 }
551}
552
553#[cfg(all(feature = "proptest", feature = "uuid"))]
556impl proptest::arbitrary::Arbitrary for Principal {
557 type Parameters = ();
558 type Strategy = proptest::strategy::BoxedStrategy<Self>;
559
560 fn arbitrary_with((): ()) -> Self::Strategy {
561 use proptest::prelude::*;
562 any::<[u8; 16]>()
563 .prop_map(|b| Self::human(Uuid::from_bytes(b)))
564 .boxed()
565 }
566}
567
568#[cfg(all(feature = "proptest", not(feature = "uuid")))]
570impl proptest::arbitrary::Arbitrary for Principal {
571 type Parameters = ();
572 type Strategy = proptest::strategy::BoxedStrategy<Self>;
573
574 fn arbitrary_with((): ()) -> Self::Strategy {
575 use proptest::prelude::*;
576 any::<String>()
577 .prop_map(|s| Self {
578 id: PrincipalId::from_owned(s),
579 kind: PrincipalKind::System,
580 #[cfg(feature = "uuid")]
581 org_path: Vec::new(),
582 })
583 .boxed()
584 }
585}
586
587#[cfg(all(feature = "proptest", feature = "uuid"))]
589impl proptest::arbitrary::Arbitrary for PrincipalId {
590 type Parameters = ();
591 type Strategy = proptest::strategy::BoxedStrategy<Self>;
592
593 fn arbitrary_with((): ()) -> Self::Strategy {
594 use proptest::prelude::*;
595 any::<[u8; 16]>()
596 .prop_map(|b| Self::from_uuid(Uuid::from_bytes(b)))
597 .boxed()
598 }
599}
600
601#[cfg(all(feature = "proptest", not(feature = "uuid")))]
602impl proptest::arbitrary::Arbitrary for PrincipalId {
603 type Parameters = ();
604 type Strategy = proptest::strategy::BoxedStrategy<Self>;
605
606 fn arbitrary_with((): ()) -> Self::Strategy {
607 use proptest::prelude::*;
608 any::<String>().prop_map(|s| Self::from_owned(s)).boxed()
609 }
610}
611
612#[cfg(feature = "proptest")]
614impl proptest::arbitrary::Arbitrary for PrincipalKind {
615 type Parameters = ();
616 type Strategy = proptest::strategy::BoxedStrategy<Self>;
617
618 fn arbitrary_with((): ()) -> Self::Strategy {
619 use proptest::prelude::*;
620 prop_oneof![
621 Just(Self::User),
622 Just(Self::Service),
623 Just(Self::System),
624 Just(Self::Device),
625 Just(Self::Agent),
626 ]
627 .boxed()
628 }
629}
630
631#[derive(Debug, Clone, PartialEq, Eq)]
655#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
656#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
657#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
658#[cfg_attr(
660 all(feature = "arbitrary", not(feature = "chrono")),
661 derive(arbitrary::Arbitrary)
662)]
663#[cfg_attr(
664 all(feature = "proptest", not(feature = "chrono")),
665 derive(proptest_derive::Arbitrary)
666)]
667pub struct AuditInfo {
668 #[cfg_attr(
670 feature = "utoipa",
671 schema(value_type = String, format = DateTime)
672 )]
673 pub created_at: Timestamp,
674 #[cfg_attr(
676 feature = "utoipa",
677 schema(value_type = String, format = DateTime)
678 )]
679 pub updated_at: Timestamp,
680 pub created_by: Principal,
682 pub updated_by: Principal,
684}
685
686impl AuditInfo {
687 #[must_use]
703 pub fn new(
704 created_at: Timestamp,
705 updated_at: Timestamp,
706 created_by: Principal,
707 updated_by: Principal,
708 ) -> Self {
709 Self {
710 created_at,
711 updated_at,
712 created_by,
713 updated_by,
714 }
715 }
716
717 #[cfg(feature = "chrono")]
737 #[must_use]
738 pub fn now(created_by: Principal) -> Self {
739 let now = chrono::Utc::now();
740 let updated_by = created_by.clone();
741 Self {
742 created_at: now,
743 updated_at: now,
744 created_by,
745 updated_by,
746 }
747 }
748
749 #[cfg(feature = "chrono")]
766 pub fn touch(&mut self, updated_by: Principal) {
767 self.updated_at = chrono::Utc::now();
768 self.updated_by = updated_by;
769 }
770}
771
772#[derive(Debug, Clone, PartialEq, Eq)]
799#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
800pub struct ResolvedPrincipal {
801 pub id: Principal,
803 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
807 pub display_name: Option<String>,
808}
809
810impl ResolvedPrincipal {
811 #[must_use]
813 pub fn new(id: Principal, display_name: Option<String>) -> Self {
814 Self { id, display_name }
815 }
816
817 #[must_use]
820 pub fn display(&self) -> &str {
821 self.display_name
822 .as_deref()
823 .unwrap_or_else(|| self.id.as_str())
824 }
825}
826
827impl From<Principal> for ResolvedPrincipal {
828 fn from(id: Principal) -> Self {
829 Self {
830 id,
831 display_name: None,
832 }
833 }
834}
835
836#[cfg(all(feature = "arbitrary", feature = "chrono"))]
843impl<'a> arbitrary::Arbitrary<'a> for AuditInfo {
844 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
845 let created_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
847 let updated_secs = <i64 as arbitrary::Arbitrary>::arbitrary(u)? % 32_503_680_000i64;
848 let created_at = chrono::DateTime::from_timestamp(created_secs.abs(), 0)
849 .unwrap_or_else(chrono::Utc::now);
850 let updated_at = chrono::DateTime::from_timestamp(updated_secs.abs(), 0)
851 .unwrap_or_else(chrono::Utc::now);
852 Ok(Self {
853 created_at,
854 updated_at,
855 created_by: Principal::arbitrary(u)?,
856 updated_by: Principal::arbitrary(u)?,
857 })
858 }
859}
860
861#[cfg(all(feature = "proptest", feature = "chrono"))]
862impl proptest::arbitrary::Arbitrary for AuditInfo {
863 type Parameters = ();
864 type Strategy = proptest::strategy::BoxedStrategy<Self>;
865
866 fn arbitrary_with((): ()) -> Self::Strategy {
867 use proptest::prelude::*;
868 (
869 0i64..=32_503_680_000i64,
870 0i64..=32_503_680_000i64,
871 any::<Principal>(),
872 any::<Principal>(),
873 )
874 .prop_map(|(cs, us, cb, ub)| Self {
875 created_at: chrono::DateTime::from_timestamp(cs, 0)
876 .unwrap_or_else(chrono::Utc::now),
877 updated_at: chrono::DateTime::from_timestamp(us, 0)
878 .unwrap_or_else(chrono::Utc::now),
879 created_by: cb,
880 updated_by: ub,
881 })
882 .boxed()
883 }
884}
885
886#[cfg(test)]
891mod tests {
892 use super::*;
893 #[cfg(feature = "uuid")]
894 use uuid::Uuid;
895
896 #[test]
899 fn principal_id_static_str() {
900 let id = PrincipalId::static_str("foo");
901 assert_eq!(id.as_str(), "foo");
902 }
903
904 #[test]
905 fn principal_id_from_owned() {
906 let id = PrincipalId::from_owned("bar".to_owned());
907 assert_eq!(id.as_str(), "bar");
908 }
909
910 #[cfg(feature = "uuid")]
911 #[test]
912 fn principal_id_from_uuid() {
913 let id = PrincipalId::from_uuid(Uuid::nil());
914 assert_eq!(id.as_str(), "00000000-0000-0000-0000-000000000000");
915 }
916
917 #[test]
918 fn principal_id_display() {
919 let id = PrincipalId::static_str("test");
920 assert_eq!(format!("{id}"), "test");
921 }
922
923 #[cfg(feature = "serde")]
924 #[test]
925 fn principal_id_serde_transparent() {
926 let id = PrincipalId::static_str("myid");
927 let json = serde_json::to_value(&id).unwrap();
928 assert_eq!(json, serde_json::json!("myid"));
929 let back: PrincipalId = serde_json::from_value(json).unwrap();
930 assert_eq!(back, id);
931 }
932
933 #[test]
936 fn principal_kind_copy_and_eq() {
937 let k1 = PrincipalKind::User;
938 let k2 = k1;
939 assert_eq!(k1, k2);
940 }
941
942 #[test]
943 fn principal_kind_all_variants() {
944 let _ = PrincipalKind::User;
945 let _ = PrincipalKind::Service;
946 let _ = PrincipalKind::System;
947 let _ = PrincipalKind::Device;
948 let _ = PrincipalKind::Agent;
949 }
950
951 #[cfg(feature = "uuid")]
954 #[test]
955 fn principal_human_has_user_kind() {
956 let p = Principal::human(Uuid::nil());
957 assert_eq!(p.kind, PrincipalKind::User);
958 }
959
960 #[cfg(feature = "uuid")]
961 #[test]
962 fn principal_human_has_empty_org_path() {
963 let p = Principal::human(Uuid::nil());
964 assert!(p.org_path.is_empty());
965 }
966
967 #[test]
968 fn principal_system_has_system_kind() {
969 let p = Principal::system("s");
970 assert_eq!(p.kind, PrincipalKind::System);
971 }
972
973 #[cfg(feature = "uuid")]
974 #[test]
975 fn principal_system_has_empty_org_path() {
976 let p = Principal::system("s");
977 assert!(p.org_path.is_empty());
978 }
979
980 #[cfg(feature = "uuid")]
981 #[test]
982 fn principal_device_has_device_kind() {
983 let p = Principal::device(Uuid::nil());
984 assert_eq!(p.kind, PrincipalKind::Device);
985 }
986
987 #[cfg(feature = "uuid")]
988 #[test]
989 fn principal_device_id_is_uuid_string() {
990 let id = Uuid::new_v4();
991 let p = Principal::device(id);
992 assert_eq!(p.as_str(), id.to_string());
993 }
994
995 #[cfg(feature = "uuid")]
996 #[test]
997 fn principal_device_has_empty_org_path() {
998 let p = Principal::device(Uuid::nil());
999 assert!(p.org_path.is_empty());
1000 }
1001
1002 #[test]
1003 fn principal_agent_has_agent_kind() {
1004 let p = Principal::agent("ops.triage-agent");
1005 assert_eq!(p.kind, PrincipalKind::Agent);
1006 }
1007
1008 #[test]
1009 fn principal_agent_preserves_static_id() {
1010 let p = Principal::agent("sdr.outreach-bot");
1011 assert_eq!(p.as_str(), "sdr.outreach-bot");
1012 }
1013
1014 #[cfg(feature = "uuid")]
1015 #[test]
1016 fn principal_agent_has_empty_org_path() {
1017 let p = Principal::agent("svc");
1018 assert!(p.org_path.is_empty());
1019 }
1020
1021 #[cfg(feature = "uuid")]
1022 #[test]
1023 fn principal_with_org_path_builder() {
1024 let org_id = crate::org_id::OrgId::generate();
1025 let p = Principal::system("test").with_org_path(vec![org_id]);
1026 assert_eq!(p.org_path.len(), 1);
1027 assert_eq!(p.org_path[0], org_id);
1028 }
1029
1030 #[cfg(feature = "uuid")]
1031 #[test]
1032 fn org_path_display_empty_for_system_principal() {
1033 let p = Principal::system("svc");
1034 assert_eq!(p.org_path_display(), "");
1035 }
1036
1037 #[cfg(feature = "uuid")]
1038 #[test]
1039 fn org_path_display_single_org() {
1040 let org_id = crate::org_id::OrgId::generate();
1041 let p = Principal::system("svc").with_org_path(vec![org_id]);
1042 assert_eq!(p.org_path_display(), org_id.to_string());
1043 }
1044
1045 #[cfg(feature = "uuid")]
1046 #[test]
1047 fn org_path_display_multiple_orgs_comma_separated() {
1048 let root = crate::org_id::OrgId::generate();
1049 let child = crate::org_id::OrgId::generate();
1050 let p = Principal::system("svc").with_org_path(vec![root, child]);
1051 assert_eq!(p.org_path_display(), format!("{root},{child}"));
1052 }
1053
1054 #[cfg(feature = "uuid")]
1055 #[test]
1056 fn principal_try_parse_accepts_valid_uuid() {
1057 let s = "550e8400-e29b-41d4-a716-446655440000";
1058 let p = Principal::try_parse(s).expect("valid UUID should parse");
1059 assert_eq!(p.as_str(), s);
1060 }
1061
1062 #[cfg(feature = "uuid")]
1063 #[test]
1064 fn principal_try_parse_sets_user_kind() {
1065 let p = Principal::try_parse("550e8400-e29b-41d4-a716-446655440000").unwrap();
1066 assert_eq!(p.kind, PrincipalKind::User);
1067 }
1068
1069 #[cfg(feature = "uuid")]
1070 #[test]
1071 fn principal_try_parse_rejects_email_string() {
1072 let err = Principal::try_parse("alice@example.com").expect_err("email must be rejected");
1073 assert_eq!(err.input, "alice@example.com");
1074 assert!(err.to_string().contains("alice@example.com"));
1075 }
1076
1077 #[cfg(feature = "uuid")]
1078 #[test]
1079 fn principal_try_parse_rejects_empty_string() {
1080 let err = Principal::try_parse("").expect_err("empty string must be rejected");
1081 assert_eq!(err.input, "");
1082 }
1083
1084 #[test]
1085 fn principal_as_str_returns_id_str() {
1086 let p = Principal::system("x");
1087 assert_eq!(p.as_str(), "x");
1088 }
1089
1090 #[cfg(feature = "uuid")]
1091 #[test]
1092 fn principal_display_forwards_to_as_str() {
1093 let p = Principal::human(Uuid::nil());
1094 let s = format!("{p}");
1095 assert_eq!(s, Uuid::nil().to_string());
1096 }
1097
1098 #[cfg(feature = "uuid")]
1099 #[test]
1100 fn principal_debug_is_not_redacted() {
1101 let p = Principal::human(Uuid::nil());
1102 let s = format!("{p:?}");
1103 assert!(
1104 s.contains(&Uuid::nil().to_string()),
1105 "debug must not redact: {s}"
1106 );
1107 assert!(s.contains("Principal"), "debug must name the type: {s}");
1108 }
1109
1110 #[test]
1111 fn principal_equality_and_hash_across_owned_and_borrowed() {
1112 use std::collections::hash_map::DefaultHasher;
1113 use std::hash::{Hash, Hasher};
1114
1115 let p1 = Principal::system("orders.bootstrap");
1116 let p2 = Principal::system("orders.bootstrap");
1117 assert_eq!(p1, p2);
1118
1119 let mut h1 = DefaultHasher::new();
1120 p1.hash(&mut h1);
1121 let mut h2 = DefaultHasher::new();
1122 p2.hash(&mut h2);
1123 assert_eq!(h1.finish(), h2.finish());
1124 }
1125
1126 #[cfg(feature = "uuid")]
1127 #[test]
1128 fn principal_clone_roundtrip() {
1129 let p = Principal::human(Uuid::nil());
1130 let q = p.clone();
1131 assert_eq!(p, q);
1132 }
1133
1134 #[cfg(all(feature = "serde", feature = "uuid"))]
1135 #[test]
1136 fn principal_serde_struct_roundtrip_human() {
1137 let p = Principal::human(Uuid::nil());
1138 let json = serde_json::to_value(&p).unwrap();
1139 let back: Principal = serde_json::from_value(json).unwrap();
1140 assert_eq!(back, p);
1141 }
1142
1143 #[cfg(feature = "serde")]
1144 #[test]
1145 fn principal_serde_struct_roundtrip_system() {
1146 let p = Principal::system("billing.rotation-engine");
1147 let json = serde_json::to_value(&p).unwrap();
1148 let back: Principal = serde_json::from_value(json).unwrap();
1149 assert_eq!(back, p);
1150 }
1151
1152 #[cfg(all(feature = "serde", feature = "uuid"))]
1153 #[test]
1154 fn principal_serde_includes_org_path() {
1155 let p = Principal::system("test");
1156 let json = serde_json::to_value(&p).unwrap();
1157 assert!(json.get("org_path").is_some());
1158 }
1159
1160 #[cfg(all(feature = "chrono", feature = "uuid"))]
1163 #[test]
1164 fn now_sets_created_at_and_updated_at() {
1165 let actor = Principal::human(Uuid::nil());
1166 let before = chrono::Utc::now();
1167 let info = AuditInfo::now(actor.clone());
1168 let after = chrono::Utc::now();
1169
1170 assert!(info.created_at >= before && info.created_at <= after);
1171 assert!(info.updated_at >= before && info.updated_at <= after);
1172 assert_eq!(info.created_by, actor);
1173 assert_eq!(info.updated_by, actor);
1174 }
1175
1176 #[cfg(all(feature = "chrono", feature = "serde"))]
1177 #[test]
1178 fn now_with_system_principal() {
1179 let info = AuditInfo::now(Principal::system("billing.rotation-engine"));
1180 let json = serde_json::to_value(&info).unwrap();
1181 let back: AuditInfo = serde_json::from_value(json).unwrap();
1182 assert_eq!(back, info);
1183 }
1184
1185 #[cfg(all(feature = "chrono", feature = "uuid"))]
1186 #[test]
1187 fn touch_updates_updated_at_and_updated_by() {
1188 let mut info = AuditInfo::now(Principal::human(Uuid::nil()));
1189 let engine = Principal::system("billing.rotation-engine");
1190 let before_touch = chrono::Utc::now();
1191 info.touch(engine.clone());
1192 let after_touch = chrono::Utc::now();
1193
1194 assert!(info.updated_at >= before_touch && info.updated_at <= after_touch);
1195 assert_eq!(info.updated_by, engine);
1196 }
1197
1198 #[cfg(all(feature = "chrono", feature = "uuid"))]
1199 #[test]
1200 fn new_constructor() {
1201 let now = chrono::Utc::now();
1202 let actor = Principal::human(Uuid::nil());
1203 let engine = Principal::system("billing.rotation-engine");
1204 let info = AuditInfo::new(now, now, actor.clone(), engine.clone());
1205 assert_eq!(info.created_at, now);
1206 assert_eq!(info.updated_at, now);
1207 assert_eq!(info.created_by, actor);
1208 assert_eq!(info.updated_by, engine);
1209 }
1210
1211 #[cfg(all(feature = "chrono", feature = "serde"))]
1212 #[test]
1213 fn serde_round_trip_with_system_actor() {
1214 let info = AuditInfo::now(Principal::system("orders.bootstrap"));
1215 let json = serde_json::to_value(&info).unwrap();
1216 let back: AuditInfo = serde_json::from_value(json).unwrap();
1217 assert_eq!(back, info);
1218 }
1219
1220 #[cfg(all(feature = "chrono", feature = "serde"))]
1221 #[test]
1222 fn serde_actor_fields_are_always_present() {
1223 let info = AuditInfo::now(Principal::system("orders.bootstrap"));
1224 let json = serde_json::to_value(&info).unwrap();
1225 assert!(
1226 json.get("created_by").is_some(),
1227 "created_by must always serialize"
1228 );
1229 assert!(
1230 json.get("updated_by").is_some(),
1231 "updated_by must always serialize"
1232 );
1233 assert_eq!(
1235 json["created_by"]["id"],
1236 serde_json::json!("orders.bootstrap")
1237 );
1238 }
1239
1240 #[test]
1243 fn principal_parse_error_display_contains_input() {
1244 let err = PrincipalParseError {
1245 input: "bad-value".to_owned(),
1246 };
1247 assert!(err.to_string().contains("bad-value"));
1248 }
1249
1250 #[cfg(feature = "std")]
1251 #[test]
1252 fn principal_parse_error_is_std_error() {
1253 let err = PrincipalParseError {
1254 input: "x".to_owned(),
1255 };
1256 let _: &dyn std::error::Error = &err;
1257 }
1258
1259 #[cfg(feature = "uuid")]
1262 #[test]
1263 fn resolved_principal_new_and_display_with_name() {
1264 let p = Principal::human(Uuid::nil());
1265 let r = ResolvedPrincipal::new(p, Some("Alice Martin".to_owned()));
1266 assert_eq!(r.display(), "Alice Martin");
1267 }
1268
1269 #[cfg(feature = "uuid")]
1270 #[test]
1271 fn resolved_principal_display_falls_back_to_uuid() {
1272 let p = Principal::human(Uuid::nil());
1273 let r = ResolvedPrincipal::new(p.clone(), None);
1274 assert_eq!(r.display(), p.as_str());
1275 }
1276
1277 #[cfg(feature = "uuid")]
1278 #[test]
1279 fn resolved_principal_from_principal() {
1280 let p = Principal::human(Uuid::nil());
1281 let r = ResolvedPrincipal::from(p.clone());
1282 assert_eq!(r.id, p);
1283 assert!(r.display_name.is_none());
1284 }
1285
1286 #[cfg(all(feature = "uuid", feature = "serde"))]
1287 #[test]
1288 fn resolved_principal_serde_omits_none_display_name() {
1289 let p = Principal::human(Uuid::nil());
1290 let r = ResolvedPrincipal::from(p);
1291 let json = serde_json::to_value(&r).unwrap();
1292 assert!(
1293 json.get("display_name").is_none(),
1294 "display_name must be absent when None"
1295 );
1296 }
1297
1298 #[cfg(all(feature = "uuid", feature = "serde"))]
1299 #[test]
1300 fn resolved_principal_serde_includes_display_name_when_set() {
1301 let p = Principal::human(Uuid::nil());
1302 let r = ResolvedPrincipal::new(p, Some("Bob".to_owned()));
1303 let json = serde_json::to_value(&r).unwrap();
1304 assert_eq!(json["display_name"], serde_json::json!("Bob"));
1305 }
1306
1307 #[test]
1310 fn device_lease_kind_variants_distinct() {
1311 assert_ne!(DeviceLeaseKind::Connection, DeviceLeaseKind::RequestStream);
1312 }
1313
1314 #[test]
1315 fn device_lease_kind_copy() {
1316 let k = DeviceLeaseKind::Connection;
1317 let k2 = k;
1318 assert_eq!(k, k2);
1319 }
1320
1321 #[test]
1324 fn device_lease_new_stores_fields() {
1325 let lease = DeviceLease::new(DeviceLeaseKind::RequestStream, Some(100), 1800);
1326 assert_eq!(lease.kind, DeviceLeaseKind::RequestStream);
1327 assert_eq!(lease.max_concurrent, Some(100));
1328 assert_eq!(lease.refresh_secs, 1800);
1329 }
1330
1331 #[test]
1332 fn device_lease_new_clamps_refresh_to_max() {
1333 let lease = DeviceLease::new(DeviceLeaseKind::Connection, None, 9999);
1334 assert_eq!(lease.refresh_secs, DeviceLease::MAX_REFRESH_SECS);
1335 }
1336
1337 #[test]
1338 fn device_lease_max_refresh_secs_is_one_hour() {
1339 assert_eq!(DeviceLease::MAX_REFRESH_SECS, 3_600);
1340 }
1341
1342 #[test]
1343 fn device_lease_new_no_cap() {
1344 let lease = DeviceLease::new(DeviceLeaseKind::Connection, None, 600);
1345 assert_eq!(lease.max_concurrent, None);
1346 assert_eq!(lease.refresh_secs, 600);
1347 }
1348
1349 #[cfg(feature = "serde")]
1350 #[test]
1351 fn device_lease_kind_serde_roundtrip() {
1352 let k = DeviceLeaseKind::Connection;
1353 let json = serde_json::to_value(k).unwrap();
1354 let back: DeviceLeaseKind = serde_json::from_value(json).unwrap();
1355 assert_eq!(k, back);
1356 }
1357
1358 #[cfg(feature = "serde")]
1359 #[test]
1360 fn device_lease_serde_roundtrip() {
1361 let lease = DeviceLease::new(DeviceLeaseKind::RequestStream, Some(50), 300);
1362 let json = serde_json::to_value(&lease).unwrap();
1363 let back: DeviceLease = serde_json::from_value(json).unwrap();
1364 assert_eq!(back.kind, lease.kind);
1365 assert_eq!(back.max_concurrent, lease.max_concurrent);
1366 assert_eq!(back.refresh_secs, lease.refresh_secs);
1367 }
1368}