node-app-build 7.1.7

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! `node-app build` / `package` / `test` for `app_type: "burger"`, driven
//! end to end through the real CLI with fake `bun` and `node-app-burger`
//! executables on PATH (neither real tool is needed to run these tests).

use assert_cmd::Command;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

const FAKE_BUN: &str = r#"#!/bin/sh
printf '%s\n' "$*" >> "$FAKE_LOG"
if [ "$1" = "install" ]; then exit 0; fi
case "$2" in
  build)
    mkdir -p dist && printf 'export {};\n' > dist/index.js ;;
  test-prepare)
    out=""
    while [ $# -gt 0 ]; do
      if [ "$1" = "--out" ]; then out="$2"; fi
      shift
    done
    root="$(pwd -P)"
    mkdir -p "$out/tests"
    printf 'export {};\n' > "$out/tests/a.test.js"
    printf 'export {};\n' > "$out/tests/b.test.js"
    printf '{"imports":{}}\n' > "$out/importmap.json"
    printf '{"importMap":"%s/%s/importmap.json","tests":[{"source":"%s/tests/a.test.ts","output":"%s/%s/tests/a.test.js"},{"source":"%s/tests/b.test.ts","output":"%s/%s/tests/b.test.js"}]}\n' \
      "$root" "$out" "$root" "$root" "$out" "$root" "$root" "$out" > "$out/test-plan.json" ;;
  *) exit 64 ;;
esac
"#;

fn write_executable(path: &Path, contents: &str) {
    fs::create_dir_all(path.parent().unwrap()).unwrap();
    fs::write(path, contents).unwrap();
    fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}

struct Fixture {
    _tmp: tempfile::TempDir,
    project: PathBuf,
    bin: PathBuf,
    log: PathBuf,
}

fn burger_project() -> Fixture {
    let tmp = tempfile::tempdir().unwrap();
    let root = tmp.path().canonicalize().unwrap();
    let project = root.join("hello-burger");
    let files = [
        (
            "manifest.json",
            r#"{"manifest_version":2,"abi":"v1","name":"hello-burger","version":"0.1.0","app_type":"burger","entrypoint":"dist/index.js","description":"Hello","maintainer":"T <t@example.com>","nodeApp":{"blueprint":">=1","minNodeVersion":"7.1.0"}}"#,
        ),
        (
            "package.json",
            r#"{"name":"hello-burger","version":"0.1.0"}"#,
        ),
        ("src/index.ts", "export {};\n"),
        ("tests/a.test.ts", "export {};\n"),
        ("tests/b.test.ts", "export {};\n"),
        (
            "node_modules/@econ-v1/app-sdk/dist/burger-build/cli.js",
            "// fake\n",
        ),
    ];
    for (path, contents) in files {
        let full = project.join(path);
        fs::create_dir_all(full.parent().unwrap()).unwrap();
        fs::write(full, contents).unwrap();
    }
    let bin = root.join("fake-bin");
    write_executable(&bin.join("bun"), FAKE_BUN);
    let log = root.join("bun.log");
    Fixture {
        _tmp: tmp,
        project,
        bin,
        log,
    }
}

fn node_app(fixture: &Fixture) -> Command {
    let mut cmd = Command::cargo_bin("node-app").unwrap();
    cmd.env("PATH", format!("{}:/usr/bin:/bin", fixture.bin.display()))
        .env("FAKE_LOG", &fixture.log)
        .env_remove("NODE_BURGER_BIN")
        .env_remove("NODE_APP_BURGER_DIR")
        .env_remove("CARGO_TARGET_DIR");
    cmd
}

#[test]
fn burger_app_builds_with_burger_build_and_packages_against_node_app_burger() {
    let fixture = burger_project();

    node_app(&fixture)
        .args(["build", "--path"])
        .arg(&fixture.project)
        .assert()
        .success();
    let log = fs::read_to_string(&fixture.log).unwrap();
    assert!(log.contains("install --silent"), "{log}");
    assert!(
        log.contains(
            "node_modules/@econ-v1/app-sdk/dist/burger-build/cli.js build src/index.ts --outdir dist"
        ),
        "{log}"
    );

    let out = fixture.project.join("out");
    node_app(&fixture)
        .env("NODE_APP_PACKAGE_STAGE_ONLY", "1")
        .args(["package", "--path"])
        .arg(&fixture.project)
        .arg("--out")
        .arg(&out)
        .assert()
        .success();
    let control = fs::read_to_string(out.join("staging/DEBIAN/control")).unwrap();
    assert!(control.contains("Architecture: all"), "{control}");
    assert!(
        control.contains("Depends: node-app-burger (>= 0.1.0), node (>= 7.1.0)"),
        "{control}"
    );
    let app_root = out.join("staging/usr/lib/node/apps/hello-burger");
    assert!(app_root.join("dist/index.js").is_file());
    assert!(!app_root.join("private_modules").exists());
}

#[test]
fn burger_package_rejects_private_runtime_modules() {
    let fixture = burger_project();
    fs::write(
        fixture.project.join("manifest.json"),
        r#"{"manifest_version":2,"abi":"v1","name":"hello-burger","version":"0.1.0","app_type":"burger","entrypoint":"dist/index.js","nodeApp":{"minNodeVersion":"7.1.0","privateRuntime":["sharp"]}}"#,
    )
    .unwrap();
    fs::create_dir_all(fixture.project.join("dist")).unwrap();
    fs::write(fixture.project.join("dist/index.js"), "export {};\n").unwrap();

    let output = node_app(&fixture)
        .env("NODE_APP_PACKAGE_STAGE_ONLY", "1")
        .args(["package", "--path"])
        .arg(&fixture.project)
        .arg("--out")
        .arg(fixture.project.join("out"))
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("burger apps cannot ship manifest.nodeApp.privateRuntime (sharp)"),
        "{stderr}"
    );
}

const FAKE_BURGER: &str = r#"#!/bin/sh
printf '%s\n' "$*" > "$FAKE_BURGER_ARGS"
exit "${FAKE_BURGER_EXIT:-0}"
"#;

#[test]
fn burger_test_prepares_then_runs_the_selected_files_in_node_app_burger() {
    let fixture = burger_project();
    let burger = fixture.bin.join("node-app-burger");
    write_executable(&burger, FAKE_BURGER);
    let args_file = fixture.project.join("burger-args.txt");
    // Run from outside the project: a relative --junit is the caller's path.
    let caller = fixture.project.parent().unwrap().join("ci");
    fs::create_dir_all(&caller).unwrap();

    node_app(&fixture)
        .current_dir(&caller)
        .env("NODE_BURGER_BIN", &burger)
        .env("FAKE_BURGER_ARGS", &args_file)
        .args(["test", "--path"])
        .arg(&fixture.project)
        .args(["--junit", "reports/junit.xml", "b.test"])
        .assert()
        .success();

    assert!(
        caller.join("reports").is_dir(),
        "the --junit directory is created for the runner"
    );
    let log = fs::read_to_string(&fixture.log).unwrap();
    assert!(
        log.contains("test-prepare --src src --tests tests --out target/burger-test"),
        "{log}"
    );
    let out = fixture.project.join("target/burger-test");
    assert_eq!(
        fs::read_to_string(&args_file).unwrap().trim_end(),
        format!(
            "test --import-map {} --junit {} {}",
            out.join("importmap.json").display(),
            caller.join("reports/junit.xml").display(),
            out.join("tests/b.test.js").display()
        )
    );
}

#[test]
fn burger_test_fails_when_node_app_burger_fails() {
    let fixture = burger_project();
    let burger = fixture.bin.join("node-app-burger");
    write_executable(&burger, FAKE_BURGER);

    node_app(&fixture)
        .env("NODE_BURGER_BIN", &burger)
        .env("FAKE_BURGER_ARGS", fixture.project.join("burger-args.txt"))
        .env("FAKE_BURGER_EXIT", "1")
        .args(["test", "--path"])
        .arg(&fixture.project)
        .assert()
        .failure();
}

#[test]
fn burger_test_without_a_runtime_binary_says_where_it_looked() {
    let fixture = burger_project();
    let output = node_app(&fixture)
        .args(["test", "--path"])
        .arg(&fixture.project)
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("node-app-burger not found"), "{stderr}");
    assert!(
        stderr.contains(
            &fixture
                .project
                .join("modules/node-app-burger")
                .display()
                .to_string()
        ),
        "{stderr}"
    );
}