Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
10 kB · 317 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318use std::collections::BTreeMap;use std::io::{Read as _, Write as _};use std::net::{TcpListener, TcpStream};use std::path::{Path, PathBuf};use std::process::{Child, Command, Stdio};use std::thread;use std::time::{Duration, Instant};
use crate::config::ValidatedConfig;use crate::manifest::{BuildStats, GenerationManifest, StaticResponse};
#[test]fn wire_keys_encodes_user_segments_but_keeps_separators() { use super::wire_keys; // invariant: separator stays literal, user value gets form-encoded. let spellings = wire_keys("r_001|refs/heads/main"); assert_eq!(spellings[0], "r_001|refs/heads/main"); assert_eq!(spellings[1], "r_001|refs%2Fheads%2Fmain"); // invariant: path segments escape spaces and ':' too. let spellings = wire_keys("c_ab12|docs/a b.md|false"); assert_eq!(spellings[1], "c_ab12|docs%2Fa%20b.md|false"); // invariant: already-encoded keys are left alone. assert_eq!( wire_keys("r_001|refs%2Fheads%2Fmain"), vec!["r_001|refs%2Fheads%2Fmain".to_string()] ); // invariant: non-composite keys are returned as-is (repo aliases encode themselves). assert_eq!(wire_keys("did:plc:abc"), vec!["did:plc:abc".to_string()]);}
struct Nginx { binary: PathBuf, config: PathBuf, child: Child,}
impl Drop for Nginx { fn drop(&mut self) { let _ = Command::new(&self.binary) .args(["-s", "quit", "-c"]) .arg(&self.config) .stdout(Stdio::null()) .stderr(Stdio::null()) .status();
let deadline = Instant::now() + Duration::from_secs(1); while Instant::now() < deadline { if matches!(self.child.try_wait(), Ok(Some(_))) { return; } thread::sleep(Duration::from_millis(10)); } let _ = self.child.kill(); let _ = self.child.wait(); }}
fn response(body_hash: &str) -> StaticResponse { StaticResponse { body_hash: body_hash.into(), status: 200, content_type: "text/plain; charset=utf-8".into(), headers: Vec::new(), cas_key: body_hash.into(), }}
fn write_response(generation: &Path, body_hash: &str, body: &str) { std::fs::write(generation.join("www/_responses").join(body_hash), body).unwrap();}
fn get(port: u16, target: &str) -> (String, String) { let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .set_read_timeout(Some(Duration::from_secs(2))) .unwrap(); write!( stream, "GET {target} HTTP/1.1\r\nHost: knot.test\r\nConnection: close\r\n\r\n" ) .unwrap();
let mut response = String::new(); stream.read_to_string(&mut response).unwrap(); let (head, body) = response.split_once("\r\n\r\n").unwrap(); assert!(head.starts_with("HTTP/1.1 200 "), "{response}"); assert!( head.lines() .any(|line| line.eq_ignore_ascii_case("content-type: text/plain; charset=utf-8")), "{response}" );
(head.into(), body.into())}
#[test]#[ignore = "requires nginx; set NGINX_BINARY"]fn emitted_config_passes_nginx_test_and_serves_routes() { let nginx_binary = PathBuf::from( std::env::var_os("NGINX_BINARY") .expect("NGINX_BINARY must name the real nginx binary for this ignored test"), ); let temp = tempfile::tempdir().unwrap(); let generation = temp.path().join("generation"); std::fs::create_dir_all(generation.join("www/_responses")).unwrap();
let config_path = temp.path().join("knot-static.toml"); std::fs::write( &config_path, r#"[knot]hostname = "knot.test"owner = "did:plc:owner"[repo]scan_path = "repos"[secrets]sealed_key_file = "sealed-keys"[output]root = "output""#, ) .unwrap(); let config = ValidatedConfig::load(&config_path).unwrap();
let routes = BTreeMap::from([ ("did.json".into(), response("did")), ("version".into(), response("ver")), ("owner".into(), response("own")), ("health".into(), response("hlt")), ( "sh.tangled.repo.describeRepo?repoDid=did:plc:repo".into(), response("dsc"), ), ( "sh.tangled.repo.tag?repo=did:plc:repo&tag=v1".into(), response("tag"), ), ]); let manifest = GenerationManifest { generation_id: "test".into(), generator_profile: "test".into(), conf_template: "test".into(), knot_did: "did:web:knot.test".into(), hostname: "knot.test".into(), owner: "did:plc:owner".into(), static_version: "test".into(), capabilities: Vec::new(), profile: serde_json::json!({}), repos: Vec::new(), routes, sections: BTreeMap::new(), section_routes: BTreeMap::new(), section_objects: BTreeMap::new(), repo_aliases: BTreeMap::from([ ("did:plc:repo".into(), "r0".into()), ("did:plc:owner/repo".into(), "r0".into()), ]), ref_aliases: BTreeMap::new(), stats: BuildStats::default(), };
for (hash, body) in [ ("did", "did body"), ("ver", "version body"), ("own", "owner body"), ("hlt", "health body"), ("dsc", "describe body"), ("tag", "tag body"), ] { write_response(&generation, hash, body); }
// invariant: smart upload-pack artifacts captured under smart/ directory. let git_dir = generation.join("www/_git/r0/smart"); std::fs::create_dir_all(&git_dir).unwrap(); std::fs::write( git_dir.join("advertisement"), b"001e# service=git-upload-pack\n0000ADVERTISED", ) .unwrap(); std::fs::write(git_dir.join("upload-pack"), b"0008NAK\nPACKDATA").unwrap();
super::emit(&config, &manifest, &generation).unwrap(); let emitted_config = generation.join("nginx/knot-static.conf"); let port = TcpListener::bind(("127.0.0.1", 0)) .unwrap() .local_addr() .unwrap() .port(); let text = std::fs::read_to_string(&emitted_config) .unwrap() .replace("listen 8080;", &format!("listen 127.0.0.1:{port};")); std::fs::write(&emitted_config, text).unwrap();
let check = Command::new(&nginx_binary) .args(["-t", "-c"]) .arg(&emitted_config) .output() .unwrap(); assert!( check.status.success(), "nginx -t failed:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&check.stdout), String::from_utf8_lossy(&check.stderr) );
let child = Command::new(&nginx_binary) .args(["-c"]) .arg(&emitted_config) .args(["-g", "daemon off;"]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .unwrap(); let mut nginx = Nginx { binary: nginx_binary, config: emitted_config, child, };
let deadline = Instant::now() + Duration::from_secs(2); loop { if TcpStream::connect(("127.0.0.1", port)).is_ok() { break; } if let Some(status) = nginx.child.try_wait().unwrap() { panic!( "nginx exited during startup with {status}; error log:\n{}", std::fs::read_to_string(generation.join("nginx/error.log")).unwrap_or_default() ); } assert!( Instant::now() < deadline, "nginx did not start in two seconds" ); thread::sleep(Duration::from_millis(10)); }
for (target, expected) in [ ("/.well-known/did.json", "did body"), ("/xrpc/sh.tangled.knot.version", "version body"), ("/xrpc/sh.tangled.owner", "owner body"), ("/xrpc/_health", "health body"), ( "/xrpc/sh.tangled.repo.describeRepo?repoDid=did:plc:repo", "describe body", ), ( "/xrpc/sh.tangled.repo.describeRepo?repoDid=did%3Aplc%3Arepo", "describe body", ), ( "/xrpc/sh.tangled.repo.describeRepo?repo=did:plc:owner/repo", "describe body", ), ( "/xrpc/sh.tangled.repo.tag?repo=did:plc:owner/repo&tag=v1", "tag body", ), ] { let (_, body) = get(port, target); assert_eq!(body, expected, "GET {target}"); }
// invariant: smart upload-pack negotiation (knotmirror + modern git clients). let (status, head, body) = raw_request( port, "GET /did:plc:repo/info/refs?service=git-upload-pack HTTP/1.1\r\n", ); assert_eq!(status, "HTTP/1.1 200 ", "info/refs advertisement"); assert!( head.to_ascii_lowercase() .contains("content-type: application/x-git-upload-pack-advertisement"), "wrong info/refs content type: {head}" ); assert_eq!(body, "001e# service=git-upload-pack\n0000ADVERTISED");
let (status, head, body) = raw_request( port, "POST /did:plc:repo/git-upload-pack HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody", ); assert_eq!(status, "HTTP/1.1 200 ", "upload-pack POST"); assert!( head.to_ascii_lowercase() .contains("content-type: application/x-git-upload-pack-result"), "wrong upload-pack content type: {head}" ); assert_eq!(body, "0008NAK\nPACKDATA");
for refused in [ "GET /did:plc:repo/info/refs HTTP/1.1\r\n", "GET /did:plc:repo/info/refs?service=git-receive-pack HTTP/1.1\r\n", ] { let (status, _, _) = raw_request(port, refused); assert_ne!(status, "HTTP/1.1 200 ", "{refused} must be refused"); }}
/// Raw HTTP/1.1 request; returns (status line, head, body).fn raw_request(port: u16, request_head: &str) -> (String, String, String) { let mut stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); stream .set_read_timeout(Some(Duration::from_secs(2))) .unwrap(); write!( stream, "{request_head}Host: knot.test\r\nConnection: close\r\n\r\n" ) .unwrap(); let mut response = Vec::new(); stream.read_to_end(&mut response).unwrap(); let text = String::from_utf8_lossy(&response); let (head, body) = text.split_once("\r\n\r\n").unwrap(); let status = head.lines().next().unwrap_or_default(); (format!("{status} "), head.to_string(), body.to_string())}