use std::fs;
use std::path::Path;
use tempfile::NamedTempFile;
fn validate_api_key_format( secret : &str ) -> Result< (), String >
{
let trimmed = secret.trim();
if trimmed.len() < 10
{
return Err( "API key too short - minimum 10 characters required".to_string() );
}
if trimmed.len() > 200
{
return Err( "API key too long - maximum 200 characters allowed".to_string() );
}
if !trimmed.starts_with( "sk-" )
{
return Err( "API key must start with 'sk-' prefix".to_string() );
}
let key_part = &trimmed[ 3.. ];
if key_part.is_empty()
{
return Err( "API key missing content after 'sk-' prefix".to_string() );
}
if !key_part.chars().all( | c | c.is_ascii_alphanumeric() || "_-".contains( c ) )
{
return Err( "API key contains invalid characters - only alphanumeric, underscore, and hyphen allowed".to_string() );
}
Ok( () )
}
fn load_secret_from_env( env_var : &str ) -> Result< String, String >
{
let secret_string = std::env::var( env_var )
.map_err( | e | format!( "Missing environment variable {env_var}: {e}" ) )?;
validate_api_key_format( secret_string.trim() )
.map_err( | e | format!( "Invalid secret format in {env_var}: {e}" ) )?;
Ok( secret_string.trim().to_string() )
}
fn load_secret_from_path( path : &Path ) -> Result< String, String >
{
let secret_string = fs::read_to_string( path )
.map_err( | e | format!( "Failed to read secret file : {e}" ) )?;
validate_api_key_format( secret_string.trim() )
.map_err( | e | format!( "Invalid secret format in file : {e}" ) )?;
Ok( secret_string.trim().to_string() )
}
#[ test ]
fn test_api_key_validation_min_length()
{
let result = validate_api_key_format( "" );
assert!( result.is_err(), "Empty secret should be rejected" );
assert!( result.unwrap_err().contains( "too short" ), "Error should mention length" );
let result = validate_api_key_format( "sk-" );
assert!( result.is_err(), "Very short secret should be rejected" );
assert!( result.unwrap_err().contains( "too short" ), "Error should mention length for very short key" );
let result = validate_api_key_format( "sk-1234567" );
assert!( result.is_ok(), "10-character secret should be valid" );
}
#[ test ]
fn test_api_key_validation_max_length()
{
let long_key = format!( "sk-{}", "a".repeat( 250 ) );
let result = validate_api_key_format( &long_key );
assert!( result.is_err(), "Extremely long secret should be rejected" );
assert!( result.unwrap_err().contains( "too long" ), "Error should mention length" );
}
#[ test ]
fn test_api_key_validation_prefix()
{
let test_cases = vec![
( "invalid_key", "missing sk- prefix" ),
( "pk-test123", "wrong prefix" ),
( "test-sk-123", "prefix in wrong position" ),
];
for ( invalid_key, description ) in test_cases
{
let result = validate_api_key_format( invalid_key );
assert!( result.is_err(), "Should reject key with {description}: {invalid_key}" );
assert!( result.unwrap_err().contains( "sk-" ), "Error should mention sk- prefix" );
}
}
#[ test ]
fn test_api_key_validation_character_set()
{
let test_cases = vec![
( "sk-test@123", "contains @ symbol" ),
( "sk-test#123", "contains # symbol" ),
( "sk-test$123", "contains $ symbol" ),
( "sk-test%123", "contains % symbol" ),
( "sk-test^123", "contains ^ symbol" ),
( "sk-test&123", "contains & symbol" ),
( "sk-test*123", "contains * symbol" ),
( "sk-test+123", "contains + symbol" ),
( "sk-test=123", "contains = symbol" ),
( "sk-test|123", "contains | symbol" ),
( "sk-test\\123", "contains backslash" ),
( "sk-test/123", "contains forward slash" ),
( "sk-test 123", "contains space" ),
( "sk-test\t123", "contains tab" ),
( "sk-test\n123", "contains newline" ),
];
for ( invalid_key, description ) in test_cases
{
let result = validate_api_key_format( invalid_key );
assert!( result.is_err(), "Should reject key {description}: {invalid_key}" );
assert!( result.unwrap_err().contains( "invalid characters" ), "Error should mention invalid characters" );
}
}
#[ test ]
fn test_api_key_validation_valid_characters()
{
let standard_key = format!( "sk-{}", "a".repeat( 48 ) ); let valid_keys = vec![
"sk-test123",
"sk-TEST123",
"sk-test_123",
"sk-test-123",
"sk-123456789",
"sk-abcdefghijklmnopqrstuvwxyz",
"sk-ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"sk-test_key-123",
&standard_key,
];
for valid_key in valid_keys
{
let result = validate_api_key_format( valid_key );
assert!( result.is_ok(), "Should accept valid key : {valid_key}" );
}
}
#[ test ]
fn test_env_loading_missing_var()
{
let result = load_secret_from_env( "NONEXISTENT_API_KEY_12345" );
assert!( result.is_err(), "Should fail when environment variable doesn't exist" );
assert!( result.unwrap_err().contains( "NONEXISTENT_API_KEY_12345" ), "Error should mention the env var name" );
}
#[ test ]
fn test_env_loading_invalid_format()
{
std ::env::set_var( "TEST_INVALID_API_KEY", "invalid_format" );
let result = load_secret_from_env( "TEST_INVALID_API_KEY" );
assert!( result.is_err(), "Should fail with invalid API key format" );
assert!( result.unwrap_err().contains( "Invalid secret format" ), "Error should mention format validation" );
std ::env::remove_var( "TEST_INVALID_API_KEY" );
}
#[ test ]
fn test_env_loading_valid_format()
{
std ::env::set_var( "TEST_VALID_API_KEY", "sk-test1234567890" );
let result = load_secret_from_env( "TEST_VALID_API_KEY" );
assert!( result.is_ok(), "Should succeed with valid API key format" );
std ::env::remove_var( "TEST_VALID_API_KEY" );
}
#[ test ]
fn test_file_loading_nonexistent_file()
{
let nonexistent_path = Path::new( "/tmp/nonexistent_secret_file_12345.txt" );
let result = load_secret_from_path( nonexistent_path );
assert!( result.is_err(), "Should fail when file doesn't exist" );
assert!( result.unwrap_err().contains( "Failed to read secret file" ), "Error should mention file read failure" );
}
#[ test ]
fn test_file_loading_invalid_format()
{
let temp_file = NamedTempFile::new().expect( "Failed to create temp file" );
fs ::write( temp_file.path(), "invalid_api_key_format" ).expect( "Failed to write to temp file" );
let result = load_secret_from_path( temp_file.path() );
assert!( result.is_err(), "Should fail with invalid format in file" );
assert!( result.unwrap_err().contains( "Invalid secret format in file" ), "Error should mention format validation" );
}
#[ test ]
fn test_file_loading_valid_format()
{
let temp_file = NamedTempFile::new().expect( "Failed to create temp file" );
fs ::write( temp_file.path(), "sk-test1234567890" ).expect( "Failed to write to temp file" );
let result = load_secret_from_path( temp_file.path() );
assert!( result.is_ok(), "Should succeed with valid format in file" );
}
#[ test ]
fn test_file_loading_handles_whitespace()
{
let test_cases = vec![
( " sk-test1234567890 ", "spaces around key" ),
( "\tsk-test1234567890\t", "tabs around key" ),
( "\nsk-test1234567890\n", "newlines around key" ),
( " \t\n sk-test1234567890 \n\t ", "mixed whitespace" ),
];
for ( key_with_whitespace, description ) in test_cases
{
let temp_file = NamedTempFile::new().expect( "Failed to create temp file" );
fs ::write( temp_file.path(), key_with_whitespace ).expect( "Failed to write to temp file" );
let result = load_secret_from_path( temp_file.path() );
assert!( result.is_ok(), "Should handle {description}" );
}
}
#[ test ]
fn test_file_loading_empty_file()
{
let temp_file = NamedTempFile::new().expect( "Failed to create temp file" );
fs ::write( temp_file.path(), "" ).expect( "Failed to write to temp file" );
let result = load_secret_from_path( temp_file.path() );
assert!( result.is_err(), "Should fail with empty file" );
assert!( result.unwrap_err().contains( "Invalid secret format in file" ), "Error should mention format validation" );
}
#[ test ]
fn test_file_loading_whitespace_only()
{
let whitespace_cases = vec![
" ",
"\t\t\t",
"\n\n\n",
" \t\n \t\n ",
];
for whitespace in whitespace_cases
{
let temp_file = NamedTempFile::new().expect( "Failed to create temp file" );
fs ::write( temp_file.path(), whitespace ).expect( "Failed to write to temp file" );
let result = load_secret_from_path( temp_file.path() );
assert!( result.is_err(), "Should fail with whitespace-only file" );
assert!( result.unwrap_err().contains( "Invalid secret format in file" ), "Error should mention format validation" );
}
}
#[ test ]
fn test_error_message_quality()
{
let validation_test_cases = vec![
( validate_api_key_format( "" ), "empty string", vec![ "too short", "minimum" ] ),
( validate_api_key_format( "no-prefix-1234567890" ), "missing prefix", vec![ "sk-", "prefix" ] ),
( validate_api_key_format( "sk-test@invalid" ), "invalid chars", vec![ "invalid characters" ] ),
];
let env_test_cases = vec![
( load_secret_from_env( "DEFINITELY_NONEXISTENT_VAR_12345" ).map(|_| ()), "missing env var", vec![ "DEFINITELY_NONEXISTENT_VAR_12345" ] ),
];
for ( result, test_name, expected_substrings ) in validation_test_cases.into_iter().chain(env_test_cases.into_iter())
{
assert!( result.is_err(), "Test case '{test_name}' should fail" );
let error_msg = result.unwrap_err();
println!( "Error message for '{test_name}': {error_msg}" );
for expected in expected_substrings
{
assert!( error_msg.to_lowercase().contains( &expected.to_lowercase() ),
"Error message for '{test_name}' should contain '{expected}': {error_msg}" );
}
assert!( error_msg.len() >= 20, "Error message for '{test_name}' should be descriptive : {error_msg}" );
}
}