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.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        let payload_bytes = URL_SAFE_NO_PAD
574            .decode(parts[1])
575            .map_err(|e| LicenseError::DecodeError(e.to_string()))?;
576
577        let payload: JwtPayload = serde_json::from_slice(&payload_bytes)
578            .map_err(|e| LicenseError::DecodeError(e.to_string()))?;
579
580        // Convert to LicenseClaims
581        let claims = LicenseClaims {
582            issuer: payload.iss,
583            subject: payload.sub.clone(),
584            audience: payload.aud.unwrap_or_default(),
585            issued_at: payload.iat.and_then(|ts| DateTime::from_timestamp(ts, 0)),
586            expires_at: payload.exp.and_then(|ts| DateTime::from_timestamp(ts, 0)),
587            not_before: payload.nbf.and_then(|ts| DateTime::from_timestamp(ts, 0)),
588            tenant_id: payload.tenant_id.unwrap_or(payload.sub),
589            plan: payload.plan.unwrap_or_else(|| "free".to_string()),
590            features: payload.features.unwrap_or_default(),
591            limits: payload.limits.unwrap_or(serde_json::Value::Null),
592            grace_period_hours: payload
593                .grace_period_hours
594                .unwrap_or(DEFAULT_GRACE_PERIOD_HOURS),
595        };
596
597        Ok(claims)
598    }
599
600    /// Require a valid license, returning an error if not valid
601    pub fn require_valid(&self) -> Result<(), LicenseError> {
602        let status = self.validate();
603
604        match status {
605            LicenseStatus::DevMode | LicenseStatus::Valid | LicenseStatus::GracePeriod => Ok(()),
606            LicenseStatus::Expired => Err(LicenseError::Expired),
607            LicenseStatus::Unlicensed => Err(LicenseError::NoToken),
608            LicenseStatus::Invalid => Err(LicenseError::ValidationFailed(
609                "Invalid license".to_string(),
610            )),
611        }
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
619    use serial_test::serial;
620
621    /// Helper to create a test JWT token
622    fn create_test_jwt(
623        tenant_id: &str,
624        plan: &str,
625        exp_offset_secs: i64,
626        nbf_offset_secs: i64,
627        grace_period_hours: i64,
628    ) -> String {
629        let now = Utc::now().timestamp();
630
631        let header = serde_json::json!({
632            "alg": "RS256",
633            "typ": "JWT"
634        });
635
636        let payload = serde_json::json!({
637            "iss": "https://license.phyware.io",
638            "sub": tenant_id,
639            "aud": "phytrace-sdk",
640            "iat": now,
641            "exp": now + exp_offset_secs,
642            "nbf": now + nbf_offset_secs,
643            "tenant_id": tenant_id,
644            "plan": plan,
645            "features": ["telemetry", "safety_analysis"],
646            "limits": {"events_per_day": 1000000},
647            "grace_period_hours": grace_period_hours
648        });
649
650        let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
651        let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
652        let sig_b64 = URL_SAFE_NO_PAD.encode(b"fake_signature");
653
654        format!("{}.{}.{}", header_b64, payload_b64, sig_b64)
655    }
656
657    #[test]
658    #[serial]
659    fn test_dev_mode_detection() {
660        // Test with env var set
661        env::set_var(DEV_MODE_ENV, "1");
662        assert!(LicenseValidator::check_dev_mode());
663
664        env::set_var(DEV_MODE_ENV, "true");
665        assert!(LicenseValidator::check_dev_mode());
666
667        env::set_var(DEV_MODE_ENV, "yes");
668        assert!(LicenseValidator::check_dev_mode());
669
670        env::set_var(DEV_MODE_ENV, "on");
671        assert!(LicenseValidator::check_dev_mode());
672
673        env::set_var(DEV_MODE_ENV, "false");
674        assert!(!LicenseValidator::check_dev_mode());
675
676        env::set_var(DEV_MODE_ENV, "0");
677        assert!(!LicenseValidator::check_dev_mode());
678
679        env::remove_var(DEV_MODE_ENV);
680        assert!(!LicenseValidator::check_dev_mode());
681    }
682
683    #[test]
684    fn test_license_status_as_str() {
685        assert_eq!(LicenseStatus::Valid.as_str(), "valid");
686        assert_eq!(LicenseStatus::GracePeriod.as_str(), "grace_period");
687        assert_eq!(LicenseStatus::Expired.as_str(), "expired");
688        assert_eq!(LicenseStatus::Invalid.as_str(), "invalid");
689        assert_eq!(LicenseStatus::DevMode.as_str(), "dev_mode");
690        assert_eq!(LicenseStatus::Unlicensed.as_str(), "unlicensed");
691    }
692
693    #[test]
694    fn test_allows_operation() {
695        assert!(LicenseStatus::Valid.allows_operation());
696        assert!(LicenseStatus::GracePeriod.allows_operation());
697        assert!(LicenseStatus::DevMode.allows_operation());
698        assert!(!LicenseStatus::Expired.allows_operation());
699        assert!(!LicenseStatus::Invalid.allows_operation());
700        assert!(!LicenseStatus::Unlicensed.allows_operation());
701    }
702
703    #[test]
704    fn test_should_quarantine() {
705        assert!(!LicenseStatus::Valid.should_quarantine());
706        assert!(LicenseStatus::GracePeriod.should_quarantine());
707        assert!(!LicenseStatus::DevMode.should_quarantine());
708        assert!(!LicenseStatus::Expired.should_quarantine());
709        assert!(!LicenseStatus::Invalid.should_quarantine());
710        assert!(!LicenseStatus::Unlicensed.should_quarantine());
711    }
712
713    #[tokio::test]
714    #[serial]
715    async fn test_dev_mode_validation() {
716        env::set_var(DEV_MODE_ENV, "1");
717        let validator = LicenseValidator::new(None, None, 72);
718        let status = validator.validate();
719        assert_eq!(status, LicenseStatus::DevMode);
720        assert!(validator.is_dev_mode());
721        env::remove_var(DEV_MODE_ENV);
722    }
723
724    #[test]
725    #[serial]
726    fn test_no_token_validation() {
727        // Ensure dev mode is off before creating validator
728        env::remove_var(DEV_MODE_ENV);
729        // Create validator after env is set
730        let validator = LicenseValidator::new(None, None, 72);
731        let status = validator.validate();
732        assert_eq!(status, LicenseStatus::Unlicensed);
733    }
734
735    #[test]
736    #[serial]
737    fn test_valid_token_validation() {
738        env::remove_var(DEV_MODE_ENV);
739        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
740        let validator = LicenseValidator::new(Some(token), None, 72);
741        let status = validator.validate();
742        assert_eq!(status, LicenseStatus::Valid);
743    }
744
745    #[test]
746    #[serial]
747    fn test_expired_token_within_grace() {
748        env::remove_var(DEV_MODE_ENV);
749        // Expired 1 hour ago, but grace period is 72 hours
750        let token = create_test_jwt("test-tenant", "professional", -3600, -7200, 72);
751        let validator = LicenseValidator::new(Some(token), None, 72);
752        let status = validator.validate();
753        assert_eq!(status, LicenseStatus::GracePeriod);
754    }
755
756    #[test]
757    #[serial]
758    fn test_expired_token_past_grace() {
759        env::remove_var(DEV_MODE_ENV);
760        // Expired 100 hours ago, grace period is 72 hours
761        let token = create_test_jwt("test-tenant", "professional", -360000, -360000, 72);
762        let validator = LicenseValidator::new(Some(token), None, 72);
763        let status = validator.validate();
764        assert_eq!(status, LicenseStatus::Expired);
765    }
766
767    #[test]
768    #[serial]
769    fn test_not_yet_valid_token() {
770        env::remove_var(DEV_MODE_ENV);
771        // Valid in the future (nbf is 1 hour from now)
772        let token = create_test_jwt("test-tenant", "professional", 86400, 3600, 72);
773        let validator = LicenseValidator::new(Some(token), None, 72);
774        let status = validator.validate();
775        assert_eq!(status, LicenseStatus::Invalid);
776    }
777
778    #[test]
779    #[serial]
780    fn test_malformed_token() {
781        env::remove_var(DEV_MODE_ENV);
782        let validator = LicenseValidator::new(Some("not.valid".to_string()), None, 72);
783        let status = validator.validate();
784        assert_eq!(status, LicenseStatus::Invalid);
785    }
786
787    #[test]
788    #[serial]
789    fn test_invalid_base64_token() {
790        env::remove_var(DEV_MODE_ENV);
791        let validator = LicenseValidator::new(Some("xxx.!!!invalid!!!.zzz".to_string()), None, 72);
792        let status = validator.validate();
793        assert_eq!(status, LicenseStatus::Invalid);
794    }
795
796    #[test]
797    #[serial]
798    fn test_invalid_json_payload() {
799        env::remove_var(DEV_MODE_ENV);
800        let header = URL_SAFE_NO_PAD.encode(b"{\"alg\":\"RS256\"}");
801        let payload = URL_SAFE_NO_PAD.encode(b"not json");
802        let sig = URL_SAFE_NO_PAD.encode(b"sig");
803        let token = format!("{}.{}.{}", header, payload, sig);
804
805        let validator = LicenseValidator::new(Some(token), None, 72);
806        let status = validator.validate();
807        assert_eq!(status, LicenseStatus::Invalid);
808    }
809
810    #[tokio::test]
811    #[serial]
812    async fn test_tenant_id_dev_mode() {
813        env::set_var(DEV_MODE_ENV, "1");
814        let validator = LicenseValidator::new(None, None, 72);
815        validator.validate();
816        assert_eq!(validator.tenant_id().await, "dev-tenant");
817        env::remove_var(DEV_MODE_ENV);
818    }
819
820    #[tokio::test]
821    #[serial]
822    async fn test_plan_dev_mode() {
823        env::set_var(DEV_MODE_ENV, "1");
824        let validator = LicenseValidator::new(None, None, 72);
825        validator.validate();
826        assert_eq!(validator.plan().await, "dev");
827        env::remove_var(DEV_MODE_ENV);
828    }
829
830    #[tokio::test]
831    #[serial]
832    async fn test_tenant_id_with_token() {
833        env::remove_var(DEV_MODE_ENV);
834        let token = create_test_jwt("my-tenant-123", "enterprise", 86400, -3600, 72);
835        let validator = LicenseValidator::new(Some(token), None, 72);
836        validator.validate();
837        assert_eq!(validator.tenant_id().await, "my-tenant-123");
838    }
839
840    #[tokio::test]
841    #[serial]
842    async fn test_plan_with_token() {
843        env::remove_var(DEV_MODE_ENV);
844        let token = create_test_jwt("test-tenant", "enterprise", 86400, -3600, 72);
845        let validator = LicenseValidator::new(Some(token), None, 72);
846        validator.validate();
847        assert_eq!(validator.plan().await, "enterprise");
848    }
849
850    #[tokio::test]
851    #[serial]
852    async fn test_current_status() {
853        env::remove_var(DEV_MODE_ENV);
854        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
855        let validator = LicenseValidator::new(Some(token), None, 72);
856
857        // Before validation
858        assert_eq!(validator.current_status().await, LicenseStatus::Unlicensed);
859
860        // After validation
861        validator.validate();
862        assert_eq!(validator.current_status().await, LicenseStatus::Valid);
863    }
864
865    #[tokio::test]
866    #[serial]
867    async fn test_license_metadata_valid() {
868        env::remove_var(DEV_MODE_ENV);
869        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
870        let validator = LicenseValidator::new(Some(token), None, 72);
871        validator.validate();
872
873        let meta = validator.get_license_metadata().await;
874        assert_eq!(meta.status, "valid");
875        assert_eq!(meta.tenant_id, "test-tenant");
876        assert_eq!(meta.plan, "professional");
877        assert!(!meta.quarantine);
878        assert!(meta.grace_started_at.is_none());
879    }
880
881    #[tokio::test]
882    #[serial]
883    async fn test_license_metadata_grace_period() {
884        env::remove_var(DEV_MODE_ENV);
885        let token = create_test_jwt("test-tenant", "professional", -3600, -7200, 72);
886        let validator = LicenseValidator::new(Some(token), None, 72);
887        validator.validate();
888
889        let meta = validator.get_license_metadata().await;
890        assert_eq!(meta.status, "grace_period");
891        assert!(meta.quarantine);
892        assert!(meta.grace_started_at.is_some());
893        assert!(meta.grace_expires_at.is_some());
894    }
895
896    #[tokio::test]
897    #[serial]
898    async fn test_license_metadata_dev_mode() {
899        env::set_var(DEV_MODE_ENV, "1");
900        let validator = LicenseValidator::new(None, None, 72);
901        validator.validate();
902
903        let meta = validator.get_license_metadata().await;
904        assert_eq!(meta.status, "dev_mode");
905        assert_eq!(meta.tenant_id, "dev-tenant");
906        assert_eq!(meta.plan, "dev");
907        assert!(!meta.quarantine);
908        env::remove_var(DEV_MODE_ENV);
909    }
910
911    #[test]
912    #[serial]
913    fn test_require_valid_with_valid_license() {
914        env::remove_var(DEV_MODE_ENV);
915        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
916        let validator = LicenseValidator::new(Some(token), None, 72);
917        assert!(validator.require_valid().is_ok());
918    }
919
920    #[test]
921    #[serial]
922    fn test_require_valid_dev_mode() {
923        env::set_var(DEV_MODE_ENV, "1");
924        let validator = LicenseValidator::new(None, None, 72);
925        assert!(validator.require_valid().is_ok());
926        env::remove_var(DEV_MODE_ENV);
927    }
928
929    #[test]
930    #[serial]
931    fn test_require_valid_grace_period() {
932        env::remove_var(DEV_MODE_ENV);
933        let token = create_test_jwt("test-tenant", "professional", -3600, -7200, 72);
934        let validator = LicenseValidator::new(Some(token), None, 72);
935        assert!(validator.require_valid().is_ok());
936    }
937
938    #[test]
939    #[serial]
940    fn test_require_valid_expired() {
941        env::remove_var(DEV_MODE_ENV);
942        let token = create_test_jwt("test-tenant", "professional", -360000, -360000, 72);
943        let validator = LicenseValidator::new(Some(token), None, 72);
944        let result = validator.require_valid();
945        assert!(result.is_err());
946        assert!(matches!(result.unwrap_err(), LicenseError::Expired));
947    }
948
949    #[test]
950    #[serial]
951    fn test_require_valid_no_token() {
952        env::remove_var(DEV_MODE_ENV);
953        let validator = LicenseValidator::new(None, None, 72);
954        let result = validator.require_valid();
955        assert!(result.is_err());
956        assert!(matches!(result.unwrap_err(), LicenseError::NoToken));
957    }
958
959    #[test]
960    #[serial]
961    fn test_require_valid_invalid() {
962        env::remove_var(DEV_MODE_ENV);
963        let validator = LicenseValidator::new(Some("invalid.jwt.token".to_string()), None, 72);
964        let result = validator.require_valid();
965        assert!(result.is_err());
966        assert!(matches!(
967            result.unwrap_err(),
968            LicenseError::ValidationFailed(_)
969        ));
970    }
971
972    #[test]
973    fn test_license_claims_defaults() {
974        let claims = LicenseClaims::default();
975        assert_eq!(claims.issuer, "");
976        assert_eq!(claims.subject, "");
977        assert_eq!(claims.tenant_id, "");
978        assert_eq!(claims.plan, "");
979        assert!(claims.features.is_empty());
980        // Note: Default derive uses 0, serde default uses DEFAULT_GRACE_PERIOD_HOURS
981        // The grace_period_hours is properly set via serde deserialization
982        assert_eq!(claims.grace_period_hours, 0);
983    }
984
985    #[test]
986    fn test_license_metadata_serialization() {
987        let meta = LicenseMetadata {
988            status: "valid".to_string(),
989            tenant_id: "test".to_string(),
990            plan: "pro".to_string(),
991            quarantine: false,
992            grace_started_at: None,
993            grace_expires_at: None,
994            last_online_check: None,
995        };
996
997        let json = serde_json::to_string(&meta).unwrap();
998        assert!(json.contains("_license_status"));
999        assert!(json.contains("_license_tenant_id"));
1000        assert!(json.contains("_quarantine"));
1001    }
1002
1003    #[test]
1004    fn test_license_error_display() {
1005        let err = LicenseError::NoToken;
1006        assert_eq!(err.to_string(), "No license token provided");
1007
1008        let err = LicenseError::Expired;
1009        assert_eq!(err.to_string(), "License expired");
1010
1011        let err = LicenseError::InvalidFormat("bad format".to_string());
1012        assert!(err.to_string().contains("bad format"));
1013
1014        let err = LicenseError::ValidationFailed("test".to_string());
1015        assert!(err.to_string().contains("test"));
1016    }
1017
1018    #[tokio::test]
1019    #[serial]
1020    async fn test_validate_online_no_endpoint() {
1021        env::remove_var(DEV_MODE_ENV);
1022        let token = create_test_jwt("test-tenant", "professional", 86400, -3600, 72);
1023        // No endpoint provided, should fall back to offline validation
1024        let validator = LicenseValidator::new(Some(token), None, 72);
1025        let status = validator.validate_online().await;
1026        assert_eq!(status, LicenseStatus::Valid);
1027    }
1028
1029    #[tokio::test]
1030    #[serial]
1031    async fn test_validate_online_dev_mode() {
1032        env::set_var(DEV_MODE_ENV, "1");
1033        let validator =
1034            LicenseValidator::new(None, Some("https://api.phycloud.io".to_string()), 72);
1035        let status = validator.validate_online().await;
1036        assert_eq!(status, LicenseStatus::DevMode);
1037        env::remove_var(DEV_MODE_ENV);
1038    }
1039}