Menu
AkurAI-Build
publicLatest change 41f5eba4919e59f92e84857cf4b4314e159da9fb - tests: exercise native listener end to end by Olafur Bui
//! Listener-level coverage for the native MiniJinja and authenticated vector surface.
#![cfg(unix)]
mod support;
use std::{fs, process::Command};
use support::{ProcessServer, ScratchDir, free_port};
#[test]
fn native_http_pages_assets_probes_auth_and_vectors() {
let mut server = NativeServer::start();
let health = server.process.request("GET", "/api/health");
health.assert_status(200);
health.assert_security_headers();
let payload: serde_json::Value = serde_json::from_slice(&health.body).expect("decode health");
assert_eq!(payload["status"], "ok");
assert_eq!(payload["service"], "bunfork");
assert_eq!(payload["version"], env!("CARGO_PKG_VERSION"));
assert!(payload["uptime_seconds"].is_u64());
let ready = server.process.request("GET", "/api/ready");
ready.assert_status(200);
assert_eq!(ready.body, br#"{"message":"ok"}"#);
let page = server.process.request("GET", "/?tag=one&tag=two");
page.assert_status(200);
page.assert_body_contains("native /");
page.assert_body_contains("tenant=integration");
page.assert_body_contains("model=fixture");
page.assert_body_contains("tag=one tag=two");
page.assert_security_headers();
let dynamic = server.process.request("GET", "/notes/hello-rust");
dynamic.assert_status(200);
dynamic.assert_body_contains("slug=hello-rust");
dynamic.assert_body_contains("path=/notes/hello-rust");
let head = server.process.request("HEAD", "/notes/hello-rust");
head.assert_status(200);
assert!(head.body.is_empty(), "native HEAD returned a body");
let method = server.process.request("POST", "/notes/hello-rust");
method.assert_status(405);
method.assert_header_contains("allow", "GET");
method.assert_header_contains("allow", "HEAD");
let missing = server.process.request("GET", "/missing");
missing.assert_status(404);
missing.assert_body_contains("native missing /missing");
let asset = server.process.request("GET", "/assets/app.css");
asset.assert_status(200);
asset.assert_header_contains("content-type", "text/css");
asset.assert_header_contains("etag", "W/");
asset.assert_body_contains("rgb(10 20 30)");
let upsert = br#"{"id":"note-1","content":"hello vectors","embedding":[1.0,0.0]}"#;
let content_type = [("Content-Type", "application/json")];
let unauthorized =
server
.process
.request_with_body("PUT", "/api/vectors/note-1", &content_type, upsert);
unauthorized.assert_status(401);
let authorization = format!("Bearer {}", server.token);
let rejected_origin = [
("Authorization", authorization.as_str()),
("Content-Type", "application/json"),
("Origin", "https://attacker.invalid"),
];
let rejected =
server
.process
.request_with_body("PUT", "/api/vectors/note-1", &rejected_origin, upsert);
rejected.assert_status(400);
rejected.assert_body_contains("cross-origin mutation rejected");
let accepted_headers = [
("Authorization", authorization.as_str()),
("Content-Type", "application/json"),
("Origin", server.origin.as_str()),
];
let stored =
server
.process
.request_with_body("PUT", "/api/vectors/note-1", &accepted_headers, upsert);
stored.assert_status(201);
assert_eq!(stored.body, br#"{"message":"stored"}"#);
let search_headers = [
("Authorization", authorization.as_str()),
("Content-Type", "application/json"),
];
let search = server.process.request_with_body(
"POST",
"/api/vectors/search",
&search_headers,
br#"{"embedding":[1.0,0.0],"limit":1}"#,
);
search.assert_status(200);
let payload: serde_json::Value = serde_json::from_slice(&search.body).expect("decode search");
assert_eq!(payload["matches"][0]["id"], "note-1");
assert_eq!(payload["matches"][0]["content"], "hello vectors");
let delete_headers = [
("Authorization", authorization.as_str()),
("Origin", server.origin.as_str()),
];
let deleted =
server
.process
.request_with_body("DELETE", "/api/vectors/note-1", &delete_headers, &[]);
deleted.assert_status(204);
assert!(deleted.body.is_empty());
server.process.shutdown_with_sigterm();
}
struct NativeServer {
process: ProcessServer,
_scratch: ScratchDir,
token: String,
origin: String,
}
impl NativeServer {
fn start() -> Self {
let scratch = ScratchDir::new("native-http");
let pages = scratch.path().join("pages");
let public = scratch.path().join("public");
fs::create_dir_all(pages.join("notes")).expect("create native page fixtures");
fs::create_dir(&public).expect("create native public fixture");
fs::write(
pages.join("index.html"),
"native {{ pathname }} tenant={{ tenant }} model={{ model }} {% for value in query.tag %}tag={{ value }} {% endfor %}",
)
.expect("write native index fixture");
fs::write(
pages.join("notes/[slug].html"),
"slug={{ params.slug }} path={{ pathname }}",
)
.expect("write native dynamic fixture");
fs::write(pages.join("_404.html"), "native missing {{ pathname }}")
.expect("write native 404 fixture");
fs::write(public.join("app.css"), "body { color: rgb(10 20 30); }")
.expect("write native asset fixture");
let database = scratch.path().join("data/bunfork.db");
let key_file = scratch.path().join("database.key");
let token_file = scratch.path().join("api.token");
run_success(
Command::new(env!("CARGO_BIN_EXE_bunfork"))
.arg("keygen")
.arg("--out")
.arg(&key_file),
"generate database key",
);
run_success(
Command::new(env!("CARGO_BIN_EXE_bunfork"))
.arg("keygen")
.arg("--out")
.arg(&token_file),
"generate API token",
);
run_success(
Command::new(env!("CARGO_BIN_EXE_bunfork"))
.arg("--database")
.arg(&database)
.arg("--key-file")
.arg(&key_file)
.arg("migrate"),
"migrate native database",
);
let token = fs::read_to_string(&token_file)
.expect("read generated API token")
.trim()
.to_owned();
let address = format!("127.0.0.1:{}", free_port());
let origin = format!("http://{address}");
let mut command = Command::new(env!("CARGO_BIN_EXE_bunfork"));
command
.arg("--database")
.arg(&database)
.arg("--key-file")
.arg(&key_file)
.args(["--tenant", "integration", "--model", "fixture", "serve"])
.arg("--address")
.arg(&address)
.arg("--pages")
.arg(&pages)
.arg("--public")
.arg(&public)
.arg("--token-file")
.arg(&token_file)
.arg("--public-origin")
.arg(&origin)
.current_dir(scratch.path());
for variable in [
"BUNFORK_DB",
"BUNFORK_DB_KEY",
"BUNFORK_KEY_FILE",
"BUNFORK_API_TOKEN",
"BUNFORK_TOKEN_FILE",
"BUNFORK_TENANT",
"BUNFORK_MODEL",
"BUNFORK_PUBLIC_ORIGIN",
] {
command.env_remove(variable);
}
Self {
process: ProcessServer::start(command, address),
_scratch: scratch,
token,
origin,
}
}
}
fn run_success(command: &mut Command, label: &str) {
let output = command
.output()
.unwrap_or_else(|error| panic!("{label}: {error}"));
assert!(
output.status.success(),
"{label} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}