//! Browsing pages (summary, tree, blob, raw, refs) against real repos. mod common; use axum::body::{Body, to_bytes}; use axum::http::{HeaderMap, Request, StatusCode, header}; use common::{Rich, bare, rich_repo}; use ferrenium::config::Config; use ferrenium::server; use ferrenium::state::{AppState, Overrides}; use tower::ServiceExt; struct Site { _root: tempfile::TempDir, app: axum::Router, rich: Rich, } fn site(extra: &str) -> Site { let root = tempfile::tempdir().unwrap(); let rich = rich_repo(root.path(), "rich.git"); rich_repo(root.path(), "forks/nested.git"); bare(root.path(), "empty.git", &[]); let toml = format!( "[repos]\nscan_path = {:?}\n[watch]\nenabled = false\n\ [site]\nclone_urls = [\"https://git.example.com/{{name}}\", \"git@example.com:{{name}}\"]\n\ [ui]\nmax_blob_kb = 2\n{extra}", root.path().to_str().unwrap() ); let mut config = Config::parse(&toml).expect("parse"); config.finish().expect("valid"); let state = AppState::new(config, Overrides::default()); let config = state.config.load_full(); state.index.full_scan(&config); Site { app: server::app(state), rich, _root: root, } } struct Res { status: StatusCode, headers: HeaderMap, body: String, } impl Res { fn header(&self, name: header::HeaderName) -> &str { self.headers .get(&name) .map(|v| v.to_str().unwrap()) .unwrap_or_default() } } async fn fetch(app: &axum::Router, uri: &str, etag: Option<&str>) -> Res { let mut req = Request::get(uri); if let Some(etag) = etag { req = req.header(header::IF_NONE_MATCH, etag); } let res = app .clone() .oneshot(req.body(Body::empty()).unwrap()) .await .unwrap(); let status = res.status(); let headers = res.headers().clone(); let body = to_bytes(res.into_body(), usize::MAX).await.unwrap(); Res { status, headers, body: String::from_utf8_lossy(&body).into_owned(), } } async fn ok(app: &axum::Router, uri: &str) -> Res { let res = fetch(app, uri, None).await; assert_eq!(res.status, StatusCode::OK, "{uri}: {}", res.body); assert_csp_clean(&res.body); res } /// Pages must work under `script-src 'self'` and no inline styles. fn assert_csp_clean(html: &str) { assert!(!html.contains("\n"); let res = fetch(&s.app, "/rich/raw/main/img/logo.png", None).await; assert_eq!(res.header(header::CONTENT_TYPE), "image/png"); let res = fetch(&s.app, "/rich/raw/main/data.bin", None).await; assert_eq!(res.header(header::CONTENT_TYPE), "application/octet-stream"); let res = fetch(&s.app, "/rich/raw/main/page.html?download", None).await; assert_eq!( res.header(header::CONTENT_DISPOSITION), "attachment; filename=\"page.html\"; filename*=UTF-8''page.html" ); let res = fetch( &s.app, "/rich/raw/main/%C3%BCn%C3%AFcode%20dir/f%C3%AFle%20%231.txt?download", None, ) .await; assert!( res.header(header::CONTENT_DISPOSITION) .contains("filename*=UTF-8''f%C3%AFle%20%231.txt") ); // Raw by full id is cacheable; ETag revalidates. let uri = format!("/rich/raw/{}/src/main.rs", s.rich.head); let res = fetch(&s.app, &uri, None).await; assert_eq!(res.header(header::CACHE_CONTROL), "public, max-age=86400"); let etag = res.header(header::ETAG).to_string(); assert_eq!( fetch(&s.app, &uri, Some(&etag)).await.status, StatusCode::NOT_MODIFIED ); assert_eq!( fetch(&s.app, "/rich/raw/main/src", None).await.status, StatusCode::NOT_FOUND ); } #[tokio::test] async fn refs_page() { let s = site(""); let res = ok(&s.app, "/rich/refs").await; let html = &res.body; let main = html.find(r#"href="/rich/tree/main""#).expect("main"); let feature = html .find(r#"href="/rich/tree/feature/x""#) .expect("feature"); assert!(main < feature, "default branch first"); assert!(html.contains(r#"Default"#)); assert!(html.contains("Release 1.1"), "annotated tag subject"); assert!(html.contains(r#"href="/rich/tree/v1.0""#)); assert!(html.contains(r#"Refs"#)); } #[tokio::test] async fn base_path_prefixes_repo_links() { let s = site("[server]\nbase_path = \"/git/\"\n"); let res = ok(&s.app, "/git/rich").await; assert!(res.body.contains(r#"href="/git/rich/tree/main/src""#)); assert!( res.body .contains(r#"href="/git/rich/blob/main/docs/guide.md""#) ); let res = fetch(&s.app, "/git/forks/nested.git", None).await; assert_eq!(res.header(header::LOCATION), "/git/forks/nested"); ok(&s.app, "/git/rich/blob/main/src/main.rs").await; } #[tokio::test] async fn every_referenced_icon_exists() { let s = site(""); let res = ok(&s.app, "/rich").await; let sprite_url = res .body .split('"') .find(|s| s.contains("icons/sprite.svg#")) .unwrap() .split('#') .next() .unwrap() .to_string(); let sprite = ok(&s.app, &sprite_url).await.body; for id in res .body .split("sprite.svg#") .skip(1) .map(|s| s.split('"').next().unwrap()) { assert!( sprite.contains(&format!("id=\"{id}\"")), "missing icon {id}" ); } }