//! Browsing files on the default branch, and the links a README's relative //! references turn into. mod common; use assert2::check; use common::Fixture; use poem::{http::StatusCode, test::TestClient}; const PNG: &[u8] = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR"; const README: &str = "# Widget See the [license](LICENSE), the [guide](docs/guide.md), the [docs folder](docs/), the [root license](/LICENSE), the [spaced file](docs/my%20file.md), an [external site](https://example.com/x), [mail](mailto:a@b.c) and [usage](#usage). ![logo](docs/logo.png) "; const GUIDE: &str = "# Guide\n\nBack to the [README](../README.md); see the [logo](logo.png) and ![it](logo.png).\n"; fn fixture() -> Fixture { let fx = Fixture::new(); fx.repo_bytes( "widget", true, "Widgets", &[ ("README.md", README.as_bytes()), ("LICENSE", b"MIT License <2026>\n"), ("docs/guide.md", GUIDE.as_bytes()), ("docs/my file.md", b"# Spaced\n"), ("docs/logo.png", PNG), ("docs/index.html", b""), ("src/main.rs", b"fn main() {}\n"), ], ); fx.repo("secret", false, "Secret", &[("LICENSE", "no\n")]); fx } async fn text(client: &TestClient, path: &str) -> (StatusCode, String) { let resp = client.get(path).send().await; let status = resp.0.status(); (status, resp.0.into_body().into_string().await.unwrap()) } #[tokio::test] async fn readme_links_point_into_the_file_and_raw_views() { let fx = fixture(); let client = TestClient::new(fx.app.routes()); let (status, body) = text(&client, "/widget").await; check!(status == StatusCode::OK); check!(body.contains("href=\"/widget/file/LICENSE\"")); check!(body.contains("href=\"/widget/file/docs/guide.md\"")); check!(body.contains("href=\"/widget/file/docs\"")); check!(body.contains("href=\"/widget/file/docs/my%20file.md\"")); check!(body.contains("src=\"/widget/raw/docs/logo.png\"")); check!(body.contains("src=\"/widget/raw/docs/logo.png\" width=\"100\"")); check!(body.contains("href=\"https://example.com/x\"")); check!(body.contains("href=\"mailto:a@b.c\"")); check!(body.contains("href=\"#user-content-usage\"")); check!(body.matches("href=\"/widget/file/LICENSE\"").count() == 2); } #[tokio::test] async fn file_view_shows_text_markdown_and_images() { let fx = fixture(); let client = TestClient::new(fx.app.routes()); let (status, body) = text(&client, "/widget/file/LICENSE").await; check!(status == StatusCode::OK); check!(body.contains("MIT License <2026>")); check!(body.contains("href=\"/widget/raw/LICENSE\"")); let (status, body) = text(&client, "/widget/file/docs/guide.md").await; check!(status == StatusCode::OK); check!(body.contains("

Guide

")); check!(body.contains("href=\"/widget/file/README.md\"")); check!(body.contains("href=\"/widget/file/docs/logo.png\"")); check!(body.contains("src=\"/widget/raw/docs/logo.png\"")); let (status, body) = text(&client, "/widget/file/docs/logo.png").await; check!(status == StatusCode::OK); check!(body.contains("docs/")); check!(body.contains("href=\"/widget/file/README.md\">README.md")); check!( body.find("docs/") < body.find("LICENSE"), "directories come first" ); let (status, body) = text(&client, "/widget/file/docs/").await; check!(status == StatusCode::OK); check!(body.contains("href=\"/widget/file/docs/guide.md\">guide.md")); check!(body.contains("href=\"/widget/file\">files")); check!(body.contains("docs")); } #[tokio::test] async fn unreadable_readme_offers_its_raw_bytes() { let fx = Fixture::new(); fx.repo_bytes("latin", true, "", &[("README", b"Caf\xe9\n")]); let client = TestClient::new(fx.app.routes()); let (status, body) = text(&client, "/latin").await; check!(status == StatusCode::OK); check!(body.contains("binary file")); check!(body.contains("href=\"/latin/raw/README\"")); } #[tokio::test] async fn submodules_are_neither_listed_nor_served() { let fx = fixture(); let path = fx.dir.path().join("widget.git"); let repo = git2::Repository::open_bare(&path).expect("open"); let head = repo.head().unwrap().peel_to_commit().unwrap(); let mut builder = repo.treebuilder(Some(&head.tree().unwrap())).unwrap(); let elsewhere = git2::Oid::from_str("0123456789abcdef0123456789abcdef01234567").unwrap(); builder.insert("vendor", elsewhere, 0o160000).unwrap(); let tree = repo.find_tree(builder.write().unwrap()).unwrap(); let sig = git2::Signature::now("Test", "test@example.com").unwrap(); repo.commit(Some("HEAD"), &sig, &sig, "submodule", &tree, &[&head]) .unwrap(); let client = TestClient::new(fx.app.routes()); let (status, body) = text(&client, "/widget/file").await; check!(status == StatusCode::OK); check!(!body.contains("vendor")); for path in ["/widget/file/vendor", "/widget/raw/vendor"] { let (status, _) = text(&client, path).await; check!(status == StatusCode::NOT_FOUND, "{path}"); } } #[tokio::test] async fn raw_view_serves_bytes_with_safe_media_types() { let fx = fixture(); let client = TestClient::new(fx.app.routes()); let resp = client.get("/widget/raw/docs/logo.png").send().await; resp.assert_status_is_ok(); resp.assert_content_type("image/png"); resp.assert_header("x-content-type-options", "nosniff"); resp.assert_header("content-security-policy", "sandbox"); check!(resp.0.into_body().into_vec().await.unwrap() == PNG); let resp = client.get("/widget/raw/LICENSE").send().await; resp.assert_status_is_ok(); resp.assert_content_type("text/plain; charset=utf-8"); resp.assert_header("content-length", "19"); let resp = client.get("/widget/raw/docs/index.html").send().await; resp.assert_status_is_ok(); resp.assert_content_type("text/plain; charset=utf-8"); } #[tokio::test] async fn nothing_leaks_from_private_or_invalid_paths() { let fx = fixture(); let client = TestClient::new(fx.app.routes()); for path in [ "/secret/file", "/secret/file/LICENSE", "/secret/raw/LICENSE", "/widget/file/nope.txt", "/widget/raw/docs", "/widget/raw/nope.txt", "/widget/file/../secret", "/widget/raw/..%2FLICENSE", ] { let resp = client.get(path).send().await; check!(resp.0.status() == StatusCode::NOT_FOUND, "{path}"); } } #[tokio::test] async fn raw_view_streams_a_file_too_large_for_a_page() { let fx = Fixture::new(); let big: Vec = (0..3 * 1024 * 1024).map(|i| (i % 251) as u8).collect(); fx.repo_bytes("big", true, "Big", &[("blob.bin", &big)]); let client = TestClient::new(fx.app.routes()); let resp = client.get("/big/raw/blob.bin").send().await; resp.assert_status_is_ok(); resp.assert_content_type("application/octet-stream"); resp.assert_header("content-length", &big.len().to_string()); let body = resp.0.into_body().into_vec().await.unwrap(); check!(body == big); }