mod common;
use common::Sandbox;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
const HOST: &str = "127.0.0.1";
fn pick_free_port() -> u16 {
let listener = std::net::TcpListener::bind(format!("{}:0", HOST)).expect("bind ephemeral");
listener.local_addr().expect("local_addr").port()
}
fn spawn_server(s: &Sandbox, port: u16) -> Child {
Command::new(env!("CARGO_BIN_EXE_req"))
.args([
"--file",
s.path().to_str().unwrap(),
"serve",
"--host",
HOST,
"--port",
&port.to_string(),
"--read-only",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn req serve")
}
fn wait_for_bind(port: u16, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if TcpStream::connect_timeout(
&format!("{}:{}", HOST, port).parse().unwrap(),
Duration::from_millis(200),
)
.is_ok()
{
return true;
}
std::thread::sleep(Duration::from_millis(75));
}
false
}
fn http_get(port: u16, path: &str) -> (u16, String) {
let mut stream = TcpStream::connect(format!("{}:{}", HOST, port)).expect("connect to server");
stream.set_read_timeout(Some(Duration::from_secs(5))).ok();
write!(
stream,
"GET {} HTTP/1.1\r\nHost: {}:{}\r\nConnection: close\r\n\r\n",
path, HOST, port
)
.expect("write request");
let mut buf = String::new();
stream.read_to_string(&mut buf).ok();
let mut lines = buf.splitn(2, "\r\n");
let status_line = lines.next().unwrap_or("");
let rest = lines.next().unwrap_or("");
let body = rest
.split_once("\r\n\r\n")
.map(|x| x.1)
.unwrap_or("")
.to_string();
let code = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(0);
(code, body)
}
struct GuardedChild(Option<Child>);
impl Drop for GuardedChild {
fn drop(&mut self) {
if let Some(mut c) = self.0.take() {
let _ = c.kill();
let _ = c.wait();
}
}
}
fn fixture() -> (Sandbox, GuardedChild, u16) {
let s = Sandbox::new();
s.init("p");
let _ = s.run(&[
"add",
"--title",
"Hosted on the local web server for inspection",
"--statement",
"The system shall render this requirement at GET /r/REQ-0001.",
"--rationale",
"Fixture for serve smoke tests.",
"--kind",
"constraint",
"--priority",
"could",
]);
let port = pick_free_port();
let child = spawn_server(&s, port);
let bound = wait_for_bind(port, Duration::from_secs(10));
assert!(
bound,
"req serve did not bind to {}:{} within 10s",
HOST, port
);
(s, GuardedChild(Some(child)), port)
}
#[test]
fn req_0016_serve_root_returns_html_index() {
let (_s, _child, port) = fixture();
let (code, body) = http_get(port, "/");
assert_eq!(code, 200, "index should return 200, got {}", code);
assert!(
body.contains("<html"),
"body should be HTML: {}",
&body[..body.len().min(200)]
);
assert!(body.contains("REQ-0001"), "index should list REQ-0001");
}
#[test]
fn req_0016_serve_show_route_returns_html_detail() {
let (_s, _child, port) = fixture();
let (code, body) = http_get(port, "/r/REQ-0001");
assert_eq!(code, 200);
assert!(body.contains("REQ-0001"));
assert!(body.contains("Hosted on the local web server"));
assert!(body.contains("Statement") || body.contains("statement"));
}
#[test]
fn req_0016_serve_api_list_returns_json_array() {
let (_s, _child, port) = fixture();
let (code, body) = http_get(port, "/api/list");
assert_eq!(code, 200);
let v: serde_json::Value = serde_json::from_str(&body)
.unwrap_or_else(|_| panic!("/api/list should return JSON, got: {}", body));
let arr = v.as_array().expect("array of requirements");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"].as_str().unwrap(), "REQ-0001");
}
#[test]
fn req_0016_serve_api_show_returns_json_object() {
let (_s, _child, port) = fixture();
let (code, body) = http_get(port, "/api/r/REQ-0001");
assert_eq!(code, 200);
let v: serde_json::Value = serde_json::from_str(&body).expect("json object");
assert_eq!(v["id"].as_str().unwrap(), "REQ-0001");
}
#[test]
fn req_0016_serve_unknown_id_returns_404() {
let (_s, _child, port) = fixture();
let bogus = format!("REQ-{:04}", 9999);
let url = format!("/api/r/{}", bogus);
let (code, _body) = http_get(port, &url);
assert_eq!(code, 404);
}
#[test]
fn req_0016_serve_html_escapes_user_supplied_strings() {
let s = Sandbox::new();
s.init("p");
let _ = s.run(&[
"add",
"--title",
"Has <script>tag and \"quotes\" in title",
"--statement",
"The system shall escape these characters on render.",
"--rationale",
"Fixture for HTML-escape behaviour in serve.",
"--kind",
"constraint",
"--priority",
"could",
]);
let port = pick_free_port();
let child = spawn_server(&s, port);
let _guard = GuardedChild(Some(child));
assert!(wait_for_bind(port, Duration::from_secs(10)));
let (code, body) = http_get(port, "/");
assert_eq!(code, 200);
assert!(
!body.contains("<script>tag"),
"raw < entity leaked through escape: {}",
&body[..body.len().min(400)]
);
assert!(
body.contains("<script>tag") || body.contains("<script>"),
"expected < entity in escaped output"
);
}
#[test]
fn req_0134_serve_safety_view_and_api() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
let _ = s.run(&[
"hazard",
"add",
"-t",
"Hazardous mode",
"--harm",
"operator could be hurt",
"-C",
"C_C",
"-F",
"F_B",
"-P",
"P_B",
"-W",
"W3",
]);
let _ = s.run(&["sf", "add", "-t", "Interlock", "--mitigates", "HAZ-0001"]);
let port = pick_free_port();
let child = spawn_server(&s, port);
assert!(
wait_for_bind(port, Duration::from_secs(10)),
"serve did not bind"
);
let _guard = GuardedChild(Some(child));
let (code, body) = http_get(port, "/");
assert_eq!(code, 200);
assert!(body.contains("/safety"), "index should link to /safety");
let (code, body) = http_get(port, "/safety");
assert_eq!(code, 200, "/safety should return 200");
assert!(body.contains("HAZ-0001"), "/safety should list the hazard");
assert!(body.contains("SIL3"), "/safety should show the derived SIL");
assert!(
body.contains("not qualified per IEC 61508-3"),
"/safety must carry the disclaimer"
);
let (code, body) = http_get(port, "/api/safety");
assert_eq!(code, 200, "/api/safety should return 200");
assert!(body.contains("\"hazards\""), "api should include hazards");
assert!(body.contains("HAZ-0001"));
}
#[test]
fn req_0147_web_navigates_safety_chain_and_verification() {
let s = Sandbox::new();
s.init("p");
s.enable_safety();
s.run(&[
"hazard", "add", "-t", "H", "--harm", "hurt", "-C", "C_C", "-F", "F_B", "-P", "P_B", "-W",
"W3",
]);
s.run(&["sf", "add", "-t", "Stop fn", "--mitigates", "HAZ-0001"]);
s.run(&[
"sreq",
"add",
"-t",
"Stop the blade",
"-s",
"The system shall stop the blade on demand.",
"-r",
"operator safety",
"-a",
"stops",
"--realizes",
"SF-0001",
]);
s.run(&[
"sreq", "update", "SR-0001", "--status", "approved", "--reason", "r",
]);
s.run(&[
"sreq",
"update",
"SR-0001",
"--status",
"implemented",
"--reason",
"r",
]);
s.run(&[
"sreq",
"verify",
"SR-0001",
"--by",
"automated",
"--notes",
"bench",
]);
s.run(&["verification", "plan", "SR-0001", "--plan", "p"]);
s.run(&[
"verification",
"analysis",
"SR-0001",
"--findings",
"reviewed",
"--result",
"pass",
]);
s.run(&[
"verification",
"test",
"SR-0001",
"--findings",
"bench",
"--result",
"pass",
]);
s.run(&[
"verification",
"conclude",
"SR-0001",
"--statement",
"meets",
"--promote",
]);
s.run(&["verification", "confirm", "SR-0001"]);
let port = pick_free_port();
let _child = GuardedChild(Some(spawn_server(&s, port)));
assert!(
wait_for_bind(port, Duration::from_secs(10)),
"req serve did not bind"
);
let (c0, safety) = http_get(port, "/safety");
assert_eq!(c0, 200);
assert!(
safety.contains("/s/HAZ-0001"),
"safety page must link the hazard to its detail page:\n{safety}"
);
let (c1, haz) = http_get(port, "/s/HAZ-0001");
assert_eq!(c1, 200);
assert!(
haz.contains("/s/SF-0001") && haz.contains("/s/SR-0001"),
"hazard page must render the SF→SR chain as links:\n{haz}"
);
let (_c2, sf) = http_get(port, "/s/SF-0001");
assert!(
sf.contains("/s/HAZ-0001") && sf.contains("/s/SR-0001"),
"SF page must link the hazard it mitigates and the SR that realizes it:\n{sf}"
);
let (c3, sr) = http_get(port, "/s/SR-0001");
assert_eq!(c3, 200);
assert!(
sr.contains("/s/SF-0001"),
"SR page must link the safety function it realizes:\n{sr}"
);
assert!(
sr.contains("Verification dossier") && sr.contains("human-confirmed"),
"SR page must show the verification dossier with the human confirmation:\n{sr}"
);
}