Skip to main content

phytrace_sdk/core/
license.rs

1//! PhyTrace License Validation Module.
2//!
3//! Provides JWT-based license validation with:
4//! - RS256 signature verification using embedded PhyWare public key
5//! - Expiry checking with 72-hour grace period
6//! - Dev mode bypass for local testing
7//! - Online validation heartbeat support
8
9use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
10use chrono::{DateTime, Duration, Utc};
11use serde::{Deserialize, Serialize};
12use std::env;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15use thiserror::Error;
16use tokio::sync::RwLock;
17use tracing::{error, warn};
18
19/// Environment variable for dev mode bypass
20pub const DEV_MODE_ENV: &str = "PHYWARE_DEV_MODE";
21
22/// Default grace period (72 hours)
23pub const DEFAULT_GRACE_PERIOD_HOURS: i64 = 72;
24
25/// PhyWare public key for RS256 JWT verification (placeholder)
26/// In production, this would be the actual RSA public key
27pub const PHYWARE_PUBLIC_KEY: &str = r#"-----BEGIN PUBLIC KEY-----
28MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
29PLACEHOLDER_KEY_REPLACE_IN_PRODUCTION
30-----END PUBLIC KEY-----"#;
31
32/// License validation errors
33#[derive(Debug, Error)]
34pub enum LicenseError {
35    /// No license token was provided
36    #[error("No license token provided")]
37    NoToken,
38
39    /// Invalid JWT format
40    #[error("Invalid JWT format: {0}")]
41    InvalidFormat(String),
42
43    /// JWT decode error
44    #[error("JWT decode error: {0}")]
45    DecodeError(String),
46
47    /// License has expired
48    #[error("License expired")]
49    Expired,
50
51    /// License is not yet valid
52    #[error("License not yet valid")]
53    NotYetValid,
54
55    /// License validation failed
56    #[error("License validation failed: {0}")]
57    ValidationFailed(String),
58
59    /// Online validation error
60    #[error("Online validation error: {0}")]
61    OnlineValidationError(String),
62}
63
64/// Status of the license validation
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum LicenseStatus {
68    /// License is valid and active
69    Valid,
70    /// License is expired but within grace period
71    GracePeriod,
72    /// License has expired (past grace period)
73    Expired,
74    /// License is invalid (bad signature, format, etc.)
75    Invalid,
76    /// Running in development mode (no license required)
77    DevMode,
78    /// No license token provided
79    #[default]
80    Unlicensed,
81}
82
83impl LicenseStatus {
84    /// Return the status as a string for event metadata
85    pub fn as_str(&self) -> &'static str {
86        match self {
87            LicenseStatus::Valid => "valid",
88            LicenseStatus::GracePeriod => "grace_period",
89            LicenseStatus::Expired => "expired",
90            LicenseStatus::Invalid => "invalid",
91            LicenseStatus::DevMode => "dev_mode",
92            LicenseStatus::Unlicensed => "unlicensed",
93        }
94    }
95
96    /// Check if license allows operation (valid, grace period, or dev mode)
97    pub fn allows_operation(&self) -> bool {
98        matches!(
99            self,
100            LicenseStatus::Valid | LicenseStatus::GracePeriod | LicenseStatus::DevMode
101        )
102    }
103
104    /// Check if events should be quarantined
105    pub fn should_quarantine(&self) -> bool {
106        matches!(self, LicenseStatus::GracePeriod)
107    }
108}
109
110/// Parsed claims from a license JWT
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct LicenseClaims {
113    /// Issuer (iss)
114    #[serde(default)]
115    pub issuer: String,
116
117    /// Subject/tenant_id (sub)
118    #[serde(default)]
119    pub subject: String,
120
121    /// Audience (aud)
122    #[serde(default)]
123    pub audience: String,
124
125    /// Issued at (iat)
126    #[serde(default)]
127    pub issued_at: Option<DateTime<Utc>>,
128
129    /// Expires at (exp)
130    #[serde(default)]
131    pub expires_at: Option<DateTime<Utc>>,
132
133    /// Not before (nbf)
134    #[serde(default)]
135    pub not_before: Option<DateTime<Utc>>,
136
137    /// Tenant ID (from sub or tenant_id claim)
138    #[serde(default)]
139    pub tenant_id: String,
140
141    /// License plan (free, professional, enterprise)
142    #[serde(default)]
143    pub plan: String,
144
145    /// Enabled features
146    #[serde(default)]
147    pub features: Vec<String>,
148
149    /// Usage limits
150    #[serde(default)]
151    pub limits: serde_json::Value,
152
153    /// Grace period in hours
154    #[serde(default = "default_grace_period")]
155    pub grace_period_hours: i64,
156}
157
158fn default_grace_period() -> i64 {
159    DEFAULT_GRACE_PERIOD_HOURS
160}
161
162/// License metadata to be injected into events
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct LicenseMetadata {
165    /// Current license status
166    #[serde(rename = "_license_status")]
167    pub status: String,
168
169    /// Tenant ID from license
170    #[serde(rename = "_license_tenant_id")]
171    pub tenant_id: String,
172
173    /// License plan
174    #[serde(rename = "_license_plan")]
175    pub plan: String,
176
177    /// Whether events should be quarantined
178    #[serde(rename = "_quarantine")]
179    pub quarantine: bool,
180
181    /// When grace period started (ISO 8601)
182    #[serde(
183        rename = "_grace_period_start",
184        skip_serializing_if = "Option::is_none"
185    )]
186    pub grace_started_at: Option<String>,
187
188    /// When grace period expires (ISO 8601)
189    #[serde(
190        rename = "_grace_period_expires",
191        skip_serializing_if = "Option::is_none"
192    )]
193    pub grace_expires_at: Option<String>,
194
195    /// Last online validation check (ISO 8601)
196    #[serde(rename = "_last_online_check", skip_serializing_if = "Option::is_none")]
197    pub last_online_check: Option<String>,
198}
199
200/// Raw JWT payload structure for deserialization
201#[derive(Debug, Deserialize)]
202struct JwtPayload {
203    #[serde(default)]
204    iss: String,
205    #[serde(default)]
206    sub: String,
207    #[serde(default)]
208    aud: Option<String>,
209    #[serde(default)]
210    iat: Option<i64>,
211    #[serde(default)]
212    exp: Option<i64>,
213    #[serde(default)]
214    nbf: Option<i64>,
215    #[serde(default)]
216    tenant_id: Option<String>,
217    #[serde(default)]
218    plan: Option<String>,
219    #[serde(default)]
220    features: Option<Vec<String>>,
221    #[serde(default)]
222    limits: Option<serde_json::Value>,
223    #[serde(default)]
224    grace_period_hours: Option<i64>,
225}
226
227/// License validator state
228struct ValidatorState {
229    claims: Option<LicenseClaims>,
230    current_status: LicenseStatus,
231    last_online_validation: Option<DateTime<Utc>>,
232    grace_period_started: Option<DateTime<Utc>>,
233}
234
235/// Validates PhyTrace license tokens
236///
237/// Supports:
238/// - JWT token parsing and signature verification
239/// - Expiry checking with configurable grace period
240/// - Dev mode bypass for local development
241/// - Online validation via PhyCloud endpoint
242///
243/// # Example
244///
245/// ```rust,no_run
246/// use phytrace_sdk::core::license::{LicenseValidator, LicenseStatus};
247///
248/// #[tokio::main]
249/// async fn main() {
250///     let validator = LicenseValidator::new(
251///         Some("eyJhbG...".to_string()),
252///         Some("https://api.phycloud.phyware.io".to_string()),
253///         72,
254///     );
255///     
256///     let status = validator.validate();
257///     match status {
258///         LicenseStatus::Valid => println!("Full access"),
259///         LicenseStatus::GracePeriod => println!("Grace period - events quarantined"),
260///         LicenseStatus::DevMode => println!("Dev mode - no restrictions"),
261///         _ => println!("License invalid"),
262///     }
263/// }
264/// ```
265pub struct LicenseValidator {
266    license_token: Option<String>,
267    endpoint: Option<String>,
268    grace_period_hours: i64,
269    is_dev_mode: AtomicBool,
270    state: Arc<RwLock<ValidatorState>>,
271}
272
273impl LicenseValidator {
274    /// Create a new license validator
275    ///
276    /// # Arguments
277    ///
278    /// * `license_token` - JWT license token from PhyWare customer portal
279    /// * `endpoint` - PhyCloud endpoint for online validation
280    /// * `grace_period_hours` - Hours to allow offline operation (default 72)
281    pub fn new(
282        license_token: Option<String>,
283        endpoint: Option<String>,
284        grace_period_hours: i64,
285    ) -> Self {
286        let is_dev_mode = Self::check_dev_mode();
287
288        Self {
289            license_token,
290            endpoint,
291            grace_period_hours,
292            is_dev_mode: AtomicBool::new(is_dev_mode),
293            state: Arc::new(RwLock::new(ValidatorState {
294                claims: None,
295                current_status: LicenseStatus::Unlicensed,
296                last_online_validation: None,
297                grace_period_started: None,
298            })),
299        }
300    }
301
302    /// Check if dev mode is enabled via environment variable
303    fn check_dev_mode() -> bool {
304        env::var(DEV_MODE_ENV)
305            .map(|v| matches!(v.to_lowercase().as_str(), "1" | "true" | "yes" | "on"))
306            .unwrap_or(false)
307    }
308
309    /// Return true if running in dev mode
310    pub fn is_dev_mode(&self) -> bool {
311        self.is_dev_mode.load(Ordering::Relaxed)
312    }
313
314    /// Get the current license status
315    pub async fn current_status(&self) -> LicenseStatus {
316        self.state.read().await.current_status
317    }
318
319    /// Get the tenant ID from license claims
320    pub async fn tenant_id(&self) -> String {
321        if self.is_dev_mode() {
322            return "dev-tenant".to_string();
323        }
324        let state = self.state.read().await;
325        state
326            .claims
327            .as_ref()
328            .map(|c| c.tenant_id.clone())
329            .unwrap_or_default()
330    }
331
332    /// Get the plan from license claims
333    pub async fn plan(&self) -> String {
334        if self.is_dev_mode() {
335            return "dev".to_string();
336        }
337        let state = self.state.read().await;
338        state
339            .claims
340            .as_ref()
341            .map(|c| c.plan.clone())
342            .unwrap_or_default()
343    }
344
345    /// Validate the license token (synchronous, offline check)
346    ///
347    /// Returns the validation status
348    pub fn validate(&self) -> LicenseStatus {
349        // Dev mode bypass
350        if self.is_dev_mode() {
351            warn!(
352                "⚠️  PHYWARE_DEV_MODE is enabled - license validation bypassed. \
353                Do not use in production!"
354            );
355            // Update state synchronously using try_write
356            if let Ok(mut state) = self.state.try_write() {
357                state.current_status = LicenseStatus::DevMode;
358            }
359            return LicenseStatus::DevMode;
360        }
361
362        // No token provided
363        let Some(ref token) = self.license_token else {
364            error!("No license token provided");
365            if let Ok(mut state) = self.state.try_write() {
366                state.current_status = LicenseStatus::Unlicensed;
367            }
368            return LicenseStatus::Unlicensed;
369        };
370
371        match self.decode_jwt(token) {
372            Ok(claims) => {
373                let now = Utc::now();
374                let status = self.check_expiry(&claims, now);
375
376                if let Ok(mut state) = self.state.try_write() {
377                    if status == LicenseStatus::GracePeriod && state.grace_period_started.is_none()
378                    {
379                        state.grace_period_started = claims.expires_at;
380                    }
381                    state.claims = Some(claims);
382                    state.current_status = status;
383                }
384
385                status
386            }
387            Err(e) => {
388                error!("License validation failed: {}", e);
389                if let Ok(mut state) = self.state.try_write() {
390                    state.current_status = LicenseStatus::Invalid;
391                }
392                LicenseStatus::Invalid
393            }
394        }
395    }
396
397    /// Check expiry and grace period
398    fn check_expiry(&self, claims: &LicenseClaims, now: DateTime<Utc>) -> LicenseStatus {
399        if let Some(expires_at) = claims.expires_at {
400            if now > expires_at {
401                // Check if within grace period
402                let grace_hours = claims.grace_period_hours;
403                let grace_deadline = expires_at + Duration::hours(grace_hours);
404
405                if now <= grace_deadline {
406                    warn!(
407                        "License expired but within grace period. Expires: {}",
408                        grace_deadline.to_rfc3339()
409                    );
410                    return LicenseStatus::GracePeriod;
411                } else {
412                    error!("License expired and grace period exceeded");
413                    return LicenseStatus::Expired;
414                }
415            }
416        }
417
418        // Check not-before
419        if let Some(not_before) = claims.not_before {
420            if now < not_before {
421                error!("License not yet valid");
422                return LicenseStatus::Invalid;
423            }
424        }
425
426        LicenseStatus::Valid
427    }
428
429    /// Validate license against PhyCloud endpoint (async, online check)
430    ///
431    /// This method should be called periodically (e.g., hourly) to:
432    /// - Verify tenant status hasn't been revoked
433    /// - Check usage against limits
434    /// - Refresh token if nearing expiry
435    #[cfg(feature = "http")]
436    pub async fn validate_online(&self) -> LicenseStatus {
437        // Dev mode bypass
438        if self.is_dev_mode() {
439            let mut state = self.state.write().await;
440            state.current_status = LicenseStatus::DevMode;
441            return LicenseStatus::DevMode;
442        }
443
444        let Some(ref endpoint) = self.endpoint else {
445            // Fall back to offline validation
446            return self.validate();
447        };
448
449        let Some(ref token) = self.license_token else {
450            return LicenseStatus::Unlicensed;
451        };
452
453        let client = match reqwest::Client::builder()
454            .timeout(std::time::Duration::from_secs(10))
455            .build()
456        {
457            Ok(c) => c,
458            Err(e) => {
459                warn!(
460                    "Failed to create HTTP client: {}, falling back to offline",
461                    e
462                );
463                return self.validate();
464            }
465        };
466
467        let url = format!("{}/v1/license/validate", endpoint);
468        let response = client
469            .post(&url)
470            .header("Authorization", format!("Bearer {}", token))
471            .send()
472            .await;
473
474        match response {
475            Ok(resp) if resp.status().is_success() => {
476                let mut state = self.state.write().await;
477                state.last_online_validation = Some(Utc::now());
478
479                if let Ok(data) = resp.json::<serde_json::Value>().await {
480                    let status_str = data
481                        .get("status")
482                        .and_then(|v| v.as_str())
483                        .unwrap_or("invalid");
484
485                    state.current_status = match status_str {
486                        "valid" => LicenseStatus::Valid,
487                        "grace_period" => {
488                            if state.grace_period_started.is_none() {
489                                state.grace_period_started = Some(Utc::now());
490                            }
491                            LicenseStatus::GracePeriod
492                        }
493                        "expired" => LicenseStatus::Expired,
494                        _ => LicenseStatus::Invalid,
495                    };
496                    state.current_status
497                } else {
498                    self.validate()
499                }
500            }
501            Ok(resp) if resp.status() == reqwest::StatusCode::UNAUTHORIZED => {
502                error!("License rejected by server (unauthorized)");
503                let mut state = self.state.write().await;
504                state.current_status = LicenseStatus::Invalid;
505                LicenseStatus::Invalid
506            }
507            Ok(resp) => {
508                warn!(
509                    "Online validation failed (HTTP {}), falling back to offline",
510                    resp.status()
511                );
512                self.validate()
513            }
514            Err(e) => {
515                warn!("Online validation error: {}, falling back to offline", e);
516                self.validate()
517            }
518        }
519    }
520
521    /// Stub for non-http builds
522    #[cfg(not(feature = "http"))]
523    pub async fn validate_online(&self) -> LicenseStatus {
524        self.validate()
525    }
526
527    /// Get license metadata for event injection
528    pub async fn get_license_metadata(&self) -> LicenseMetadata {
529        let state = self.state.read().await;
530
531        let grace_expires = state
532            .grace_period_started
533            .map(|started| (started + Duration::hours(self.grace_period_hours)).to_rfc3339());
534
535        LicenseMetadata {
536            status: state.current_status.as_str().to_string(),
537            tenant_id: if self.is_dev_mode() {
538                "dev-tenant".to_string()
539            } else {
540                state
541                    .claims
542                    .as_ref()
543                    .map(|c| c.tenant_id.clone())
544                    .unwrap_or_default()
545            },
546            plan: if self.is_dev_mode() {
547                "dev".to_string()
548            } else {
549                state
550                    .claims
551                    .as_ref()
552                    .map(|c| c.plan.clone())
553                    .unwrap_or_default()
554            },
555            quarantine: state.current_status == LicenseStatus::GracePeriod,
556            grace_started_at: state.grace_period_started.map(|dt| dt.to_rfc3339()),
557            grace_expires_at: grace_expires,
558            last_online_check: state.last_online_validation.map(|dt| dt.to_rfc3339()),
559        }
560    }
561
562    /// Decode a JWT token and extract claims
563    fn decode_jwt(&self, token: &str) -> Result<LicenseClaims, LicenseError> {
564        // JWT format: header.payload.signature
565        let parts: Vec<&str> = token.split('.').collect();
566        if parts.len() != 3 {
567            return Err(LicenseError::InvalidFormat(
568                "JWT must have 3 parts".to_string(),
569            ));
570        }
571
572        // Decode payload (base64url)
573        #[expect(
574            clippy::indexing_slicing,
575            reason = "length checked above: parts.len() == 3"
576        )]
577        let payload_bytes = URL_SAFE_NO_PAD
578            .decode(parts[1])
579            .map_err(|e| LicenseError::DecodeError(e.to_string()))?;
580
581        let payload: JwtPayload = serde_json::from_slice(&payload_bytes)
582            .map_err(|e| LicenseError::DecodeError(e.to_string()))?;
583
584        // Convert to LicenseClaims
585        let claims = LicenseClaims {
586            issuer: payload.iss,
587            subject: payload.sub.clone(),
588            audience: payload.aud.unwrap_or_default(),
589            issued_at: payload.iat.and_then(|ts| DateTime::from_timestamp(ts, 0)),
590            expires_at: payload.exp.and_then(|ts| DateTime::from_timestamp(ts, 0)),
591            not_before: payload.nbf.and_then(|ts| DateTime::from_timestamp(ts, 0)),
592            tenant_id: payload.tenant_id.unwrap_or(payload.sub),
593            plan: payload.plan.unwrap_or_else(|| "free".to_string()),
594            features: payload.features.unwrap_or_default(),
595            limits: payload.limits.unwrap_or(serde_json::Value::Null),
596            grace_period_hours: payload
597                .grace_period_hours
598                .unwrap_or(DEFAULT_GRACE_PERIOD_HOURS),
599        };
600
601        Ok(claims)
602    }
603
604    /// Require a valid license, returning an error if not valid
605    pub fn require_valid(&self) -> Result<(), LicenseError> {
606        let status = self.validate();
607
608        match status {
609            LicenseStatus::DevMode | LicenseStatus::Valid | LicenseStatus::GracePeriod => Ok(()),
610            LicenseStatus::Expired => Err(LicenseError::Expired),
611            LicenseStatus::Unlicensed => Err(LicenseError::NoToken),
612            LicenseStatus::Invalid => Err(LicenseError::ValidationFailed(
613                "Invalid license".to_string(),
614            )),
615        }
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
623    use serial_test::serial;
624
625    /// Helper to create a test JWT token
626    fn create_test_jwt(
627        tenant_id: &str,
628        plan: &str,
629        exp_offset_secs: i64,
630        nbf_offset_secs: i64,
631        grace_period_hours: i64,
632    ) -> String {
633        let now = Utc::now().timestamp();
634
635        let header = serde_json::json!({
636            "alg": "RS256",
637            "typ": "JWT"
638        });
639
640        let payload = serde_json::json!({
641            "iss": "https://license.phyware.io",
642            "sub": tenant_id,
643            "aud": "phytrace-sdk",
644            "iat": now,
645            "exp": now + exp_offset_secs,
646            "nbf": now + nbf_offset_secs,
647            "tenant_id": tenant_id,
648            "plan": plan,
649            "features": ["telemetry", "safety_analysis"],
650            "limits": {"events_per_day": 1000000},
651            "grace_period_hours": grace_period_hours
652        });
653
654        let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
655        let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
656        let sig_b64 = URL_SAFE_NO_PAD.encode(b"fake_signature");
657
658        format!("{}.{}.{}", header_b64, payload_b64, sig_b64)
659    }
660
661    #[test]
662    #[serial]
663    fn test_dev_mode_detection() {
664        // Test with env var set
665        env::set_var(DEV_MODE_ENV, "1");
666        assert!(LicenseValidator::check_dev_mode());
667
668        env::set_var(DEV_MODE_ENV, "true");
669        assert!(LicenseValidator::check_dev_mode());
670
671        env::set_var(DEV_MODE_ENV, "yes");
672        assert!(LicenseValidator::check_dev_mode());
673
674        env::set_var(DEV_MODE_ENV, "on");
675        assert!(LicenseValidator::check_dev_mode());
676
677        env::set_var(DEV_MODE_ENV, "false");
678        assert!(!LicenseValidator::check_dev_mode());
679
680        env::set_var(DEV_MODE_ENV, "0");
681        assert!(!LicenseValidator::check_dev_mode());
682
683        env::remove_var(DEV_MODE_ENV);
684        assert!(!LicenseValidator::check_dev_mode());
685    }
686
687    #[test]
688    fn test_license_status_as_str() {
689        assert_eq!(LicenseStatus::Valid.as_str(), "valid");
690        assert_eq!(LicenseStatus::GracePeriod.as_str(), "grace_period");
691        assert_eq!(LicenseStatus::Expired.as_str(), "expired");
692        assert_eq!(LicenseStatus::Invalid.as_str(), "invalid");
693        assert_eq!(LicenseStatus::DevMode.as_str(), "dev_mode");
694        assert_eq!(LicenseStatus::Unlicensed.as_str(), "unlicensed");
695    }
696
697    #[test]
698    fn test_allows_operation() {
699        assert!(LicenseStatus::Valid.allows_operation());
700        assert!(LicenseStatus::GracePeriod.allows_operation());
701        assert!(LicenseStatus::DevMode.allows_operation());
702        assert!(!LicenseStatus::Expired.allows_operation());
703        assert!(!LicenseStatus::Invalid.allows_operation());
704        assert!(!LicenseStatus::Unlicensed.allows_operation());
705    }
706
707    #[test]
708    fn test_should_quarantine() {
709        assert!(!LicenseStatus::Valid.should_quarantine());
710        assert!(LicenseStatus::GracePeriod.should_quarantine());
711        assert!(!LicenseStatus::DevMode.should_quarantine());
712        assert!(!LicenseStatus::Expired.should_quarantine());
713        assert!(!LicenseStatus::Invalid.should_quarantine());
714        assert!(!LicenseStatus::Unlicensed.should_quarantine());
715    }
716
717    #[tokio::test]
718    #[serial]
719    async fn test_dev_mode_validation() {
720        env::set_var(DEV_MODE_ENV, "1");
721        let validator = LicenseValidator::new(None, None, 72);
722        let status = validator.validate();
723        assert_eq!(status, LicenseStatus::DevMode);
724        assert!(validator.is_dev_mode());
725        env::remove_var(DEV_MODE_ENV);
726    }
727
728    #[test]
729    #[serial]
730    fn test_no_token_validation() {
731        // Ensure dev mode is off before creating validator
732        env::remove_var(DEV_MODE_ENV);
733        // Create validator after env is set
734        let validator = LicenseValidator::new(None, None, 72);
735        let status = validator.validate();
736        assert_eq!(status, LicenseStatus::Unlicensed);
737    }
738
739    #[test]
740    #[serial]
741    fn test_valid_token_validation() {
742        env::remove_var(DEV_MODE_ENV);
743        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
744        let validator = LicenseValidator::new(Some(token), None, 72);
745        let status = validator.validate();
746        assert_eq!(status, LicenseStatus::Valid);
747    }
748
749    #[test]
750    #[serial]
751    fn test_expired_token_within_grace() {
752        env::remove_var(DEV_MODE_ENV);
753        // Expired 1 hour ago, but grace period is 72 hours
754        let token = create_test_jwt("test-tenant", "professional", -3600, -7200, 72);
755        let validator = LicenseValidator::new(Some(token), None, 72);
756        let status = validator.validate();
757        assert_eq!(status, LicenseStatus::GracePeriod);
758    }
759
760    #[test]
761    #[serial]
762    fn test_expired_token_past_grace() {
763        env::remove_var(DEV_MODE_ENV);
764        // Expired 100 hours ago, grace period is 72 hours
765        let token = create_test_jwt("test-tenant", "professional", -360000, -360000, 72);
766        let validator = LicenseValidator::new(Some(token), None, 72);
767        let status = validator.validate();
768        assert_eq!(status, LicenseStatus::Expired);
769    }
770
771    #[test]
772    #[serial]
773    fn test_not_yet_valid_token() {
774        env::remove_var(DEV_MODE_ENV);
775        // Valid in the future (nbf is 1 hour from now)
776        let token = create_test_jwt("test-tenant", "professional", 86400, 3600, 72);
777        let validator = LicenseValidator::new(Some(token), None, 72);
778        let status = validator.validate();
779        assert_eq!(status, LicenseStatus::Invalid);
780    }
781
782    #[test]
783    #[serial]
784    fn test_malformed_token() {
785        env::remove_var(DEV_MODE_ENV);
786        let validator = LicenseValidator::new(Some("not.valid".to_string()), None, 72);
787        let status = validator.validate();
788        assert_eq!(status, LicenseStatus::Invalid);
789    }
790
791    #[test]
792    #[serial]
793    fn test_invalid_base64_token() {
794        env::remove_var(DEV_MODE_ENV);
795        let validator = LicenseValidator::new(Some("xxx.!!!invalid!!!.zzz".to_string()), None, 72);
796        let status = validator.validate();
797        assert_eq!(status, LicenseStatus::Invalid);
798    }
799
800    #[test]
801    #[serial]
802    fn test_invalid_json_payload() {
803        env::remove_var(DEV_MODE_ENV);
804        let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"RS256\"}");
805        let payload = URL_SAFE_NO_PAD.encode(b"not json");
806        let sig = URL_SAFE_NO_PAD.encode(b"sig");
807        let token = format!("{}.{}.{}", header, payload, sig);
808
809        let validator = LicenseValidator::new(Some(token), None, 72);
810        let status = validator.validate();
811        assert_eq!(status, LicenseStatus::Invalid);
812    }
813
814    #[tokio::test]
815    #[serial]
816    async fn test_tenant_id_dev_mode() {
817        env::set_var(DEV_MODE_ENV, "1");
818        let validator = LicenseValidator::new(None, None, 72);
819        validator.validate();
820        assert_eq!(validator.tenant_id().await, "dev-tenant");
821        env::remove_var(DEV_MODE_ENV);
822    }
823
824    #[tokio::test]
825    #[serial]
826    async fn test_plan_dev_mode() {
827        env::set_var(DEV_MODE_ENV, "1");
828        let validator = LicenseValidator::new(None, None, 72);
829        validator.validate();
830        assert_eq!(validator.plan().await, "dev");
831        env::remove_var(DEV_MODE_ENV);
832    }
833
834    #[tokio::test]
835    #[serial]
836    async fn test_tenant_id_with_token() {
837        env::remove_var(DEV_MODE_ENV);
838        let token = create_test_jwt("my-tenant-123", "enterprise", 86400, -3600, 72);
839        let validator = LicenseValidator::new(Some(token), None, 72);
840        validator.validate();
841        assert_eq!(validator.tenant_id().await, "my-tenant-123");
842    }
843
844    #[tokio::test]
845    #[serial]
846    async fn test_plan_with_token() {
847        env::remove_var(DEV_MODE_ENV);
848        let token = create_test_jwt("test-tenant", "enterprise", 86400, -3600, 72);
849        let validator = LicenseValidator::new(Some(token), None, 72);
850        validator.validate();
851        assert_eq!(validator.plan().await, "enterprise");
852    }
853
854    #[tokio::test]
855    #[serial]
856    async fn test_current_status() {
857        env::remove_var(DEV_MODE_ENV);
858        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
859        let validator = LicenseValidator::new(Some(token), None, 72);
860
861        // Before validation
862        assert_eq!(validator.current_status().await, LicenseStatus::Unlicensed);
863
864        // After validation
865        validator.validate();
866        assert_eq!(validator.current_status().await, LicenseStatus::Valid);
867    }
868
869    #[tokio::test]
870    #[serial]
871    async fn test_license_metadata_valid() {
872        env::remove_var(DEV_MODE_ENV);
873        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
874        let validator = LicenseValidator::new(Some(token), None, 72);
875        validator.validate();
876
877        let meta = validator.get_license_metadata().await;
878        assert_eq!(meta.status, "valid");
879        assert_eq!(meta.tenant_id, "test-tenant");
880        assert_eq!(meta.plan, "professional");
881        assert!(!meta.quarantine);
882        assert!(meta.grace_started_at.is_none());
883    }
884
885    #[tokio::test]
886    #[serial]
887    async fn test_license_metadata_grace_period() {
888        env::remove_var(DEV_MODE_ENV);
889        let token = create_test_jwt("test-tenant", "professional", -3600, -7200, 72);
890        let validator = LicenseValidator::new(Some(token), None, 72);
891        validator.validate();
892
893        let meta = validator.get_license_metadata().await;
894        assert_eq!(meta.status, "grace_period");
895        assert!(meta.quarantine);
896        assert!(meta.grace_started_at.is_some());
897        assert!(meta.grace_expires_at.is_some());
898    }
899
900    #[tokio::test]
901    #[serial]
902    async fn test_license_metadata_dev_mode() {
903        env::set_var(DEV_MODE_ENV, "1");
904        let validator = LicenseValidator::new(None, None, 72);
905        validator.validate();
906
907        let meta = validator.get_license_metadata().await;
908        assert_eq!(meta.status, "dev_mode");
909        assert_eq!(meta.tenant_id, "dev-tenant");
910        assert_eq!(meta.plan, "dev");
911        assert!(!meta.quarantine);
912        env::remove_var(DEV_MODE_ENV);
913    }
914
915    #[test]
916    #[serial]
917    fn test_require_valid_with_valid_license() {
918        env::remove_var(DEV_MODE_ENV);
919        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
920        let validator = LicenseValidator::new(Some(token), None, 72);
921        validator.require_valid().unwrap();
922    }
923
924    #[test]
925    #[serial]
926    fn test_require_valid_dev_mode() {
927        env::set_var(DEV_MODE_ENV, "1");
928        let validator = LicenseValidator::new(None, None, 72);
929        validator.require_valid().unwrap();
930        env::remove_var(DEV_MODE_ENV);
931    }
932
933    #[test]
934    #[serial]
935    fn test_require_valid_grace_period() {
936        env::remove_var(DEV_MODE_ENV);
937        let token = create_test_jwt("test-tenant", "professional", -3600, -7200, 72);
938        let validator = LicenseValidator::new(Some(token), None, 72);
939        validator.require_valid().unwrap();
940    }
941
942    #[test]
943    #[serial]
944    fn test_require_valid_expired() {
945        env::remove_var(DEV_MODE_ENV);
946        let token = create_test_jwt("test-tenant", "professional", -360000, -360000, 72);
947        let validator = LicenseValidator::new(Some(token), None, 72);
948        let result = validator.require_valid();
949        assert!(matches!(result.unwrap_err(), LicenseError::Expired));
950    }
951
952    #[test]
953    #[serial]
954    fn test_require_valid_no_token() {
955        env::remove_var(DEV_MODE_ENV);
956        let validator = LicenseValidator::new(None, None, 72);
957        let result = validator.require_valid();
958        assert!(matches!(result.unwrap_err(), LicenseError::NoToken));
959    }
960
961    #[test]
962    #[serial]
963    fn test_require_valid_invalid() {
964        env::remove_var(DEV_MODE_ENV);
965        let validator = LicenseValidator::new(Some("invalid.jwt.token".to_string()), None, 72);
966        let result = validator.require_valid();
967        assert!(matches!(
968            result.unwrap_err(),
969            LicenseError::ValidationFailed(_)
970        ));
971    }
972
973    #[test]
974    fn test_license_claims_defaults() {
975        let claims = LicenseClaims::default();
976        assert_eq!(claims.issuer, "");
977        assert_eq!(claims.subject, "");
978        assert_eq!(claims.tenant_id, "");
979        assert_eq!(claims.plan, "");
980        assert!(claims.features.is_empty());
981        // Note: Default derive uses 0, serde default uses DEFAULT_GRACE_PERIOD_HOURS
982        // The grace_period_hours is properly set via serde deserialization
983        assert_eq!(claims.grace_period_hours, 0);
984    }
985
986    #[test]
987    fn test_license_metadata_serialization() {
988        let meta = LicenseMetadata {
989            status: "valid".to_string(),
990            tenant_id: "test".to_string(),
991            plan: "pro".to_string(),
992            quarantine: false,
993            grace_started_at: None,
994            grace_expires_at: None,
995            last_online_check: None,
996        };
997
998        let json = serde_json::to_string(&meta).unwrap();
999        assert!(json.contains("_license_status"));
1000        assert!(json.contains("_license_tenant_id"));
1001        assert!(json.contains("_quarantine"));
1002    }
1003
1004    #[test]
1005    fn test_license_error_display() {
1006        let err = LicenseError::NoToken;
1007        assert_eq!(err.to_string(), "No license token provided");
1008
1009        let err = LicenseError::Expired;
1010        assert_eq!(err.to_string(), "License expired");
1011
1012        let err = LicenseError::InvalidFormat("bad format".to_string());
1013        assert!(err.to_string().contains("bad format"));
1014
1015        let err = LicenseError::ValidationFailed("test".to_string());
1016        assert!(err.to_string().contains("test"));
1017    }
1018
1019    #[tokio::test]
1020    #[serial]
1021    async fn test_validate_online_no_endpoint() {
1022        env::remove_var(DEV_MODE_ENV);
1023        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
1024        // No endpoint provided, should fall back to offline validation
1025        let validator = LicenseValidator::new(Some(token), None, 72);
1026        let status = validator.validate_online().await;
1027        assert_eq!(status, LicenseStatus::Valid);
1028    }
1029
1030    #[tokio::test]
1031    #[serial]
1032    async fn test_validate_online_dev_mode() {
1033        env::set_var(DEV_MODE_ENV, "1");
1034        let validator = LicenseValidator::new(
1035            None,
1036            Some("https://api.phycloud.phyware.io".to_string()),
1037            72,
1038        );
1039        let status = validator.validate_online().await;
1040        assert_eq!(status, LicenseStatus::DevMode);
1041        env::remove_var(DEV_MODE_ENV);
1042    }
1043}