use claude_profile::token::{ self, TokenStatus };
use tempfile::TempDir;
fn make_credentials( expires_at_ms : u64 ) -> String
{
format!(
r#"{{"claudeAiOauth":{{"accessToken":"tok","refreshToken":"ref","expiresAt":{expires_at_ms},"scopes":[],"subscriptionType":"max","rateLimitTier":"standard"}}}}"#
)
}
fn setup_temp_credentials( expires_at_ms : u64 ) -> TempDir
{
let dir = TempDir::new().expect( "temp dir" );
let claude = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude ).expect( "create .claude" );
std::fs::write( claude.join( ".credentials.json" ), make_credentials( expires_at_ms ) )
.expect( "write credentials" );
std::env::set_var( "HOME", dir.path() );
dir
}
fn now_ms() -> u64
{
u64::try_from(
std::time::SystemTime::now()
.duration_since( std::time::UNIX_EPOCH )
.unwrap_or_default()
.as_millis(),
)
.unwrap_or( u64::MAX )
}
#[ test ]
fn status_returns_expired_when_expires_at_in_past()
{
let _dir = setup_temp_credentials( 1 ); assert_eq!( token::status().expect( "status" ), TokenStatus::Expired );
}
#[ test ]
fn status_returns_valid_when_far_future()
{
let _dir = setup_temp_credentials( u64::MAX );
match token::status().expect( "status" )
{
TokenStatus::Valid { .. } => {}
other => panic!( "expected Valid, got {other:?}" ),
}
}
#[ test ]
fn status_returns_expiring_soon_within_default_threshold()
{
let thirty_min_ms = 30u64 * 60 * 1_000;
let expiry = now_ms().saturating_add( thirty_min_ms );
let _dir = setup_temp_credentials( expiry );
match token::status().expect( "status" )
{
TokenStatus::ExpiringSoon { .. } => {}
other => panic!( "expected ExpiringSoon, got {other:?}" ),
}
}
#[ test ]
fn status_with_threshold_zero_classifies_non_expired_as_expiring_soon()
{
let _dir = setup_temp_credentials( u64::MAX );
match token::status_with_threshold( 0 ).expect( "status" )
{
TokenStatus::Valid { .. } => {}
other => panic!( "expected Valid at threshold 0 for far-future token, got {other:?}" ),
}
}
#[ test ]
fn status_returns_error_when_credentials_file_missing()
{
let dir = TempDir::new().expect( "temp dir" );
let claude = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude ).expect( "create .claude" );
std::env::set_var( "HOME", dir.path() );
assert!( token::status().is_err(), "expected Err when credentials missing" );
}
#[ test ]
fn status_returns_error_when_expires_at_missing_from_credentials()
{
let dir = TempDir::new().expect( "temp dir" );
let claude = dir.path().join( ".claude" );
std::fs::create_dir_all( &claude ).expect( "create .claude" );
std::fs::write( claude.join( ".credentials.json" ), r#"{"claudeAiOauth":{}}"# )
.expect( "write" );
std::env::set_var( "HOME", dir.path() );
assert!( token::status().is_err(), "expected Err when expiresAt missing" );
}
#[ test ]
fn status_with_custom_threshold_classifies_correctly()
{
let one_hour_ms = 60u64 * 60 * 1_000;
let expiry = now_ms().saturating_add( one_hour_ms );
let _dir = setup_temp_credentials( expiry );
match token::status_with_threshold( 7_200 ).expect( "status" )
{
TokenStatus::ExpiringSoon { .. } => {}
other => panic!( "expected ExpiringSoon with 2h threshold for 1h expiry, got {other:?}" ),
}
}
use claude_profile::token::parse_expires_at;
#[ test ]
fn parse_expires_at_standard_format()
{
let json = r#"{"claudeAiOauth":{"expiresAt":1774016492576}}"#;
assert_eq!( parse_expires_at( json ), Some( 1_774_016_492_576 ) );
}
#[ test ]
fn parse_expires_at_with_whitespace()
{
let json = r#"{"claudeAiOauth":{"expiresAt": 1774016492576}}"#;
assert_eq!( parse_expires_at( json ), Some( 1_774_016_492_576 ) );
}
#[ test ]
fn parse_expires_at_missing_returns_none()
{
let json = r#"{"claudeAiOauth":{"accessToken":"abc"}}"#;
assert_eq!( parse_expires_at( json ), None );
}
#[ test ]
fn parse_expires_at_empty_value_returns_none()
{
let json = r#"{"expiresAt":}"#;
assert_eq!( parse_expires_at( json ), None );
}
#[ test ]
fn parse_expires_at_epoch_plus_one_is_some()
{
let json = r#"{"claudeAiOauth":{"expiresAt":1}}"#;
let result = parse_expires_at( json );
assert_eq!( result, Some( 1 ) );
assert!( now_ms() > 1, "sanity: now must be after epoch+1ms" );
}