//! `/_status` against a reference PDS: a mirror read as its code page //! reads it, one that can no longer be, and `knotless status` showing it. #[path = "../../knotless/tests/common/mod.rs"] mod common; use common::*; use knotless::commit::{Change, Spec}; use knotless::status::Status; use knotless_serve::{App, router}; use std::time::Duration; fn get(url: &str) -> (u16, Status) { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let res = reqwest::get(url).await.unwrap(); let code = res.status().as_u16(); (code, res.json().await.unwrap()) }) } fn cli(args: &[&str]) -> (i32, String) { let mut argv = vec!["knotless".to_string()]; argv.extend(args.iter().map(|a| a.to_string())); // On a thread of its own: the command's poll is more than a test // thread's stack holds, unoptimised. let run = std::thread::Builder::new() .stack_size(32 << 20) .spawn(move || { let mut out = Vec::new(); // No appview, so a bare `status` never asks knotless.dev. let env = knotless::env::Env::from_vars(|k| (k == "KNOTLESS_APPVIEW").then(String::new)); let code = knotless::cli::main_with(argv, env, &mut &b""[..], &mut out); (code, String::from_utf8(out).unwrap()) }); run.unwrap().join().unwrap() } #[test] fn status_reads_a_mirror() { let s = Sandbox::new(); let rt = tokio::runtime::Runtime::new().unwrap(); let url = knotless::url::RepoUrl::parse(&s.url("probe")).unwrap(); let spec = Spec { branch: "main".into(), message: "first".into(), changes: vec![Change::Put { path: "a.txt".into(), data: b"a\n".to_vec(), executable: false, }], retries: 3, ..Default::default() }; let tip = rt .block_on(knotless::commit::commit(&ctx(&s), &url, &spec)) .unwrap() .sha; // Trusted for a minute, so the mirror is not synced again under the test. let app = App::new(ctx(&s), Duration::from_secs(60)); let listener = rt .block_on(tokio::net::TcpListener::bind("127.0.0.1:0")) .unwrap(); let base = format!("http://{}", listener.local_addr().unwrap()); let routes = router(app.clone()); rt.spawn(async move { axum::serve(listener, routes).await.unwrap() }); let handle = s.account.handle.clone(); // Nothing followed, no index, no mirror asked after: well. let (code, status) = get(&format!("{base}/_status")); assert_eq!(code, 200); assert!(status.ok && status.problems.is_empty(), "{status:?}"); // The mirror reads: its commit, and its packs counted. let probe = format!("{handle}/probe"); let (code, status) = get(&format!("{base}/_status?repo={probe}.git")); assert_eq!(code, 200, "{status:?}"); let mirror = status.mirror.unwrap(); assert_eq!(mirror.commit.as_deref(), Some(tip.as_str())); assert!(mirror.packs.unwrap() >= 1, "{mirror:?}"); let (code, text) = cli(&["status", &base, "--repo", &probe]); assert_eq!(code, 0, "{text}"); assert!(text.starts_with(&format!("ok\t{base}")), "{text}"); assert!( text.contains(&format!("mirror\t{probe}\tok at {}", &tip[..12])), "{text}" ); let (code, json) = cli(&["status", &base, "--json"]); assert_eq!(code, 0); assert!(serde_json::from_str::(&json).unwrap().ok); // No such repository, or no repository named at all. let (code, status) = get(&format!("{base}/_status?repo={handle}/nope")); assert_eq!(code, 503); let error = status.mirror.unwrap().error.unwrap(); assert!(error.contains("no such repository"), "{error}"); assert!(status.problems[0].starts_with(&format!("mirror {handle}/nope: "))); let (code, status) = get(&format!("{base}/_status?repo=probe")); assert_eq!(code, 503); let error = status.mirror.unwrap().error.unwrap(); assert_eq!(error, "?repo= takes /"); // Its packs gone: the objects can no longer be read, as when a mirror // outgrew the room its open handle had for packs. let git = rt.block_on(app.mirror(&handle, "probe")).unwrap(); for pack in std::fs::read_dir(git.dir.join("objects/pack")).unwrap() { std::fs::remove_file(pack.unwrap().path()).unwrap(); } let (code, status) = get(&format!("{base}/_status?repo={probe}")); assert_eq!(code, 503); let mirror = status.mirror.unwrap(); assert_eq!( (mirror.ok, mirror.packs, mirror.commit), (false, None, None) ); assert!(mirror.error.is_some()); let (code, text) = cli(&["status", &base, "--repo", &probe]); assert_eq!(code, 1); assert!(text.starts_with("UNHEALTHY"), "{text}"); // What the command is asked wrongly. assert_eq!(cli(&["status"]).0, 1); assert_eq!(cli(&["status", &base, "--bogus"]).0, 1); assert_eq!(cli(&["status", "http://127.0.0.1:1"]).0, 1); assert_eq!(cli(&["status", &format!("{base}/healthz")]).0, 1); }