//! End-to-end tests: build real git repositories in a temp dir, then check
//! the HTML pages and do a real `git clone` against a running server.
use axum::body::Body;
use axum::http::{Request, StatusCode};
use forge::config::Config;
use forge::web::{router, AppState};
use http_body_util::BodyExt;
use std::path::Path;
use std::process::Command;
use tower::ServiceExt; // for `oneshot`
/// Run a git command in `dir`, panicking with its output if it fails.
fn git(dir: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.args([
"-c",
"user.name=Test User",
"-c",
"user.email=test@example.com",
"-c",
"init.defaultBranch=main",
])
.args(args)
.current_dir(dir)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_AUTHOR_DATE", "2026-01-01T12:00:00Z")
.env("GIT_COMMITTER_DATE", "2026-01-01T12:00:00Z")
.output()
.expect("git is installed");
assert!(out.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&out.stderr));
String::from_utf8_lossy(&out.stdout).into_owned()
}
/// A temp dir holding: `work/` (normal repo, 56 commits) and `bare.git`.
struct Fixture {
dir: tempfile::TempDir,
head: String,
change_commit: String,
}
fn fixture() -> Fixture {
let dir = tempfile::tempdir().unwrap();
let work = dir.path().join("work");
std::fs::create_dir(&work).unwrap();
git(&work, &["init", "-q"]);
std::fs::write(
work.join("README.md"),
"# Hello Forge\n\nSome **bold** text.\n\n\n",
)
.unwrap();
std::fs::create_dir(work.join("src")).unwrap();
std::fs::write(work.join("src/main.rs"), "fn main() {\n println!(\"old line\");\n}\n").unwrap();
std::fs::write(work.join("src/odd name #1.txt"), "spaces & hashes \n").unwrap();
git(&work, &["add", "."]);
git(&work, &["commit", "-q", "-m", "initial commit"]);
// 54 filler commits so the log has more than one page (50 per page).
for i in 0..54 {
git(&work, &["commit", "-q", "--allow-empty", "-m", &format!("filler {i}")]);
}
std::fs::write(work.join("src/main.rs"), "fn main() {\n println!(\"new line\");\n}\n").unwrap();
git(&work, &["commit", "-q", "-am", "change "]);
let change_commit = git(&work, &["rev-parse", "HEAD"]).trim().to_string();
git(&work, &["tag", "v1.0"]);
git(&work, &["branch", "feature/x"]);
let head = change_commit.clone();
git(dir.path(), &["clone", "-q", "--bare", "work", "bare.git"]);
Fixture { dir, head, change_commit }
}
fn state_for(f: &Fixture) -> forge::web::Shared {
let toml = format!(
r#"
[server]
site_title = "test forge"
clone_base_url = "http://example.test"
[[repo]]
name = "work"
path = "{}"
description = "non-bare repo"
[[repo]]
name = "bare"
path = "{}"
description = "a bare repo"
"#,
f.dir.path().join("work").display(),
f.dir.path().join("bare.git").display(),
);
let cfg = Config::from_toml(&toml).unwrap();
let repos = cfg.validate().unwrap();
AppState::new(cfg.server, repos)
}
/// Make one request against the router (no network) and return status + body.
async fn get(f: &Fixture, uri: &str) -> (StatusCode, String) {
let app = router(state_for(f));
let res = app.oneshot(Request::get(uri).body(Body::empty()).unwrap()).await.unwrap();
let status = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
(status, String::from_utf8_lossy(&bytes).into_owned())
}
#[tokio::test]
async fn index_lists_repos_and_escapes_descriptions() {
let f = fixture();
let (status, body) = get(&f, "/").await;
assert_eq!(status, StatusCode::OK);
assert!(body.contains("href=\"/work/\""));
assert!(body.contains("href=\"/bare/\""));
assert!(body.contains("non-bare <b>repo</b>"));
assert!(!body.contains("repo"));
assert!(body.contains("ago"), "idle column should show an age");
}
#[tokio::test]
async fn summary_shows_refs_commits_and_safe_readme() {
let f = fixture();
for repo in ["work", "bare"] {
let (status, body) = get(&f, &format!("/{repo}/")).await;
assert_eq!(status, StatusCode::OK, "{repo}");
assert!(body.contains("feature/x"), "branch listed");
assert!(body.contains("v1.0"), "tag listed");
assert!(body.contains("change <greeting>"), "commit subject escaped");
assert!(body.contains("Hello Forge
"), "README rendered");
assert!(body.contains("bold"));
assert!(!body.contains("