use std::time::{Duration, SystemTime};
use tracing::{debug, warn};
use super::token::{TokenInfo, TokenType, TokenValidationResult};
pub struct TokenValidator {
expiry_threshold_seconds: u64,
}
impl TokenValidator {
pub fn new(expiry_threshold_seconds: u64) -> Self {
Self {
expiry_threshold_seconds,
}
}
pub fn validate(&self, token: &TokenInfo) -> TokenValidationResult {
if token.access_token.is_empty() {
warn!("Token validation failed: empty access token");
return TokenValidationResult::Invalid("validation failed".to_string());
}
let now = SystemTime::now();
if now >= token.expires_at {
debug!("Token validation result: expired");
return TokenValidationResult::Expired;
}
let threshold = Duration::from_secs(self.expiry_threshold_seconds);
if now + threshold >= token.expires_at {
let time_until_expiry = token.expires_at.duration_since(now).unwrap_or_default();
debug!("Token validation result: expiring soon");
return TokenValidationResult::ExpiringSoon(time_until_expiry);
}
debug!("Token validation result: valid");
TokenValidationResult::Valid
}
pub fn validate_token_format(&self, token: &str) -> TokenValidationResult {
if token.is_empty() {
return TokenValidationResult::Invalid("validation failed".to_string());
}
if token.starts_with("cli_") {
if token.len() < 10 {
return TokenValidationResult::Invalid("validation failed".to_string());
}
} else if token.starts_with("t-") {
if token.len() < 8 {
return TokenValidationResult::Invalid("validation failed".to_string());
}
} else if token.starts_with("u-") {
if token.len() < 8 {
return TokenValidationResult::Invalid("validation failed".to_string());
}
} else {
return TokenValidationResult::Invalid("validation failed".to_string());
}
TokenValidationResult::Valid
}
pub fn validate_token_type(
&self,
token: &str,
expected_type: TokenType,
) -> TokenValidationResult {
let format_result = self.validate_token_format(token);
if !format_result.is_valid() {
return format_result;
}
match expected_type {
TokenType::AppAccessToken => {
if token.starts_with("cli_") {
TokenValidationResult::Valid
} else {
TokenValidationResult::Invalid("invalid app access token format".to_string())
}
}
TokenType::TenantAccessToken => {
if token.starts_with("t-") {
TokenValidationResult::Valid
} else {
TokenValidationResult::Invalid("invalid tenant access token format".to_string())
}
}
TokenType::UserAccessToken => {
if token.starts_with("u-") {
TokenValidationResult::Valid
} else {
TokenValidationResult::Invalid("invalid user access token format".to_string())
}
}
}
}
pub fn should_refresh(&self, token: &TokenInfo) -> bool {
!self.validate(token).is_valid()
}
pub fn get_remaining_time(&self, token: &TokenInfo) -> Option<Duration> {
if token.is_expired() {
None
} else {
token.time_until_expiry()
}
}
pub fn validate_batch(&self, tokens: &[TokenInfo]) -> Vec<TokenValidationResult> {
tokens.iter().map(|token| self.validate(token)).collect()
}
pub fn filter_valid_tokens<'a>(&self, tokens: &'a [TokenInfo]) -> Vec<&'a TokenInfo> {
tokens
.iter()
.filter(|token| self.validate(token).is_valid())
.collect()
}
pub fn filter_tokens_needing_refresh<'a>(&self, tokens: &'a [TokenInfo]) -> Vec<&'a TokenInfo> {
tokens
.iter()
.filter(|token| self.should_refresh(token))
.collect()
}
}
impl Default for TokenValidator {
fn default() -> Self {
Self::new(300) }
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
fn create_test_token(expires_in_secs: u64) -> TokenInfo {
TokenInfo::new(
"cli_test_token".to_string(),
TokenType::AppAccessToken,
Duration::from_secs(expires_in_secs),
"test_app".to_string(),
)
}
#[test]
fn test_valid_token() {
let validator = TokenValidator::default();
let token = create_test_token(3600);
let result = validator.validate(&token);
assert!(result.is_valid());
}
#[test]
fn test_expired_token() {
let validator = TokenValidator::default();
let token = create_test_token(0);
let result = validator.validate(&token);
assert_eq!(result, TokenValidationResult::Expired);
}
#[test]
fn test_expiring_soon_token() {
let validator = TokenValidator::new(3600); let token = create_test_token(1800);
let result = validator.validate(&token);
assert!(matches!(result, TokenValidationResult::ExpiringSoon(_)));
}
#[test]
fn test_token_format_validation() {
let validator = TokenValidator::default();
assert!(validator.validate_token_format("cli_test123").is_valid());
assert!(validator.validate_token_format("t-test123").is_valid());
assert!(validator.validate_token_format("u-test123").is_valid());
assert!(validator.validate_token_format("").is_invalid());
assert!(validator.validate_token_format("short").is_invalid());
assert!(validator
.validate_token_format("unknown_prefix")
.is_invalid());
}
#[test]
fn test_token_type_validation() {
let validator = TokenValidator::default();
assert!(validator
.validate_token_type("cli_test123", TokenType::AppAccessToken)
.is_valid());
assert!(validator
.validate_token_type("t-test123", TokenType::AppAccessToken)
.is_invalid());
assert!(validator
.validate_token_type("t-test123", TokenType::TenantAccessToken)
.is_valid());
assert!(validator
.validate_token_type("cli_test123", TokenType::TenantAccessToken)
.is_invalid());
assert!(validator
.validate_token_type("u-test123", TokenType::UserAccessToken)
.is_valid());
assert!(validator
.validate_token_type("cli_test123", TokenType::UserAccessToken)
.is_invalid());
}
#[test]
fn test_batch_validation() {
let validator = TokenValidator::default();
let tokens = vec![
create_test_token(3600), create_test_token(0), create_test_token(1800), ];
let results = validator.validate_batch(&tokens);
assert_eq!(results[0], TokenValidationResult::Valid);
assert_eq!(results[1], TokenValidationResult::Expired);
assert_eq!(results[2], TokenValidationResult::Valid);
}
#[test]
fn test_filter_tokens() {
let validator = TokenValidator::default();
let tokens = vec![
create_test_token(3600), create_test_token(0), create_test_token(1800), ];
let valid_tokens = validator.filter_valid_tokens(&tokens);
assert_eq!(valid_tokens.len(), 2);
let refresh_needed = validator.filter_tokens_needing_refresh(&tokens);
assert_eq!(refresh_needed.len(), 1); }
#[test]
fn test_validate_rejects_empty_access_token() {
let validator = TokenValidator::default();
let mut token = create_test_token(3600);
token.access_token.clear();
let result = validator.validate(&token);
assert!(matches!(result, TokenValidationResult::Invalid(_)));
}
#[test]
fn test_validate_token_format_length_boundaries() {
let validator = TokenValidator::default();
assert!(validator.validate_token_format("cli_123456").is_valid());
assert!(validator.validate_token_format("cli_12345").is_invalid());
assert!(validator.validate_token_format("t-123456").is_valid());
assert!(validator.validate_token_format("t-12345").is_invalid());
assert!(validator.validate_token_format("u-123456").is_valid());
assert!(validator.validate_token_format("u-12345").is_invalid());
}
#[test]
fn test_validate_token_type_mismatch_returns_specific_message() {
let validator = TokenValidator::default();
let result = validator.validate_token_type("u-test123", TokenType::AppAccessToken);
match result {
TokenValidationResult::Invalid(msg) => {
assert_eq!(msg, "invalid app access token format");
}
_ => panic!("expected invalid token type result"),
}
}
#[test]
fn test_should_refresh_for_expiring_soon_and_expired() {
let validator = TokenValidator::new(600);
let expiring_soon = create_test_token(300);
let expired = create_test_token(0);
let long_lived = create_test_token(3600);
assert!(validator.should_refresh(&expiring_soon));
assert!(validator.should_refresh(&expired));
assert!(!validator.should_refresh(&long_lived));
}
#[test]
fn test_get_remaining_time_for_valid_and_expired_tokens() {
let validator = TokenValidator::default();
let valid = create_test_token(1200);
let expired = create_test_token(0);
let remaining = validator
.get_remaining_time(&valid)
.expect("valid token should have remaining time");
assert!(remaining.as_secs() <= 1200);
assert!(remaining.as_secs() > 0);
assert!(validator.get_remaining_time(&expired).is_none());
}
#[test]
fn test_filter_tokens_needing_refresh_includes_expiring_soon() {
let validator = TokenValidator::new(3600);
let tokens = vec![
create_test_token(3601), create_test_token(3500), create_test_token(0), ];
let refresh_needed = validator.filter_tokens_needing_refresh(&tokens);
assert_eq!(refresh_needed.len(), 2);
}
#[test]
fn test_validate_batch_empty_list() {
let validator = TokenValidator::default();
let tokens: Vec<TokenInfo> = vec![];
let results = validator.validate_batch(&tokens);
assert!(results.is_empty());
}
#[test]
fn test_filter_valid_tokens_empty_list() {
let validator = TokenValidator::default();
let tokens: Vec<TokenInfo> = vec![];
let valid = validator.filter_valid_tokens(&tokens);
assert!(valid.is_empty());
let need_refresh = validator.filter_tokens_needing_refresh(&tokens);
assert!(need_refresh.is_empty());
}
#[test]
fn test_validator_default_configuration() {
let validator = TokenValidator::default();
let token = create_test_token(240);
let result = validator.validate(&token);
assert!(matches!(result, TokenValidationResult::ExpiringSoon(_)));
}
#[test]
fn test_validator_custom_threshold() {
let validator = TokenValidator::new(60); let token = create_test_token(120);
let result = validator.validate(&token);
assert!(result.is_valid());
let token_near = create_test_token(30); let result_near = validator.validate(&token_near);
assert!(matches!(
result_near,
TokenValidationResult::ExpiringSoon(_)
));
}
#[test]
fn test_validator_zero_threshold() {
let validator = TokenValidator::new(0);
let token = create_test_token(1);
let result = validator.validate(&token);
assert!(result.is_valid() || matches!(result, TokenValidationResult::ExpiringSoon(_)));
}
#[test]
fn test_validate_token_format_all_types() {
let validator = TokenValidator::default();
assert!(validator.validate_token_format("cli_1234567890").is_valid());
assert!(validator.validate_token_format("cli_123").is_invalid());
assert!(validator.validate_token_format("t-12345678").is_valid());
assert!(validator.validate_token_format("t-123").is_invalid());
assert!(validator.validate_token_format("u-12345678").is_valid());
assert!(validator.validate_token_format("u-123").is_invalid()); }
#[test]
fn test_validate_token_with_different_app_types() {
let validator = TokenValidator::default();
let self_build = TokenInfo::new(
"cli_self_build".to_string(),
TokenType::AppAccessToken,
Duration::from_secs(3600),
"self_build".to_string(),
);
let marketplace = TokenInfo::new(
"cli_marketplace".to_string(),
TokenType::AppAccessToken,
Duration::from_secs(3600),
"marketplace".to_string(),
);
assert!(validator.validate(&self_build).is_valid());
assert!(validator.validate(&marketplace).is_valid());
}
#[test]
fn test_validate_token_with_refresh_token() {
let validator = TokenValidator::default();
let mut token = create_test_token(3600);
token.refresh_token = Some("refresh_123".to_string());
let result = validator.validate(&token);
assert!(result.is_valid());
}
#[test]
fn test_validator_large_threshold() {
let validator = TokenValidator::new(86400); let token = create_test_token(3600);
let result = validator.validate(&token);
assert!(matches!(result, TokenValidationResult::ExpiringSoon(_)));
}
#[test]
fn test_validator_exact_boundary() {
let threshold = 300; let validator = TokenValidator::new(threshold);
let token = create_test_token(threshold);
let result = validator.validate(&token);
assert!(matches!(result, TokenValidationResult::ExpiringSoon(_)));
}
#[test]
fn test_validate_batch_all_expired() {
let validator = TokenValidator::default();
let tokens = vec![
create_test_token(0),
create_test_token(0),
create_test_token(0),
];
let results = validator.validate_batch(&tokens);
assert_eq!(results.len(), 3);
assert!(results
.iter()
.all(|r| matches!(r, TokenValidationResult::Expired)));
}
#[test]
fn test_validate_batch_all_valid() {
let validator = TokenValidator::default();
let tokens = vec![
create_test_token(3600),
create_test_token(7200),
create_test_token(600), ];
let results = validator.validate_batch(&tokens);
assert_eq!(results.len(), 3);
assert_eq!(results[0], TokenValidationResult::Valid);
assert_eq!(results[1], TokenValidationResult::Valid);
assert_eq!(results[2], TokenValidationResult::Valid);
}
#[test]
fn test_filter_valid_tokens_returns_correct_references() {
let validator = TokenValidator::default();
let tokens = vec![
TokenInfo::new(
"cli_valid1".to_string(),
TokenType::AppAccessToken,
Duration::from_secs(3600),
"test".to_string(),
),
TokenInfo::new(
"cli_expired".to_string(),
TokenType::AppAccessToken,
Duration::from_secs(0),
"test".to_string(),
),
TokenInfo::new(
"cli_valid2".to_string(),
TokenType::AppAccessToken,
Duration::from_secs(7200),
"test".to_string(),
),
];
std::thread::sleep(Duration::from_millis(5));
let valid_refs = validator.filter_valid_tokens(&tokens);
assert_eq!(valid_refs.len(), 2);
assert_eq!(valid_refs[0].access_token, "cli_valid1");
assert_eq!(valid_refs[1].access_token, "cli_valid2");
}
}