use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
const BIN: &str = env!("CARGO_BIN_EXE_microscope-mem");
struct Server(Child);
impl Drop for Server {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("bind ephemeral port")
.local_addr()
.expect("read local addr")
.port()
}
fn scratch_root() -> PathBuf {
let dir = std::env::temp_dir().join("microscope-rest-bridge-scratch");
for sub in ["output", "tmp"] {
let _ = std::fs::create_dir_all(dir.join(sub));
}
let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures");
if let Ok(entries) = std::fs::read_dir(&fixtures) {
for entry in entries.flatten() {
let name = entry.file_name();
if name.to_string_lossy().ends_with(".bin") || name.to_string_lossy().ends_with(".idx")
{
let _ = std::fs::copy(entry.path(), dir.join("output").join(&name));
}
}
}
dir
}
fn start(port: u16) -> Result<Server, String> {
let missing = std::env::temp_dir().join("microscope-rest-bridge-absent.toml");
let child = Command::new(BIN)
.args(["bridge", "--host", "127.0.0.1", "--port"])
.arg(port.to_string())
.env("MICROSCOPE_CONFIG", missing)
.current_dir(scratch_root())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("spawn: {e}"))?;
let deadline = Instant::now() + Duration::from_secs(20);
while Instant::now() < deadline {
if TcpStream::connect_timeout(
&format!("127.0.0.1:{port}").parse().expect("parse addr"),
Duration::from_millis(200),
)
.is_ok()
{
return Ok(Server(child));
}
std::thread::sleep(Duration::from_millis(100));
}
Err("bridge did not start listening".to_string())
}
fn start_any() -> (Server, u16) {
let mut last = String::new();
for _ in 0..5 {
let port = free_port();
match start(port) {
Ok(s) => return (s, port),
Err(e) => last = e,
}
}
panic!("could not start the bridge: {last}");
}
struct Response {
status: u16,
headers: Vec<(String, String)>,
}
impl Response {
fn header(&self, name: &str) -> Option<&str> {
let want = name.to_ascii_lowercase();
self.headers
.iter()
.find(|(k, _)| k == &want)
.map(|(_, v)| v.as_str())
}
}
fn request(port: u16, method: &str, path: &str, extra: &[(&str, &str)]) -> Response {
let mut stream = TcpStream::connect(format!("127.0.0.1:{port}")).expect("connect to bridge");
stream
.set_read_timeout(Some(Duration::from_secs(20)))
.expect("set read timeout");
let mut head = format!("{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n");
for (k, v) in extra {
head.push_str(&format!("{k}: {v}\r\n"));
}
head.push_str("\r\n");
stream.write_all(head.as_bytes()).expect("write request");
stream.flush().expect("flush request");
let mut raw = Vec::new();
stream.read_to_end(&mut raw).expect("read response");
let text = String::from_utf8_lossy(&raw).into_owned();
let mut lines = text.split("\r\n");
let status_line = lines.next().unwrap_or_default();
let status: u16 = status_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| panic!("unparseable status line: {status_line:?}"));
let mut headers = Vec::new();
for line in lines {
if line.is_empty() {
break; }
if let Some((k, v)) = line.split_once(':') {
headers.push((k.trim().to_ascii_lowercase(), v.trim().to_string()));
}
}
Response { status, headers }
}
#[test]
fn openapi_spec_is_served_over_http() {
let (_server, port) = start_any();
let r = request(port, "GET", "/openapi.json", &[]);
assert_eq!(
r.status, 200,
"the advertised /openapi.json must be reachable"
);
assert!(
r.header("content-type")
.is_some_and(|ct| ct.contains("json")),
"spec must be served as JSON, got {:?}",
r.header("content-type")
);
}
#[test]
fn v1_routes_answer() {
let (_server, port) = start_any();
for path in ["/v1/status", "/v1/session"] {
let r = request(port, "GET", path, &[]);
assert_eq!(r.status, 200, "{path} should answer 200");
}
}
#[test]
fn status_codes_are_correct() {
let (_server, port) = start_any();
let r = request(port, "POST", "/v1/status", &[]);
assert_eq!(r.status, 405, "POST on a GET route must be 405");
let r = request(port, "GET", "/v1/no-such-route", &[]);
assert_eq!(r.status, 404, "an unknown path must be 404");
let r = request(port, "GET", "/v1/recall", &[]);
assert_eq!(r.status, 400, "recall without a query is a bad request");
let r = request(port, "GET", "/mobile/feed", &[]);
assert_eq!(r.status, 404, "/mobile/feed is not a route");
}
#[test]
fn cors_defaults_to_wildcard() {
let (_server, port) = start_any();
let r = request(
port,
"OPTIONS",
"/v1/status",
&[
("Origin", "https://claude.ai"),
("Access-Control-Request-Method", "GET"),
],
);
assert_eq!(r.status, 200, "a CORS preflight must be answered");
assert_eq!(
r.header("access-control-allow-origin"),
Some("*"),
"with no cors_origin configured the default is a wildcard"
);
}
#[test]
fn non_loopback_without_api_key_is_refused() {
let missing = std::env::temp_dir().join("microscope-rest-bridge-absent.toml");
let out = Command::new(BIN)
.args(["bridge", "--host", "0.0.0.0", "--port", "1"])
.env("MICROSCOPE_CONFIG", missing)
.output()
.expect("run bridge on 0.0.0.0");
assert!(
!out.status.success(),
"binding 0.0.0.0 with no api_key must fail, got {:?}",
out.status
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("refusing to start"),
"the refusal should say why, got: {stderr}"
);
}
#[test]
fn status_on_uninitialised_store_is_500() {
let port = free_port();
let empty = std::env::temp_dir().join("microscope-rest-bridge-empty");
let _ = std::fs::remove_dir_all(&empty);
let _ = std::fs::create_dir_all(&empty);
let missing = std::env::temp_dir().join("microscope-rest-bridge-absent.toml");
let child = Command::new(BIN)
.args(["bridge", "--host", "127.0.0.1", "--port"])
.arg(port.to_string())
.env("MICROSCOPE_CONFIG", missing)
.current_dir(&empty)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn bridge against an empty directory");
let server = Server(child);
let deadline = Instant::now() + Duration::from_secs(20);
while Instant::now() < deadline && TcpStream::connect(format!("127.0.0.1:{port}")).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
let r = request(port, "GET", "/v1/status", &[]);
assert_eq!(
r.status, 500,
"an uninitialised store currently answers 500, not 200 or 404"
);
drop(server);
let _ = std::fs::remove_dir_all(&empty);
}