Menu
AkurAI-Build
publicLatest change ecb926d91773abf60abe185bc4ab032d315fd92a - Add Framework-style AkurAI Build documentation by Ólafur Búi Ólafsson
use std::{fs, path::Path, process::Command};
use anyhow::{Context, Result};
mod support;
use support::ProcessServer;
/// Verify that the server serves static pages, assets, and security headers
/// on public-facing routes.
#[test]
fn static_pages_and_assets() -> Result<()> {
let work = tempfile::tempdir()?;
let data = work.path().join("data");
let key_file = work.path().join("key.bin");
let token_file = work.path().join("token.txt");
let webhook_secret_file = work.path().join("webhook.txt");
run_cli(&data, &key_file, &["keygen", "--out", path(&key_file)?])?;
run_cli(&data, &key_file, &["migrate"])?;
write_secret(
&token_file,
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
)?;
write_secret(
&webhook_secret_file,
"aaaabbbbccccddddeeeeffff00001111aaaabbbbccccddddeeeeffff00001111",
)?;
let mut command = Command::new(env!("CARGO_BIN_EXE_akurai"));
command.args([
"--data",
path(&data)?,
"--key-file",
path(&key_file)?,
"serve",
"--address",
"127.0.0.1:0",
"--token-file",
path(&token_file)?,
"--webhook-secret-file",
path(&webhook_secret_file)?,
"--public-origin",
"http://localhost",
"--allow-native",
"--worker-count",
"1",
]);
command.env_remove("AKURAI_DATA");
command.env_remove("AKURAI_KEY_FILE");
command.env_remove("AKURAI_ADDRESS");
command.env_remove("AKURAI_TOKEN_FILE");
command.env_remove("AKURAI_WEBHOOK_SECRET_FILE");
command.env_remove("AKURAI_PUBLIC_ORIGIN");
command.env_remove("AKURAI_ALLOW_NATIVE");
command.env_remove("AKURAI_WORKERS");
// Strip ambient AkurAI Auth (OIDC) configuration so the login contract is
// deterministic regardless of the shell that invoked the test (e.g. the
// deploy gate sources a production env file that would otherwise leak).
command.env_remove("AKURAI_BUILD_IDP_ISSUER");
command.env_remove("AKURAI_BUILD_IDP_INTERNAL_URL");
command.env_remove("AKURAI_BUILD_IDP_CLIENT_ID");
command.env_remove("AKURAI_BUILD_IDP_CLIENT_SECRET");
command.env_remove("AKURAI_BUILD_IDP_REDIRECT");
command.env_remove("AKURAI_BUILD_ADMIN_EMAILS");
let mut server = ProcessServer::start_dynamic(command);
// Health check.
let health = server.request("GET", "/api/health");
health.assert_status(200);
// State with auth.
let state = server.request_with_headers(
"GET",
"/api/state",
&[(
"Authorization",
"Bearer 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
)],
);
state.assert_status(200);
// Landing page with retry for startup race.
let landing = retry_landing(&server)?;
landing.assert_body_contains("AkurAI Build");
landing.assert_security_headers();
// Public documentation.
let docs = server.request("GET", "/docs");
docs.assert_status(200);
docs.assert_body_contains("AkurAI Build documentation");
docs.assert_body_contains("akurai_run_queue");
docs.assert_security_headers();
// Login is gated on AkurAI Auth (OIDC) configuration. With no client
// configured in this hermetic test, /login must fail closed with the
// internal "not configured" error rather than render a broken page.
let login = server.request("GET", "/login");
login.assert_status(500);
login.assert_body_contains("internal");
// CSS asset.
let css = server.request("GET", "/assets/app.css");
css.assert_status(200);
css.assert_header_contains("content-type", "text/css");
// JS asset.
let js = server.request("GET", "/assets/app.js");
js.assert_status(200);
js.assert_header_contains("content-type", "text/javascript");
assert!(!js.body.is_empty(), "JS asset should not be empty");
// Security headers on landing page.
let landing2 = server.request("GET", "/");
landing2.assert_header_contains("x-content-type-options", "nosniff");
landing2.assert_header_contains("x-frame-options", "deny");
server.shutdown_with_sigterm();
Ok(())
}
fn retry_landing(server: &ProcessServer) -> Result<support::HttpResponse> {
let mut response = server.request("GET", "/");
if response.status == 200 {
return Ok(response);
}
// Single retry after a brief pause for async runtime to settle.
std::thread::sleep(std::time::Duration::from_millis(500));
response = server.request("GET", "/");
anyhow::ensure!(
response.status == 200,
"landing page returned {} after retry",
response.status
);
Ok(response)
}
fn write_secret(path: &Path, content: &str) -> Result<()> {
fs::write(path, content)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
fn run_cli(data: &Path, key: &Path, arguments: &[&str]) -> Result<()> {
let output = Command::new(env!("CARGO_BIN_EXE_akurai"))
.args(["--data", path(data)?, "--key-file", path(key)?])
.args(arguments)
.output()?;
anyhow::ensure!(
output.status.success(),
"CLI {} failed: {}",
arguments.join(" "),
String::from_utf8_lossy(&output.stderr)
);
Ok(())
}
fn path(path: &Path) -> Result<&str> {
path.to_str().context("test path is not UTF-8")
}