OXYGENHUB
# 2d2dcb7a2778 spectra / tests / bridge.rs
4.1 KiB · Rust · last changed spectra status, probe and bridge: what the VS Code extension drives (#31) 1 day ago source blame history raw
1
//! What the VS Code extension drives (spec §18.1): `spectra status` and
2
//! `spectra probe` say what is set up and what a server is, and `spectra
3
//! bridge` signs for the extension, refusing anyone without its token and
4
//! any web page.
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
    // Signed as the owner, so the API answers as for the human.
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
    // No token, a wrong one, a web page's Origin, or another Host: refused.
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
    // It ends when its input closes.
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
}