diff --git a/tests/cli_init.rs b/tests/cli_init.rs deleted file mode 100644 index 7247d51e750f0beafde2a84689797690b1faf5ad..0000000000000000000000000000000000000000 --- a/tests/cli_init.rs +++ /dev/null @@ -1,129 +0,0 @@ -use assert_cmd::Command; -use predicates::prelude::*; -use tempfile::TempDir; - -fn td(home: &TempDir) -> Command { - let mut cmd = Command::cargo_bin("td").unwrap(); - cmd.env("HOME", home.path()); - cmd -} - -#[test] -fn init_creates_project_snapshot_and_binding() { - let tmp = TempDir::new().unwrap(); - - td(&tmp) - .args(["init", "demo"]) - .current_dir(&tmp) - .assert() - .success() - .stderr(predicate::str::contains("initialized project 'demo'")); - - let root = tmp.path().join(".local/share/td"); - assert!(root.join("projects/demo/base.loro").is_file()); - assert!(root.join("projects/demo/changes").is_dir()); - - let bindings_path = root.join("bindings.json"); - assert!(bindings_path.is_file()); - let bindings: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(bindings_path).unwrap()).unwrap(); - let canonical_cwd = std::fs::canonicalize(tmp.path()).unwrap(); - assert_eq!( - bindings["bindings"][canonical_cwd.to_string_lossy().as_ref()] - .as_str() - .unwrap(), - "demo" - ); -} - -#[test] -fn init_fails_when_project_already_exists() { - let tmp = TempDir::new().unwrap(); - - td(&tmp) - .args(["init", "demo"]) - .current_dir(&tmp) - .assert() - .success(); - - td(&tmp) - .args(["init", "demo"]) - .current_dir(&tmp) - .assert() - .failure() - .stderr(predicate::str::contains("already exists")); -} - -#[test] -fn use_binds_another_directory_to_existing_project() { - let tmp = TempDir::new().unwrap(); - let other = tmp.path().join("other"); - std::fs::create_dir_all(&other).unwrap(); - - td(&tmp) - .args(["init", "demo"]) - .current_dir(&tmp) - .assert() - .success(); - - td(&tmp) - .args(["create", "Created from original binding"]) - .current_dir(&tmp) - .assert() - .success(); - - td(&tmp) - .args(["use", "demo"]) - .current_dir(&other) - .assert() - .success(); - - td(&tmp) - .args(["list"]) - .current_dir(&other) - .assert() - .success() - .stdout(predicate::str::contains("Created from original binding")); -} - -#[test] -fn init_json_outputs_success() { - let tmp = TempDir::new().unwrap(); - - td(&tmp) - .args(["--json", "init", "demo"]) - .current_dir(&tmp) - .assert() - .success() - .stdout(predicate::str::contains(r#""success":true"#)) - .stdout(predicate::str::contains(r#""project":"demo""#)); -} - -#[test] -fn projects_lists_all_initialized_projects() { - let tmp = TempDir::new().unwrap(); - let api_dir = tmp.path().join("api"); - let web_dir = tmp.path().join("web"); - std::fs::create_dir_all(&api_dir).unwrap(); - std::fs::create_dir_all(&web_dir).unwrap(); - - td(&tmp) - .args(["init", "api"]) - .current_dir(&api_dir) - .assert() - .success(); - - td(&tmp) - .args(["init", "web"]) - .current_dir(&web_dir) - .assert() - .success(); - - td(&tmp) - .args(["projects"]) - .current_dir(&api_dir) - .assert() - .success() - .stdout(predicate::str::contains("api")) - .stdout(predicate::str::contains("web")); -} diff --git a/tests/cli_project.rs b/tests/cli_project.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ee7b3fd8471fe4e4f3bd4633207912b540e293a --- /dev/null +++ b/tests/cli_project.rs @@ -0,0 +1,300 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use tempfile::TempDir; + +fn td(home: &TempDir) -> Command { + let mut cmd = Command::cargo_bin("td").unwrap(); + cmd.env("HOME", home.path()); + cmd +} + +// --------------------------------------------------------------------------- +// init +// --------------------------------------------------------------------------- + +#[test] +fn init_creates_project_snapshot_and_binding() { + let tmp = TempDir::new().unwrap(); + + td(&tmp) + .args(["project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .success() + .stderr(predicate::str::contains("initialized project 'demo'")); + + let root = tmp.path().join(".local/share/td"); + assert!(root.join("projects/demo/base.loro").is_file()); + assert!(root.join("projects/demo/changes").is_dir()); + + let bindings_path = root.join("bindings.json"); + assert!(bindings_path.is_file()); + let bindings: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(bindings_path).unwrap()).unwrap(); + let canonical_cwd = std::fs::canonicalize(tmp.path()).unwrap(); + assert_eq!( + bindings["bindings"][canonical_cwd.to_string_lossy().as_ref()] + .as_str() + .unwrap(), + "demo" + ); +} + +#[test] +fn init_fails_when_project_already_exists() { + let tmp = TempDir::new().unwrap(); + + td(&tmp) + .args(["project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .success(); + + td(&tmp) + .args(["project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .failure() + .stderr(predicate::str::contains("already exists")); +} + +#[test] +fn init_json_outputs_success() { + let tmp = TempDir::new().unwrap(); + + td(&tmp) + .args(["--json", "project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .success() + .stdout(predicate::str::contains(r#""success":true"#)) + .stdout(predicate::str::contains(r#""project":"demo""#)); +} + +// --------------------------------------------------------------------------- +// bind +// --------------------------------------------------------------------------- + +#[test] +fn bind_links_another_directory_to_existing_project() { + let tmp = TempDir::new().unwrap(); + let other = tmp.path().join("other"); + std::fs::create_dir_all(&other).unwrap(); + + td(&tmp) + .args(["project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .success(); + + td(&tmp) + .args(["create", "Created from original binding"]) + .current_dir(&tmp) + .assert() + .success(); + + td(&tmp) + .args(["project", "bind", "demo"]) + .current_dir(&other) + .assert() + .success(); + + td(&tmp) + .args(["list"]) + .current_dir(&other) + .assert() + .success() + .stdout(predicate::str::contains("Created from original binding")); +} + +// --------------------------------------------------------------------------- +// list +// --------------------------------------------------------------------------- + +#[test] +fn list_shows_all_initialized_projects() { + let tmp = TempDir::new().unwrap(); + let api_dir = tmp.path().join("api"); + let web_dir = tmp.path().join("web"); + std::fs::create_dir_all(&api_dir).unwrap(); + std::fs::create_dir_all(&web_dir).unwrap(); + + td(&tmp) + .args(["project", "init", "api"]) + .current_dir(&api_dir) + .assert() + .success(); + + td(&tmp) + .args(["project", "init", "web"]) + .current_dir(&web_dir) + .assert() + .success(); + + td(&tmp) + .args(["project", "list"]) + .current_dir(&api_dir) + .assert() + .success() + .stdout(predicate::str::contains("api")) + .stdout(predicate::str::contains("web")); +} + +// --------------------------------------------------------------------------- +// unbind +// --------------------------------------------------------------------------- + +#[test] +fn unbind_removes_binding_and_subsequent_list_fails() { + let tmp = TempDir::new().unwrap(); + + td(&tmp) + .args(["project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .success(); + + // Verify the binding works before we remove it. + td(&tmp).args(["list"]).current_dir(&tmp).assert().success(); + + td(&tmp) + .args(["project", "unbind"]) + .current_dir(&tmp) + .assert() + .success(); + + // Without an explicit --project flag, list should now fail. + td(&tmp) + .args(["list"]) + .current_dir(&tmp) + .assert() + .failure() + .stderr(predicate::str::contains("no project selected")); +} + +#[test] +fn unbind_fails_when_no_binding_exists() { + let tmp = TempDir::new().unwrap(); + + td(&tmp) + .args(["project", "unbind"]) + .current_dir(&tmp) + .assert() + .failure() + .stderr(predicate::str::contains("not bound to any project")); +} + +// --------------------------------------------------------------------------- +// delete +// --------------------------------------------------------------------------- + +#[test] +fn delete_removes_project_data_and_binding() { + let tmp = TempDir::new().unwrap(); + + td(&tmp) + .args(["project", "init", "demo"]) + .current_dir(&tmp) + .assert() + .success(); + + let root = tmp.path().join(".local/share/td"); + assert!(root.join("projects/demo/base.loro").is_file()); + + td(&tmp) + .args(["project", "delete", "demo"]) + .current_dir(&tmp) + .assert() + .success(); + + // Project directory should be gone. + assert!(!root.join("projects/demo").exists()); + + // Binding should be removed from bindings.json. + let bindings: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(root.join("bindings.json")).unwrap()) + .unwrap(); + let canonical_cwd = std::fs::canonicalize(tmp.path()).unwrap(); + assert!( + bindings["bindings"][canonical_cwd.to_string_lossy().as_ref()].is_null(), + "binding should have been removed after project deletion" + ); +} + +#[test] +fn delete_fails_for_nonexistent_project() { + let tmp = TempDir::new().unwrap(); + + // We still need at least one project so the data dir exists, but we + // delete one that was never created. + td(&tmp) + .args(["project", "init", "real"]) + .current_dir(&tmp) + .assert() + .success(); + + td(&tmp) + .args(["project", "delete", "nonexistent"]) + .current_dir(&tmp) + .assert() + .failure() + .stderr(predicate::str::contains("project 'nonexistent' not found")); +} + +#[test] +fn delete_cleans_up_all_bindings_for_project() { + let tmp = TempDir::new().unwrap(); + let dir_a = tmp.path().join("a"); + let dir_b = tmp.path().join("b"); + std::fs::create_dir_all(&dir_a).unwrap(); + std::fs::create_dir_all(&dir_b).unwrap(); + + // Init in dir_a, bind dir_b to the same project. + td(&tmp) + .args(["project", "init", "shared"]) + .current_dir(&dir_a) + .assert() + .success(); + + td(&tmp) + .args(["project", "bind", "shared"]) + .current_dir(&dir_b) + .assert() + .success(); + + // Sanity-check: both dirs should resolve. + td(&tmp) + .args(["list"]) + .current_dir(&dir_a) + .assert() + .success(); + td(&tmp) + .args(["list"]) + .current_dir(&dir_b) + .assert() + .success(); + + // Delete the project entirely. + td(&tmp) + .args(["project", "delete", "shared"]) + .current_dir(&dir_a) + .assert() + .success(); + + // Neither directory should have a binding any more. + let bindings: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(tmp.path().join(".local/share/td/bindings.json")).unwrap(), + ) + .unwrap(); + + let canonical_a = std::fs::canonicalize(&dir_a).unwrap(); + let canonical_b = std::fs::canonicalize(&dir_b).unwrap(); + assert!( + bindings["bindings"][canonical_a.to_string_lossy().as_ref()].is_null(), + "binding for dir_a should have been removed" + ); + assert!( + bindings["bindings"][canonical_b.to_string_lossy().as_ref()].is_null(), + "binding for dir_b should have been removed" + ); +}