use std::fs;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::process::{Child, Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
}
impl Fixture {
fn new(label: &str) -> Self {
let ordinal = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"noxid-stage0-{label}-{}-{ordinal}",
std::process::id()
));
fs::create_dir_all(&root).expect("create fixture root");
Self { root }
}
fn write(&self, relative: &str, contents: &str) {
let path = self.root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, contents).expect("write fixture file");
}
fn noxid(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(args)
.current_dir(&self.root)
.env_remove("DATABASE_URL")
.env_remove("VERCEL")
.env_remove("NETLIFY")
.env_remove("RAILWAY_ENVIRONMENT")
.output()
.expect("run noxid")
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn output_text(output: &Output) -> String {
format!(
"status: {}\nstdout:\n{}\nstderr:\n{}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn write_endpoint_project(fixture: &Fixture, host_source: &str) {
fixture.write("Noxid.toml", "[app]\ntitle = \"Stage 0\"\n");
fixture.write("package.json", "{\"type\":\"module\"}\n");
fixture.write(
"server/api/probe.get.nox",
"endpoint Probe { result: String }\n",
);
fixture.write("server/host.ts", host_source);
}
#[test]
fn build_refuses_a_developer_module_that_exports_the_principal_authority_installer() {
let fixture = Fixture::new("installer-shadowed");
write_endpoint_project(
&fixture,
"import { install } from './utils/authority.js';\nvoid install;\nexport const endpoints = Object.freeze({ 'endpoint:Probe@1': async () => 'ok' });\n",
);
fixture.write(
"server/utils/authority.ts",
"export function __installNoxidPrincipalAuthority() { return { bind() {} }; }\nexport const install = __installNoxidPrincipalAuthority;\n",
);
let output = fixture.noxid(&["build", ".", "--out-dir", "dist"]);
let text = output_text(&output);
assert!(!output.status.success(), "{text}");
assert!(
text.contains("error[PRINCIPAL_AUTHORITY_INSTALLER_SHADOWED]"),
"{text}"
);
assert!(text.contains("server__utils__authority.js"), "{text}");
assert!(!fixture.root.join("dist/server/handler.js").exists());
}
#[test]
fn build_without_any_installer_wires_no_principal_authority_import() {
let fixture = Fixture::new("installer-absent");
write_endpoint_project(
&fixture,
"export const endpoints = Object.freeze({ 'endpoint:Probe@1': async () => 'ok' });\n",
);
let output = fixture.noxid(&["build", ".", "--out-dir", "dist"]);
assert!(output.status.success(), "{}", output_text(&output));
let handler =
fs::read_to_string(fixture.root.join("dist/server/handler.js")).expect("read handler");
assert!(
!handler.contains("import { __installNoxidPrincipalAuthority }"),
"{handler}"
);
assert!(
handler.contains("typeof __installNoxidPrincipalAuthority === \"function\""),
"runtime guard for an absent authority must remain: {handler}"
);
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.expect("bind ephemeral port")
.local_addr()
.expect("read ephemeral port")
.port()
}
struct NodeServer {
child: Child,
port: u16,
}
impl NodeServer {
#[allow(clippy::zombie_processes)]
fn start(directory: &std::path::Path) -> Self {
let port = free_port();
let child = Command::new("node")
.arg("server.mjs")
.current_dir(directory)
.env("PORT", port.to_string())
.env_remove("DATABASE_URL")
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn emitted Node server");
let deadline = Instant::now() + Duration::from_secs(30);
while Instant::now() < deadline {
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return Self { child, port };
}
std::thread::sleep(Duration::from_millis(100));
}
let mut child = child;
let _ = child.kill();
let _ = child.wait();
panic!("emitted Node server did not listen on {port} within 30 seconds");
}
fn status(&self, path: &str) -> u16 {
let mut stream =
TcpStream::connect(("127.0.0.1", self.port)).expect("connect to emitted server");
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.expect("set read timeout");
write!(
stream,
"GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
)
.expect("write request");
let mut response = String::new();
let _ = stream.read_to_string(&mut response);
response
.split_whitespace()
.nth(1)
.and_then(|code| code.parse().ok())
.unwrap_or_else(|| panic!("no status line for {path}: {response}"))
}
}
impl Drop for NodeServer {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn node_adapter_serves_assets_but_never_server_source_or_build_metadata() {
let fixture = Fixture::new("node-static-root");
fixture.write("Noxid.toml", "[app]\ntitle = \"Static root\"\n");
fixture.write(
"src/routes/+page.nox",
"component Home { route { title: \"Home\" } view { <main>Home</main> } }\n",
);
let adapted = fixture.noxid(&["adapt", ".", "--adapter", "node", "--out-dir", "dist"]);
assert!(adapted.status.success(), "{}", output_text(&adapted));
let dist = fixture.root.join("dist");
assert!(dist.join("server.mjs").is_file());
assert!(dist.join("app.js").is_file(), "build did not emit app.js");
assert!(
dist.join("app.manifest.json").is_file(),
"build did not emit app.manifest.json"
);
fs::create_dir_all(dist.join("server")).expect("create server directory");
fs::write(dist.join("server/handler.js"), "export const secret = 1;\n")
.expect("write planted server module");
fs::write(dist.join("api-contract.json"), "{}\n").expect("write planted contract");
let server = NodeServer::start(&dist);
assert_eq!(server.status("/app.js"), 200, "app.js must stay public");
assert_eq!(server.status("/"), 200, "index must stay public");
assert_eq!(server.status("/server/handler.js"), 404);
assert_eq!(server.status("/server/"), 404);
assert_eq!(server.status("/server"), 404);
assert_eq!(server.status("/app.manifest.json"), 404);
assert_eq!(server.status("/app.devtools.json"), 404);
assert_eq!(server.status("/api-contract.json"), 404);
assert!(
matches!(server.status("/..%2Fserver%2Fhandler.js"), 400 | 404),
"traversal is refused before any file is read"
);
drop(server);
let deno = fixture.noxid(&["adapt", ".", "--adapter", "deno", "--out-dir", "dist-deno"]);
assert!(deno.status.success(), "{}", output_text(&deno));
let deno_server = fs::read_to_string(fixture.root.join("dist-deno/server.ts"))
.expect("Deno adapter emitted server.ts");
assert!(deno_server.contains("Deno.serve"), "{deno_server}");
fs::copy(
fixture.root.join("dist-deno/server.ts"),
fixture.root.join("dist-deno/server.mjs"),
)
.expect("stage Deno entry for the Node-hosted stub");
fixture.write("dist-deno/server/handler.js", "export const secret = 1;\n");
fixture.write(
"deno-static.mjs",
r#"import { readFile, realpath, stat } from "node:fs/promises";
const reads = [];
globalThis.Deno = {
serve: (_options, handler) => { globalThis.__handler = handler; return { shutdown: async () => {} }; },
env: { get: () => undefined, toObject: () => ({}) },
readFile: async (path) => { reads.push(String(path)); return new Uint8Array(await readFile(path)); },
realPath: (path) => realpath(path),
stat: async (path) => { const value = await stat(path); return { isDirectory: value.isDirectory(), isFile: value.isFile(), size: value.size }; },
addSignalListener: () => {},
};
await import("./dist-deno/server.mjs");
const response = await globalThis.__handler(new Request("http://noxid.test/server/handler.js"));
console.log(JSON.stringify({ status: response.status, reads }));
"#,
);
let driven = Command::new("node")
.arg("deno-static.mjs")
.current_dir(&fixture.root)
.output()
.expect("drive emitted Deno static handler");
let report = String::from_utf8_lossy(&driven.stdout);
assert!(driven.status.success(), "{}", output_text(&driven));
assert!(
report.contains("\"status\":404") && report.contains("\"reads\":[]"),
"the Deno static handler reached or served planted server source: {report}"
);
assert!(
deno_server.contains("const isNoxidReservedDeploymentPath = (value)")
&& deno_server
.contains("relative.includes(\"%\") || isNoxidReservedDeploymentPath(relative)")
&& deno_server.contains("\"server\""),
"{deno_server}"
);
}
fn copy_tree(from: &std::path::Path, to: &std::path::Path) {
fs::create_dir_all(to).expect("create copy destination");
for entry in fs::read_dir(from).expect("read deployment root") {
let entry = entry.expect("deployment entry");
let target = to.join(entry.file_name());
if entry.file_type().expect("entry type").is_dir() {
copy_tree(&entry.path(), &target);
} else {
fs::copy(entry.path(), &target).expect("copy deployment file");
}
}
}
#[test]
fn the_post_realpath_reserved_recheck_is_what_closes_case_folded_spellings() {
let fixture = Fixture::new("realpath-recheck");
fixture.write("Noxid.toml", "[app]\ntitle = \"Realpath re-check\"\n");
fixture.write(
"src/routes/+page.nox",
"component Home { route { title: \"Home\" } view { <main>Home</main> } }\n",
);
let adapted = fixture.noxid(&["adapt", ".", "--adapter", "node", "--out-dir", "dist"]);
assert!(adapted.status.success(), "{}", output_text(&adapted));
let dist = fixture.root.join("dist");
fs::create_dir_all(dist.join("server")).expect("create server directory");
fs::write(
dist.join("server/handler.js"),
"export const secret = \"REALPATH-RECHECK-LEAK\";\n",
)
.expect("plant server source");
fs::write(dist.join("api-contract.json"), "{\"planted\":true}\n").expect("plant contract");
let emitted = fs::read_to_string(dist.join("server.mjs")).expect("read emitted server");
assert!(
emitted.contains("import fs from \"node:fs/promises\";"),
"the emitted handler no longer imports the native realpath:\n{emitted}"
);
let realpath = emitted
.find("file = await fs.realpath(file);")
.expect("emitted handler resolves the served file through realpath");
let recheck = emitted
.find("if (isReservedStaticPath(served) || isNoxidReservedDeploymentPath(served))")
.expect("emitted handler re-checks the canonical served path");
assert!(
realpath < recheck,
"the reserved re-check no longer runs on the realpath-canonical path"
);
let server = NodeServer::start(&dist);
assert_eq!(server.status("/"), 200, "the shell must stay public");
for path in [
"/%C5%BFerver/handler.js",
"/api-contract.j%C5%BFon",
"/SERVER/handler.js",
"/API-CONTRACT.JSON",
] {
assert_eq!(
server.status(path),
404,
"`{path}` reached a compiler-owned artifact"
);
}
drop(server);
let folds = fs::canonicalize(dist.join("\u{17f}erver/handler.js"))
.is_ok_and(|resolved| resolved.ends_with("server/handler.js"));
if !folds {
eprintln!(
"the post-realpath re-check mutation: SKIP (this filesystem does not fold U+017F)"
);
return;
}
let mutated = fixture.root.join("dist-mutated");
copy_tree(&dist, &mutated);
let entry = mutated.join("server.mjs");
let source = fs::read_to_string(&entry).expect("read copied server");
let disabled = source.replace(
"if (isReservedStaticPath(served) || isNoxidReservedDeploymentPath(served)) {",
"if (false) {",
);
assert_ne!(source, disabled, "the re-check was not found to disable");
fs::write(&entry, &disabled).expect("write the mutated server");
let leaking = NodeServer::start(&mutated);
assert_eq!(
leaking.status("/%C5%BFerver/handler.js"),
200,
"removing the post-realpath re-check did not open the long-s bypass, so \
this test is not measuring the check it claims to pin"
);
assert_eq!(
leaking.status("/api-contract.j%C5%BFon"),
200,
"removing the post-realpath re-check did not open the long-s bypass for \
build metadata"
);
assert_eq!(leaking.status("/server/handler.js"), 404);
}