| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
mod common; |
| 7 |
|
| 8 |
use common::{add_agent, hermetic, run, setup, text, Serving}; |
| 9 |
use serde_json::Value; |
| 10 |
use std::io::{BufRead, BufReader, Read, Write}; |
| 11 |
use std::process::Stdio; |
| 12 |
|
| 13 |
fn http(url: &str, method: &str, path: &str, headers: &[(&str, &str)], body: &str) -> (u16, String) { |
| 14 |
let hp = url.strip_prefix("http://").unwrap(); |
| 15 |
let mut s = std::net::TcpStream::connect(hp).unwrap(); |
| 16 |
s.set_read_timeout(Some(std::time::Duration::from_secs(10))).unwrap(); |
| 17 |
let mut req = format!("{method} {path} HTTP/1.1\r\nconnection: close\r\ncontent-length: {}\r\n", body.len()); |
| 18 |
if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("host")) { |
| 19 |
req.push_str(&format!("host: {hp}\r\n")); |
| 20 |
} |
| 21 |
for (k, v) in headers { |
| 22 |
req.push_str(&format!("{k}: {v}\r\n")); |
| 23 |
} |
| 24 |
req.push_str("\r\n"); |
| 25 |
req.push_str(body); |
| 26 |
s.write_all(req.as_bytes()).unwrap(); |
| 27 |
let mut out = String::new(); |
| 28 |
let _ = s.read_to_string(&mut out); |
| 29 |
let status = out.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0); |
| 30 |
(status, out.split_once("\r\n\r\n").map(|x| x.1.to_string()).unwrap_or_default()) |
| 31 |
} |
| 32 |
|
| 33 |
#[test] |
| 34 |
fn status_says_what_is_set_up() { |
| 35 |
let dir = tempfile::tempdir().unwrap(); |
| 36 |
let d = dir.path(); |
| 37 |
let v: Value = serde_json::from_slice(&run(d, &["status", "--json"]).stdout).unwrap(); |
| 38 |
assert!(v["identity"].is_null() && v["server"].is_null() && v["local"]["initialised"] == false, "{v}"); |
| 39 |
let made: Value = serde_json::from_slice(&run(d, &["identity", "create", "--name", "Andy McCall", "--json"]).stdout).unwrap(); |
| 40 |
assert_eq!(made["key"].as_str().unwrap().len(), 64); |
| 41 |
assert!(run(d, &["init"]).status.success()); |
| 42 |
let v: Value = serde_json::from_slice(&run(d, &["status", "--json"]).stdout).unwrap(); |
| 43 |
assert_eq!(v["identity"]["name"], "Andy McCall", "init made a successor (it delegates to the instance), so the key moved on"); |
| 44 |
assert_ne!(v["identity"]["key"], made["key"]); |
| 45 |
assert_eq!(v["local"]["initialised"], true); |
| 46 |
assert_eq!(v["reachable"], false, "not serving"); |
| 47 |
} |
| 48 |
|
| 49 |
#[tokio::test] |
| 50 |
async fn the_bridge_signs_for_its_holder_only() { |
| 51 |
let dir = setup(); |
| 52 |
let d = dir.path(); |
| 53 |
add_agent(d, "claude-oxygen", &[]); |
| 54 |
let srv = Serving::start(d); |
| 55 |
let mut child = hermetic(d).args(["bridge"]).stdin(Stdio::piped()).stdout(Stdio::piped()).spawn().unwrap(); |
| 56 |
let mut line = String::new(); |
| 57 |
BufReader::new(child.stdout.take().unwrap()).read_line(&mut line).unwrap(); |
| 58 |
let hello: Value = serde_json::from_str(&line).unwrap(); |
| 59 |
let (url, token) = (hello["url"].as_str().unwrap().to_string(), hello["token"].as_str().unwrap().to_string()); |
| 60 |
let bearer = format!("Bearer {token}"); |
| 61 |
|
| 62 |
|
| 63 |
let (status, body) = http(&url, "GET", "/api/v1/brief", &[("authorization", &bearer)], ""); |
| 64 |
assert_eq!(status, 200, "{body}"); |
| 65 |
assert_eq!(serde_json::from_str::<Value>(&body).unwrap()["actor"], "human"); |
| 66 |
let (status, body) = http( |
| 67 |
&url, |
| 68 |
"PUT", |
| 69 |
"/api/v1/briefs", |
| 70 |
&[("authorization", &bearer), ("content-type", "application/json")], |
| 71 |
r#"{"text":"From the editor."}"#, |
| 72 |
); |
| 73 |
assert_eq!(status, 200, "{body}"); |
| 74 |
assert!(text(&run(d, &["brief", "show"])).contains("From the editor.")); |
| 75 |
|
| 76 |
|
| 77 |
assert_eq!(http(&url, "GET", "/api/v1/brief", &[], "").0, 401); |
| 78 |
assert_eq!(http(&url, "GET", "/api/v1/brief", &[("authorization", "Bearer nope")], "").0, 401); |
| 79 |
assert_eq!(http(&url, "GET", "/api/v1/brief", &[("authorization", &bearer), ("origin", "http://evil.example")], "").0, 403); |
| 80 |
assert_eq!(http(&url, "GET", "/api/v1/brief", &[("authorization", &bearer), ("host", "evil.example")], "").0, 403); |
| 81 |
|
| 82 |
|
| 83 |
drop(child.stdin.take()); |
| 84 |
let status = tokio::task::spawn_blocking(move || child.wait()).await.unwrap().unwrap(); |
| 85 |
assert!(status.success()); |
| 86 |
srv.stop(); |
| 87 |
} |