aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/root_validation.rs457
1 files changed, 457 insertions, 0 deletions
diff --git a/tests/root_validation.rs b/tests/root_validation.rs
new file mode 100644
index 0000000..49d33dc
--- /dev/null
+++ b/tests/root_validation.rs
@@ -0,0 +1,457 @@
1use assert_cmd::Command;
2use predicates::prelude::*;
3use std::fs;
4use std::path::PathBuf;
5use std::time::Duration;
6use tempfile::TempDir;
7
8// Root directory validation edge case tests
9
10#[test]
11fn test_root_dir_is_file_not_directory() {
12 let temp_dir = TempDir::new().unwrap();
13
14 // Create a file instead of a directory
15 let file_path = temp_dir.path().join("not_a_directory");
16 fs::write(&file_path, "this is a file").unwrap();
17
18 // Try to use the file as root directory - should fail
19 let mut cmd = Command::cargo_bin("demon").unwrap();
20 cmd.args(&[
21 "run",
22 "--root-dir",
23 file_path.to_str().unwrap(),
24 "test",
25 "echo",
26 "hello",
27 ])
28 .assert()
29 .failure()
30 .stderr(predicate::str::contains("not a directory"));
31}
32
33#[test]
34fn test_root_dir_does_not_exist() {
35 let temp_dir = TempDir::new().unwrap();
36
37 // Use a non-existent path
38 let nonexistent_path = temp_dir.path().join("does_not_exist");
39
40 // Try to use non-existent path as root directory - should fail
41 let mut cmd = Command::cargo_bin("demon").unwrap();
42 cmd.args(&[
43 "run",
44 "--root-dir",
45 nonexistent_path.to_str().unwrap(),
46 "test",
47 "echo",
48 "hello",
49 ])
50 .assert()
51 .failure()
52 .stderr(predicate::str::contains("does not exist"));
53}
54
55#[test]
56fn test_git_root_demon_dir_exists_as_file() {
57 // Create a temporary git repo
58 let temp_dir = TempDir::new().unwrap();
59 let git_dir = temp_dir.path().join(".git");
60 std::fs::create_dir(&git_dir).unwrap();
61
62 // Create .demon as a FILE instead of directory
63 let demon_file = temp_dir.path().join(".demon");
64 fs::write(&demon_file, "this should be a directory").unwrap();
65
66 // Change to the temp directory
67 let original_dir = std::env::current_dir().unwrap();
68 std::env::set_current_dir(temp_dir.path()).unwrap();
69
70 // Restore directory when done
71 struct DirGuard(PathBuf);
72 impl Drop for DirGuard {
73 fn drop(&mut self) {
74 let _ = std::env::set_current_dir(&self.0);
75 }
76 }
77 let _guard = DirGuard(original_dir);
78
79 // Run command without --root-dir (should use git root and fail)
80 let mut cmd = Command::cargo_bin("demon").unwrap();
81 cmd.args(&["run", "test", "echo", "hello"])
82 .assert()
83 .failure()
84 .stderr(predicate::str::contains("exists but is not a directory"));
85}
86
87#[test]
88fn test_git_root_demon_dir_permission_denied() {
89 // This test is tricky to implement portably since it requires creating
90 // a directory with restricted permissions. We'll create a more comprehensive
91 // test that simulates the condition by creating a read-only parent directory.
92
93 let temp_dir = TempDir::new().unwrap();
94 let git_dir = temp_dir.path().join(".git");
95 std::fs::create_dir(&git_dir).unwrap();
96
97 // Create a subdirectory and make it read-only
98 let subdir = temp_dir.path().join("subdir");
99 std::fs::create_dir(&subdir).unwrap();
100 let subdir_git = subdir.join(".git");
101 std::fs::create_dir(&subdir_git).unwrap();
102
103 // Make the subdirectory read-only (this should prevent .demon creation)
104 #[cfg(unix)]
105 {
106 use std::os::unix::fs::PermissionsExt;
107 let mut perms = std::fs::metadata(&subdir).unwrap().permissions();
108 perms.set_mode(0o444); // Read-only
109 std::fs::set_permissions(&subdir, perms).unwrap();
110 }
111
112 // Change to the subdirectory
113 let original_dir = std::env::current_dir().unwrap();
114
115 // Handle the case where changing to the directory fails due to permissions
116 if std::env::set_current_dir(&subdir).is_err() {
117 // This is actually the expected behavior for a directory with insufficient permissions
118 return;
119 }
120
121 // Restore directory and permissions when done
122 struct TestGuard {
123 original_dir: PathBuf,
124 #[cfg(unix)]
125 restore_path: PathBuf,
126 }
127 impl Drop for TestGuard {
128 fn drop(&mut self) {
129 let _ = std::env::set_current_dir(&self.original_dir);
130 #[cfg(unix)]
131 {
132 use std::os::unix::fs::PermissionsExt;
133 if let Ok(mut perms) =
134 std::fs::metadata(&self.restore_path).map(|m| m.permissions())
135 {
136 perms.set_mode(0o755);
137 let _ = std::fs::set_permissions(&self.restore_path, perms);
138 }
139 }
140 }
141 }
142 let _guard = TestGuard {
143 original_dir,
144 #[cfg(unix)]
145 restore_path: subdir.clone(),
146 };
147
148 // Run command without --root-dir - should fail due to permission denied
149 #[cfg(unix)]
150 {
151 let mut cmd = Command::cargo_bin("demon").unwrap();
152 cmd.args(&["run", "test", "echo", "hello"])
153 .assert()
154 .failure()
155 .stderr(predicate::str::contains(
156 "Failed to create daemon directory",
157 ));
158 }
159}
160
161#[test]
162fn test_no_git_root_and_no_root_dir() {
163 // Create a temporary directory that's NOT a git repository
164 let temp_dir = TempDir::new().unwrap();
165
166 // Change to the temp directory
167 let original_dir = std::env::current_dir().unwrap();
168 std::env::set_current_dir(temp_dir.path()).unwrap();
169
170 // Restore directory when done
171 struct DirGuard(PathBuf);
172 impl Drop for DirGuard {
173 fn drop(&mut self) {
174 let _ = std::env::set_current_dir(&self.0);
175 }
176 }
177 let _guard = DirGuard(original_dir);
178
179 // Run command without --root-dir and outside git repo - should fail
180 let mut cmd = Command::cargo_bin("demon").unwrap();
181 cmd.args(&["run", "test", "echo", "hello"])
182 .assert()
183 .failure()
184 .stderr(predicate::str::contains("No git repository found"));
185}
186
187#[test]
188fn test_invalid_utf8_path_handling() {
189 // This test checks handling of paths with invalid UTF-8 characters
190 // This is primarily relevant on Unix systems where paths can contain arbitrary bytes
191
192 // Try to use a path with null bytes (should be invalid on most systems)
193 // We expect this to fail at the OS level before reaching our validation code
194 let result = std::panic::catch_unwind(|| {
195 let mut cmd = Command::cargo_bin("demon").unwrap();
196 cmd.args(&[
197 "run",
198 "--root-dir",
199 "path\0with\0nulls",
200 "test",
201 "echo",
202 "hello",
203 ])
204 .assert()
205 .failure();
206 });
207 // Either the command fails (good) or it panics due to null bytes (also expected)
208 // This documents that our validation doesn't need to handle null bytes since the OS catches them
209 if result.is_err() {
210 // The test environment caught the null byte issue, which is expected behavior
211 return;
212 }
213}
214
215#[test]
216fn test_deeply_nested_nonexistent_path() {
217 let temp_dir = TempDir::new().unwrap();
218
219 // Create a path with many levels that don't exist
220 let deep_path = temp_dir
221 .path()
222 .join("does")
223 .join("not")
224 .join("exist")
225 .join("at")
226 .join("all")
227 .join("very")
228 .join("deep")
229 .join("path");
230
231 let mut cmd = Command::cargo_bin("demon").unwrap();
232 cmd.args(&[
233 "run",
234 "--root-dir",
235 deep_path.to_str().unwrap(),
236 "test",
237 "echo",
238 "hello",
239 ])
240 .assert()
241 .failure()
242 .stderr(predicate::str::contains("does not exist"));
243}
244
245#[test]
246fn test_root_dir_is_symlink_to_directory() {
247 let temp_dir = TempDir::new().unwrap();
248
249 // Create a real directory
250 let real_dir = temp_dir.path().join("real_directory");
251 std::fs::create_dir(&real_dir).unwrap();
252
253 // Create a symlink to it (on systems that support it)
254 let symlink_path = temp_dir.path().join("symlink_to_dir");
255
256 #[cfg(unix)]
257 {
258 std::os::unix::fs::symlink(&real_dir, &symlink_path).unwrap();
259
260 // Using symlink as root dir should work (following the symlink)
261 let mut cmd = Command::cargo_bin("demon").unwrap();
262 cmd.args(&[
263 "run",
264 "--root-dir",
265 symlink_path.to_str().unwrap(),
266 "test",
267 "echo",
268 "hello",
269 ])
270 .assert()
271 .success();
272
273 // Verify files were created in the real directory (following symlink)
274 std::thread::sleep(Duration::from_millis(100));
275 assert!(real_dir.join("test.pid").exists());
276 assert!(real_dir.join("test.stdout").exists());
277 assert!(real_dir.join("test.stderr").exists());
278 }
279}
280
281#[test]
282fn test_root_dir_is_symlink_to_file() {
283 let temp_dir = TempDir::new().unwrap();
284
285 // Create a regular file
286 let regular_file = temp_dir.path().join("regular_file");
287 fs::write(&regular_file, "content").unwrap();
288
289 // Create a symlink to the file
290 let symlink_path = temp_dir.path().join("symlink_to_file");
291
292 #[cfg(unix)]
293 {
294 std::os::unix::fs::symlink(&regular_file, &symlink_path).unwrap();
295
296 // Using symlink to file as root dir should fail
297 let mut cmd = Command::cargo_bin("demon").unwrap();
298 cmd.args(&[
299 "run",
300 "--root-dir",
301 symlink_path.to_str().unwrap(),
302 "test",
303 "echo",
304 "hello",
305 ])
306 .assert()
307 .failure()
308 .stderr(predicate::str::contains("not a directory"));
309 }
310}
311
312#[test]
313fn test_root_dir_is_broken_symlink() {
314 let temp_dir = TempDir::new().unwrap();
315
316 // Create a symlink to a non-existent target
317 let broken_symlink = temp_dir.path().join("broken_symlink");
318
319 #[cfg(unix)]
320 {
321 std::os::unix::fs::symlink("nonexistent_target", &broken_symlink).unwrap();
322
323 // Using broken symlink as root dir should fail
324 let mut cmd = Command::cargo_bin("demon").unwrap();
325 cmd.args(&[
326 "run",
327 "--root-dir",
328 broken_symlink.to_str().unwrap(),
329 "test",
330 "echo",
331 "hello",
332 ])
333 .assert()
334 .failure()
335 .stderr(predicate::str::contains("does not exist"));
336 }
337}
338
339#[test]
340fn test_improved_error_messages() {
341 let temp_dir = TempDir::new().unwrap();
342
343 // Test 1: Better error message for non-existent directory
344 let nonexistent_path = temp_dir.path().join("does_not_exist");
345 let mut cmd = Command::cargo_bin("demon").unwrap();
346 cmd.args(&[
347 "run",
348 "--root-dir",
349 nonexistent_path.to_str().unwrap(),
350 "test",
351 "echo",
352 "hello",
353 ])
354 .assert()
355 .failure()
356 .stderr(predicate::str::contains(
357 "Please create the directory first",
358 ));
359
360 // Test 2: Better error message for file instead of directory
361 let file_path = temp_dir.path().join("not_a_directory");
362 fs::write(&file_path, "this is a file").unwrap();
363
364 let mut cmd = Command::cargo_bin("demon").unwrap();
365 cmd.args(&[
366 "run",
367 "--root-dir",
368 file_path.to_str().unwrap(),
369 "test",
370 "echo",
371 "hello",
372 ])
373 .assert()
374 .failure()
375 .stderr(predicate::str::contains(
376 "Please specify a directory path, not a file",
377 ));
378}
379
380#[test]
381fn test_write_permission_validation() {
382 // This test can only run on Unix systems where we can create read-only directories
383 #[cfg(unix)]
384 {
385 use std::os::unix::fs::PermissionsExt;
386
387 let temp_dir = TempDir::new().unwrap();
388 let readonly_dir = temp_dir.path().join("readonly");
389 std::fs::create_dir(&readonly_dir).unwrap();
390
391 // Make the directory read-only
392 let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
393 perms.set_mode(0o444);
394 std::fs::set_permissions(&readonly_dir, perms.clone()).unwrap();
395
396 // Attempt to use the read-only directory should fail with permission error
397 let mut cmd = Command::cargo_bin("demon").unwrap();
398 cmd.args(&[
399 "run",
400 "--root-dir",
401 readonly_dir.to_str().unwrap(),
402 "test",
403 "echo",
404 "hello",
405 ])
406 .assert()
407 .failure()
408 .stderr(predicate::str::contains(
409 "Cannot write to specified root directory",
410 ))
411 .stderr(predicate::str::contains(
412 "Please check directory permissions",
413 ));
414
415 // Clean up - restore permissions so temp dir can be deleted
416 perms.set_mode(0o755);
417 let _ = std::fs::set_permissions(&readonly_dir, perms);
418 }
419}
420
421#[test]
422fn test_path_canonicalization() {
423 let temp_dir = TempDir::new().unwrap();
424
425 // Create a real directory
426 let real_dir = temp_dir.path().join("real_directory");
427 std::fs::create_dir(&real_dir).unwrap();
428
429 #[cfg(unix)]
430 {
431 // Create a symlink to it
432 let symlink_path = temp_dir.path().join("symlink_to_dir");
433 std::os::unix::fs::symlink(&real_dir, &symlink_path).unwrap();
434
435 // Using symlink as root dir should work and files should be created in the real directory
436 let mut cmd = Command::cargo_bin("demon").unwrap();
437 cmd.args(&[
438 "run",
439 "--root-dir",
440 symlink_path.to_str().unwrap(),
441 "canonical_test",
442 "echo",
443 "hello",
444 ])
445 .assert()
446 .success();
447
448 // Verify files were created in the real directory (symlink resolved)
449 std::thread::sleep(Duration::from_millis(100));
450 assert!(real_dir.join("canonical_test.pid").exists());
451 assert!(real_dir.join("canonical_test.stdout").exists());
452 assert!(real_dir.join("canonical_test.stderr").exists());
453
454 // Note: symlink_path.join() will also resolve to the same files since it's a symlink
455 // The key test is that files are in the canonical/real location
456 }
457}