bonds-core 0.1.6

Core library for managing symlink-based bonds with SQLite persistence
Documentation
mod common;

use bonds_core::BondError;
use common::setup;
use tempfile::TempDir;

#[test]
#[cfg_attr(windows, ignore)]
fn full_lifecycle_create_list_delete() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("my_bond");

    // Create
    let bond = mgr.create_bond(src.path(), &tgt, None).unwrap();
    assert!(tgt.exists());
    assert!(tgt.symlink_metadata().unwrap().file_type().is_symlink());

    // List
    let bonds = mgr.list_bonds().unwrap();
    assert_eq!(bonds.len(), 1);
    assert_eq!(bonds[0].id(), bond.id());

    // Delete
    mgr.delete_bond(bond.id(), false).unwrap();
    assert!(!tgt.exists());
    assert!(mgr.list_bonds().unwrap().is_empty());
}

#[test]
#[cfg_attr(windows, ignore)]
fn symlink_resolves_to_source_contents() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();

    // Write via source, read via symlink to verify link integrity.
    std::fs::write(src.path().join("hello.txt"), "world").unwrap();

    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");
    mgr.create_bond(src.path(), &tgt, None).unwrap();

    let content = std::fs::read_to_string(tgt.join("hello.txt")).unwrap();
    assert_eq!(content, "world");
}

#[test]
#[cfg_attr(windows, ignore)]
fn delete_with_target_removes_actual_files() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr.create_bond(src.path(), &tgt, None).unwrap();

    // Simulate broken bond: symlink replaced with real directory.
    std::fs::remove_file(&tgt).unwrap();
    std::fs::create_dir(&tgt).unwrap();
    std::fs::write(tgt.join("file.txt"), "data").unwrap();

    let err = mgr.delete_bond(bond.id(), false).unwrap_err();
    assert!(matches!(err, BondError::InvalidPath(_)));

    let removed = mgr.delete_bond(bond.id(), true).unwrap();
    assert_eq!(removed.id(), bond.id());
    assert!(!tgt.exists());
}

#[test]
#[cfg_attr(windows, ignore)]
fn update_bond_target() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let old_tgt = tgt_dir.path().join("old_link");
    let new_tgt = tgt_dir.path().join("new_link");

    let bond = mgr.create_bond(src.path(), &old_tgt, None).unwrap();
    let updated = mgr
        .update_bond(bond.id(), None, Some(new_tgt.clone()), None)
        .unwrap();

    assert_eq!(updated.id(), bond.id());
    assert_eq!(updated.target(), new_tgt);
    assert_eq!(updated.source(), bond.source());
    assert!(!old_tgt.exists());
    assert!(new_tgt.symlink_metadata().unwrap().file_type().is_symlink());

    let fetched = mgr.get_bond(bond.id()).unwrap();
    assert_eq!(fetched.target(), new_tgt);
}

#[test]
#[cfg_attr(windows, ignore)]
fn update_bond_source() {
    let (mgr, _db) = setup();
    let old_src = TempDir::new().unwrap();
    std::fs::write(old_src.path().join("a.txt"), "aaa").unwrap();

    let new_src = TempDir::new().unwrap();
    std::fs::write(new_src.path().join("b.txt"), "bbb").unwrap();

    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr.create_bond(old_src.path(), &tgt, None).unwrap();
    assert!(tgt.join("a.txt").exists());

    let updated = mgr
        .update_bond(bond.id(), Some(new_src.path().to_path_buf()), None, None)
        .unwrap();
    assert_eq!(updated.source(), new_src.path());

    assert!(tgt.join("b.txt").exists());
    assert!(!tgt.join("a.txt").exists());
}

#[test]
#[cfg_attr(windows, ignore)]
fn update_bond_rejects_missing_source() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr.create_bond(src.path(), &tgt, None).unwrap();

    let bad_src = std::path::PathBuf::from("/nonexistent/path");
    let err = mgr
        .update_bond(bond.id(), Some(bad_src), None, None)
        .unwrap_err();

    assert!(matches!(err, BondError::InvalidPath(_)));
    assert!(tgt.symlink_metadata().unwrap().file_type().is_symlink());
}

#[test]
#[cfg_attr(windows, ignore)]
fn update_bond_rejects_occupied_target() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr.create_bond(src.path(), &tgt, None).unwrap();

    let occupied = tgt_dir.path().join("occupied");
    std::fs::create_dir(&occupied).unwrap();

    let err = mgr
        .update_bond(bond.id(), None, Some(occupied), None)
        .unwrap_err();

    assert!(matches!(err, BondError::AlreadyExists));
}

#[test]
#[cfg_attr(windows, ignore)]
fn create_and_lookup_by_name() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr
        .create_bond(src.path(), &tgt, Some("my-project".into()))
        .unwrap();
    assert_eq!(bond.name(), Some("my-project"));

    let found = mgr.get_bond("my-project").unwrap();
    assert_eq!(found.id(), bond.id());

    let all = mgr.list_bonds().unwrap();
    assert_eq!(all[0].name(), Some("my-project"));
}

#[test]
#[cfg_attr(windows, ignore)]
fn duplicate_name_rejected() {
    let (mgr, _db) = setup();
    let src1 = TempDir::new().unwrap();
    let src2 = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();

    mgr.create_bond(src1.path(), tgt_dir.path().join("a"), Some("taken".into()))
        .unwrap();

    let err = mgr
        .create_bond(src2.path(), tgt_dir.path().join("b"), Some("taken".into()))
        .unwrap_err();

    assert!(matches!(err, BondError::AlreadyExists));
}

#[test]
#[cfg_attr(windows, ignore)]
fn get_bond_by_prefix() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr.create_bond(src.path(), &tgt, None).unwrap();

    // Prefix lookup should uniquely resolve.
    let prefix = &bond.id()[..8];
    let found = mgr.get_bond(prefix).unwrap();
    assert_eq!(found.id(), bond.id());
}

#[test]
#[cfg_attr(windows, ignore)]
fn update_bond_name() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    let bond = mgr
        .create_bond(src.path(), &tgt, Some("old-name".into()))
        .unwrap();

    let updated = mgr
        .update_bond(bond.id(), None, None, Some("new-name".into()))
        .unwrap();
    assert_eq!(updated.name(), Some("new-name"));

    let found = mgr.get_bond("new-name").unwrap();
    assert_eq!(found.id(), bond.id());
}

#[test]
#[cfg_attr(windows, ignore)]
fn create_bond_into_empty_dir() {
    let (mgr, _db) = setup();
    let src = TempDir::new().unwrap();
    let tgt_dir = TempDir::new().unwrap();
    let tgt = tgt_dir.path().join("link");

    // Target path exists as empty directory; create should replace it with symlink.
    std::fs::create_dir(&tgt).unwrap();

    let _bond = mgr.create_bond(src.path(), &tgt, None).unwrap();
    assert!(tgt.symlink_metadata().unwrap().file_type().is_symlink());
}