//! What the VS Code extension drives (spec ยง18.1): `spectra status` and //! `spectra probe` say what is set up and what a server is, and `spectra //! bridge` signs for the extension, refusing anyone without its token and //! any web page. mod common; use common::{add_agent, hermetic, run, setup, text, Serving}; use serde_json::Value; use std::io::{BufRead, BufReader, Read, Write}; use std::process::Stdio; fn http(url: &str, method: &str, path: &str, headers: &[(&str, &str)], body: &str) -> (u16, String) { let hp = url.strip_prefix("http://").unwrap(); let mut s = std::net::TcpStream::connect(hp).unwrap(); s.set_read_timeout(Some(std::time::Duration::from_secs(10))).unwrap(); let mut req = format!("{method} {path} HTTP/1.1\r\nconnection: close\r\ncontent-length: {}\r\n", body.len()); if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("host")) { req.push_str(&format!("host: {hp}\r\n")); } for (k, v) in headers { req.push_str(&format!("{k}: {v}\r\n")); } req.push_str("\r\n"); req.push_str(body); s.write_all(req.as_bytes()).unwrap(); let mut out = String::new(); let _ = s.read_to_string(&mut out); let status = out.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0); (status, out.split_once("\r\n\r\n").map(|x| x.1.to_string()).unwrap_or_default()) } #[test] fn status_says_what_is_set_up() { let dir = tempfile::tempdir().unwrap(); let d = dir.path(); let v: Value = serde_json::from_slice(&run(d, &["status", "--json"]).stdout).unwrap(); assert!(v["identity"].is_null() && v["server"].is_null() && v["local"]["initialised"] == false, "{v}"); let made: Value = serde_json::from_slice(&run(d, &["identity", "create", "--name", "Andy McCall", "--json"]).stdout).unwrap(); assert_eq!(made["key"].as_str().unwrap().len(), 64); assert!(run(d, &["init"]).status.success()); let v: Value = serde_json::from_slice(&run(d, &["status", "--json"]).stdout).unwrap(); assert_eq!(v["identity"]["name"], "Andy McCall", "init made a successor (it delegates to the instance), so the key moved on"); assert_ne!(v["identity"]["key"], made["key"]); assert_eq!(v["local"]["initialised"], true); assert_eq!(v["reachable"], false, "not serving"); } #[tokio::test] async fn the_bridge_signs_for_its_holder_only() { let dir = setup(); let d = dir.path(); add_agent(d, "claude-oxygen", &[]); let srv = Serving::start(d); let mut child = hermetic(d).args(["bridge"]).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().unwrap(); let mut line = String::new(); BufReader::new(child.stdout.take().unwrap()).read_line(&mut line).unwrap(); let hello: Value = serde_json::from_str(&line).unwrap(); let (url, token) = (hello["url"].as_str().unwrap().to_string(), hello["token"].as_str().unwrap().to_string()); let bearer = format!("Bearer {token}"); // Signed as the owner, so the API answers as for the human. let (status, body) = http(&url, "GET", "/api/v1/brief", &[("authorization", &bearer)], ""); assert_eq!(status, 200, "{body}"); assert_eq!(serde_json::from_str::(&body).unwrap()["actor"], "human"); let (status, body) = http( &url, "PUT", "/api/v1/briefs", &[("authorization", &bearer), ("content-type", "application/json")], r#"{"text":"From the editor."}"#, ); assert_eq!(status, 200, "{body}"); assert!(text(&run(d, &["brief", "show"])).contains("From the editor.")); // No token, a wrong one, a web page's Origin, or another Host: refused. assert_eq!(http(&url, "GET", "/api/v1/brief", &[], "").0, 401); assert_eq!(http(&url, "GET", "/api/v1/brief", &[("authorization", "Bearer nope")], "").0, 401); assert_eq!(http(&url, "GET", "/api/v1/brief", &[("authorization", &bearer), ("origin", "http://evil.example")], "").0, 403); assert_eq!(http(&url, "GET", "/api/v1/brief", &[("authorization", &bearer), ("host", "evil.example")], "").0, 403); // It ends when its input closes. drop(child.stdin.take()); let status = tokio::task::spawn_blocking(move || child.wait()).await.unwrap().unwrap(); assert!(status.success()); srv.stop(); }