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 #[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 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 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 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 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 env::remove_var(DEV_MODE_ENV);
733 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 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 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 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 assert_eq!(validator.current_status().await, LicenseStatus::Unlicensed);
863
864 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 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 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}