AkurAI Build
Menu

AkurAI-Build

public

Latest 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)
    );
}