use assert_cmd::Command;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn bin() -> Command {
Command::cargo_bin("node-app").expect("build binary")
}
fn templates_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../infra/repo-templates")
.canonicalize()
.expect("infra/repo-templates exists")
}
#[test]
fn new_bun_then_validate_passes() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("my-test-app");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"my-test-app",
"--type",
"bun",
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
assert!(dest.join("manifest.json").exists(), "manifest.json missing");
assert!(dest.join("package.json").exists(), "package.json missing");
assert!(dest.join("src/index.ts").exists(), "src/index.ts missing");
assert!(dest.join("ui/index.html").exists(), "ui/index.html missing");
assert!(
dest.join("debian/control.template").exists(),
"debian/control.template missing"
);
assert!(dest.join("debian/postinst").exists(), "debian/postinst missing");
assert!(dest.join("debian/prerm").exists(), "debian/prerm missing");
let manifest = fs::read_to_string(dest.join("manifest.json")).unwrap();
assert!(
manifest.contains("\"name\": \"my-test-app\""),
"name placeholder not substituted: {}",
manifest
);
assert!(
manifest.contains("\"manifest_version\": 2"),
"v2 marker missing"
);
assert!(manifest.contains("\"abi\": \"v1\""), "abi missing");
assert!(
manifest.contains("A Node mini app"),
"description placeholder not substituted: {}",
manifest
);
bin()
.args(["validate", "--path", dest.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn new_bun_fullstack_has_ui_dir() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("my-fullstack-app");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"my-fullstack-app",
"--type",
"bun-fullstack",
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
assert!(dest.join("manifest.json").exists());
assert!(dest.join("ui/src/App.tsx").exists(), "ui/src/App.tsx missing");
assert!(dest.join("ui/package.json").exists(), "ui/package.json missing");
let manifest = fs::read_to_string(dest.join("manifest.json")).unwrap();
assert!(
manifest.contains("\"has_ui\": true"),
"has_ui should be true in fullstack template: {}",
manifest
);
}
#[test]
fn new_cdylib_template_renders_but_validator_rejects() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("my-native-app");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"my-native-app",
"--type",
"cdylib",
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
assert!(dest.join("Cargo.toml").exists());
assert!(dest.join("src/lib.rs").exists());
assert!(dest.join("manifest.json").exists());
bin()
.args(["validate", "--path", dest.to_str().unwrap()])
.assert()
.failure()
.stderr(predicates::str::contains("native"));
}
#[test]
fn new_standalone_rust_has_systemd_files() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("my-standalone-svc");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"my-standalone-svc",
"--type",
"standalone-rust",
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
assert!(dest.join("Cargo.toml").exists(), "Cargo.toml missing");
assert!(dest.join("src/main.rs").exists(), "src/main.rs missing");
assert!(dest.join("src/ipc.rs").exists(), "src/ipc.rs missing");
assert!(dest.join("manifest.json").exists(), "manifest.json missing");
assert!(
dest.join("systemd/node-app-my-standalone-svc.service").exists(),
"systemd unit file missing"
);
let manifest = fs::read_to_string(dest.join("manifest.json")).unwrap();
assert!(
manifest.contains("\"app_type\": \"standalone\""),
"app_type should be standalone: {}",
manifest
);
bin()
.args(["validate", "--path", dest.to_str().unwrap()])
.assert()
.success();
}
#[test]
fn new_standalone_bun_has_ipc_client() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("my-standalone-bun");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"my-standalone-bun",
"--type",
"standalone-bun",
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
assert!(dest.join("src/index.ts").exists(), "src/index.ts missing");
assert!(dest.join("src/ipc.ts").exists(), "src/ipc.ts missing");
assert!(dest.join("manifest.json").exists(), "manifest.json missing");
assert!(
dest.join("systemd/node-app-my-standalone-bun.service").exists(),
"systemd unit file missing"
);
}
#[test]
fn name_placeholder_substituted_in_all_templates() {
for kind in &[
"bun",
"bun-fullstack",
"cdylib",
"cdylib-fullstack",
"standalone-rust",
"standalone-bun",
] {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("test-subst");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"test-subst",
"--type",
kind,
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
let manifest = fs::read_to_string(dest.join("manifest.json")).unwrap();
assert!(
manifest.contains("\"name\": \"test-subst\""),
"name not substituted in {} template: {}",
kind,
manifest
);
assert!(
!manifest.contains("{{name}}"),
"unreplaced {{name}} placeholder in {} template: {}",
kind,
manifest
);
}
}
#[test]
fn rejects_invalid_names() {
let tmp = TempDir::new().unwrap();
let bad = tmp.path().join("Bad-Name");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args(["new", "Bad-Name", "--type", "bun", "--out", bad.to_str().unwrap()])
.assert()
.failure();
let prefixed = tmp.path().join("node-app-foo");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"node-app-foo",
"--type",
"bun",
"--out",
prefixed.to_str().unwrap(),
])
.assert()
.failure();
}
#[test]
fn new_with_git_initializes_repo() {
if std::process::Command::new("git")
.arg("--version")
.output()
.is_err()
{
eprintln!("skipping: git not on PATH");
return;
}
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("git-test-app");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"git-test-app",
"--type",
"bun",
"--out",
dest.to_str().unwrap(),
"--git",
"--no-deps-update",
])
.assert()
.success();
assert!(dest.join(".git").is_dir(), ".git/ should exist after --git");
let log = std::process::Command::new("git")
.current_dir(&dest)
.args(["log", "--oneline"])
.output()
.expect("git log");
assert!(
log.status.success(),
"git log failed: {}",
String::from_utf8_lossy(&log.stderr)
);
let out = String::from_utf8_lossy(&log.stdout);
assert_eq!(
out.lines().count(),
1,
"expected exactly one initial commit, got: {}",
out
);
assert!(
out.contains("git-test-app"),
"commit msg should mention app name: {}",
out
);
let branch = std::process::Command::new("git")
.current_dir(&dest)
.args(["branch", "--show-current"])
.output()
.expect("git branch");
let branch_name = String::from_utf8_lossy(&branch.stdout).trim().to_string();
assert_eq!(branch_name, "main", "expected branch=main, got: {}", branch_name);
}
#[test]
fn dev_help_renders() {
bin().args(["dev", "--help"]).assert().success();
}
#[test]
fn dev_daemon_and_monorepo_are_mutually_exclusive() {
bin()
.args(["dev", "--daemon", "docker", "--monorepo", "/tmp"])
.assert()
.failure()
.stderr(predicates::str::contains("cannot be used with"));
}
#[test]
fn dev_unknown_daemon_value_errors_clearly() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("dev-bogus-daemon");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"dev-bogus-daemon",
"--type",
"bun",
"--out",
dest.to_str().unwrap(),
"--no-deps-update",
])
.assert()
.success();
bin()
.args(["dev", "-p", dest.to_str().unwrap(), "--daemon", "wat", "--once"])
.assert()
.failure()
.stderr(predicates::str::contains("unknown --daemon value"));
}
#[test]
fn rejects_malformed_github_slug() {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("malformed-test");
bin()
.env("NODE_APP_TEMPLATES_REPO", templates_dir())
.args([
"new",
"malformed-test",
"--type",
"bun",
"--out",
dest.to_str().unwrap(),
"--github",
"no-slash",
"--no-deps-update",
])
.assert()
.failure()
.stderr(predicates::str::contains("org/repo"));
}