//! End-to-end: a real server on a loopback port, clients over HTTP. use oxy_core::repo::{Repo, SignatureStatus}; use std::fs; use std::net::TcpListener; use std::path::Path; fn write(root: &Path, rel: &str, content: &[u8]) { let p = root.join(rel); fs::create_dir_all(p.parent().unwrap()).unwrap(); fs::write(p, content).unwrap(); } /// Point `$OXY_HOME` at a throwaway directory once per test process so no /// test reads or writes the developer's real user configuration or keys. fn hermetic() { static ONCE: std::sync::Once = std::sync::Once::new(); static DIR: std::sync::OnceLock = std::sync::OnceLock::new(); ONCE.call_once(|| { let d = tempfile::tempdir().unwrap(); std::env::set_var("OXY_HOME", d.path()); DIR.set(d).ok(); }); } fn start(root: &Path, tokens: Vec) -> String { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); let cfg = oxy_server::ServerConfig { root: root.to_path_buf(), tokens }; std::thread::spawn(move || oxy_server::run_blocking(listener, cfg).unwrap()); format!("http://127.0.0.1:{port}") } #[test] fn clone_push_pull_over_http() { hermetic(); let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let served = root.join("served"); Repo::init(&served.join("origin")).unwrap(); let base = start(&served, vec![]); let url = format!("{base}/origin"); let mut a = Repo::clone(&url, &root.join("a")).unwrap().with_keys_dir(root.join("keys")); let (key, _) = a.create_identity("Ada", "ada@example.org").unwrap(); write(&root.join("a"), "f.txt", b"hello\n"); let h1 = a.commit("hello", &[]).unwrap(); let r = a.push("origin", false).unwrap(); assert!(r[0].ok, "{}", r[0].reason); let origin = Repo::open(&served.join("origin")).unwrap(); assert_eq!(origin.bookmark_heads().unwrap(), vec![h1]); assert!(matches!(origin.authenticate_change(&h1).unwrap(), SignatureStatus::Valid { key: k, sealed: true, .. } if k == key)); let b = Repo::clone(&url, &root.join("b")).unwrap(); assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"hello\n"); write(&root.join("b"), "g.txt", b"g\n"); let h2 = b.commit("g", &[]).unwrap(); assert!(b.push("origin", false).unwrap()[0].ok); let pr = a.pull("origin").unwrap(); assert!(pr.changed && pr.checked_out); assert_eq!(fs::read(root.join("a/g.txt")).unwrap(), b"g\n"); let mut heads = a.bookmark_heads().unwrap(); heads.sort(); let mut want = vec![h1, h2]; want.sort(); assert_eq!(heads, want); // Unknown repositories and names the server refuses are surfaced as errors. let err = Repo::clone(&format!("{base}/nope"), &root.join("c")).unwrap_err().to_string(); assert!(err.contains("404"), "{err}"); fs::create_dir_all(served.join(".hidden/.oxy")).unwrap(); assert!(Repo::clone(&format!("{base}/.hidden"), &root.join("d")).unwrap_err().to_string().contains("404")); } #[test] fn push_requires_a_token_when_configured() { hermetic(); let dir = tempfile::tempdir().unwrap(); let root = dir.path(); let served = root.join("served"); Repo::init(&served.join("origin")).unwrap(); let base = start(&served, vec!["s3cret".into()]); let url = format!("{base}/origin"); // Reads are open. let mut a = Repo::clone(&url, &root.join("a")).unwrap(); write(&root.join("a"), "f.txt", b"x\n"); a.commit("x", &[]).unwrap(); let err = a.push("origin", false).unwrap_err().to_string(); assert!(err.contains("401"), "{err}"); a.config.set("remote.origin.token", "wrong").unwrap(); a.save_config().unwrap(); assert!(a.push("origin", false).unwrap_err().to_string().contains("401")); a.config.set("remote.origin.token", "s3cret").unwrap(); a.save_config().unwrap(); assert!(a.push("origin", false).unwrap()[0].ok); let origin = Repo::open(&served.join("origin")).unwrap(); assert_eq!(origin.bookmark_heads().unwrap().len(), 1); }