site.rs
raw
//! The HTML site: listing, repository pages, and what stays hidden.
mod common;
use assert2::check;
use common::{Fixture, PUBLIC_URL};
use poem::{http::StatusCode, test::TestClient};
const README: &str = "# Widget\n\nA *widget* for testing.\n\n<script>alert(1)</script>\n";
fn fixture() -> Fixture {
let fx = Fixture::new();
fx.repo(
"widget",
true,
"Widgets for everyone",
&[("README.md", README)],
);
fx.repo(
"secret",
false,
"Nobody's business",
&[("README.md", "# Secret\n")],
);
fx.repo("plain", true, "", &[("README", "Just text <here>\n")]);
fx.repo("bare", true, "Nothing committed", &[]);
fx
}
#[tokio::test]
async fn index_lists_only_public_repositories() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let resp = client.get("/").send().await;
resp.assert_status_is_ok();
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("href=\"/widget\""));
check!(body.contains("Widgets for everyone"));
check!(body.contains("href=\"/plain\""));
check!(body.contains("href=\"/bare\""));
check!(!body.contains("secret"));
check!(body.contains("Test Site"));
}
#[tokio::test]
async fn index_survives_a_repository_that_cannot_be_opened() {
let fx = fixture();
let broken = fx.dir.path().join("broken.git");
std::fs::create_dir(&broken).unwrap();
common::set_public(&broken, true);
let client = TestClient::new(fx.app.routes());
let resp = client.get("/").send().await;
resp.assert_status_is_ok();
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("href=\"/widget\""));
check!(!body.contains("broken"));
}
#[tokio::test]
async fn a_directory_named_dot_git_dot_git_is_not_a_second_listing() {
let fx = Fixture::new();
fx.repo("tool", true, "The tool", &[]);
fx.repo("tool.git", true, "Named with a suffix", &[]);
let client = TestClient::new(fx.app.routes());
let body = client
.get("/")
.send()
.await
.0
.into_body()
.into_string()
.await
.unwrap();
check!(body.matches("href=\"/tool\"").count() == 1);
check!(!body.contains("Named with a suffix"));
check!(client.get("/tool.git.git").send().await.0.status() == StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn index_is_sorted_case_insensitively() {
let fx = Fixture::new();
for name in ["zeta", "Alpha", "beta", "Gamma"] {
fx.repo(name, true, "", &[]);
}
let client = TestClient::new(fx.app.routes());
let body = client
.get("/")
.send()
.await
.0
.into_body()
.into_string()
.await
.unwrap();
let position = |name: &str| body.find(&format!("href=\"/{name}\"")).expect(name);
check!(position("Alpha") < position("beta"));
check!(position("beta") < position("Gamma"));
check!(position("Gamma") < position("zeta"));
}
#[tokio::test]
async fn index_paginates_when_there_are_more_repositories_than_fit() {
let fx = Fixture::with_page_size(2);
for name in ["a", "b", "c", "d", "e"] {
fx.repo(name, true, "", &[]);
}
let client = TestClient::new(fx.app.routes());
let first = client
.get("/")
.send()
.await
.0
.into_body()
.into_string()
.await
.unwrap();
check!(first.contains("href=\"/a\"") && first.contains("href=\"/b\""));
check!(!first.contains("href=\"/c\""));
check!(first.contains("Page 1 of 3"));
check!(first.contains("href=\"/?page=2\""));
let last = client
.get("/?page=3")
.send()
.await
.0
.into_body()
.into_string()
.await
.unwrap();
check!(last.contains("href=\"/e\"") && !last.contains("href=\"/d\""));
check!(last.contains("href=\"/?page=2\"") && !last.contains("href=\"/?page=4\""));
let beyond = client.get("/?page=4").send().await;
check!(beyond.0.status() == StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn index_has_no_pager_when_everything_fits() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let body = client
.get("/")
.send()
.await
.0
.into_body()
.into_string()
.await
.unwrap();
check!(!body.contains("class=\"pager\""));
}
#[tokio::test]
async fn repository_page_shows_readme_and_clone_url() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let resp = client.get("/widget").send().await;
resp.assert_status_is_ok();
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("<h1>Widget</h1>"));
check!(body.contains("<em>widget</em>"));
check!(!body.contains("<script"));
check!(body.contains(&format!("git clone {PUBLIC_URL}/widget.git")));
check!(body.contains("Widgets for everyone"));
check!(body.contains("main"));
}
#[tokio::test]
async fn repository_page_accepts_dot_git_suffix_and_trailing_slash() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
for path in ["/widget.git", "/widget/", "/widget/file/"] {
client.get(path).send().await.assert_status_is_ok();
}
}
#[tokio::test]
async fn clone_url_follows_the_request_host_when_nothing_is_configured() {
let fx = Fixture::new();
fx.repo("widget", true, "", &[]);
let mut config = fx.app.config.clone();
config.public_url = None;
let app = mugit::App::new(config).unwrap();
let client = TestClient::new(app.routes());
let body = client
.get("/widget")
.header("host", "git.example.org")
.send()
.await
.0
.into_body()
.into_string()
.await
.unwrap();
check!(body.contains("git clone http://git.example.org/widget.git"));
}
#[tokio::test]
async fn every_failure_is_a_page_of_the_site() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let resp = client.get("/?page=abc").send().await;
check!(resp.0.status() == StatusCode::BAD_REQUEST);
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("Bad Request"));
check!(body.contains("Test Site"));
}
#[tokio::test]
async fn plain_readme_is_shown_verbatim_and_escaped() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let resp = client.get("/plain").send().await;
resp.assert_status_is_ok();
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("Just text <here>"));
}
#[tokio::test]
async fn empty_repository_page_still_renders() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
let resp = client.get("/bare").send().await;
resp.assert_status_is_ok();
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("no README"));
}
#[tokio::test]
async fn private_and_missing_repositories_are_indistinguishable() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
for path in [
"/secret",
"/secret.git",
"/nonexistent",
"/.hidden",
"/a..b",
] {
let resp = client.get(path).send().await;
check!(resp.0.status() == StatusCode::NOT_FOUND, "{path}");
let body = resp.0.into_body().into_string().await.unwrap();
check!(body.contains("Not Found"), "{path}");
check!(body.contains("Test Site"), "{path}");
}
}
#[tokio::test]
async fn unpublishing_hides_a_repository_immediately() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
client.get("/widget").send().await.assert_status_is_ok();
common::set_public(&fx.dir.path().join("widget.git"), false);
let resp = client.get("/widget").send().await;
check!(resp.0.status() == StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn every_page_carries_the_browser_policy() {
let fx = fixture();
let client = TestClient::new(fx.app.routes());
for path in [
"/",
"/widget",
"/widget/file",
"/widget/file/README.md",
"/nonexistent",
] {
let resp = client.get(path).send().await;
let headers = resp.0.headers();
let policy = headers
.get("content-security-policy")
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
check!(policy.contains("default-src 'none'"), "{path}");
check!(policy.contains("frame-ancestors 'none'"), "{path}");
check!(headers.get("referrer-policy").is_some(), "{path}");
check!(
headers
.get("x-content-type-options")
.is_some_and(|v| v == "nosniff"),
"{path}"
);
check!(headers.get("strict-transport-security").is_none(), "{path}");
}
}
#[tokio::test]
async fn strict_transport_is_demanded_once_there_is_a_tls_domain() {
let fx = Fixture::with_args(&["--tls-domain", "git.test"]);
let client = TestClient::new(fx.app.routes());
let resp = client.get("/").send().await;
resp.assert_status_is_ok();
resp.assert_header("strict-transport-security", "max-age=31536000");
}