Skip to main content

max / audiofiles

18.3 KB · 640 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4 use crate::test_helpers::insert_fake_sample;
5
6 fn setup() -> Database {
7 Database::open_in_memory().unwrap()
8 }
9
10 #[test]
11 fn create_and_list_vfs() {
12 let db = setup();
13 let id = create_vfs(&db, "Library").unwrap();
14 let list = list_vfs(&db).unwrap();
15 assert_eq!(list.len(), 1);
16 assert_eq!(list[0].id, id);
17 assert_eq!(list[0].name, "Library");
18 }
19
20 #[test]
21 fn full_tree_excludes_tombstoned_files_keeps_folders() {
22 let db = setup();
23 let vfs_id = create_vfs(&db, "Library").unwrap();
24 let dir = create_directory(&db, vfs_id, None, "Drums").unwrap();
25 insert_fake_sample(&db, "live");
26 insert_fake_sample(&db, "dead");
27 create_sample_link(
28 &db,
29 vfs_id,
30 Some(dir),
31 "live.wav",
32 &crate::SampleHash::from_trusted("live"),
33 )
34 .unwrap();
35 create_sample_link(
36 &db,
37 vfs_id,
38 Some(dir),
39 "dead.wav",
40 &crate::SampleHash::from_trusted("dead"),
41 )
42 .unwrap();
43
44 let before = list_full_tree(&db).unwrap();
45 assert!(before.iter().any(|n| n.path.ends_with("dead.wav")));
46
47 // Tombstone "dead": its leaf must vanish from the mirror's tree, but the
48 // Drums folder (which still holds live.wav) must remain.
49 crate::store::tombstone_sample(&db, "dead").unwrap();
50 let after = list_full_tree(&db).unwrap();
51 assert!(!after.iter().any(|n| n.path.ends_with("dead.wav")));
52 assert!(after.iter().any(|n| n.path.ends_with("live.wav")));
53 assert!(
54 after
55 .iter()
56 .any(|n| n.node_type == NodeType::Directory && n.path.ends_with("Drums"))
57 );
58 }
59
60 #[test]
61 fn rename_vfs_works() {
62 let db = setup();
63 let id = create_vfs(&db, "Old").unwrap();
64 rename_vfs(&db, id, "New").unwrap();
65 let list = list_vfs(&db).unwrap();
66 assert_eq!(list[0].name, "New");
67 }
68
69 #[test]
70 fn delete_vfs_cascades_nodes() {
71 let db = setup();
72 let vfs_id = create_vfs(&db, "Test").unwrap();
73 create_directory(&db, vfs_id, None, "folder").unwrap();
74
75 delete_vfs(&db, vfs_id).unwrap();
76
77 let count: i64 = db
78 .conn()
79 .query_row("SELECT COUNT(*) FROM vfs_nodes", [], |row| row.get(0))
80 .unwrap();
81 assert_eq!(count, 0);
82 }
83
84 #[test]
85 fn directory_tree_crud() {
86 let db = setup();
87 let vfs_id = create_vfs(&db, "Lib").unwrap();
88
89 let dir_id = create_directory(&db, vfs_id, None, "Drums").unwrap();
90 let sub_id = create_directory(&db, vfs_id, Some(dir_id), "Kicks").unwrap();
91
92 let root_children = list_children(&db, vfs_id, None).unwrap();
93 assert_eq!(root_children.len(), 1);
94 assert_eq!(root_children[0].name, "Drums");
95
96 let sub_children = list_children(&db, vfs_id, Some(dir_id)).unwrap();
97 assert_eq!(sub_children.len(), 1);
98 assert_eq!(sub_children[0].name, "Kicks");
99 assert_eq!(sub_children[0].id, sub_id);
100 }
101
102 #[test]
103 fn sample_links() {
104 let db = setup();
105 insert_fake_sample(&db, "abc123");
106 let vfs_id = create_vfs(&db, "Lib").unwrap();
107
108 let node_id = create_sample_link(
109 &db,
110 vfs_id,
111 None,
112 "kick.wav",
113 &crate::SampleHash::from_trusted("abc123"),
114 )
115 .unwrap();
116 let node = get_node(&db, node_id).unwrap();
117 assert_eq!(node.node_type, NodeType::Sample);
118 assert_eq!(node.sample_hash.as_deref(), Some("abc123"));
119 }
120
121 #[test]
122 fn rename_and_move_node() {
123 let db = setup();
124 let vfs_id = create_vfs(&db, "Lib").unwrap();
125 let dir_a = create_directory(&db, vfs_id, None, "A").unwrap();
126 let dir_b = create_directory(&db, vfs_id, None, "B").unwrap();
127 let child = create_directory(&db, vfs_id, Some(dir_a), "Child").unwrap();
128
129 rename_node(&db, child, "Renamed").unwrap();
130 let node = get_node(&db, child).unwrap();
131 assert_eq!(node.name, "Renamed");
132
133 move_node(&db, child, Some(dir_b)).unwrap();
134 let node = get_node(&db, child).unwrap();
135 assert_eq!(node.parent_id, Some(dir_b));
136 }
137
138 #[test]
139 fn delete_node_cascades() {
140 let db = setup();
141 let vfs_id = create_vfs(&db, "Lib").unwrap();
142 let parent = create_directory(&db, vfs_id, None, "Parent").unwrap();
143 create_directory(&db, vfs_id, Some(parent), "Child").unwrap();
144
145 delete_node(&db, parent).unwrap();
146
147 let count: i64 = db
148 .conn()
149 .query_row("SELECT COUNT(*) FROM vfs_nodes", [], |row| row.get(0))
150 .unwrap();
151 assert_eq!(count, 0);
152 }
153
154 #[test]
155 fn root_level_name_conflict() {
156 let db = setup();
157 let vfs_id = create_vfs(&db, "Lib").unwrap();
158 create_directory(&db, vfs_id, None, "Drums").unwrap();
159
160 let result = create_directory(&db, vfs_id, None, "Drums");
161 assert!(matches!(result, Err(CoreError::NameConflict(_))));
162 }
163
164 #[test]
165 fn root_name_uniqueness_enforced_by_index() {
166 // The partial unique index (M029) is the backstop for the non-atomic
167 // COUNT-then-INSERT check: a second same-name root insert must be
168 // rejected by the DB even when it bypasses the Rust-side check, the
169 // concurrent-insert race past check_root_name_conflict.
170 let db = setup();
171 let vfs_id = create_vfs(&db, "Lib").unwrap();
172 db.conn()
173 .execute(
174 "INSERT INTO vfs_nodes (vfs_id, parent_id, name, node_type, created_at)
175 VALUES (?1, NULL, 'Drums', 'directory', 0)",
176 rusqlite::params![vfs_id],
177 )
178 .unwrap();
179 let dup = db.conn().execute(
180 "INSERT INTO vfs_nodes (vfs_id, parent_id, name, node_type, created_at)
181 VALUES (?1, NULL, 'Drums', 'directory', 0)",
182 rusqlite::params![vfs_id],
183 );
184 assert!(
185 dup.is_err(),
186 "partial unique index must reject a duplicate root name"
187 );
188 assert!(crate::error::is_unique_violation(&dup.unwrap_err()));
189 }
190
191 #[test]
192 fn breadcrumb_trail() {
193 let db = setup();
194 let vfs_id = create_vfs(&db, "Lib").unwrap();
195 let a = create_directory(&db, vfs_id, None, "A").unwrap();
196 let b = create_directory(&db, vfs_id, Some(a), "B").unwrap();
197 let c = create_directory(&db, vfs_id, Some(b), "C").unwrap();
198
199 let crumbs = get_breadcrumb(&db, c).unwrap();
200 assert_eq!(crumbs.len(), 3);
201 assert_eq!(crumbs[0].name, "A");
202 assert_eq!(crumbs[1].name, "B");
203 assert_eq!(crumbs[2].name, "C");
204 }
205
206 #[test]
207 fn list_children_sorts_dirs_first() {
208 let db = setup();
209 insert_fake_sample(&db, "sample1");
210 let vfs_id = create_vfs(&db, "Lib").unwrap();
211
212 create_sample_link(
213 &db,
214 vfs_id,
215 None,
216 "zzz.wav",
217 &crate::SampleHash::from_trusted("sample1"),
218 )
219 .unwrap();
220 create_directory(&db, vfs_id, None, "AAA").unwrap();
221
222 let children = list_children(&db, vfs_id, None).unwrap();
223 assert_eq!(children[0].node_type, NodeType::Directory);
224 assert_eq!(children[1].node_type, NodeType::Sample);
225 }
226
227 #[test]
228 fn collect_subtree_flat() {
229 let db = setup();
230 let vfs_id = create_vfs(&db, "Lib").unwrap();
231 insert_fake_sample(&db, "s1");
232 let dir = create_directory(&db, vfs_id, None, "Dir").unwrap();
233 create_sample_link(
234 &db,
235 vfs_id,
236 Some(dir),
237 "s1.wav",
238 &crate::SampleHash::from_trusted("s1"),
239 )
240 .unwrap();
241
242 let subtree = collect_subtree(&db, dir).unwrap();
243 assert_eq!(subtree.len(), 2); // dir + sample
244 assert!(subtree.iter().any(|n| n.name == "Dir"));
245 assert!(subtree.iter().any(|n| n.name == "s1.wav"));
246 }
247
248 #[test]
249 fn collect_subtree_nested() {
250 let db = setup();
251 let vfs_id = create_vfs(&db, "Lib").unwrap();
252 let a = create_directory(&db, vfs_id, None, "A").unwrap();
253 let b = create_directory(&db, vfs_id, Some(a), "B").unwrap();
254 create_directory(&db, vfs_id, Some(b), "C").unwrap();
255
256 let subtree = collect_subtree(&db, a).unwrap();
257 assert_eq!(subtree.len(), 3);
258 let names: Vec<&str> = subtree.iter().map(|n| n.name.as_str()).collect();
259 assert!(names.contains(&"A"));
260 assert!(names.contains(&"B"));
261 assert!(names.contains(&"C"));
262 }
263
264 #[test]
265 fn collect_subtree_empty_dir() {
266 let db = setup();
267 let vfs_id = create_vfs(&db, "Lib").unwrap();
268 let dir = create_directory(&db, vfs_id, None, "Empty").unwrap();
269
270 let subtree = collect_subtree(&db, dir).unwrap();
271 assert_eq!(subtree.len(), 1);
272 assert_eq!(subtree[0].name, "Empty");
273 }
274
275 #[test]
276 fn list_all_directories_works() {
277 let db = setup();
278 let vfs_id = create_vfs(&db, "Lib").unwrap();
279 let drums = create_directory(&db, vfs_id, None, "Drums").unwrap();
280 create_directory(&db, vfs_id, Some(drums), "Kicks").unwrap();
281 create_directory(&db, vfs_id, Some(drums), "Snares").unwrap();
282 create_directory(&db, vfs_id, None, "Vocals").unwrap();
283
284 let dirs = list_all_directories(&db, vfs_id).unwrap();
285 let paths: Vec<&str> = dirs.iter().map(|(_, p)| p.as_str()).collect();
286 assert_eq!(
287 paths,
288 vec!["Drums", "Drums/Kicks", "Drums/Snares", "Vocals"]
289 );
290 }
291
292 #[test]
293 fn list_all_directories_empty_vfs() {
294 let db = setup();
295 let vfs_id = create_vfs(&db, "Lib").unwrap();
296 let dirs = list_all_directories(&db, vfs_id).unwrap();
297 assert!(dirs.is_empty());
298 }
299
300 #[test]
301 fn enriched_query_includes_cloud_only_false() {
302 let db = setup();
303 let vfs_id = create_vfs(&db, "Lib").unwrap();
304 crate::test_helpers::insert_fake_sample(&db, "hash1");
305 create_sample_link(
306 &db,
307 vfs_id,
308 None,
309 "kick.wav",
310 &crate::SampleHash::from_trusted("hash1"),
311 )
312 .unwrap();
313
314 let nodes = list_children_enriched(&db, vfs_id, None).unwrap();
315 assert_eq!(nodes.len(), 1);
316 assert!(!nodes[0].cloud_only);
317 }
318
319 #[test]
320 fn enriched_query_includes_cloud_only_true() {
321 let db = setup();
322 let vfs_id = create_vfs(&db, "Lib").unwrap();
323 crate::test_helpers::insert_fake_sample(&db, "hash1");
324 create_sample_link(
325 &db,
326 vfs_id,
327 None,
328 "kick.wav",
329 &crate::SampleHash::from_trusted("hash1"),
330 )
331 .unwrap();
332
333 // Set cloud_only=1 directly
334 db.conn()
335 .execute("UPDATE samples SET cloud_only = 1 WHERE hash = 'hash1'", [])
336 .unwrap();
337
338 let nodes = list_children_enriched(&db, vfs_id, None).unwrap();
339 assert_eq!(nodes.len(), 1);
340 assert!(nodes[0].cloud_only);
341 }
342
343 #[test]
344 fn enriched_query_directory_cloud_only_false() {
345 let db = setup();
346 let vfs_id = create_vfs(&db, "Lib").unwrap();
347 create_directory(&db, vfs_id, None, "Drums").unwrap();
348
349 let nodes = list_children_enriched(&db, vfs_id, None).unwrap();
350 assert_eq!(nodes.len(), 1);
351 // Directories have no sample_hash so cloud_only defaults to false
352 assert!(!nodes[0].cloud_only);
353 }
354
355 #[test]
356 fn find_nodes_by_hashes_returns_correct_results() {
357 let db = setup();
358 let vfs_id = create_vfs(&db, "Lib").unwrap();
359 insert_fake_sample(&db, "hash_a");
360 insert_fake_sample(&db, "hash_b");
361 insert_fake_sample(&db, "hash_c");
362 create_sample_link(
363 &db,
364 vfs_id,
365 None,
366 "a.wav",
367 &crate::SampleHash::from_trusted("hash_a"),
368 )
369 .unwrap();
370 create_sample_link(
371 &db,
372 vfs_id,
373 None,
374 "b.wav",
375 &crate::SampleHash::from_trusted("hash_b"),
376 )
377 .unwrap();
378 create_sample_link(
379 &db,
380 vfs_id,
381 None,
382 "c.wav",
383 &crate::SampleHash::from_trusted("hash_c"),
384 )
385 .unwrap();
386
387 let results = find_nodes_by_hashes(&db, vfs_id, &["hash_b", "hash_a"]).unwrap();
388
389 // Returns results in input order
390 assert_eq!(results.len(), 2);
391 assert_eq!(results[0].node.sample_hash.as_deref(), Some("hash_b"));
392 assert_eq!(results[1].node.sample_hash.as_deref(), Some("hash_a"));
393 }
394
395 #[test]
396 fn find_nodes_by_hashes_empty_input() {
397 let db = setup();
398 let vfs_id = create_vfs(&db, "Lib").unwrap();
399
400 let results = find_nodes_by_hashes(&db, vfs_id, &[]).unwrap();
401 assert!(results.is_empty());
402 }
403
404 #[test]
405 fn find_nodes_by_hashes_nonexistent_hash() {
406 let db = setup();
407 let vfs_id = create_vfs(&db, "Lib").unwrap();
408
409 let results = find_nodes_by_hashes(&db, vfs_id, &["nonexistent"]).unwrap();
410 assert!(results.is_empty());
411 }
412
413 #[test]
414 fn find_nodes_by_hashes_includes_cloud_only() {
415 let db = setup();
416 let vfs_id = create_vfs(&db, "Lib").unwrap();
417 insert_fake_sample(&db, "hash_cloud");
418 create_sample_link(
419 &db,
420 vfs_id,
421 None,
422 "cloud.wav",
423 &crate::SampleHash::from_trusted("hash_cloud"),
424 )
425 .unwrap();
426
427 db.conn()
428 .execute(
429 "UPDATE samples SET cloud_only = 1 WHERE hash = 'hash_cloud'",
430 [],
431 )
432 .unwrap();
433
434 let results = find_nodes_by_hashes(&db, vfs_id, &["hash_cloud"]).unwrap();
435 assert_eq!(results.len(), 1);
436 assert!(results[0].cloud_only);
437 }
438
439 #[test]
440 fn move_node_rejects_circular_parent_to_child() {
441 let db = setup();
442 let vfs_id = create_vfs(&db, "Lib").unwrap();
443 let a = create_directory(&db, vfs_id, None, "A").unwrap();
444 let b = create_directory(&db, vfs_id, Some(a), "B").unwrap();
445 let c = create_directory(&db, vfs_id, Some(b), "C").unwrap();
446
447 // Moving A under C would create A -> B -> C -> A cycle
448 let result = move_node(&db, a, Some(c));
449 assert!(result.is_err());
450 let err_msg = format!("{}", result.unwrap_err());
451 assert!(
452 err_msg.contains("circular"),
453 "expected circular error, got: {err_msg}"
454 );
455 }
456
457 #[test]
458 fn move_node_allows_valid_reparent() {
459 let db = setup();
460 let vfs_id = create_vfs(&db, "Lib").unwrap();
461 let a = create_directory(&db, vfs_id, None, "A").unwrap();
462 let b = create_directory(&db, vfs_id, Some(a), "B").unwrap();
463 let c = create_directory(&db, vfs_id, Some(b), "C").unwrap();
464
465 // Moving C under A (skipping B) is valid, no cycle
466 move_node(&db, c, Some(a)).unwrap();
467 let node = get_node(&db, c).unwrap();
468 assert_eq!(node.parent_id, Some(a));
469 }
470
471 #[test]
472 fn move_node_to_root_succeeds() {
473 let db = setup();
474 let vfs_id = create_vfs(&db, "Lib").unwrap();
475 let a = create_directory(&db, vfs_id, None, "A").unwrap();
476 let b = create_directory(&db, vfs_id, Some(a), "B").unwrap();
477
478 // Moving A to root is always valid
479 move_node(&db, a, None).unwrap();
480 let node = get_node(&db, a).unwrap();
481 assert_eq!(node.parent_id, None);
482
483 // Moving B to root is also valid
484 move_node(&db, b, None).unwrap();
485 let node = get_node(&db, b).unwrap();
486 assert_eq!(node.parent_id, None);
487 }
488
489 #[test]
490 fn move_node_rejects_self_as_parent() {
491 let db = setup();
492 let vfs_id = create_vfs(&db, "Lib").unwrap();
493 let a = create_directory(&db, vfs_id, None, "A").unwrap();
494
495 // Moving A under itself creates a trivial cycle
496 let result = move_node(&db, a, Some(a));
497 assert!(result.is_err());
498 }
499
500 // --- Node name validation tests ---
501
502 #[test]
503 fn validate_node_name_accepts_valid_names() {
504 assert!(validate_node_name("kick.wav").is_ok());
505 assert!(validate_node_name("My Folder").is_ok());
506 assert!(validate_node_name("drums-2024").is_ok());
507 assert!(validate_node_name("a").is_ok());
508 assert!(validate_node_name("...").is_ok()); // three dots is fine
509 }
510
511 #[test]
512 fn validate_node_name_rejects_empty() {
513 let result = validate_node_name("");
514 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
515 }
516
517 #[test]
518 fn validate_node_name_rejects_dot() {
519 let result = validate_node_name(".");
520 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
521 }
522
523 #[test]
524 fn validate_node_name_rejects_dotdot() {
525 let result = validate_node_name("..");
526 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
527 }
528
529 #[test]
530 fn validate_node_name_rejects_forward_slash() {
531 let result = validate_node_name("foo/bar");
532 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
533 }
534
535 #[test]
536 fn validate_node_name_rejects_backslash() {
537 let result = validate_node_name("foo\\bar");
538 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
539 }
540
541 #[test]
542 fn validate_node_name_rejects_null_byte() {
543 let result = validate_node_name("foo\0bar");
544 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
545 }
546
547 #[test]
548 fn create_directory_rejects_invalid_name() {
549 let db = setup();
550 let vfs_id = create_vfs(&db, "Lib").unwrap();
551 let result = create_directory(&db, vfs_id, None, "..");
552 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
553 }
554
555 #[test]
556 fn create_sample_link_rejects_invalid_name() {
557 let db = setup();
558 insert_fake_sample(&db, "hash1");
559 let vfs_id = create_vfs(&db, "Lib").unwrap();
560 let result = create_sample_link(
561 &db,
562 vfs_id,
563 None,
564 "foo/bar.wav",
565 &crate::SampleHash::from_trusted("hash1"),
566 );
567 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
568 }
569
570 #[test]
571 fn rename_node_rejects_invalid_name() {
572 let db = setup();
573 let vfs_id = create_vfs(&db, "Lib").unwrap();
574 let dir = create_directory(&db, vfs_id, None, "Valid").unwrap();
575 let result = rename_node(&db, dir, "");
576 assert!(matches!(result, Err(CoreError::InvalidNodeName(_))));
577 }
578
579 // --- list_full_tree tests ---
580
581 #[test]
582 fn list_full_tree_empty_vfs() {
583 let db = setup();
584 create_vfs(&db, "Lib").unwrap();
585 let tree = list_full_tree(&db).unwrap();
586 assert!(tree.is_empty());
587 }
588
589 #[test]
590 fn list_full_tree_builds_paths() {
591 let db = setup();
592 insert_fake_sample(&db, "s1");
593 let vfs_id = create_vfs(&db, "Library").unwrap();
594 let drums = create_directory(&db, vfs_id, None, "Drums").unwrap();
595 let kicks = create_directory(&db, vfs_id, Some(drums), "Kicks").unwrap();
596 create_sample_link(
597 &db,
598 vfs_id,
599 Some(kicks),
600 "808.wav",
601 &crate::SampleHash::from_trusted("s1"),
602 )
603 .unwrap();
604
605 let tree = list_full_tree(&db).unwrap();
606 let paths: Vec<&str> = tree.iter().map(|n| n.path.as_str()).collect();
607 assert_eq!(
608 paths,
609 vec![
610 "Library/Drums",
611 "Library/Drums/Kicks",
612 "Library/Drums/Kicks/808.wav",
613 ]
614 );
615 // Last node is a sample with the correct hash
616 assert_eq!(tree[2].node_type, NodeType::Sample);
617 assert_eq!(tree[2].sample_hash.as_deref(), Some("s1"));
618 }
619
620 #[test]
621 fn list_full_tree_multiple_vfs() {
622 let db = setup();
623 insert_fake_sample(&db, "s1");
624 let v1 = create_vfs(&db, "Alpha").unwrap();
625 let v2 = create_vfs(&db, "Beta").unwrap();
626 create_directory(&db, v1, None, "Dir1").unwrap();
627 create_sample_link(
628 &db,
629 v2,
630 None,
631 "sample.wav",
632 &crate::SampleHash::from_trusted("s1"),
633 )
634 .unwrap();
635
636 let tree = list_full_tree(&db).unwrap();
637 let paths: Vec<&str> = tree.iter().map(|n| n.path.as_str()).collect();
638 assert_eq!(paths, vec!["Alpha/Dir1", "Beta/sample.wav"]);
639 }
640