Skip to main content

mqdb_core/
license.rs

1// Copyright 2025-2026 LabOverWire. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4#[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}