mod common;
use anyhow::Result;
use common::TestEnv;
#[test]
fn add_file_creates_symlink_and_tracks() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_home_file(".zshrc", "# my zshrc config")
.build()?;
env.assert_home_regular_file(".zshrc");
env.assert_file_not_tracked(".zshrc");
let home_path = env.home_path(".zshrc");
let repo_file = env.profile_file_path("default", ".zshrc");
std::fs::create_dir_all(repo_file.parent().unwrap())?;
std::fs::copy(&home_path, &repo_file)?;
std::fs::remove_file(&home_path)?;
std::os::unix::fs::symlink(&repo_file, &home_path)?;
let mut tracking = env.load_tracking()?;
tracking
.symlinks
.push(dotstate::utils::symlink_manager::TrackedSymlink {
target: home_path.clone(),
source: repo_file.clone(),
created_at: chrono::Utc::now(),
backup: None,
});
env.save_tracking(&tracking)?;
let mut manifest = env.load_manifest()?;
if let Some(profile) = manifest.profiles.iter_mut().find(|p| p.name == "default") {
profile.synced_files.push(".zshrc".to_string());
}
manifest.save(&env.repo_path)?;
env.assert_is_symlink(".zshrc");
env.assert_symlink_points_to(".zshrc", &repo_file);
env.assert_file_tracked(".zshrc");
env.assert_file_in_profile("default", ".zshrc");
assert_eq!(
env.home_file_content(".zshrc"),
Some("# my zshrc config".to_string())
);
Ok(())
}
#[test]
fn synced_file_content_is_accessible_via_symlink() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_synced_file("default", ".bashrc", "export PATH=/usr/bin")
.build()?;
let content = env.home_file_content(".bashrc");
assert_eq!(content, Some("export PATH=/usr/bin".to_string()));
env.assert_is_symlink(".bashrc");
env.assert_file_tracked(".bashrc");
env.assert_file_in_profile("default", ".bashrc");
Ok(())
}
#[test]
fn add_file_handles_nested_directory_structure() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_home_file(
".config/app/settings/config.toml",
"[settings]\nkey = \"value\"",
)
.build()?;
env.assert_home_regular_file(".config/app/settings/config.toml");
let home_path = env.home_path(".config/app/settings/config.toml");
let repo_file = env.profile_file_path("default", ".config/app/settings/config.toml");
std::fs::create_dir_all(repo_file.parent().unwrap())?;
std::fs::copy(&home_path, &repo_file)?;
std::fs::remove_file(&home_path)?;
std::os::unix::fs::symlink(&repo_file, &home_path)?;
let mut tracking = env.load_tracking()?;
tracking
.symlinks
.push(dotstate::utils::symlink_manager::TrackedSymlink {
target: home_path.clone(),
source: repo_file.clone(),
created_at: chrono::Utc::now(),
backup: None,
});
env.save_tracking(&tracking)?;
env.assert_is_symlink(".config/app/settings/config.toml");
assert!(env
.repo_file_path("default/.config/app/settings/config.toml")
.exists());
Ok(())
}
#[test]
fn test_env_with_multiple_synced_files() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_synced_file("default", ".zshrc", "zsh config")
.with_synced_file("default", ".bashrc", "bash config")
.with_synced_file("default", ".vimrc", "vim config")
.build()?;
env.assert_is_symlink(".zshrc");
env.assert_is_symlink(".bashrc");
env.assert_is_symlink(".vimrc");
env.assert_file_tracked(".zshrc");
env.assert_file_tracked(".bashrc");
env.assert_file_tracked(".vimrc");
env.assert_file_in_profile("default", ".zshrc");
env.assert_file_in_profile("default", ".bashrc");
env.assert_file_in_profile("default", ".vimrc");
Ok(())
}
#[test]
fn remove_file_restores_original_and_untracks() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_synced_file("default", ".zshrc", "# original content")
.build()?;
env.assert_is_symlink(".zshrc");
env.assert_file_tracked(".zshrc");
let home_path = env.home_path(".zshrc");
let repo_file = env.profile_file_path("default", ".zshrc");
let content = std::fs::read_to_string(&repo_file)?;
std::fs::remove_file(&home_path)?;
std::fs::write(&home_path, &content)?;
let mut tracking = env.load_tracking()?;
tracking.symlinks.retain(|s| s.target != home_path);
env.save_tracking(&tracking)?;
let mut manifest = env.load_manifest()?;
if let Some(profile) = manifest.profiles.iter_mut().find(|p| p.name == "default") {
profile.synced_files.retain(|f| f != ".zshrc");
}
manifest.save(&env.repo_path)?;
env.assert_no_symlink(".zshrc");
env.assert_home_regular_file(".zshrc");
env.assert_file_not_tracked(".zshrc");
env.assert_file_not_in_profile("default", ".zshrc");
assert_eq!(
env.home_file_content(".zshrc"),
Some("# original content".to_string())
);
Ok(())
}
#[test]
fn remove_file_when_symlink_already_gone() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_synced_file("default", ".zshrc", "content")
.build()?;
env.delete_symlink_without_tracking(".zshrc")?;
assert!(!env.home_path(".zshrc").exists());
env.assert_file_tracked(".zshrc");
let mut tracking = env.load_tracking()?;
let home_path = env.home_path(".zshrc");
tracking.symlinks.retain(|s| s.target != home_path);
env.save_tracking(&tracking)?;
env.assert_file_not_tracked(".zshrc");
Ok(())
}
#[test]
fn remove_file_when_repo_source_missing() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_synced_file("default", ".zshrc", "content")
.build()?;
env.delete_repo_file_without_manifest("default/.zshrc")?;
env.assert_is_symlink(".zshrc");
assert!(!env.profile_file_path("default", ".zshrc").exists());
assert!(env.home_file_content(".zshrc").is_none());
Ok(())
}
#[test]
fn common_file_is_symlinked_when_profile_activated() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_common_file(".gitconfig", "[user]\nname = Test")
.build()?;
env.assert_is_symlink(".gitconfig");
env.assert_file_in_common(".gitconfig");
env.assert_file_tracked(".gitconfig");
let expected_target = env.common_path().join(".gitconfig");
env.assert_symlink_points_to(".gitconfig", &expected_target);
Ok(())
}
#[test]
fn common_file_not_symlinked_when_profile_not_activated() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_selected_profile("default") .with_common_file(".gitconfig", "[user]\nname = Test")
.build()?;
assert!(!env.home_path(".gitconfig").exists());
env.assert_file_in_common(".gitconfig");
assert!(env.common_path().join(".gitconfig").exists());
Ok(())
}
#[test]
fn multiple_profiles_share_common_files() -> Result<()> {
let env = TestEnv::new()
.with_profile("work")
.with_profile("home")
.with_activated_profile("work")
.with_common_file(".gitconfig", "shared git config")
.with_synced_file("work", ".work-specific", "work stuff")
.build()?;
env.assert_is_symlink(".gitconfig");
env.assert_file_in_common(".gitconfig");
env.assert_is_symlink(".work-specific");
env.assert_file_in_profile("work", ".work-specific");
env.assert_file_not_in_profile("home", ".work-specific");
Ok(())
}
#[test]
fn detect_tracking_without_symlink() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.build()?;
let source = env.profile_file_path("default", ".orphan");
std::fs::create_dir_all(source.parent().unwrap())?;
std::fs::write(&source, "orphan content")?;
env.add_tracking_without_symlink(".orphan", &source)?;
env.assert_file_tracked(".orphan");
assert!(!env.home_path(".orphan").exists());
Ok(())
}
#[test]
fn detect_symlink_without_tracking() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.build()?;
let source = env.profile_file_path("default", ".manual");
std::fs::create_dir_all(source.parent().unwrap())?;
std::fs::write(&source, "manual content")?;
env.create_symlink_without_tracking(".manual", &source)?;
env.assert_is_symlink(".manual");
env.assert_file_not_tracked(".manual");
Ok(())
}
#[test]
fn detect_manifest_tracking_mismatch() -> Result<()> {
let env = TestEnv::new()
.with_profile("default")
.with_activated_profile("default")
.with_synced_file("default", ".zshrc", "content")
.build()?;
let mut tracking = env.load_tracking()?;
tracking.symlinks.clear();
env.save_tracking(&tracking)?;
env.assert_file_in_profile("default", ".zshrc");
env.assert_file_not_tracked(".zshrc");
env.assert_is_symlink(".zshrc");
Ok(())
}
#[test]
fn test_env_creates_correct_structure() -> Result<()> {
let env = TestEnv::new().with_profile("default").with_git().build()?;
assert!(env.home_dir.exists());
assert!(env.repo_path.exists());
assert!(env.config_dir.exists());
assert!(env.backup_dir.exists());
assert!(env.repo_path.join("default").exists());
assert!(env.repo_path.join("common").exists());
assert!(env.repo_path.join(".git").exists());
assert!(env.config_path().exists());
assert!(env.repo_path.join(".dotstate-profiles.toml").exists());
Ok(())
}
#[test]
fn test_env_builder_creates_expected_state() -> Result<()> {
let env = TestEnv::new()
.with_profile("work")
.with_profile("home")
.with_activated_profile("work")
.with_synced_file("work", ".workrc", "work config")
.with_synced_file("home", ".homerc", "home config")
.with_common_file(".shared", "shared")
.with_home_file(".local", "local only")
.with_backup_enabled()
.build()?;
env.assert_profile_exists("work");
env.assert_profile_exists("home");
env.assert_active_profile("work");
env.assert_profile_activated();
env.assert_is_symlink(".workrc");
env.assert_file_in_profile("work", ".workrc");
assert!(env.profile_file_path("home", ".homerc").exists());
assert!(!env.home_path(".homerc").exists());
env.assert_is_symlink(".shared");
env.assert_file_in_common(".shared");
env.assert_home_regular_file(".local");
let config = env.load_config()?;
assert!(config.backup_enabled);
Ok(())
}