api_openai 0.3.0

OpenAI's API for accessing large language models (LLMs).
Documentation
//! Minimal error path testing for Secret module (standalone tests)

use std::fs;
use std::path::Path;
use tempfile::NamedTempFile;

// Manual re-implementation of basic Secret functionality for testing
// Since we can't use the full api_openai crate due to dependency issues

/// Simple API key validation function (extracted from `Secret::new` logic)
fn validate_api_key_format( secret : &str ) -> Result< (), String >
{
  let trimmed = secret.trim();

  // Check minimum length
  if trimmed.len() < 10
  {
    return Err( "API key too short - minimum 10 characters required".to_string() );
  }

  // Check maximum reasonable length (prevent extremely long strings)
  if trimmed.len() > 200
  {
    return Err( "API key too long - maximum 200 characters allowed".to_string() );
  }

  // Check for OpenAI API key prefix
  if !trimmed.starts_with( "sk-" )
  {
    return Err( "API key must start with 'sk-' prefix".to_string() );
  }

  // Check for valid characters after prefix
  let key_part = &trimmed[ 3.. ];
  if key_part.is_empty()
  {
    return Err( "API key missing content after 'sk-' prefix".to_string() );
  }

  // Validate character set (alphanumeric and common special characters)
  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( () )
}

/// Load secret from environment variable (extracted logic)
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() )
}

/// Load secret from file path (extracted logic)
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 that API key validation rejects minimum length violations
#[ test ]
fn test_api_key_validation_min_length()
{
  // Test empty string
  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" );

  // Test very short string (will hit minimum length check first)
  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" );

  // Test missing content after prefix (should be long enough but have empty content)
  // This is actually impossible with the current logic since we check length first
  // But we can test that a valid 10-character key works
  let result = validate_api_key_format( "sk-1234567" );
  assert!( result.is_ok(), "10-character secret should be valid" );
}

/// Test that API key validation rejects maximum length violations
#[ test ]
fn test_api_key_validation_max_length()
{
  // Create a very long string (over 200 chars)
  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 that API key validation enforces prefix requirement
#[ 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 that API key validation enforces character set restrictions
#[ 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 that API key validation accepts valid character sets
#[ test ]
fn test_api_key_validation_valid_characters()
{
  let standard_key = format!( "sk-{}", "a".repeat( 48 ) ); // Standard OpenAI key length
  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 environment variable loading with missing variable
#[ 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 environment variable loading with invalid format
#[ test ]
fn test_env_loading_invalid_format()
{
  // Set invalid API key format in environment
  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" );

  // Clean up
  std ::env::remove_var( "TEST_INVALID_API_KEY" );
}

/// Test environment variable loading with valid format
#[ test ]
fn test_env_loading_valid_format()
{
  // Set valid API key format in environment
  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" );

  // Clean up
  std ::env::remove_var( "TEST_VALID_API_KEY" );
}

/// Test file loading with nonexistent file
#[ 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 file loading with invalid format in file
#[ 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 file loading with valid format in file
#[ 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 file loading handles whitespace correctly
#[ 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 file loading with empty file
#[ 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 file loading with only whitespace
#[ 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 error message formatting and completeness
#[ test ]
fn test_error_message_quality()
{
  // Test that error messages contain sufficient detail for debugging

  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}" );
    }

    // Error messages should not be too short (at least 20 characters)
    assert!( error_msg.len() >= 20, "Error message for '{test_name}' should be descriptive : {error_msg}" );
  }
}