1#[derive(Clone, Debug)]
5pub struct LicenseInfo {
6 pub customer: String,
7 pub tier: LicenseTier,
8 pub features: LicenseFeatures,
9 pub expires_at: u64,
10 pub trial: bool,
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum LicenseTier {
15 Pro,
16 Enterprise,
17}
18
19#[derive(Clone, Copy, Debug, Default)]
20pub struct LicenseFeatures {
21 pub vault: bool,
22 pub cluster: bool,
23}
24
25impl LicenseInfo {
26 #[must_use]
27 pub fn check_runtime_expiry(expires_at: u64) -> bool {
28 let now = std::time::SystemTime::now()
29 .duration_since(std::time::UNIX_EPOCH)
30 .map_or(u64::MAX, |d| d.as_secs());
31 now > expires_at
32 }
33
34 #[must_use]
35 pub fn is_expired(&self) -> bool {
36 Self::check_runtime_expiry(self.expires_at)
37 }
38
39 #[must_use]
40 pub fn days_remaining(&self) -> i64 {
41 let now = std::time::SystemTime::now()
42 .duration_since(std::time::UNIX_EPOCH)
43 .map_or(0, |d| d.as_secs());
44 if now == 0 || now > self.expires_at {
45 return 0;
46 }
47 i64::try_from((self.expires_at - now) / 86400).unwrap_or(i64::MAX)
48 }
49}
50
51impl std::fmt::Display for LicenseTier {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::Pro => write!(f, "pro"),
55 Self::Enterprise => write!(f, "enterprise"),
56 }
57 }
58}
59
60impl std::fmt::Display for LicenseFeatures {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match (self.vault, self.cluster) {
63 (true, true) => write!(f, "vault, cluster"),
64 (true, false) => write!(f, "vault"),
65 (false, true) => write!(f, "cluster"),
66 (false, false) => write!(f, "none"),
67 }
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn features_display_all_combinations() {
77 let both = LicenseFeatures {
78 vault: true,
79 cluster: true,
80 };
81 assert_eq!(both.to_string(), "vault, cluster");
82
83 let vault_only = LicenseFeatures {
84 vault: true,
85 cluster: false,
86 };
87 assert_eq!(vault_only.to_string(), "vault");
88
89 let cluster_only = LicenseFeatures {
90 vault: false,
91 cluster: true,
92 };
93 assert_eq!(cluster_only.to_string(), "cluster");
94
95 let none = LicenseFeatures {
96 vault: false,
97 cluster: false,
98 };
99 assert_eq!(none.to_string(), "none");
100 }
101
102 #[test]
103 fn check_runtime_expiry_past_returns_true() {
104 let past = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .unwrap()
107 .as_secs()
108 - 3600;
109 assert!(LicenseInfo::check_runtime_expiry(past));
110 }
111
112 #[test]
113 fn check_runtime_expiry_future_returns_false() {
114 let future = std::time::SystemTime::now()
115 .duration_since(std::time::UNIX_EPOCH)
116 .unwrap()
117 .as_secs()
118 + 3600;
119 assert!(!LicenseInfo::check_runtime_expiry(future));
120 }
121
122 #[test]
123 fn check_runtime_expiry_exact_now_returns_false() {
124 let now = std::time::SystemTime::now()
125 .duration_since(std::time::UNIX_EPOCH)
126 .unwrap()
127 .as_secs();
128 assert!(!LicenseInfo::check_runtime_expiry(now));
129 }
130
131 #[test]
132 fn is_expired_reflects_runtime_check() {
133 let info_expired = LicenseInfo {
134 customer: "x".into(),
135 tier: LicenseTier::Pro,
136 features: LicenseFeatures::default(),
137 expires_at: 0,
138 trial: false,
139 };
140 assert!(info_expired.is_expired());
141
142 let future = std::time::SystemTime::now()
143 .duration_since(std::time::UNIX_EPOCH)
144 .unwrap()
145 .as_secs()
146 + 86400;
147 let info_valid = LicenseInfo {
148 customer: "x".into(),
149 tier: LicenseTier::Pro,
150 features: LicenseFeatures::default(),
151 expires_at: future,
152 trial: false,
153 };
154 assert!(!info_valid.is_expired());
155 }
156}