use workspace_tools :: { Workspace, WorkspaceError };
use std ::
{
env,
fs,
};
use tempfile ::TempDir;
static ENV_TEST_MUTEX : std ::sync ::Mutex< () > = std ::sync ::Mutex ::new( () );
fn lock_env_mutex() -> std ::sync ::MutexGuard< 'static, () >
{
ENV_TEST_MUTEX.lock().unwrap_or_else( std ::sync ::PoisonError::into_inner )
}
fn restore_env_var( key : &str, original : Option< String > )
{
match original
{
Some( value ) => env ::set_var( key, value ),
None => env ::remove_var( key ),
}
}
mod from_pro_env_tests
{
use super :: *;
#[ test ]
fn test_pro_env_with_valid_path()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original = env ::var( "PRO" ).ok();
env ::set_var( "PRO", temp_dir.path() );
let result = Workspace ::from_pro_env();
restore_env_var( "PRO", original );
assert!( result.is_ok(), "`from_pro_env()` should succeed with valid path" );
let workspace = result.unwrap();
assert_eq!( workspace.root(), temp_dir.path(), "workspace root should match $PRO path" );
}
#[ test ]
fn test_pro_env_with_nonexistent_path()
{
let _lock = lock_env_mutex();
let original = env ::var( "PRO" ).ok();
let thread_id = std ::thread ::current().id();
let timestamp = std ::time ::SystemTime ::now()
.duration_since( std ::time ::UNIX_EPOCH )
.unwrap_or_default()
.as_nanos();
let nonexistent = env ::temp_dir()
.join( format!( "nonexistent_pro_{thread_id:?}_{timestamp}" ) );
env ::set_var( "PRO", &nonexistent );
let result = Workspace ::from_pro_env();
restore_env_var( "PRO", original );
assert!( result.is_err(), "`from_pro_env()` should fail with nonexistent path" );
match result.unwrap_err()
{
WorkspaceError ::PathNotFound( path ) =>
{
assert_eq!( path, nonexistent, "error should contain the nonexistent path" );
}
other => panic!( "expected PathNotFound error, got: {other:?}" ),
}
}
#[ test ]
fn test_pro_env_missing()
{
let _lock = lock_env_mutex();
let original = env ::var( "PRO" ).ok();
env ::remove_var( "PRO" );
let result = Workspace ::from_pro_env();
restore_env_var( "PRO", original );
assert!( result.is_err(), "`from_pro_env()` should fail when PRO not set" );
match result.unwrap_err()
{
WorkspaceError ::EnvironmentVariableMissing( var ) =>
{
assert_eq!( var, "PRO", "error should mention PRO variable" );
}
other => panic!( "expected EnvironmentVariableMissing error, got: {other:?}" ),
}
}
#[ test ]
fn test_pro_env_path_normalization()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original = env ::var( "PRO" ).ok();
let redundant_path = temp_dir.path().join( "." );
env ::set_var( "PRO", &redundant_path );
let result = Workspace ::from_pro_env();
restore_env_var( "PRO", original );
assert!( result.is_ok(), "`from_pro_env()` should succeed with redundant path" );
let workspace = result.unwrap();
let root_str = workspace.root().to_string_lossy();
assert!( !root_str.ends_with( "/." ), "path should not end with '/.' after normalization" );
assert!( !root_str.contains( "/./" ), "path should not contain '/./' after normalization" );
}
}
mod from_home_dir_tests
{
use super :: *;
#[ test ]
fn test_home_dir_with_valid_home()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
env ::remove_var( "USERPROFILE" );
env ::set_var( "HOME", temp_dir.path() );
let result = Workspace ::from_home_dir();
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert!( result.is_ok(), "`from_home_dir()` should succeed with valid HOME" );
let workspace = result.unwrap();
assert_eq!( workspace.root(), temp_dir.path(), "workspace root should match $HOME path" );
}
#[ test ]
fn test_home_dir_with_valid_userprofile()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
env ::remove_var( "HOME" );
env ::set_var( "USERPROFILE", temp_dir.path() );
let result = Workspace ::from_home_dir();
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert!( result.is_ok(), "`from_home_dir()` should succeed with valid USERPROFILE" );
let workspace = result.unwrap();
assert_eq!( workspace.root(), temp_dir.path(), "workspace root should match USERPROFILE path" );
}
#[ test ]
fn test_home_dir_missing_both()
{
let _lock = lock_env_mutex();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
env ::remove_var( "HOME" );
env ::remove_var( "USERPROFILE" );
let result = Workspace ::from_home_dir();
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert!( result.is_err(), "`from_home_dir()` should fail when neither HOME nor USERPROFILE set" );
match result.unwrap_err()
{
WorkspaceError ::EnvironmentVariableMissing( var ) =>
{
assert!(
var.contains( "HOME" ) || var.contains( "USERPROFILE" ),
"error should mention HOME or USERPROFILE, got: {var}"
);
}
other => panic!( "expected EnvironmentVariableMissing error, got: {other:?}" ),
}
}
#[ test ]
fn test_home_dir_with_nonexistent_path()
{
let _lock = lock_env_mutex();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
let thread_id = std ::thread ::current().id();
let timestamp = std ::time ::SystemTime ::now()
.duration_since( std ::time ::UNIX_EPOCH )
.unwrap_or_default()
.as_nanos();
let nonexistent = env ::temp_dir()
.join( format!( "nonexistent_home_{thread_id:?}_{timestamp}" ) );
env ::remove_var( "USERPROFILE" );
env ::set_var( "HOME", &nonexistent );
let result = Workspace ::from_home_dir();
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert!( result.is_err(), "`from_home_dir()` should fail with nonexistent path" );
match result.unwrap_err()
{
WorkspaceError ::PathNotFound( path ) =>
{
assert_eq!( path, nonexistent, "error should contain the nonexistent path" );
}
other => panic!( "expected PathNotFound error, got: {other:?}" ),
}
}
#[ test ]
fn test_home_dir_priority()
{
let _lock = lock_env_mutex();
let temp_dir_home = TempDir ::new().unwrap();
let temp_dir_userprofile = TempDir ::new().unwrap();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
env ::set_var( "HOME", temp_dir_home.path() );
env ::set_var( "USERPROFILE", temp_dir_userprofile.path() );
let result = Workspace ::from_home_dir();
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert!( result.is_ok(), "`from_home_dir()` should succeed when both are set" );
let workspace = result.unwrap();
assert_eq!(
workspace.root(),
temp_dir_home.path(),
"workspace root should use HOME (priority over USERPROFILE)"
);
}
}
mod resolve_with_extended_fallbacks_tests
{
use super :: *;
#[ test ]
fn test_extended_fallbacks_uses_pro()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original_workspace = env ::var( "WORKSPACE_PATH" ).ok();
let original_pro = env ::var( "PRO" ).ok();
let original_home = env ::var( "HOME" ).ok();
let original_cwd = env ::current_dir().ok();
env ::remove_var( "WORKSPACE_PATH" );
env ::set_var( "PRO", temp_dir.path() );
let temp_home = TempDir ::new().unwrap();
env ::set_var( "HOME", temp_home.path() );
let test_cwd = TempDir ::new().unwrap();
env ::set_current_dir( test_cwd.path() ).ok();
let workspace = Workspace ::resolve_with_extended_fallbacks();
if let Some( cwd ) = original_cwd
{
env ::set_current_dir( cwd ).ok();
}
restore_env_var( "WORKSPACE_PATH", original_workspace );
restore_env_var( "PRO", original_pro );
restore_env_var( "HOME", original_home );
assert_eq!(
workspace.root(),
temp_dir.path(),
"`resolve_with_extended_fallbacks()` should use $PRO when WORKSPACE_PATH not set"
);
}
#[ test ]
fn test_extended_fallbacks_uses_home()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original_workspace = env ::var( "WORKSPACE_PATH" ).ok();
let original_pro = env ::var( "PRO" ).ok();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
let original_cwd = env ::current_dir().ok();
env ::remove_var( "WORKSPACE_PATH" );
env ::remove_var( "PRO" );
env ::remove_var( "USERPROFILE" );
env ::set_var( "HOME", temp_dir.path() );
let test_cwd = TempDir ::new().unwrap();
env ::set_current_dir( test_cwd.path() ).ok();
let workspace = Workspace ::resolve_with_extended_fallbacks();
if let Some( cwd ) = original_cwd
{
env ::set_current_dir( cwd ).ok();
}
restore_env_var( "WORKSPACE_PATH", original_workspace );
restore_env_var( "PRO", original_pro );
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert_eq!(
workspace.root(),
temp_dir.path(),
"`resolve_with_extended_fallbacks()` should use $HOME when PRO not set"
);
}
#[ test ]
fn test_extended_fallbacks_final_cwd()
{
let _lock = lock_env_mutex();
let original_workspace = env ::var( "WORKSPACE_PATH" ).ok();
let original_pro = env ::var( "PRO" ).ok();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
env ::remove_var( "WORKSPACE_PATH" );
env ::remove_var( "PRO" );
env ::remove_var( "HOME" );
env ::remove_var( "USERPROFILE" );
let workspace = Workspace ::resolve_with_extended_fallbacks();
restore_env_var( "WORKSPACE_PATH", original_workspace );
restore_env_var( "PRO", original_pro );
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
assert!(
workspace.root().exists(),
"`resolve_with_extended_fallbacks()` should always succeed with valid path"
);
assert!(
workspace.root().is_absolute(),
"resolved workspace root should be absolute path"
);
}
#[ test ]
fn test_extended_fallbacks_priority_order()
{
let _lock = lock_env_mutex();
let workspace_path_dir = TempDir ::new().unwrap();
let pro_dir = TempDir ::new().unwrap();
let home_dir = TempDir ::new().unwrap();
let original_workspace = env ::var( "WORKSPACE_PATH" ).ok();
let original_pro = env ::var( "PRO" ).ok();
let original_home = env ::var( "HOME" ).ok();
let original_cwd = env ::current_dir().ok();
env ::set_var( "WORKSPACE_PATH", workspace_path_dir.path() );
env ::set_var( "PRO", pro_dir.path() );
env ::set_var( "HOME", home_dir.path() );
let test_cwd = TempDir ::new().unwrap();
env ::set_current_dir( test_cwd.path() ).ok();
let workspace = Workspace ::resolve_with_extended_fallbacks();
if let Some( cwd ) = original_cwd
{
env ::set_current_dir( cwd ).ok();
}
restore_env_var( "WORKSPACE_PATH", original_workspace );
restore_env_var( "PRO", original_pro );
restore_env_var( "HOME", original_home );
assert_eq!(
workspace.root(),
workspace_path_dir.path(),
"`resolve_with_extended_fallbacks()` should use WORKSPACE_PATH when available (highest priority)"
);
}
}
mod integration_tests
{
use super :: *;
#[ test ]
fn test_installed_app_with_pro_loads_secrets()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original_workspace = env ::var( "WORKSPACE_PATH" ).ok();
let original_pro = env ::var( "PRO" ).ok();
let original_cwd = env ::current_dir().ok();
env ::remove_var( "WORKSPACE_PATH" );
env ::set_var( "PRO", temp_dir.path() );
let secret_dir = temp_dir.path().join( "secret" );
fs ::create_dir_all( &secret_dir ).unwrap();
let secret_file = secret_dir.join( "-secrets.sh" );
fs ::write( &secret_file, "GITHUB_TOKEN=test_token_123\nAPI_KEY=secret_key_456\n" ).unwrap();
let test_cwd = TempDir ::new().unwrap();
env ::set_current_dir( test_cwd.path() ).ok();
#[ cfg_attr( not( feature = "secrets" ), allow( unused_variables ) ) ]
let workspace = Workspace ::resolve_with_extended_fallbacks();
#[ cfg( feature = "secrets" ) ]
{
let secrets = workspace.load_secrets_from_file( "-secrets.sh" );
assert!( secrets.is_ok(), "should load secrets from $PRO/secret/ directory" );
let secrets_map = secrets.unwrap();
assert_eq!(
secrets_map.get( "GITHUB_TOKEN" ).map( String::as_str ),
Some( "test_token_123" ),
"should load GITHUB_TOKEN from secrets file"
);
assert_eq!(
secrets_map.get( "API_KEY" ).map( String::as_str ),
Some( "secret_key_456" ),
"should load API_KEY from secrets file"
);
}
if let Some( cwd ) = original_cwd
{
env ::set_current_dir( cwd ).ok();
}
restore_env_var( "WORKSPACE_PATH", original_workspace );
restore_env_var( "PRO", original_pro );
}
#[ test ]
fn test_installed_app_without_pro_uses_home()
{
let _lock = lock_env_mutex();
let temp_dir = TempDir ::new().unwrap();
let original_workspace = env ::var( "WORKSPACE_PATH" ).ok();
let original_pro = env ::var( "PRO" ).ok();
let original_home = env ::var( "HOME" ).ok();
let original_userprofile = env ::var( "USERPROFILE" ).ok();
let original_cwd = env ::current_dir().ok();
env ::remove_var( "WORKSPACE_PATH" );
env ::remove_var( "PRO" );
env ::remove_var( "USERPROFILE" );
env ::set_var( "HOME", temp_dir.path() );
let secret_dir = temp_dir.path().join( "secret" );
fs ::create_dir_all( &secret_dir ).unwrap();
let secret_file = secret_dir.join( "-secrets.sh" );
fs ::write( &secret_file, "GITHUB_TOKEN=home_token_789\n" ).unwrap();
let test_cwd = TempDir ::new().unwrap();
env ::set_current_dir( test_cwd.path() ).ok();
#[ cfg_attr( not( feature = "secrets" ), allow( unused_variables ) ) ]
let workspace = Workspace ::resolve_with_extended_fallbacks();
#[ cfg( feature = "secrets" ) ]
{
let secrets = workspace.load_secrets_from_file( "-secrets.sh" );
assert!( secrets.is_ok(), "should load secrets from $HOME/secret/ directory" );
let secrets_map = secrets.unwrap();
assert_eq!(
secrets_map.get( "GITHUB_TOKEN" ).map( String::as_str ),
Some( "home_token_789" ),
"should load GITHUB_TOKEN from HOME secrets file"
);
}
if let Some( cwd ) = original_cwd
{
env ::set_current_dir( cwd ).ok();
}
restore_env_var( "WORKSPACE_PATH", original_workspace );
restore_env_var( "PRO", original_pro );
restore_env_var( "HOME", original_home );
restore_env_var( "USERPROFILE", original_userprofile );
}
}