1use 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
19pub const DEV_MODE_ENV: &str = "PHYWARE_DEV_MODE";
21
22pub const DEFAULT_GRACE_PERIOD_HOURS: i64 = 72;
24
25pub const PHYWARE_PUBLIC_KEY: &str = r#"-----BEGIN PUBLIC KEY-----
28MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy
29PLACEHOLDER_KEY_REPLACE_IN_PRODUCTION
30-----END PUBLIC KEY-----"#;
31
32#[derive(Debug, Error)]
34pub enum LicenseError {
35 #[error("No license token provided")]
37 NoToken,
38
39 #[error("Invalid JWT format: {0}")]
41 InvalidFormat(String),
42
43 #[error("JWT decode error: {0}")]
45 DecodeError(String),
46
47 #[error("License expired")]
49 Expired,
50
51 #[error("License not yet valid")]
53 NotYetValid,
54
55 #[error("License validation failed: {0}")]
57 ValidationFailed(String),
58
59 #[error("Online validation error: {0}")]
61 OnlineValidationError(String),
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
66#[serde(rename_all = "snake_case")]
67pub enum LicenseStatus {
68 Valid,
70 GracePeriod,
72 Expired,
74 Invalid,
76 DevMode,
78 #[default]
80 Unlicensed,
81}
82
83impl LicenseStatus {
84 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 pub fn allows_operation(&self) -> bool {
98 matches!(
99 self,
100 LicenseStatus::Valid | LicenseStatus::GracePeriod | LicenseStatus::DevMode
101 )
102 }
103
104 pub fn should_quarantine(&self) -> bool {
106 matches!(self, LicenseStatus::GracePeriod)
107 }
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct LicenseClaims {
113 #[serde(default)]
115 pub issuer: String,
116
117 #[serde(default)]
119 pub subject: String,
120
121 #[serde(default)]
123 pub audience: String,
124
125 #[serde(default)]
127 pub issued_at: Option<DateTime<Utc>>,
128
129 #[serde(default)]
131 pub expires_at: Option<DateTime<Utc>>,
132
133 #[serde(default)]
135 pub not_before: Option<DateTime<Utc>>,
136
137 #[serde(default)]
139 pub tenant_id: String,
140
141 #[serde(default)]
143 pub plan: String,
144
145 #[serde(default)]
147 pub features: Vec<String>,
148
149 #[serde(default)]
151 pub limits: serde_json::Value,
152
153 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct LicenseMetadata {
165 #[serde(rename = "_license_status")]
167 pub status: String,
168
169 #[serde(rename = "_license_tenant_id")]
171 pub tenant_id: String,
172
173 #[serde(rename = "_license_plan")]
175 pub plan: String,
176
177 #[serde(rename = "_quarantine")]
179 pub quarantine: bool,
180
181 #[serde(
183 rename = "_grace_period_start",
184 skip_serializing_if = "Option::is_none"
185 )]
186 pub grace_started_at: Option<String>,
187
188 #[serde(
190 rename = "_grace_period_expires",
191 skip_serializing_if = "Option::is_none"
192 )]
193 pub grace_expires_at: Option<String>,
194
195 #[serde(rename = "_last_online_check", skip_serializing_if = "Option::is_none")]
197 pub last_online_check: Option<String>,
198}
199
200#[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
227struct 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
235pub 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 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 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 pub fn is_dev_mode(&self) -> bool {
311 self.is_dev_mode.load(Ordering::Relaxed)
312 }
313
314 pub async fn current_status(&self) -> LicenseStatus {
316 self.state.read().await.current_status
317 }
318
319 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 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 pub fn validate(&self) -> LicenseStatus {
349 if self.is_dev_mode() {
351 warn!(
352 "⚠️ PHYWARE_DEV_MODE is enabled - license validation bypassed. \
353 Do not use in production!"
354 );
355 if let Ok(mut state) = self.state.try_write() {
357 state.current_status = LicenseStatus::DevMode;
358 }
359 return LicenseStatus::DevMode;
360 }
361
362 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 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 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 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 #[cfg(feature = "http")]
436 pub async fn validate_online(&self) -> LicenseStatus {
437 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 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 #[cfg(not(feature = "http"))]
523 pub async fn validate_online(&self) -> LicenseStatus {
524 self.validate()
525 }
526
527 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 fn decode_jwt(&self, token: &str) -> Result<LicenseClaims, LicenseError> {
564 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 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 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 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 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 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 env::remove_var(DEV_MODE_ENV);
729 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 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 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 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 assert_eq!(validator.current_status().await, LicenseStatus::Unlicensed);
859
860 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 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 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}