//! Integration tests for FileTokenStorage encryption //! //! These tests verify: //! - Encrypted token storage and retrieval //! - Backward compatibility (hex → encrypted migration) //! - Encryption key derivation consistency //! - Error handling for corrupted/invalid data //! //! Note: Requires test-utils feature to access with_directory() method #![cfg(feature = "test-utils")] use anyhow::Result; use std::fs; use std::path::PathBuf; use tempfile::TempDir; use tli::auth::token_manager::{FileTokenStorage, TokenStorage}; /// Helper: Create FileTokenStorage with temporary directory fn create_test_storage() -> Result<(FileTokenStorage, TempDir)> { let temp_dir = TempDir::new()?; let storage = FileTokenStorage::with_directory(temp_dir.path().to_path_buf())?; Ok((storage, temp_dir)) } /// Helper: Get token file path fn get_token_path(temp_dir: &TempDir, token_type: &str) -> PathBuf { temp_dir.path().join(format!("{}_token", token_type)) } #[tokio::test] async fn test_file_storage_encrypted_roundtrip() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let test_token = "test_access_token_12345"; // Store token storage.store_access_token(test_token).await?; // Verify file contains "ENC:" prefix (not hex) let token_path = get_token_path(&temp_dir, "access"); let file_contents = fs::read_to_string(&token_path)?; assert!( file_contents.starts_with("ENC:"), "Token file should have ENC: prefix, got: {}", &file_contents[..10.min(file_contents.len())] ); // Verify it's NOT hex format assert!( !file_contents.chars().all(|c| c.is_ascii_hexdigit()), "Token should not be in hex format" ); // Retrieve token and verify it matches original let retrieved = storage.get_access_token().await?; assert_eq!( retrieved.as_deref(), Some(test_token), "Retrieved token should match original" ); // Cleanup drop(storage); Ok(()) } #[tokio::test] async fn test_file_storage_migration_hex_to_encrypted() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let test_token = "test_migration_token_67890"; let token_path = get_token_path(&temp_dir, "access"); // Manually create hex-encoded token file (Wave 154 format) let hex_encoded = hex::encode(test_token.as_bytes()); fs::write(&token_path, &hex_encoded)?; println!("Created hex token file: {}", hex_encoded); // Use FileTokenStorage to read (should detect hex and decode) let retrieved = storage.get_access_token().await?; assert_eq!( retrieved.as_deref(), Some(test_token), "Should successfully read hex-encoded token" ); // Use FileTokenStorage to write (should upgrade to encrypted) storage.store_access_token(test_token).await?; // Read file directly, verify "ENC:" prefix let file_contents = fs::read_to_string(&token_path)?; assert!( file_contents.starts_with("ENC:"), "Token file should be upgraded to ENC: format, got: {}", &file_contents[..10.min(file_contents.len())] ); // Verify it's no longer hex assert!( !file_contents.chars().all(|c| c.is_ascii_hexdigit()), "Token should no longer be in hex format" ); // Verify token is still readable let final_retrieved = storage.get_access_token().await?; assert_eq!( final_retrieved.as_deref(), Some(test_token), "Token should still be readable after upgrade" ); // Cleanup drop(storage); Ok(()) } #[tokio::test] async fn test_file_storage_encryption_key_derivation() -> Result<()> { let temp_dir = TempDir::new()?; let test_token = "test_key_derivation_token"; // Store same token with first instance { let storage1 = FileTokenStorage::with_directory(temp_dir.path().to_path_buf())?; storage1.store_access_token(test_token).await?; drop(storage1); } let token_path = get_token_path(&temp_dir, "access"); let first_encrypted = fs::read_to_string(&token_path)?; assert!( first_encrypted.starts_with("ENC:"), "First encryption should use ENC: format" ); // Store same token with second instance { let storage2 = FileTokenStorage::with_directory(temp_dir.path().to_path_buf())?; storage2.store_access_token(test_token).await?; drop(storage2); } let second_encrypted = fs::read_to_string(&token_path)?; assert!( second_encrypted.starts_with("ENC:"), "Second encryption should use ENC: format" ); // Verify both can decrypt (same system key) // Note: Encrypted values will differ due to random nonce, but both should decrypt correctly let storage3 = FileTokenStorage::with_directory(temp_dir.path().to_path_buf())?; let retrieved = storage3.get_access_token().await?; assert_eq!( retrieved.as_deref(), Some(test_token), "Both encryptions should decrypt to original token" ); // Cleanup drop(storage3); Ok(()) } #[tokio::test] async fn test_file_storage_corrupted_encrypted_data() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let test_token = "test_corruption_token"; // Store encrypted token storage.store_access_token(test_token).await?; let token_path = get_token_path(&temp_dir, "access"); let original_contents = fs::read_to_string(&token_path)?; // Manually corrupt the file (change bytes after "ENC:" prefix) let corrupted = if original_contents.len() > 20 { let mut chars: Vec = original_contents.chars().collect(); // Corrupt a character in the middle of the encrypted data let idx = original_contents.len() / 2; chars[idx] = if chars[idx] == 'A' { 'B' } else { 'A' }; chars.into_iter().collect() } else { "ENC:corrupted_data".to_string() }; fs::write(&token_path, corrupted)?; // Attempt to read → should return error (GCM tag verification fails) let result = storage.get_access_token().await; assert!( result.is_err(), "Reading corrupted encrypted data should fail" ); // Cleanup drop(storage); Ok(()) } #[tokio::test] async fn test_file_storage_wrong_format_prefix() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let token_path = get_token_path(&temp_dir, "access"); // Manually create file with "WRONG:" prefix fs::write(&token_path, "WRONG:invalid_format_data")?; // Attempt to read → should return error let result = storage.get_access_token().await; assert!( result.is_err(), "Reading token with wrong prefix should fail" ); // Cleanup drop(storage); Ok(()) } #[tokio::test] async fn test_file_storage_empty_file() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let token_path = get_token_path(&temp_dir, "access"); // Create empty token file fs::write(&token_path, "")?; // Attempt to read → empty string decodes as empty hex (backward compatibility) // This returns Some("") (empty token), which is technically valid let result = storage.get_access_token().await?; assert_eq!( result, Some(String::new()), "Empty file decodes to empty token (hex backward compatibility)" ); // Cleanup drop(storage); Ok(()) } #[tokio::test] async fn test_file_storage_both_tokens_encrypted() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let access_token = "test_access_token_both"; let refresh_token = "test_refresh_token_both"; // Store both access_token and refresh_token storage.store_access_token(access_token).await?; storage.store_refresh_token(refresh_token).await?; // Verify both files use "ENC:" format let access_path = get_token_path(&temp_dir, "access"); let refresh_path = get_token_path(&temp_dir, "refresh"); let access_contents = fs::read_to_string(&access_path)?; let refresh_contents = fs::read_to_string(&refresh_path)?; assert!( access_contents.starts_with("ENC:"), "Access token should use ENC: format" ); assert!( refresh_contents.starts_with("ENC:"), "Refresh token should use ENC: format" ); // Retrieve both tokens successfully let retrieved_access = storage.get_access_token().await?; let retrieved_refresh = storage.get_refresh_token().await?; assert_eq!( retrieved_access.as_deref(), Some(access_token), "Access token should match" ); assert_eq!( retrieved_refresh.as_deref(), Some(refresh_token), "Refresh token should match" ); // Cleanup drop(storage); Ok(()) } #[tokio::test] async fn test_file_storage_encryption_idempotent() -> Result<()> { let (storage, temp_dir) = create_test_storage()?; let test_token = "test_idempotent_token"; let token_path = get_token_path(&temp_dir, "access"); // Store token storage.store_access_token(test_token).await?; // Read and re-store token 3 times for i in 1..=3 { let retrieved = storage.get_access_token().await?; assert_eq!( retrieved.as_deref(), Some(test_token), "Token should be readable on iteration {}", i ); storage.store_access_token(test_token).await?; // Verify still encrypted let contents = fs::read_to_string(&token_path)?; assert!( contents.starts_with("ENC:"), "Token should remain encrypted after iteration {}", i ); } // Verify final token is still readable let final_retrieved = storage.get_access_token().await?; assert_eq!( final_retrieved.as_deref(), Some(test_token), "Final token should still be readable" ); // Cleanup drop(storage); Ok(()) }