cli_project.rs

  1use assert_cmd::Command;
  2use predicates::prelude::*;
  3use tempfile::TempDir;
  4
  5fn td(home: &TempDir) -> Command {
  6    let mut cmd = Command::cargo_bin("td").unwrap();
  7    cmd.env("HOME", home.path());
  8    cmd
  9}
 10
 11// ---------------------------------------------------------------------------
 12// init
 13// ---------------------------------------------------------------------------
 14
 15#[test]
 16fn init_creates_project_snapshot_and_binding() {
 17    let tmp = TempDir::new().unwrap();
 18
 19    td(&tmp)
 20        .args(["project", "init", "demo"])
 21        .current_dir(&tmp)
 22        .assert()
 23        .success()
 24        .stderr(predicate::str::contains("initialized project 'demo'"));
 25
 26    let root = tmp.path().join(".local/share/td");
 27    assert!(root.join("projects/demo/base.loro").is_file());
 28    assert!(root.join("projects/demo/changes").is_dir());
 29
 30    let bindings_path = root.join("bindings.json");
 31    assert!(bindings_path.is_file());
 32    let bindings: serde_json::Value =
 33        serde_json::from_str(&std::fs::read_to_string(bindings_path).unwrap()).unwrap();
 34    let canonical_cwd = std::fs::canonicalize(tmp.path()).unwrap();
 35    assert_eq!(
 36        bindings["bindings"][canonical_cwd.to_string_lossy().as_ref()]
 37            .as_str()
 38            .unwrap(),
 39        "demo"
 40    );
 41}
 42
 43#[test]
 44fn init_fails_when_project_already_exists() {
 45    let tmp = TempDir::new().unwrap();
 46
 47    td(&tmp)
 48        .args(["project", "init", "demo"])
 49        .current_dir(&tmp)
 50        .assert()
 51        .success();
 52
 53    td(&tmp)
 54        .args(["project", "init", "demo"])
 55        .current_dir(&tmp)
 56        .assert()
 57        .failure()
 58        .stderr(predicate::str::contains("already exists"));
 59}
 60
 61#[test]
 62fn init_json_outputs_success() {
 63    let tmp = TempDir::new().unwrap();
 64
 65    td(&tmp)
 66        .args(["--json", "project", "init", "demo"])
 67        .current_dir(&tmp)
 68        .assert()
 69        .success()
 70        .stdout(predicate::str::contains(r#""success":true"#))
 71        .stdout(predicate::str::contains(r#""project":"demo""#));
 72}
 73
 74// ---------------------------------------------------------------------------
 75// bind
 76// ---------------------------------------------------------------------------
 77
 78#[test]
 79fn bind_links_another_directory_to_existing_project() {
 80    let tmp = TempDir::new().unwrap();
 81    let other = tmp.path().join("other");
 82    std::fs::create_dir_all(&other).unwrap();
 83
 84    td(&tmp)
 85        .args(["project", "init", "demo"])
 86        .current_dir(&tmp)
 87        .assert()
 88        .success();
 89
 90    td(&tmp)
 91        .args(["create", "Created from original binding"])
 92        .current_dir(&tmp)
 93        .assert()
 94        .success();
 95
 96    td(&tmp)
 97        .args(["project", "bind", "demo"])
 98        .current_dir(&other)
 99        .assert()
100        .success();
101
102    td(&tmp)
103        .args(["list"])
104        .current_dir(&other)
105        .assert()
106        .success()
107        .stdout(predicate::str::contains("Created from original binding"));
108}
109
110// ---------------------------------------------------------------------------
111// list
112// ---------------------------------------------------------------------------
113
114#[test]
115fn list_shows_all_initialized_projects() {
116    let tmp = TempDir::new().unwrap();
117    let api_dir = tmp.path().join("api");
118    let web_dir = tmp.path().join("web");
119    std::fs::create_dir_all(&api_dir).unwrap();
120    std::fs::create_dir_all(&web_dir).unwrap();
121
122    td(&tmp)
123        .args(["project", "init", "api"])
124        .current_dir(&api_dir)
125        .assert()
126        .success();
127
128    td(&tmp)
129        .args(["project", "init", "web"])
130        .current_dir(&web_dir)
131        .assert()
132        .success();
133
134    td(&tmp)
135        .args(["project", "list"])
136        .current_dir(&api_dir)
137        .assert()
138        .success()
139        .stdout(predicate::str::contains("api"))
140        .stdout(predicate::str::contains("web"));
141}
142
143// ---------------------------------------------------------------------------
144// unbind
145// ---------------------------------------------------------------------------
146
147#[test]
148fn unbind_removes_binding_and_subsequent_list_fails() {
149    let tmp = TempDir::new().unwrap();
150
151    td(&tmp)
152        .args(["project", "init", "demo"])
153        .current_dir(&tmp)
154        .assert()
155        .success();
156
157    // Verify the binding works before we remove it.
158    td(&tmp).args(["list"]).current_dir(&tmp).assert().success();
159
160    td(&tmp)
161        .args(["project", "unbind"])
162        .current_dir(&tmp)
163        .assert()
164        .success();
165
166    // Without an explicit --project flag, list should now fail.
167    td(&tmp)
168        .args(["list"])
169        .current_dir(&tmp)
170        .assert()
171        .failure()
172        .stderr(predicate::str::contains("no project selected"));
173}
174
175#[test]
176fn unbind_fails_when_no_binding_exists() {
177    let tmp = TempDir::new().unwrap();
178
179    td(&tmp)
180        .args(["project", "unbind"])
181        .current_dir(&tmp)
182        .assert()
183        .failure()
184        .stderr(predicate::str::contains("not bound to any project"));
185}
186
187// ---------------------------------------------------------------------------
188// delete
189// ---------------------------------------------------------------------------
190
191#[test]
192fn delete_removes_project_data_and_binding() {
193    let tmp = TempDir::new().unwrap();
194
195    td(&tmp)
196        .args(["project", "init", "demo"])
197        .current_dir(&tmp)
198        .assert()
199        .success();
200
201    let root = tmp.path().join(".local/share/td");
202    assert!(root.join("projects/demo/base.loro").is_file());
203
204    td(&tmp)
205        .args(["project", "delete", "demo"])
206        .current_dir(&tmp)
207        .assert()
208        .success();
209
210    // Project directory should be gone.
211    assert!(!root.join("projects/demo").exists());
212
213    // Binding should be removed from bindings.json.
214    let bindings: serde_json::Value =
215        serde_json::from_str(&std::fs::read_to_string(root.join("bindings.json")).unwrap())
216            .unwrap();
217    let canonical_cwd = std::fs::canonicalize(tmp.path()).unwrap();
218    assert!(
219        bindings["bindings"][canonical_cwd.to_string_lossy().as_ref()].is_null(),
220        "binding should have been removed after project deletion"
221    );
222}
223
224#[test]
225fn delete_fails_for_nonexistent_project() {
226    let tmp = TempDir::new().unwrap();
227
228    // We still need at least one project so the data dir exists, but we
229    // delete one that was never created.
230    td(&tmp)
231        .args(["project", "init", "real"])
232        .current_dir(&tmp)
233        .assert()
234        .success();
235
236    td(&tmp)
237        .args(["project", "delete", "nonexistent"])
238        .current_dir(&tmp)
239        .assert()
240        .failure()
241        .stderr(predicate::str::contains("project 'nonexistent' not found"));
242}
243
244#[test]
245fn delete_cleans_up_all_bindings_for_project() {
246    let tmp = TempDir::new().unwrap();
247    let dir_a = tmp.path().join("a");
248    let dir_b = tmp.path().join("b");
249    std::fs::create_dir_all(&dir_a).unwrap();
250    std::fs::create_dir_all(&dir_b).unwrap();
251
252    // Init in dir_a, bind dir_b to the same project.
253    td(&tmp)
254        .args(["project", "init", "shared"])
255        .current_dir(&dir_a)
256        .assert()
257        .success();
258
259    td(&tmp)
260        .args(["project", "bind", "shared"])
261        .current_dir(&dir_b)
262        .assert()
263        .success();
264
265    // Sanity-check: both dirs should resolve.
266    td(&tmp)
267        .args(["list"])
268        .current_dir(&dir_a)
269        .assert()
270        .success();
271    td(&tmp)
272        .args(["list"])
273        .current_dir(&dir_b)
274        .assert()
275        .success();
276
277    // Delete the project entirely.
278    td(&tmp)
279        .args(["project", "delete", "shared"])
280        .current_dir(&dir_a)
281        .assert()
282        .success();
283
284    // Neither directory should have a binding any more.
285    let bindings: serde_json::Value = serde_json::from_str(
286        &std::fs::read_to_string(tmp.path().join(".local/share/td/bindings.json")).unwrap(),
287    )
288    .unwrap();
289
290    let canonical_a = std::fs::canonicalize(&dir_a).unwrap();
291    let canonical_b = std::fs::canonicalize(&dir_b).unwrap();
292    assert!(
293        bindings["bindings"][canonical_a.to_string_lossy().as_ref()].is_null(),
294        "binding for dir_a should have been removed"
295    );
296    assert!(
297        bindings["bindings"][canonical_b.to_string_lossy().as_ref()].is_null(),
298        "binding for dir_b should have been removed"
299    );
300}