use hemx_test::TestProcess; use std::fs; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const STARTUP_TIMEOUT: Duration = Duration::from_secs(12); fn available_address() -> String { let listener = TcpListener::bind("127.0.0.1:0").expect("reserve test port"); let address = listener.local_addr().expect("test address"); drop(listener); address.to_string() } fn test_path(label: &str) -> PathBuf { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("system clock") .as_nanos(); std::env::temp_dir().join(format!("hemx-saas-{label}-{}-{nonce}", std::process::id())) } fn start(address: &str, store: &Path) -> TestProcess { let mut command = Command::new(env!("CARGO_BIN_EXE_hemx-saas-example")); command .env("HEMX_SAAS_ADDR", address) .env("HEMX_SAAS_STORE", store); TestProcess::start(command, "hemx-saas", address, STARTUP_TIMEOUT).expect("start SaaS app") } fn request( address: &str, method: &str, path: &str, headers: &[(&str, &str)], body: &str, ) -> String { let mut stream = TcpStream::connect(address).expect("connect to SaaS app"); write!( stream, "{method} {path} HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\nContent-Length: {}\r\n", body.len() ) .expect("write request line"); for (name, value) in headers { write!(stream, "{name}: {value}\r\n").expect("write request header"); } write!(stream, "\r\n{body}").expect("finish request"); let mut response = String::new(); stream.read_to_string(&mut response).expect("read response"); response } fn create(address: &str, name: &str, bearer: &str, csrf: &str, origin: &str) -> String { create_at_version(address, name, bearer, csrf, origin, None) } fn create_at_version( address: &str, name: &str, bearer: &str, csrf: &str, origin: &str, fingerprint: Option<&str>, ) -> String { let mut headers = vec![ ("Authorization", bearer), ("Origin", origin), ("Content-Type", "application/x-www-form-urlencoded"), ]; if let Some(fingerprint) = fingerprint { headers.push(("x-hemx-fingerprint", fingerprint)); } request( address, "POST", "/projects", &headers, &format!("name={name}&csrf={csrf}"), ) } fn response_header<'a>(response: &'a str, name: &str) -> &'a str { response .lines() .find_map(|line| { let (header_name, value) = line.split_once(':')?; header_name.eq_ignore_ascii_case(name).then(|| value.trim()) }) .unwrap_or_else(|| panic!("missing {name} response header")) } fn ready_fingerprint(response: &str) -> &str { let marker = "\"fingerprint\":\""; let start = response.find(marker).expect("readiness fingerprint") + marker.len(); let end = response[start..].find('"').expect("fingerprint end") + start; &response[start..end] } #[test] fn authenticated_project_mutation_is_atomic_and_survives_restart() { // test req: auth/001 req: auth/002 req: auth/004 req: security/004 req: security/006 // test req: security/009 req: operations/001 req: operations/006 req: v1_release/003 let address = available_address(); let origin = format!("http://{address}"); let store = test_path("durable"); { let _app = start(&address, &store); let home = request(&address, "GET", "/", &[], ""); let csp = response_header(&home, "content-security-policy"); assert!(csp.contains("default-src 'self'"), "{csp}"); assert!(csp.contains("script-src 'self'"), "{csp}"); assert!(csp.contains("object-src 'none'"), "{csp}"); assert!(csp.contains("form-action 'self'"), "{csp}"); assert!(!csp.contains("unsafe-inline"), "{csp}"); assert!(!csp.contains("unsafe-eval"), "{csp}"); assert_eq!(response_header(&home, "x-content-type-options"), "nosniff"); assert_eq!( response_header(&home, "referrer-policy"), "strict-origin-when-cross-origin" ); assert!(!home.contains("