AkurAI Build
Menu

AkurAI-Build

public

Latest change 0927fa34c62bad0be6af111ef8454b5e43d92233 - test: cover repository tree sidebar data by Ólafur Búi Ólafsson

use std::{fs, path::Path, process::Command};

use anyhow::{Context, Result, ensure};
use serde_json::json;

mod support;
use support::ProcessServer;

/// Spin up the AkurAI Build HTTP server on an ephemeral port and verify health,
/// security headers, and API contract.
#[test]
fn server_health_and_api_contract() -> 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");

    let repository = work.path().join("repository");
    fs::create_dir(&repository)?;
    git(&repository, &["init", "-b", "main"])?;
    git(&repository, &["config", "user.name", "AkurAI Test"])?;
    git(
        &repository,
        &["config", "user.email", "test@example.invalid"],
    )?;
    fs::write(repository.join("README.md"), "# Fixture repository\n")?;
    fs::write(repository.join("lib.rs"), "pub fn fixture() {}\n")?;
    git(&repository, &["add", "."])?;
    git(&repository, &["commit", "-m", "Add fixture source"])?;

    // Generate key.
    run_cli(&data, &key_file, &["keygen", "--out", path(&key_file)?])?;
    // Migrate database.
    run_cli(&data, &key_file, &["migrate"])?;
    // Write secret files with correct permissions.
    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",
    ]);
    // Remove env vars that might override test config.
    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");

    let mut server = ProcessServer::start_dynamic(command);

    // Health endpoint (unauthenticated).
    let health = server.request("GET", "/api/health");
    health.assert_status(200);
    health.assert_body_contains("\"ok\"");

    // State endpoint requires auth.
    let no_auth = server.request("GET", "/api/state");
    no_auth.assert_status(401);

    // State endpoint with auth.
    let state = server.request_with_headers(
        "GET",
        "/api/state",
        &[(
            "Authorization",
            "Bearer 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
        )],
    );
    state.assert_status(200);
    state.assert_body_contains("\"ok\"");
    state.assert_body_contains("\"repositories\"");

    // Workers endpoint with auth.
    let workers = server.request_with_headers(
        "GET",
        "/api/workers",
        &[(
            "Authorization",
            "Bearer 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
        )],
    );
    workers.assert_status(200);
    workers.assert_body_contains("\"ok\"");

    // Landing page.
    let landing = server.request("GET", "/");
    landing.assert_status(200);
    landing.assert_body_contains("AkurAI Build");

    // Security headers on landing page.
    landing.assert_security_headers();

    // CSS asset served with correct content type.
    let css = server.request("GET", "/assets/app.css");
    css.assert_status(200);
    css.assert_header_contains("content-type", "text/css");

    // JS asset served.
    let js = server.request("GET", "/assets/app.js");
    js.assert_status(200);
    js.assert_header_contains("content-type", "text/javascript");

    // 404 page.
    let not_found = server.request("GET", "/nonexistent/path");
    assert!(
        not_found.status == 404 || not_found.status == 200,
        "should return 404 or landing page for unknown paths, got {}",
        not_found.status
    );

    let add_body = serde_json::to_vec(&json!({
        "name": "api-test",
        "url": path(&repository)?,
        "branch": "main"
    }))?;
    let add_repo = server.request_with_body(
        "POST",
        "/api/repos",
        &[
            (
                "Authorization",
                "Bearer 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
            ),
            ("Content-Type", "application/json"),
        ],
        &add_body,
    );
    add_repo.assert_status(200);
    add_repo.assert_body_contains("\"api-test\"");

    let tree = server.request_with_headers(
        "GET",
        "/api/repos/api-test/tree",
        &[(
            "Authorization",
            "Bearer 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
        )],
    );
    tree.assert_status(200);
    tree.assert_body_contains("README.md");
    tree.assert_body_contains("\"recent_commits\"");
    tree.assert_body_contains("\"contributors\"");
    tree.assert_body_contains("\"languages\"");

    // Repository list via state.
    let state2 = server.request_with_headers(
        "GET",
        "/api/state",
        &[(
            "Authorization",
            "Bearer 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
        )],
    );
    state2.assert_body_contains("api-test");

    server.shutdown_with_sigterm();
    Ok(())
}

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 git(directory: &Path, arguments: &[&str]) -> Result<()> {
    let status = Command::new("git")
        .current_dir(directory)
        .args(arguments)
        .status()?;
    ensure!(status.success(), "git {} failed", arguments.join(" "));
    Ok(())
}

fn path(path: &Path) -> Result<&str> {
    path.to_str().context("test path is not UTF-8")
}