//! Integration tests for file-based token persistence //! //! Tests that tokens persist across CLI invocations (the critical fix). //! These tests verify the file storage mechanism works correctly by: //! 1. Testing FileTokenStorage directly (simulating cross-process persistence) //! 2. Verifying tokens survive "process restart" (new storage instance) //! 3. Testing cleanup operations (logout clears files) //! //! Note: These tests use FileTokenStorage directly rather than full CLI invocations //! because authentication requires a real API Gateway. The critical behavior being tested //! is that FileTokenStorage persists tokens between separate instances (simulating //! separate process invocations). //! //! FileTokenStorage is a reliable alternative to KeyringTokenStorage on Linux, //! where the keyring crate has a bug preventing cross-process token retrieval. // Suppress false-positive unused_crate_dependencies warnings // dev-dependencies are shared across ALL test targets in the crate // This test may not use all deps, but they are required by other integration tests #![allow(unused_crate_dependencies)] #![allow(clippy::tests_outside_test_module, clippy::str_to_string, clippy::non_ascii_literal, clippy::shadow_unrelated, clippy::shadow_reuse, clippy::unwrap_used, clippy::expect_used, clippy::assertions_on_result_states, clippy::use_debug, clippy::let_underscore_must_use, clippy::string_add, clippy::string_add_assign, clippy::wildcard_enum_match_arm, clippy::unseparated_literal_suffix, clippy::indexing_slicing, clippy::doc_markdown, clippy::similar_names, clippy::impl_trait_in_params, unused_imports, dead_code, clippy::panic)] use anyhow::Result; use serial_test::serial; use std::time::{SystemTime, UNIX_EPOCH}; use fxt::auth::token_manager::{FileTokenStorage, TokenStorage}; /// Helper function to generate test JWT token fn generate_test_token(username: &str, expires_in_seconds: u64) -> String { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); // Create a simple test JWT-like token (not cryptographically valid, but sufficient for testing) format!("test_token_{}_{}", username, now + expires_in_seconds) } /// Helper function to clear file storage before tests /// /// Cleans up both access and refresh token files. async fn cleanup_storage() -> Result<()> { let storage = FileTokenStorage::new()?; // Clear both access and refresh tokens let _ = storage.clear_access_token().await; let _ = storage.remove_refresh_token().await; Ok(()) } /// Test that tokens persist in file storage between "CLI invocations" (storage instances) /// /// This test simulates the critical fix: tokens stored in files by one process /// can be retrieved by another process (simulated by creating new storage instances). #[tokio::test] #[serial] async fn test_token_persistence_across_invocations() -> Result<()> { // Clean storage first cleanup_storage().await?; // Invocation 1: Login (store tokens) { let storage = FileTokenStorage::new()?; let access_token = generate_test_token("testuser", 3600); let refresh_token = generate_test_token("testuser_refresh", 7200); storage.store_access_token(&access_token).await?; storage.store_refresh_token(&refresh_token).await?; // Verify storage succeeded assert_eq!(storage.get_access_token().await?.unwrap(), access_token); assert_eq!(storage.get_refresh_token().await?.unwrap(), refresh_token); } // Storage instance dropped (simulates process exit) // Invocation 2: Check status (NEW storage instance - simulates new process) { let storage = FileTokenStorage::new()?; // Tokens should still be available from files let access_token = storage.get_access_token().await?; assert!( access_token.is_some(), "Access token should persist in files" ); assert!(access_token.unwrap().starts_with("test_token_testuser_")); let refresh_token = storage.get_refresh_token().await?; assert!( refresh_token.is_some(), "Refresh token should persist in files" ); assert!(refresh_token .unwrap() .starts_with("test_token_testuser_refresh_")); } // Storage instance dropped // Invocation 3: Use auth for command (ANOTHER new storage instance) { let storage = FileTokenStorage::new()?; // Should still have valid tokens assert!(storage.get_access_token().await?.is_some()); assert!(storage.get_refresh_token().await?.is_some()); } // Cleanup cleanup_storage().await?; Ok(()) } /// Test that logout clears tokens from file storage /// /// Verifies that when a user logs out, both access and refresh tokens /// are completely removed from the file storage. #[tokio::test] #[serial] async fn test_logout_clears_storage() -> Result<()> { cleanup_storage().await?; // Login (store tokens) let storage = FileTokenStorage::new()?; let access_token = generate_test_token("testuser", 3600); let refresh_token = generate_test_token("testuser_refresh", 7200); storage.store_access_token(&access_token).await?; storage.store_refresh_token(&refresh_token).await?; // Verify tokens exist in files assert!(storage.get_access_token().await?.is_some()); assert!(storage.get_refresh_token().await?.is_some()); // Logout (clear tokens) storage.clear_access_token().await?; storage.remove_refresh_token().await?; // Verify tokens cleared from files assert!( storage.get_access_token().await?.is_none(), "Access token should be cleared" ); assert!( storage.get_refresh_token().await?.is_none(), "Refresh token should be cleared" ); Ok(()) } /// Test token refresh updates file storage /// /// Verifies that when tokens are refreshed, the new tokens are stored /// in the files and can be retrieved. #[tokio::test] #[serial] async fn test_refresh_updates_storage() -> Result<()> { cleanup_storage().await?; // Login (initial tokens) let storage = FileTokenStorage::new()?; let original_token = generate_test_token("testuser", 3600); let refresh_token = generate_test_token("testuser_refresh", 7200); storage.store_access_token(&original_token).await?; storage.store_refresh_token(&refresh_token).await?; // Get original token let retrieved_original = storage .get_access_token() .await? .expect("Original token should exist"); assert_eq!(retrieved_original, original_token); // Simulate refresh (store new access token) tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; // Ensure timestamp differs let new_token = generate_test_token("testuser_refreshed", 3600); storage.store_access_token(&new_token).await?; // Get new token let retrieved_new = storage .get_access_token() .await? .expect("New token should exist"); // Verify token changed assert_ne!(retrieved_new, original_token, "Refresh should update token"); assert_eq!(retrieved_new, new_token, "New token should match"); // Cleanup cleanup_storage().await?; Ok(()) } /// Test multiple commands work without re-authentication /// /// Simulates running multiple authenticated commands in sequence, /// verifying that tokens persist and remain available. #[tokio::test] #[serial] async fn test_multiple_commands_with_single_login() -> Result<()> { cleanup_storage().await?; // Login once (store tokens) { let storage = FileTokenStorage::new()?; let access_token = generate_test_token("testuser", 3600); let refresh_token = generate_test_token("testuser_refresh", 7200); storage.store_access_token(&access_token).await?; storage.store_refresh_token(&refresh_token).await?; } // First process exits // Run 5 authenticated commands in sequence (each creates new storage instance) for i in 0..5 { let storage = FileTokenStorage::new()?; // Each command should have access to tokens let access_token = storage.get_access_token().await?; assert!( access_token.is_some(), "Command {} should have access to token from files", i ); let refresh_token = storage.get_refresh_token().await?; assert!( refresh_token.is_some(), "Command {} should have access to refresh token from files", i ); } // Cleanup cleanup_storage().await?; Ok(()) } /// Test that commands fail gracefully when not authenticated /// /// Verifies that attempting to retrieve tokens when none are stored /// returns None rather than erroring. #[tokio::test] #[serial] async fn test_commands_fail_without_authentication() -> Result<()> { cleanup_storage().await?; // Try to get tokens without login let storage = FileTokenStorage::new()?; // Should return None (not authenticated) let access_token = storage.get_access_token().await?; assert!( access_token.is_none(), "Should have no access token when not authenticated" ); let refresh_token = storage.get_refresh_token().await?; assert!( refresh_token.is_none(), "Should have no refresh token when not authenticated" ); Ok(()) } /// Test file storage isolation between different storage instances /// /// Note: FileTokenStorage shares the same directory for all instances, /// so this test verifies that all instances see the same tokens. /// This is actually desired behavior for CLI tools. #[tokio::test] #[serial] async fn test_storage_instance_sharing() -> Result<()> { cleanup_storage().await?; // Create two separate storage instances (simulating different CLI invocations) let storage1 = FileTokenStorage::new()?; let storage2 = FileTokenStorage::new()?; // Store token via storage1 let token = generate_test_token("testuser", 3600); storage1.store_refresh_token(&token).await?; // Verify storage2 can also see the token (shared storage) let retrieved = storage2.get_refresh_token().await?.unwrap(); assert_eq!( retrieved, token, "Both storage instances should see the same token" ); // Cleanup cleanup_storage().await?; Ok(()) } /// Test access token storage and retrieval /// /// Verifies that access tokens can be stored and retrieved independently /// from refresh tokens. #[tokio::test] #[serial] async fn test_access_token_persistence() -> Result<()> { cleanup_storage().await?; // Store access token let storage = FileTokenStorage::new()?; let access_token = generate_test_token("testuser_access", 3600); println!("Storing access token: {}", access_token); storage.store_access_token(&access_token).await?; println!("Access token stored successfully"); // Verify it was stored (same instance) let check = storage.get_access_token().await?; println!("Immediate retrieval result: {:?}", check); // Create new storage instance (simulate process restart) let storage2 = FileTokenStorage::new()?; // Retrieve access token let retrieved_result = storage2.get_access_token().await?; println!("New instance retrieval result: {:?}", retrieved_result); let retrieved = retrieved_result.expect("Access token should persist"); assert_eq!(retrieved, access_token); // Cleanup cleanup_storage().await?; Ok(()) } /// Test clear operation is idempotent /// /// Verifies that calling clear/remove multiple times doesn't error. #[tokio::test] #[serial] async fn test_clear_is_idempotent() -> Result<()> { cleanup_storage().await?; let storage = FileTokenStorage::new()?; // Store tokens storage.store_access_token("test_token").await?; storage.store_refresh_token("test_refresh").await?; // Clear once storage.clear_access_token().await?; storage.remove_refresh_token().await?; // Clear again (should not error) storage.clear_access_token().await?; storage.remove_refresh_token().await?; // Clear third time (should still not error) storage.clear_access_token().await?; storage.remove_refresh_token().await?; Ok(()) }