use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
}
impl Fixture {
fn new(label: &str, server: &str) -> Self {
let ordinal = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!(
"noxid-wo22-openapi-{label}-{}-{ordinal}",
std::process::id()
));
fs::create_dir_all(root.join("server/api/items/[id]")).unwrap();
fs::write(
root.join("Noxid.toml"),
format!("[app]\ntitle = \"Inventory API\"\n\n[server]\n{server}"),
)
.unwrap();
fs::write(root.join("package.json"), "{\"type\":\"module\"}\n").unwrap();
fs::write(
root.join("server/api/items/[id]/update.post.nox"),
r#"
endpoint UpdateItem {
description: "Updates an inventory item."
params { id: Int }
query { notify: Optional<Boolean> }
body { count: Int }
result: Int
timeout: 3s
limit: 2 per minute per ip
}
"#,
)
.unwrap();
fs::write(
root.join("server/host.js"),
"export const endpoints = { 'endpoint:UpdateItem@1': async ({ count }) => count };\n",
)
.unwrap();
Self { root }
}
fn build(&self) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(["build", ".", "--out-dir", "dist"])
.current_dir(&self.root)
.output()
.unwrap()
}
fn node(&self, source: &str) -> Output {
let path = self.root.join("dist/openapi-check.mjs");
fs::write(&path, source).unwrap();
Command::new("node")
.arg(path.file_name().unwrap())
.current_dir(self.root.join("dist"))
.output()
.unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn assert_success(output: &Output, context: &str) {
assert!(
output.status.success(),
"{context}:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn build_always_emits_openapi_but_default_off_door_is_404() {
let fixture = Fixture::new("off", "");
let build = fixture.build();
assert_success(&build, "build default-off OpenAPI fixture");
let document = fs::read_to_string(fixture.root.join("dist/api.openapi.json")).unwrap();
assert!(document.contains("\"openapi\": \"3.1.0\""), "{document}");
assert!(
document.contains("\"/api/items/{id}/update\""),
"{document}"
);
assert!(document.contains("\"post\""), "{document}");
assert!(
document.contains("Updates an inventory item."),
"{document}"
);
assert!(
document.contains("UpdateItem(params { id: Int }, query { notify: Optional<Boolean> }, body { count: Int }) -> Int"),
"{document}"
);
let manifest =
fs::read_to_string(fixture.root.join("dist/server/security.manifest.json")).unwrap();
assert!(
manifest.contains("\"surfaces\":{\"apiDocs\":false,\"mcp\":false}"),
"{manifest}"
);
let node = fixture.node(
r#"
import { fetch } from "./server/handler.js";
const response = await fetch(new Request("http://noxid.test/_noxid/openapi.json"));
if (response.status !== 404) throw new Error(`default-off docs returned ${response.status}`);
"#,
);
assert_success(&node, "default-off OpenAPI route");
}
#[test]
fn api_docs_serves_exact_artifact_and_error_payload_matches_published_schema() {
let fixture = Fixture::new("on", "api_docs = true\nmcp = false\n");
let build = fixture.build();
assert_success(&build, "build enabled OpenAPI fixture");
let manifest =
fs::read_to_string(fixture.root.join("dist/server/security.manifest.json")).unwrap();
assert!(
manifest.contains("\"surfaces\":{\"apiDocs\":true,\"mcp\":false}"),
"{manifest}"
);
let node = fixture.node(
r#"
import fs from "node:fs";
import { fetch } from "./server/handler.js";
const published = fs.readFileSync("api.openapi.json", "utf8");
const docs = await fetch(new Request("http://noxid.test/_noxid/openapi.json"));
if (docs.status !== 200 || docs.headers.get("content-type") !== "application/json; charset=utf-8") throw new Error(`docs response ${docs.status} ${docs.headers.get("content-type")}`);
if (await docs.text() !== published) throw new Error("served OpenAPI differs from build artifact");
const method = await fetch(new Request("http://noxid.test/_noxid/openapi.json", { method: "POST" }));
if (method.status !== 405) throw new Error(`non-GET OpenAPI door returned ${method.status}`);
const refusal = await fetch(new Request("http://noxid.test/api/items/4/update", {
method: "POST",
headers: { "content-type": "application/json", "x-real-ip": "127.0.0.1" },
body: JSON.stringify({ count: "wrong" }),
}));
if (refusal.status !== 422) throw new Error(`typed refusal returned ${refusal.status}`);
const value = await refusal.json();
const document = JSON.parse(published);
const schema = document.components.schemas.NoxidErrorResponse;
function matches(schema, value) {
if (schema.$ref) return matches(document.components.schemas[schema.$ref.split("/").at(-1)], value);
if (Object.hasOwn(schema, "const") && value !== schema.const) return false;
if (Array.isArray(schema.type)) {
if (!schema.type.some((type) => matches({ type }, value))) return false;
} else if (schema.type === "object" && (value === null || typeof value !== "object" || Array.isArray(value))) return false;
else if (schema.type === "string" && typeof value !== "string") return false;
else if (schema.type === "null" && value !== null) return false;
if (schema.required?.some((key) => !Object.hasOwn(value, key))) return false;
if (schema.properties) {
for (const [key, property] of Object.entries(schema.properties)) if (Object.hasOwn(value, key) && !matches(property, value[key])) return false;
if (schema.additionalProperties === false && Object.keys(value).some((key) => !Object.hasOwn(schema.properties, key))) return false;
}
return true;
}
if (!matches(schema, value)) throw new Error(`actual refusal violates NoxidErrorResponse: ${JSON.stringify(value)}`);
"#,
);
assert_success(&node, "served OpenAPI and response schema parity");
}
#[test]
fn agent_surface_keys_require_real_toml_booleans() {
let fixture = Fixture::new("invalid-bool", "api_docs = \"true\"\n");
let build = fixture.build();
assert!(!build.status.success());
let stderr = String::from_utf8_lossy(&build.stderr);
assert!(
stderr.contains("error[SERVER_AGENT_SURFACE_BOOLEAN_REQUIRED]")
&& stderr.contains("must be the boolean `true` or `false`"),
"{stderr}"
);
assert!(!fixture.root.join("dist/api.openapi.json").exists());
}