use serde_json::json;
#[test]
fn test_offline_entitlement_serialization() {
let entitlement = json!({
"key": "pro-features",
"expires_at": 1735689600
});
assert_eq!(entitlement["key"], "pro-features");
assert_eq!(entitlement["expires_at"], 1735689600);
}
#[test]
fn test_offline_token_payload_structure() {
let payload = json!({
"nbf": 1735689600,
"exp": 1735776000,
"iat": 1735689600,
"license_key": "TEST-KEY-123",
"product_slug": "my-product",
"mode": "hardware_locked",
"plan_key": "enterprise",
"seat_limit": 10,
"entitlements": [
{ "key": "feature-a" },
{ "key": "feature-b", "expires_at": 1735900000 }
],
"metadata": { "org": "acme" }
});
assert_eq!(payload["license_key"], "TEST-KEY-123");
assert_eq!(payload["product_slug"], "my-product");
assert_eq!(payload["mode"], "hardware_locked");
assert_eq!(payload["seat_limit"], 10);
assert_eq!(payload["entitlements"].as_array().unwrap().len(), 2);
}
#[test]
fn test_signature_structure() {
let signature = json!({
"algorithm": "ed25519",
"key_id": "key-2024-01",
"value": "SGVsbG9Xb3JsZA" });
assert_eq!(signature["algorithm"], "ed25519");
assert!(signature["key_id"].as_str().unwrap().starts_with("key-"));
assert!(!signature["value"].as_str().unwrap().is_empty());
}
#[test]
fn test_full_offline_token_response_structure() {
let now = chrono::Utc::now().timestamp();
let token_response = json!({
"object": "offline_token",
"token": {
"schema_version": 1,
"nbf": now - 3600,
"exp": now + 3600,
"iat": now - 3600,
"license_key": "TEST-KEY",
"product_slug": "test-product",
"mode": "hardware_locked",
"plan_key": "pro",
"seat_limit": 5,
"device_id": "device-123",
"kid": "key-2024",
"entitlements": [
{ "key": "feature-a" },
{ "key": "feature-b", "expires_at": now + 7200 }
],
"metadata": null
},
"canonical": "{\"canonical\":\"json\"}",
"signature": {
"algorithm": "ed25519",
"key_id": "key-2024",
"value": "base64url-encoded-signature"
}
});
assert_eq!(token_response["object"], "offline_token");
assert!(token_response["token"]["nbf"].is_i64());
assert!(token_response["token"]["exp"].is_i64());
assert_eq!(token_response["signature"]["algorithm"], "ed25519");
}
#[test]
fn test_token_not_yet_valid_detection() {
let now = chrono::Utc::now().timestamp();
let future_nbf = now + 3600; let exp = future_nbf + 7200;
let is_valid = now >= future_nbf && now <= exp;
assert!(!is_valid, "Token should not be valid before nbf");
}
#[test]
fn test_token_expired_detection() {
let now = chrono::Utc::now().timestamp();
let past_nbf = now - 7200; let past_exp = now - 3600;
let is_valid = now >= past_nbf && now <= past_exp;
assert!(!is_valid, "Token should not be valid after exp");
}
#[test]
fn test_token_valid_window() {
let now = chrono::Utc::now().timestamp();
let nbf = now - 3600; let exp = now + 3600;
let is_valid = now >= nbf && now <= exp;
assert!(is_valid, "Token should be valid within window");
}
#[test]
fn test_license_expiration_check() {
let now = chrono::Utc::now().timestamp();
let token_nbf = now - 3600;
let token_exp = now + 3600;
let license_exp = now - 1800;
let token_valid = now >= token_nbf && now <= token_exp;
let license_valid = license_exp > now;
assert!(token_valid, "Token itself should be valid");
assert!(!license_valid, "But license should be expired");
}
#[test]
fn test_grace_period_calculation() {
let now = chrono::Utc::now().timestamp();
let token_exp = now - 86400; let grace_period_days = 7;
let grace_end = token_exp + (grace_period_days * 86400);
let is_in_grace = now > token_exp && now <= grace_end;
assert!(is_in_grace, "Should be in grace period");
let way_past_grace = token_exp + (grace_period_days * 86400) + 1;
let is_past_grace = way_past_grace > grace_end;
assert!(is_past_grace, "Should be past grace period");
}
#[test]
fn test_grace_period_disabled() {
let now = chrono::Utc::now().timestamp();
let token_exp = now - 3600; let grace_period_days = 0;
let grace_end = token_exp + (grace_period_days * 86400);
let is_valid = now <= grace_end;
assert!(!is_valid, "Should not be valid with no grace period");
}
#[test]
fn test_clock_forward_detection() {
let last_seen = chrono::Utc::now().timestamp();
let current_time = last_seen + 86400 * 30;
let time_diff = current_time - last_seen;
let max_expected_diff = 86400 * 7;
let is_suspicious = time_diff > max_expected_diff;
assert!(is_suspicious, "Large forward jump should be suspicious");
}
#[test]
fn test_clock_backward_detection() {
let last_seen = chrono::Utc::now().timestamp();
let current_time = last_seen - 3600;
let is_backward = current_time < last_seen;
assert!(is_backward, "Backward time should be detected");
}
#[test]
fn test_clock_within_acceptable_skew() {
let last_seen = chrono::Utc::now().timestamp();
let max_clock_skew = 300;
let current_time = last_seen + 120; let time_diff = (current_time - last_seen).abs();
assert!(
time_diff <= max_clock_skew,
"Small drift should be acceptable"
);
let current_time = last_seen - 60; let time_diff = (current_time - last_seen).abs();
assert!(
time_diff <= max_clock_skew,
"Small NTP correction should be acceptable"
);
}
#[test]
fn test_monotonic_time_tracking() {
let timestamps: Vec<i64> = vec![1000, 1100, 1200, 1300, 1400];
let is_monotonic = timestamps.windows(2).all(|w| w[1] >= w[0]);
assert!(
is_monotonic,
"Timestamps should be monotonically increasing"
);
let bad_timestamps: Vec<i64> = vec![1000, 1100, 900, 1300]; let has_backward_jump = bad_timestamps.windows(2).any(|w| w[1] < w[0]);
assert!(has_backward_jump, "Should detect backward jump");
}
#[test]
fn test_signing_key_response_structure() {
let key_response = json!({
"object": "signing_key",
"key_id": "key-2024-01",
"algorithm": "ed25519",
"public_key": "MCowBQYDK2VwAyEAbase64encodedkey==",
"created_at": "2024-01-01T00:00:00Z",
"status": "active"
});
assert_eq!(key_response["object"], "signing_key");
assert_eq!(key_response["algorithm"], "ed25519");
assert!(key_response["public_key"].as_str().unwrap().len() > 10);
assert_eq!(key_response["status"], "active");
}
#[test]
fn test_key_id_format() {
let valid_key_ids = vec!["key-2024-01", "key-2024-02", "prod-key-1"];
for key_id in valid_key_ids {
assert!(key_id.contains("-"), "Key ID should contain separator");
assert!(key_id.len() > 5, "Key ID should be reasonably long");
}
}
#[test]
fn test_canonical_json_deterministic() {
let data = json!({
"z": "last",
"a": "first",
"m": "middle"
});
let serialized1 = serde_json::to_string(&data).unwrap();
let serialized2 = serde_json::to_string(&data).unwrap();
assert_eq!(serialized1, serialized2);
}
#[test]
fn test_canonical_json_no_whitespace() {
let data = json!({"key": "value", "number": 123});
let canonical = serde_json::to_string(&data).unwrap();
assert!(!canonical.contains(" "), "Should not have extra spaces");
assert!(!canonical.contains('\n'), "Should not have newlines");
}
#[test]
fn test_offline_entitlement_to_regular() {
let offline_entitlement = json!({
"key": "feature-x",
"expires_at": 1735689600 });
let unix_ts = offline_entitlement["expires_at"].as_i64().unwrap();
let datetime = chrono::DateTime::from_timestamp(unix_ts, 0);
assert!(datetime.is_some());
assert_eq!(datetime.unwrap().timestamp(), 1735689600);
}
#[test]
fn test_offline_entitlement_without_expiry() {
let offline_entitlement = json!({
"key": "perpetual-feature"
});
let expires_at = offline_entitlement.get("expires_at");
assert!(expires_at.is_none() || expires_at.unwrap().is_null());
}
#[test]
fn test_offline_validation_result_structure() {
let result = json!({
"object": "validation_result",
"valid": true,
"code": null,
"message": null,
"warnings": null,
"license": {
"object": "license",
"key": "TEST-KEY",
"status": "active",
"mode": "hardware_locked",
"plan_key": "pro",
"seat_limit": 5,
"active_seats": 0, "active_entitlements": [
{"key": "feature-a"},
{"key": "feature-b", "expires_at": "2025-12-31T00:00:00Z"}
],
"product": {
"slug": "test-product",
"name": "Test Product"
}
},
"activation": null });
assert!(result["valid"].as_bool().unwrap());
assert_eq!(result["license"]["status"], "active");
assert_eq!(result["license"]["active_seats"], 0);
assert!(result["activation"].is_null());
}