1use assert_cmd::cargo::cargo_bin_cmd;
2use predicates::prelude::*;
3use tempfile::TempDir;
4
5fn td(home: &TempDir) -> assert_cmd::Command {
6 let mut cmd = cargo_bin_cmd!("td");
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}