Skip to main content

max / makenotwork

21.6 KB · 651 lines History Blame Raw
1 //! Git browser route tests: repo overview, tree, file, commits, raw, 404s.
2 //!
3 //! Creates temp bare repos with git2 to test the actual HTTP routes.
4
5 use crate::harness::TestHarness;
6
7 /// Create a temp bare repo at `{dir}/testowner/testrepo.git` with two commits on "main".
8 /// Commit 1 (root): README.md, src/main.rs
9 /// Commit 2: modifies src/main.rs (adds a line)
10 fn make_test_repo(dir: &std::path::Path) {
11 let bare_path = dir.join("testowner").join("testrepo.git");
12 std::fs::create_dir_all(&bare_path).unwrap();
13 let bare_repo = git2::Repository::init_bare(&bare_path).unwrap();
14
15 let sig = git2::Signature::now("Test", "test@example.com").unwrap();
16
17 // Create blobs
18 let readme_oid = bare_repo.blob(b"# Test Repo\n\nHello world.").unwrap();
19 let main_rs_oid = bare_repo
20 .blob(b"fn main() {\n println!(\"hello\");\n}\n")
21 .unwrap();
22
23 // Build src/ subtree
24 let mut src_tb = bare_repo.treebuilder(None).unwrap();
25 src_tb.insert("main.rs", main_rs_oid, 0o100644).unwrap();
26 let src_tree_oid = src_tb.write().unwrap();
27
28 // Build root tree
29 let mut root_tb = bare_repo.treebuilder(None).unwrap();
30 root_tb.insert("README.md", readme_oid, 0o100644).unwrap();
31 root_tb.insert("src", src_tree_oid, 0o040000).unwrap();
32 let root_tree_oid = root_tb.write().unwrap();
33 let root_tree = bare_repo.find_tree(root_tree_oid).unwrap();
34
35 let first_commit_oid = bare_repo
36 .commit(
37 Some("refs/heads/main"),
38 &sig,
39 &sig,
40 "Initial commit",
41 &root_tree,
42 &[],
43 )
44 .unwrap();
45 bare_repo.set_head("refs/heads/main").unwrap();
46
47 // Second commit: modify src/main.rs
48 let first_commit = bare_repo.find_commit(first_commit_oid).unwrap();
49 let main_rs_oid_v2 = bare_repo
50 .blob(b"fn main() {\n println!(\"hello\");\n println!(\"world\");\n}\n")
51 .unwrap();
52 let mut src_tb2 = bare_repo.treebuilder(None).unwrap();
53 src_tb2.insert("main.rs", main_rs_oid_v2, 0o100644).unwrap();
54 let src_tree_oid2 = src_tb2.write().unwrap();
55 let mut root_tb2 = bare_repo.treebuilder(None).unwrap();
56 root_tb2.insert("README.md", readme_oid, 0o100644).unwrap();
57 root_tb2.insert("src", src_tree_oid2, 0o040000).unwrap();
58 let root_tree_oid2 = root_tb2.write().unwrap();
59 let root_tree2 = bare_repo.find_tree(root_tree_oid2).unwrap();
60 bare_repo
61 .commit(
62 Some("refs/heads/main"),
63 &sig,
64 &sig,
65 "Add world output",
66 &root_tree2,
67 &[&first_commit],
68 )
69 .unwrap();
70 }
71
72 /// Set up a harness with git repos and a user matching the disk owner.
73 async fn setup_git_harness(tmp: &tempfile::TempDir) -> TestHarness {
74 let mut h = TestHarness::with_git_repos(tmp.path().to_str().unwrap().to_string()).await;
75 // Create a user whose username matches the disk directory
76 h.signup("testowner", "testowner@example.com", "password123").await;
77 h
78 }
79
80 // ── 404 when git not configured ──
81
82 #[tokio::test]
83 async fn git_repo_returns_404_when_not_configured() {
84 let mut h = TestHarness::new().await;
85
86 let resp = h.client.get("/git/owner/repo").await;
87 assert_eq!(resp.status, 404, "No git_repos_path → 404");
88 }
89
90 // ── Repo overview ──
91
92 #[tokio::test]
93 async fn git_repo_overview() {
94 let tmp = tempfile::TempDir::new().unwrap();
95 make_test_repo(tmp.path());
96 let mut h = setup_git_harness(&tmp).await;
97
98 let resp = h.client.get("/git/testowner/testrepo").await;
99 assert!(
100 resp.status.is_success(),
101 "Repo overview failed: {} {}",
102 resp.status, resp.text
103 );
104 // HTML should contain the repo name and README content
105 assert!(resp.text.contains("testrepo"), "Should show repo name");
106 assert!(resp.text.contains("Test Repo"), "Should render README");
107 }
108
109 // ── Nonexistent repo ──
110
111 #[tokio::test]
112 async fn git_nonexistent_repo_returns_404() {
113 let tmp = tempfile::TempDir::new().unwrap();
114 make_test_repo(tmp.path());
115 let mut h = setup_git_harness(&tmp).await;
116
117 let resp = h.client.get("/git/testowner/nope").await;
118 assert_eq!(resp.status, 404);
119 }
120
121 // ── Tree at ref ──
122
123 #[tokio::test]
124 async fn git_tree_at_ref() {
125 let tmp = tempfile::TempDir::new().unwrap();
126 make_test_repo(tmp.path());
127 let mut h = setup_git_harness(&tmp).await;
128
129 let resp = h.client.get("/git/testowner/testrepo/tree/main").await;
130 assert!(
131 resp.status.is_success(),
132 "Tree at ref failed: {} {}",
133 resp.status, resp.text
134 );
135 // Should list files: README.md and src/
136 assert!(resp.text.contains("README.md"), "Should show README.md");
137 assert!(resp.text.contains("src"), "Should show src directory");
138 }
139
140 // ── Subdirectory ──
141
142 #[tokio::test]
143 async fn git_tree_subdirectory() {
144 let tmp = tempfile::TempDir::new().unwrap();
145 make_test_repo(tmp.path());
146 let mut h = setup_git_harness(&tmp).await;
147
148 let resp = h
149 .client
150 .get("/git/testowner/testrepo/tree/main/src")
151 .await;
152 assert!(
153 resp.status.is_success(),
154 "Subdirectory failed: {} {}",
155 resp.status, resp.text
156 );
157 assert!(resp.text.contains("main.rs"), "Should show main.rs in src/");
158 }
159
160 // ── File view ──
161
162 #[tokio::test]
163 async fn git_file_view() {
164 let tmp = tempfile::TempDir::new().unwrap();
165 make_test_repo(tmp.path());
166 let mut h = setup_git_harness(&tmp).await;
167
168 let resp = h
169 .client
170 .get("/git/testowner/testrepo/tree/main/src/main.rs")
171 .await;
172 assert!(
173 resp.status.is_success(),
174 "File view failed: {} {}",
175 resp.status, resp.text
176 );
177 assert!(
178 resp.text.contains("println!"),
179 "Should show file content with println!"
180 );
181 }
182
183 #[tokio::test]
184 async fn git_file_nonexistent_returns_404() {
185 let tmp = tempfile::TempDir::new().unwrap();
186 make_test_repo(tmp.path());
187 let mut h = setup_git_harness(&tmp).await;
188
189 let resp = h
190 .client
191 .get("/git/testowner/testrepo/tree/main/nope.txt")
192 .await;
193 assert_eq!(resp.status, 404);
194 }
195
196 // ── Commit log ──
197
198 #[tokio::test]
199 async fn git_commit_log() {
200 let tmp = tempfile::TempDir::new().unwrap();
201 make_test_repo(tmp.path());
202 let mut h = setup_git_harness(&tmp).await;
203
204 let resp = h
205 .client
206 .get("/git/testowner/testrepo/commits/main")
207 .await;
208 assert!(
209 resp.status.is_success(),
210 "Commit log failed: {} {}",
211 resp.status, resp.text
212 );
213 assert!(
214 resp.text.contains("Initial commit"),
215 "Should show commit message"
216 );
217 assert!(
218 resp.text.contains("Add world output"),
219 "Should show second commit message"
220 );
221 }
222
223 // ── Raw file ──
224
225 #[tokio::test]
226 async fn git_raw_file() {
227 let tmp = tempfile::TempDir::new().unwrap();
228 make_test_repo(tmp.path());
229 let mut h = setup_git_harness(&tmp).await;
230
231 let resp = h
232 .client
233 .get("/git/testowner/testrepo/raw/main/README.md")
234 .await;
235 assert!(
236 resp.status.is_success(),
237 "Raw file failed: {} {}",
238 resp.status, resp.text
239 );
240 assert!(
241 resp.text.contains("# Test Repo"),
242 "Should return raw file content"
243 );
244 }
245
246 // ── Path traversal ──
247
248 #[tokio::test]
249 async fn git_path_traversal_rejected() {
250 let tmp = tempfile::TempDir::new().unwrap();
251 make_test_repo(tmp.path());
252 let mut h = setup_git_harness(&tmp).await;
253
254 let resp = h.client.get("/git/../etc/testrepo").await;
255 // Axum may normalize or reject; we just check it doesn't succeed
256 assert!(
257 resp.status.is_client_error() || resp.status.is_server_error() || resp.status == 404,
258 "Traversal should not succeed: {}",
259 resp.status
260 );
261 }
262
263 // ── Invalid ref ──
264
265 #[tokio::test]
266 async fn git_invalid_ref_returns_404() {
267 let tmp = tempfile::TempDir::new().unwrap();
268 make_test_repo(tmp.path());
269 let mut h = setup_git_harness(&tmp).await;
270
271 let resp = h
272 .client
273 .get("/git/testowner/testrepo/tree/nonexistent-branch")
274 .await;
275 assert_eq!(resp.status, 404);
276 }
277
278 // ── Visibility: private repo ──
279
280 #[tokio::test]
281 async fn git_private_repo_hidden_from_anonymous() {
282 let tmp = tempfile::TempDir::new().unwrap();
283 make_test_repo(tmp.path());
284 let mut h = setup_git_harness(&tmp).await;
285
286 // Visit the repo to auto-register it
287 let resp = h.client.get("/git/testowner/testrepo").await;
288 assert!(resp.status.is_success());
289
290 // Set visibility to private via SQL
291 sqlx::query("UPDATE git_repos SET visibility = 'private' WHERE name = 'testrepo'")
292 .execute(&h.db)
293 .await
294 .unwrap();
295
296 // Log out so we're anonymous
297 h.client.post_form("/logout", "").await;
298
299 let resp = h.client.get("/git/testowner/testrepo").await;
300 assert_eq!(resp.status, 404, "Private repo should be 404 for anonymous users");
301 }
302
303 #[tokio::test]
304 async fn git_private_repo_visible_to_owner() {
305 let tmp = tempfile::TempDir::new().unwrap();
306 make_test_repo(tmp.path());
307 let mut h = setup_git_harness(&tmp).await;
308
309 // Visit the repo to auto-register it
310 let resp = h.client.get("/git/testowner/testrepo").await;
311 assert!(resp.status.is_success());
312
313 // Set visibility to private
314 sqlx::query("UPDATE git_repos SET visibility = 'private' WHERE name = 'testrepo'")
315 .execute(&h.db)
316 .await
317 .unwrap();
318
319 // Log in as the owner
320 h.login("testowner", "password123").await;
321
322 let resp = h.client.get("/git/testowner/testrepo").await;
323 assert!(resp.status.is_success(), "Owner should see private repo: {} {}", resp.status, resp.text);
324 }
325
326 // ── Commit detail ──
327
328 #[tokio::test]
329 async fn git_commit_detail_page() {
330 let tmp = tempfile::TempDir::new().unwrap();
331 make_test_repo(tmp.path());
332 let mut h = setup_git_harness(&tmp).await;
333
334 // Get the HEAD commit OID from the commit log page
335 let log_resp = h.client.get("/git/testowner/testrepo/commits/main").await;
336 assert!(log_resp.status.is_success());
337
338 // Extract a commit OID from the page (look for /commit/ link)
339 let oid = log_resp.text
340 .split("/git/testowner/testrepo/commit/")
341 .nth(1)
342 .and_then(|s| s.split('"').next())
343 .expect("Should find commit OID link in commit log");
344
345 let resp = h.client.get(&format!("/git/testowner/testrepo/commit/{oid}")).await;
346 assert!(resp.status.is_success(), "Commit detail failed: {} {}", resp.status, resp.text);
347 assert!(resp.text.contains("file"), "Should show diff stats");
348 }
349
350 #[tokio::test]
351 async fn git_commit_detail_root_commit() {
352 let tmp = tempfile::TempDir::new().unwrap();
353 make_test_repo(tmp.path());
354 let mut h = setup_git_harness(&tmp).await;
355
356 // Get the root commit — it's the oldest one. Fetch commit log page 1.
357 let log_resp = h.client.get("/git/testowner/testrepo/commits/main").await;
358 assert!(log_resp.status.is_success());
359
360 // The root commit's message is "Initial commit"
361 // Find its OID link
362 let text = &log_resp.text;
363 let initial_idx = text.find("Initial commit").expect("Should find Initial commit");
364 // The OID link is nearby — search after the message for /commit/ link
365 let after_initial = &text[initial_idx..];
366 let oid = after_initial
367 .split("/git/testowner/testrepo/commit/")
368 .nth(1)
369 .and_then(|s| s.split('"').next())
370 .expect("Should find commit OID for root commit");
371
372 let resp = h.client.get(&format!("/git/testowner/testrepo/commit/{oid}")).await;
373 assert!(resp.status.is_success(), "Root commit detail failed: {} {}", resp.status, resp.text);
374 assert!(resp.text.contains("Initial commit"), "Should show root commit message");
375 // Root commit should have additions (all files are new)
376 assert!(resp.text.contains("insertion"), "Root commit should show insertions");
377 }
378
379 #[tokio::test]
380 async fn git_commit_detail_nonexistent_404() {
381 let tmp = tempfile::TempDir::new().unwrap();
382 make_test_repo(tmp.path());
383 let mut h = setup_git_harness(&tmp).await;
384
385 let resp = h.client.get("/git/testowner/testrepo/commit/0000000000000000000000000000000000000000").await;
386 assert_eq!(resp.status, 404);
387 }
388
389 #[tokio::test]
390 async fn git_commit_detail_invalid_oid_404() {
391 let tmp = tempfile::TempDir::new().unwrap();
392 make_test_repo(tmp.path());
393 let mut h = setup_git_harness(&tmp).await;
394
395 let resp = h.client.get("/git/testowner/testrepo/commit/not-a-valid-oid").await;
396 assert_eq!(resp.status, 404);
397 }
398
399 // ── Blame view ──
400
401 #[tokio::test]
402 async fn git_blame_view() {
403 let tmp = tempfile::TempDir::new().unwrap();
404 make_test_repo(tmp.path());
405 let mut h = setup_git_harness(&tmp).await;
406
407 let resp = h.client.get("/git/testowner/testrepo/blame/main/src/main.rs").await;
408 assert!(resp.status.is_success(), "Blame view failed: {} {}", resp.status, resp.text);
409 assert!(resp.text.contains("println!"), "Blame should show file content");
410 // Should have commit short OIDs in the blame gutter
411 assert!(resp.text.contains("/commit/"), "Blame should link to commits");
412 }
413
414 #[tokio::test]
415 async fn git_blame_nonexistent_file_404() {
416 let tmp = tempfile::TempDir::new().unwrap();
417 make_test_repo(tmp.path());
418 let mut h = setup_git_harness(&tmp).await;
419
420 let resp = h.client.get("/git/testowner/testrepo/blame/main/nope.txt").await;
421 assert_eq!(resp.status, 404);
422 }
423
424 // ── User repos listing ──
425
426 #[tokio::test]
427 async fn git_user_repos_listing() {
428 let tmp = tempfile::TempDir::new().unwrap();
429 make_test_repo(tmp.path());
430 let mut h = setup_git_harness(&tmp).await;
431
432 // Visit repo to auto-register it
433 let resp = h.client.get("/git/testowner/testrepo").await;
434 assert!(resp.status.is_success());
435
436 let resp = h.client.get("/git/testowner").await;
437 assert!(resp.status.is_success(), "User repos listing failed: {} {}", resp.status, resp.text);
438 assert!(resp.text.contains("testrepo"), "Should list the repo");
439 }
440
441 #[tokio::test]
442 async fn git_user_repos_nonexistent_user_404() {
443 let tmp = tempfile::TempDir::new().unwrap();
444 make_test_repo(tmp.path());
445 let mut h = setup_git_harness(&tmp).await;
446
447 let resp = h.client.get("/git/nobody").await;
448 assert_eq!(resp.status, 404);
449 }
450
451 // ── Git explore (landing) ──
452
453 #[tokio::test]
454 async fn git_landing_shows_explore_logged_in() {
455 let tmp = tempfile::TempDir::new().unwrap();
456 make_test_repo(tmp.path());
457 let mut h = setup_git_harness(&tmp).await;
458 h.login("testowner", "password123").await;
459
460 // Visit repo to auto-register it
461 let resp = h.client.get("/git/testowner/testrepo").await;
462 assert!(resp.status.is_success());
463
464 let resp = h.client.get("/git").await;
465 assert_eq!(resp.status, 200, "Explore page should return 200 for logged-in users");
466 assert!(resp.text.contains("Repositories"), "Should show Repositories heading");
467 }
468
469 #[tokio::test]
470 async fn git_landing_shows_explore_anonymous() {
471 let tmp = tempfile::TempDir::new().unwrap();
472 make_test_repo(tmp.path());
473 let mut h = setup_git_harness(&tmp).await;
474
475 // Visit repo to auto-register it
476 let resp = h.client.get("/git/testowner/testrepo").await;
477 assert!(resp.status.is_success());
478
479 // Logout
480 h.client.post_form("/logout", "").await;
481
482 let resp = h.client.get("/git").await;
483 assert_eq!(resp.status, 200, "Explore page should return 200 for anonymous users");
484 assert!(resp.text.contains("Repositories"), "Should show Repositories heading");
485 }
486
487 #[tokio::test]
488 async fn git_explore_page_shows_public_repos() {
489 let tmp = tempfile::TempDir::new().unwrap();
490 make_test_repo(tmp.path());
491 let mut h = setup_git_harness(&tmp).await;
492
493 // Visit repo to auto-register it (default visibility is public)
494 let resp = h.client.get("/git/testowner/testrepo").await;
495 assert!(resp.status.is_success());
496
497 let resp = h.client.get("/git").await;
498 assert_eq!(resp.status, 200);
499 assert!(resp.text.contains("testowner"), "Should show owner name");
500 assert!(resp.text.contains("testrepo"), "Should show repo name");
501 }
502
503 #[tokio::test]
504 async fn git_explore_page_hides_private_repos() {
505 let tmp = tempfile::TempDir::new().unwrap();
506 make_test_repo(tmp.path());
507 let mut h = setup_git_harness(&tmp).await;
508
509 // Visit repo to auto-register it
510 let resp = h.client.get("/git/testowner/testrepo").await;
511 assert!(resp.status.is_success());
512
513 // Set visibility to private
514 sqlx::query("UPDATE git_repos SET visibility = 'private' WHERE name = 'testrepo'")
515 .execute(&h.db)
516 .await
517 .unwrap();
518
519 // Logout and check explore page
520 h.client.post_form("/logout", "").await;
521
522 let resp = h.client.get("/git").await;
523 assert_eq!(resp.status, 200);
524 assert!(!resp.text.contains("testrepo"), "Private repo should not appear on explore page");
525 }
526
527 // ── File history ──
528
529 #[tokio::test]
530 async fn git_file_history() {
531 let tmp = tempfile::TempDir::new().unwrap();
532 make_test_repo(tmp.path());
533 let mut h = setup_git_harness(&tmp).await;
534
535 let resp = h.client.get("/git/testowner/testrepo/log/main/src/main.rs").await;
536 assert!(resp.status.is_success(), "File history failed: {} {}", resp.status, resp.text);
537 // Both commits touched src/main.rs
538 assert!(resp.text.contains("Initial commit"), "Should show initial commit");
539 assert!(resp.text.contains("Add world output"), "Should show second commit");
540 }
541
542 #[tokio::test]
543 async fn git_file_history_filters_unrelated() {
544 let tmp = tempfile::TempDir::new().unwrap();
545 make_test_repo(tmp.path());
546 let mut h = setup_git_harness(&tmp).await;
547
548 let resp = h.client.get("/git/testowner/testrepo/log/main/README.md").await;
549 assert!(resp.status.is_success(), "File history failed: {} {}", resp.status, resp.text);
550 // README.md was only added in the initial commit, not changed in the second
551 assert!(resp.text.contains("Initial commit"), "Should show initial commit for README.md");
552 assert!(!resp.text.contains("Add world output"), "Should not show unrelated commit");
553 }
554
555 #[tokio::test]
556 async fn git_file_history_nonexistent_file() {
557 let tmp = tempfile::TempDir::new().unwrap();
558 make_test_repo(tmp.path());
559 let mut h = setup_git_harness(&tmp).await;
560
561 let resp = h.client.get("/git/testowner/testrepo/log/main/nope.txt").await;
562 assert!(resp.status.is_success(), "Nonexistent file history should render empty, not 404: {} {}", resp.status, resp.text);
563 assert!(resp.text.contains("No commits found"), "Should show empty message");
564 }
565
566 #[tokio::test]
567 async fn git_file_view_has_history_link() {
568 let tmp = tempfile::TempDir::new().unwrap();
569 make_test_repo(tmp.path());
570 let mut h = setup_git_harness(&tmp).await;
571
572 let resp = h.client.get("/git/testowner/testrepo/tree/main/src/main.rs").await;
573 assert!(resp.status.is_success());
574 assert!(resp.text.contains("/log/main/src/main.rs"), "Should have history link");
575 }
576
577 // ── File view line linking ──
578
579 #[tokio::test]
580 async fn git_file_view_has_line_links() {
581 let tmp = tempfile::TempDir::new().unwrap();
582 make_test_repo(tmp.path());
583 let mut h = setup_git_harness(&tmp).await;
584
585 let resp = h.client.get("/git/testowner/testrepo/tree/main/src/main.rs").await;
586 assert!(resp.status.is_success());
587 assert!(resp.text.contains("href=\"#L1\""), "Should have line link anchors");
588 assert!(resp.text.contains("id=\"L1\""), "Should have line anchor IDs");
589 }
590
591 // ── File view has blame link ──
592
593 #[tokio::test]
594 async fn git_file_view_has_blame_link() {
595 let tmp = tempfile::TempDir::new().unwrap();
596 make_test_repo(tmp.path());
597 let mut h = setup_git_harness(&tmp).await;
598
599 let resp = h.client.get("/git/testowner/testrepo/tree/main/src/main.rs").await;
600 assert!(resp.status.is_success());
601 assert!(resp.text.contains("/blame/main/src/main.rs"), "Should have blame link");
602 }
603
604 // ── Nav bar consistency ──
605
606 #[tokio::test]
607 async fn git_nav_bar_present_on_all_pages() {
608 let tmp = tempfile::TempDir::new().unwrap();
609 make_test_repo(tmp.path());
610 let mut h = setup_git_harness(&tmp).await;
611
612 // Repo overview
613 let resp = h.client.get("/git/testowner/testrepo").await;
614 assert!(resp.text.contains("git-nav-links"), "Repo overview should have nav");
615
616 // Tree
617 let resp = h.client.get("/git/testowner/testrepo/tree/main").await;
618 assert!(resp.text.contains("git-nav-links"), "Tree should have nav");
619
620 // Subdirectory
621 let resp = h.client.get("/git/testowner/testrepo/tree/main/src").await;
622 assert!(resp.text.contains("git-nav-links"), "Subdirectory should have nav");
623
624 // File view
625 let resp = h.client.get("/git/testowner/testrepo/tree/main/src/main.rs").await;
626 assert!(resp.text.contains("git-nav-links"), "File view should have nav");
627
628 // Commits
629 let resp = h.client.get("/git/testowner/testrepo/commits/main").await;
630 assert!(resp.text.contains("git-nav-links"), "Commits should have nav");
631 }
632
633 // ── No emoji in tree views ──
634
635 #[tokio::test]
636 async fn git_tree_no_emoji() {
637 let tmp = tempfile::TempDir::new().unwrap();
638 make_test_repo(tmp.path());
639 let mut h = setup_git_harness(&tmp).await;
640
641 let resp = h.client.get("/git/testowner/testrepo").await;
642 assert!(!resp.text.contains("\u{1F4C1}"), "Repo overview should not have folder emoji");
643 assert!(!resp.text.contains("\u{1F4C4}"), "Repo overview should not have file emoji");
644 assert!(!resp.text.contains("📁"), "No folder emoji HTML entity");
645 assert!(!resp.text.contains("📄"), "No file emoji HTML entity");
646
647 let resp = h.client.get("/git/testowner/testrepo/tree/main/src").await;
648 assert!(!resp.text.contains("\u{1F4C1}"), "Subdirectory should not have folder emoji");
649 assert!(!resp.text.contains("\u{1F4C4}"), "Subdirectory should not have file emoji");
650 }
651